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 +38 -12
- package/dist/auth.js +16 -18
- package/dist/cli.js +8 -234
- package/dist/commands.js +238 -0
- package/dist/content.js +11 -11
- package/dist/info.js +6 -3
- package/dist/install.js +36 -38
- package/dist/lists.js +5 -5
- package/dist/mcp.js +25 -28
- package/dist/setup.js +140 -0
- package/dist/skill-docs.js +36 -0
- package/dist/sync.js +12 -8
- 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 -104
package/dist/mcp.js
CHANGED
|
@@ -11,7 +11,7 @@ import { AGENT_IDS, AGENTS, codexHome } from './agents.js';
|
|
|
11
11
|
import { api, siteUrl } from './api.js';
|
|
12
12
|
import { jsonServerNames, setJsonServer, setTomlServer, tomlServerNames } from './config-files.js';
|
|
13
13
|
import { configDir } from './credentials.js';
|
|
14
|
-
import { bold, CliError, dim, done, fail,
|
|
14
|
+
import { bold, CliError, cyan, dim, done, fail, info, next, stars, success, warn } from './ui.js';
|
|
15
15
|
/** 接受 owner/repo[/sub],或者网站上的详情页地址 */
|
|
16
16
|
export function parseMcpSpec(input) {
|
|
17
17
|
const s = input.trim();
|
|
@@ -22,13 +22,13 @@ export function parseMcpSpec(input) {
|
|
|
22
22
|
export async function fetchMcp(spec) {
|
|
23
23
|
const id = parseMcpSpec(spec);
|
|
24
24
|
if (!id)
|
|
25
|
-
return fail('usage',
|
|
25
|
+
return fail('usage', `无法识别的 MCP Server:${spec}`, '写成 owner/repo、owner/repo/sub,或网站上的详情页地址');
|
|
26
26
|
try {
|
|
27
27
|
return await api(`/api/cli/mcp/${id}`);
|
|
28
28
|
}
|
|
29
29
|
catch (err) {
|
|
30
30
|
if (err instanceof CliError && err.code === 'not_found')
|
|
31
|
-
fail('not_found', `没有找到 MCP Server:${id}`, '运行 funcoding mcp find <关键词>
|
|
31
|
+
fail('not_found', `没有找到 MCP Server:${id}`, '运行 funcoding mcp find <关键词>');
|
|
32
32
|
throw err;
|
|
33
33
|
}
|
|
34
34
|
}
|
|
@@ -47,7 +47,7 @@ export async function mcpInfo(spec) {
|
|
|
47
47
|
info(dim(page));
|
|
48
48
|
info('');
|
|
49
49
|
if (s.launches.length === 0)
|
|
50
|
-
return info(
|
|
50
|
+
return info(`没有标准安装包,按 README 手动配置:${s.repo.url}`);
|
|
51
51
|
info(bold(`启动方式(配置名 ${s.configName})`));
|
|
52
52
|
for (const l of s.launches) {
|
|
53
53
|
info(` ${describeLaunch(l)}`);
|
|
@@ -73,7 +73,7 @@ async function configuredNames(loc) {
|
|
|
73
73
|
return loc.format === 'toml' ? tomlServerNames(text) : jsonServerNames(text);
|
|
74
74
|
}
|
|
75
75
|
catch {
|
|
76
|
-
warn(
|
|
76
|
+
warn(`无法解析,已跳过:${file}`);
|
|
77
77
|
return [];
|
|
78
78
|
}
|
|
79
79
|
}
|
|
@@ -96,7 +96,7 @@ async function writeConfig(loc, name, config) {
|
|
|
96
96
|
next = setJsonServer(text, name, config ? jsonEntry(loc.agent, config) : null);
|
|
97
97
|
}
|
|
98
98
|
catch (err) {
|
|
99
|
-
return fail('error',
|
|
99
|
+
return fail('error', `无法解析:${loc.file}(${err.message})`, '修正这个文件后重试');
|
|
100
100
|
}
|
|
101
101
|
}
|
|
102
102
|
const backup = await backupFile(loc.file, loc.agent);
|
|
@@ -147,25 +147,25 @@ export function toConfig(l, opts) {
|
|
|
147
147
|
/** 配置一个 Server 到多个目标;失败抛 CliError */
|
|
148
148
|
export async function addServer(s, opts) {
|
|
149
149
|
if (s.devtool)
|
|
150
|
-
warn(`${s.name}
|
|
150
|
+
warn(`${s.name} 属于 MCP 开发工具(SDK、框架等),可能不能直接运行`);
|
|
151
151
|
if (s.launches.length === 0)
|
|
152
|
-
fail('not_found', `${s.name}
|
|
152
|
+
fail('not_found', `${s.name} 没有标准安装包`, `按 README 手动配置:${s.repo.url}`, { readme: s.repo.url });
|
|
153
153
|
const name = opts.name ?? s.configName;
|
|
154
154
|
if (!/^[A-Za-z0-9_-]{1,64}$/.test(name))
|
|
155
|
-
fail('usage',
|
|
155
|
+
fail('usage', `配置名只能用字母、数字、下划线和连字符:${name}`);
|
|
156
156
|
// 先检查所有目标,确定都能写再动手
|
|
157
157
|
const plan = [];
|
|
158
158
|
for (const t of opts.targets) {
|
|
159
159
|
const picked = pickLaunch(s.launches, t.agent, opts);
|
|
160
160
|
if (!picked)
|
|
161
|
-
fail('not_found', `${s.name} 没有 ${AGENTS[t.agent].label} 支持的启动方式${opts.via ? `(--via ${opts.via})` : ''}
|
|
161
|
+
fail('not_found', `${s.name} 没有 ${AGENTS[t.agent].label} 支持的启动方式${opts.via ? `(--via ${opts.via})` : ''}`, `运行 funcoding mcp info ${s.id}`);
|
|
162
162
|
if ('missing' in picked) {
|
|
163
163
|
const { launch, inputs } = picked.missing;
|
|
164
164
|
fail('usage', `${s.name} 需要提供:${inputs.join('、')}`, `向用户索取后运行 funcoding mcp add ${s.id} ${inputs.map((i) => `${flagOf(launch)} ${i}=<值>`).join(' ')}`, { missing: inputs, flag: flagOf(launch) });
|
|
165
165
|
}
|
|
166
166
|
const loc = locationOf(t.agent, t.scope);
|
|
167
167
|
if (!opts.force && (await configuredNames(loc)).includes(name))
|
|
168
|
-
fail('exists', `${AGENTS[t.agent].label}
|
|
168
|
+
fail('exists', `${AGENTS[t.agent].label} 已配置 ${name}`, '覆盖请加 --force');
|
|
169
169
|
plan.push({ loc, config: toConfig(picked.launch, opts), launch: picked.launch });
|
|
170
170
|
}
|
|
171
171
|
const results = [];
|
|
@@ -183,7 +183,7 @@ export async function addServer(s, opts) {
|
|
|
183
183
|
if (r)
|
|
184
184
|
fail('error', `claude mcp add-json 失败:${r.output}`);
|
|
185
185
|
if (!loc.file)
|
|
186
|
-
fail('error', '没有找到 claude 命令,无法写入 Claude Code
|
|
186
|
+
fail('error', '没有找到 claude 命令,无法写入 Claude Code 的用户配置', `安装 Claude Code 后运行:claude mcp add-json ${name} ${shellQuote(json)} --scope user`);
|
|
187
187
|
}
|
|
188
188
|
const backup = await writeConfig(loc, name, config);
|
|
189
189
|
results.push({ ...base, file: loc.file, ...(backup ? { backup } : {}) });
|
|
@@ -193,16 +193,11 @@ export async function addServer(s, opts) {
|
|
|
193
193
|
export async function mcpAdd(spec, opts) {
|
|
194
194
|
const results = await addServer(await fetchMcp(spec), opts);
|
|
195
195
|
done({ added: results }, () => {
|
|
196
|
-
for (const r of results)
|
|
197
|
-
|
|
198
|
-
if (r.file)
|
|
199
|
-
info(dim(` ${r.file}`));
|
|
200
|
-
if (r.backup)
|
|
201
|
-
info(dim(` 原文件备份在 ${r.backup}`));
|
|
202
|
-
}
|
|
196
|
+
for (const r of results)
|
|
197
|
+
success(`已配置 ${r.name}(${AGENTS[r.agent].label},${r.scope === 'user' ? '用户目录' : '当前项目'})`, [r.launch, r.file, r.backup && `备份:${r.backup}`]);
|
|
203
198
|
if (results.some((r) => r.agent === 'claude-code' && r.scope === 'project'))
|
|
204
|
-
|
|
205
|
-
|
|
199
|
+
warn('当前项目的 Server 需要在 Claude Code 里确认后才会启用');
|
|
200
|
+
next('重启智能体或新开会话');
|
|
206
201
|
});
|
|
207
202
|
}
|
|
208
203
|
// ---------------- list / remove ----------------
|
|
@@ -215,14 +210,16 @@ export async function mcpList(sel) {
|
|
|
215
210
|
servers.push({ agent, scope, name, file: loc.file ?? join(homedir(), '.claude.json') });
|
|
216
211
|
}
|
|
217
212
|
done({ servers }, () => {
|
|
218
|
-
if (servers.length === 0)
|
|
219
|
-
|
|
213
|
+
if (servers.length === 0) {
|
|
214
|
+
info('暂无已配置的 MCP Server');
|
|
215
|
+
return next(cyan('funcoding mcp find <关键词>'));
|
|
216
|
+
}
|
|
220
217
|
for (const agent of AGENT_IDS)
|
|
221
218
|
for (const scope of sel.scopes) {
|
|
222
219
|
const group = servers.filter((s) => s.agent === agent && s.scope === scope);
|
|
223
220
|
if (group.length === 0)
|
|
224
221
|
continue;
|
|
225
|
-
info(`${bold(AGENTS[agent].label)} ${dim(`${scope === 'user' ? '
|
|
222
|
+
info(`${bold(AGENTS[agent].label)} ${dim(`${scope === 'user' ? '用户目录' : '当前项目'} ${group[0].file}`)}`);
|
|
226
223
|
for (const s of group)
|
|
227
224
|
info(` ${s.name}`);
|
|
228
225
|
info('');
|
|
@@ -231,7 +228,7 @@ export async function mcpList(sel) {
|
|
|
231
228
|
}
|
|
232
229
|
export async function mcpRemove(names, sel) {
|
|
233
230
|
if (names.length === 0)
|
|
234
|
-
fail('usage', '
|
|
231
|
+
fail('usage', '缺少要删除的配置名', '运行 funcoding mcp list');
|
|
235
232
|
const removed = [];
|
|
236
233
|
for (const name of names) {
|
|
237
234
|
let found = false;
|
|
@@ -250,16 +247,16 @@ export async function mcpRemove(names, sel) {
|
|
|
250
247
|
if (r)
|
|
251
248
|
fail('error', `claude mcp remove 失败:${r.output}`);
|
|
252
249
|
if (!loc.file)
|
|
253
|
-
fail('error', '没有找到 claude 命令,无法修改 Claude Code
|
|
250
|
+
fail('error', '没有找到 claude 命令,无法修改 Claude Code 的用户配置', `安装 Claude Code 后运行:claude mcp remove ${name} --scope user`);
|
|
254
251
|
}
|
|
255
252
|
const backup = await writeConfig(loc, name, null);
|
|
256
253
|
removed.push({ agent, scope, name, file: loc.file, ...(backup ? { backup } : {}) });
|
|
257
254
|
}
|
|
258
255
|
if (!found)
|
|
259
|
-
fail('not_found', `没有找到配置名为 ${name} 的 MCP Server`, '运行 funcoding mcp list
|
|
256
|
+
fail('not_found', `没有找到配置名为 ${name} 的 MCP Server`, '运行 funcoding mcp list');
|
|
260
257
|
}
|
|
261
258
|
done({ removed }, () => {
|
|
262
259
|
for (const r of removed)
|
|
263
|
-
|
|
260
|
+
success(`已删除 ${r.name}(${AGENTS[r.agent].label})`, [r.file]);
|
|
264
261
|
});
|
|
265
262
|
}
|
package/dist/setup.js
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cli setup / cli upgrade,做法参照 sentry-cli(cli.sentry.dev/agentic-usage):
|
|
3
|
+
* - setup:没有全局安装时用 npm 全局安装本 CLI(--no-global 跳过),再把随 CLI 发布的 funcoding-cli Skill
|
|
4
|
+
* (skill/funcoding-cli/:SKILL.md + references/)装到本机检测到的各智能体的用户目录(--no-agent-skills 跳过),
|
|
5
|
+
* 让智能体知道什么时候、怎么用这个 CLI。Skill 打包在 CLI 里,不联网
|
|
6
|
+
* - upgrade:全局安装最新版,再用新版刷新已安装的 funcoding-cli Skill
|
|
7
|
+
* 旧版本装的 funcoding Skill(init 时代)在 setup 和刷新时一并删掉
|
|
8
|
+
*/
|
|
9
|
+
import { spawnSync } from 'node:child_process';
|
|
10
|
+
import { randomBytes } from 'node:crypto';
|
|
11
|
+
import { existsSync } from 'node:fs';
|
|
12
|
+
import { cp, mkdir, readFile, rename, rm, stat, writeFile } from 'node:fs/promises';
|
|
13
|
+
import { homedir } from 'node:os';
|
|
14
|
+
import { delimiter, join } from 'node:path';
|
|
15
|
+
import { AGENT_IDS, AGENTS, DEFAULT_AGENT, installedAgents, skillsRoot } from './agents.js';
|
|
16
|
+
import { USER_AGENT, VERSION } from './api.js';
|
|
17
|
+
import { META_FILE, moveAway } from './install.js';
|
|
18
|
+
import { done, fail, next, step, success, warn } from './ui.js';
|
|
19
|
+
const isWindows = process.platform === 'win32';
|
|
20
|
+
/** 随 CLI 发布的 Skill 的目录名,也是它的 name */
|
|
21
|
+
export const AGENT_SKILL = 'funcoding-cli';
|
|
22
|
+
/** 0.3 及以前 init 装的同类 Skill,setup 和刷新时删掉 */
|
|
23
|
+
const LEGACY_SKILL = 'funcoding';
|
|
24
|
+
const SOURCE_DIR = new URL('../skill/funcoding-cli/', import.meta.url);
|
|
25
|
+
/** PATH 上能不能找到这个命令;npx 运行时会把临时目录加进 PATH,那些目录不算 */
|
|
26
|
+
export function onPath(command, env = process.env) {
|
|
27
|
+
const exts = isWindows ? ['.cmd', '.exe', ''] : [''];
|
|
28
|
+
return (env.PATH ?? '')
|
|
29
|
+
.split(delimiter)
|
|
30
|
+
.filter((dir) => dir && !/[\\/]_npx[\\/]/.test(dir))
|
|
31
|
+
.some((dir) => exts.some((ext) => existsSync(join(dir, command + ext))));
|
|
32
|
+
}
|
|
33
|
+
const EACCES_DOC = 'https://docs.npmjs.com/resolving-eacces-permissions-errors-when-installing-packages-globally';
|
|
34
|
+
function npmInstall(spec) {
|
|
35
|
+
// npm 的输出写到 stderr,不影响 --json 的 stdout
|
|
36
|
+
return spawnSync('npm', ['install', '--global', spec], { stdio: ['ignore', process.stderr, process.stderr], shell: isWindows }).status === 0;
|
|
37
|
+
}
|
|
38
|
+
function installGlobal() {
|
|
39
|
+
if (onPath('funcoding'))
|
|
40
|
+
return { status: 'present' };
|
|
41
|
+
const pkg = `funcoding-cli@${VERSION}`;
|
|
42
|
+
step(`正在全局安装 funcoding-cli ${VERSION}`);
|
|
43
|
+
if (npmInstall(pkg))
|
|
44
|
+
return { status: 'installed' };
|
|
45
|
+
return { status: 'failed', hint: `运行 npm install -g ${pkg}(权限不足见 ${EACCES_DOC})` };
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* 要装 Skill 的智能体:本机检测到的全部;~/.agents 存在也算(Codex 和其他读 ~/.agents/skills 的智能体用它);
|
|
49
|
+
* 一个都没检测到时装给默认的 Claude Code
|
|
50
|
+
*/
|
|
51
|
+
export function skillAgents(home = homedir()) {
|
|
52
|
+
const found = new Set(installedAgents({ home }));
|
|
53
|
+
if (existsSync(join(home, '.agents')))
|
|
54
|
+
found.add('codex');
|
|
55
|
+
return found.size > 0 ? AGENT_IDS.filter((a) => found.has(a)) : [DEFAULT_AGENT];
|
|
56
|
+
}
|
|
57
|
+
/** 随 CLI 发布的 SKILL.md(skills update --check 用来比较) */
|
|
58
|
+
export const agentSkillContent = () => readFile(new URL('SKILL.md', SOURCE_DIR), 'utf8');
|
|
59
|
+
const exists = (p) => stat(p).then(() => true, () => false);
|
|
60
|
+
const isOurs = async (dir) => {
|
|
61
|
+
const meta = await readFile(join(dir, META_FILE), 'utf8').catch(() => null);
|
|
62
|
+
return !!meta && !!JSON.parse(meta).builtin;
|
|
63
|
+
};
|
|
64
|
+
/** 把 funcoding-cli Skill 装到这些智能体;同名但不是本 CLI 装的目录要 --force,并且先备份 */
|
|
65
|
+
export async function installAgentSkill(targets, force) {
|
|
66
|
+
const plan = targets.map((t) => ({ ...t, root: skillsRoot(t.agent, t.scope), dir: join(skillsRoot(t.agent, t.scope), AGENT_SKILL) }));
|
|
67
|
+
const foreign = [];
|
|
68
|
+
for (const p of plan)
|
|
69
|
+
if ((await exists(p.dir)) && !(await isOurs(p.dir)))
|
|
70
|
+
foreign.push(p.dir);
|
|
71
|
+
if (foreign.length > 0 && !force)
|
|
72
|
+
fail('exists', `已存在同名 Skill:${foreign.join('、')}`, '覆盖请加 --force(会先备份)', { paths: foreign });
|
|
73
|
+
const results = [];
|
|
74
|
+
for (const p of plan) {
|
|
75
|
+
await mkdir(p.root, { recursive: true });
|
|
76
|
+
const tmp = join(p.root, `.funcoding-tmp-${randomBytes(4).toString('hex')}`);
|
|
77
|
+
await cp(SOURCE_DIR, tmp, { recursive: true });
|
|
78
|
+
await writeFile(join(tmp, META_FILE), `${JSON.stringify({ id: AGENT_SKILL, name: AGENT_SKILL, builtin: true, installedAt: new Date().toISOString(), cli: USER_AGENT }, null, 2)}\n`);
|
|
79
|
+
let backup;
|
|
80
|
+
if (await exists(p.dir)) {
|
|
81
|
+
if (await isOurs(p.dir))
|
|
82
|
+
await rm(p.dir, { recursive: true, force: true });
|
|
83
|
+
else
|
|
84
|
+
backup = await moveAway(p.dir, p.agent, AGENT_SKILL);
|
|
85
|
+
}
|
|
86
|
+
await rename(tmp, p.dir);
|
|
87
|
+
// 旧版 funcoding Skill:只删本 CLI 装的
|
|
88
|
+
const legacy = join(p.root, LEGACY_SKILL);
|
|
89
|
+
const removedLegacy = (await isOurs(legacy)) ? legacy : undefined;
|
|
90
|
+
if (removedLegacy)
|
|
91
|
+
await rm(removedLegacy, { recursive: true, force: true });
|
|
92
|
+
results.push({ agent: p.agent, scope: p.scope, dir: p.dir, ...(backup ? { backup } : {}), ...(removedLegacy ? { removedLegacy } : {}) });
|
|
93
|
+
}
|
|
94
|
+
return results;
|
|
95
|
+
}
|
|
96
|
+
export async function setup(opts) {
|
|
97
|
+
const cli = opts.global ? installGlobal() : { status: 'skipped' };
|
|
98
|
+
const skills = opts.agentSkills ? await installAgentSkill(skillAgents().map((agent) => ({ agent, scope: 'user' })), opts.force) : [];
|
|
99
|
+
done({ version: VERSION, cli, skills }, () => {
|
|
100
|
+
if (cli.status === 'installed')
|
|
101
|
+
success(`已全局安装 funcoding-cli ${VERSION}`, ['命令:funcoding(简写 fun)']);
|
|
102
|
+
if (cli.status === 'present')
|
|
103
|
+
success(`已全局安装 funcoding-cli`, ['命令:funcoding(简写 fun)']);
|
|
104
|
+
if (cli.status === 'failed')
|
|
105
|
+
warn(`全局安装失败。${cli.hint}`);
|
|
106
|
+
if (skills.length > 0) {
|
|
107
|
+
success(`已为 ${skills.map((s) => AGENTS[s.agent].label).join('、')} 安装智能体 Skill:${AGENT_SKILL}`, [
|
|
108
|
+
...skills.map((s) => s.dir),
|
|
109
|
+
...skills.flatMap((s) => (s.backup ? [`备份:${s.backup}`] : [])),
|
|
110
|
+
]);
|
|
111
|
+
next('新开智能体会话,直接描述需求');
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
export function upgrade() {
|
|
116
|
+
step('正在全局安装 funcoding-cli@latest');
|
|
117
|
+
if (!npmInstall('funcoding-cli@latest'))
|
|
118
|
+
fail('error', '升级失败', `运行 npm install -g funcoding-cli@latest(权限不足见 ${EACCES_DOC})`);
|
|
119
|
+
const version = spawnSync('funcoding', ['--version'], { encoding: 'utf8', shell: isWindows }).stdout?.trim() || null;
|
|
120
|
+
// 用新版本刷新已安装的智能体 Skill(包括旧版的 funcoding);一个都没装过时会报未找到,忽略
|
|
121
|
+
const refreshed = spawnSync('funcoding', ['skills', 'update', AGENT_SKILL, '--json'], { encoding: 'utf8', shell: isWindows });
|
|
122
|
+
const skills = (() => {
|
|
123
|
+
try {
|
|
124
|
+
return JSON.parse(refreshed.stdout).skills ?? [];
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
return [];
|
|
128
|
+
}
|
|
129
|
+
})();
|
|
130
|
+
done({ from: VERSION, to: version, skills }, () => {
|
|
131
|
+
if (version && version !== VERSION)
|
|
132
|
+
success(`已升级到 funcoding-cli ${version}`, [`原版本 ${VERSION}`]);
|
|
133
|
+
else
|
|
134
|
+
success(`已是最新版本 ${version ?? VERSION}`);
|
|
135
|
+
if (skills.length > 0)
|
|
136
|
+
success('已刷新智能体 Skill', skills.map((s) => s.dir));
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
/** 用于 info:本机已装的 funcoding-cli Skill 目录 */
|
|
140
|
+
export const agentSkillDirs = (home = homedir()) => AGENT_IDS.map((a) => join(home, ...AGENTS[a].skills, AGENT_SKILL)).filter((d) => existsSync(d));
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 由命令表生成 funcoding-cli Skill 的 references/<分组>.md(每组命令一份,智能体按需读取),
|
|
3
|
+
* 并把 SKILL.md frontmatter 里的 version 同步成 package.json 的版本。
|
|
4
|
+
* 写文件在 scripts/gen-skill.ts(pnpm skill),skill-docs.test.ts 检查仓库里的文件是不是最新的
|
|
5
|
+
*/
|
|
6
|
+
import { BIN, COMMANDS, GLOBAL, OPTIONS } from './commands.js';
|
|
7
|
+
function renderCommand(group, cmd) {
|
|
8
|
+
const params = (cmd.params ?? []).map(([p]) => p).join(' ');
|
|
9
|
+
const out = [`## ${[BIN, group.name, cmd.name, params].filter(Boolean).join(' ')}`, '', cmd.summary];
|
|
10
|
+
if (cmd.params?.length)
|
|
11
|
+
out.push('', '参数:', '', ...cmd.params.map(([p, d]) => `- \`${p}\`:${d}`));
|
|
12
|
+
if (cmd.options?.length)
|
|
13
|
+
out.push('', '选项:', '', ...cmd.options.map((k) => `- \`${OPTIONS[k].flag}\`:${OPTIONS[k].desc}`));
|
|
14
|
+
if (cmd.examples?.length)
|
|
15
|
+
out.push('', '示例:', '', '```bash', ...cmd.examples, '```');
|
|
16
|
+
return out.join('\n');
|
|
17
|
+
}
|
|
18
|
+
/** 文件名 → 内容 */
|
|
19
|
+
export function renderReferences() {
|
|
20
|
+
const out = {};
|
|
21
|
+
const global = GLOBAL.map((k) => `\`${OPTIONS[k].flag}\``).join('、');
|
|
22
|
+
for (const group of COMMANDS.filter((c) => c.commands)) {
|
|
23
|
+
const body = [
|
|
24
|
+
`# ${BIN} ${group.name}`,
|
|
25
|
+
'',
|
|
26
|
+
group.summary,
|
|
27
|
+
'',
|
|
28
|
+
`所有命令都支持 ${global}。本文件由 scripts/gen-skill.ts 根据命令表生成。`,
|
|
29
|
+
...group.commands.flatMap((cmd) => ['', renderCommand(group, cmd)]),
|
|
30
|
+
];
|
|
31
|
+
out[`${group.name}.md`] = `${body.join('\n')}\n`;
|
|
32
|
+
}
|
|
33
|
+
return out;
|
|
34
|
+
}
|
|
35
|
+
/** 把 SKILL.md frontmatter 里的 version 换成 version */
|
|
36
|
+
export const withVersion = (skillMd, version) => skillMd.replace(/^version: .*$/m, `version: ${version}`);
|
package/dist/sync.js
CHANGED
|
@@ -2,7 +2,7 @@ import { siteUrl } from './api.js';
|
|
|
2
2
|
import { fetchMe, requireToken } from './auth.js';
|
|
3
3
|
import { fetchSkill, installSkill, scanAll } from './install.js';
|
|
4
4
|
import { addServer, fetchMcp } from './mcp.js';
|
|
5
|
-
import { bold, CliError, cyan, dim, done, green, info, stars, truncate, yellow } from './ui.js';
|
|
5
|
+
import { bold, CliError, cyan, dim, done, green, info, next, stars, truncate, yellow } from './ui.js';
|
|
6
6
|
export function printCards(title, cards, hrefOf) {
|
|
7
7
|
if (cards.length === 0)
|
|
8
8
|
return;
|
|
@@ -19,19 +19,23 @@ export async function favorites() {
|
|
|
19
19
|
const me = await fetchMe(await requireToken());
|
|
20
20
|
const { skills, mcp } = me.favorites;
|
|
21
21
|
done({ skills, mcp }, () => {
|
|
22
|
-
if (skills.length + mcp.length === 0)
|
|
23
|
-
|
|
22
|
+
if (skills.length + mcp.length === 0) {
|
|
23
|
+
info('暂无收藏');
|
|
24
|
+
return next(`在 ${siteUrl('/skills/')} 收藏 Skill 或 MCP Server`);
|
|
25
|
+
}
|
|
24
26
|
printCards('收藏的 Skill', skills, (c) => `funcoding skills add ${c.id}`);
|
|
25
27
|
printCards('收藏的 MCP Server', mcp, (c) => `funcoding mcp add ${c.id}`);
|
|
26
|
-
|
|
28
|
+
next(cyan('funcoding favorites sync'));
|
|
27
29
|
});
|
|
28
30
|
}
|
|
29
31
|
export async function sync(targets, dryRun) {
|
|
30
32
|
const me = await fetchMe(await requireToken());
|
|
31
33
|
const items = await installCards(me.favorites, targets, dryRun);
|
|
32
34
|
done({ dryRun, items }, () => {
|
|
33
|
-
if (items.length === 0)
|
|
34
|
-
|
|
35
|
+
if (items.length === 0) {
|
|
36
|
+
info('暂无收藏');
|
|
37
|
+
return next(`在 ${siteUrl('/skills/')} 收藏 Skill 或 MCP Server`);
|
|
38
|
+
}
|
|
35
39
|
printItems(items);
|
|
36
40
|
});
|
|
37
41
|
}
|
|
@@ -84,9 +88,9 @@ export async function installCards(cards, targets, dryRun) {
|
|
|
84
88
|
}
|
|
85
89
|
return items;
|
|
86
90
|
}
|
|
87
|
-
/**
|
|
91
|
+
/** 按「已安装 / 已存在 / 跳过 / 待安装」逐行打印 installCards 的结果 */
|
|
88
92
|
export function printItems(items) {
|
|
89
|
-
const label = { installed: green('
|
|
93
|
+
const label = { installed: green('已安装'), present: dim('已存在'), skipped: yellow('跳过 '), planned: cyan('待安装') };
|
|
90
94
|
for (const i of items) {
|
|
91
95
|
info(`${label[i.status]} ${i.kind === 'skill' ? 'Skill' : 'MCP'} ${i.id}${i.reason ? dim(`:${i.reason}`) : ''}`);
|
|
92
96
|
if (i.hint)
|
package/dist/ui.js
CHANGED
|
@@ -4,7 +4,14 @@
|
|
|
4
4
|
* 过程提示一律写到 stderr;不加时是给人看的文字
|
|
5
5
|
* - 退出码固定(见 EXIT),智能体可以据此决定下一步
|
|
6
6
|
* - 颜色只在交互终端里用,NO_COLOR 或 --json 时关闭
|
|
7
|
+
* 给人看的文字统一用这几种行(写法规则见 README「输出」):
|
|
8
|
+
* 正在… step() 进度,灰色
|
|
9
|
+
* ✓ 已<动词><对象> success() 结果;下面缩进两格、灰色的是细节(路径、版本、备份位置)
|
|
10
|
+
* ! … warn() 提醒
|
|
11
|
+
* ✗ … reportError() 错误,下一行是怎么办
|
|
12
|
+
* 下一步:… next() 只在确实需要时出现
|
|
7
13
|
*/
|
|
14
|
+
import { homedir } from 'node:os';
|
|
8
15
|
export const EXIT = { ok: 0, error: 1, usage: 2, not_found: 3, exists: 4, auth: 5 };
|
|
9
16
|
export class CliError extends Error {
|
|
10
17
|
code;
|
|
@@ -22,6 +29,11 @@ let json = false;
|
|
|
22
29
|
export const setJsonMode = (v) => {
|
|
23
30
|
json = v;
|
|
24
31
|
};
|
|
32
|
+
/** --quiet:不输出给人看的文字(--json 的结果照常输出) */
|
|
33
|
+
let quiet = false;
|
|
34
|
+
export const setQuiet = (v) => {
|
|
35
|
+
quiet = v;
|
|
36
|
+
};
|
|
25
37
|
export const isJsonMode = () => json;
|
|
26
38
|
const color = () => !json && !!process.stdout.isTTY && !process.env.NO_COLOR;
|
|
27
39
|
const wrap = (code) => (s) => (color() ? `\x1b[${code}m${s}\x1b[0m` : s);
|
|
@@ -32,8 +44,37 @@ export const yellow = wrap('33');
|
|
|
32
44
|
export const red = wrap('31');
|
|
33
45
|
export const cyan = wrap('36');
|
|
34
46
|
/** 给人看的输出:--json 时改写到 stderr,不污染 stdout 的 JSON */
|
|
35
|
-
export const info = (msg) =>
|
|
36
|
-
|
|
47
|
+
export const info = (msg) => {
|
|
48
|
+
if (quiet)
|
|
49
|
+
return;
|
|
50
|
+
if (json)
|
|
51
|
+
console.error(msg);
|
|
52
|
+
else
|
|
53
|
+
console.log(msg);
|
|
54
|
+
};
|
|
55
|
+
export const warn = (msg) => {
|
|
56
|
+
if (!quiet)
|
|
57
|
+
console.error(yellow(`! ${msg}`));
|
|
58
|
+
};
|
|
59
|
+
/** 进度:正在做什么 */
|
|
60
|
+
export const step = (msg) => info(dim(`${msg}…`));
|
|
61
|
+
/** 给人看的文字里,用户主目录写成 ~(细节行里可能是「备份:<路径>」) */
|
|
62
|
+
export const tildify = (text) => {
|
|
63
|
+
const home = homedir();
|
|
64
|
+
return text.replace(new RegExp(`${home.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}(?=[\\/]|$)`, 'g'), '~');
|
|
65
|
+
};
|
|
66
|
+
/** 结果:✓ 一行,下面是缩进的细节(路径里的用户主目录写成 ~) */
|
|
67
|
+
export const success = (msg, details = []) => {
|
|
68
|
+
info(green(`✓ ${msg}`));
|
|
69
|
+
for (const d of details)
|
|
70
|
+
if (d)
|
|
71
|
+
info(dim(` ${tildify(d)}`));
|
|
72
|
+
};
|
|
73
|
+
/** 下一步:一条一行 */
|
|
74
|
+
export const next = (...lines) => {
|
|
75
|
+
for (const l of lines)
|
|
76
|
+
info(`${dim('下一步:')}${l}`);
|
|
77
|
+
};
|
|
37
78
|
export function fail(code, message, hint, details) {
|
|
38
79
|
throw new CliError(code, message, hint, details);
|
|
39
80
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "funcoding-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "funcoding.ai 的命令行:给 AI 编程智能体用,查找和安装 Agent Skills、配置 MCP Server、同步收藏、查看 AI 热点和中文文档",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -13,7 +13,8 @@
|
|
|
13
13
|
"skill"
|
|
14
14
|
],
|
|
15
15
|
"scripts": {
|
|
16
|
-
"
|
|
16
|
+
"skill": "node --experimental-strip-types --no-warnings scripts/gen-skill.ts",
|
|
17
|
+
"build": "pnpm skill && rm -rf dist && tsc -p tsconfig.json",
|
|
17
18
|
"test": "node --experimental-strip-types --no-warnings --test 'src/**/*.test.ts'",
|
|
18
19
|
"prepublishOnly": "pnpm build && pnpm test"
|
|
19
20
|
},
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: funcoding-cli
|
|
3
|
+
version: 0.4.0
|
|
4
|
+
description: 用 funcoding 命令行(funcoding-cli)从 funcoding.ai 查找并安装 Agent Skills 和 MCP Server,管理已安装的 Skill,同步用户在网站上的收藏和别人分享的清单,查看 AI 热点和日报,搜索和阅读 Claude Code、Codex、Cursor 等智能体的中文文档。当用户想找某类 Skill 或 MCP Server、安装或更新 Skill、配置 MCP、同步收藏,了解最近的 AI 新闻,或查阅 AI 编程工具的中文文档时使用。
|
|
5
|
+
requires:
|
|
6
|
+
bins: ["funcoding"]
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# funcoding-cli
|
|
10
|
+
|
|
11
|
+
`funcoding` 命令(npm 包 `funcoding-cli`,简写 `fun`)连接 funcoding.ai:Skill 和 MCP Server 目录、用户的收藏、AI 热点、中文文档。
|
|
12
|
+
|
|
13
|
+
> **核心规则:直接运行命令,始终加 `--json`。** 不要自己拼网站接口、不要去 GitHub 上找文件、不要手改各智能体的配置文件,这些 CLI 都会做,并且会先备份。
|
|
14
|
+
|
|
15
|
+
## 约定
|
|
16
|
+
|
|
17
|
+
- **`--json`**:stdout 只有一个 JSON 对象。成功 `{ "ok": true, ... }`;失败 `{ "ok": false, "error": { "code", "message", "hint" } }`。过程提示在 stderr,可以忽略。
|
|
18
|
+
- **出错先看 `error.hint`**:通常是下一步该运行的命令或该补的参数。
|
|
19
|
+
- **退出码**:0 成功,1 一般错误,2 参数错误或缺少必填项,3 未找到,4 目标已存在(需要 `--force`),5 未登录或登录已过期。
|
|
20
|
+
- **`--agent=<name>`**:`claude-code`、`codex`、`cursor`,多个用逗号分隔,`all` 是本机检测到的全部。不写时在 Claude Code 里默认 `claude-code`,**在其他智能体里必须写**。
|
|
21
|
+
- **`--project`**:安装到当前项目(可以随仓库提交);不加时安装到用户目录。
|
|
22
|
+
- **不确定参数时**:先读 `references/` 下对应的文件,或运行 `funcoding <命令> --help`。
|
|
23
|
+
- **环境**:`funcoding info --json` 返回版本、登录状态、本机检测到的智能体和各自的 Skill 目录。
|
|
24
|
+
|
|
25
|
+
没有 `funcoding` 命令时运行 `npx --yes funcoding-cli@latest cli setup --json`;`cli.status` 为 `failed` 时把 `cli.hint` 告诉用户,在那之前用 `npx --yes funcoding-cli@latest <命令>` 代替。
|
|
26
|
+
|
|
27
|
+
## 安装 Skill
|
|
28
|
+
|
|
29
|
+
1. `funcoding skills find <关键词> --json`:结果的 `skills[].id` 形如 `owner/repo/skill`。
|
|
30
|
+
2. **安装前检查**:`funcoding skills info <id> --json` 返回 SKILL.md 全文(`skill.content`)和文件清单(`skill.files`,可执行脚本在 `skill.scripts`)。用一两句话告诉用户它做什么;有执行任意命令、读取或外传密钥和个人数据、修改系统设置等操作时要明确指出。**得到用户确认后再安装。**
|
|
31
|
+
3. `funcoding skills add <id> --json`:目录已存在时以退出码 4 停下,向用户确认后再加 `--force`(旧版本会先备份)。
|
|
32
|
+
4. 告诉用户安装位置(`installed[].dir`)和调用方式:Claude Code、Cursor 输入 `/<目录名>`,Codex 输入 `$<目录名>`,也可以直接描述任务。新开会话后生效。
|
|
33
|
+
|
|
34
|
+
`skills list` 列出已安装的 Skill(`source` 不为空的是 funcoding 安装的);`skills update [--check]` 检查或更新;`skills remove` 删除前会备份,删除其他来源的 Skill 要 `--force`,先征得用户同意。
|
|
35
|
+
|
|
36
|
+
## 配置 MCP Server
|
|
37
|
+
|
|
38
|
+
1. `funcoding mcp find <关键词> --json`,再用 `funcoding mcp info <id> --json` 看启动方式(`server.launches`)和需要的环境变量(`env`)或请求头(`headers`)。
|
|
39
|
+
2. `funcoding mcp add <id> --env=KEY=VALUE --header=KEY=VALUE --json`:
|
|
40
|
+
- 缺少必填项时以退出码 2 返回,`error.missing` 列出缺少的项。**向用户索取,不要编造**;密钥只通过参数传入,不要写进别的文件。
|
|
41
|
+
- 没有标准安装包时以退出码 3 返回,`error.readme` 是 README 地址,按 README 手动配置。
|
|
42
|
+
3. 重启智能体或新开会话后生效。Claude Code 当前项目的 Server 需要用户在 Claude Code 里确认。
|
|
43
|
+
|
|
44
|
+
## 收藏和清单
|
|
45
|
+
|
|
46
|
+
- 收藏需要登录:`funcoding auth status --json` 退出码 5 表示未登录;`funcoding auth login` 会在 stderr 输出验证码和链接,并尝试打开浏览器,把链接和验证码告诉用户,命令会等到用户确认后才返回。
|
|
47
|
+
- `funcoding favorites sync --dry-run --json` 先给用户看会安装什么,再去掉 `--dry-run` 执行。
|
|
48
|
+
- 用户发来 `https://funcoding.ai/share/<id>/` 时:`funcoding lists info <链接> --json` 查看,`funcoding lists add <链接> --dry-run --json` 预览后安装。不需要登录。
|
|
49
|
+
- 两者结果的 `items[].status`:`installed` 已安装,`present` 已存在,`skipped` 跳过(附 `reason` 和 `hint`,多半是 MCP 缺少必填的环境变量),`planned` 待安装(`--dry-run`)。
|
|
50
|
+
|
|
51
|
+
## 热点和文档
|
|
52
|
+
|
|
53
|
+
- `funcoding news list --json`:当前 AI 热榜;`funcoding news daily [YYYY-MM-DD] --json`:AI 日报。数据来自 AIHOT,转述时保留来源。
|
|
54
|
+
- `funcoding docs search <关键词> [--agent=<slug>] --json`,`funcoding docs fetch <智能体>/<路径> --json`:中文文档全文,附官方来源和核实日期;命令、配置和价格以官方文档为准。
|
|
55
|
+
|
|
56
|
+
## 命令参考
|
|
57
|
+
|
|
58
|
+
| 文件 | 命令 |
|
|
59
|
+
| --- | --- |
|
|
60
|
+
| [references/cli.md](references/cli.md) | `cli setup`、`cli upgrade` |
|
|
61
|
+
| [references/skills.md](references/skills.md) | `skills find / info / add / list / update / remove` |
|
|
62
|
+
| [references/mcp.md](references/mcp.md) | `mcp find / info / add / list / remove` |
|
|
63
|
+
| [references/favorites.md](references/favorites.md) | `favorites list / sync` |
|
|
64
|
+
| [references/lists.md](references/lists.md) | `lists info / add` |
|
|
65
|
+
| [references/auth.md](references/auth.md) | `auth login / logout / status` |
|
|
66
|
+
| [references/news.md](references/news.md) | `news list / daily` |
|
|
67
|
+
| [references/docs.md](references/docs.md) | `docs search / fetch` |
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# funcoding auth
|
|
2
|
+
|
|
3
|
+
登录 funcoding.ai
|
|
4
|
+
|
|
5
|
+
所有命令都支持 `--json`、`-h, --help`、`-V, --version`。本文件由 scripts/gen-skill.ts 根据命令表生成。
|
|
6
|
+
|
|
7
|
+
## funcoding auth login
|
|
8
|
+
|
|
9
|
+
在浏览器中授权登录(设备码)
|
|
10
|
+
|
|
11
|
+
选项:
|
|
12
|
+
|
|
13
|
+
- `--no-browser`:不自动打开浏览器
|
|
14
|
+
|
|
15
|
+
## funcoding auth logout
|
|
16
|
+
|
|
17
|
+
退出登录,并使登录凭证失效
|
|
18
|
+
|
|
19
|
+
## funcoding auth status
|
|
20
|
+
|
|
21
|
+
查看当前登录的账号
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# funcoding cli
|
|
2
|
+
|
|
3
|
+
安装、升级本工具
|
|
4
|
+
|
|
5
|
+
所有命令都支持 `--json`、`-h, --help`、`-V, --version`。本文件由 scripts/gen-skill.ts 根据命令表生成。
|
|
6
|
+
|
|
7
|
+
## funcoding cli setup
|
|
8
|
+
|
|
9
|
+
全局安装本工具,并为本机的智能体安装 funcoding-cli Skill
|
|
10
|
+
|
|
11
|
+
选项:
|
|
12
|
+
|
|
13
|
+
- `--no-global`:不全局安装
|
|
14
|
+
- `--no-agent-skills`:不为智能体安装 funcoding-cli Skill
|
|
15
|
+
- `-f, --force`:目标已存在时覆盖(原内容先备份到配置目录)
|
|
16
|
+
- `-q, --quiet`:不输出给人看的文字(脚本里用)
|
|
17
|
+
|
|
18
|
+
示例:
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
npx --yes funcoding-cli@latest cli setup
|
|
22
|
+
funcoding cli setup --no-agent-skills
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## funcoding cli upgrade
|
|
26
|
+
|
|
27
|
+
升级到最新版本,并刷新已安装的 funcoding-cli Skill
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# funcoding docs
|
|
2
|
+
|
|
3
|
+
AI 编程智能体的中文文档
|
|
4
|
+
|
|
5
|
+
所有命令都支持 `--json`、`-h, --help`、`-V, --version`。本文件由 scripts/gen-skill.ts 根据命令表生成。
|
|
6
|
+
|
|
7
|
+
## funcoding docs search <query>
|
|
8
|
+
|
|
9
|
+
搜索文档
|
|
10
|
+
|
|
11
|
+
参数:
|
|
12
|
+
|
|
13
|
+
- `<query>`:关键词
|
|
14
|
+
|
|
15
|
+
选项:
|
|
16
|
+
|
|
17
|
+
- `-a, --agent=<slug>`:只搜这个智能体的文档,比如 claude-code、codex、gemini-cli
|
|
18
|
+
- `--limit=<n>`:返回条数(1–30,默认 10)
|
|
19
|
+
|
|
20
|
+
## funcoding docs fetch <path>
|
|
21
|
+
|
|
22
|
+
读取文档全文(Markdown),开头附官方来源和核实日期
|
|
23
|
+
|
|
24
|
+
参数:
|
|
25
|
+
|
|
26
|
+
- `<path>`:<智能体>/<路径>,比如 claude-code/build/hooks;也可以是网站上的文档地址
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# funcoding favorites
|
|
2
|
+
|
|
3
|
+
网站上收藏的 Skill 和 MCP Server(需要登录)
|
|
4
|
+
|
|
5
|
+
所有命令都支持 `--json`、`-h, --help`、`-V, --version`。本文件由 scripts/gen-skill.ts 根据命令表生成。
|
|
6
|
+
|
|
7
|
+
## funcoding favorites list
|
|
8
|
+
|
|
9
|
+
列出收藏
|
|
10
|
+
|
|
11
|
+
## funcoding favorites sync
|
|
12
|
+
|
|
13
|
+
安装全部收藏,已安装的跳过
|
|
14
|
+
|
|
15
|
+
选项:
|
|
16
|
+
|
|
17
|
+
- `-a, --agent=<name>`:目标智能体:claude-code、codex、cursor,多个用逗号分隔,all 表示本机检测到的全部(默认:当前所在的智能体,否则 claude-code)
|
|
18
|
+
- `-p, --project`:安装到当前项目(可随仓库提交),不加时安装到用户目录
|
|
19
|
+
- `--dry-run`:只列出将要执行的操作
|