bfi-cli 0.1.102 → 0.1.104

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
@@ -15,27 +15,33 @@ bfi package install @team/kit --agent claude-code
15
15
  ## 明确选择插件
16
16
 
17
17
  ```sh
18
- bfi skills package install @team/kit --as plugin --version 1.0.0
19
- bfi package install @team/kit --as plugin --agent claude-code --version 1.0.0 --plan
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
22
- bfi package install ./kit.tgz --as plugin --agent codex --root bundle --version 1.0.0 --plan
18
+ # 下载并校验默认安装版本(= 最新发布版),不安装
19
+ bfi skills package download @team/kit --as plugin -o ./kit.zip
20
+ bfi skills package download @team/kit --as plugin --format tgz -o ./kit.tgz
21
+ bfi skills package download @team/kit --as plugin --format tar.gz -o ./kit.tar.gz
22
+
23
+ # 生成版本化原生目录并更新本地市场索引;由用户执行输出中的宿主命令
24
+ bfi skills package prepare @team/kit --as plugin --agent claude-code --dir ./kit-handoff
25
+
26
+ # 按默认版本重新下载/交接,不依赖安装记录
27
+ bfi skills package update @team/kit --as plugin --agent claude-code --dir ./kit-handoff
28
+
29
+ # 把命名空间注册为 Claude Code 市场(写入 ~/.claude/settings.json,headersHelper 使用 bfi 登录态)
30
+ bfi skills package marketplace add global
31
+
32
+ # 本地归档
33
+ bfi package prepare ./kit.tgz --as plugin --agent codex --root bundle --dir ./kit-handoff
23
34
  bfi package list --as plugin --scope project
24
35
  ```
25
36
 
