funcoding-cli 0.2.0 → 0.2.1

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
@@ -13,41 +13,56 @@
13
13
 
14
14
  把这句话发给你的智能体:
15
15
 
16
- > 运行 `npx --yes funcoding-cli@latest setup`,然后帮我找一个处理 PDF 的 Skill 并安装。
16
+ > 运行 `npx --yes funcoding-cli@latest init`,然后帮我找一个处理 PDF 的 Skill 并安装。
17
17
 
18
- `setup` 会给当前智能体装上 funcoding Skill([skill/funcoding/SKILL.md](skill/funcoding/SKILL.md))。Skill 里写明了:
18
+ `init` 做两件事:
19
19
 
20
- - 什么时候用哪条命令
21
- - 安装前先用 `info` 检查内容,并征得用户同意
22
- - 缺少 MCP 密钥时向用户索取,不要编造
20
+ 1. 没有全局安装时,用 npm 全局安装本工具,之后终端里可以直接用 `funcoding`,或者简写 `fun`。加 `--no-global` 跳过这一步。
21
+ 2. 给当前智能体装上 funcoding Skill([skill/funcoding/SKILL.md](skill/funcoding/SKILL.md))。Skill 里写明了:
22
+ - 什么时候用哪条命令
23
+ - 安装前先用 `skills info` 检查内容,并征得用户同意
24
+ - 缺少 MCP 密钥时向用户索取,不要编造
23
25
 
24
- 也可以全局安装,之后的命令是 `funcoding`:
26
+ 也可以自己全局安装:
25
27
 
26
28
  ```bash
27
29
  npm i -g funcoding-cli
28
- funcoding --help
30
+ fun --help
29
31
  ```
30
32
 
33
+ 没有用简写 `fc`:它是 zsh 和 bash 的内建命令(编辑历史命令用),shell 会优先执行内建命令。
34
+
31
35
  ## 命令
32
36
 
