bfi-cli 0.1.102 → 0.1.103
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 +2 -2
- package/dist/_chunks/command.mjs +1 -3
- package/dist/_chunks/libs/@clack/core.mjs +28 -2
- package/dist/_chunks/libs/@clack/prompts.mjs +32 -2
- package/dist/_chunks/mcp.mjs +19 -87
- package/dist/_chunks/receipt.mjs +1 -1
- package/dist/_chunks/registry.mjs +83 -0
- package/dist/cli.mjs +799 -133
- package/package.json +1 -1
- package/dist/_chunks/files.mjs +0 -89
- package/dist/_chunks/manager.mjs +0 -436
package/README.md
CHANGED
|
@@ -101,7 +101,7 @@ Codex 不支持独立旧 SSE、拼接变量或 env_vars 重命名。`cwd` 仅在
|
|
|
101
101
|
|
|
102
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
103
|
|
|
104
|
-
单个技能与技能组合都沿用原有安装路径:`bfi skills add`
|
|
104
|
+
单个技能与技能组合都沿用原有安装路径:`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
105
|
|
|
106
106
|
```sh
|
|
107
107
|
bfi skills add @global/bfi-configuring-ui
|
|
@@ -134,7 +134,7 @@ MCP 连接结构复用已有标准化校验;已知凭据字段、敏感输入
|
|
|
134
134
|
|
|
135
135
|
### P4 网页安装入口与运行时检查
|
|
136
136
|
|
|
137
|
-
技能组合的安装卡片直接列出 CLI
|
|
137
|
+
技能组合的安装卡片直接列出 CLI 命令(安装 bfi、登录、安装组合、更新)并提供 ZIP 下载,不解析内容计划;组合关联 MCP 由安装命令自动一并安装。单个技能使用原有的 `bfi skills add` 命令和技能原始 ZIP 下载;插件的安装抽屉先解析固定内容计划,再生成 `--content-plan` 实际命令。目标 Agent 和范围可独立省略,CLI 交互确认。安装、npm 升级与登录命令均可复制。
|
|
138
138
|
|
|
139
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
140
|
|
package/dist/_chunks/command.mjs
CHANGED
|
@@ -3,10 +3,8 @@ 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 "
|
|
9
|
-
import { checkPath, readConfig, writeConfig } from "./files.mjs";
|
|
7
|
+
import { SkillHubAPI, checkPath, getAgentBaseDir, planInstall, readConfig, readDocument, serversIn, targetFor, writeConfig } from "../cli.mjs";
|
|
10
8
|
import { canonical, hash, stableManifest, verifyPlan } from "./digest.mjs";
|
|
11
9
|
import { InstallationClient, credentialForRegistry, registryUrl } from "./client.mjs";
|
|
12
10
|
import { resolveAgents } from "./agents2.mjs";
|
|
@@ -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 };
|
package/dist/_chunks/mcp.mjs
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
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);
|
package/dist/_chunks/receipt.mjs
CHANGED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { normalizeConfig, object, validName } from "./model.mjs";
|
|
2
|
+
async function readCatalog(registry, query = "", fetcher = fetch) {
|
|
3
|
+
let base;
|
|
4
|
+
try {
|
|
5
|
+
base = new URL(registry);
|
|
6
|
+
} catch {
|
|
7
|
+
throw new Error("SkillHub 地址无效。");
|
|
8
|
+
}
|
|
9
|
+
if (!["https:", "http:"].includes(base.protocol) || base.username || base.password || base.search || base.hash) throw new Error("SkillHub 地址必须为无凭据的 HTTP(S) 地址。");
|
|
10
|
+
const entries = [];
|
|
11
|
+
const seen = /* @__PURE__ */ new Set();
|
|
12
|
+
for (let page = 0; page < 100; page++) {
|
|
13
|
+
const url = new URL(`${base.toString().replace(/\/$/, "")}/api/web/mcps`);
|
|
14
|
+
url.search = new URLSearchParams({
|
|
15
|
+
query,
|
|
16
|
+
page: String(page),
|
|
17
|
+
size: "100"
|
|
18
|
+
}).toString();
|
|
19
|
+
const controller = new AbortController();
|
|
20
|
+
const timeout = setTimeout(() => controller.abort(), 15e3);
|
|
21
|
+
try {
|
|
22
|
+
const response = await fetcher(url, {
|
|
23
|
+
signal: controller.signal,
|
|
24
|
+
redirect: "error",
|
|
25
|
+
headers: { Accept: "application/json" }
|
|
26
|
+
});
|
|
27
|
+
if (!response.ok) throw new Error();
|
|
28
|
+
const reader = response.body?.getReader();
|
|
29
|
+
if (!reader) throw new Error();
|
|
30
|
+
const chunks = [];
|
|
31
|
+
let size = 0;
|
|
32
|
+
try {
|
|
33
|
+
while (true) {
|
|
34
|
+
const chunk = await reader.read();
|
|
35
|
+
if (chunk.done) break;
|
|
36
|
+
size += chunk.value.byteLength;
|
|
37
|
+
if (size > 2097152) throw new Error();
|
|
38
|
+
chunks.push(chunk.value);
|
|
39
|
+
}
|
|
40
|
+
} finally {
|
|
41
|
+
await reader.cancel().catch(() => {});
|
|
42
|
+
}
|
|
43
|
+
const envelope = object(JSON.parse(Buffer.concat(chunks).toString("utf8")));
|
|
44
|
+
if (envelope.code !== 0) throw new Error();
|
|
45
|
+
const data = object(envelope.data);
|
|
46
|
+
if (!Array.isArray(data.items) || typeof data.total !== "number" || data.total < 0) throw new Error();
|
|
47
|
+
for (const item of data.items) {
|
|
48
|
+
const row = object(item);
|
|
49
|
+
if (typeof row.registrationName !== "string") throw new Error();
|
|
50
|
+
const name = validName(row.registrationName);
|
|
51
|
+
if (seen.has(name)) throw new Error();
|
|
52
|
+
seen.add(name);
|
|
53
|
+
entries.push({
|
|
54
|
+
registrationName: name,
|
|
55
|
+
id: typeof row.id === "string" ? row.id : void 0,
|
|
56
|
+
title: typeof row.title === "string" ? row.title : void 0,
|
|
57
|
+
description: typeof row.description === "string" ? row.description : void 0,
|
|
58
|
+
revision: typeof row.revision === "number" ? row.revision : void 0,
|
|
59
|
+
config: normalizeConfig(row.config),
|
|
60
|
+
runtime: row.runtime ? { source: String(object(row.runtime).source) } : null
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
if (entries.length >= data.total) return entries;
|
|
64
|
+
if (!data.items.length) throw new Error();
|
|
65
|
+
} catch {
|
|
66
|
+
throw new Error("读取 MCP 市场失败:请核对 SkillHub 地址、网络及服务版本;未修改本地配置。");
|
|
67
|
+
} finally {
|
|
68
|
+
clearTimeout(timeout);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
throw new Error("MCP 市场分页超过安全上限,请缩小查询范围。");
|
|
72
|
+
}
|
|
73
|
+
async function catalogEntries(registry, names) {
|
|
74
|
+
const entries = [];
|
|
75
|
+
for (const name of names) {
|
|
76
|
+
validName(name);
|
|
77
|
+
const matches = (await readCatalog(registry, name)).filter((entry) => entry.registrationName === name);
|
|
78
|
+
if (matches.length !== 1) throw new Error(`市场中未找到唯一 MCP:${name}。请用 bfi mcp search 查询准确名称。`);
|
|
79
|
+
entries.push(matches[0]);
|
|
80
|
+
}
|
|
81
|
+
return entries;
|
|
82
|
+
}
|
|
83
|
+
export { catalogEntries, readCatalog };
|