26
- - Claude Code **2.1.251** 的已验证组件开放受控原生注册;仍须满足精确发布版本的服务端授权、宿主/adapter 版本和 LOAD 证据。没有这些证据时不会安装。Codex **0.136.0** 仅开放原生技能型插件的人工交接;ZCode、CodeBuddy、Qwen Code 未实测,只导出并明确提示待适配。详见 [插件验证矩阵](PLUGIN-VALIDATION.md)。
27
- - 通用安装命令不必指定 Agent 和范围,CLI 会在交互终端中询问;非交互调用必须显式选择,`--yes` 不会代选。不支持的宿主或组件仍明确停止,不降级成技能安装。
28
- - `--plan` 只给计划/阻断原因;`--export` 原样导出归档,不计作安装。目标文件必须不存在,扩展名与源格式一致。
29
- - `--prepare-native <新目录>` 生成完整原始插件目录及独立市场索引,输出真实宿主命令;不注册宿主、不执行 Hooks、不创建 bfi 安装归属。只在宿主版本、作用域及全部必要组件受支持时生成。审阅后由用户确认执行;安装后保留该目录。人工交接的更新/卸载由原生宿主管理。
30
- - `--plan` 返回 READY 后,去掉 `--plan` 并确认可原生注册;非交互需显式 `--yes`。结果 REGISTERED 仅表示注册和文件归属已核对,需重启宿主检查所有组件,不能视为运行功能验证通过。
31
- - 市场来源必须写完整名称 `@namespace/package`,不要求用户输入数据库 ID。未指定版本时只解析一次可见 stable;没有 stable 则要求明确版本,不猜 latest。
32
- - 本地支持 zip/tgz/tar.gz,多根必须用 `--root` 选择;保留全部原始文件、执行位和未知字段,不自动转换各宿主格式。缺原生版本时可用 `--version` 描述本地导出身份,但不会改源文件或绕过市场正式发布的版本校验。
33
- - 原生安装/卸载/交接要求明确范围:`--scope user`(或 `-g`)/ `--scope project`;交互终端省略时询问,非交互或 `--yes` 不能代选。只读计划/导出/列表省略时按项目范围查看。插件独立使用 `.bfi/plugin-lock.json`,不与旧技能锁混用。
34
- - 未知安装结果保留 PREPARED 并阻止盲目重试;更新/卸载必须证明精确来源、宿主、范围、版本及安装归属。
35
- - 同一已安装内容重复安装不重复写入;原生版本更新暂不自动替换,提示 NATIVE_UPDATE_REQUIRES_USER 并保留旧版。卸载保留数据、市场索引及原始目录;重装使用新的独立市场,不覆盖保留目录。明确的同名预检冲突记录 FAILED,可解决后重试;原生命令结果不明才保留 PREPARED。
36
- - 所有路径都先执行归档安全检查。疑似凭据、非法路径、缺失关键组件或不支持的格式会停止,不能为导出或安装绕过,也不会自动降级成技能安装。
37
-
38
- 服务器插件功能默认开启;显式设置 `PACKAGE_PLUGIN_ENABLED=false` 时仍可使用既有技能包,但插件市场操作不可用。本地原始包计划/导出不等于服务器发布。
37
+ - 版本默认取「默认安装版本」,即最近一次发布;无需 `--version`。没有默认版本时才要求显式版本,不猜 latest。
38
+ - Claude Code **2.1.251** 与 Codex **0.136.0** 已实现适配器,但原生自动注册仍受服务端能力与运行时 LOAD 证据门禁;不满足时使用 `download` / `prepare`,不会降级成技能安装。ZCode、CodeBuddy、Qwen Code 只导出/交接。
39
+ - `download` 不要求 `--agent`,只导出并校验归档;支持 `--format zip|tgz|tar.gz`(默认按输出扩展名或 zip)。zip 校验归档摘要,tar 格式校验 treeDigest 与逐文件摘要;目标文件必须不存在。
40
+ - `prepare` 生成 `versions/<version>/plugin` 与市场索引(`.claude-plugin/marketplace.json` 或 `.agents/plugins/marketplace.json`),更新时原子改写索引指向新版本;不注册宿主、不执行 Hooks、不创建安装归属。
41
+ - `update`、`download`、`prepare` 均不以安装记录为前提;记录只用于原生注册的归属校验与卸载。
42
+ - 市场来源必须写完整名称 `@namespace/package`;跨来源凭据不转发。
43
+ - 原生安装/卸载要求明确范围:`--scope user`(或 `-g`)/ `--scope project`;交互终端省略时询问,非交互或 `--yes` 不能代选。
44
+ - 未知安装结果保留 PREPARED 并阻止盲目重试;卸载保留数据与市场索引。
39
45
 
40
46
  ## 独立 MCP 安装与测试
41
47
 
@@ -101,7 +107,7 @@ Codex 不支持独立旧 SSE、拼接变量或 env_vars 重命名。`cwd` 仅在
101
107
 
102
108
  `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
109
 
104
- 单个技能与技能组合都沿用原有安装路径:`bfi skills add` 安装单个技能,`bfi skills package install` 安装组合内的全部技能;组合关联的 MCP 使用 `bfi mcp install` 单独安装。组合 ZIP 为各技能文件夹加根目录 `mcp.json`(仅当组合关联 MCP 时存在)。其它 Git/本地技能来源仍走既有 skills.sh 路径;独立 MCP 和显式 `--as plugin` 不变。缺 Agent/范围时分别询问,不默认增加 universal Agent。非交互必须显式给出目标、范围和 `-y`;不支持 `--skills-only`,关联 MCP 不可取消。
110
+ 单个技能与技能组合都沿用原有安装路径:`bfi skills add` 安装单个技能;`bfi skills package install` 一次下载组合包,批量安装全部技能,并在组合包含 `mcp.json` 时一并安装关联 MCP。安装前会展示技能与 MCP 清单,MCP 需要的变量在交互终端中隐藏输入;用户级配置可直接物化,项目级配置只保存变量引用。组合 ZIP 为各技能文件夹加根目录 `mcp.json`(仅当组合关联 MCP 时存在)。其它 Git/本地技能来源仍走既有 skills.sh 路径;独立 MCP 和显式 `--as plugin` 不变。缺 Agent/范围时分别询问,不默认增加 universal Agent。非交互必须显式给出目标、范围和 `-y`;不支持 `--skills-only`,关联 MCP 不可取消。
105
111
 
106
112
  ```sh