37
+ 格式参照 [android CLI](https://github.com/android/skills/blob/main/devtools/android-cli/SKILL.md):
38
+
39
+ - 顶层是 `init`、`update`、`info`、`help` 这几条。
40
+ - 其余命令按「名词 动词」分组。
41
+ - 每条命令都可以加 `--help`,查看它的用法、选项和位置参数,比如 `funcoding skills add --help`。
42
+ - 选项可以写成 `--agent=codex`,也可以写成 `--agent codex`。
43
+
33
44
  | 命令 | 说明 |
34
45
  | --- | --- |
35
- | `search <关键词>` | 搜索 Skill 和 MCP Server |
36
- | `info <owner/repo/skill>` | 查看 Skill 的描述、文件清单和 SKILL.md 全文 |
37
- | `install <id…>` | 安装一个或多个 Skill,也可以粘贴网站上的详情页地址 |
38
- | `list` | 列出已安装的 Skill:默认列出全部智能体,包括用户目录和当前项目 |
39
- | `update [名称] [--check]` | 按原来源重新安装;`--check` 只比较远端的 SKILL.md,不安装 |
40
- | `remove <名称…>` | 删除通过 funcoding 安装的 Skill,删除前先备份;删除其他来源的 Skill 需要 `--force` |
41
- | `mcp info <owner/repo>` | 查看启动方式(远程地址、npm 或 PyPI 包),以及需要的环境变量或请求头 |
42
- | `mcp add <owner/repo>` | 写入智能体的 MCP 配置;必填项用 `--env KEY=VALUE` 或 `--header KEY=VALUE` 提供,`--via remote\|npm\|pypi` 指定启动方式 |
43
- | `mcp list` / `mcp remove <配置名…>` | 列出或删除 MCP 配置,修改前备份原文件 |
44
- | `login` / `logout` / `whoami` | 在浏览器中授权登录(设备码)、退出登录并使登录凭证失效、查看当前账号 |
45
- | `favorites` | 列出网站上收藏的 Skill 和 MCP Server |
46
- | `sync [--dry-run]` | 安装全部收藏,已安装的跳过;需要密钥的 MCP 会列出缺少哪些值 |
47
- | `news [--daily [YYYY-MM-DD]]` | 查看 AI 热榜,或某一天的 AI 日报 |
48
- | `docs search <关键词> [--agent <智能体>]` | 搜索中文文档 |
49
- | `docs read <智能体/路径>` | 读取文档的 Markdown 原文,开头附官方来源和核实日期 |
50
- | `setup` | 给智能体装上 funcoding Skill;`update` 时会一起更新到当前 CLI 版本 |
46
+ | `init` | 全局安装本工具,并给智能体装上 funcoding Skill |
47
+ | `update` | 把本工具更新到最新版本,并刷新已安装的 funcoding Skill |
48
+ | `info [<field>]` | 环境信息:版本、站点、是否全局安装、登录状态、本机的智能体和 Skill 目录、配置目录 |
49
+ | `help [<command>]` | 显示帮助 |
50
+ | `skills find <query>` | 查找 Skill |
51
+ | `skills info <id>` | 查看 Skill 的描述、文件清单和 SKILL.md 全文 |
52
+ | `skills add <id>...` | 安装一个或多个 Skill,也可以粘贴网站上的详情页地址 |
53
+ | `skills list` | 列出已安装的 Skill:默认列出全部智能体,包括用户目录和当前项目 |
54
+ | `skills update [<name>] [--check]` | 按原来源重新安装;`--check` 只比较远端的 SKILL.md,不安装 |
55
+ | `skills remove <name>...` | 删除通过 funcoding 安装的 Skill,删除前先备份;删除其他来源的 Skill 需要 `--force` |
56
+ | `mcp find <query>` | 查找 MCP Server |
57
+ | `mcp info <id>` | 查看启动方式(远程地址、npm 或 PyPI 包),以及需要的环境变量或请求头 |
58
+ | `mcp add <id>` | 写入智能体的 MCP 配置;必填项用 `--env=KEY=VALUE` 或 `--header=KEY=VALUE` 提供,`--via=remote\|npm\|pypi` 指定启动方式 |
59
+ | `mcp list` / `mcp remove <name>...` | 列出或删除 MCP 配置,修改前备份原文件 |
60
+ | `favorites list` | 列出网站上收藏的 Skill 和 MCP Server |
61
+ | `favorites sync [--dry-run]` | 安装全部收藏,已安装的跳过;需要密钥的 MCP 会列出缺少哪些值 |
62
+ | `auth login` / `auth logout` / `auth status` | 在浏览器中授权登录(设备码)、退出登录并使登录凭证失效、查看当前账号 |
63
+ | `news list` / `news daily [<YYYY-MM-DD>]` | AI 热榜,或某一天的 AI 日报 |
64
+ | `docs search <query> [--agent=<slug>]` | 搜索中文文档 |
65
+ | `docs fetch <agent>/<path>` | 读取文档的 Markdown 原文,开头附官方来源和核实日期 |
51
66
 
52
67
  ## 给智能体的约定
53
68
 
@@ -55,9 +70,9 @@ funcoding --help
55
70
  - 成功时为 `{ "ok": true, ... }`
56
71
  - 失败时为 `{ "ok": false, "error": { "code", "message", "hint", ... } }`
57
72
  - 过程提示一律写到 stderr。
58
- - **不交互**:需要确认的操作通过参数表达,比如覆盖时加 `--force`,`sync` 可以先加 `--dry-run` 预览。
73
+ - **不交互**:需要确认的操作通过参数表达,比如覆盖时加 `--force`,`favorites sync` 可以先加 `--dry-run` 预览。
59
74
  - **`--agent`**:可选 `claude-code`、`codex`、`cursor`,多个用逗号分隔。`all` 表示本机检测到的全部智能体,检测方法是看用户目录下有没有对应的配置目录。不指定时,在 Claude Code 中运行(`CLAUDECODE=1`)默认装给 Claude Code,其他情况也默认 Claude Code。Codex 和 Cursor 没有文档化的环境变量,CLI 不去猜,需要显式指定。
60
- - **范围**:`--project` 装到当前项目,不加时装到用户目录。`list`、`update`、`remove`、`mcp list`、`mcp remove` 默认同时查看两处,可以用 `--project` 或 `--user` 缩小范围。
75
+ - **范围**:`--project` 装到当前项目,不加时装到用户目录。`skills list`、`skills update`、`skills remove`、`mcp list`、`mcp remove` 默认同时查看两处,可以用 `--project` 或 `--user` 缩小范围。
61
76
 
62
77
  | 退出码 | 含义 |
63
78
  | --- | --- |
package/dist/api.js CHANGED
@@ -11,7 +11,7 @@ export class ApiError extends CliError {
11
11
  status;
12
12
  body;
13
13
  constructor(message, status, body) {
14
- super(codeOf(status), message, status === 401 ? '运行 funcoding login 重新登录' : undefined);
14
+ super(codeOf(status), message, status === 401 ? '运行 funcoding auth login 重新登录' : undefined);
15
15
  this.status = status;
16
16
  this.body = body;
17
17
  }
package/dist/auth.js CHANGED
@@ -23,7 +23,7 @@ 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 logout')}。`));
26
+ return done({ user: me.user, alreadyLoggedIn: true }, () => info(`已经登录:${bold(me.user.name)}${me.user.githubLogin ? dim(` (@${me.user.githubLogin})`) : ''}。要换账号先运行 ${cyan('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 }) });
@@ -62,11 +62,11 @@ export async function login(opts) {
62
62
  if (e?.error === 'access_denied')
63
63
  fail('auth', '你在浏览器里拒绝了这次登录。');
64
64
  if (e?.error === 'expired_token')
65
- fail('auth', '验证码过期了。', '重新运行 funcoding login');
65
+ fail('auth', '验证码过期了。', '重新运行 funcoding auth login');
66
66
  throw err;
67
67
  }
68
68
  }
69
- fail('auth', '验证码过期了。', '重新运行 funcoding login');
69
+ fail('auth', '验证码过期了。', '重新运行 funcoding auth login');
70
70
  }
71
71
  export async function logout() {
72
72
  const c = await loadCredentials(SITE);
@@ -84,13 +84,13 @@ export async function fetchMe(token) {
84
84
  export async function requireToken() {
85
85
  const c = await loadCredentials(SITE);
86
86
  if (!c)
87
- return fail('auth', '还没有登录。', '运行 funcoding login');
87
+ return fail('auth', '还没有登录。', '运行 funcoding auth login');
88
88
  return c.token;
89
89
  }
90
- export async function whoami() {
90
+ export async function status() {
91
91
  const c = await loadCredentials(SITE);
92
92
  if (!c)
93
- return fail('auth', '未登录。', '运行 funcoding login');
93
+ return fail('auth', '未登录。', '运行 funcoding auth login');
94
94
  const me = await fetchMe(c.token);
95
95
  done({ user: me.user, favorites: { skills: me.favorites.skills.length, mcp: me.favorites.mcp.length }, site: SITE }, () => {
96
96
  info(`${bold(me.user.name)}${me.user.githubLogin ? dim(` (@${me.user.githubLogin})`) : ''}`);
package/dist/cli.js CHANGED
@@ -1,75 +1,71 @@
1
1
  #!/usr/bin/env node
2
- /** funcoding-cli:给 AI 编程智能体用的 funcoding.ai 命令行(人也能用) */
2
+ /**
3
+ * funcoding-cli:给 AI 编程智能体用的 funcoding.ai 命令行(人也能用),命令是 funcoding,简写 fun。
4
+ * 命令格式参照 android CLI:顶层是 init / update / info / help,其余按「名词 动词」分组(skills add、mcp list…),
5
+ * 每条命令都有自己的 --help。命令表 COMMANDS 同时用来解析参数和生成帮助
6
+ */
3
7
  import { parseArgs } from 'node:util';
4
- import { AGENT_IDS, DEFAULT_AGENT, resolveAgents } from './agents.js';
8
+ import { AGENT_IDS, resolveAgents } from './agents.js';
5
9
  import { SITE, VERSION } from './api.js';
6
- import { login, logout, whoami } from './auth.js';
7
- import { docsRead, docsSearch, news, search } from './content.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';
8
14
  import { install, list, remove, skillInfo, update } from './install.js';
9
15
  import { mcpAdd, mcpInfo, mcpList, mcpRemove } from './mcp.js';
10
- import { setup } from './setup.js';
11
16
  import { favorites, sync } from './sync.js';
12
17
  import { bold, CliError, dim, fail, info, reportError, setJsonMode } from './ui.js';
13
- const HELP = `${bold('funcoding')} ${dim(`v${VERSION}`)} — 从 ${SITE} 查找和安装 Agent Skills、MCP Server
14
-
15
- ${bold('Skill')}
16
- search <关键词> 搜索 Skill 和 MCP Server
17
- info <owner/repo/skill> 查看 Skill:描述、文件清单和 SKILL.md 全文(安装前检查用)
18
- install <id…> 安装 Skill(也可以粘贴网站上的详情页地址)
19
- list 列出已安装的 Skill
20
- update [名称] [--check] 按原来源重新安装;--check 只检查有没有更新
21
- remove <名称…> 删除由 funcoding 安装的 Skill(先备份)
22
-
23
- ${bold('MCP')}
24
- mcp info <owner/repo> 查看启动方式和需要的环境变量
25
- mcp add <owner/repo> 写入智能体的 MCP 配置(--env / --header 提供必填项)
26
- mcp list 列出已配置的 MCP Server
27
- mcp remove <配置名…> 删除配置(先备份)
28
-
29
- ${bold('收藏')}
30
- login / logout / whoami 在浏览器中授权登录 / 退出 / 查看当前账号
31
- favorites 列出网站上收藏的 Skill 和 MCP Server
32
- sync [--dry-run] 安装全部收藏,已装的跳过
33
-
34
- ${bold('热点与文档')}
35
- news [--daily [日期]] AI 热榜,或某天的 AI 日报
36
- docs search <关键词> 搜索中文文档(--agent 可按智能体筛选)
37
- docs read <智能体/路径> 读取文档全文(Markdown)
38
-
39
- ${bold('其他')}
40
- setup 给智能体装上 funcoding Skill,让它学会使用本工具
41
-
42
- ${bold('选项')}
43
- --json 只向 stdout 输出一个 JSON 对象(给智能体用)
44
- -a, --agent <名称> ${AGENT_IDS.join(' | ')} | all,多个用逗号分隔(默认:当前所在的智能体,否则 ${DEFAULT_AGENT})
45
- -p, --project 装到当前项目;list / update / remove / mcp list 时只看当前项目
46
- -u, --user list / update / remove / mcp list 时只看用户目录(默认两处都看)
47
- -f, --force 目标已存在时覆盖(旧版本先备份)
48
- -e, --env KEY=VALUE mcp add:stdio Server 的环境变量,可重复
49
- --header KEY=VALUE mcp add:远程 Server 的请求头,可重复
50
- --via <方式> mcp add:remote | npm | pypi
51
- --name <配置名> mcp add:自定义配置名
52
- --dry-run sync:只列出将要执行的操作
53
- --check update:只检查不安装
54
- --daily [日期] news:日报,YYYY-MM-DD,不写为最新一期
55
- --limit <N> search / news / docs search:返回条数
56
- --no-browser login:不自动打开浏览器
57
- -h, --help 显示帮助
58
- -v, --version 显示版本
59
-
60
- ${bold('退出码')}
61
- 0 成功 1 一般错误 2 参数错误或缺少必填项 3 未找到 4 目标已存在 5 未登录或登录已过期
62
-
63
- ${bold('例子')}
64
- npx funcoding-cli search pdf --json
65
- npx funcoding-cli info anthropics/skills/pdf --json
66
- npx funcoding-cli install anthropics/skills/pdf --agent codex --project
67
- npx funcoding-cli mcp add upstash/context7 --agent all
68
- `;
18
+ /** 帮助和提示里统一写全名;fun 是同一个命令的简写 */
19
+ const BIN = 'funcoding';
20
+ const OPTIONS = {
21
+ json: { type: 'boolean', flag: '--json', desc: '只向 stdout 输出一个 JSON 对象(给智能体用)' },
22
+ help: { type: 'boolean', short: 'h', flag: '-h, --help', desc: '显示这条命令的帮助' },
23
+ version: { type: 'boolean', short: 'V', flag: '-V, --version', desc: '显示版本' },
24
+ agent: { type: 'string', short: 'a', flag: '-a, --agent=<name>', desc: `目标智能体:${AGENT_IDS.join('、')},多个用逗号分隔,all 表示本机检测到的全部(默认:当前所在的智能体,否则 claude-code)` },
25
+ 'scope-agent': { type: 'string', short: 'a', flag: '-a, --agent=<name>', desc: `只看这些智能体:${AGENT_IDS.join('、')},多个用逗号分隔(默认全部)` },
26
+ 'doc-agent': { type: 'string', short: 'a', flag: '-a, --agent=<slug>', desc: '只搜这个智能体的文档,比如 claude-code、codex、gemini-cli' },
27
+ project: { type: 'boolean', short: 'p', flag: '-p, --project', desc: '装到当前项目(可随仓库提交),不加时装到用户目录' },
28
+ 'only-project': { type: 'boolean', short: 'p', flag: '-p, --project', desc: '只看当前项目' },
29
+ user: { type: 'boolean', short: 'u', flag: '-u, --user', desc: '只看用户目录(默认用户目录和当前项目都看)' },
30
+ force: { type: 'boolean', short: 'f', flag: '-f, --force', desc: '目标已存在时覆盖(原内容先备份到配置目录)' },
31
+ env: { type: 'string', short: 'e', multiple: true, flag: '-e, --env=<KEY=VALUE>', desc: 'stdio Server 的环境变量,可重复' },
32
+ header: { type: 'string', multiple: true, flag: '--header=<KEY=VALUE>', desc: '远程 Server 的请求头,可重复' },
33
+ via: { type: 'string', flag: '--via=<kind>', desc: '指定启动方式:remote、npm 或 pypi' },
34
+ name: { type: 'string', flag: '--name=<name>', desc: '自定义配置名(默认取自 Server 名称)' },
35
+ 'dry-run': { type: 'boolean', flag: '--dry-run', desc: '只列出将要执行的操作' },
36
+ check: { type: 'boolean', flag: '--check', desc: '只检查远端有没有更新,不安装' },
37
+ limit: { type: 'string', flag: '--limit=<n>', desc: '返回条数(1–30,默认 10)' },
38
+ 'no-browser': { type: 'boolean', flag: '--no-browser', desc: '不自动打开浏览器' },
39
+ 'no-global': { type: 'boolean', flag: '--no-global', desc: '不全局安装,只装 funcoding Skill' },
40
+ };
41
+ const GLOBAL = ['json', 'help', 'version'];
42
+ const str = (v) => (typeof v === 'string' ? v : undefined);
43
+ const limitOf = (v) => Math.min(30, Math.max(1, Number(str(v.limit)) || 10));
44
+ const need = (args, what, example) => {
45
+ if (!args[0])
46
+ fail('usage', `缺少${what}。`, `例如 ${example}`);
47
+ return args;
48
+ };
49
+ /** 写入类命令的目标:智能体 × 一个范围 */
50
+ function targets(v) {
51
+ const agents = resolveAgents(str(v.agent));
52
+ if (!agents)
53
+ return fail('usage', `不认识的智能体:${str(v.agent)}`, `可选:${AGENT_IDS.join('、')}、all`);
54
+ const scope = v.project ? 'project' : 'user';
55
+ return agents.map((agent) => ({ agent, scope }));
56
+ }
57
+ /** 读取和维护类命令的范围:默认全部智能体、两个范围 */
58
+ function selection(v) {
59
+ const agents = str(v.agent) ? resolveAgents(str(v.agent)) : AGENT_IDS;
60
+ if (!agents)
61
+ return fail('usage', `不认识的智能体:${str(v.agent)}`, `可选:${AGENT_IDS.join('、')}、all`);
62
+ const scopes = v.project && !v.user ? ['project'] : v.user && !v.project ? ['user'] : ['user', 'project'];
63
+ return { agents, scopes };
64
+ }
69
65
  /** KEY=VALUE 列表转成对象 */
70
66
  function pairs(list, flag) {
71
67
  const out = {};
72
- for (const item of list ?? []) {
68
+ for (const item of Array.isArray(list) ? list : []) {
73
69
  const i = item.indexOf('=');
74
70
  if (i <= 0)
75
71
  fail('usage', `${flag} 要写成 KEY=VALUE:${item}`);
@@ -77,144 +73,245 @@ function pairs(list, flag) {
77
73
  }
78
74
  return out;
79
75
  }
76
+ const COMMANDS = [
77
+ {
78
+ name: 'init',
79
+ summary: '初始化:全局安装本工具(之后可用 funcoding 或 fun),并给智能体装上 funcoding Skill',
80
+ options: ['agent', 'project', 'force', 'no-global'],
81
+ examples: ['npx --yes funcoding-cli@latest init', `${BIN} init --agent=all`],
82
+ run: ({ values: v }) => init(targets(v), !!v.force, !v['no-global']),
83
+ },
84
+ {
85
+ name: 'update',
86
+ summary: '把本工具更新到最新版本,并刷新已安装的 funcoding Skill',
87
+ run: () => selfUpdate(),
88
+ },
89
+ {
90
+ name: 'info',
91
+ summary: '环境信息:版本、站点、登录状态、本机的智能体和 Skill 目录、配置目录',
92
+ params: [['<field>', '只输出这一项,比如 version、user、agents;不写时输出全部']],
93
+ run: ({ args }) => envInfo(args[0]),
94
+ },
95
+ {
96
+ name: 'help',
97
+ summary: '显示帮助',
98
+ params: [['<command>', '要查看帮助的命令,比如 skills add']],
99
+ },
100
+ {
101
+ name: 'skills',
102
+ summary: '查找、安装和管理 Agent Skills',
103
+ commands: [
104
+ { name: 'find', summary: '按关键词查找 Skill', params: [['<query>', '关键词']], options: ['limit'], run: ({ args, values }) => find('skill', args.join(' '), limitOf(values)) },
105
+ {
106
+ name: 'info',
107
+ summary: '查看 Skill 的描述、文件清单和 SKILL.md 全文(安装前检查用)',
108
+ params: [['<id>', 'owner/repo/skill,或网站上的详情页地址']],
109
+ run: ({ args }) => skillInfo(need(args, ' Skill', `${BIN} skills info anthropics/skills/pdf`)[0]),
110
+ },
111
+ {
112
+ name: 'add',
113
+ summary: '安装一个或多个 Skill',
114
+ params: [['<id>...', 'owner/repo/skill,或网站上的详情页地址']],
115
+ options: ['agent', 'project', 'force'],
116
+ examples: [`${BIN} skills add anthropics/skills/pdf`, `${BIN} skills add anthropics/skills/pdf --agent=codex,cursor --project`],
117
+ run: ({ args, values: v }) => install(need(args, '要安装的 Skill', `${BIN} skills add anthropics/skills/pdf`), targets(v), !!v.force),
118
+ },
119
+ { name: 'list', summary: '列出已安装的 Skill', options: ['scope-agent', 'only-project', 'user'], run: ({ values }) => list(selection(values)) },
120
+ {
121
+ name: 'update',
122
+ summary: '按原来源重新安装由 funcoding 安装的 Skill',
123
+ params: [['<name>', '目录名或 Skill id;不写时更新全部']],
124
+ options: ['check', 'scope-agent', 'only-project', 'user'],
125
+ run: ({ args, values: v }) => update(args[0], selection(v), !!v.check),
126
+ },
127
+ {
128
+ name: 'remove',
129
+ summary: '删除由 funcoding 安装的 Skill(先备份)',
130
+ params: [['<name>...', '目录名或 Skill id']],
131
+ options: ['force', 'scope-agent', 'only-project', 'user'],
132
+ run: ({ args, values: v }) => remove(need(args, '要删除的 Skill', `${BIN} skills remove pdf`), selection(v), !!v.force),
133
+ },
134
+ ],
135
+ },
136
+ {
137
+ name: 'mcp',
138
+ summary: '查找 MCP Server,并写入各智能体的 MCP 配置',
139
+ commands: [
140
+ { name: 'find', summary: '按关键词查找 MCP Server', params: [['<query>', '关键词']], options: ['limit'], run: ({ args, values }) => find('mcp', args.join(' '), limitOf(values)) },
141
+ {
142
+ name: 'info',
143
+ summary: '查看启动方式,以及需要提供的环境变量或请求头',
144
+ params: [['<id>', 'owner/repo 或 owner/repo/sub,或网站上的详情页地址']],
145
+ run: ({ args }) => mcpInfo(need(args, ' MCP Server', `${BIN} mcp info github/github-mcp-server`)[0]),
146
+ },
147
+ {
148
+ name: 'add',
149
+ summary: '把 MCP Server 写入智能体的配置',
150
+ params: [['<id>', 'owner/repo 或 owner/repo/sub,或网站上的详情页地址']],
151
+ options: ['agent', 'project', 'force', 'env', 'header', 'via', 'name'],
152
+ examples: [`${BIN} mcp add vexa-ai/vexa --header=Authorization="Bearer <key>" --agent=cursor`],
153
+ run: ({ args, values: v }) => {
154
+ const via = str(v.via);
155
+ if (via && !['remote', 'npm', 'pypi'].includes(via))
156
+ fail('usage', `--via 只能是 remote、npm 或 pypi:${via}`);
157
+ return mcpAdd(need(args, ' MCP Server', `${BIN} mcp add github/github-mcp-server`)[0], {
158
+ targets: targets(v),
159
+ env: pairs(v.env, '--env'),
160
+ headers: pairs(v.header, '--header'),
161
+ via: via,
162
+ name: str(v.name),
163
+ force: !!v.force,
164
+ });
165
+ },
166
+ },
167
+ { name: 'list', summary: '列出已配置的 MCP Server', options: ['scope-agent', 'only-project', 'user'], run: ({ values }) => mcpList(selection(values)) },
168
+ {
169
+ name: 'remove',
170
+ summary: '删除 MCP Server 配置(先备份原文件)',
171
+ params: [['<name>...', '配置名,见 mcp list']],
172
+ options: ['scope-agent', 'only-project', 'user'],
173
+ run: ({ args, values }) => mcpRemove(need(args, '要删除的配置名', `${BIN} mcp remove vexa`), selection(values)),
174
+ },
175
+ ],
176
+ },
177
+ {
178
+ name: 'favorites',
179
+ summary: '网站上收藏的 Skill 和 MCP Server(需要登录)',
180
+ commands: [
181
+ { name: 'list', summary: '列出收藏', run: () => favorites() },
182
+ { name: 'sync', summary: '安装全部收藏,已安装的跳过', options: ['agent', 'project', 'dry-run'], run: ({ values: v }) => sync(targets(v), !!v['dry-run']) },
183
+ ],
184
+ },
185
+ {
186
+ name: 'auth',
187
+ summary: '登录 funcoding.ai',
188
+ commands: [
189
+ { name: 'login', summary: '在浏览器中授权登录(设备码)', options: ['no-browser'], run: ({ values }) => login({ browser: !values['no-browser'] }) },
190
+ { name: 'logout', summary: '退出登录,并使登录凭证失效', run: () => logout() },
191
+ { name: 'status', summary: '查看当前登录的账号', run: () => status() },
192
+ ],
193
+ },
194
+ {
195
+ name: 'news',
196
+ summary: 'AI 热点',
197
+ commands: [
198
+ { name: 'list', summary: '当前 AI 热榜:多家来源报道的事件,按热度排序', options: ['limit'], run: ({ values }) => newsList(limitOf(values)) },
199
+ { name: 'daily', summary: '某一天的 AI 日报', params: [['<date>', 'YYYY-MM-DD;不写时为最新一期']], run: ({ args }) => newsDaily(args[0]) },
200
+ ],
201
+ },
202
+ {
203
+ name: 'docs',
204
+ summary: 'AI 编程智能体的中文文档',
205
+ commands: [
206
+ { name: 'search', summary: '搜索文档', params: [['<query>', '关键词']], options: ['doc-agent', 'limit'], run: ({ args, values }) => docsSearch(args.join(' '), str(values.agent)?.toLowerCase(), limitOf(values)) },
207
+ {
208
+ name: 'fetch',
209
+ summary: '读取文档全文(Markdown),开头附官方来源和核实日期',
210
+ params: [['<path>', '<智能体>/<路径>,比如 claude-code/build/hooks;也可以是网站上的文档地址']],
211
+ run: ({ args }) => docsFetch(need(args, '文档路径', `${BIN} docs fetch claude-code/build/hooks`)[0]),
212
+ },
213
+ ],
214
+ },
215
+ ];
216
+ // ---------------- 帮助 ----------------
217
+ const pad = (rows, indent = ' ') => {
218
+ const w = Math.max(...rows.map(([a]) => a.length)) + 2;
219
+ return rows.map(([a, b]) => `${indent}${a.padEnd(w)}${b}`).join('\n');
220
+ };
221
+ function usageLine(path) {
222
+ const cmd = path.at(-1);
223
+ const names = path.map((c) => c.name).join(' ');
224
+ if (!cmd)
225
+ return `${BIN} [-hV] [--json] <command>`;
226
+ if (cmd.commands)
227
+ return `${BIN} ${names} <command> [options]`;
228
+ const opts = (cmd.options ?? []).map((k) => `[${OPTIONS[k].flag.split(', ').pop()}]`).join(' ');
229
+ const params = (cmd.params ?? []).map(([p]) => (cmd.name === 'help' || cmd.name === 'info' || p === '<name>' || p === '<date>' ? `[${p}]` : p)).join(' ');
230
+ return [BIN, names, opts, params].filter(Boolean).join(' ');
231
+ }
232
+ function helpText(path) {
233
+ const cmd = path.at(-1);
234
+ const out = [`${bold('Usage:')} ${usageLine(path)}`];
235
+ if (!cmd) {
236
+ out.push(`${dim(`funcoding v${VERSION}`)} — 从 ${SITE} 查找和安装 Agent Skills、配置 MCP Server、查看 AI 热点和中文文档`);
237
+ out.push(dim('可以简写为 fun,比如 fun skills find pdf;给智能体用时加 --json'));
238
+ }
239
+ else
240
+ out.push(cmd.summary);
241
+ const options = cmd?.options ?? [];
242
+ if (options.length)
243
+ out.push('', bold('Options:'), pad(options.map((k) => [OPTIONS[k].flag, OPTIONS[k].desc])));
244
+ out.push('', bold(cmd ? 'Global options:' : 'Options:'), pad(GLOBAL.map((k) => [OPTIONS[k].flag, OPTIONS[k].desc])));
245
+ if (cmd?.params)
246
+ out.push('', bold('Positional parameters:'), pad(cmd.params));
247
+ const subs = cmd ? cmd.commands : COMMANDS;
248
+ if (subs)
249
+ out.push('', bold('Commands:'), pad(subs.map((c) => [c.name, c.summary])));
250
+ if (cmd?.examples)
251
+ out.push('', bold('Examples:'), cmd.examples.map((e) => ` ${e}`).join('\n'));
252
+ if (!cmd) {
253
+ out.push('', bold('Exit codes:'), ' 0 成功 1 一般错误 2 参数错误或缺少必填项 3 未找到 4 目标已存在 5 未登录或登录已过期');
254
+ out.push('', `运行 ${BIN} <command> --help 查看每条命令的用法。`);
255
+ }
256
+ return out.join('\n');
257
+ }
258
+ /** 从位置参数里找出命令路径,剩下的是这条命令的参数 */
259
+ function resolve(words) {
260
+ const path = [];
261
+ let list = COMMANDS;
262
+ let i = 0;
263
+ while (list && i < words.length) {
264
+ const c = list.find((x) => x.name === words[i]);
265
+ if (!c) {
266
+ const where = path.map((p) => p.name).join(' ');
267
+ fail('usage', `不认识的命令:${[where, words[i]].filter(Boolean).join(' ')}`, `运行 ${BIN} ${where ? `${where} ` : ''}--help 查看可用命令`);
268
+ }
269
+ path.push(c);
270
+ list = c.commands;
271
+ i++;
272
+ }
273
+ return { path, rest: words.slice(i) };
274
+ }
275
+ function parserConfig(keys) {
276
+ const out = {};
277
+ for (const k of keys) {
278
+ const { type, ...o } = OPTIONS[k];
279
+ // scope-agent / doc-agent 等是同一个 --agent 的不同说明
280
+ const name = k === 'scope-agent' || k === 'doc-agent' ? 'agent' : k === 'only-project' ? 'project' : k;
281
+ out[name] = { type, ...(o.short ? { short: o.short } : {}), ...(o.multiple ? { multiple: true } : {}) };
282
+ }
283
+ return out;
284
+ }
285
+ // ---------------- 入口 ----------------
80
286
  async function main() {
81
287
  const argv = process.argv.slice(2);
82
288
  // 参数解析出错时也要按 JSON 输出
83
289
  setJsonMode(argv.includes('--json'));
84
- // --daily 后面的日期可选:没跟日期时补一个空串,避免把后面的参数当成日期
85
- const i = argv.indexOf('--daily');
86
- if (i >= 0 && !/^\d{4}-\d{2}-\d{2}$|^latest$/.test(argv[i + 1] ?? ''))
87
- argv.splice(i + 1, 0, '');
88
- const { values, positionals } = parseArgs({
89
- args: argv,
90
- allowPositionals: true,
91
- options: {
92
- json: { type: 'boolean', default: false },
93
- agent: { type: 'string', short: 'a' },
94
- project: { type: 'boolean', short: 'p', default: false },
95
- user: { type: 'boolean', short: 'u', default: false },
96
- force: { type: 'boolean', short: 'f', default: false },
97
- env: { type: 'string', short: 'e', multiple: true },
98
- header: { type: 'string', multiple: true },
99
- via: { type: 'string' },
100
- name: { type: 'string' },
101
- 'dry-run': { type: 'boolean', default: false },
102
- check: { type: 'boolean', default: false },
103
- daily: { type: 'string' },
104
- limit: { type: 'string' },
105
- 'no-browser': { type: 'boolean', default: false },
106
- help: { type: 'boolean', short: 'h', default: false },
107
- version: { type: 'boolean', short: 'v', default: false },
108
- },
109
- });
110
- setJsonMode(values.json);
290
+ // 先宽松解析一遍,找出命令路径
291
+ const loose = parseArgs({ args: argv, allowPositionals: true, strict: false, options: parserConfig(Object.keys(OPTIONS)) });
292
+ const { path, rest } = resolve(loose.positionals);
293
+ const cmd = path.at(-1);
294
+ if (cmd?.name === 'help' && path.length === 1)
295
+ return info(helpText(resolve(rest).path));
296
+ // 再按这条命令允许的选项严格解析
297
+ const parsed = parseArgs({ args: argv, allowPositionals: true, options: parserConfig([...GLOBAL, ...(cmd?.options ?? [])]) });
298
+ const values = parsed.values;
299
+ const positionals = parsed.positionals;
111
300
  if (values.version)
112
301
  return info(VERSION);
113
- const [command, ...args] = positionals;
114
- if (values.help || !command)
115
- return info(HELP);
116
- const limit = Math.min(30, Math.max(1, Number(values.limit) || 10));
117
- /** 写入类命令的目标:智能体 × 一个范围 */
118
- const targets = () => {
119
- const agents = resolveAgents(values.agent);
120
- if (!agents)
121
- return fail('usage', `不认识的智能体:${values.agent}`, `可选:${AGENT_IDS.join('、')}、all`);
122
- const scope = values.project ? 'project' : 'user';
123
- return agents.map((agent) => ({ agent, scope }));
124
- };
125
- /** 读取和维护类命令的范围:默认全部智能体、两个范围 */
126
- const selection = () => {
127
- const agents = values.agent ? resolveAgents(values.agent) : AGENT_IDS;
128
- if (!agents)
129
- return fail('usage', `不认识的智能体:${values.agent}`, `可选:${AGENT_IDS.join('、')}、all`);
130
- const scopes = values.project && !values.user ? ['project'] : values.user && !values.project ? ['user'] : ['user', 'project'];
131
- return { agents, scopes };
132
- };
133
- switch (command) {
134
- case 'search':
135
- case 's':
136
- return search(args.join(' '), limit);
137
- case 'info':
138
- if (!args[0])
139
- fail('usage', '缺少 Skill。', '例如 funcoding info anthropics/skills/pdf');
140
- return skillInfo(args[0]);
141
- case 'install':
142
- case 'add':
143
- case 'i':
144
- return install(args, targets(), values.force);
145
- case 'list':
146
- case 'ls':
147
- return list(selection());
148
- case 'update':
149
- case 'up':
150
- return update(args[0], selection(), values.check);
151
- case 'remove':
152
- case 'uninstall':
153
- case 'rm':
154
- return remove(args, selection(), values.force);
155
- case 'mcp': {
156
- const [sub, ...rest] = args;
157
- if (sub === 'info') {
158
- if (!rest[0])
159
- fail('usage', '缺少 MCP Server。', '例如 funcoding mcp info upstash/context7');
160
- return mcpInfo(rest[0]);
161
- }
162
- if (sub === 'add' || sub === 'install') {
163
- if (!rest[0])
164
- fail('usage', '缺少 MCP Server。', '例如 funcoding mcp add upstash/context7');
165
- if (values.via && !['remote', 'npm', 'pypi'].includes(values.via))
166
- fail('usage', `--via 只能是 remote、npm 或 pypi:${values.via}`);
167
- return mcpAdd(rest[0], {
168
- targets: targets(),
169
- env: pairs(values.env, '--env'),
170
- headers: pairs(values.header, '--header'),
171
- via: values.via,
172
- name: values.name,
173
- force: values.force,
174
- });
175
- }
176
- if (sub === 'list' || sub === 'ls')
177
- return mcpList(selection());
178
- if (sub === 'remove' || sub === 'rm')
179
- return mcpRemove(rest, selection());
180
- return fail('usage', `不认识的命令:mcp ${sub ?? ''}`.trim(), '可用:mcp info / add / list / remove');
181
- }
182
- case 'login':
183
- return login({ browser: !values['no-browser'] });
184
- case 'logout':
185
- return logout();
186
- case 'whoami':
187
- return whoami();
188
- case 'favorites':
189
- case 'fav':
190
- return favorites();
191
- case 'sync':
192
- return sync(targets(), values['dry-run']);
193
- case 'news':
194
- return news({ daily: values.daily, limit });
195
- case 'docs': {
196
- const [sub, ...rest] = args;
197
- // 文档的 --agent 是站内任意智能体的 slug,不限于能装 Skill 的三个
198
- if (sub === 'search')
199
- return docsSearch(rest.join(' '), values.agent?.toLowerCase(), limit);
200
- if (sub === 'read') {
201
- if (!rest[0])
202
- fail('usage', '缺少文档。', '例如 funcoding docs read claude-code/hooks');
203
- return docsRead(rest[0]);
204
- }
205
- return fail('usage', `不认识的命令:docs ${sub ?? ''}`.trim(), '可用:docs search / read');
206
- }
207
- case 'setup':
208
- return setup(targets(), values.force);
209
- default:
210
- return fail('usage', `不认识的命令:${command}`, '运行 funcoding --help 查看用法');
211
- }
302
+ if (values.help || !cmd?.run)
303
+ return info(helpText(path));
304
+ await cmd.run({ args: positionals.slice(path.length), values });
212
305
  }
213
306
  main().catch((err) => {
214
307
  if (err instanceof CliError)
215
308
  return reportError(err);
216
- // parseArgs 的参数错误
217
- if (err instanceof TypeError && err.code?.startsWith('ERR_PARSE_ARGS'))
218
- return reportError(new CliError('usage', err.message, '运行 funcoding --help 查看用法'));
309
+ // parseArgs 的参数错误,比如这条命令不支持的选项
310
+ if (err instanceof TypeError && err.code?.startsWith('ERR_PARSE_ARGS')) {
311
+ const unknown = /Unknown option '([^']+)'/.exec(err.message)?.[1];
312
+ const missing = /Option '([^']+)' argument missing/.exec(err.message)?.[1];
313
+ const message = unknown ? `这条命令不支持选项 ${unknown}` : missing ? `选项 ${missing.split(', ').pop()?.replace(' <value>', '')} 缺少取值` : err.message;
314
+ return reportError(new CliError('usage', message, `运行 ${BIN} <command> --help 查看这条命令支持的选项`));
315
+ }
219
316
  reportError(new CliError('error', err instanceof Error ? err.message : String(err)));
220
317
  });
package/dist/content.js CHANGED
@@ -1,4 +1,4 @@
1
- /** search / news / docs:只读查询网站内容 */
1
+ /** skills find / mcp find / news / docs:只读查询网站内容 */
2
2
  import { api, siteUrl } from './api.js';
3
3
  import { bold, CliError, cyan, dim, done, fail, info, stars, truncate } from './ui.js';
4
4
  function printCards(title, cards, hrefOf) {
@@ -13,15 +13,17 @@ function printCards(title, cards, hrefOf) {
13
13
  }
14
14
  info('');
15
15
  }
16
- export async function search(query, limit) {
16
+ /** skills find / mcp find */
17
+ export async function find(kind, query, limit) {
18
+ const cmd = kind === 'skill' ? 'skills' : 'mcp';
17
19
  if (!query)
18
- fail('usage', '缺少搜索词。', '例如 funcoding search pdf');
20
+ fail('usage', '缺少关键词。', `例如 funcoding ${cmd} find pdf`);
19
21
  const r = await api(`/api/cli/search?q=${encodeURIComponent(query)}&limit=${limit}`);
20
- done({ skills: r.skills, mcp: r.mcp }, () => {
21
- if (r.skills.length + r.mcp.length === 0)
22
- return info(`没有找到「${query}」相关的 Skill 或 MCP Server。`);
23
- printCards('Skills', r.skills, (c) => `funcoding install ${c.id}`);
24
- printCards('MCP Server', r.mcp, (c) => `funcoding mcp add ${c.id}`);
22
+ const items = kind === 'skill' ? r.skills : r.mcp;
23
+ done({ [kind === 'skill' ? 'skills' : 'servers']: items }, () => {
24
+ if (items.length === 0)
25
+ return info(`没有找到「${query}」相关的${kind === 'skill' ? ' Skill' : ' MCP Server'}。`);
26
+ printCards(kind === 'skill' ? 'Skills' : 'MCP Server', items, (c) => `funcoding ${cmd} add ${c.id}`);
25
27
  });
26
28
  }
27
29
  function printStory(s, n) {
@@ -30,33 +32,33 @@ function printStory(s, n) {
30
32
  info(` ${truncate(s.summary, (process.stdout.columns || 100) - 6)}`);
31
33
  info(dim(` ${s.url}`));
32
34
  }
33
- export async function news(opts) {
34
- if (opts.daily !== undefined) {
35
- const day = opts.daily || 'latest';
36
- if (day !== 'latest' && !/^\d{4}-\d{2}-\d{2}$/.test(day))
37
- fail('usage', '日期要写成 YYYY-MM-DD。');
38
- const issue = await api(`/api/cli/news?daily=${day}`);
39
- return done({ daily: issue }, () => {
40
- info(bold(`AI 日报 · 第 ${issue.no} 期 · ${issue.day}`));
41
- info(dim(issue.url));
35
+ export async function newsDaily(date) {
36
+ const day = date || 'latest';
37
+ if (day !== 'latest' && !/^\d{4}-\d{2}-\d{2}$/.test(day))
38
+ fail('usage', `日期要写成 YYYY-MM-DD:${day}`);
39
+ const issue = await api(`/api/cli/news?daily=${day}`);
40
+ done({ daily: issue }, () => {
41
+ info(bold(`AI 日报 · 第 ${issue.no} 期 · ${issue.day}`));
42
+ info(dim(issue.url));
43
+ info('');
44
+ info(bold('头条'));
45
+ printStory(issue.headline);
46
+ for (const sec of issue.sections) {
42
47
  info('');
43
- info(bold('头条'));
44
- printStory(issue.headline);
45
- for (const sec of issue.sections) {
46
- info('');
47
- info(bold(sec.label));
48
- for (const s of sec.stories)
49
- printStory(s);
50
- }
51
- if (issue.flash.length > 0) {
52
- info('');
53
- info(bold('快讯'));
54
- for (const s of issue.flash)
55
- info(` · ${s.title} ${dim(s.url)}`);
56
- }
57
- });
58
- }
59
- const r = await api(`/api/cli/news?limit=${opts.limit}`);
48
+ info(bold(sec.label));
49
+ for (const s of sec.stories)
50
+ printStory(s);
51
+ }
52
+ if (issue.flash.length > 0) {
53
+ info('');
54
+ info(bold('快讯'));
55
+ for (const s of issue.flash)
56
+ info(` · ${s.title} ${dim(s.url)}`);
57
+ }
58
+ });
59
+ }
60
+ export async function newsList(limit) {
61
+ const r = await api(`/api/cli/news?limit=${limit}`);
60
62
  done({ updatedAt: r.updatedAt, stories: r.stories }, () => {
61
63
  if (r.stories.length === 0)
62
64
  return info('暂无热点。');
@@ -66,7 +68,7 @@ export async function news(opts) {
66
68
  }
67
69
  export async function docsSearch(query, agent, limit) {
68
70
  if (!query)
69
- fail('usage', '缺少搜索词。', '例如 funcoding docs search hooks --agent claude-code');
71
+ fail('usage', '缺少搜索词。', '例如 funcoding docs search hooks --agent=claude-code');
70
72
  const params = new URLSearchParams({ q: query, limit: String(limit), ...(agent ? { agent } : {}) });
71
73
  const r = await api(`/api/cli/docs/search?${params}`);
72
74
  done({ docs: r.docs }, () => {
@@ -78,10 +80,10 @@ export async function docsSearch(query, agent, limit) {
78
80
  info(` ${truncate(d.description, (process.stdout.columns || 100) - 4)}`);
79
81
  }
80
82
  info('');
81
- info(dim(`运行 ${cyan('funcoding docs read <id>')} 阅读全文。`));
83
+ info(dim(`运行 ${cyan('funcoding docs fetch <id>')} 阅读全文。`));
82
84
  });
83
85
  }
84
- export async function docsRead(id) {
86
+ export async function docsFetch(id) {
85
87
  const path = id
86
88
  .trim()
87
89
  .replace(/^https?:\/\/[^/]+/, '')
package/dist/info.js ADDED
@@ -0,0 +1,40 @@
1
+ /** info:环境信息,只读本机状态,不访问网络 */
2
+ import { AGENT_IDS, AGENTS, currentAgent, installedAgents, skillsRoot } from './agents.js';
3
+ import { SITE, VERSION } from './api.js';
4
+ import { configDir, credentialsPath, loadCredentials } from './credentials.js';
5
+ import { onPath } from './init.js';
6
+ import { bold, dim, done, fail, info } from './ui.js';
7
+ export async function envInfo(field) {
8
+ const creds = await loadCredentials(SITE);
9
+ const found = installedAgents();
10
+ const data = {
11
+ version: VERSION,
12
+ site: SITE,
13
+ node: process.versions.node,
14
+ /** 是否已全局安装(终端里能直接用 funcoding / fun) */
15
+ global: onPath('funcoding'),
16
+ user: creds?.user ?? null,
17
+ currentAgent: currentAgent(),
18
+ agents: AGENT_IDS.map((id) => ({ id, installed: found.includes(id), skills: { user: skillsRoot(id, 'user'), project: skillsRoot(id, 'project') } })),
19
+ configDir: configDir(),
20
+ credentials: credentialsPath(),
21
+ };
22
+ if (field) {
23
+ if (!(field in data))
24
+ fail('usage', `没有这一项:${field}`, `可选:${Object.keys(data).join('、')}`);
25
+ const value = data[field];
26
+ return done({ [field]: value }, () => info(typeof value === 'object' && value !== null ? JSON.stringify(value, null, 2) : String(value)));
27
+ }
28
+ done(data, () => {
29
+ const row = (k, v) => info(`${bold(k.padEnd(14))}${v}`);
30
+ row('version', VERSION);
31
+ row('site', SITE);
32
+ row('node', process.versions.node);
33
+ row('global', data.global ? '已全局安装(funcoding / fun)' : '未全局安装,运行 npx --yes funcoding-cli@latest init');
34
+ row('user', creds?.user ? `${creds.user.name}${creds.user.githubLogin ? ` (@${creds.user.githubLogin})` : ''}` : '未登录');
35
+ row('currentAgent', data.currentAgent ? AGENTS[data.currentAgent].label : '无');
36
+ for (const id of AGENT_IDS)
37
+ row(id, `${found.includes(id) ? '已安装' : dim('未检测到')} ${dim(skillsRoot(id, 'user'))}`);
38
+ row('configDir', configDir());
39
+ });
40
+ }
package/dist/init.js ADDED
@@ -0,0 +1,90 @@
1
+ /**
2
+ * init:
3
+ * 1. 没有全局安装时用 npm 全局安装本 CLI,之后终端里可以直接用 funcoding 或简写 fun(--no-global 跳过)
4
+ * 2. 把随 CLI 发布的 funcoding Skill(skill/funcoding/SKILL.md)装到智能体,让它知道什么时候、怎么用这个 CLI。
5
+ * 这个 Skill 不在网站目录里,来源记录标成 builtin,update 时从当前版本的 CLI 重新复制
6
+ */
7
+ import { spawnSync } from 'node:child_process';
8
+ import { randomBytes } from 'node:crypto';
9
+ import { existsSync } from 'node:fs';
10
+ import { mkdir, readFile, rename, rm, stat, writeFile } from 'node:fs/promises';
11
+ import { delimiter, dirname, join } from 'node:path';
12
+ import { AGENTS, skillsRoot } from './agents.js';
13
+ import { USER_AGENT, VERSION } from './api.js';
14
+ import { META_FILE, moveAway } from './install.js';
15
+ import { cyan, dim, done, fail, green, info, warn } from './ui.js';
16
+ const isWindows = process.platform === 'win32';
17
+ /** PATH 上能不能找到这个命令;npx 运行时会把临时目录加进 PATH,那些目录不算 */
18
+ export function onPath(command, env = process.env) {
19
+ const exts = isWindows ? ['.cmd', '.exe', ''] : [''];
20
+ return (env.PATH ?? '')
21
+ .split(delimiter)
22
+ .filter((dir) => dir && !/[\\/]_npx[\\/]/.test(dir))
23
+ .some((dir) => exts.some((ext) => existsSync(join(dir, command + ext))));
24
+ }
25
+ const EACCES_DOC = '权限不足时参考 https://docs.npmjs.com/resolving-eacces-permissions-errors-when-installing-packages-globally';
26
+ /** 全局安装当前版本;npm 的输出写到 stderr,不影响 --json 的 stdout */
27
+ function installGlobal() {
28
+ if (onPath('funcoding'))
29
+ return { status: 'present' };
30
+ const pkg = `funcoding-cli@${VERSION}`;
31
+ info(dim(`正在全局安装 ${pkg},之后可以直接使用 funcoding 或 fun 命令…`));
32
+ const r = spawnSync('npm', ['install', '--global', pkg], { stdio: ['ignore', process.stderr, process.stderr], shell: isWindows });
33
+ if (r.status === 0)
34
+ return { status: 'installed' };
35
+ return { status: 'failed', hint: `手动运行 npm install -g ${pkg}(${EACCES_DOC})` };
36
+ }
37
+ /** update:全局安装最新版,再用新版刷新已安装的 funcoding Skill */
38
+ export function selfUpdate() {
39
+ info(dim('正在用 npm 全局安装 funcoding-cli@latest…'));
40
+ const r = spawnSync('npm', ['install', '--global', 'funcoding-cli@latest'], { stdio: ['ignore', process.stderr, process.stderr], shell: isWindows });
41
+ if (r.status !== 0)
42
+ fail('error', '更新失败。', `手动运行 npm install -g funcoding-cli@latest(${EACCES_DOC})`);
43
+ const version = spawnSync('funcoding', ['--version'], { encoding: 'utf8', shell: isWindows }).stdout?.trim() || null;
44
+ // 让新版本把随包的 funcoding Skill 复制过去;没装过这个 Skill 时会报未找到,忽略
45
+ spawnSync('funcoding', ['skills', 'update', BUILTIN_ID, '--json'], { stdio: 'ignore', shell: isWindows });
46
+ done({ from: VERSION, to: version }, () => info(green(version && version !== VERSION ? `✓ 已从 ${VERSION} 更新到 ${version}` : `✓ 已是最新版本 ${version ?? VERSION}`)));
47
+ }
48
+ export const BUILTIN_ID = 'funcoding';
49
+ const SOURCE = new URL('../skill/funcoding/SKILL.md', import.meta.url);
50
+ export const builtinContent = () => readFile(SOURCE, 'utf8');
51
+ export async function installBuiltin(targets, force) {
52
+ const content = await builtinContent();
53
+ const dirs = targets.map((t) => ({ ...t, dir: join(skillsRoot(t.agent, t.scope), BUILTIN_ID) }));
54
+ const foreign = [];
55
+ for (const d of dirs) {
56
+ const meta = await readFile(join(d.dir, META_FILE), 'utf8').catch(() => null);
57
+ const present = meta !== null || (await stat(d.dir).then(() => true, () => false));
58
+ // 之前 init 装的直接换成新版;同名的其他 Skill 要 --force,并且先备份
59
+ if (present && !(meta && JSON.parse(meta).builtin))
60
+ foreign.push(d);
61
+ }
62
+ if (foreign.length > 0 && !force)
63
+ fail('exists', `${foreign.map((d) => d.dir).join('、')} 已存在,且不是 funcoding init 安装的。`, '确认要覆盖就加 --force(会先备份到配置目录)');
64
+ for (const d of dirs) {
65
+ const tmp = join(dirname(d.dir), `.funcoding-tmp-${randomBytes(4).toString('hex')}`);
66
+ await mkdir(tmp, { recursive: true });
67
+ await writeFile(join(tmp, 'SKILL.md'), content);
68
+ await writeFile(join(tmp, META_FILE), `${JSON.stringify({ id: BUILTIN_ID, name: BUILTIN_ID, builtin: true, installedAt: new Date().toISOString(), cli: USER_AGENT }, null, 2)}\n`);
69
+ if (foreign.includes(d))
70
+ await moveAway(d.dir, d.agent, BUILTIN_ID);
71
+ else
72
+ await rm(d.dir, { recursive: true, force: true });
73
+ await rename(tmp, d.dir);
74
+ }
75
+ return dirs;
76
+ }
77
+ export async function init(targets, force, global) {
78
+ const cli = global ? installGlobal() : { status: 'skipped' };
79
+ const dirs = await installBuiltin(targets, force);
80
+ done({ cli, installed: dirs.map((d) => ({ id: BUILTIN_ID, agent: d.agent, scope: d.scope, dir: d.dir })) }, () => {
81
+ if (cli.status === 'installed')
82
+ info(green(`✓ 已全局安装 funcoding-cli,在终端里输入 ${cyan('funcoding')} 或 ${cyan('fun')} 即可使用`));
83
+ if (cli.status === 'failed')
84
+ warn(`全局安装失败,${cli.hint}`);
85
+ for (const d of dirs)
86
+ info(green(`✓ 已把 funcoding Skill 装到 ${d.dir}`));
87
+ info(`之后可以直接让 ${dirs.map((d) => AGENTS[d.agent].label).join('、')} 帮你查找和安装 Skill、配置 MCP,例如「找一个处理 PDF 的 Skill 装上」。`);
88
+ info(dim(`也可以输入 ${[...new Set(dirs.map((d) => cyan(AGENTS[d.agent].invoke(BUILTIN_ID))))].join(' / ')} 主动调用。新开会话后生效。`));
89
+ });
90
+ }
package/dist/install.js CHANGED
@@ -10,7 +10,7 @@ import { AGENT_IDS, AGENTS, skillsRoot } from './agents.js';
10
10
  import { api, SITE, siteUrl, USER_AGENT } from './api.js';
11
11
  import { configDir } from './credentials.js';
12
12
  import { isSafeFolderName, resolveInside } from './paths.js';
13
- import { builtinContent, installBuiltin } from './setup.js';
13
+ import { builtinContent, installBuiltin } from './init.js';
14
14
  import { bold, CliError, cyan, dim, done, fail, green, info, stars, warn } from './ui.js';
15
15
  /** 和服务端一致的上限 */
16
16
  const MAX_FILES = 200;
@@ -66,7 +66,7 @@ export async function fetchSkill(spec) {
66
66
  }
67
67
  catch (err) {
68
68
  if (err instanceof CliError && err.code === 'not_found')
69
- fail('not_found', `没有找到 Skill:${id}`, `运行 funcoding search <关键词> 查找`);
69
+ fail('not_found', `没有找到 Skill:${id}`, '运行 funcoding skills find <关键词> 查找');
70
70
  throw err;
71
71
  }
72
72
  }
@@ -160,7 +160,7 @@ function printInstalled(r) {
160
160
  }
161
161
  export async function install(specs, targets, force) {
162
162
  if (specs.length === 0)
163
- fail('usage', '缺少要安装的 Skill。', '例如 funcoding install anthropics/skills/pdf');
163
+ fail('usage', '缺少要安装的 Skill。', '例如 funcoding skills add anthropics/skills/pdf');
164
164
  const installed = [];
165
165
  for (const spec of specs)
166
166
  installed.push(...(await installSkill(await fetchSkill(spec), targets, force)));
@@ -200,7 +200,7 @@ export async function list(sel) {
200
200
  const items = await scanAll(sel);
201
201
  done({ skills: items }, () => {
202
202
  if (items.length === 0)
203
- return info(`还没有安装 Skill。可以运行 ${cyan('funcoding search <关键词>')} 查找。`);
203
+ return info(`还没有安装 Skill。可以运行 ${cyan('funcoding skills find <关键词>')} 查找。`);
204
204
  for (const agent of AGENT_IDS) {
205
205
  for (const scope of sel.scopes) {
206
206
  const group = items.filter((s) => s.agent === agent && s.scope === scope);
@@ -220,7 +220,7 @@ const matches = (s, name) => !name || s.folder === name || s.source?.id === name
220
220
  export async function update(name, sel, check) {
221
221
  const items = (await scanAll(sel)).filter((s) => s.source && matches(s, name));
222
222
  if (items.length === 0)
223
- fail('not_found', name ? `没有找到由 funcoding 安装的 ${name}` : '没有由 funcoding 安装的 Skill', '运行 funcoding list 查看已安装的 Skill');
223
+ fail('not_found', name ? `没有找到由 funcoding 安装的 ${name}` : '没有由 funcoding 安装的 Skill', '运行 funcoding skills list 查看已安装的 Skill');
224
224
  const results = [];
225
225
  const cache = new Map();
226
226
  for (const s of items) {
@@ -252,19 +252,19 @@ export async function update(name, sel, check) {
252
252
  for (const r of results)
253
253
  info(`${label[r.status]} ${r.id} ${dim(r.dir)}`);
254
254
  if (check && results.some((r) => r.status === 'outdated'))
255
- info(dim(`运行 ${cyan('funcoding update')} 更新。`));
255
+ info(dim(`运行 ${cyan('funcoding skills update')} 更新。`));
256
256
  });
257
257
  }
258
258
  // ---------------- remove ----------------
259
259
  export async function remove(names, sel, force) {
260
260
  if (names.length === 0)
261
- fail('usage', '缺少要删除的 Skill。', '运行 funcoding list 查看已安装的 Skill');
261
+ fail('usage', '缺少要删除的 Skill。', '运行 funcoding skills list 查看已安装的 Skill');
262
262
  const all = await scanAll(sel);
263
263
  const removed = [];
264
264
  for (const name of names) {
265
265
  const found = all.filter((s) => s.folder === name || s.source?.id === name.toLowerCase());
266
266
  if (found.length === 0)
267
- fail('not_found', `没有找到已安装的 ${name}`, '运行 funcoding list 查看已安装的 Skill');
267
+ fail('not_found', `没有找到已安装的 ${name}`, '运行 funcoding skills list 查看已安装的 Skill');
268
268
  const manual = found.filter((s) => !s.source);
269
269
  if (manual.length > 0 && !force)
270
270
  fail('usage', `${manual.map((s) => s.dir).join('、')} 不是由 funcoding 安装的。`, '确认要删除就加 --force(会先备份到配置目录)', { paths: manual.map((s) => s.dir) });
package/dist/mcp.js CHANGED
@@ -28,7 +28,7 @@ export async function fetchMcp(spec) {
28
28
  }
29
29
  catch (err) {
30
30
  if (err instanceof CliError && err.code === 'not_found')
31
- fail('not_found', `没有找到 MCP Server:${id}`, '运行 funcoding search <关键词> 查找');
31
+ fail('not_found', `没有找到 MCP Server:${id}`, '运行 funcoding mcp find <关键词> 查找');
32
32
  throw err;
33
33
  }
34
34
  }
package/dist/setup.js CHANGED
@@ -1,14 +1,38 @@
1
1
  /**
2
- * setup:把随 CLI 发布的 funcoding Skill(skill/funcoding/SKILL.md)装到智能体,让它知道什么时候、怎么用这个 CLI。
3
- * 这个 Skill 不在网站目录里,来源记录标成 builtin,update 时从当前版本的 CLI 重新复制。
2
+ * setup:
3
+ * 1. 没有全局安装时用 npm 全局安装本 CLI,之后终端里可以直接用 funcoding 或简写 fun(--no-global 跳过)
4
+ * 2. 把随 CLI 发布的 funcoding Skill(skill/funcoding/SKILL.md)装到智能体,让它知道什么时候、怎么用这个 CLI。
5
+ * 这个 Skill 不在网站目录里,来源记录标成 builtin,update 时从当前版本的 CLI 重新复制
4
6
  */
7
+ import { spawnSync } from 'node:child_process';
5
8
  import { randomBytes } from 'node:crypto';
9
+ import { existsSync } from 'node:fs';
6
10
  import { mkdir, readFile, rename, rm, stat, writeFile } from 'node:fs/promises';
7
- import { dirname, join } from 'node:path';
11
+ import { delimiter, dirname, join } from 'node:path';
8
12
  import { AGENTS, skillsRoot } from './agents.js';
9
- import { USER_AGENT } from './api.js';
13
+ import { USER_AGENT, VERSION } from './api.js';
10
14
  import { META_FILE, moveAway } from './install.js';
11
- import { cyan, dim, done, fail, green, info } from './ui.js';
15
+ import { cyan, dim, done, fail, green, info, warn } from './ui.js';
16
+ const isWindows = process.platform === 'win32';
17
+ /** PATH 上能不能找到这个命令;npx 运行时会把临时目录加进 PATH,那些目录不算 */
18
+ export function onPath(command, env = process.env) {
19
+ const exts = isWindows ? ['.cmd', '.exe', ''] : [''];
20
+ return (env.PATH ?? '')
21
+ .split(delimiter)
22
+ .filter((dir) => dir && !/[\\/]_npx[\\/]/.test(dir))
23
+ .some((dir) => exts.some((ext) => existsSync(join(dir, command + ext))));
24
+ }
25
+ /** 全局安装当前版本;npm 的输出写到 stderr,不影响 --json 的 stdout */
26
+ function installGlobal() {
27
+ if (onPath('funcoding'))
28
+ return { status: 'present' };
29
+ const pkg = `funcoding-cli@${VERSION}`;
30
+ info(dim(`正在全局安装 ${pkg},之后可以直接使用 funcoding 或 fun 命令…`));
31
+ const r = spawnSync('npm', ['install', '--global', pkg], { stdio: ['ignore', process.stderr, process.stderr], shell: isWindows });
32
+ if (r.status === 0)
33
+ return { status: 'installed' };
34
+ return { status: 'failed', hint: `手动运行 npm install -g ${pkg}(权限不足时参考 https://docs.npmjs.com/resolving-eacces-permissions-errors-when-installing-packages-globally)` };
35
+ }
12
36
  export const BUILTIN_ID = 'funcoding';
13
37
  const SOURCE = new URL('../skill/funcoding/SKILL.md', import.meta.url);
14
38
  export const builtinContent = () => readFile(SOURCE, 'utf8');
@@ -38,9 +62,14 @@ export async function installBuiltin(targets, force) {
38
62
  }
39
63
  return dirs;
40
64
  }
41
- export async function setup(targets, force) {
65
+ export async function setup(targets, force, global) {
66
+ const cli = global ? installGlobal() : { status: 'skipped' };
42
67
  const dirs = await installBuiltin(targets, force);
43
- done({ installed: dirs.map((d) => ({ id: BUILTIN_ID, agent: d.agent, scope: d.scope, dir: d.dir })) }, () => {
68
+ done({ cli, installed: dirs.map((d) => ({ id: BUILTIN_ID, agent: d.agent, scope: d.scope, dir: d.dir })) }, () => {
69
+ if (cli.status === 'installed')
70
+ info(green(`✓ 已全局安装 funcoding-cli,在终端里输入 ${cyan('funcoding')} 或 ${cyan('fun')} 即可使用`));
71
+ if (cli.status === 'failed')
72
+ warn(`全局安装失败,${cli.hint}`);
44
73
  for (const d of dirs)
45
74
  info(green(`✓ 已把 funcoding Skill 装到 ${d.dir}`));
46
75
  info(`之后可以直接让 ${dirs.map((d) => AGENTS[d.agent].label).join('、')} 帮你查找和安装 Skill、配置 MCP,例如「找一个处理 PDF 的 Skill 装上」。`);
package/dist/sync.js CHANGED
@@ -21,9 +21,9 @@ export async function favorites() {
21
21
  done({ skills, mcp }, () => {
22
22
  if (skills.length + mcp.length === 0)
23
23
  return info(`还没有收藏。可以在 ${siteUrl('/skills/')} 收藏 Skill 和 MCP Server。`);
24
- printCards('收藏的 Skill', skills, (c) => `funcoding install ${c.id}`);
24
+ printCards('收藏的 Skill', skills, (c) => `funcoding skills add ${c.id}`);
25
25
  printCards('收藏的 MCP Server', mcp, (c) => `funcoding mcp add ${c.id}`);
26
- info(dim(`运行 ${cyan('funcoding sync')} 一次安装全部收藏。`));
26
+ info(dim(`运行 ${cyan('funcoding favorites sync')} 一次安装全部收藏。`));
27
27
  });
28
28
  }
29
29
  export async function sync(targets, dryRun) {
package/package.json CHANGED
@@ -1,10 +1,11 @@
1
1
  {
2
2
  "name": "funcoding-cli",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "funcoding.ai 的命令行:给 AI 编程智能体用,查找和安装 Agent Skills、配置 MCP Server、同步收藏、查看 AI 热点和中文文档",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "funcoding": "dist/cli.js",
8
+ "fun": "dist/cli.js",
8
9
  "funcoding-cli": "dist/cli.js"
9
10
  },
10
11
  "files": [
@@ -1,61 +1,96 @@
1
1
  ---
2
2
  name: funcoding
3
- description: 用 funcoding-cli 从 funcoding.ai 查找并安装 Agent Skills 和 MCP Server,管理已安装的 Skill,同步用户在网站上的收藏,查看 AI 热点和日报,搜索和阅读 Claude Code、Codex、Cursor 等智能体的中文文档。当用户想找某类 Skill 或 MCP Server、安装或更新 Skill、配置 MCP、同步收藏、了解 AI 圈最近的新闻,或查阅 AI 编程工具的中文文档时使用。
3
+ description: 用 funcoding 命令行(funcoding-cli)从 funcoding.ai 查找并安装 Agent Skills 和 MCP Server,管理已安装的 Skill,同步用户在网站上的收藏,查看 AI 热点和日报,搜索和阅读 Claude Code、Codex、Cursor 等智能体的中文文档。当用户想找某类 Skill 或 MCP Server、安装或更新 Skill、配置 MCP、同步收藏、了解 AI 圈最近的新闻,或查阅 AI 编程工具的中文文档时使用。
4
4
  ---
5
5
 
6
- # funcoding-cli
6
+ # funcoding
7
7
 
8
- funcoding-cli 是 funcoding.ai 的命令行工具。运行方式:
8
+ 本 Skill 说明如何使用 `funcoding` 命令行(npm 包 `funcoding-cli`,命令可简写为 `fun`)。
9
+
10
+ ## 安装
11
+
12
+ 如果 PATH 里找不到 `funcoding` 命令,运行下面的命令安装(需要 Node.js 18.17 及以上):
9
13
 
10
14
  ```bash
11
- npx --yes funcoding-cli@latest <命令> --json
15
+ npx --yes funcoding-cli@latest init --json
12
16
  ```
13
17
 
14
- 已全局安装时(`npm i -g funcoding-cli`)可以直接用 `funcoding <命令>`。需要 Node.js 18.17 及以上。
18
+ `init` 会全局安装本工具,并装上本 Skill。返回结果里 `cli.status` 为 `failed` 时(比如没有权限),把 `cli.hint` 告诉用户;在用户处理好之前,可以用 `npx --yes funcoding-cli@latest <命令>` 临时代替 `funcoding <命令>`。
19
+
20
+ `funcoding update` 会把本工具更新到最新版本,并同时更新本 Skill。
15
21
 
16
22
  ## 使用约定
17
23
 
18
24
  - **始终加 `--json`**:stdout 只输出一个 JSON 对象。成功时是 `{ "ok": true, ... }`,失败时是 `{ "ok": false, "error": { "code", "message", "hint" } }`。过程提示写在 stderr,可以忽略。
19
25
  - 出错时先看 `error.hint`,里面通常是下一步该运行的命令或该补的参数。
20
26
  - 退出码:0 成功,1 一般错误,2 参数错误或缺少必填项,3 未找到,4 目标已存在(需要 `--force`),5 未登录或登录已过期。
21
- - `--agent` 指定装给哪个智能体:`claude-code`、`codex`、`cursor`,多个用逗号分隔;`all` 表示本机检测到的全部智能体。不指定时,在 Claude Code 里运行默认装给 Claude Code,其他情况也默认 Claude Code。如果你不是 Claude Code,请显式加上 `--agent`。
27
+ - `--agent=<name>` 指定装给哪个智能体:`claude-code`、`codex`、`cursor`,多个用逗号分隔;`all` 表示本机检测到的全部智能体。不指定时默认装给 Claude Code。**如果你不是 Claude Code,请显式加上 `--agent`。**
22
28
  - `--project` 装到当前项目(可以随仓库提交,团队共用);不加时装到用户目录,对所有项目生效。
29
+ - 不确定某条命令的参数时,运行 `funcoding <命令> --help`,比如 `funcoding skills add --help`。
30
+ - `funcoding info --json` 返回本机环境:已安装的智能体、各自的 Skill 目录、登录状态。
23
31
 
24
- ## 安装 Skill
32
+ ## Skill 管理
25
33
 
26
- 1. 查找:`search <关键词>`。结果里的 `skills[].id` 形如 `owner/repo/skill`。
27
- 2. **安装前先检查**:`info <id>` 会返回 SKILL.md 全文(`skill.content`)和文件清单(`skill.files`,`skill.scripts` 是其中的可执行脚本)。用一两句话告诉用户这个 Skill 做什么;如果有执行任意命令、读取或外传密钥和个人数据、修改系统设置等危险操作,要明确指出。**得到用户确认后再安装。**
28
- 3. 安装:`install <id> [--agent ...] [--project]`。目录已存在时会以退出码 4 停下;向用户确认后再加 `--force`,旧版本会备份到配置目录。
34
+ 使用 `skills` 命令:
35
+
36
+ 1. `funcoding skills find <query>`:查找 Skill。结果里的 `skills[].id` 形如 `owner/repo/skill`。
37
+ 2. **安装前先检查**:`funcoding skills info <id>` 返回 SKILL.md 全文(`skill.content`)和文件清单(`skill.files`,其中的可执行脚本列在 `skill.scripts`)。用一两句话告诉用户这个 Skill 做什么;如果有执行任意命令、读取或外传密钥和个人数据、修改系统设置等危险操作,要明确指出。**得到用户确认后再安装。**
38
+ 3. `funcoding skills add <id>... [--agent=<name>] [--project]`:安装。目录已存在时会以退出码 4 停下;向用户确认后再加 `--force`,旧版本会备份到配置目录。
29
39
  4. 告诉用户装到了哪个目录(`installed[].dir`),以及如何调用:Claude Code 和 Cursor 输入 `/<目录名>`,Codex 输入 `$<目录名>`,也可以直接描述任务,让智能体自动选用。新装的 Skill 可能需要新开会话才会加载。
30
40
 
31
41
  管理已安装的 Skill:
32
42
 
33
- - `list`:列出所有智能体在用户目录和当前项目里的 Skill。`source` 不为空的是通过 funcoding 安装的。
34
- - `update --check`:检查远端 SKILL.md 是否有变化,不安装。`update [名称]`:按原来源重新安装。
35
- - `remove <名称>`:删除前会先备份。只能删除通过 funcoding 安装的 Skill;删除其他 Skill 需要加 `--force`,务必先征得用户同意。
43
+ - `funcoding skills list`:列出所有智能体在用户目录和当前项目里的 Skill。`source` 不为空的是通过 funcoding 安装的。
44
+ - `funcoding skills update --check`:检查远端 SKILL.md 是否有变化,不安装。`funcoding skills update [<name>]`:按原来源重新安装。
45
+ - `funcoding skills remove <name>...`:删除前会先备份。只能删除通过 funcoding 安装的 Skill;删除其他 Skill 需要加 `--force`,务必先征得用户同意。
46
+
47
+ ## MCP 配置
36
48
 
37
- ## 配置 MCP Server
49
+ 使用 `mcp` 命令:
38
50
 
39
- 1. `search <关键词>` 结果里的 `mcp[].id` 形如 `owner/repo`。
40
- 2. `mcp info <id>`:查看可用的启动方式(`server.launches`),每种方式需要的环境变量(`env`)或请求头(`headers`),以及是否必填。
41
- 3. `mcp add <id> [--agent ...] [--project] [--env KEY=VALUE ...] [--header KEY=VALUE ...] [--via remote|npm|pypi]`。
51
+ 1. `funcoding mcp find <query>`:查找 MCP Server。结果里的 `servers[].id` 形如 `owner/repo`。
52
+ 2. `funcoding mcp info <id>`:查看可用的启动方式(`server.launches`),每种方式需要的环境变量(`env`)或请求头(`headers`),以及是否必填。
53
+ 3. `funcoding mcp add <id> [--agent=<name>] [--project] [--env=KEY=VALUE ...] [--header=KEY=VALUE ...] [--via=remote|npm|pypi]`:
42
54
  - 缺少必填项时,会以退出码 2 返回,`error.missing` 列出缺少的项。**向用户索取这些值,不要编造。** 密钥只通过参数传入,不要写进其他文件。
43
55
  - 没有登记标准安装包时,会以退出码 3 返回,`error.readme` 是 README 地址。这时按 README 手动配置。
44
- 4. 配置在重启智能体或新开会话后生效。Claude Code 的项目范围 Server 需要用户在 Claude Code 里确认后才会启用。
45
- 5. `mcp list` 列出已配置的 Server,`mcp remove <配置名>` 删除,删除前会备份原文件。
56
+ 4. 配置在重启智能体或新开会话后生效。Claude Code 项目范围的 Server 需要用户在 Claude Code 里确认后才会启用。
57
+ 5. `funcoding mcp list` 列出已配置的 Server;`funcoding mcp remove <name>...` 删除,删除前会备份原文件。
46
58
 
47
59
  ## 收藏
48
60
 
49
- 用户在 funcoding.ai 上收藏的 Skill 和 MCP Server:
61
+ 用户在 funcoding.ai 上收藏的 Skill 和 MCP Server,需要先登录:
62
+
63
+ - `funcoding auth status`:查看是否已登录。返回退出码 5 表示未登录。
64
+ - `funcoding auth login`:会显示验证码和授权地址(在 stderr 中),并尝试打开浏览器。把地址和验证码告诉用户,请用户在浏览器中确认;命令会一直等到用户确认后才返回。
65
+ - `funcoding favorites list`:列出收藏。
66
+ - `funcoding favorites sync [--agent=<name>] [--project] [--dry-run]`:安装全部收藏,已装的跳过。建议先用 `--dry-run` 给用户看将要安装什么。`items[]` 里 `status` 为 `skipped` 的条目附有原因和提示,通常是 MCP 缺少必填的环境变量。
50
67
 
51
- - `whoami`:查看是否已登录。返回退出码 5 表示未登录。
52
- - `login`:会显示验证码和授权地址(在 stderr 中),并尝试打开浏览器。把地址和验证码告诉用户,请用户在浏览器中确认;命令会一直等到用户确认后才返回。
53
- - `favorites`:列出收藏。
54
- - `sync [--agent ...] [--project] [--dry-run]`:安装全部收藏,已装的跳过。建议先用 `--dry-run` 给用户看将要安装什么。`items[]` 里 `status` 为 `skipped` 的条目附有原因和提示,通常是 MCP 缺少必填的环境变量。
68
+ ## AI 热点
55
69
 
56
- ## 热点与文档
70
+ - `funcoding news list [--limit=<n>]`:当前 AI 热榜(多家来源报道的事件,按热度排序,附中文标题和摘要)。
71
+ - `funcoding news daily [<YYYY-MM-DD>]`:某一天的 AI 日报,不写日期时为最新一期。
57
72
 
58
- - `news [--limit N]`:当前 AI 热榜(多家来源报道的事件,按热度排序,中文标题和摘要)。
59
- - `news --daily [YYYY-MM-DD]`:某天的 AI 日报,不带日期时为最新一期。
60
- - `docs search <关键词> [--agent <智能体>]`:搜索中文文档。这里的 `--agent` 可以是站内任意智能体,比如 `claude-code`、`codex`、`cursor`、`gemini-cli`。
61
- - `docs read <id>`:读取文档全文(Markdown),开头附官方来源链接和核实日期。命令、配置和价格以官方文档为准。
73
+ ## 文档
74
+
75
+ `docs` 命令用来查阅 AI 编程智能体的中文文档。这些文档按官方文档的章节结构整理,并标注了官方来源和核实日期。
76
+
77
+ - `funcoding docs search <query> [--agent=<slug>]`:搜索文档。这里的 `--agent` 可以是站内任意智能体,比如 `claude-code`、`codex`、`cursor`、`gemini-cli`。
78
+ - `funcoding docs fetch <agent>/<path>`:读取文档全文(Markdown),开头附官方来源链接和核实日期。命令、配置和价格以官方文档为准。
79
+
80
+ ## `funcoding help` 输出
81
+
82
+ ```
83
+ Usage: funcoding [-hV] [--json] <command>
84
+
85
+ Commands:
86
+ init 初始化:全局安装本工具(之后可用 funcoding 或 fun),并给智能体装上 funcoding Skill
87
+ update 把本工具更新到最新版本,并刷新已安装的 funcoding Skill
88
+ info 环境信息:版本、站点、登录状态、本机的智能体和 Skill 目录、配置目录
89
+ help 显示帮助
90
+ skills 查找、安装和管理 Agent Skills(find / info / add / list / update / remove)
91
+ mcp 查找 MCP Server,并写入各智能体的 MCP 配置(find / info / add / list / remove)
92
+ favorites 网站上收藏的 Skill 和 MCP Server(list / sync)
93
+ auth 登录 funcoding.ai(login / logout / status)
94
+ news AI 热点(list / daily)
95
+ docs AI 编程智能体的中文文档(search / fetch)
96
+ ```