funcoding-cli 0.2.1 → 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 +41 -13
- package/dist/auth.js +16 -18
- package/dist/cli.js +8 -214
- package/dist/commands.js +238 -0
- package/dist/content.js +36 -26
- package/dist/info.js +6 -3
- package/dist/install.js +36 -38
- package/dist/lists.js +42 -0
- package/dist/mcp.js +25 -28
- package/dist/setup.js +110 -48
- package/dist/skill-docs.js +36 -0
- package/dist/sync.js +30 -17
- package/dist/ui.js +43 -2
- package/package.json +3 -2
- package/skill/funcoding-cli/SKILL.md +67 -0
- package/skill/funcoding-cli/references/auth.md +21 -0
- package/skill/funcoding-cli/references/cli.md +27 -0
- package/skill/funcoding-cli/references/docs.md +26 -0
- package/skill/funcoding-cli/references/favorites.md +19 -0
- package/skill/funcoding-cli/references/lists.md +27 -0
- package/skill/funcoding-cli/references/mcp.md +73 -0
- package/skill/funcoding-cli/references/news.md +21 -0
- package/skill/funcoding-cli/references/skills.md +86 -0
- package/dist/init.js +0 -90
- package/skill/funcoding/SKILL.md +0 -96
package/dist/commands.js
ADDED
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 命令表和选项表:cli.ts 用它解析参数、生成每条命令的 --help,gen-skill.ts 用它生成 funcoding-cli Skill 的 references/。
|
|
3
|
+
* 命令格式参照 android CLI 和 sentry-cli:按「名词 动词」分组(cli setup、skills add、mcp list…),顶层只有 info 和 help
|
|
4
|
+
*/
|
|
5
|
+
import { AGENT_IDS, resolveAgents } from './agents.js';
|
|
6
|
+
import { login, logout, status } from './auth.js';
|
|
7
|
+
import { docsFetch, docsSearch, find, newsDaily, newsList } from './content.js';
|
|
8
|
+
import { envInfo } from './info.js';
|
|
9
|
+
import { install, list, remove, skillInfo, update } from './install.js';
|
|
10
|
+
import { listAdd, listInfo } from './lists.js';
|
|
11
|
+
import { mcpAdd, mcpInfo, mcpList, mcpRemove } from './mcp.js';
|
|
12
|
+
import { setup, upgrade } from './setup.js';
|
|
13
|
+
import { favorites, sync } from './sync.js';
|
|
14
|
+
import { fail, setQuiet } from './ui.js';
|
|
15
|
+
/** 帮助和提示里统一写全名;fun 是同一个命令的简写 */
|
|
16
|
+
export const BIN = 'funcoding';
|
|
17
|
+
export const OPTIONS = {
|
|
18
|
+
json: { type: 'boolean', flag: '--json', desc: '只向 stdout 输出一个 JSON 对象(给智能体用)' },
|
|
19
|
+
help: { type: 'boolean', short: 'h', flag: '-h, --help', desc: '显示这条命令的帮助' },
|
|
20
|
+
version: { type: 'boolean', short: 'V', flag: '-V, --version', desc: '显示版本' },
|
|
21
|
+
agent: { type: 'string', short: 'a', flag: '-a, --agent=<name>', desc: `目标智能体:${AGENT_IDS.join('、')},多个用逗号分隔,all 表示本机检测到的全部(默认:当前所在的智能体,否则 claude-code)` },
|
|
22
|
+
'scope-agent': { type: 'string', short: 'a', flag: '-a, --agent=<name>', desc: `只看这些智能体:${AGENT_IDS.join('、')},多个用逗号分隔(默认全部)` },
|
|
23
|
+
'doc-agent': { type: 'string', short: 'a', flag: '-a, --agent=<slug>', desc: '只搜这个智能体的文档,比如 claude-code、codex、gemini-cli' },
|
|
24
|
+
project: { type: 'boolean', short: 'p', flag: '-p, --project', desc: '安装到当前项目(可随仓库提交),不加时安装到用户目录' },
|
|
25
|
+
'only-project': { type: 'boolean', short: 'p', flag: '-p, --project', desc: '只看当前项目' },
|
|
26
|
+
user: { type: 'boolean', short: 'u', flag: '-u, --user', desc: '只看用户目录(默认用户目录和当前项目都看)' },
|
|
27
|
+
force: { type: 'boolean', short: 'f', flag: '-f, --force', desc: '目标已存在时覆盖(原内容先备份到配置目录)' },
|
|
28
|
+
env: { type: 'string', short: 'e', multiple: true, flag: '-e, --env=<KEY=VALUE>', desc: 'stdio Server 的环境变量,可重复' },
|
|
29
|
+
header: { type: 'string', multiple: true, flag: '--header=<KEY=VALUE>', desc: '远程 Server 的请求头,可重复' },
|
|
30
|
+
via: { type: 'string', flag: '--via=<kind>', desc: '指定启动方式:remote、npm 或 pypi' },
|
|
31
|
+
name: { type: 'string', flag: '--name=<name>', desc: '自定义配置名(默认取自 Server 名称)' },
|
|
32
|
+
'dry-run': { type: 'boolean', flag: '--dry-run', desc: '只列出将要执行的操作' },
|
|
33
|
+
check: { type: 'boolean', flag: '--check', desc: '只检查远端有没有更新,不安装' },
|
|
34
|
+
limit: { type: 'string', flag: '--limit=<n>', desc: '返回条数(1–30,默认 10)' },
|
|
35
|
+
'no-browser': { type: 'boolean', flag: '--no-browser', desc: '不自动打开浏览器' },
|
|
36
|
+
'no-global': { type: 'boolean', flag: '--no-global', desc: '不全局安装' },
|
|
37
|
+
'no-agent-skills': { type: 'boolean', flag: '--no-agent-skills', desc: '不为智能体安装 funcoding-cli Skill' },
|
|
38
|
+
quiet: { type: 'boolean', short: 'q', flag: '-q, --quiet', desc: '不输出给人看的文字(脚本里用)' },
|
|
39
|
+
};
|
|
40
|
+
export const GLOBAL = ['json', 'help', 'version'];
|
|
41
|
+
const str = (v) => (typeof v === 'string' ? v : undefined);
|
|
42
|
+
const limitOf = (v) => Math.min(30, Math.max(1, Number(str(v.limit)) || 10));
|
|
43
|
+
const need = (args, what, example) => {
|
|
44
|
+
if (!args[0])
|
|
45
|
+
fail('usage', `缺少${what}`, `例如 ${example}`);
|
|
46
|
+
return args;
|
|
47
|
+
};
|
|
48
|
+
/** 写入类命令的目标:智能体 × 一个范围 */
|
|
49
|
+
function targets(v) {
|
|
50
|
+
const agents = resolveAgents(str(v.agent));
|
|
51
|
+
if (!agents)
|
|
52
|
+
return fail('usage', `不支持的智能体:${str(v.agent)}`, `可选:${AGENT_IDS.join('、')}、all`);
|
|
53
|
+
const scope = v.project ? 'project' : 'user';
|
|
54
|
+
return agents.map((agent) => ({ agent, scope }));
|
|
55
|
+
}
|
|
56
|
+
/** 读取和维护类命令的范围:默认全部智能体、两个范围 */
|
|
57
|
+
function selection(v) {
|
|
58
|
+
const agents = str(v.agent) ? resolveAgents(str(v.agent)) : AGENT_IDS;
|
|
59
|
+
if (!agents)
|
|
60
|
+
return fail('usage', `不支持的智能体:${str(v.agent)}`, `可选:${AGENT_IDS.join('、')}、all`);
|
|
61
|
+
const scopes = v.project && !v.user ? ['project'] : v.user && !v.project ? ['user'] : ['user', 'project'];
|
|
62
|
+
return { agents, scopes };
|
|
63
|
+
}
|
|
64
|
+
/** KEY=VALUE 列表转成对象 */
|
|
65
|
+
function pairs(list, flag) {
|
|
66
|
+
const out = {};
|
|
67
|
+
for (const item of Array.isArray(list) ? list : []) {
|
|
68
|
+
const i = item.indexOf('=');
|
|
69
|
+
if (i <= 0)
|
|
70
|
+
fail('usage', `${flag} 格式应为 KEY=VALUE:${item}`);
|
|
71
|
+
out[item.slice(0, i)] = item.slice(i + 1);
|
|
72
|
+
}
|
|
73
|
+
return out;
|
|
74
|
+
}
|
|
75
|
+
export const COMMANDS = [
|
|
76
|
+
{
|
|
77
|
+
name: 'info',
|
|
78
|
+
summary: '环境信息:版本、站点、登录状态、本机的智能体和 Skill 目录、配置目录',
|
|
79
|
+
params: [['<field>', '只输出这一项,比如 version、user、agents;不写时输出全部']],
|
|
80
|
+
run: ({ args }) => envInfo(args[0]),
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
name: 'help',
|
|
84
|
+
summary: '显示帮助',
|
|
85
|
+
params: [['<command>', '要查看帮助的命令,比如 skills add']],
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
name: 'cli',
|
|
89
|
+
summary: '安装、升级本工具',
|
|
90
|
+
commands: [
|
|
91
|
+
{
|
|
92
|
+
name: 'setup',
|
|
93
|
+
summary: '全局安装本工具,并为本机的智能体安装 funcoding-cli Skill',
|
|
94
|
+
options: ['no-global', 'no-agent-skills', 'force', 'quiet'],
|
|
95
|
+
examples: ['npx --yes funcoding-cli@latest cli setup', `${BIN} cli setup --no-agent-skills`],
|
|
96
|
+
run: ({ values: v }) => {
|
|
97
|
+
setQuiet(!!v.quiet);
|
|
98
|
+
return setup({ global: !v['no-global'], agentSkills: !v['no-agent-skills'], force: !!v.force });
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
{ name: 'upgrade', summary: '升级到最新版本,并刷新已安装的 funcoding-cli Skill', run: () => upgrade() },
|
|
102
|
+
],
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
name: 'skills',
|
|
106
|
+
summary: '查找、安装和管理 Agent Skills',
|
|
107
|
+
commands: [
|
|
108
|
+
{ name: 'find', summary: '按关键词查找 Skill', params: [['<query>', '关键词']], options: ['limit'], run: ({ args, values }) => find('skill', args.join(' '), limitOf(values)) },
|
|
109
|
+
{
|
|
110
|
+
name: 'info',
|
|
111
|
+
summary: '查看 Skill 的描述、文件清单和 SKILL.md 全文(安装前检查用)',
|
|
112
|
+
params: [['<id>', 'owner/repo/skill,或网站上的详情页地址']],
|
|
113
|
+
run: ({ args }) => skillInfo(need(args, ' Skill', `${BIN} skills info anthropics/skills/pdf`)[0]),
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
name: 'add',
|
|
117
|
+
summary: '安装一个或多个 Skill',
|
|
118
|
+
params: [['<id>...', 'owner/repo/skill,或网站上的详情页地址']],
|
|
119
|
+
options: ['agent', 'project', 'force'],
|
|
120
|
+
examples: [`${BIN} skills add anthropics/skills/pdf`, `${BIN} skills add anthropics/skills/pdf --agent=codex,cursor --project`],
|
|
121
|
+
run: ({ args, values: v }) => install(need(args, '要安装的 Skill', `${BIN} skills add anthropics/skills/pdf`), targets(v), !!v.force),
|
|
122
|
+
},
|
|
123
|
+
{ name: 'list', summary: '列出已安装的 Skill', options: ['scope-agent', 'only-project', 'user'], run: ({ values }) => list(selection(values)) },
|
|
124
|
+
{
|
|
125
|
+
name: 'update',
|
|
126
|
+
summary: '按原来源重新安装由 funcoding 安装的 Skill',
|
|
127
|
+
params: [['<name>', '目录名或 Skill id;不写时更新全部']],
|
|
128
|
+
options: ['check', 'scope-agent', 'only-project', 'user'],
|
|
129
|
+
run: ({ args, values: v }) => update(args[0], selection(v), !!v.check),
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
name: 'remove',
|
|
133
|
+
summary: '删除由 funcoding 安装的 Skill(先备份)',
|
|
134
|
+
params: [['<name>...', '目录名或 Skill id']],
|
|
135
|
+
options: ['force', 'scope-agent', 'only-project', 'user'],
|
|
136
|
+
run: ({ args, values: v }) => remove(need(args, '要删除的 Skill', `${BIN} skills remove pdf`), selection(v), !!v.force),
|
|
137
|
+
},
|
|
138
|
+
],
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
name: 'mcp',
|
|
142
|
+
summary: '查找 MCP Server,并写入各智能体的 MCP 配置',
|
|
143
|
+
commands: [
|
|
144
|
+
{ name: 'find', summary: '按关键词查找 MCP Server', params: [['<query>', '关键词']], options: ['limit'], run: ({ args, values }) => find('mcp', args.join(' '), limitOf(values)) },
|
|
145
|
+
{
|
|
146
|
+
name: 'info',
|
|
147
|
+
summary: '查看启动方式,以及需要提供的环境变量或请求头',
|
|
148
|
+
params: [['<id>', 'owner/repo 或 owner/repo/sub,或网站上的详情页地址']],
|
|
149
|
+
run: ({ args }) => mcpInfo(need(args, ' MCP Server', `${BIN} mcp info github/github-mcp-server`)[0]),
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
name: 'add',
|
|
153
|
+
summary: '把 MCP Server 写入智能体的配置',
|
|
154
|
+
params: [['<id>', 'owner/repo 或 owner/repo/sub,或网站上的详情页地址']],
|
|
155
|
+
options: ['agent', 'project', 'force', 'env', 'header', 'via', 'name'],
|
|
156
|
+
examples: [`${BIN} mcp add vexa-ai/vexa --header=Authorization="Bearer <key>" --agent=cursor`],
|
|
157
|
+
run: ({ args, values: v }) => {
|
|
158
|
+
const via = str(v.via);
|
|
159
|
+
if (via && !['remote', 'npm', 'pypi'].includes(via))
|
|
160
|
+
fail('usage', `--via 可选 remote、npm、pypi:${via}`);
|
|
161
|
+
return mcpAdd(need(args, ' MCP Server', `${BIN} mcp add github/github-mcp-server`)[0], {
|
|
162
|
+
targets: targets(v),
|
|
163
|
+
env: pairs(v.env, '--env'),
|
|
164
|
+
headers: pairs(v.header, '--header'),
|
|
165
|
+
via: via,
|
|
166
|
+
name: str(v.name),
|
|
167
|
+
force: !!v.force,
|
|
168
|
+
});
|
|
169
|
+
},
|
|
170
|
+
},
|
|
171
|
+
{ name: 'list', summary: '列出已配置的 MCP Server', options: ['scope-agent', 'only-project', 'user'], run: ({ values }) => mcpList(selection(values)) },
|
|
172
|
+
{
|
|
173
|
+
name: 'remove',
|
|
174
|
+
summary: '删除 MCP Server 配置(先备份原文件)',
|
|
175
|
+
params: [['<name>...', '配置名,见 mcp list']],
|
|
176
|
+
options: ['scope-agent', 'only-project', 'user'],
|
|
177
|
+
run: ({ args, values }) => mcpRemove(need(args, '要删除的配置名', `${BIN} mcp remove vexa`), selection(values)),
|
|
178
|
+
},
|
|
179
|
+
],
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
name: 'favorites',
|
|
183
|
+
summary: '网站上收藏的 Skill 和 MCP Server(需要登录)',
|
|
184
|
+
commands: [
|
|
185
|
+
{ name: 'list', summary: '列出收藏', run: () => favorites() },
|
|
186
|
+
{ name: 'sync', summary: '安装全部收藏,已安装的跳过', options: ['agent', 'project', 'dry-run'], run: ({ values: v }) => sync(targets(v), !!v['dry-run']) },
|
|
187
|
+
],
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
name: 'lists',
|
|
191
|
+
summary: '别人分享的 Skill 和 MCP 清单(不需要登录)',
|
|
192
|
+
commands: [
|
|
193
|
+
{
|
|
194
|
+
name: 'info',
|
|
195
|
+
summary: '查看清单里的 Skill 和 MCP Server',
|
|
196
|
+
params: [['<link>', '清单链接(https://funcoding.ai/share/<id>/)或 id']],
|
|
197
|
+
run: ({ args }) => listInfo(need(args, '清单链接', `${BIN} lists info https://funcoding.ai/share/k3x9q2abcd/`)[0]),
|
|
198
|
+
},
|
|
199
|
+
{
|
|
200
|
+
name: 'add',
|
|
201
|
+
summary: '安装清单里的全部 Skill 和 MCP Server,已安装的跳过',
|
|
202
|
+
params: [['<link>', '清单链接(https://funcoding.ai/share/<id>/)或 id']],
|
|
203
|
+
options: ['agent', 'project', 'dry-run'],
|
|
204
|
+
run: ({ args, values: v }) => listAdd(need(args, '清单链接', `${BIN} lists add https://funcoding.ai/share/k3x9q2abcd/`)[0], targets(v), !!v['dry-run']),
|
|
205
|
+
},
|
|
206
|
+
],
|
|
207
|
+
},
|
|
208
|
+
{
|
|
209
|
+
name: 'auth',
|
|
210
|
+
summary: '登录 funcoding.ai',
|
|
211
|
+
commands: [
|
|
212
|
+
{ name: 'login', summary: '在浏览器中授权登录(设备码)', options: ['no-browser'], run: ({ values }) => login({ browser: !values['no-browser'] }) },
|
|
213
|
+
{ name: 'logout', summary: '退出登录,并使登录凭证失效', run: () => logout() },
|
|
214
|
+
{ name: 'status', summary: '查看当前登录的账号', run: () => status() },
|
|
215
|
+
],
|
|
216
|
+
},
|
|
217
|
+
{
|
|
218
|
+
name: 'news',
|
|
219
|
+
summary: 'AI 热点',
|
|
220
|
+
commands: [
|
|
221
|
+
{ name: 'list', summary: '当前 AI 热榜:过去 48 小时被多个来源共同报道的事件,前 10 名', options: ['limit'], run: ({ values }) => newsList(limitOf(values)) },
|
|
222
|
+
{ name: 'daily', summary: '某一天的 AI 日报', params: [['<date>', 'YYYY-MM-DD;不写时为最新一期']], run: ({ args }) => newsDaily(args[0]) },
|
|
223
|
+
],
|
|
224
|
+
},
|
|
225
|
+
{
|
|
226
|
+
name: 'docs',
|
|
227
|
+
summary: 'AI 编程智能体的中文文档',
|
|
228
|
+
commands: [
|
|
229
|
+
{ name: 'search', summary: '搜索文档', params: [['<query>', '关键词']], options: ['doc-agent', 'limit'], run: ({ args, values }) => docsSearch(args.join(' '), str(values.agent)?.toLowerCase(), limitOf(values)) },
|
|
230
|
+
{
|
|
231
|
+
name: 'fetch',
|
|
232
|
+
summary: '读取文档全文(Markdown),开头附官方来源和核实日期',
|
|
233
|
+
params: [['<path>', '<智能体>/<路径>,比如 claude-code/build/hooks;也可以是网站上的文档地址']],
|
|
234
|
+
run: ({ args }) => docsFetch(need(args, '文档路径', `${BIN} docs fetch claude-code/build/hooks`)[0]),
|
|
235
|
+
},
|
|
236
|
+
],
|
|
237
|
+
},
|
|
238
|
+
];
|
package/dist/content.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/** skills find / mcp find / news / docs:只读查询网站内容 */
|
|
2
2
|
import { api, siteUrl } from './api.js';
|
|
3
|
-
import { bold, CliError, cyan, dim, done, fail, info, stars, truncate } from './ui.js';
|
|
3
|
+
import { bold, CliError, cyan, dim, done, fail, info, next, stars, truncate } from './ui.js';
|
|
4
4
|
function printCards(title, cards, hrefOf) {
|
|
5
5
|
if (cards.length === 0)
|
|
6
6
|
return;
|
|
@@ -17,70 +17,80 @@ function printCards(title, cards, hrefOf) {
|
|
|
17
17
|
export async function find(kind, query, limit) {
|
|
18
18
|
const cmd = kind === 'skill' ? 'skills' : 'mcp';
|
|
19
19
|
if (!query)
|
|
20
|
-
fail('usage', '
|
|
20
|
+
fail('usage', '缺少关键词', `例如 funcoding ${cmd} find pdf`);
|
|
21
21
|
const r = await api(`/api/cli/search?q=${encodeURIComponent(query)}&limit=${limit}`);
|
|
22
22
|
const items = kind === 'skill' ? r.skills : r.mcp;
|
|
23
23
|
done({ [kind === 'skill' ? 'skills' : 'servers']: items }, () => {
|
|
24
24
|
if (items.length === 0)
|
|
25
|
-
return info(
|
|
25
|
+
return info(`没有找到和「${query}」相关的${kind === 'skill' ? ' Skill' : ' MCP Server'}`);
|
|
26
26
|
printCards(kind === 'skill' ? 'Skills' : 'MCP Server', items, (c) => `funcoding ${cmd} add ${c.id}`);
|
|
27
27
|
});
|
|
28
28
|
}
|
|
29
|
-
|
|
30
|
-
info(`${n ? `${String(n).padStart(2)}. ` : ' · '}${bold(s.title)}${dim(` ${s.sourceCount} 家报道`)}`);
|
|
31
|
-
if (s.summary)
|
|
32
|
-
info(` ${truncate(s.summary, (process.stdout.columns || 100) - 6)}`);
|
|
33
|
-
info(dim(` ${s.url}`));
|
|
34
|
-
}
|
|
29
|
+
const width = () => (process.stdout.columns || 100) - 6;
|
|
35
30
|
export async function newsDaily(date) {
|
|
36
31
|
const day = date || 'latest';
|
|
37
32
|
if (day !== 'latest' && !/^\d{4}-\d{2}-\d{2}$/.test(day))
|
|
38
|
-
fail('usage',
|
|
33
|
+
fail('usage', `日期格式应为 YYYY-MM-DD:${day}`);
|
|
39
34
|
const issue = await api(`/api/cli/news?daily=${day}`);
|
|
40
35
|
done({ daily: issue }, () => {
|
|
41
|
-
info(bold(`AI 日报 ·
|
|
36
|
+
info(bold(`AI 日报 · ${issue.day}`));
|
|
42
37
|
info(dim(issue.url));
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
38
|
+
if (issue.lead) {
|
|
39
|
+
info('');
|
|
40
|
+
info(bold(issue.lead.title));
|
|
41
|
+
info(` ${truncate(issue.lead.summary, width())}`);
|
|
42
|
+
}
|
|
46
43
|
for (const sec of issue.sections) {
|
|
47
44
|
info('');
|
|
48
45
|
info(bold(sec.label));
|
|
49
|
-
for (const
|
|
50
|
-
|
|
46
|
+
for (const i of sec.items) {
|
|
47
|
+
info(` · ${bold(i.title)}${dim(` ${i.source}`)}`);
|
|
48
|
+
if (i.summary)
|
|
49
|
+
info(` ${truncate(i.summary, width())}`);
|
|
50
|
+
info(dim(` ${i.url}`));
|
|
51
|
+
}
|
|
51
52
|
}
|
|
52
|
-
if (issue.
|
|
53
|
+
if (issue.flashes.length > 0) {
|
|
53
54
|
info('');
|
|
54
55
|
info(bold('快讯'));
|
|
55
|
-
for (const
|
|
56
|
-
info(` · ${
|
|
56
|
+
for (const i of issue.flashes)
|
|
57
|
+
info(` · ${i.title} ${dim(i.url)}`);
|
|
57
58
|
}
|
|
59
|
+
info('');
|
|
60
|
+
info(dim('数据来自 AIHOT(aihot.news)'));
|
|
58
61
|
});
|
|
59
62
|
}
|
|
60
63
|
export async function newsList(limit) {
|
|
61
64
|
const r = await api(`/api/cli/news?limit=${limit}`);
|
|
62
65
|
done({ updatedAt: r.updatedAt, stories: r.stories }, () => {
|
|
63
66
|
if (r.stories.length === 0)
|
|
64
|
-
return info('
|
|
67
|
+
return info('暂无热点');
|
|
65
68
|
info(bold('AI 热榜') + dim(` ${siteUrl('/news/')}`));
|
|
66
|
-
|
|
69
|
+
for (const s of r.stories) {
|
|
70
|
+
info(`${String(s.rank).padStart(2)}. ${bold(s.title)}${dim(` ${s.sourceCount} 个来源`)}`);
|
|
71
|
+
if (s.latest)
|
|
72
|
+
info(` ${truncate(s.latest, width() - 5)}`);
|
|
73
|
+
info(dim(` ${s.url}`));
|
|
74
|
+
}
|
|
75
|
+
info('');
|
|
76
|
+
info(dim('数据来自 AIHOT(aihot.news)'));
|
|
67
77
|
});
|
|
68
78
|
}
|
|
69
79
|
export async function docsSearch(query, agent, limit) {
|
|
70
80
|
if (!query)
|
|
71
|
-
fail('usage', '
|
|
81
|
+
fail('usage', '缺少关键词', '例如 funcoding docs search hooks --agent=claude-code');
|
|
72
82
|
const params = new URLSearchParams({ q: query, limit: String(limit), ...(agent ? { agent } : {}) });
|
|
73
83
|
const r = await api(`/api/cli/docs/search?${params}`);
|
|
74
84
|
done({ docs: r.docs }, () => {
|
|
75
85
|
if (r.docs.length === 0)
|
|
76
|
-
return info(
|
|
86
|
+
return info(`没有找到和「${query}」相关的文档`);
|
|
77
87
|
for (const d of r.docs) {
|
|
78
88
|
info(`${cyan(d.id)} ${bold(d.title)} ${dim(d.agent)}`);
|
|
79
89
|
if (d.description)
|
|
80
90
|
info(` ${truncate(d.description, (process.stdout.columns || 100) - 4)}`);
|
|
81
91
|
}
|
|
82
92
|
info('');
|
|
83
|
-
|
|
93
|
+
next(cyan('funcoding docs fetch <id>'));
|
|
84
94
|
});
|
|
85
95
|
}
|
|
86
96
|
export async function docsFetch(id) {
|
|
@@ -90,10 +100,10 @@ export async function docsFetch(id) {
|
|
|
90
100
|
.replace(/^\/?agents\//, '')
|
|
91
101
|
.replace(/^\/+|\/+$/g, '');
|
|
92
102
|
if (!/^[a-z0-9][\w-]*(\/[\w.-]+)*$/i.test(path))
|
|
93
|
-
fail('usage', '
|
|
103
|
+
fail('usage', `无法识别的文档路径:${path}`, '写成 <智能体>/<路径>,例如 claude-code/build/hooks');
|
|
94
104
|
const markdown = await api(`/api/cli/docs/${path}`, { headers: { Accept: 'text/markdown' } }).catch((err) => {
|
|
95
105
|
if (err instanceof CliError && err.code === 'not_found')
|
|
96
|
-
fail('not_found', `没有找到文档:${path}`, '运行 funcoding docs search <关键词>
|
|
106
|
+
fail('not_found', `没有找到文档:${path}`, '运行 funcoding docs search <关键词>');
|
|
97
107
|
throw err;
|
|
98
108
|
});
|
|
99
109
|
done({ id: path, url: siteUrl(`/agents/${path}/`), markdown }, () => info(markdown));
|
package/dist/info.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { AGENT_IDS, AGENTS, currentAgent, installedAgents, skillsRoot } from './agents.js';
|
|
3
3
|
import { SITE, VERSION } from './api.js';
|
|
4
4
|
import { configDir, credentialsPath, loadCredentials } from './credentials.js';
|
|
5
|
-
import { onPath } from './
|
|
5
|
+
import { AGENT_SKILL, agentSkillDirs, onPath } from './setup.js';
|
|
6
6
|
import { bold, dim, done, fail, info } from './ui.js';
|
|
7
7
|
export async function envInfo(field) {
|
|
8
8
|
const creds = await loadCredentials(SITE);
|
|
@@ -14,6 +14,8 @@ export async function envInfo(field) {
|
|
|
14
14
|
/** 是否已全局安装(终端里能直接用 funcoding / fun) */
|
|
15
15
|
global: onPath('funcoding'),
|
|
16
16
|
user: creds?.user ?? null,
|
|
17
|
+
/** 已安装的 funcoding-cli Skill 目录 */
|
|
18
|
+
agentSkill: agentSkillDirs(),
|
|
17
19
|
currentAgent: currentAgent(),
|
|
18
20
|
agents: AGENT_IDS.map((id) => ({ id, installed: found.includes(id), skills: { user: skillsRoot(id, 'user'), project: skillsRoot(id, 'project') } })),
|
|
19
21
|
configDir: configDir(),
|
|
@@ -30,11 +32,12 @@ export async function envInfo(field) {
|
|
|
30
32
|
row('version', VERSION);
|
|
31
33
|
row('site', SITE);
|
|
32
34
|
row('node', process.versions.node);
|
|
33
|
-
row('global', data.global ? '
|
|
35
|
+
row('global', data.global ? '已安装(funcoding、fun)' : '未安装,运行 npx --yes funcoding-cli@latest cli setup');
|
|
36
|
+
row('agentSkill', data.agentSkill.length > 0 ? data.agentSkill.join('、') : `未安装,运行 funcoding cli setup 安装 ${AGENT_SKILL}`);
|
|
34
37
|
row('user', creds?.user ? `${creds.user.name}${creds.user.githubLogin ? ` (@${creds.user.githubLogin})` : ''}` : '未登录');
|
|
35
38
|
row('currentAgent', data.currentAgent ? AGENTS[data.currentAgent].label : '无');
|
|
36
39
|
for (const id of AGENT_IDS)
|
|
37
|
-
row(id, `${found.includes(id) ? '
|
|
40
|
+
row(id, `${found.includes(id) ? '已检测到' : dim('未检测到')} ${dim(skillsRoot(id, 'user'))}`);
|
|
38
41
|
row('configDir', configDir());
|
|
39
42
|
});
|
|
40
43
|
}
|
package/dist/install.js
CHANGED
|
@@ -10,8 +10,8 @@ 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 {
|
|
14
|
-
import { bold, CliError, cyan, dim, done, fail, green, info, stars, warn } from './ui.js';
|
|
13
|
+
import { AGENT_SKILL, agentSkillContent, installAgentSkill } from './setup.js';
|
|
14
|
+
import { bold, CliError, cyan, dim, done, fail, green, info, next, stars, step, success, warn } from './ui.js';
|
|
15
15
|
/** 和服务端一致的上限 */
|
|
16
16
|
const MAX_FILES = 200;
|
|
17
17
|
const MAX_BYTES = 10 * 1024 * 1024;
|
|
@@ -34,7 +34,7 @@ async function download(url, maxBytes) {
|
|
|
34
34
|
}
|
|
35
35
|
const buf = new Uint8Array(await res.arrayBuffer());
|
|
36
36
|
if (buf.byteLength > maxBytes)
|
|
37
|
-
throw new CliError('error',
|
|
37
|
+
throw new CliError('error', `文件超过 10 MB:${url}`);
|
|
38
38
|
return buf;
|
|
39
39
|
}
|
|
40
40
|
async function mapLimit(items, limit, fn) {
|
|
@@ -60,13 +60,13 @@ export async function moveAway(dir, agent, folder) {
|
|
|
60
60
|
export async function fetchSkill(spec) {
|
|
61
61
|
const id = parseSkillSpec(spec);
|
|
62
62
|
if (!id)
|
|
63
|
-
return fail('usage',
|
|
63
|
+
return fail('usage', `无法识别的 Skill:${spec}`, `写成 owner/repo/skill,或 ${SITE}/skills/ 下的详情页地址`);
|
|
64
64
|
try {
|
|
65
65
|
return await api(`/api/cli/skills/${id}`);
|
|
66
66
|
}
|
|
67
67
|
catch (err) {
|
|
68
68
|
if (err instanceof CliError && err.code === 'not_found')
|
|
69
|
-
fail('not_found', `没有找到 Skill:${id}`, '运行 funcoding skills find <关键词>
|
|
69
|
+
fail('not_found', `没有找到 Skill:${id}`, '运行 funcoding skills find <关键词>');
|
|
70
70
|
throw err;
|
|
71
71
|
}
|
|
72
72
|
}
|
|
@@ -82,7 +82,7 @@ export async function skillInfo(spec) {
|
|
|
82
82
|
info(dim(`${s.repo.url}${s.path ? `/tree/${s.repo.branch}/${s.path}` : ''}`));
|
|
83
83
|
info(dim(page));
|
|
84
84
|
info('');
|
|
85
|
-
info(bold(`文件(${s.files.length}
|
|
85
|
+
info(bold(`文件(${s.files.length} 个${s.truncated ? ',未列全' : ''})`));
|
|
86
86
|
for (const f of s.files)
|
|
87
87
|
info(` ${f.path}${scripts.includes(f.path) ? dim(' 可执行脚本') : ''}`);
|
|
88
88
|
info('');
|
|
@@ -96,12 +96,12 @@ async function downloadTo(skill, dir) {
|
|
|
96
96
|
await mapLimit(skill.files, 4, async (f) => {
|
|
97
97
|
const target = resolveInside(dir, f.path);
|
|
98
98
|
if (!target)
|
|
99
|
-
throw new CliError('error',
|
|
99
|
+
throw new CliError('error', `已拒绝超出目标目录的路径:${f.path}`);
|
|
100
100
|
const url = skill.rawBase + f.path.split('/').map(encodeURIComponent).join('/');
|
|
101
101
|
const data = await download(url, MAX_BYTES);
|
|
102
102
|
total += data.byteLength;
|
|
103
103
|
if (total > MAX_BYTES)
|
|
104
|
-
throw new CliError('error', 'Skill
|
|
104
|
+
throw new CliError('error', 'Skill 超过 10 MB', `从仓库手动安装:${skill.repo.url}`);
|
|
105
105
|
await mkdir(dirname(target), { recursive: true });
|
|
106
106
|
await writeFile(target, data);
|
|
107
107
|
});
|
|
@@ -112,13 +112,13 @@ async function downloadTo(skill, dir) {
|
|
|
112
112
|
/** 装到多个目标:只下载一次,其余目标复制 */
|
|
113
113
|
export async function installSkill(skill, targets, force) {
|
|
114
114
|
if (!isSafeFolderName(skill.folder))
|
|
115
|
-
fail('error',
|
|
115
|
+
fail('error', `目录名不合法:${skill.folder}`);
|
|
116
116
|
if (skill.files.length === 0 || !skill.files.some((f) => f.path === 'SKILL.md'))
|
|
117
|
-
fail('not_found', `${skill.id}
|
|
117
|
+
fail('not_found', `${skill.id} 缺少 SKILL.md,可能已从仓库删除`);
|
|
118
118
|
if (skill.files.length > MAX_FILES)
|
|
119
|
-
fail('error',
|
|
119
|
+
fail('error', `文件超过 ${MAX_FILES} 个(${skill.files.length} 个)`, `从仓库手动安装:${skill.repo.url}`);
|
|
120
120
|
if (skill.truncated)
|
|
121
|
-
warn(
|
|
121
|
+
warn(`文件过多,只安装前 ${skill.files.length} 个,其余从仓库补齐:${skill.repo.url}`);
|
|
122
122
|
const plan = targets.map((t) => ({ ...t, root: skillsRoot(t.agent, t.scope), dest: join(skillsRoot(t.agent, t.scope), skill.folder) }));
|
|
123
123
|
if (!force) {
|
|
124
124
|
const taken = [];
|
|
@@ -126,9 +126,9 @@ export async function installSkill(skill, targets, force) {
|
|
|
126
126
|
if (await exists(p.dest))
|
|
127
127
|
taken.push(p.dest);
|
|
128
128
|
if (taken.length > 0)
|
|
129
|
-
fail('exists',
|
|
129
|
+
fail('exists', `已存在:${taken.join('、')}`, '覆盖请加 --force(会先备份)', { paths: taken });
|
|
130
130
|
}
|
|
131
|
-
|
|
131
|
+
step(`正在安装 ${skill.id}(${skill.files.length} 个文件)`);
|
|
132
132
|
const results = [];
|
|
133
133
|
let source = null;
|
|
134
134
|
for (const p of plan) {
|
|
@@ -151,23 +151,18 @@ export async function installSkill(skill, targets, force) {
|
|
|
151
151
|
}
|
|
152
152
|
return results;
|
|
153
153
|
}
|
|
154
|
-
function printInstalled(r) {
|
|
155
|
-
const a = AGENTS[r.agent];
|
|
156
|
-
info(green(`✓ 已安装到 ${r.dir}`));
|
|
157
|
-
if (r.backup)
|
|
158
|
-
info(dim(` 旧版本备份在 ${r.backup}`));
|
|
159
|
-
info(` 在 ${a.label} 里输入 ${cyan(a.invoke(r.dir.split(/[\\/]/).pop()))} 调用,或直接描述任务让它自动选用。`);
|
|
160
|
-
}
|
|
161
154
|
export async function install(specs, targets, force) {
|
|
162
155
|
if (specs.length === 0)
|
|
163
|
-
fail('usage', '缺少要安装的 Skill
|
|
156
|
+
fail('usage', '缺少要安装的 Skill', '例如 funcoding skills add anthropics/skills/pdf');
|
|
164
157
|
const installed = [];
|
|
165
158
|
for (const spec of specs)
|
|
166
159
|
installed.push(...(await installSkill(await fetchSkill(spec), targets, force)));
|
|
167
160
|
done({ installed }, () => {
|
|
168
161
|
for (const r of installed)
|
|
169
|
-
|
|
170
|
-
|
|
162
|
+
success(`已安装 ${r.dir.split(/[\\/]/).pop()}(${AGENTS[r.agent].label})`, [r.dir, r.backup && `备份:${r.backup}`]);
|
|
163
|
+
warn('Skill 可能运行其中的脚本,使用前先阅读 SKILL.md');
|
|
164
|
+
const invokes = [...new Set(installed.map((r) => `${AGENTS[r.agent].label} 输入 ${cyan(AGENTS[r.agent].invoke(r.dir.split(/[\\/]/).pop()))}`))];
|
|
165
|
+
next(`新开会话,${invokes.join(',')}`);
|
|
171
166
|
});
|
|
172
167
|
}
|
|
173
168
|
const frontmatter = (text, key) => new RegExp(`^${key}:\\s*["']?(.+?)["']?\\s*$`, 'm').exec(/^---\r?\n([\s\S]*?)\r?\n---/.exec(text)?.[1] ?? '')?.[1];
|
|
@@ -199,14 +194,16 @@ export async function scanAll(sel) {
|
|
|
199
194
|
export async function list(sel) {
|
|
200
195
|
const items = await scanAll(sel);
|
|
201
196
|
done({ skills: items }, () => {
|
|
202
|
-
if (items.length === 0)
|
|
203
|
-
|
|
197
|
+
if (items.length === 0) {
|
|
198
|
+
info('暂无已安装的 Skill');
|
|
199
|
+
return next(cyan('funcoding skills find <关键词>'));
|
|
200
|
+
}
|
|
204
201
|
for (const agent of AGENT_IDS) {
|
|
205
202
|
for (const scope of sel.scopes) {
|
|
206
203
|
const group = items.filter((s) => s.agent === agent && s.scope === scope);
|
|
207
204
|
if (group.length === 0)
|
|
208
205
|
continue;
|
|
209
|
-
info(`${bold(AGENTS[agent].label)} ${dim(`${scope === 'user' ? '
|
|
206
|
+
info(`${bold(AGENTS[agent].label)} ${dim(`${scope === 'user' ? '用户目录' : '当前项目'} ${skillsRoot(agent, scope)}`)}`);
|
|
210
207
|
for (const s of group)
|
|
211
208
|
info(` ${s.folder}${s.source ? dim(` ← ${s.source.id}`) : ''}`);
|
|
212
209
|
info('');
|
|
@@ -215,12 +212,13 @@ export async function list(sel) {
|
|
|
215
212
|
});
|
|
216
213
|
}
|
|
217
214
|
// ---------------- update ----------------
|
|
218
|
-
|
|
215
|
+
// 旧版本的 funcoding Skill 也按 funcoding-cli 匹配,刷新时换成新版
|
|
216
|
+
const matches = (s, name) => !name || s.folder === name || s.source?.id === name.toLowerCase() || (!!s.source?.builtin && name === AGENT_SKILL);
|
|
219
217
|
/** 按 .funcoding.json 记录的来源重新安装;check 时只比较远端 SKILL.md 有没有变化 */
|
|
220
218
|
export async function update(name, sel, check) {
|
|
221
219
|
const items = (await scanAll(sel)).filter((s) => s.source && matches(s, name));
|
|
222
220
|
if (items.length === 0)
|
|
223
|
-
fail('not_found', name ? `没有找到由 funcoding 安装的 ${name}` : '
|
|
221
|
+
fail('not_found', name ? `没有找到由 funcoding 安装的 ${name}` : '暂无由 funcoding 安装的 Skill', '运行 funcoding skills list');
|
|
224
222
|
const results = [];
|
|
225
223
|
const cache = new Map();
|
|
226
224
|
for (const s of items) {
|
|
@@ -228,10 +226,10 @@ export async function update(name, sel, check) {
|
|
|
228
226
|
if (s.source.builtin) {
|
|
229
227
|
const local = await readFile(join(s.dir, 'SKILL.md'), 'utf8').catch(() => null);
|
|
230
228
|
if (check)
|
|
231
|
-
results.push({ id, agent: s.agent, scope: s.scope, dir: s.dir, status: local === (await
|
|
229
|
+
results.push({ id: AGENT_SKILL, agent: s.agent, scope: s.scope, dir: s.dir, status: local === (await agentSkillContent()) ? 'up_to_date' : 'outdated' });
|
|
232
230
|
else {
|
|
233
|
-
await
|
|
234
|
-
results.push({ id, agent: s.agent, scope: s.scope, dir:
|
|
231
|
+
const [r] = await installAgentSkill([{ agent: s.agent, scope: s.scope }], true);
|
|
232
|
+
results.push({ id: AGENT_SKILL, agent: s.agent, scope: s.scope, dir: r.dir, status: 'updated' });
|
|
235
233
|
}
|
|
236
234
|
continue;
|
|
237
235
|
}
|
|
@@ -250,29 +248,29 @@ export async function update(name, sel, check) {
|
|
|
250
248
|
done({ skills: results }, () => {
|
|
251
249
|
const label = { updated: green('已更新'), up_to_date: dim('已是最新'), outdated: cyan('有更新'), unknown: dim('无法比较') };
|
|
252
250
|
for (const r of results)
|
|
253
|
-
info(`${label[r.status]} ${r.id}
|
|
251
|
+
info(`${label[r.status]} ${r.id} ${dim(r.dir)}`);
|
|
254
252
|
if (check && results.some((r) => r.status === 'outdated'))
|
|
255
|
-
|
|
253
|
+
next(cyan('funcoding skills update'));
|
|
256
254
|
});
|
|
257
255
|
}
|
|
258
256
|
// ---------------- remove ----------------
|
|
259
257
|
export async function remove(names, sel, force) {
|
|
260
258
|
if (names.length === 0)
|
|
261
|
-
fail('usage', '缺少要删除的 Skill
|
|
259
|
+
fail('usage', '缺少要删除的 Skill', '运行 funcoding skills list');
|
|
262
260
|
const all = await scanAll(sel);
|
|
263
261
|
const removed = [];
|
|
264
262
|
for (const name of names) {
|
|
265
263
|
const found = all.filter((s) => s.folder === name || s.source?.id === name.toLowerCase());
|
|
266
264
|
if (found.length === 0)
|
|
267
|
-
fail('not_found', `没有找到已安装的 ${name}`, '运行 funcoding skills list
|
|
265
|
+
fail('not_found', `没有找到已安装的 ${name}`, '运行 funcoding skills list');
|
|
268
266
|
const manual = found.filter((s) => !s.source);
|
|
269
267
|
if (manual.length > 0 && !force)
|
|
270
|
-
fail('usage',
|
|
268
|
+
fail('usage', `不是由 funcoding 安装的:${manual.map((s) => s.dir).join('、')}`, '删除请加 --force(会先备份)', { paths: manual.map((s) => s.dir) });
|
|
271
269
|
for (const s of found)
|
|
272
270
|
removed.push({ id: s.source?.id ?? null, agent: s.agent, scope: s.scope, dir: s.dir, backup: await moveAway(s.dir, s.agent, s.folder) });
|
|
273
271
|
}
|
|
274
272
|
done({ removed }, () => {
|
|
275
273
|
for (const r of removed)
|
|
276
|
-
|
|
274
|
+
success(`已删除 ${r.dir.split(/[\\/]/).pop()}(${AGENTS[r.agent].label})`, [r.dir, `备份:${r.backup}`]);
|
|
277
275
|
});
|
|
278
276
|
}
|