107
113
  bfi skills add @global/bfi-configuring-ui
@@ -134,7 +140,7 @@ MCP 连接结构复用已有标准化校验;已知凭据字段、敏感输入
134
140
 
135
141
  ### P4 网页安装入口与运行时检查
136
142
 
137
- 技能组合的安装卡片直接列出 CLI 命令(登录、安装组合、安装关联 MCP)并提供 ZIP 下载,不解析内容计划;单个技能使用原有的 `bfi skills add` 命令和技能原始 ZIP 下载;插件的安装抽屉先解析固定内容计划,再生成 `--content-plan` 实际命令。目标 Agent 和范围可独立省略,CLI 交互确认。安装、npm 升级与登录命令均可复制。
143
+ 技能组合的安装卡片直接列出 CLI 命令(安装 bfi、登录、安装组合、更新)并提供 ZIP 下载,不解析内容计划;组合关联 MCP 由安装命令自动一并安装。单个技能使用原有的 `bfi skills add` 命令和技能原始 ZIP 下载;插件的安装抽屉先解析固定内容计划,再生成 `--content-plan` 实际命令。目标 Agent 和范围可独立省略,CLI 交互确认。安装、npm 升级与登录命令均可复制。
138
144
 
139
145
  服务端 `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
146
 
@@ -3,12 +3,9 @@ import { confirm as confirm$1, multiselect, select } from "./libs/@clack/prompts
3
3
  import { inspectPluginArchive } from "./intent.mjs";
4
4
  import { loadGlobalConfig, loadProjectConfig, resolveConfig } from "./config.mjs";
5
5
  import { agents, detectInstalledAgents, setAgents } from "./agents.mjs";
6
- import { SkillHubAPI, getAgentBaseDir } from "../cli.mjs";
7
6
  import { builtinCommands, normalizeConfig, variables } from "./model.mjs";
8
- import { planInstall, readDocument, serversIn, targetFor } from "./manager.mjs";
9
- import { checkPath, readConfig, writeConfig } from "./files.mjs";
10
7
  import { canonical, hash, stableManifest, verifyPlan } from "./digest.mjs";
11
- import { InstallationClient, credentialForRegistry, registryUrl } from "./client.mjs";
8
+ import { InstallationClient, SkillHubAPI, checkPath, credentialForRegistry, getAgentBaseDir, planInstall, readConfig, readDocument, registryUrl, serversIn, targetFor, writeConfig } from "../cli.mjs";
12
9
  import { resolveAgents } from "./agents2.mjs";
13
10
  import { assertWritable, atomicPrivate, checkpoint, fingerprint, operationRoot, ownershipDigest, pathKey, readReceipt, receipts, safeDirectory, treeFingerprint, withInstallationLock } from "./receipt.mjs";
14
11
  import { randomUUID } from "node:crypto";
@@ -572,8 +572,9 @@ var PluginRegistryClient = class {
572
572
  async release(namespace, slug, version) {
573
573
  return this.json(`${this.base(namespace, slug)}/plugin/releases/${segment(version)}`);
574
574
  }
575
- async download(namespace, slug, version, maximum = pluginArchiveLimits.compressedBytes) {
576
- return this.bytes(`${this.base(namespace, slug)}/plugin/releases/${segment(version)}/download`, maximum);
575
+ async download(namespace, slug, version, format = "zip", maximum = pluginArchiveLimits.compressedBytes) {
576
+ const suffix = format === "zip" ? "" : `?format=${encodeURIComponent(format)}`;
577
+ return this.bytes(`${this.base(namespace, slug)}/plugin/releases/${segment(version)}/download${suffix}`, maximum);
577
578
  }
578
579
  };
579
580
  const pluginTargets = [
@@ -600,10 +601,13 @@ function parsePackageIntent(args) {
600
601
  "--plan",
601
602
  "--export",
602
603
  "--prepare-native",
604
+ "--dir",
605
+ "--output",
606
+ "--format",
603
607
  "--scope",
604
608
  "--root",
605
609
  "--operation"
606
- ].includes(value) || /^(--plan|--export|--prepare-native|--scope|--root|--operation)=/u.test(value)) || isPluginArchive(output[1] ?? ""))) throw new Error("Plugin archives and plugin options require explicit --as plugin; no skills were installed");
610
+ ].includes(value) || /^(--plan|--export|--prepare-native|--dir|--output|--format|--scope|--root|--operation)=/u.test(value)) || isPluginArchive(output[1] ?? ""))) throw new Error("Plugin archives and plugin options require explicit --as plugin; no skills were installed");
607
611
  return {
608
612
  mode,
609
613
  args: output
@@ -615,12 +619,17 @@ function parsePluginArguments(args) {
615
619
  i: "install",
616
620
  update: "update",
617
621
  up: "update",
622
+ download: "download",
623
+ dl: "download",
624
+ export: "download",
625
+ prepare: "prepare",
626
+ handoff: "prepare",
618
627
  uninstall: "uninstall",
619
628
  rm: "uninstall",
620
629
  list: "list",
621
630
  ls: "list"
622
631
  }[args[0] ?? ""];
623
- if (!command) throw new Error("Plugin intent supports install, update, uninstall and list");
632
+ if (!command) throw new Error("Plugin intent supports install, update, download, prepare, uninstall and list");
624
633
  const result = {
625
634
  command,
626
635
  scope: "project",
@@ -634,7 +643,10 @@ function parsePluginArguments(args) {
634
643
  "--scope",
635
644
  "--root",
636
645
  "--export",
646
+ "--output",
647
+ "--format",
637
648
  "--prepare-native",
649
+ "--dir",
638
650
  "--operation",
639
651
  "--registry",
640
652
  "--token",
@@ -663,9 +675,16 @@ function parsePluginArguments(args) {
663
675
  if (!["project", "user"].includes(value)) throw new Error("Plugin scope must be project or user");
664
676
  result.scope = value;
665
677
  } else if (flag === "--root") result.root = value;
666
- else if (flag === "--export") result.exportPath = value;
667
- else if (flag === "--prepare-native") result.preparePath = value;
668
- else if (flag === "--operation") result.operationId = value;
678
+ else if (flag === "--export" || flag === "--output") result.exportPath = value;
679
+ else if (flag === "--prepare-native" || flag === "--dir") result.preparePath = value;
680
+ else if (flag === "--format") {
681
+ if (![
682
+ "zip",
683
+ "tgz",
684
+ "tar.gz"
685
+ ].includes(value)) throw new Error("Plugin archive format must be zip, tgz or tar.gz");
686
+ result.format = value;
687
+ } else if (flag === "--operation") result.operationId = value;
669
688
  } else if (flag === "--global") result.scope = "user";
670
689
  else if (flag === "--plan") result.planOnly = true;
671
690
  else if (flag === "--json") result.json = true;
@@ -677,9 +696,21 @@ function parsePluginArguments(args) {
677
696
  if (command !== "list" && !result.source && !(command === "uninstall" && result.operationId)) throw new Error("Specify @namespace/package or a local zip/tgz path");
678
697
  if (result.source && !/^@[^/\s]+\/[^/\s]+$/u.test(result.source) && !isPluginArchive(result.source)) throw new Error("Use a namespace-qualified @namespace/package reference or local archive");
679
698
  if (result.planOnly && result.exportPath) throw new Error("Plan and export are separate operations");
680
- if (result.preparePath && (result.planOnly || result.exportPath || command !== "install")) throw new Error("Native handoff, plan and export are separate install operations");
681
- if (result.root !== void 0 && !isPluginArchive(result.source ?? "") || result.operationId && command !== "uninstall" || (result.exportPath || result.root !== void 0) && command !== "install") throw new Error("Plugin option is invalid for this operation");
682
- if (command === "list" && (result.source || result.version || result.planOnly || result.exportPath || result.operationId)) throw new Error("Invalid plugin list options");
699
+ if (result.planOnly && command !== "install") throw new Error("--plan is only available for plugin install");
700
+ if (result.preparePath && result.exportPath) throw new Error("Download and native handoff are separate operations");
701
+ if (command === "download" && (result.planOnly || result.preparePath)) throw new Error("Download only supports --output; use prepare for native handoff");
702
+ if (command === "prepare" && !result.agent) throw new Error("AGENT_REQUIRED: native handoff needs --agent claude-code or --agent codex");
703
+ if ((result.exportPath || result.preparePath) && ![
704
+ "install",
705
+ "update",
706
+ "download",
707
+ "prepare"
708
+ ].includes(command)) throw new Error("Plugin option is invalid for this operation");
709
+ if (result.root !== void 0 && !isPluginArchive(result.source ?? "")) throw new Error("--root only applies to local plugin archives");
710
+ if (result.operationId && command !== "uninstall") throw new Error("--operation is only valid for uninstall");
711
+ if (command === "list" && (result.source || result.version || result.planOnly || result.exportPath || result.preparePath || result.operationId)) throw new Error("Invalid plugin list options");
712
+ if (command === "prepare" && result.planOnly) throw new Error("Plan and native handoff are separate operations");
713
+ if (result.format && !(command === "download" || result.exportPath)) throw new Error("--format requires download or --export");
683
714
  return result;
684
715
  }
685
716
  export { PluginInstallRecords, PluginRegistryClient, inspectPluginArchive, isPluginArchive, isPluginVersion, materializePluginTree, normalizeRegistry, parsePackageIntent, parsePluginArguments, pluginArchiveLimits, pluginInstallKey, pluginSlotKey, pluginTargets, selectPluginRoot, sha256, treeDigest };
@@ -1,5 +1,5 @@
1
1
  import { __commonJSMin } from "../../rolldown-runtime.mjs";
2
- import "node:util";
2
+ import { styleText } from "node:util";
3
3
  import { stdin, stdout } from "node:process";
4
4
  import l__default, * as l from "node:readline";
5
5
  import { ReadStream } from "node:tty";
@@ -734,6 +734,32 @@ var a = class extends y {
734
734
  });
735
735
  }
736
736
  };
737
+ let u$1 = class u extends y {
738
+ _mask = "•";
739
+ get cursor() {
740
+ return this._cursor;
741
+ }
742
+ get masked() {
743
+ return this.userInput.replaceAll(/./g, this._mask);
744
+ }
745
+ get userInputWithCursor() {
746
+ if (this.state === "submit" || this.state === "cancel") return this.masked;
747
+ const t = this.userInput;
748
+ if (this.cursor >= t.length) return `${this.masked}${styleText(["inverse", "hidden"], "_")}`;
749
+ const s = this.masked, r = s.slice(0, this.cursor), i = s.slice(this.cursor, this.cursor + 1), o = s.slice(this.cursor + 1);
750
+ return `${r}${styleText("inverse", i)}${o}`;
751
+ }
752
+ clear() {
753
+ this._clearUserInput();
754
+ }
755
+ constructor({ mask: t, ...s }) {
756
+ super(s), this._mask = t ?? "•", this.on("userInput", (r) => {
757
+ this._setValue(r);
758
+ }), this.on("finalize", () => {
759
+ this.value === void 0 && (this.value = "");
760
+ });
761
+ }
762
+ };
737
763
  let n$1 = class n extends y {
738
764
  options;
739
765
  cursor = 0;
@@ -760,4 +786,4 @@ let n$1 = class n extends y {
760
786
  });
761
787
  }
762
788
  };
763
- export { a, block, fastStringWidth, getColumns, getRows, isCancel, n$1, r, require_src, settings, u$2, wrapAnsi, wrapTextWithPrefix };
789
+ export { a, block, fastStringWidth, getColumns, getRows, isCancel, n$1, r, require_src, settings, u$1, u$2, wrapAnsi, wrapTextWithPrefix };
@@ -1,5 +1,5 @@
1
1
  import { __exportAll } from "../../rolldown-runtime.mjs";
2
- import { a, block, fastStringWidth, getColumns, getRows, n$1, r, require_src, settings, u$2, wrapAnsi, wrapTextWithPrefix } from "./core.mjs";
2
+ import { a, block, fastStringWidth, getColumns, getRows, n$1, r, require_src, settings, u$1, u$2, wrapAnsi, wrapTextWithPrefix } from "./core.mjs";
3
3
  import { styleText } from "node:util";
4
4
  import process$1 from "node:process";
5
5
  import "node:fs";
@@ -20,6 +20,7 @@ var dist_exports = /* @__PURE__ */ __exportAll({
20
20
  S_CORNER_TOP_RIGHT: () => S_CORNER_TOP_RIGHT,
21
21
  S_ERROR: () => S_ERROR,
22
22
  S_INFO: () => S_INFO,
23
+ S_PASSWORD_MASK: () => S_PASSWORD_MASK,
23
24
  S_RADIO_ACTIVE: () => S_RADIO_ACTIVE,
24
25
  S_RADIO_INACTIVE: () => S_RADIO_INACTIVE,
25
26
  S_STEP_ACTIVE: () => S_STEP_ACTIVE,
@@ -39,6 +40,7 @@ var dist_exports = /* @__PURE__ */ __exportAll({
39
40
  multiselect: () => multiselect,
40
41
  note: () => note,
41
42
  outro: () => outro,
43
+ password: () => password,
42
44
  select: () => select,
43
45
  spinner: () => spinner,
44
46
  symbol: () => symbol,
@@ -66,6 +68,7 @@ const S_RADIO_INACTIVE = unicodeOr("○", " ");
66
68
  const S_CHECKBOX_ACTIVE = unicodeOr("◻", "[•]");
67
69
  const S_CHECKBOX_SELECTED = unicodeOr("◼", "[+]");
68
70
  const S_CHECKBOX_INACTIVE = unicodeOr("◻", "[ ]");
71
+ const S_PASSWORD_MASK = unicodeOr("▪", "•");
69
72
  const S_BAR_H = unicodeOr("─", "-");
70
73
  const S_CORNER_TOP_RIGHT = unicodeOr("╮", "+");
71
74
  const S_CONNECT_LEFT = unicodeOr("├", "+");
@@ -446,6 +449,33 @@ ${h}
446
449
  ${styleText("gray", l$1 + S_BAR_H.repeat(t + 2) + S_CORNER_BOTTOM_RIGHT)}
447
450
  `);
