bfi-cli 0.1.99 → 0.1.101

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
- # BFI CLI:技能包与可选插件
1
+ # BFI CLI:技能组合与完整插件
2
2
 
3
- 技能安装沿用 skills.sh 衍生安装器。技能包默认仍是技能的组合;插件导入、编辑、发布在同一个包的可选插件能力中管理,两者的内容和版本独立。
3
+ 技能包在创建时确定为技能组合或插件。技能组合安装已有技能及全部关联 MCP;完整插件使用单独的原生宿主安装链路,不转换成技能组合。
4
4
 
5
5
  ## 默认安装技能
6
6
 
@@ -8,33 +8,145 @@
8
8
  bfi package install @team/kit --agent claude-code
9
9
  ```
10
10
 
11
- 默认不会安装 Hook、MCP 或原生插件。包内来源保留完整 `@namespace/skill`,不同空间的同名技能使用来源 hash 派生的独立安装目录,原始 SKILL.md 不改写;不再按裸 slug 合并。
11
+ 技能组合默认包含全部关联 MCP,但不会安装原生插件或 Hook。包内来源保留完整 `@namespace/skill`,不同来源的同名技能使用稳定身份派生的独立安装目录,原始 SKILL.md 不改写;不再按裸 slug 合并。
12
12
 
13
13
  旧安装记录不会静默迁移或批量删除;无法证明归属或没有确认卸载成功时保留记录并提示。独立 `bfi add` 的命名不因此改变。
14
14
 
15
15
  ## 明确选择插件
16
16
 
17
17
  ```sh
18
+ bfi skills package install @team/kit --as plugin --version 1.0.0
18
19
  bfi package install @team/kit --as plugin --agent claude-code --version 1.0.0 --plan
19
20
  bfi package install @team/kit --as plugin --agent claude-code --version 1.0.0 --export ./kit-1.0.0.zip
21
+ bfi package install @team/kit --as plugin --agent claude-code --scope user --version 1.0.0 --prepare-native ./kit-1.0.0-native
20
22
  bfi package install ./kit.tgz --as plugin --agent codex --root bundle --version 1.0.0 --plan
21
23
  bfi package list --as plugin --scope project
