funcoding-cli 0.3.0 → 0.4.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 CHANGED
@@ -9,20 +9,28 @@
9
9
 
10
10
  人也可以直接使用。需要 Node.js 18.17 及以上。
11
11
 
12
- ## 让智能体用起来
12
+ ## 给智能体用
13
13
 
14
- 把这句话发给你的智能体:
14
+ 做法参照 [sentry-cli](https://cli.sentry.dev/agentic-usage/):命令全局可用,智能体通过一个 Skill 知道什么时候、怎么用它。把这句话发给你的智能体:
15
15
 
16
- > 运行 `npx --yes funcoding-cli@latest init`,然后帮我找一个处理 PDF 的 Skill 并安装。
16
+ > 运行 `npx --yes funcoding-cli@latest cli setup`,然后帮我找一个处理 PDF 的 Skill 并安装。
17
17
 
18
- `init` 做两件事:
18
+ `cli setup` 做两件事:
19
19
 
20
- 1. 没有全局安装时,用 npm 全局安装本工具,之后终端里可以直接用 `funcoding`,或者简写 `fun`。加 `--no-global` 跳过这一步。
21
- 2. 给当前智能体装上 funcoding Skill([skill/funcoding/SKILL.md](skill/funcoding/SKILL.md))。Skill 里写明了:
22
- - 什么时候用哪条命令
20
+ 1. 没有全局安装时,用 npm 全局安装本工具,之后终端里可以直接用 `funcoding`,或者简写 `fun`。加 `--no-global` 跳过。
21
+ 2. 为本机检测到的智能体安装 funcoding-cli Skill([skill/funcoding-cli/](skill/funcoding-cli/SKILL.md)),安装到各自的用户目录(如 `~/.claude/skills/funcoding-cli/`;`~/.agents` 存在时也安装到 `~/.agents/skills/`)。加 `--no-agent-skills` 跳过。Skill 里写明了:
22
+ - 什么时候用哪条命令(详细用法在 `references/`,由命令表生成)
23
23
  - 安装前先用 `skills info` 检查内容,并征得用户同意
24
24
  - 缺少 MCP 密钥时向用户索取,不要编造
25
25
 
26
+ `cli upgrade` 升级本工具,并用新版本刷新已安装的 Skill。Skill 的 frontmatter 带 `version`,和 CLI 版本一致。
27
+
28
+ 只要 Skill、不全局安装命令时,也可以用 [skills](https://github.com/vercel-labs/skills) 从网站安装(网站在 `/.well-known/skills/` 提供同一份文件):
29
+
30
+ ```bash
31
+ npx skills add https://funcoding.ai
32
+ ```
33
+
26
34
  也可以自己全局安装:
27
35
 
28
36
  ```bash
@@ -36,15 +44,15 @@ fun --help
36
44
 
37
45
  格式参照 [android CLI](https://github.com/android/skills/blob/main/devtools/android-cli/SKILL.md):
38
46
 
39
- - 顶层是 `init`、`update`、`info`、`help` 这几条。
47
+ - 顶层是 `info`、`help`,安装和升级本工具在 `cli` 组。
40
48
  - 其余命令按「名词 动词」分组。
41
49
  - 每条命令都可以加 `--help`,查看它的用法、选项和位置参数,比如 `funcoding skills add --help`。
42
50
  - 选项可以写成 `--agent=codex`,也可以写成 `--agent codex`。
43
51
 
44
52
  | 命令 | 说明 |
45
53
  | --- | --- |
46
- | `init` | 全局安装本工具,并给智能体装上 funcoding Skill |
47
- | `update` | 把本工具更新到最新版本,并刷新已安装的 funcoding Skill |
54
+ | `cli setup` | 全局安装本工具,并为本机的智能体安装 funcoding-cli Skill;`--no-global`、`--no-agent-skills` 跳过对应步骤,`--quiet` 不输出提示 |
55
+ | `cli upgrade` | 升级到最新版本,并刷新已安装的 funcoding-cli Skill |
48
56
  | `info [<field>]` | 环境信息:版本、站点、是否全局安装、登录状态、本机的智能体和 Skill 目录、配置目录 |
49
57
  | `help [<command>]` | 显示帮助 |
50
58
  | `skills find <query>` | 查找 Skill |
@@ -73,8 +81,8 @@ fun --help
73
81
  - 失败时为 `{ "ok": false, "error": { "code", "message", "hint", ... } }`
74
82
  - 过程提示一律写到 stderr。
75
83
  - **不交互**:需要确认的操作通过参数表达,比如覆盖时加 `--force`,`favorites sync`、`lists add` 可以先加 `--dry-run` 预览。
76
- - **`--agent`**:可选 `claude-code`、`codex`、`cursor`,多个用逗号分隔。`all` 表示本机检测到的全部智能体,检测方法是看用户目录下有没有对应的配置目录。不指定时,在 Claude Code 中运行(`CLAUDECODE=1`)默认装给 Claude Code,其他情况也默认 Claude Code。Codex 和 Cursor 没有文档化的环境变量,CLI 不去猜,需要显式指定。
77
- - **范围**:`--project` 装到当前项目,不加时装到用户目录。`skills list`、`skills update`、`skills remove`、`mcp list`、`mcp remove` 默认同时查看两处,可以用 `--project` 或 `--user` 缩小范围。
84
+ - **`--agent`**:可选 `claude-code`、`codex`、`cursor`,多个用逗号分隔。`all` 表示本机检测到的全部智能体,检测方法是看用户目录下有没有对应的配置目录。不指定时,在 Claude Code 中运行(`CLAUDECODE=1`)默认安装到 Claude Code,其他情况也默认 Claude Code。Codex 和 Cursor 没有文档化的环境变量,CLI 不去猜,需要显式指定。
85
+ - **范围**:`--project` 安装到当前项目,不加时安装到用户目录。`skills list`、`skills update`、`skills remove`、`mcp list`、`mcp remove` 默认同时查看两处,可以用 `--project` 或 `--user` 缩小范围。
78
86
 
79
87
  | 退出码 | 含义 |
80
88
  | --- | --- |
@@ -85,6 +93,24 @@ fun --help
85
93
  | 4 | 目标已存在,需要 `--force` |
86
94
  | 5 | 未登录或登录已过期 |
87
95
 
96
+ ## 输出
97
+
98
+ 不加 `--json` 时是给人看的文字,只用这几种行(`src/ui.ts`):
99
+
100
+ ```text
101
+ 正在安装 anthropics/skills/pdf(12 个文件)…
102
+ ✓ 已安装 pdf(Claude Code)
103
+ ~/.claude/skills/pdf
104
+ ! Skill 可能运行其中的脚本,使用前先阅读 SKILL.md
105
+ 下一步:新开会话,Claude Code 输入 /pdf
106
+ ```
107
+
108
+ - `正在…`:进度,灰色
109
+ - `✓ 已<动词><对象>`:结果;下面缩进两格的是细节(路径、版本、备份位置),用户主目录写成 `~`
110
+ - `!`:提醒;`✗`:错误,下一行是怎么办
111
+ - `下一步:`:只在确实需要时出现
112
+ - 用简洁书面语,不用「装到」「即可」之类口语和公文腔;`--quiet` 时只输出错误
113
+
88
114
  ## 安装位置
89
115
 
90
116
  | `--agent` | Skill(用户目录 / 项目) | MCP(用户 / 项目) |
package/dist/auth.js CHANGED
@@ -5,7 +5,7 @@
5
5
  import { spawn } from 'node:child_process';
6
6
  import { api, SITE } from './api.js';
7
7
  import { clearCredentials, credentialsPath, loadCredentials, saveCredentials } from './credentials.js';
8
- import { bold, cyan, dim, done, fail, green, info, warn } from './ui.js';
8
+ import { bold, cyan, dim, done, fail, info, step, success, warn } from './ui.js';
9
9
  const CLIENT_ID = 'funcoding-cli';
10
10
  const absolute = (u) => new URL(u, SITE).href;
11
11
  const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
@@ -23,18 +23,16 @@ export async function login(opts) {
23
23
  if (existing) {
24
24
  const me = await fetchMe(existing.token).catch(() => null);
25
25
  if (me) {
26
- return done({ user: me.user, alreadyLoggedIn: true }, () => info(`已经登录:${bold(me.user.name)}${me.user.githubLogin ? dim(` (@${me.user.githubLogin})`) : ''}。要换账号先运行 ${cyan('funcoding auth logout')}。`));
26
+ return done({ user: me.user, alreadyLoggedIn: true }, () => success(`已登录:${me.user.name}${me.user.githubLogin ? ` (@${me.user.githubLogin})` : ''}`, [`换账号先运行 funcoding auth logout`]));
27
27
  }
28
28
  }
29
29
  const code = await api('/api/auth/device/code', { method: 'POST', body: JSON.stringify({ client_id: CLIENT_ID }) });
30
30
  const url = absolute(code.verification_uri_complete ?? `${code.verification_uri}?user_code=${code.user_code}`);
31
- info('');
32
- info(` 在浏览器里确认这个验证码:${bold(pretty(code.user_code))}`);
33
- info(` ${cyan(url)}`);
34
- info('');
31
+ info(`验证码:${bold(pretty(code.user_code))}`);
32
+ info(`打开:${cyan(url)}`);
35
33
  if (opts.browser)
36
34
  openBrowser(url);
37
- info(dim('等待浏览器里确认…(Ctrl+C 取消)'));
35
+ step('正在等待浏览器确认(Ctrl+C 取消)');
38
36
  let interval = Math.max(code.interval ?? 5, 1) * 1000;
39
37
  const deadline = Date.now() + code.expires_in * 1000;
40
38
  while (Date.now() < deadline) {
@@ -47,8 +45,7 @@ export async function login(opts) {
47
45
  const me = await fetchMe(token.access_token);
48
46
  await saveCredentials({ token: token.access_token, site: SITE, user: { name: me.user.name, githubLogin: me.user.githubLogin }, savedAt: new Date().toISOString() });
49
47
  return done({ user: me.user, credentials: credentialsPath() }, () => {
50
- info(green(`✓ 已登录:${me.user.name}${me.user.githubLogin ? ` (@${me.user.githubLogin})` : ''}`));
51
- info(dim(`凭证保存在 ${credentialsPath()}`));
48
+ success(`已登录:${me.user.name}${me.user.githubLogin ? ` (@${me.user.githubLogin})` : ''}`, [`凭证:${credentialsPath()}`]);
52
49
  });
53
50
  }
54
51
  catch (err) {
@@ -60,22 +57,22 @@ export async function login(opts) {
60
57
  continue;
61
58
  }
62
59
  if (e?.error === 'access_denied')
63
- fail('auth', '你在浏览器里拒绝了这次登录。');
60
+ fail('auth', '已在浏览器中拒绝登录');
64
61
  if (e?.error === 'expired_token')
65
- fail('auth', '验证码过期了。', '重新运行 funcoding auth login');
62
+ fail('auth', '验证码已过期', '重新运行 funcoding auth login');
66
63
  throw err;
67
64
  }
68
65
  }
69
- fail('auth', '验证码过期了。', '重新运行 funcoding auth login');
66
+ fail('auth', '验证码已过期', '重新运行 funcoding auth login');
70
67
  }
71
68
  export async function logout() {
72
69
  const c = await loadCredentials(SITE);
73
70
  if (!c)
74
- return done({ loggedOut: true, wasLoggedIn: false }, () => info('当前没有登录。'));
71
+ return done({ loggedOut: true, wasLoggedIn: false }, () => info('当前未登录'));
75
72
  // 顺便让服务端的会话失效;失败也照样删除本地凭证
76
- await api('/api/auth/sign-out', { method: 'POST', token: c.token, body: '{}' }).catch(() => warn('没能通知服务端退出,本地凭证已删除。'));
73
+ await api('/api/auth/sign-out', { method: 'POST', token: c.token, body: '{}' }).catch(() => warn('无法通知服务端退出,已删除本地凭证'));
77
74
  await clearCredentials();
78
- done({ loggedOut: true, wasLoggedIn: true }, () => info(green('✓ 已退出登录')));
75
+ done({ loggedOut: true, wasLoggedIn: true }, () => success('已退出登录'));
79
76
  }
80
77
  export async function fetchMe(token) {
81
78
  return api('/api/cli/me', { token });
@@ -84,16 +81,17 @@ export async function fetchMe(token) {
84
81
  export async function requireToken() {
85
82
  const c = await loadCredentials(SITE);
86
83
  if (!c)
87
- return fail('auth', '还没有登录。', '运行 funcoding auth login');
84
+ return fail('auth', '未登录', '运行 funcoding auth login');
88
85
  return c.token;
89
86
  }
90
87
  export async function status() {
91
88
  const c = await loadCredentials(SITE);
92
89
  if (!c)
93
- return fail('auth', '未登录。', '运行 funcoding auth login');
90
+ return fail('auth', '未登录', '运行 funcoding auth login');
94
91
  const me = await fetchMe(c.token);
95
92
  done({ user: me.user, favorites: { skills: me.favorites.skills.length, mcp: me.favorites.mcp.length }, site: SITE }, () => {
96
93
  info(`${bold(me.user.name)}${me.user.githubLogin ? dim(` (@${me.user.githubLogin})`) : ''}`);
97
- info(dim(`${me.favorites.skills.length} 个收藏的 Skill,${me.favorites.mcp.length} 个收藏的 MCP · ${SITE}`));
94
+ info(dim(`收藏:${me.favorites.skills.length} 个 Skill,${me.favorites.mcp.length} 个 MCP Server`));
95
+ info(dim(`站点:${SITE}`));
98
96
  });
99
97
  }
package/dist/cli.js CHANGED
@@ -1,238 +1,12 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
3
  * funcoding-cli:给 AI 编程智能体用的 funcoding.ai 命令行(人也能用),命令是 funcoding,简写 fun。
4
- * 命令格式参照 android CLI:顶层是 init / update / info / help,其余按「名词 动词」分组(skills add、mcp list…),
5
- * 每条命令都有自己的 --help。命令表 COMMANDS 同时用来解析参数和生成帮助
4
+ * 命令表在 commands.ts;这里负责解析参数、生成 --help 和运行命令
6
5
  */
7
6
  import { parseArgs } from 'node:util';
8
- import { AGENT_IDS, resolveAgents } from './agents.js';
9
7
  import { SITE, VERSION } from './api.js';
10
- import { login, logout, status } from './auth.js';
11
- import { docsFetch, docsSearch, find, newsDaily, newsList } from './content.js';
12
- import { envInfo } from './info.js';
13
- import { init, selfUpdate } from './init.js';
14
- import { install, list, remove, skillInfo, update } from './install.js';
15
- import { mcpAdd, mcpInfo, mcpList, mcpRemove } from './mcp.js';
16
- import { listAdd, listInfo } from './lists.js';
17
- import { favorites, sync } from './sync.js';
8
+ import { BIN, COMMANDS, GLOBAL, OPTIONS } from './commands.js';
18
9
  import { bold, CliError, dim, fail, info, reportError, setJsonMode } from './ui.js';
19
- /** 帮助和提示里统一写全名;fun 是同一个命令的简写 */
20
- const BIN = 'funcoding';
21
- const OPTIONS = {
22
- json: { type: 'boolean', flag: '--json', desc: '只向 stdout 输出一个 JSON 对象(给智能体用)' },
23
- help: { type: 'boolean', short: 'h', flag: '-h, --help', desc: '显示这条命令的帮助' },
24
- version: { type: 'boolean', short: 'V', flag: '-V, --version', desc: '显示版本' },
25
- agent: { type: 'string', short: 'a', flag: '-a, --agent=<name>', desc: `目标智能体:${AGENT_IDS.join('、')},多个用逗号分隔,all 表示本机检测到的全部(默认:当前所在的智能体,否则 claude-code)` },
26
- 'scope-agent': { type: 'string', short: 'a', flag: '-a, --agent=<name>', desc: `只看这些智能体:${AGENT_IDS.join('、')},多个用逗号分隔(默认全部)` },
27
- 'doc-agent': { type: 'string', short: 'a', flag: '-a, --agent=<slug>', desc: '只搜这个智能体的文档,比如 claude-code、codex、gemini-cli' },
28
- project: { type: 'boolean', short: 'p', flag: '-p, --project', desc: '装到当前项目(可随仓库提交),不加时装到用户目录' },
29
- 'only-project': { type: 'boolean', short: 'p', flag: '-p, --project', desc: '只看当前项目' },
30
- user: { type: 'boolean', short: 'u', flag: '-u, --user', desc: '只看用户目录(默认用户目录和当前项目都看)' },
31
- force: { type: 'boolean', short: 'f', flag: '-f, --force', desc: '目标已存在时覆盖(原内容先备份到配置目录)' },
32
- env: { type: 'string', short: 'e', multiple: true, flag: '-e, --env=<KEY=VALUE>', desc: 'stdio Server 的环境变量,可重复' },
33
- header: { type: 'string', multiple: true, flag: '--header=<KEY=VALUE>', desc: '远程 Server 的请求头,可重复' },
34
- via: { type: 'string', flag: '--via=<kind>', desc: '指定启动方式:remote、npm 或 pypi' },
35
- name: { type: 'string', flag: '--name=<name>', desc: '自定义配置名(默认取自 Server 名称)' },
36
- 'dry-run': { type: 'boolean', flag: '--dry-run', desc: '只列出将要执行的操作' },
37
- check: { type: 'boolean', flag: '--check', desc: '只检查远端有没有更新,不安装' },
38
- limit: { type: 'string', flag: '--limit=<n>', desc: '返回条数(1–30,默认 10)' },
39
- 'no-browser': { type: 'boolean', flag: '--no-browser', desc: '不自动打开浏览器' },
40
- 'no-global': { type: 'boolean', flag: '--no-global', desc: '不全局安装,只装 funcoding Skill' },
41
- };
42
- const GLOBAL = ['json', 'help', 'version'];
43
- const str = (v) => (typeof v === 'string' ? v : undefined);
44
- const limitOf = (v) => Math.min(30, Math.max(1, Number(str(v.limit)) || 10));
45
- const need = (args, what, example) => {
46
- if (!args[0])
47
- fail('usage', `缺少${what}。`, `例如 ${example}`);
48
- return args;
49
- };
50
- /** 写入类命令的目标:智能体 × 一个范围 */
51
- function targets(v) {
52
- const agents = resolveAgents(str(v.agent));
53
- if (!agents)
54
- return fail('usage', `不认识的智能体:${str(v.agent)}`, `可选:${AGENT_IDS.join('、')}、all`);
55
- const scope = v.project ? 'project' : 'user';
56
- return agents.map((agent) => ({ agent, scope }));
57
- }
58
- /** 读取和维护类命令的范围:默认全部智能体、两个范围 */
59
- function selection(v) {
60
- const agents = str(v.agent) ? resolveAgents(str(v.agent)) : AGENT_IDS;
61
- if (!agents)
62
- return fail('usage', `不认识的智能体:${str(v.agent)}`, `可选:${AGENT_IDS.join('、')}、all`);
63
- const scopes = v.project && !v.user ? ['project'] : v.user && !v.project ? ['user'] : ['user', 'project'];
64
- return { agents, scopes };
65
- }
66
- /** KEY=VALUE 列表转成对象 */
67
- function pairs(list, flag) {
68
- const out = {};
69
- for (const item of Array.isArray(list) ? list : []) {
70
- const i = item.indexOf('=');
71
- if (i <= 0)
72
- fail('usage', `${flag} 要写成 KEY=VALUE:${item}`);
73
- out[item.slice(0, i)] = item.slice(i + 1);
74
- }
75
- return out;
76
- }
77
- const COMMANDS = [
78
- {
79
- name: 'init',
80
- summary: '初始化:全局安装本工具(之后可用 funcoding 或 fun),并给智能体装上 funcoding Skill',
81
- options: ['agent', 'project', 'force', 'no-global'],
82
- examples: ['npx --yes funcoding-cli@latest init', `${BIN} init --agent=all`],
83
- run: ({ values: v }) => init(targets(v), !!v.force, !v['no-global']),
84
- },
85
- {
86
- name: 'update',
87
- summary: '把本工具更新到最新版本,并刷新已安装的 funcoding Skill',
88
- run: () => selfUpdate(),
89
- },
90
- {
91
- name: 'info',
92
- summary: '环境信息:版本、站点、登录状态、本机的智能体和 Skill 目录、配置目录',
93
- params: [['<field>', '只输出这一项,比如 version、user、agents;不写时输出全部']],
94
- run: ({ args }) => envInfo(args[0]),
95
- },
96
- {
97
- name: 'help',
98
- summary: '显示帮助',
99
- params: [['<command>', '要查看帮助的命令,比如 skills add']],
100
- },
101
- {
102
- name: 'skills',
103
- summary: '查找、安装和管理 Agent Skills',
104
- commands: [
105
- { name: 'find', summary: '按关键词查找 Skill', params: [['<query>', '关键词']], options: ['limit'], run: ({ args, values }) => find('skill', args.join(' '), limitOf(values)) },
106
- {
107
- name: 'info',
108
- summary: '查看 Skill 的描述、文件清单和 SKILL.md 全文(安装前检查用)',
109
- params: [['<id>', 'owner/repo/skill,或网站上的详情页地址']],
110
- run: ({ args }) => skillInfo(need(args, ' Skill', `${BIN} skills info anthropics/skills/pdf`)[0]),
111
- },
112
- {
113
- name: 'add',
114
- summary: '安装一个或多个 Skill',
115
- params: [['<id>...', 'owner/repo/skill,或网站上的详情页地址']],
116
- options: ['agent', 'project', 'force'],
117
- examples: [`${BIN} skills add anthropics/skills/pdf`, `${BIN} skills add anthropics/skills/pdf --agent=codex,cursor --project`],
118
- run: ({ args, values: v }) => install(need(args, '要安装的 Skill', `${BIN} skills add anthropics/skills/pdf`), targets(v), !!v.force),
119
- },
120
- { name: 'list', summary: '列出已安装的 Skill', options: ['scope-agent', 'only-project', 'user'], run: ({ values }) => list(selection(values)) },
121
- {
122
- name: 'update',
123
- summary: '按原来源重新安装由 funcoding 安装的 Skill',
124
- params: [['<name>', '目录名或 Skill id;不写时更新全部']],
125
- options: ['check', 'scope-agent', 'only-project', 'user'],
126
- run: ({ args, values: v }) => update(args[0], selection(v), !!v.check),
127
- },
128
- {
129
- name: 'remove',
130
- summary: '删除由 funcoding 安装的 Skill(先备份)',
131
- params: [['<name>...', '目录名或 Skill id']],
132
- options: ['force', 'scope-agent', 'only-project', 'user'],
133
- run: ({ args, values: v }) => remove(need(args, '要删除的 Skill', `${BIN} skills remove pdf`), selection(v), !!v.force),
134
- },
135
- ],
136
- },
137
- {
138
- name: 'mcp',
139
- summary: '查找 MCP Server,并写入各智能体的 MCP 配置',
140
- commands: [
141
- { name: 'find', summary: '按关键词查找 MCP Server', params: [['<query>', '关键词']], options: ['limit'], run: ({ args, values }) => find('mcp', args.join(' '), limitOf(values)) },
142
- {
143
- name: 'info',
144
- summary: '查看启动方式,以及需要提供的环境变量或请求头',
145
- params: [['<id>', 'owner/repo 或 owner/repo/sub,或网站上的详情页地址']],
146
- run: ({ args }) => mcpInfo(need(args, ' MCP Server', `${BIN} mcp info github/github-mcp-server`)[0]),
147
- },
148
- {
149
- name: 'add',
150
- summary: '把 MCP Server 写入智能体的配置',
151
- params: [['<id>', 'owner/repo 或 owner/repo/sub,或网站上的详情页地址']],
152
- options: ['agent', 'project', 'force', 'env', 'header', 'via', 'name'],
153
- examples: [`${BIN} mcp add vexa-ai/vexa --header=Authorization="Bearer <key>" --agent=cursor`],
154
- run: ({ args, values: v }) => {
155
- const via = str(v.via);
156
- if (via && !['remote', 'npm', 'pypi'].includes(via))
157
- fail('usage', `--via 只能是 remote、npm 或 pypi:${via}`);
158
- return mcpAdd(need(args, ' MCP Server', `${BIN} mcp add github/github-mcp-server`)[0], {
159
- targets: targets(v),
160
- env: pairs(v.env, '--env'),
161
- headers: pairs(v.header, '--header'),
162
- via: via,
163
- name: str(v.name),
164
- force: !!v.force,
165
- });
166
- },
167
- },
168
- { name: 'list', summary: '列出已配置的 MCP Server', options: ['scope-agent', 'only-project', 'user'], run: ({ values }) => mcpList(selection(values)) },
169
- {
170
- name: 'remove',
171
- summary: '删除 MCP Server 配置(先备份原文件)',
172
- params: [['<name>...', '配置名,见 mcp list']],
173
- options: ['scope-agent', 'only-project', 'user'],
174
- run: ({ args, values }) => mcpRemove(need(args, '要删除的配置名', `${BIN} mcp remove vexa`), selection(values)),
175
- },
176
- ],
177
- },
178
- {
179
- name: 'favorites',
180
- summary: '网站上收藏的 Skill 和 MCP Server(需要登录)',
181
- commands: [
182
- { name: 'list', summary: '列出收藏', run: () => favorites() },
183
- { name: 'sync', summary: '安装全部收藏,已安装的跳过', options: ['agent', 'project', 'dry-run'], run: ({ values: v }) => sync(targets(v), !!v['dry-run']) },
184
- ],
185
- },
186
- {
187
- name: 'lists',
188
- summary: '别人分享的 Skill 和 MCP 清单(不需要登录)',
189
- commands: [
190
- {
191
- name: 'info',
192
- summary: '查看清单里的 Skill 和 MCP Server',
193
- params: [['<link>', '清单链接(https://funcoding.ai/share/<id>/)或 id']],
194
- run: ({ args }) => listInfo(need(args, '清单链接', `${BIN} lists info https://funcoding.ai/share/k3x9q2abcd/`)[0]),
195
- },
196
- {
197
- name: 'add',
198
- summary: '安装清单里的全部 Skill 和 MCP Server,已安装的跳过',
199
- params: [['<link>', '清单链接(https://funcoding.ai/share/<id>/)或 id']],
200
- options: ['agent', 'project', 'dry-run'],
201
- run: ({ args, values: v }) => listAdd(need(args, '清单链接', `${BIN} lists add https://funcoding.ai/share/k3x9q2abcd/`)[0], targets(v), !!v['dry-run']),
202
- },
203
- ],
204
- },
205
- {
206
- name: 'auth',
207
- summary: '登录 funcoding.ai',
208
- commands: [
209
- { name: 'login', summary: '在浏览器中授权登录(设备码)', options: ['no-browser'], run: ({ values }) => login({ browser: !values['no-browser'] }) },
210
- { name: 'logout', summary: '退出登录,并使登录凭证失效', run: () => logout() },
211
- { name: 'status', summary: '查看当前登录的账号', run: () => status() },
212
- ],
213
- },
214
- {
215
- name: 'news',
216
- summary: 'AI 热点',
217
- commands: [
218
- { name: 'list', summary: '当前 AI 热榜:过去 48 小时被多个来源共同报道的事件,前 10 名', options: ['limit'], run: ({ values }) => newsList(limitOf(values)) },
219
- { name: 'daily', summary: '某一天的 AI 日报', params: [['<date>', 'YYYY-MM-DD;不写时为最新一期']], run: ({ args }) => newsDaily(args[0]) },
220
- ],
221
- },
222
- {
223
- name: 'docs',
224
- summary: 'AI 编程智能体的中文文档',
225
- commands: [
226
- { name: 'search', summary: '搜索文档', params: [['<query>', '关键词']], options: ['doc-agent', 'limit'], run: ({ args, values }) => docsSearch(args.join(' '), str(values.agent)?.toLowerCase(), limitOf(values)) },
227
- {
228
- name: 'fetch',
229
- summary: '读取文档全文(Markdown),开头附官方来源和核实日期',
230
- params: [['<path>', '<智能体>/<路径>,比如 claude-code/build/hooks;也可以是网站上的文档地址']],
231
- run: ({ args }) => docsFetch(need(args, '文档路径', `${BIN} docs fetch claude-code/build/hooks`)[0]),
232
- },
233
- ],
234
- },
235
- ];
236
10
  // ---------------- 帮助 ----------------