448
451
  };
452
+ const password = (e) => new u$1({
453
+ validate: e.validate,
454
+ mask: e.mask ?? S_PASSWORD_MASK,
455
+ signal: e.signal,
456
+ input: e.input,
457
+ output: e.output,
458
+ render() {
459
+ const r = e.withGuide ?? settings.withGuide, o = `${r ? `${styleText("gray", S_BAR)}
460
+ ` : ""}${symbol(this.state)} ${e.message}
461
+ `, m = this.userInputWithCursor, i = this.masked;
462
+ switch (this.state) {
463
+ case "error": {
464
+ const s = r ? `${styleText("yellow", S_BAR)} ` : "", n = r ? `${styleText("yellow", S_BAR_END)} ` : "", d = i ?? "";
465
+ return e.clearOnError && this.clear(), `${o.trim()}
466
+ ${s}${d}
467
+ ${n}${styleText("yellow", this.error)}
468
+ `;
469
+ }
470
+ case "submit": return `${o}${r ? `${styleText("gray", S_BAR)} ` : ""}${i ? styleText("dim", i) : ""}`;
471
+ case "cancel": return `${o}${r ? `${styleText("gray", S_BAR)} ` : ""}${i ? styleText(["strikethrough", "dim"], i) : ""}${i && r ? `
472
+ ${styleText("gray", S_BAR)}` : ""}`;
473
+ default: return `${o}${r ? `${styleText("cyan", S_BAR)} ` : ""}${m}
474
+ ${r ? styleText("cyan", S_BAR_END) : ""}
475
+ `;
476
+ }
477
+ }
478
+ }).prompt();
449
479
  const W = (l) => styleText("magenta", l);