22
24
  ```
23
25
 
24
- - 当前 Claude Code、Codex、ZCode、CodeBuddy、Qwen Code 均为 **EXPORT_ONLY**。尚未取得完整运行时/安装归属证据,不执行原生安装或静默退回只装技能。
26
+ - Claude Code **2.1.251** 的已验证组件开放受控原生注册;仍须满足精确发布版本的服务端授权、宿主/adapter 版本和 LOAD 证据。没有这些证据时不会安装。Codex **0.136.0** 仅开放原生技能型插件的人工交接;ZCode、CodeBuddy、Qwen Code 未实测,只导出并明确提示待适配。详见 [插件验证矩阵](PLUGIN-VALIDATION.md)。
27
+ - 通用安装命令不必指定 Agent 和范围,CLI 会在交互终端中询问;非交互调用必须显式选择,`--yes` 不会代选。不支持的宿主或组件仍明确停止,不降级成技能安装。
25
28
  - `--plan` 只给计划/阻断原因;`--export` 原样导出归档,不计作安装。目标文件必须不存在,扩展名与源格式一致。
29
+ - `--prepare-native <新目录>` 生成完整原始插件目录及独立市场索引,输出真实宿主命令;不注册宿主、不执行 Hooks、不创建 bfi 安装归属。只在宿主版本、作用域及全部必要组件受支持时生成。审阅后由用户确认执行;安装后保留该目录。人工交接的更新/卸载由原生宿主管理。
30
+ - `--plan` 返回 READY 后,去掉 `--plan` 并确认可原生注册;非交互需显式 `--yes`。结果 REGISTERED 仅表示注册和文件归属已核对,需重启宿主检查所有组件,不能视为运行功能验证通过。
26
31
  - 市场来源必须写完整名称 `@namespace/package`,不要求用户输入数据库 ID。未指定版本时只解析一次可见 stable;没有 stable 则要求明确版本,不猜 latest。
27
32
  - 本地支持 zip/tgz/tar.gz,多根必须用 `--root` 选择;保留全部原始文件、执行位和未知字段,不自动转换各宿主格式。缺原生版本时可用 `--version` 描述本地导出身份,但不会改源文件或绕过市场正式发布的版本校验。
28
- - `--scope project` 为默认;`--scope user`(或 `-g`)显式选择用户范围。插件独立使用 `.bfi/plugin-lock.json`,不与旧技能锁混用。
33
+ - 原生安装/卸载/交接要求明确范围:`--scope user`(或 `-g`)/ `--scope project`;交互终端省略时询问,非交互或 `--yes` 不能代选。只读计划/导出/列表省略时按项目范围查看。插件独立使用 `.bfi/plugin-lock.json`,不与旧技能锁混用。
29
34
  - 未知安装结果保留 PREPARED 并阻止盲目重试;更新/卸载必须证明精确来源、宿主、范围、版本及安装归属。
35
+ - 同一已安装内容重复安装不重复写入;原生版本更新暂不自动替换,提示 NATIVE_UPDATE_REQUIRES_USER 并保留旧版。卸载保留数据、市场索引及原始目录;重装使用新的独立市场,不覆盖保留目录。明确的同名预检冲突记录 FAILED,可解决后重试;原生命令结果不明才保留 PREPARED。
36
+ - 所有路径都先执行归档安全检查。疑似凭据、非法路径、缺失关键组件或不支持的格式会停止,不能为导出或安装绕过,也不会自动降级成技能安装。
30
37
 
31
38
  服务器插件功能默认开启;显式设置 `PACKAGE_PLUGIN_ENABLED=false` 时仍可使用既有技能包,但插件市场操作不可用。本地原始包计划/导出不等于服务器发布。
32
39
 
40
+ ## 独立 MCP 安装与测试
41
+
42
+ MCP 使用独立市场,不加入技能目录,也不修改插件快照。安装默认用户级,只登记选中服务器;不会自动升级 CLI、安装第三方包或运行其代码。
43
+
44
+ ```sh
45
+ bfi mcp search
46
+ bfi mcp info bfi-pm
47
+ bfi mcp agents
48
+ bfi mcp install bfi-db --agent codex --dry-run
49
+ bfi mcp install bfi-db bfi-code-search weknora-bfi --agent codex --yes
50
+ bfi mcp install bfi-pm --agent codex --input-env pm_ak=PM_AK --input-env pm_sk=PM_SK --yes
51
+ bfi mcp list --agent codex
52
+ bfi mcp test bfi-db bfi-code-search weknora-bfi --agent codex --yes
53
+ bfi mcp remove bfi-db --agent codex --dry-run
54
+ ```
55
+
56
+ `--file servers.json` 接受标准 `{"mcpServers":{...}}` JSON/JSONC,可离线导入;不带名称时安装文件中的全部条目,带名称时只安装选中项。`--scope project` 写当前项目;`--config <path>` 明确指定目标文件,格式仍由 `--agent` 决定。市场地址沿用 BFI 的 registry 设置,可用 `--registry` 覆盖;目录查询匿名执行,不发送账号 Token。
57
+
58
+ ### 宿主格式
59
+
60
+ | Agent | 用户配置 / 项目配置 | 根键与 HTTP 形态 | 变量引用 |
61
+ |---|---|---|---|
62
+ | `claude-code`(`claude`) | `~/.claude.json` / `.mcp.json` | `mcpServers`,`type: http` + `url` | `${VAR}` |
63
+ | `codex` | `$CODEX_HOME/config.toml`(默认 `~/.codex`) / `.codex/config.toml` | `mcp_servers`,`url`、`http_headers` | `env_http_headers`;stdio `env_vars` |
64
+ | `cursor` | `~/.cursor/mcp.json` / `.cursor/mcp.json` | `mcpServers`,`url`,HTTP/SSE 自动协商 | `${env:VAR}` |
65
+ | `qwen-code`(`qwen`) | `~/.qwen/settings.json` / `.qwen/settings.json` | `mcpServers`,HTTP `httpUrl` / SSE `url` | `${VAR}` |
66
+ | `gemini-cli`(`gemini`) | `~/.gemini/settings.json` / `.gemini/settings.json` | 同 Qwen | `${VAR}` |
67
+ | `zcode` | `~/.zcode/cli/config.json` / `.zcode/config.json` | **`mcp.servers`**,`type` + `url` | 通用插值未确证,默认拒绝变量配置 |
68
+ | `codebuddy` | `~/.codebuddy/.mcp.json` / `.mcp.json` | `mcpServers`,`type` + `url` | `${VAR}` |
69
+ | `kimi-code`(`kimi`) | `$KIMI_CODE_HOME/mcp.json`(默认 `~/.kimi-code`) / `.kimi-code/mcp.json` | `mcpServers`,`url`,SSE 用 `transport: sse` | 通用插值未确证,默认拒绝变量配置 |
70
+ | `opencode` | `$XDG_CONFIG_HOME/opencode/opencode.json(c)` / `opencode.json(c)` | `mcp`,`type: remote`;stdio 用 `local` + 命令数组 | `{env:VAR}` |
71
+
72
+ CodeBuddy 用户级按 `.codebuddy/.mcp.json` → `.codebuddy/mcp.json` → `.codebuddy.json` 选择已存在文件,不新建文件遮蔽旧配置。设置 `CLAUDE_CONFIG_DIR` 或检测到旧 Kimi 布局时,需用 `--config` 明确文件。WorkBuddy/IDE 专有配置未确证,不猜路径。支持的 CLI 格式不等于所有历史版本都兼容。
73
+
74
+ stdio 支持 Node.js、Python 以及任意已准备好的本地可执行程序,保留 `command` 和参数数组,不通过 shell 拼接。上传运行包仍需先在本机下载、检查并准备启动路径,再通过 `--file` 安装;不会执行市场服务器上的临时路径。
75
+
76
+ Codex 不支持独立旧 SSE、拼接变量或 env_vars 重命名。`cwd` 仅在已确证的 Codex、Qwen Code、Gemini CLI、Kimi Code 中写入,其它目标拒绝该字段,避免宿主忽略后从错误目录启动。不支持的字段/组合会在整批写入前报错,不能静默丢弃。
77
+
78
+ ### 保留配置与凭据
79
+
80
+ - 新增只改选中节点,其它服务器、宿主设置保留。相同配置重复安装不写文件、不生成备份。不同同名配置需显式 `--replace`,`--yes` 只确认,不表示允许覆盖。
81
+ - `--replace` 替换连接字段,保留 enabled、工具过滤等宿主扩展;有宿主专有认证时拒绝自动更换目标。TOML 采用独立表增量编辑并校验完整解析结果;复杂内联结构会拒绝改写。
82
+ - 写入前取得锁、校验文件身份/内容,原文件备份为同目录 `.bfi-mcp-*.bak`,临时文件原子替换。Unix 文件权限收紧至 `0600`,只读文件拒绝更新;Windows 需自行保证用户目录 ACL。锁不能完全排除其它编辑器在最后校验之后的竞争。
83
+ - 不跟随配置路径符号链接,不覆盖损坏文件。异常退出残留 `.bfi-mcp.lock` 时,先确认没有正在运行的安装,再手工移除锁。备份可能含旧密钥,不要分享或提交仓库。
84
+ - `${user_config.pm_ak}` 与 `${pm_ak}` 都可通过 `--input-env pm_ak=PM_AK` 绑定;默认仅保存原生引用,不要求安装时读取真实值。**Agent 进程必须能继承该环境变量**;GUI 启动不一定继承终端环境,重启 Agent 或在其原生秘密设置中配置。
85
+ - ZCode/Kimi 通用引用未确证时,可显式使用用户级 `--materialize-inputs` 将当前进程环境值写入私有配置;项目级禁止物化。配置内已有的字面密钥也需要此显式选择。不会修改 shell profile,不会把密钥加入 CLI 参数或输出。
86
+ - 三个 BFI stdio MCP 复用本机 `bfi auth login` 状态,不注入 `BFI_TOKEN`。安装不执行登录或改变现有账号;可用 `bfi auth whoami` 检查。
87
+
88
+ ### 连接验证的边界
89
+
90
+ `test` 从目标文件读回原生配置,默认每个服务 30 秒(`--timeout` 最大 300 秒),执行 initialize 和分页 tools/list,最多 100 页、5000 个工具。stdio 测试会运行本机程序并继承本机环境/登录态;HTTP/SSE 测试会发送配置认证头,须交互确认或 `--yes`。不调用业务工具、不自动登录 OAuth、不授予项目信任。原始服务错误/日志不回显,避免泄露凭据;无变量、超时、认证失败均有明确阶段和失败退出码。
91
+
92
+ 通用协议测试通过不代表目标 Agent 已经加载;项目配置仍需在对应 Agent 中审核。Codex、Claude Code 的原生读取另行验收,其它适配器以格式/合并测试为基础,不宣称已在全部客户端运行。
93
+
94
+ 格式依据:[Claude Code](https://code.claude.com/docs/en/mcp)、[Codex](https://developers.openai.com/codex/mcp)、[Cursor](https://cursor.com/docs/mcp)、[Qwen Code](https://qwenlm.github.io/qwen-code-docs/en/users/features/mcp/)、[Gemini CLI](https://geminicli.com/docs/tools/mcp-server/)、[ZCode](https://zcode.z.ai/cn/docs/mcp-services)、[CodeBuddy Code](https://www.codebuddy.ai/docs/cli/mcp)、[Kimi Code](https://www.kimi.com/code/docs/en/kimi-code-cli/customization/mcp.html)、[OpenCode](https://opencode.ai/docs/mcp-servers/)。
95
+
33
96
  ## 开发验证
34
97
 
98
+ ### 统一安装 P3(本地构建,尚未发布 npm)
99
+
100
+ `bfi capabilities --json` 只读输出此构建实际实现的能力,不联网、不读取登录态、不修改配置。本地构建登记 `skill-install-v1`、`content-plan-v1`、`collection-bundle-v1`、`collection-mcp-v1`、`scope-selection-v1`。不要仅凭 npm 版本号判断能力;本次没有发布 npm、部署前后端或执行生产迁移。
101
+
102
+ `src/lib/installation/contracts.ts` 与 `test-fixtures/installation/collection-v1.json` 固定后续 Java/TypeScript 契约:ID 均为字符串;直接安装根明确列举,嵌套 SKILL.md 只是支持文件;MCP 只允许凭据占位符。当前解析器检查 schema、根重叠、大小写冲突、路径和字面凭据,不下载或执行文件,不开放服务端接口。每文件上限 50 MiB、总计 500 MiB、文件数 20,000,未来服务端必须使用相同限制或更严格限制。
103
+
104
+ 技能组合安装走固定内容计划;单个技能 `bfi skills add @空间/名称` 仍走原有技能安装路径,只下载该技能,不生成内容计划、不涉及 MCP。其它 Git/本地技能来源仍走既有 skills.sh 路径;独立 MCP 和显式 `--as plugin` 不变。缺 Agent/范围时分别询问,不默认增加 universal Agent。非交互必须显式给出目标、范围和 `-y`;不支持 `--skills-only`,关联 MCP 不可取消。
105
+
106
+ ```sh
107
+ bfi skills add @global/bfi-configuring-ui
108
+ bfi skills package install @global/bip-mdd-dev
109
+ bfi skills package install @global/bip-mdd-dev --agent codex,claude-code --scope user -y
110
+ bfi skills package install @global/bip-mdd-dev --content-plan PLAN_ID --agent codex --global -y
111
+ bfi skills package install --from-archive ./collection.zip --agent codex --scope project -y
112
+ bfi skills package update @global/bip-mdd-dev
113
+ bfi skills update @global/bfi-configuring-ui
114
+ bfi skills package install --operation OPERATION_ID -y
115
+ ```
116
+
117
+ `PLAN_ID` / `OPERATION_ID` 为说明占位符,执行时填写真实 ID。更新继承原 Agent/范围;多套安装交互选择,非交互需明确筛选。跨 registry 不复用已有 Token,也不会自动改登录账号。公开资源可匿名解析,服务需部署 P2 的内容计划接口;旧服务不会降级为逐项技能安装。
118
+
119
+ 组合每次获取一个 ZIP,多 Agent 共享同次解包结果。显式根内的嵌套 SKILL.md 仅作为支持文件复制。新安装使用 **copy 模式**,不依赖临时目录符号链接;检查清单、文件哈希、执行权限、链接/越界/重复路径和大小上限。目标目录以 registry 和技能稳定 ID 的摘要命名,避免跨空间、跨源同名碰撞;原 SKILL.md 不改写。已验证的 ZIP 缓存在 `~/.bfi/cache/installation/`,缓存命中仍先核对在线计划与授权。离线安装只保证内容完整,不代表仍有在线访问权限。
120
+
121
+ 变量按服务器绑定,例如 `--input-env bfi-pm.pm_ak=PM_AK --input-env bfi-pm.pm_sk=PM_SK`。值通过安全的本机环境提供,不填在命令参数或普通聊天中;还需确认目标 Agent(特别是 GUI 进程)可读取这些变量后指定 `--environment-ready`。不支持变量引用的宿主会阻断,不擅自物化明文秘密。缺变量、命令或不支持的上传运行包,会在任何技能/宿主配置写入前失败。
122
+
123
+ 不同内容的同名 MCP 配置需要 `--replace` 明确授权;保留其它服务器及设置。同文件的兼容多目标只写一次,路径含符号链接时拒绝。BFI MCP 通过保存登录态所属 registry 的 `/api/v1/whoami` 验证账号,不启动可能重新解析项目 registry 的 `bfi auth whoami` 子进程;区分登录失效、权限不足和网络/服务错误,不自动登录或安装依赖,不额外保存 BFI_TOKEN。完成仅表示文件安装和 MCP 注册,不执行业务工具、不替宿主授予信任。
124
+
125
+ 更新按受管 MCP 条目核对本机修改,允许保留后来新增的无关 MCP;预检到写入之间仍校验整文件指纹。确认期间若另一操作改变安装归属,则停止并要求重新预检,不删除或覆盖刚被共享的技能。`--json` 成功时只输出一个 JSON 文档且不显示交互提示,因此需要明确 Agent/范围;执行还需 `-y`(`--dry-run` 除外)。错误写入 stderr 并返回非零。
126
+
127
+ 私有回执/冻结内容在 `~/.bfi/installations/`,部分写入失败返回非零及 `INCOMPLETE`。`--operation` 按指纹恢复原操作,不能混入新目标或最新内容;本机修改会阻断,不自动回滚覆盖。若进程被强杀遗留 `active.lock`,确认没有运行中的安装进程后再删除该锁。恢复含变量 MCP 时仍需 `--environment-ready` 和有效环境。不要把该目录或备份上传(备份可能含原有凭据)。
128
+
129
+ 卸载使用 `bfi skills package uninstall @空间/组合`,或 `bfi skills remove @空间/技能`。只删除独占且未修改的受管技能;共享、原先独立存在或手改内容保留,MCP 默认保留。旧批量安装/删除不会越过新回执归属;不把旧锁文件无法证明的信息推断成受管归属。详细实测见 `../docs/testing/unified-installation-p3.md`。
130
+
131
+ MCP 连接结构复用已有标准化校验;已知凭据字段、敏感输入声明和令牌型 argv 必须为占位引用。此校验不替代发布端的秘密清理和代码审查,也不能判断任意脚本/自定义字符串是否暗含秘密。文件清单另拒绝同一路径既为文件又为目录的冲突。
132
+
133
+ 新增验证:`pnpm exec vitest run src/lib/installation`。后端共享 fixture 测试为 `InstallationContractTest`,不启动 Spring、不连接数据库。
134
+
135
+ ### P4 网页安装入口与运行时检查
136
+
137
+ 技能组合与插件的安装抽屉先解析固定内容计划,再生成 `--content-plan` 实际命令。组合展示全部直接技能及关联 MCP;单个技能仍使用原有的 `bfi skills add` 命令和技能原始 ZIP 下载,不解析内容计划。目标 Agent 和范围可独立省略,CLI 交互确认。组合命令自动带上 `--input-env MCP.参数=环境变量名` 和 `--environment-ready`;执行前须在本机设置变量并确认目标进程可读,页面不接收真实秘密。安装、更新、npm 升级与登录命令均可复制。
138
+
139
+ 服务端 `INSTALLATION_VERIFIED_CLI_VERSION` 和 `INSTALLATION_VERIFIED_CLI_CAPABILITIES` 默认空,不再作为网页展示命令的前置条件。本机执行 `bfi capabilities --json` 检查实际能力;npm 版尚未发布这些能力时可用当前开发构建验证,不把命令展示等同于已经发布。`COLLECTION_MCP_WRITE_ENABLED` 默认开启,可显式关闭;`COLLECTION_INSTALL_ENABLED=false` 会让含 MCP 的组合失败关闭,保留数据,不退回仅安装技能。旧 v1 组合读取/下载要求声明 `collection-mcp-v1` 才允许处理含 MCP 的组合;能力声明不替代授权。
140
+
141
+ 插件页面提供固定版本的 Agent 指引、CLI 计划/完整导出/原生交接命令和 ZIP 下载,按已验证宿主能力显示限制。P5 不等于所有宿主支持,支持边界见 [插件验证矩阵](PLUGIN-VALIDATION.md)。P4 页面与隔离数据库/HTTP/构建 CLI 的验证说明见 `../docs/testing/unified-installation-p4.md`;本阶段仍未发布或部署。
142
+
35
143
  ```sh
