fde2e-bu 0.0.0 → 1.3.0
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 +65 -0
- package/bin.js +13 -0
- package/package.json +30 -6
- package/src/args.mjs +92 -0
- package/src/cli.mjs +445 -0
- package/src/configdir.mjs +16 -0
- package/src/mcp/bridge.mjs +208 -0
- package/src/mcp/commands-snapshot.json +340 -0
- package/src/mcp/config-cli.mjs +172 -0
- package/src/mcp/config.mjs +257 -0
- package/src/mcp/http.mjs +250 -0
- package/src/mcp/schema.mjs +175 -0
- package/src/mcp/serve.mjs +203 -0
- package/src/misc.mjs +229 -0
- package/src/nm/bind.mjs +229 -0
- package/src/nm/host.mjs +429 -0
- package/src/nm/update.mjs +727 -0
- package/src/skills/frontmatter.mjs +150 -0
- package/src/skills/skills.mjs +323 -0
- package/index.js +0 -1
package/README.md
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# fde2e-bu —— FDE2E Browser Use 命令行(零运行时依赖)
|
|
2
|
+
|
|
3
|
+
浏览器多控执行端(MV3 扩展 `FDE2E BU Bridge`)的 CLI 伴生工具:技能包管理+MCP
|
|
4
|
+
server+扩展一次性配对。
|
|
5
|
+
|
|
6
|
+
```bash
|
|
7
|
+
# 安装(任选其一;Node ≥ 22)
|
|
8
|
+
npm i -g fde2e-bu
|
|
9
|
+
npx -y fde2e-bu@latest --help
|
|
10
|
+
|
|
11
|
+
fde2e-bu skills list | add <path|git-url> | remove <name> | validate [<path>]
|
|
12
|
+
fde2e-bu mcp serve [--http [--port 8791] [--token <t>]] # stdio 缺省;--http 见下
|
|
13
|
+
fde2e-bu mcp add --client claude|codex|gemini [--scope user|project|local]
|
|
14
|
+
fde2e-bu mcp list | remove
|
|
15
|
+
fde2e-bu bind install [--browser chrome|chromium|both] | status | remove
|
|
16
|
+
fde2e-bu status | doctor | update
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## bind —— 扩展↔CLI 一次性配对(本机,零网络;macOS/Linux)
|
|
20
|
+
|
|
21
|
+
`bind install` 写入 Chrome Native Messaging host 清单(`com.fde2e.bu`,stdio 型;
|
|
22
|
+
allowed_origins 锚定扩展固定 ID `meojillndgamkbfjijfehjaefolnfopn`)。之后在扩展
|
|
23
|
+
options 面板点「CLI 配对」,扩展经本机 host 把绑定清单
|
|
24
|
+
(profileId/角色/名称/omni server——非敏感标识,不含 token/凭据;单条 ≤50,
|
|
25
|
+
超出如实回执截断数)写入 `~/.config/fde2e-bu/pairing.json`(0600)。
|
|
26
|
+
**配对后 pairing 即成为 `BU_BRIDGE_PROFILE_ID` 的本机缺省来源**:`mcp serve`
|
|
27
|
+
(stdio 与 --http)在环境变量未设置时自动回退到配对的缺省 profileId(优先
|
|
28
|
+
master),并在 stderr 提示一行。`bind status`/`doctor` 展示配对状态并校验
|
|
29
|
+
host 脚本在位可执行。目录映射:macOS=`~/Library/Application Support/…`、
|
|
30
|
+
Linux=`~/.config/google-chrome|chromium/…`;Windows(注册表写入)待后续版本。
|
|
31
|
+
|
|
32
|
+
## mcp serve --http —— 回环 JSON-RPC(脚本集成用)
|
|
33
|
+
|
|
34
|
+
- **只绑 127.0.0.1**(缺省端口 8791,`--port 0` 临时端口)。
|
|
35
|
+
- 三重门:Host 头白名单(127.0.0.1/localhost/`[::1]`,防 DNS rebinding)+
|
|
36
|
+
Origin 校验(在场必须回环源)+ Bearer token(scheme 大小写不敏感,token 精确
|
|
37
|
+
比较;`--token` / `BU_MCP_TOKEN` / `~/.config/fde2e-bu/token` 自动生成 0600)。
|
|
38
|
+
token 打印策略:仅**首次自动生成**时打完整值;来自参数/env/既有文件只显示
|
|
39
|
+
尾 4 位与来源。空 `--token ''` 直接用法错误。
|
|
40
|
+
- 端点:`POST /` 与 `POST /mcp`(单条 JSON-RPC,与 stdio 同一处理核;批量数组
|
|
41
|
+
-32600;通知 204;body >4MB → 413);`GET /healthz` 免 token 探活。
|
|
42
|
+
- 注意:这是**受 token 保护的单端点 JSON-RPC**,不是 MCP streamable-HTTP 全规范
|
|
43
|
+
(无 SSE/会话)——面向脚本与内部集成;MCP 客户端请继续用 stdio。
|
|
44
|
+
|
|
45
|
+
## 其余能力(沿用)
|
|
46
|
+
|
|
47
|
+
- **零运行时依赖**:`util.parseArgs` + 手写 stdio JSON-RPC;`npx` 直跑免安装。
|
|
48
|
+
- **MCP 工具面**(9 工具):bu_open/bu_click/bu_fill/bu_read/bu_screenshot 六意图级
|
|
49
|
+
+窗口三件套+bu_cmd 透传;schema 源=`plugins/contracts/browser-bridge/commands.json`
|
|
50
|
+
(构建期快照内嵌+双对表测试);bu_cmd 枚举=云端可执行 21 条(7 条保留命令本地即拒)。
|
|
51
|
+
服务端按 inputSchema 校验全部输入(MCP 规范 MUST)。
|
|
52
|
+
- **传输**:`POST {BU_BRIDGE_SERVER}/api/cmd`(默认 `http://127.0.0.1:8788`);
|
|
53
|
+
`BU_BRIDGE_PROFILE_ID` 必配;`BU_INTERNAL_TOKEN`/`BU_BOUND_PROFILE_ID` 认证头
|
|
54
|
+
**仅向回环地址发送**。
|
|
55
|
+
- **skills**:agentskills.io 事实标准(`<skill>/SKILL.md`);默认写 `.agents/skills/`,
|
|
56
|
+
兼容 `.claude/skills`。
|
|
57
|
+
- **配置写入**:`mcp add/remove` 与 pairing/token 一律原子写(同目录 temp → rename),
|
|
58
|
+
落盘权限统一 **0600**(含 env 中的 profileId/令牌;既有文件也会被收紧);
|
|
59
|
+
例外:`bind install` 生成的 `nm-host.sh` 为可执行脚本(0755)。
|
|
60
|
+
- **flag 面**:各子命令只接受自述范围内的 flag/位置参数,多余项一律用法错误 exit 2
|
|
61
|
+
(不静默忽略,例如 `mcp list --profile-id x` 会被拒绝)。
|
|
62
|
+
|
|
63
|
+
设计依据:`docs/research/cli-design.md`(Claude Code/Codex/Gemini/MCP 生态调研);
|
|
64
|
+
变更轨迹:openspec `bu-cli` / `bu-p2-hardening` / `bu-bind-http-publish`。
|
|
65
|
+
License: 私有(FDE2E / 昭智羽名)。
|
package/bin.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// fde2e-bu CLI 入口(openspec change bu-cli task 1.1)。
|
|
3
|
+
// 零运行时依赖:参数解析用 Node 原生 util.parseArgs(调研 D 节裁量记录见
|
|
4
|
+
// src/cli.mjs 头注),MCP 走手写最小 stdio JSON-RPC 2.0(src/mcp/serve.mjs)。
|
|
5
|
+
import { mainSafe } from './src/cli.mjs';
|
|
6
|
+
|
|
7
|
+
mainSafe(process.argv.slice(2)).then(
|
|
8
|
+
(code) => process.exit(code ?? 0),
|
|
9
|
+
(err) => {
|
|
10
|
+
console.error(`fde2e-bu: ${err?.message ?? err}`);
|
|
11
|
+
process.exit(1);
|
|
12
|
+
},
|
|
13
|
+
);
|
package/package.json
CHANGED
|
@@ -1,9 +1,33 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fde2e-bu",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "
|
|
5
|
-
"
|
|
3
|
+
"version": "1.3.0",
|
|
4
|
+
"description": "FDE2E Browser Use CLI —— skills 管理+MCP server+扩展配对(零运行时依赖)",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"fde2e-bu": "bin.js"
|
|
8
|
+
},
|
|
9
|
+
"engines": {
|
|
10
|
+
"node": ">=22"
|
|
11
|
+
},
|
|
12
|
+
"publishConfig": {
|
|
13
|
+
"access": "public"
|
|
14
|
+
},
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "git+https://github.com/jixoai/fde2e-bu.git",
|
|
18
|
+
"directory": "cli"
|
|
19
|
+
},
|
|
20
|
+
"bugs": {
|
|
21
|
+
"url": "https://github.com/jixoai/fde2e-bu/issues"
|
|
22
|
+
},
|
|
23
|
+
"homepage": "https://www.fde2e.com/downloads/",
|
|
6
24
|
"files": [
|
|
7
|
-
"
|
|
8
|
-
|
|
9
|
-
|
|
25
|
+
"bin.js",
|
|
26
|
+
"src/",
|
|
27
|
+
"README.md"
|
|
28
|
+
],
|
|
29
|
+
"scripts": {
|
|
30
|
+
"test": "node --test test/*.test.mjs"
|
|
31
|
+
},
|
|
32
|
+
"license": "UNLICENSED"
|
|
33
|
+
}
|
package/src/args.mjs
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
// 参数解析底座(task 1.1):统一 util.parseArgs 包装+用法错误通道。
|
|
2
|
+
// 选型记录(调研 D 节裁量):零运行时依赖原则下选 Node 原生 util.parseArgs——
|
|
3
|
+
// 子命令树浅(6 组 × ≤4 子命令),原生能力足够;commander/yargs 各带传递依赖,
|
|
4
|
+
// 对「npx 首跑零安装」的分发目标是纯负担。strict:true 未知 flag 直接报用法错。
|
|
5
|
+
import { parseArgs } from 'node:util';
|
|
6
|
+
|
|
7
|
+
/** 用法/参数错误:exit code 固定 2,可携带对应 help 文本一起打印。 */
|
|
8
|
+
export class UsageError extends Error {
|
|
9
|
+
/**
|
|
10
|
+
* @param {string} message
|
|
11
|
+
* @param {{ help?: string, code?: number }} [meta]
|
|
12
|
+
*/
|
|
13
|
+
constructor(message, meta = {}) {
|
|
14
|
+
super(message);
|
|
15
|
+
this.name = 'UsageError';
|
|
16
|
+
this.exitCode = meta.code ?? 2;
|
|
17
|
+
this.help = meta.help;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* 解析子命令参数(strict;allowPositionals)。
|
|
23
|
+
* @param {string[]} args 位置参数之后的原始 argv 片段
|
|
24
|
+
* @param {{ options?: object, help?: string }} [meta]
|
|
25
|
+
* @returns {{ positionals: string[], values: Record<string, unknown> }}
|
|
26
|
+
*/
|
|
27
|
+
export function parse(args, meta = {}) {
|
|
28
|
+
try {
|
|
29
|
+
return parseArgs({
|
|
30
|
+
args,
|
|
31
|
+
options: meta.options ?? {},
|
|
32
|
+
allowPositionals: true,
|
|
33
|
+
strict: true,
|
|
34
|
+
});
|
|
35
|
+
} catch (err) {
|
|
36
|
+
throw new UsageError(err.message, { help: meta.help });
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** 逗号分隔列表 → 去空数组("--client claude,codex" → ['claude','codex'])。 */
|
|
41
|
+
export function csvList(value) {
|
|
42
|
+
if (typeof value !== 'string' || value.trim() === '') return [];
|
|
43
|
+
return value.split(',').map((s) => s.trim()).filter(Boolean);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// ---------- C1 flag 面收窄(P2-6):不适用 flag 一律用法错误,不静默忽略 ----------
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* 取 argv 片段里出现的 flag 名(长名去 `--`,短名去 `-`;`--k=v` 取 k)。
|
|
50
|
+
* `--` 之后全部视为位置参数(不当作 flag)。
|
|
51
|
+
* @param {string[]} args
|
|
52
|
+
* @returns {string[]}
|
|
53
|
+
*/
|
|
54
|
+
export function flagNames(args) {
|
|
55
|
+
const names = [];
|
|
56
|
+
let positionalOnly = false;
|
|
57
|
+
for (const tok of args) {
|
|
58
|
+
if (positionalOnly) continue;
|
|
59
|
+
if (tok === '--') { positionalOnly = true; continue; }
|
|
60
|
+
if (tok === '-' || !tok.startsWith('-')) continue;
|
|
61
|
+
names.push(tok.replace(/^--?/, '').split('=')[0]);
|
|
62
|
+
}
|
|
63
|
+
return names;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* flag 面校验:本子命令不接受的 flag → UsageError exit 2。
|
|
68
|
+
* parseArgs 的 strict 只拒「全局未知」,这里拒「对子命令无意义」(如
|
|
69
|
+
* `mcp list --profile-id`)——静默忽略是最难排查的一类 CLI 缺陷(P2-6)。
|
|
70
|
+
* @param {string[]} args 位置参数之后的 argv 片段
|
|
71
|
+
* @param {string[]} allowed 允许的 flag 名(长名不含 `--`;'help' 自动兼容 -h)
|
|
72
|
+
* @param {string} label 子命令标签(如 `mcp list`)
|
|
73
|
+
* @param {{ help?: string }} [meta]
|
|
74
|
+
*/
|
|
75
|
+
export function assertFlags(args, allowed, label, meta = {}) {
|
|
76
|
+
const allow = new Set(allowed);
|
|
77
|
+
const list = allowed.length > 0 ? allowed.map((n) => (n === 'help' ? '--help/-h' : `--${n}`)).join(' ') : '(无 flag)';
|
|
78
|
+
for (const name of flagNames(args)) {
|
|
79
|
+
// -h 是 --help 的唯一短旗标(与各组 options 的 short:'h' 对应)
|
|
80
|
+
const key = name === 'h' && allow.has('help') ? 'help' : name;
|
|
81
|
+
if (!allow.has(key)) {
|
|
82
|
+
throw new UsageError(`${label} 不支持 flag:${name.length > 1 ? `--${name}` : `-${name}`}(支持:${list})`, { help: meta.help });
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** 不接受位置参数的子命令:多余位置参数 → UsageError exit 2。 */
|
|
88
|
+
export function assertNoPositionals(positionals, label, meta = {}) {
|
|
89
|
+
if (positionals.length > 0) {
|
|
90
|
+
throw new UsageError(`${label} 不接受位置参数(收到:${positionals.join(' ')})`, { help: meta.help });
|
|
91
|
+
}
|
|
92
|
+
}
|
package/src/cli.mjs
ADDED
|
@@ -0,0 +1,445 @@
|
|
|
1
|
+
// fde2e-bu 子命令分发(task 1.1)。
|
|
2
|
+
//
|
|
3
|
+
// 设计决策(零依赖,记录于 change bu-cli):
|
|
4
|
+
// - 参数解析=Node 原生 util.parseArgs(理由见 src/args.mjs 头注:npx 首跑零安装,
|
|
5
|
+
// 子命令树浅不需要 commander 的能力面)。
|
|
6
|
+
// - MCP SDK(@modelcontextprotocol/sdk)不引:本 CLI 只做 server 侧最小面
|
|
7
|
+
// (initialize / tools/list / tools/call / ping,stdio JSON-RPC 2.0,MCP 规范
|
|
8
|
+
// 2025-06-18),手写实现约一百余行且行为完全可审计;SDK 带整棵传输/协议类树。
|
|
9
|
+
// - 子命令=两级(group → sub);每组自带 help;未知组/子命令报用法错 exit 2。
|
|
10
|
+
import { UsageError, parse, assertFlags, assertNoPositionals } from './args.mjs';
|
|
11
|
+
import { cmdSkills } from './skills/skills.mjs';
|
|
12
|
+
import { cmdMcp } from './mcp/config-cli.mjs';
|
|
13
|
+
import { runServe } from './mcp/serve.mjs';
|
|
14
|
+
import { runHttpServe, DEFAULT_HTTP_PORT } from './mcp/http.mjs';
|
|
15
|
+
import { runNmHost } from './nm/host.mjs';
|
|
16
|
+
import { cmdBindInstall, cmdBindStatus, cmdBindRemove, assertBindPlatform } from './nm/bind.mjs';
|
|
17
|
+
import { cmdSelfUpdate, DEFAULT_FEED_URL } from './nm/update.mjs';
|
|
18
|
+
import { resolveConfigDir } from './configdir.mjs';
|
|
19
|
+
import { cmdStatus, cmdDoctor } from './misc.mjs';
|
|
20
|
+
import { readFileSync } from 'node:fs';
|
|
21
|
+
import { fileURLToPath } from 'node:url';
|
|
22
|
+
import { dirname, join } from 'node:path';
|
|
23
|
+
|
|
24
|
+
const pkgRoot = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
25
|
+
|
|
26
|
+
const VERSION_HELP = `fde2e-bu ${pkgVersion()} —— FDE2E Browser Use CLI(skills 管理+MCP server)`;
|
|
27
|
+
|
|
28
|
+
const MAIN_HELP = `fde2e-bu —— FDE2E Browser Use 命令行(零运行时依赖)
|
|
29
|
+
|
|
30
|
+
用法:fde2e-bu <group> [sub] [options]
|
|
31
|
+
|
|
32
|
+
组:
|
|
33
|
+
skills 管理技能包(agentskills.io 事实标准;默认写 .agents/skills,兼容 .claude/skills)
|
|
34
|
+
list | add | remove | validate
|
|
35
|
+
mcp MCP server 与三家客户端配置
|
|
36
|
+
serve(stdio JSON-RPC 2.0;--http=仅回环 token 端点)| add | list | remove
|
|
37
|
+
status 桥服务(omni /healthz)健康探测,离线降级提示;顶部显示已配对执行器
|
|
38
|
+
doctor 自检:node 版本 / 配置路径 / 环境变量 / 扩展 zip / NM manifest / pairing
|
|
39
|
+
update 扩展自更新:发现 unpacked 安装 → feed 检查 → 下载校验原子替换
|
|
40
|
+
[--ext-dir <dir>] [--feed <url>] [--dry-run]
|
|
41
|
+
bind 扩展↔CLI 一次性配对(Chrome Native Messaging)
|
|
42
|
+
install | status | remove
|
|
43
|
+
|
|
44
|
+
通用:
|
|
45
|
+
--version 打印版本
|
|
46
|
+
--help, -h 本帮助;各组/子命令同 flag 打对应帮助
|
|
47
|
+
|
|
48
|
+
环境变量(mcp serve / status / doctor 读取):
|
|
49
|
+
BU_BRIDGE_SERVER 桥服务地址(缺省 http://127.0.0.1:8788)
|
|
50
|
+
BU_BRIDGE_PROFILE_ID 执行器 profileId(tools/call 必需)
|
|
51
|
+
BU_INTERNAL_TOKEN 内部令牌(回环来源下映射请求头 x-omni-internal)
|
|
52
|
+
BU_BOUND_PROFILE_ID 会话绑定 profileId(映射请求头 x-omni-bound-profile)
|
|
53
|
+
BU_CONFIG_DIR 配置根重定向(缺省 ~/.config/fde2e-bu;pairing/token/nm-host.sh)
|
|
54
|
+
BU_MCP_TOKEN mcp serve --http 的 token(优先级 --token > 此变量 > 配置根 token 文件)
|
|
55
|
+
|
|
56
|
+
文档:openspec/changes/bu-bind-http-publish(本能力)、docs/research/cli-design.md(设计调研)`;
|
|
57
|
+
|
|
58
|
+
const SKILLS_HELP = `fde2e-bu skills —— 技能包管理(agentskills.io 事实标准)
|
|
59
|
+
|
|
60
|
+
用法:
|
|
61
|
+
fde2e-bu skills list [--json] [--root <dir>]
|
|
62
|
+
扫描 <dir>(缺省 cwd)向上找最近的 .agents/skills 与 .claude/skills,
|
|
63
|
+
渐进披露(只读 SKILL.md frontmatter 的 name/description/version)表格输出。
|
|
64
|
+
fde2e-bu skills add <src> [--skill <subdir>] [--dest agents|claude] [--force]
|
|
65
|
+
源=本地技能包目录或 git URL(--depth 1 浅拷贝;--skill 指定仓库内子目录,
|
|
66
|
+
不得越出克隆根)。装前先过 validate;同名已装默认提示退出(--force 覆盖重装,
|
|
67
|
+
先拷后换不丢既有安装;源即目标的自装请求拒绝)。
|
|
68
|
+
fde2e-bu skills remove <name> [--dest agents|claude] [--all]
|
|
69
|
+
删除已装技能;未装时幂等提示(--all 同时清两处目录)。
|
|
70
|
+
技能名须为小写字母数字连字符——含 ../分隔符/绝对路径一律拒绝(exit 1)。
|
|
71
|
+
fde2e-bu skills validate [<path>]
|
|
72
|
+
校验技能包(缺省=扫描到的全部技能):name ≤64 小写连字符且=目录名、
|
|
73
|
+
description ≤1024、SKILL.md frontmatter 可解析。`;
|
|
74
|
+
|
|
75
|
+
const MCP_HELP = `fde2e-bu mcp —— MCP server 与客户端配置
|
|
76
|
+
|
|
77
|
+
用法:
|
|
78
|
+
fde2e-bu mcp serve [--schema-only]
|
|
79
|
+
stdio JSON-RPC 2.0 MCP server(MCP 规范 2025-06-18):
|
|
80
|
+
initialize / tools/list / tools/call / ping。
|
|
81
|
+
工具面=bu_open/bu_click/bu_fill/bu_read/bu_screenshot/bu_cmd(透传)+
|
|
82
|
+
bu_window_list/bu_window_create/bu_window_close(schema 源=契约层
|
|
83
|
+
commands.json 快照,见 src/mcp/schema.mjs)。
|
|
84
|
+
execute 经 POST {BU_BRIDGE_SERVER|http://127.0.0.1:8788}/api/cmd。
|
|
85
|
+
--schema-only 只打印工具清单 JSON(不自起服务)。
|
|
86
|
+
fde2e-bu mcp serve --http [--port N] [--token <t>]
|
|
87
|
+
HTTP 端点(面向脚本/内部集成;非 MCP streamable-HTTP 全规范,stdio 仍是
|
|
88
|
+
MCP 客户端主通道):
|
|
89
|
+
只 listen 127.0.0.1(缺省端口 ${DEFAULT_HTTP_PORT};--port 0=临时端口);
|
|
90
|
+
POST / 与 /mcp 单条 JSON-RPC(与 stdio 同一处理核;通知 204;
|
|
91
|
+
数组 -32600;解析失败 -32700);GET /healthz 免 token 探活。
|
|
92
|
+
安全三重门:Host 头白名单(127.0.0.1/localhost/[::1],防 DNS rebinding)+
|
|
93
|
+
Origin 校验(在场必须 http://127.0.0.1[:port]/http://localhost[:port],无 Origin
|
|
94
|
+
放行)+Bearer token(401)。token 解析顺序:--token > BU_MCP_TOKEN >
|
|
95
|
+
~/.config/fde2e-bu/token(无则生成 32B hex 落盘 0600;BU_CONFIG_DIR
|
|
96
|
+
可重定向)。启动打印监听地址与 token 到 stderr。SIGINT/SIGTERM 优雅关闭。
|
|
97
|
+
--http 与 --schema-only 互斥(exit 2)。
|
|
98
|
+
fde2e-bu mcp add [--client claude,codex,gemini] [--scope user|project|local]
|
|
99
|
+
[--home <dir>] [--dir <dir>] [--server <url>]
|
|
100
|
+
[--profile-id <id>] [--cmd <bin>]
|
|
101
|
+
写三家 MCP 客户端配置(缺省全三家)。scope 语义(r1 如实化):
|
|
102
|
+
claude user→~/.claude.json(顶层 mcpServers);project→.mcp.json;
|
|
103
|
+
local→~/.claude.json 的 projects[<cwd>].mcpServers(cwd=--dir 或当前目录)
|
|
104
|
+
codex 仅 user(~/.codex/config.toml);--scope project 降级写 user(打印提示);
|
|
105
|
+
local 不支持(跳过并提示)
|
|
106
|
+
gemini user→~/.gemini/settings.json;project→.gemini/settings.json;
|
|
107
|
+
local 不支持(跳过并提示)
|
|
108
|
+
fde2e-bu mcp list [--client claude,codex,gemini] [--home <dir>] [--dir <dir>]
|
|
109
|
+
打印六落点配置状态与注册形状(env 值脱敏:***尾4/已配置,不回显完整令牌)。
|
|
110
|
+
fde2e-bu mcp remove [--client ...] [--scope user|project|local] [--home <dir>] [--dir <dir>]
|
|
111
|
+
移除注册(--scope 收窄层级,缺省全清);未注册时幂等提示。
|
|
112
|
+
|
|
113
|
+
flag 面按子命令收窄(C1):不适用 flag 与多余位置参数一律用法错误 exit 2
|
|
114
|
+
(如 mcp list --profile-id x 不再被静默忽略);--port/--token 仅 --http 模式可用。
|
|
115
|
+
--home/--dir 显式重定向配置根与工作区根(测试用,不碰真实家目录)。`;
|
|
116
|
+
|
|
117
|
+
const UPDATE_HELP = `fde2e-bu update —— 扩展自更新(unpacked 独立安装)
|
|
118
|
+
|
|
119
|
+
用法:
|
|
120
|
+
fde2e-bu update [--ext-dir <dir>] [--feed <url>] [--dry-run]
|
|
121
|
+
发现本机 unpacked 安装(Chrome 系浏览器 Preferences/Secure Preferences 的
|
|
122
|
+
extensions.settings[meojillndgamkbfjijfehjaefolnfopn].path,多浏览器多
|
|
123
|
+
profile 全扫)→ fetch feed(缺省 ${DEFAULT_FEED_URL})→ semver 只认 > →
|
|
124
|
+
下载(https 强制)→ sha256/字节数双校验 → staging 解压(zip-slip/条目数/
|
|
125
|
+
根 manifest/总量四重防护)→ rm+rename 原子替换(Windows 瞬态占用退避)。
|
|
126
|
+
多处安装全部应用;任一失败不回滚其它。受 FDE2E Omni 桌面版管理的安装
|
|
127
|
+
(~/fde2e/bu/ 内或目录旁 .browser-extension-state.json)自动跳过——随 App 更新。
|
|
128
|
+
--ext-dir <dir> 显式指定安装目录(复核 manifest.json 在场即可,免扫描)
|
|
129
|
+
--feed <url> 覆写 feed 地址(测试/私有部署;仍强制 https)
|
|
130
|
+
--dry-run 只报发现与目标版本,不写盘
|
|
131
|
+
0 命中 → exit 1+指引(浏览器未开过本插件/装在非常规处时用 --ext-dir)。
|
|
132
|
+
CLI 本体的更新仍走 npm:npm i -g fde2e-bu@latest。
|
|
133
|
+
|
|
134
|
+
环境变量(NM host 路径也生效):
|
|
135
|
+
BU_UPDATE_BASE 下载基址/白名单重定向(测试假 feed server/私有部署;
|
|
136
|
+
信任重定向到指定基址,不是放宽为任意域)
|
|
137
|
+
|
|
138
|
+
设计:openspec/changes/bu-online-selfupdate(proposal/design C)。`;
|
|
139
|
+
|
|
140
|
+
const BIND_HELP = `fde2e-bu bind —— 扩展↔CLI 一次性配对(Chrome Native Messaging)
|
|
141
|
+
|
|
142
|
+
平台:支持 macOS 与 Linux(NM 清单按 Chrome 规范路径文件落盘);Windows 需注册表
|
|
143
|
+
写入,待后续——非受支持平台执行 install/status/remove 一律用法错误 exit 2。
|
|
144
|
+
|
|
145
|
+
用法:
|
|
146
|
+
fde2e-bu bind install [--browser chrome|chromium|both] [--manifests-dir <dir>]
|
|
147
|
+
[--config-dir <dir>]
|
|
148
|
+
写 Native Messaging host 落盘物(幂等覆盖):
|
|
149
|
+
<configDir>/nm-host.sh(0755)——exec env BU_CONFIG_DIR='<configDir>'
|
|
150
|
+
<node> <cli/bin.js> __nm-host 的包装(路径单引号转义)
|
|
151
|
+
(NM 要求 path 是单个可执行文件,不能内嵌参数);
|
|
152
|
+
浏览器 NM 目录 com.fde2e.bu.json——type:stdio,
|
|
153
|
+
allowed_origins=chrome-extension://meojillndgamkbfjijfehjaefolnfopn/
|
|
154
|
+
(扩展固定 key 派生的稳定 ID)。
|
|
155
|
+
目录(macOS):Chrome=~/Library/Application Support/Google/Chrome/
|
|
156
|
+
NativeMessagingHosts;Chromium=.../Chromium/NativeMessagingHosts。
|
|
157
|
+
--browser 缺省 chrome;both 写两处。
|
|
158
|
+
--manifests-dir 重定向 manifest 落点、--config-dir 重定向配置根
|
|
159
|
+
(等价 BU_CONFIG_DIR;测试/沙箱用)。装完打印下一步指引。
|
|
160
|
+
fde2e-bu bind status [--manifests-dir <dir>] [--config-dir <dir>]
|
|
161
|
+
manifest 各落点在否/内容摘要+nm-host.sh 可执行位+pairing 摘要
|
|
162
|
+
(profileId/role;缺省 profileId 优先 master)。无任何 manifest 时 exit 1。
|
|
163
|
+
fde2e-bu bind remove [--purge] [--manifests-dir <dir>] [--config-dir <dir>]
|
|
164
|
+
删 manifest(Chrome/Chromium/重定向落点全查,幂等);--purge 连 pairing.json
|
|
165
|
+
一起删。nm-host.sh 保留(重装即用;彻底清理手动删配置根)。
|
|
166
|
+
|
|
167
|
+
隐蔽子命令:fde2e-bu __nm-host(由 nm-host.sh/Chrome 启动,不在主 help)
|
|
168
|
+
——NM 帧协议 host:4 字节小端长度+UTF-8 JSON(单帧 ≤1MB),处理
|
|
169
|
+
{type:'bu:pair', bindings:[{profileId,role,name?,server?}]} → 原子写
|
|
170
|
+
<configDir>/pairing.json(0600)→ 应答 {type:'bu:pair:ok', saved:N,
|
|
171
|
+
received:M, truncated:bool}(截断如实回执);未知/畸形回 bu:pair:error
|
|
172
|
+
不崩;stdin EOF 退出 0;日志仅 stderr。
|
|
173
|
+
|
|
174
|
+
设计:openspec/changes/bu-bind-http-publish(proposal/design §1)。`;
|
|
175
|
+
|
|
176
|
+
function pkgVersion() {
|
|
177
|
+
return JSON.parse(readFileSync(join(pkgRoot, 'package.json'), 'utf8')).version;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** 顶层 --version / --help / -h。只在首参是 flag(或空 argv)时生效——组级
|
|
181
|
+
* --help(如 `skills --help`)必须落到组分支打组帮助,不得被这里抢走。 */
|
|
182
|
+
function handleGlobalFlags(argv) {
|
|
183
|
+
if (argv.length === 0) {
|
|
184
|
+
console.log(MAIN_HELP);
|
|
185
|
+
return 2; // 裸调用算用法错
|
|
186
|
+
}
|
|
187
|
+
if (!argv[0].startsWith('-')) return null; // 有组名 → 组分发
|
|
188
|
+
if (argv.includes('--version')) {
|
|
189
|
+
console.log(VERSION_HELP);
|
|
190
|
+
return 0;
|
|
191
|
+
}
|
|
192
|
+
if (argv.includes('--help') || argv.includes('-h')) {
|
|
193
|
+
console.log(MAIN_HELP);
|
|
194
|
+
return 0;
|
|
195
|
+
}
|
|
196
|
+
return null; // 未知 flag 落组分发报未知组(argv[0] 当组名)
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** 组级 help 探测:组名后紧跟 --help/-h(或组后无参)。 */
|
|
200
|
+
function wantsHelp(rest, hasRest) {
|
|
201
|
+
return rest.includes('--help') || rest.includes('-h') || !hasRest;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* CLI 主入口。
|
|
206
|
+
* @param {string[]} argv process.argv.slice(2)
|
|
207
|
+
* @returns {Promise<number>} exit code
|
|
208
|
+
*/
|
|
209
|
+
export async function main(argv) {
|
|
210
|
+
const globalExit = handleGlobalFlags(argv);
|
|
211
|
+
if (globalExit !== null) return globalExit;
|
|
212
|
+
|
|
213
|
+
const [group, ...rest] = argv;
|
|
214
|
+
|
|
215
|
+
// A2:隐蔽子命令 __nm-host——Chrome NM host(nm-host.sh exec 启动),不在主 help。
|
|
216
|
+
// 不接受任何参数(参数只能来自 NM stdio 流,命令行多参即误用)。
|
|
217
|
+
if (group === '__nm-host') {
|
|
218
|
+
if (rest.length > 0) {
|
|
219
|
+
throw new UsageError('__nm-host 不接受命令行参数(由 Chrome Native Messaging 经 nm-host.sh 启动)', { help: BIND_HELP });
|
|
220
|
+
}
|
|
221
|
+
return runNmHost();
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
switch (group) {
|
|
225
|
+
case 'skills': {
|
|
226
|
+
if (wantsHelp(rest, rest.length > 0)) {
|
|
227
|
+
console.log(SKILLS_HELP);
|
|
228
|
+
return rest.length > 0 ? 0 : 2;
|
|
229
|
+
}
|
|
230
|
+
return cmdSkills(rest);
|
|
231
|
+
}
|
|
232
|
+
case 'mcp': {
|
|
233
|
+
if (wantsHelp(rest, rest.length > 0)) {
|
|
234
|
+
console.log(MCP_HELP);
|
|
235
|
+
return rest.length > 0 ? 0 : 2;
|
|
236
|
+
}
|
|
237
|
+
const [sub, ...subRest] = rest;
|
|
238
|
+
if (sub === 'serve') {
|
|
239
|
+
// C1+A4:--schema-only/--http/--port/--token;--port/--token 仅 --http 模式可用
|
|
240
|
+
assertFlags(subRest, ['schema-only', 'http', 'port', 'token', 'help'], 'mcp serve', { help: MCP_HELP });
|
|
241
|
+
const { positionals, values } = parse(subRest, {
|
|
242
|
+
options: {
|
|
243
|
+
'schema-only': { type: 'boolean' },
|
|
244
|
+
http: { type: 'boolean' },
|
|
245
|
+
port: { type: 'string' }, // 手动校验(0~65535 整数;parseArgs 的 number 不拒 NaN)
|
|
246
|
+
token: { type: 'string' },
|
|
247
|
+
help: { type: 'boolean', short: 'h' },
|
|
248
|
+
},
|
|
249
|
+
help: MCP_HELP,
|
|
250
|
+
});
|
|
251
|
+
if (values.help) {
|
|
252
|
+
console.log(MCP_HELP);
|
|
253
|
+
return 0;
|
|
254
|
+
}
|
|
255
|
+
assertNoPositionals(positionals, 'mcp serve', { help: MCP_HELP });
|
|
256
|
+
if (values.http && values['schema-only']) {
|
|
257
|
+
// 互斥(A4):--schema-only 打印即退、--http 常驻服务,语义不可同给
|
|
258
|
+
throw new UsageError('--http 与 --schema-only 互斥(前者常驻 HTTP 服务,后者打印工具清单即退)', { help: MCP_HELP });
|
|
259
|
+
}
|
|
260
|
+
if (!values.http && (values.port !== undefined || values.token !== undefined)) {
|
|
261
|
+
throw new UsageError('--port/--token 仅在 mcp serve --http 模式下可用', { help: MCP_HELP });
|
|
262
|
+
}
|
|
263
|
+
let port = DEFAULT_HTTP_PORT;
|
|
264
|
+
if (values.port !== undefined) {
|
|
265
|
+
port = Number(values.port);
|
|
266
|
+
if (!Number.isInteger(port) || port < 0 || port > 65535) {
|
|
267
|
+
throw new UsageError(`--port 须为 0~65535 整数(0=临时端口;收到 ${values.port})`, { help: MCP_HELP });
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
// P2-12:显式空 --token 一律拒绝('' 会被 resolveHttpToken 静默降级到下一来源,
|
|
271
|
+
// 用户以为设了 token 实则没有——最危险的静默失败面)
|
|
272
|
+
if (values.token !== undefined && values.token === '') {
|
|
273
|
+
throw new UsageError('--token 不能为空(显式置空会被静默降级到 env/文件来源——要随机 token 请省略该 flag)', { help: MCP_HELP });
|
|
274
|
+
}
|
|
275
|
+
if (values.http) {
|
|
276
|
+
return runHttpServe({ port, token: values.token ? String(values.token) : undefined });
|
|
277
|
+
}
|
|
278
|
+
return runServe({ schemaOnly: Boolean(values['schema-only']) });
|
|
279
|
+
}
|
|
280
|
+
return cmdMcp(sub, subRest, MCP_HELP);
|
|
281
|
+
}
|
|
282
|
+
case 'status': {
|
|
283
|
+
// C1 + C2 + A5:flags=server/home/dir/json/config-dir/help;多余位置参数 exit 2
|
|
284
|
+
assertFlags(rest, ['server', 'home', 'dir', 'json', 'config-dir', 'help'], 'status', { help: MAIN_HELP });
|
|
285
|
+
const { positionals, values } = parse(rest, {
|
|
286
|
+
options: {
|
|
287
|
+
server: { type: 'string' },
|
|
288
|
+
home: { type: 'string' },
|
|
289
|
+
dir: { type: 'string' },
|
|
290
|
+
json: { type: 'boolean' },
|
|
291
|
+
'config-dir': { type: 'string' },
|
|
292
|
+
help: { type: 'boolean', short: 'h' },
|
|
293
|
+
},
|
|
294
|
+
help: MAIN_HELP,
|
|
295
|
+
});
|
|
296
|
+
if (values.help) {
|
|
297
|
+
console.log(MAIN_HELP);
|
|
298
|
+
return 0;
|
|
299
|
+
}
|
|
300
|
+
assertNoPositionals(positionals, 'status', { help: MAIN_HELP });
|
|
301
|
+
return cmdStatus({ server: values.server, home: values.home, dir: values.dir, json: values.json, configDir: values['config-dir'] });
|
|
302
|
+
}
|
|
303
|
+
case 'doctor': {
|
|
304
|
+
assertFlags(rest, ['home', 'dir', 'json', 'config-dir', 'manifests-dir', 'help'], 'doctor', { help: MAIN_HELP });
|
|
305
|
+
const { positionals, values } = parse(rest, {
|
|
306
|
+
options: {
|
|
307
|
+
home: { type: 'string' },
|
|
308
|
+
dir: { type: 'string' },
|
|
309
|
+
json: { type: 'boolean' },
|
|
310
|
+
'config-dir': { type: 'string' },
|
|
311
|
+
'manifests-dir': { type: 'string' },
|
|
312
|
+
help: { type: 'boolean', short: 'h' },
|
|
313
|
+
},
|
|
314
|
+
help: MAIN_HELP,
|
|
315
|
+
});
|
|
316
|
+
if (values.help) {
|
|
317
|
+
console.log(MAIN_HELP);
|
|
318
|
+
return 0;
|
|
319
|
+
}
|
|
320
|
+
assertNoPositionals(positionals, 'doctor', { help: MAIN_HELP });
|
|
321
|
+
return cmdDoctor({ home: values.home, dir: values.dir, json: values.json, configDir: values['config-dir'], manifestsDir: values['manifests-dir'] });
|
|
322
|
+
}
|
|
323
|
+
case 'update': {
|
|
324
|
+
// A3(bu-online-selfupdate):--ext-dir/--feed/--dry-run;不适用 flag 与多余
|
|
325
|
+
// 位置参数一律 exit 2(cli-tooling 规格);--feed 在参数面即强制 https。
|
|
326
|
+
assertFlags(rest, ['ext-dir', 'feed', 'dry-run', 'help'], 'update', { help: UPDATE_HELP });
|
|
327
|
+
const { positionals, values } = parse(rest, {
|
|
328
|
+
options: {
|
|
329
|
+
'ext-dir': { type: 'string' },
|
|
330
|
+
feed: { type: 'string' },
|
|
331
|
+
'dry-run': { type: 'boolean' },
|
|
332
|
+
help: { type: 'boolean', short: 'h' },
|
|
333
|
+
},
|
|
334
|
+
help: UPDATE_HELP,
|
|
335
|
+
});
|
|
336
|
+
if (values.help) {
|
|
337
|
+
console.log(UPDATE_HELP);
|
|
338
|
+
return 0;
|
|
339
|
+
}
|
|
340
|
+
assertNoPositionals(positionals, 'update', { help: UPDATE_HELP });
|
|
341
|
+
if (values['ext-dir'] !== undefined && values['ext-dir'] === '') {
|
|
342
|
+
throw new UsageError('--ext-dir 不能为空', { help: UPDATE_HELP });
|
|
343
|
+
}
|
|
344
|
+
if (values.feed !== undefined) {
|
|
345
|
+
let u;
|
|
346
|
+
try {
|
|
347
|
+
u = new URL(values.feed);
|
|
348
|
+
} catch {
|
|
349
|
+
throw new UsageError(`--feed url 无法解析:${values.feed}`, { help: UPDATE_HELP });
|
|
350
|
+
}
|
|
351
|
+
if (u.protocol !== 'https:') {
|
|
352
|
+
throw new UsageError(`--feed 必须 https(收到 ${u.protocol}//${u.host})`, { help: UPDATE_HELP });
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
return cmdSelfUpdate({
|
|
356
|
+
...(values['ext-dir'] !== undefined ? { extDir: values['ext-dir'] } : {}),
|
|
357
|
+
...(values.feed !== undefined ? { feed: values.feed } : {}),
|
|
358
|
+
dryRun: Boolean(values['dry-run']),
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
case 'bind': {
|
|
362
|
+
// A3:bind install/status/remove;--help/-h → BIND_HELP exit 0;裸 bind → 帮助 exit 2
|
|
363
|
+
if (wantsHelp(rest, rest.length > 0)) {
|
|
364
|
+
console.log(BIND_HELP);
|
|
365
|
+
return rest.length > 0 ? 0 : 2;
|
|
366
|
+
}
|
|
367
|
+
// P1-5:平台守卫(macOS/Linux)——help 可看,install/status/remove 在
|
|
368
|
+
// 未支持平台(Windows 注册表面待实装)一律 UsageError exit 2
|
|
369
|
+
assertBindPlatform(process.platform);
|
|
370
|
+
const [sub, ...subRest] = rest;
|
|
371
|
+
const BIND_SUBS = {
|
|
372
|
+
install: {
|
|
373
|
+
flags: ['browser', 'manifests-dir', 'config-dir', 'help'],
|
|
374
|
+
options: {
|
|
375
|
+
browser: { type: 'string' },
|
|
376
|
+
'manifests-dir': { type: 'string' },
|
|
377
|
+
'config-dir': { type: 'string' },
|
|
378
|
+
help: { type: 'boolean', short: 'h' },
|
|
379
|
+
},
|
|
380
|
+
},
|
|
381
|
+
status: {
|
|
382
|
+
flags: ['manifests-dir', 'config-dir', 'help'],
|
|
383
|
+
options: {
|
|
384
|
+
'manifests-dir': { type: 'string' },
|
|
385
|
+
'config-dir': { type: 'string' },
|
|
386
|
+
help: { type: 'boolean', short: 'h' },
|
|
387
|
+
},
|
|
388
|
+
},
|
|
389
|
+
remove: {
|
|
390
|
+
flags: ['purge', 'manifests-dir', 'config-dir', 'help'],
|
|
391
|
+
options: {
|
|
392
|
+
purge: { type: 'boolean' },
|
|
393
|
+
'manifests-dir': { type: 'string' },
|
|
394
|
+
'config-dir': { type: 'string' },
|
|
395
|
+
help: { type: 'boolean', short: 'h' },
|
|
396
|
+
},
|
|
397
|
+
},
|
|
398
|
+
};
|
|
399
|
+
const spec = BIND_SUBS[sub];
|
|
400
|
+
if (!spec) {
|
|
401
|
+
throw new UsageError(`未知子命令:bind ${sub ?? ''}(可用:install/status/remove)`, { help: BIND_HELP });
|
|
402
|
+
}
|
|
403
|
+
assertFlags(subRest, spec.flags, `bind ${sub}`, { help: BIND_HELP });
|
|
404
|
+
const { positionals, values } = parse(subRest, { options: spec.options, help: BIND_HELP });
|
|
405
|
+
if (values.help) {
|
|
406
|
+
console.log(BIND_HELP);
|
|
407
|
+
return 0;
|
|
408
|
+
}
|
|
409
|
+
assertNoPositionals(positionals, `bind ${sub}`, { help: BIND_HELP });
|
|
410
|
+
const configDir = resolveConfigDir({
|
|
411
|
+
...(values['config-dir'] ? { configDirFlag: values['config-dir'] } : {}),
|
|
412
|
+
});
|
|
413
|
+
const browser = values.browser ? String(values.browser) : 'chrome';
|
|
414
|
+
if (values.browser !== undefined && !['chrome', 'chromium', 'both'].includes(browser)) {
|
|
415
|
+
throw new UsageError(`--browser 取值 chrome|chromium|both(收到 ${browser})`, { help: BIND_HELP });
|
|
416
|
+
}
|
|
417
|
+
switch (sub) {
|
|
418
|
+
case 'install':
|
|
419
|
+
return cmdBindInstall({ browser, manifestsDir: values['manifests-dir'], configDir });
|
|
420
|
+
case 'status':
|
|
421
|
+
return cmdBindStatus({ manifestsDir: values['manifests-dir'], configDir });
|
|
422
|
+
case 'remove':
|
|
423
|
+
return cmdBindRemove({ manifestsDir: values['manifests-dir'], configDir, purge: Boolean(values.purge) });
|
|
424
|
+
default:
|
|
425
|
+
throw new UsageError(`未知子命令:bind ${sub}`, { help: BIND_HELP });
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
default:
|
|
429
|
+
throw new UsageError(`未知命令组:${group}(可用:skills/mcp/status/doctor/update/bind)`, { help: MAIN_HELP });
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/** 包一层 UsageError→stderr+help(bin.js 只兜底未预期异常)。 */
|
|
434
|
+
export async function mainSafe(argv) {
|
|
435
|
+
try {
|
|
436
|
+
return await main(argv);
|
|
437
|
+
} catch (err) {
|
|
438
|
+
if (err instanceof UsageError) {
|
|
439
|
+
console.error(`用法错误:${err.message}`);
|
|
440
|
+
if (err.help) console.error(`\n${err.help}`);
|
|
441
|
+
return err.exitCode;
|
|
442
|
+
}
|
|
443
|
+
throw err;
|
|
444
|
+
}
|
|
445
|
+
}
|