450
480
  const spinner = ({ indicator: l = "dots", onCancel: h, output: n = process.stdout, cancelMessage: G, errorMessage: O, frames: E = unicode ? [
451
481
  "◒",
@@ -582,4 +612,4 @@ ${f}
582
612
  }).prompt();
583
613
  };
584
614
  `${styleText("gray", S_BAR)}`;
585
- export { cancel, confirm, dist_exports, groupMultiselect, intro, log, multiselect, note, outro, select, spinner };
615
+ export { cancel, confirm, dist_exports, groupMultiselect, intro, log, multiselect, note, outro, password, select, spinner };
@@ -1,6 +1,8 @@
1
- import { builtinCommands, entriesFromJson, normalizeConfig, object, resolveVariables, validName, variables } from "./model.mjs";
2
- import { agentIds, applyPlan, decodeServer, loadTarget, planInstall, planRemove, readDocument, targetFor } from "./manager.mjs";
3
- import { readConfig } from "./files.mjs";
1
+ import { isCancel } from "./libs/@clack/core.mjs";
2
+ import { select } from "./libs/@clack/prompts.mjs";
3
+ import { builtinCommands, entriesFromJson, resolveVariables, validName, variables } from "./model.mjs";
4
+ import { agentIds, applyPlan, decodeServer, loadTarget, planInstall, planRemove, readConfig, readDocument, targetFor } from "../cli.mjs";
5
+ import { catalogEntries, readCatalog } from "./registry.mjs";
4
6
  import { join } from "node:path";