36
144
  pnpm exec vitest run src/commands/package.test.ts src/commands/package-plugin.test.ts src/lib/plugin src/test-utils.test.ts
145
+ pnpm exec vitest run src/commands/mcp.test.ts src/lib/mcp
146
+ pnpm type-check
37
147
  pnpm build
38
148
  ```
39
149
 
40
150
  官方样本用例需显式 `SKILLHUB_PLUGIN_FIXTURES`;不要为测试使用生产数据源或真实 Agent 用户配置。CLI 子进程测试工具使用临时 HOME 和环境白名单。完整限制、继承的失败用例、数据库发布门禁及本次测试事故见 `../docs/testing/package-plugin-release-checklist.md`。
151
+
152
+ 独立 MCP 的格式矩阵与真实工具发现结果见 [MCP-VALIDATION.md](MCP-VALIDATION.md)。
Binary file
@@ -738,4 +738,4 @@ function getNonUniversalAgents() {
738
738
  function isUniversalAgent(type) {
739
739
  return agents[type].skillsDir === ".agents/skills";
740
740
  }
741
- export { EVE_SUBAGENTS_DIR, agents, agents_exports, detectInstalledAgents, getEveSubagents, getNonUniversalAgents, getUniversalAgents, getVisibleUniversalAgents, isUniversalAgent };
741
+ export { EVE_SUBAGENTS_DIR, agents, agents_exports, detectInstalledAgents, getEveSubagents, getNonUniversalAgents, getUniversalAgents, getVisibleUniversalAgents, isUniversalAgent, setAgents };
@@ -114,7 +114,10 @@ async function resolveAgents(api) {
114
114
  }
115
115
  }
116
116
  const config = loadGlobalConfig();
117
- if (config?.customAgents) for (const [name, entry] of Object.entries(config.customAgents)) merged.set(name, mapCustomEntry(name, entry));
117
+ if (config?.customAgents) {
118
+ const entries = Array.isArray(config.customAgents) ? config.customAgents.map((entry) => [entry.name, entry]) : Object.entries(config.customAgents);
119
+ for (const [name, entry] of entries) merged.set(name, mapCustomEntry(name, entry));
120
+ }
118
121
  return Object.fromEntries(merged);
119
122
  }
120
123
  export { resolveAgents };
@@ -0,0 +1,107 @@
1
+ import { hash, verifyPlan } from "./digest.mjs";
2
+ function registryUrl(value) {
3
+ const url = new URL(value);
4
+ if (!["http:", "https:"].includes(url.protocol) || url.username || url.password || url.search || url.hash) throw new Error("无效的 registry 地址。");
5
+ return url.toString().replace(/\/$/, "");
6
+ }
7
+ function credentialForRegistry(registry, config) {
8
+ return config?.token && registryUrl(registry) === registryUrl(config.registry ?? "https://bfi.yyuap.com:811") ? config.token : void 0;
9
+ }
10
+ async function readResponse(response, limit) {
11
+ const chunks = [];
12
+ let size = 0;
13
+ if (!response.body) throw new Error("服务返回空响应。");
14
+ const reader = response.body.getReader();
15
+ try {
16
+ while (true) {
17
+ const chunk = await reader.read();
18
+ if (chunk.done) break;
19
+ size += chunk.value.length;
20
+ if (size > limit) throw new Error("响应超过安全大小上限。");
21
+ chunks.push(chunk.value);
22
+ }
23
+ } finally {
24
+ await reader.cancel().catch(() => {});
25
+ }
26
+ return Buffer.concat(chunks);
27
+ }
28
+ var InstallationClient = class {
29
+ registry;
30
+ token;
31
+ fetcher;
32
+ constructor(registry, token, fetcher = fetch) {
33
+ this.registry = registryUrl(registry);
34
+ this.token = token;
35
+ this.fetcher = fetcher;
36
+ }
37
+ async request(path, method = "GET", body) {
38
+ const headers = { Accept: "application/json" };
39
+ if (this.token) headers.Authorization = `Bearer ${this.token}`;
40
+ if (method === "POST") {
41
+ headers["Content-Type"] = "application/json";
42
+ if (!this.token) {
43
+ const initial = await this.request("/api/web/mcps?size=1");
44
+ const cookie = initial.headers.get("set-cookie")?.match(/(?:^|,\s*)XSRF-TOKEN=([^;]+)/)?.[1];
45
+ await initial.body?.cancel();
46
+ if (!cookie) throw new Error("无法获取匿名 CSRF 凭据,请检查服务配置。");
47
+ headers.Cookie = `XSRF-TOKEN=${cookie}`;
48
+ headers["X-XSRF-TOKEN"] = decodeURIComponent(cookie);
49
+ }
50
+ }
51
+ let response;
52
+ try {
53
+ response = await this.fetcher(`${this.registry}${path}`, {
54
+ method,
55
+ headers,
56
+ body: body === void 0 ? void 0 : JSON.stringify(body),
57
+ redirect: "error",
58
+ signal: AbortSignal.timeout(12e4)
59
+ });
60
+ } catch {
61
+ throw new Error("网络连接失败或服务重定向;未转发登录凭据。");
62
+ }
63
+ if (response.status === 401) throw new Error("登录无效或需要登录,请运行 bfi auth login 后重试。");
64
+ if (response.status === 403) throw new Error("权限不足或 CSRF 校验失败;不会覆盖现有登录。");
65
+ if (response.status >= 300 && response.status < 400) throw new Error("不允许服务重定向。");
66
+ if (!response.ok) throw new Error(`安装服务请求失败(HTTP ${response.status}),请检查权限、计划有效期和服务版本。`);
67
+ return response;
68
+ }
69
+ async plan(response, kind, ref, id) {
70
+ const envelope = JSON.parse((await readResponse(response, 12 * 1024 ** 2)).toString("utf8"));
71
+ if (envelope.code !== 0) throw new Error("安装服务拒绝请求。");
72
+ const plan = verifyPlan(envelope.data);
73
+ if (registryUrl(plan.registry) !== this.registry || plan.kind !== kind || plan.ref !== ref || id && plan.id !== id) throw new Error("计划与请求来源或资源不匹配。");
74
+ if (Date.parse(plan.expiresAt) <= Date.now()) throw new Error("内容计划已过期,请重新确认最新内容。");
75
+ return plan;
76
+ }
77
+ async resolve(kind, ref) {
78
+ return this.plan(await this.request("/api/v1/installation-content-plans", "POST", {
79
+ kind,
80
+ ref
81
+ }), kind, ref);
82
+ }
83
+ async get(id, kind, ref) {
84
+ return this.plan(await this.request(`/api/v1/installation-content-plans/${encodeURIComponent(id)}`), kind, ref, id);
85
+ }
86
+ async artifact(plan) {
87
+ const response = await this.request(`/api/v1/installation-content-plans/${encodeURIComponent(plan.id)}/artifact`);
88
+ const bytes = await readResponse(response, 510 * 1024 ** 2);
89
+ if (response.headers.get("X-Archive-SHA256") !== hash(bytes)) throw new Error("下载归档摘要不匹配。");
90
+ return bytes;
91
+ }
92
+ async verifyAccount() {
93
+ if (!this.token) throw new Error("BFI MCP 需要本机登录态,请先运行 bfi auth login;不会把 Token 写进 MCP 配置。");
94
+ const response = await this.request("/api/v1/whoami");
95
+ let envelope;
96
+ try {
97
+ envelope = JSON.parse((await readResponse(response, 1048576)).toString("utf8"));
98
+ } catch {
99
+ throw new Error("本机 BFI 登录响应无效;未覆盖现有登录。");
100
+ }
101
+ if (!envelope || typeof envelope !== "object") throw new Error("本机 BFI 登录响应无效。");
102
+ if (envelope.code !== void 0 && envelope.code !== 0) throw new Error("本机 BFI 登录态验证失败。");
103
+ const data = envelope.data ?? envelope;
104
+ if (!data?.id && !data?.user?.id) throw new Error("本机 BFI 登录响应无效。");
105
+ }
106
+ };
107
+ export { InstallationClient, credentialForRegistry, registryUrl };