Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<Version>10.0.4</Version>
<Version>10.0.5</Version>
<OutputType>Exe</OutputType>
<TargetFrameworks>net10.0;net11.0</TargetFrameworks>
<TargetFrameworks>net10.0</TargetFrameworks>
<SatelliteResourceLanguages>false</SatelliteResourceLanguages>
</PropertyGroup>

Expand All @@ -17,7 +17,7 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="System.CommandLine" Version="2.0.1" />
<PackageReference Include="System.CommandLine" Version="2.*" />
</ItemGroup>

<ItemGroup>
Expand Down
4 changes: 2 additions & 2 deletions tools/BootstrapBlazor.LLMsDocs.Cli/CommandFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -129,9 +129,9 @@ private static Command BuildGetCommand()
Console.Out.Write(await client.GetComponentAsync(name, ct));
return 0;
}
catch (FileNotFoundException)
catch (FileNotFoundException ex)
{
Console.Error.WriteLine($"error: component '{name}' not found. Try: bb-llms search <keyword>");
Console.Error.WriteLine($"error: {ex.Message} Try: bb-llms search <keyword>");

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium · Lookup errors expose source details

When a requested document is missing and the configured source path or URL contains sensitive information, BuildGetCommand prints the exception message verbatim; FetchAsync includes the full local path or request URL in missing-document exceptions, so stderr and captured agent or CI logs expose private paths or secret-bearing source URLs.

Print a generic missing-document message in the normal error path and expose detailed diagnostics only through an explicitly enabled, redacted debug mode.

Prompt for AI agents
In `tools/BootstrapBlazor.LLMsDocs.Cli/CommandFactory.cs` at line 134:

**Lookup errors expose source details**

When a requested document is missing and the configured source path or URL contains sensitive information, `BuildGetCommand` prints the exception message verbatim; `FetchAsync` includes the full local path or request URL in missing-document exceptions, so stderr and captured agent or CI logs expose private paths or secret-bearing source URLs.

Print a generic missing-document message in the normal error path and expose detailed diagnostics only through an explicitly enabled, redacted debug mode.

return 1;
}
catch (Exception ex)
Expand Down
7 changes: 6 additions & 1 deletion tools/BootstrapBlazor.LLMsDocs.Cli/Models.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,9 @@ namespace BootstrapBlazor.LLMsDocs.Cli;
/// A single component entry parsed from the llms.txt index, used by the
/// <c>list</c> and <c>search</c> commands for structured (JSON) output.
/// </summary>
public sealed record ComponentEntry(string Name, string? Description, string Category);
public sealed record ComponentEntry(string Name, string? Description, string Category)
{
public bool IsExtension { get; init; }

internal string DocumentPath { get; init; } = "";
}
23 changes: 19 additions & 4 deletions tools/BootstrapBlazor.LLMsDocs.Cli/README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# BootstrapBlazor LLMs Docs CLI(`bb-llms`)