5
7
  import { readFile } from "node:fs/promises";
6
8
  import { homedir } from "node:os";
@@ -9,87 +11,6 @@ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
9
11
  import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
10
12
  import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
11
13
  import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
12
- async function readCatalog(registry, query = "", fetcher = fetch) {
13
- let base;
14
- try {
15
- base = new URL(registry);
16
- } catch {
17
- throw new Error("SkillHub 地址无效。");
18
- }
19
- if (!["https:", "http:"].includes(base.protocol) || base.username || base.password || base.search || base.hash) throw new Error("SkillHub 地址必须为无凭据的 HTTP(S) 地址。");
20
- const entries = [];
21
- const seen = /* @__PURE__ */ new Set();
22
- for (let page = 0; page < 100; page++) {
23
- const url = new URL(`${base.toString().replace(/\/$/, "")}/api/web/mcps`);
24
- url.search = new URLSearchParams({
25
- query,
26
- page: String(page),
27
- size: "100"
28
- }).toString();
29
- const controller = new AbortController();
30
- const timeout = setTimeout(() => controller.abort(), 15e3);
31
- try {
32
- const response = await fetcher(url, {
33
- signal: controller.signal,
34
- redirect: "error",
35
- headers: { Accept: "application/json" }
36
- });
37
- if (!response.ok) throw new Error();
38
- const reader = response.body?.getReader();
39
- if (!reader) throw new Error();
40
- const chunks = [];
41
- let size = 0;
42
- try {
43
- while (true) {
44
- const chunk = await reader.read();
45
- if (chunk.done) break;
46
- size += chunk.value.byteLength;
47
- if (size > 2097152) throw new Error();
48
- chunks.push(chunk.value);
49
- }
50
- } finally {
51
- await reader.cancel().catch(() => {});
52
- }
53
- const envelope = object(JSON.parse(Buffer.concat(chunks).toString("utf8")));
54
- if (envelope.code !== 0) throw new Error();
55
- const data = object(envelope.data);
56
- if (!Array.isArray(data.items) || typeof data.total !== "number" || data.total < 0) throw new Error();
57
- for (const item of data.items) {
58
- const row = object(item);
59
- if (typeof row.registrationName !== "string") throw new Error();
60
- const name = validName(row.registrationName);
61
- if (seen.has(name)) throw new Error();
62
- seen.add(name);
63
- entries.push({
64
- registrationName: name,
65
- id: typeof row.id === "string" ? row.id : void 0,
66
- title: typeof row.title === "string" ? row.title : void 0,
67
- description: typeof row.description === "string" ? row.description : void 0,
68
- revision: typeof row.revision === "number" ? row.revision : void 0,
69
- config: normalizeConfig(row.config),
70
- runtime: row.runtime ? { source: String(object(row.runtime).source) } : null
71
- });
72
- }
73
- if (entries.length >= data.total) return entries;
74
- if (!data.items.length) throw new Error();
75
- } catch {
76
- throw new Error("读取 MCP 市场失败:请核对 SkillHub 地址、网络及服务版本;未修改本地配置。");
77
- } finally {
78
- clearTimeout(timeout);
79
- }
80
- }
81
- throw new Error("MCP 市场分页超过安全上限,请缩小查询范围。");
82
- }
83
- async function catalogEntries(registry, names) {
84
- const entries = [];
85
- for (const name of names) {
86
- validName(name);
87
- const matches = (await readCatalog(registry, name)).filter((entry) => entry.registrationName === name);
88
- if (matches.length !== 1) throw new Error(`市场中未找到唯一 MCP:${name}。请用 bfi mcp search 查询准确名称。`);
89
- entries.push(matches[0]);
90
- }
91
- return entries;
92
- }
93
14
  async function probe(config, options = {}) {
94
15
  const environment = options.environment ?? process.env;
95
16
  const timeoutMs = options.timeoutMs ?? 3e4;
@@ -230,8 +151,8 @@ const help = `bfi mcp — 安装、管理与验证独立 MCP(不影响技能
230
151
  search [query] 搜索 SkillHub MCP 市场
231
152
  info <name> 查看元信息、传输和所需变量(不显示凭据)
232
153
  agents 查看支持的 Agent 与配置路径
233
- install <name...> --agent <agent> 安装选中的 MCP,默认用户级
234
- install --file <json> --agent <agent> 离线导入 mcpServers JSON
154
+ install <name...> [--agent <agent>] 安装选中的 MCP,默认用户级;未指定 Agent 时交互选择
155
+ install --file <json> [--agent <agent>] 离线导入 mcpServers JSON
235
156
  list --agent <agent> 列出所选文件中的 MCP
236
157
  remove <name...> --agent <agent> 仅移除配置,不卸载程序
237
158
  test <name...> --agent <agent> 显式启动/连接并查询 tools/list,不调用业务工具
@@ -329,7 +250,6 @@ function parseMcpArguments(args) {
329
250
  ].includes(result.command)) throw new Error("未知 MCP 命令;请运行 bfi mcp --help。");
330
251
  if (result.command === "help") return result;
331
252
  if ([
332
- "install",
333
253
  "list",
334
254
  "remove",
335
255
  "test"
@@ -419,6 +339,18 @@ async function runMcp(args) {
419
339
  if (!entries.length && !options.json) print("未找到 MCP。");
420
340
  return;
421
341
  }
342
+ if (!options.agent) {
343
+ if (options.json || !process.stdin.isTTY || !process.stdout.isTTY) throw new Error("非交互安装需要 --agent;bfi mcp agents 可查看支持列表。");
344
+ const selected = await select({
345
+ message: "选择目标 Agent",
346
+ options: agentIds.map((agent) => ({
347
+ value: agent,
348
+ label: agent
349
+ }))
350
+ });
351
+ if (isCancel(selected)) throw new Error("已取消,未写入配置。");
352
+ options.agent = selected;
353
+ }
422
354
  const target = targetFor(options.agent, options.scope, { config: options.config });
423
355
  if (options.command === "list") {
424
356
  const { servers } = await loadTarget(target);