237
11
  const pad = (rows, indent = ' ') => {
238
12
  const w = Math.max(...rows.map(([a]) => a.length)) + 2;
@@ -253,8 +27,8 @@ function helpText(path) {
253
27
  const cmd = path.at(-1);
254
28
  const out = [`${bold('Usage:')} ${usageLine(path)}`];
255
29
  if (!cmd) {
256
- out.push(`${dim(`funcoding v${VERSION}`)} — 从 ${SITE} 查找和安装 Agent Skills、配置 MCP Server、查看 AI 热点和中文文档`);
257
- out.push(dim('可以简写为 fun,比如 fun skills find pdf;给智能体用时加 --json'));
30
+ out.push(`funcoding ${VERSION}:从 ${SITE} 查找和安装 Agent Skills、MCP Server,查阅 AI 热点和中文文档`);
31
+ out.push(dim('简写 fun,如 fun skills find pdf;智能体调用时加 --json'));
258
32
  }
259
33
  else
260
34
  out.push(cmd.summary);
@@ -271,7 +45,7 @@ function helpText(path) {
271
45
  out.push('', bold('Examples:'), cmd.examples.map((e) => ` ${e}`).join('\n'));
272
46
  if (!cmd) {
273
47
  out.push('', bold('Exit codes:'), ' 0 成功 1 一般错误 2 参数错误或缺少必填项 3 未找到 4 目标已存在 5 未登录或登录已过期');
274
- out.push('', `运行 ${BIN} <command> --help 查看每条命令的用法。`);
48
+ out.push('', dim(`每条命令的用法:${BIN} <command> --help`));
275
49
  }
276
50
  return out.join('\n');
277
51
  }
@@ -284,7 +58,7 @@ function resolve(words) {
284
58
  const c = list.find((x) => x.name === words[i]);
285
59
  if (!c) {
286
60
  const where = path.map((p) => p.name).join(' ');
287
- fail('usage', `不认识的命令:${[where, words[i]].filter(Boolean).join(' ')}`, `运行 ${BIN} ${where ? `${where} ` : ''}--help 查看可用命令`);
61
+ fail('usage', `没有这条命令:${[where, words[i]].filter(Boolean).join(' ')}`, `运行 ${BIN} ${where ? `${where} ` : ''}--help`);
288
62
  }
289
63
  path.push(c);
290
64
  list = c.commands;
@@ -330,8 +104,8 @@ main().catch((err) => {
330
104
  if (err instanceof TypeError && err.code?.startsWith('ERR_PARSE_ARGS')) {
331
105
  const unknown = /Unknown option '([^']+)'/.exec(err.message)?.[1];
332
106
  const missing = /Option '([^']+)' argument missing/.exec(err.message)?.[1];
333
- const message = unknown ? `这条命令不支持选项 ${unknown}` : missing ? `选项 ${missing.split(', ').pop()?.replace(' <value>', '')} 缺少取值` : err.message;
334
- return reportError(new CliError('usage', message, `运行 ${BIN} <command> --help 查看这条命令支持的选项`));
107
+ const message = unknown ? `不支持的选项:${unknown}` : missing ? `${missing.split(', ').pop()?.replace(' <value>', '')} 缺少取值` : err.message;
108
+ return reportError(new CliError('usage', message, `运行 ${BIN} <command> --help`));
335
109
  }
336
110
  reportError(new CliError('error', err instanceof Error ? err.message : String(err)));
337
111
  });