一个轻量命令行工具,用于从 [www.blazor.zone/llms](https://www.blazor.zone/llms)
拉取 BootstrapBlazor 组件的**官方 API 文档**(参数、事件回调、公开方法、源码链接),
拉取 BootstrapBlazor 及 BootstrapBlazor.Extensions 组件的**官方 API 文档**(参数、事件回调、公开方法、源码链接),
供 AI agent(Claude Code / Cursor / Trae / Codex 等经 Bash 调用)和开发者查询。

组件文档由官网的 `BootstrapBlazor.LLMsDocsGenerator`(生产者,CI 侧用 Roslyn 生成并发布)产出,
Expand Down Expand Up @@ -49,6 +49,7 @@ dotnet tool uninstall -g BootstrapBlazor.LLMsDocs.Cli
```bash
bb-llms search table # 按关键词搜索组件,定位准确名称
bb-llms get Table # 拉取 Table 组件的完整 API 文档(核心命令)
bb-llms get SelectCity # 拉取 Region 扩展包中 SelectCity 的 API 文档
bb-llms list # 列出全部组件
bb-llms index # 打印原始 llms.txt 索引
```
Expand All @@ -68,10 +69,12 @@ bb-llms index # 打印原始 llms.txt 索引

要点:

- `get` / `index` 为**原样透传**,不解析格式,输出即远程 `.txt` 原文。
- `list` / `search` 默认输出 **JSON**;加 `--format text` 输出纯文本(`Name - 描述` 每行一条)。
- `get` / `index` 的输出保留远程 `.txt` 原文;`get` 会先解析 `llms.txt`,按名称找到文档路径,再直接读取核心或扩展文档,不通过请求失败来尝试其他目录。本地源也必须包含该索引。
- `list` / `search` 包括组件和服务,默认输出 **JSON**;`isExtension` 表示文档位于 `extensions/`,不表示接口源码一定声明在扩展仓库。加 `--format text` 输出纯文本(`Name - 描述` 每行一条)。
- `search --limit` 默认 `20`,取值范围会被限制在 `1~100`。
- `search` 按文档路径去重后应用结果数量限制,避免多个包共用的服务占满结果;`list` 保留各包的索引项。
- 找不到组件时 `get` 退出码非 0,并提示用 `bb-llms search <关键词>` 重新定位。
- 索引缺失、索引中没有对应名称、目标文档缺失均返回非零退出码,并保留具体失败原因;不会把缺少 `llms.txt` 误报为组件不存在。

示例:

Expand All @@ -80,8 +83,12 @@ bb-llms search button --limit 5 # 最多 5 条,Button 类优先
bb-llms list --query table # 仅名称/描述含 "table" 的组件
bb-llms list --format text # 纯文本清单
bb-llms get Table # Table 的参数表 / 事件 / 方法 / GitHub 源码链接
bb-llms get SelectCity # BootstrapBlazor.Region 扩展包的 SelectCity 文档
bb-llms get IRecognizerProvider # 共享服务接口,按包列出 Azure/Baidu 注册方式
```

多个扩展包共用一个接口时,各包的索引项指向同一份服务文档;按实际使用的包选择其中一种注册方式,不要全部复制执行。

### 源与缓存选项(数据命令通用)

```
Expand Down Expand Up @@ -120,6 +127,8 @@ bb-llms get Button --base-url ./wwwroot/llms

> 含义:官网发版后,最长 24 小时本地才会感知到文档更新(用 `--refresh` 可立即刷新)。

索引同样缓存。新增组件或扩展服务已发布但提示索引中不存在时,使用 `bb-llms get SelectCity --refresh` 同时刷新索引及目标文档。

---

## 给 AI agent 使用(发现层)
Expand Down Expand Up @@ -181,12 +190,18 @@ bb-llms install --client claude # 2. 装发现层(写入

## 开发 / 本地运行

在 Extentions 仓库根目录执行 CLI 回归检查:

```powershell
.\tools\BootstrapBlazor.LLMsDocs.Cli\tests\Verify-Client.ps1

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚪ Low · Documented verification script is missing

When a maintainer follows the documented local regression-check command, the README directs maintainers to run tests/Verify-Client.ps1, but the CLI directory contains no such PowerShell script, so the documented regression check fails before exercising the client.

Add the referenced verification script or change the README to point to an existing test command.

Prompt for AI agents
In `tools/BootstrapBlazor.LLMsDocs.Cli/README.md` at line 196:

**Documented verification script is missing**

When a maintainer follows the documented local regression-check command, the README directs maintainers to run `tests/Verify-Client.ps1`, but the CLI directory contains no such PowerShell script, so the documented regression check fails before exercising the client.

Add the referenced verification script or change the README to point to an existing test command.

```

```bash
cd tools/BootstrapBlazor.LLMsDocs.Cli
dotnet build
dotnet run -- get Button
dotnet run -- search table --limit 5
dotnet run -- get Button --base-url ../BootstrapBlazor.LLMsDocsGenerator/<生成目录>/llms
dotnet run -- get Button --base-url "<生成目录>/wwwroot/llms"
```

打包冒烟:
Expand Down
22 changes: 17 additions & 5 deletions tools/BootstrapBlazor.LLMsDocs.Cli/Services/LlmsDocsClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -63,14 +63,21 @@ public LlmsDocsClient(string? baseOverride, bool refresh = false, bool noCache =

public Task<string> GetIndexAsync(CancellationToken ct = default) => FetchAsync("llms.txt", ct);

public Task<string> GetComponentAsync(string name, CancellationToken ct = default)
public async Task<string> GetComponentAsync(string name, CancellationToken ct = default)
{
if (string.IsNullOrWhiteSpace(name))
{
throw new ArgumentException("Component name is required.", nameof(name));
}

return FetchAsync($"components/{name}.txt", ct);
var entry = ParseIndex(await GetIndexAsync(ct))
.FirstOrDefault(e => string.Equals(e.Name, name, StringComparison.OrdinalIgnoreCase));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium · Extension documents become inaccessible

When the index contains core and extension entries with the same name, GetComponentAsync uses FirstOrDefault to select the first matching name, and core entries precede extension entries, so get always fetches the core document and provides no way to retrieve the extension document.

Provide a way to disambiguate matching names so callers can select the extension document.

Prompt for AI agents
In `tools/BootstrapBlazor.LLMsDocs.Cli/Services/LlmsDocsClient.cs` at line 74:

**Extension documents become inaccessible**

When the index contains core and extension entries with the same name, `GetComponentAsync` uses `FirstOrDefault` to select the first matching name, and core entries precede extension entries, so `get` always fetches the core document and provides no way to retrieve the extension document.

Provide a way to disambiguate matching names so callers can select the extension document.

if (entry is null)
{
throw new FileNotFoundException($"Component not found in index: {name}");
}

return await FetchAsync(entry.DocumentPath, ct);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium · Concurrent reads return partial documents

When one CLI invocation refreshes a remote document while another invocation reads the same cached path, FetchAsync writes refreshed cache content directly to the shared file before updating its metadata; another CLI invocation can read that file while it is truncated or partially written and return incomplete documentation.

Write cache content to a temporary file and atomically replace the cache file, coordinating metadata updates so readers cannot observe an in-progress refresh.

Prompt for AI agents
In `tools/BootstrapBlazor.LLMsDocs.Cli/Services/LlmsDocsClient.cs` at line 80:

**Concurrent reads return partial documents**

When one CLI invocation refreshes a remote document while another invocation reads the same cached path, `FetchAsync` writes refreshed cache content directly to the shared file before updating its metadata; another CLI invocation can read that file while it is truncated or partially written and return incomplete documentation.

Write cache content to a temporary file and atomically replace the cache file, coordinating metadata updates so readers cannot observe an in-progress refresh.

}

public async Task<IReadOnlyList<ComponentEntry>> ListAsync(string? query, CancellationToken ct = default)
Expand All @@ -96,6 +103,7 @@ public async Task<IReadOnlyList<ComponentEntry>> SearchAsync(string query, int l

var matches = await ListAsync(query, ct);
return matches
.DistinctBy(e => e.DocumentPath, StringComparer.Ordinal)
.OrderBy(e => e.Name.StartsWith(query, StringComparison.OrdinalIgnoreCase) ? 0 : 1)
.ThenBy(e => e.Name, StringComparer.OrdinalIgnoreCase)
.Take(Math.Clamp(limit, 1, 100))
Expand Down Expand Up @@ -157,8 +165,8 @@ private async Task<string> FetchAsync(string relativePath, CancellationToken ct)

// ---- index parsing ----------------------------------------------------

// Matches: "- [Button](components/Button.txt) - optional description"
[GeneratedRegex(@"^- \[(?<name>[^\]]+)\]\(components/[^)]+\.txt\)(?:\s*-\s*(?<desc>.*))?$")]
// Matches component and extension links in llms.txt.
[GeneratedRegex(@"^- \[(?<name>[^\]]+)\]\((?<path>(?:components|extensions)/[^)]+\.txt)\)(?:\s*-\s*(?<desc>.*))?$")]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 High · Untrusted paths escape documentation roots

When an untrusted index contains a traversal path and a caller requests that entry, ParseIndex accepts the path and GetComponentAsync passes it to FetchAsync without containment checks, so local reads escape _baseDir and remote caching writes outside _cacheRoot, exposing or overwriting local files.

Reject traversal paths and verify resolved read and cache paths stay inside their configured roots before accessing them.

Also at tools/BootstrapBlazor.LLMsDocs.Cli/Services/LlmsDocsClient.cs:203.

Prompt for AI agents
In `tools/BootstrapBlazor.LLMsDocs.Cli/Services/LlmsDocsClient.cs` at line 169:

**Untrusted paths escape documentation roots**

When an untrusted index contains a traversal path and a caller requests that entry, `ParseIndex` accepts the path and `GetComponentAsync` passes it to `FetchAsync` without containment checks, so local reads escape `_baseDir` and remote caching writes outside `_cacheRoot`, exposing or overwriting local files.

Reject traversal paths and verify resolved read and cache paths stay inside their configured roots before accessing them.

Also at `tools/BootstrapBlazor.LLMsDocs.Cli/Services/LlmsDocsClient.cs:203`.

private static partial Regex IndexLineRegex();

// Matches a category heading: "### Buttons"
Expand Down Expand Up @@ -189,7 +197,11 @@ private static List<ComponentEntry> ParseIndex(string indexText)

var name = lineMatch.Groups["name"].Value.Trim();
var desc = lineMatch.Groups["desc"].Success ? lineMatch.Groups["desc"].Value.Trim() : null;
entries.Add(new ComponentEntry(name, string.IsNullOrEmpty(desc) ? null : desc, category));
entries.Add(new ComponentEntry(name, string.IsNullOrEmpty(desc) ? null : desc, category)
{
IsExtension = lineMatch.Groups["path"].Value.StartsWith("extensions/", StringComparison.Ordinal),
DocumentPath = lineMatch.Groups["path"].Value
});
}

return entries;
Expand Down
Loading