funcoding-cli 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +42 -25
- package/dist/api.js +1 -1
- package/dist/auth.js +6 -6
- package/dist/cli.js +308 -191
- package/dist/content.js +55 -43
- package/dist/info.js +40 -0
- package/dist/init.js +90 -0
- package/dist/install.js +8 -8
- package/dist/lists.js +42 -0
- package/dist/mcp.js +1 -1
- package/dist/sync.js +24 -15
- package/package.json +3 -2
- package/skill/funcoding/SKILL.md +72 -29
- package/dist/setup.js +0 -49
package/dist/content.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/**
|
|
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,60 +13,72 @@ function printCards(title, cards, hrefOf) {
|
|
|
13
13
|
}
|
|
14
14
|
info('');
|
|
15
15
|
}
|
|
16
|
-
|
|
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', '
|
|
20
|
+
fail('usage', '缺少关键词。', `例如 funcoding ${cmd} find pdf`);
|
|
19
21
|
const r = await api(`/api/cli/search?q=${encodeURIComponent(query)}&limit=${limit}`);
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
printCards('MCP Server',
|
|
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
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
if (
|
|
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));
|
|
29
|
+
const width = () => (process.stdout.columns || 100) - 6;
|
|
30
|
+
export async function newsDaily(date) {
|
|
31
|
+
const day = date || 'latest';
|
|
32
|
+
if (day !== 'latest' && !/^\d{4}-\d{2}-\d{2}$/.test(day))
|
|
33
|
+
fail('usage', `日期要写成 YYYY-MM-DD:${day}`);
|
|
34
|
+
const issue = await api(`/api/cli/news?daily=${day}`);
|
|
35
|
+
done({ daily: issue }, () => {
|
|
36
|
+
info(bold(`AI 日报 · ${issue.day}`));
|
|
37
|
+
info(dim(issue.url));
|
|
38
|
+
if (issue.lead) {
|
|
42
39
|
info('');
|
|
43
|
-
info(bold(
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
info(
|
|
54
|
-
for (const s of issue.flash)
|
|
55
|
-
info(` · ${s.title} ${dim(s.url)}`);
|
|
40
|
+
info(bold(issue.lead.title));
|
|
41
|
+
info(` ${truncate(issue.lead.summary, width())}`);
|
|
42
|
+
}
|
|
43
|
+
for (const sec of issue.sections) {
|
|
44
|
+
info('');
|
|
45
|
+
info(bold(sec.label));
|
|
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}`));
|
|
56
51
|
}
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
|
|
52
|
+
}
|
|
53
|
+
if (issue.flashes.length > 0) {
|
|
54
|
+
info('');
|
|
55
|
+
info(bold('快讯'));
|
|
56
|
+
for (const i of issue.flashes)
|
|
57
|
+
info(` · ${i.title} ${dim(i.url)}`);
|
|
58
|
+
}
|
|
59
|
+
info('');
|
|
60
|
+
info(dim('数据来自 AIHOT(aihot.news)'));
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
export async function newsList(limit) {
|
|
64
|
+
const r = await api(`/api/cli/news?limit=${limit}`);
|
|
60
65
|
done({ updatedAt: r.updatedAt, stories: r.stories }, () => {
|
|
61
66
|
if (r.stories.length === 0)
|
|
62
67
|
return info('暂无热点。');
|
|
63
68
|
info(bold('AI 热榜') + dim(` ${siteUrl('/news/')}`));
|
|
64
|
-
|
|
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)'));
|
|
65
77
|
});
|
|
66
78
|
}
|
|
67
79
|
export async function docsSearch(query, agent, limit) {
|
|
68
80
|
if (!query)
|
|
69
|
-
fail('usage', '缺少搜索词。', '例如 funcoding docs search hooks --agent
|
|
81
|
+
fail('usage', '缺少搜索词。', '例如 funcoding docs search hooks --agent=claude-code');
|
|
70
82
|
const params = new URLSearchParams({ q: query, limit: String(limit), ...(agent ? { agent } : {}) });
|
|
71
83
|
const r = await api(`/api/cli/docs/search?${params}`);
|
|
72
84
|
done({ docs: r.docs }, () => {
|
|
@@ -78,10 +90,10 @@ export async function docsSearch(query, agent, limit) {
|
|
|
78
90
|
info(` ${truncate(d.description, (process.stdout.columns || 100) - 4)}`);
|
|
79
91
|
}
|
|
80
92
|
info('');
|
|
81
|
-
info(dim(`运行 ${cyan('funcoding docs
|
|
93
|
+
info(dim(`运行 ${cyan('funcoding docs fetch <id>')} 阅读全文。`));
|
|
82
94
|
});
|
|
83
95
|
}
|
|
84
|
-
export async function
|
|
96
|
+
export async function docsFetch(id) {
|
|
85
97
|
const path = id
|
|
86
98
|
.trim()
|
|
87
99
|
.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 './
|
|
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}`,
|
|
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
|
|
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
|
|
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/lists.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { api } from './api.js';
|
|
2
|
+
import { installCards, printCards, printItems } from './sync.js';
|
|
3
|
+
import { bold, cyan, dim, done, fail, info } from './ui.js';
|
|
4
|
+
const SHARE_ID = /^[23456789abcdefghijkmnpqrstuvwxyz]{10}$/;
|
|
5
|
+
/** 从分享链接或裸 id 里取出清单 id,不合法返回 undefined */
|
|
6
|
+
export function parseShareId(input) {
|
|
7
|
+
const s = input.trim();
|
|
8
|
+
const m = /\/share\/([^/?#]+)/.exec(s);
|
|
9
|
+
const id = (m ? m[1] : s).toLowerCase();
|
|
10
|
+
return SHARE_ID.test(id) ? id : undefined;
|
|
11
|
+
}
|
|
12
|
+
async function fetchList(input) {
|
|
13
|
+
const id = parseShareId(input);
|
|
14
|
+
if (!id)
|
|
15
|
+
fail('usage', `不是清单链接:${input}`, '清单链接形如 https://funcoding.ai/share/k3x9q2abcd/');
|
|
16
|
+
return api(`/api/cli/lists/${id}`);
|
|
17
|
+
}
|
|
18
|
+
export async function listInfo(input) {
|
|
19
|
+
const list = await fetchList(input);
|
|
20
|
+
done({ ...list }, () => {
|
|
21
|
+
info(`${bold(list.title)} ${dim(`· ${list.owner.name} 分享`)}`);
|
|
22
|
+
if (list.description)
|
|
23
|
+
info(list.description);
|
|
24
|
+
info(dim(list.url));
|
|
25
|
+
info('');
|
|
26
|
+
if (list.skills.length + list.mcp.length === 0)
|
|
27
|
+
return info('清单里还没有条目。');
|
|
28
|
+
printCards('Skill', list.skills, (c) => `funcoding skills add ${c.id}`);
|
|
29
|
+
printCards('MCP Server', list.mcp, (c) => `funcoding mcp add ${c.id}`);
|
|
30
|
+
info(dim(`运行 ${cyan(`funcoding lists add ${list.shareId}`)} 一次安装全部。`));
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
export async function listAdd(input, targets, dryRun) {
|
|
34
|
+
const list = await fetchList(input);
|
|
35
|
+
const items = await installCards(list, targets, dryRun);
|
|
36
|
+
done({ dryRun, list: { shareId: list.shareId, title: list.title, url: list.url }, items }, () => {
|
|
37
|
+
info(`${bold(list.title)} ${dim(`· ${list.owner.name} 分享`)}`);
|
|
38
|
+
if (items.length === 0)
|
|
39
|
+
return info('清单里还没有条目。');
|
|
40
|
+
printItems(items);
|
|
41
|
+
});
|
|
42
|
+
}
|
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
|
|
31
|
+
fail('not_found', `没有找到 MCP Server:${id}`, '运行 funcoding mcp find <关键词> 查找');
|
|
32
32
|
throw err;
|
|
33
33
|
}
|
|
34
34
|
}
|
package/dist/sync.js
CHANGED
|
@@ -3,7 +3,7 @@ import { fetchMe, requireToken } from './auth.js';
|
|
|
3
3
|
import { fetchSkill, installSkill, scanAll } from './install.js';
|
|
4
4
|
import { addServer, fetchMcp } from './mcp.js';
|
|
5
5
|
import { bold, CliError, cyan, dim, done, green, info, stars, truncate, yellow } from './ui.js';
|
|
6
|
-
function printCards(title, cards, hrefOf) {
|
|
6
|
+
export function printCards(title, cards, hrefOf) {
|
|
7
7
|
if (cards.length === 0)
|
|
8
8
|
return;
|
|
9
9
|
info(bold(title));
|
|
@@ -21,18 +21,27 @@ 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
|
|
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) {
|
|
30
30
|
const me = await fetchMe(await requireToken());
|
|
31
|
+
const items = await installCards(me.favorites, targets, dryRun);
|
|
32
|
+
done({ dryRun, items }, () => {
|
|
33
|
+
if (items.length === 0)
|
|
34
|
+
return info(`还没有收藏。可以在 ${siteUrl('/skills/')} 收藏 Skill 和 MCP Server。`);
|
|
35
|
+
printItems(items);
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
/** 把一组 Skill 和 MCP Server 装到 targets,已装的跳过;单个条目失败只记为跳过 */
|
|
39
|
+
export async function installCards(cards, targets, dryRun) {
|
|
31
40
|
const items = [];
|
|
32
41
|
const agents = [...new Set(targets.map((t) => t.agent))];
|
|
33
42
|
const scopes = [...new Set(targets.map((t) => t.scope))];
|
|
34
43
|
const installed = await scanAll({ agents, scopes });
|
|
35
|
-
for (const card of
|
|
44
|
+
for (const card of cards.skills) {
|
|
36
45
|
const missing = targets.filter((t) => !installed.some((s) => s.agent === t.agent && s.scope === t.scope && s.source?.id === card.id));
|
|
37
46
|
if (missing.length === 0) {
|
|
38
47
|
items.push({ kind: 'skill', id: card.id, status: 'present', agents });
|
|
@@ -53,7 +62,7 @@ export async function sync(targets, dryRun) {
|
|
|
53
62
|
items.push({ kind: 'skill', id: card.id, status: 'skipped', agents: missing.map((t) => t.agent), reason: err.message, hint: err.hint });
|
|
54
63
|
}
|
|
55
64
|
}
|
|
56
|
-
for (const card of
|
|
65
|
+
for (const card of cards.mcp) {
|
|
57
66
|
try {
|
|
58
67
|
const server = await fetchMcp(card.id);
|
|
59
68
|
if (dryRun) {
|
|
@@ -73,14 +82,14 @@ export async function sync(targets, dryRun) {
|
|
|
73
82
|
items.push({ kind: 'mcp', id: card.id, status: 'skipped', agents, reason: err.message, hint: err.hint ?? (err.code === 'usage' ? undefined : `funcoding mcp info ${card.id}`) });
|
|
74
83
|
}
|
|
75
84
|
}
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
}
|
|
85
|
-
}
|
|
85
|
+
return items;
|
|
86
|
+
}
|
|
87
|
+
/** 按「新装 / 已装 / 跳过 / 将安装」逐行打印 installCards 的结果 */
|
|
88
|
+
export function printItems(items) {
|
|
89
|
+
const label = { installed: green('新装'), present: dim('已装'), skipped: yellow('跳过'), planned: cyan('将安装') };
|
|
90
|
+
for (const i of items) {
|
|
91
|
+
info(`${label[i.status]} ${i.kind === 'skill' ? 'Skill' : 'MCP'} ${i.id}${i.reason ? dim(`:${i.reason}`) : ''}`);
|
|
92
|
+
if (i.hint)
|
|
93
|
+
info(dim(` ${i.hint}`));
|
|
94
|
+
}
|
|
86
95
|
}
|
package/package.json
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "funcoding-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
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": [
|
|
@@ -12,7 +13,7 @@
|
|
|
12
13
|
"skill"
|
|
13
14
|
],
|
|
14
15
|
"scripts": {
|
|
15
|
-
"build": "tsc -p tsconfig.json",
|
|
16
|
+
"build": "rm -rf dist && tsc -p tsconfig.json",
|
|
16
17
|
"test": "node --experimental-strip-types --no-warnings --test 'src/**/*.test.ts'",
|
|
17
18
|
"prepublishOnly": "pnpm build && pnpm test"
|
|
18
19
|
},
|
package/skill/funcoding/SKILL.md
CHANGED
|
@@ -1,61 +1,104 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: funcoding
|
|
3
|
-
description: 用 funcoding-cli
|
|
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
|
|
6
|
+
# funcoding
|
|
7
7
|
|
|
8
|
-
funcoding
|
|
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
|
|
15
|
+
npx --yes funcoding-cli@latest init --json
|
|
12
16
|
```
|
|
13
17
|
|
|
14
|
-
|
|
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
|
|
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 目录、登录状态。
|
|
31
|
+
|
|
32
|
+
## Skill 管理
|
|
23
33
|
|
|
24
|
-
|
|
34
|
+
使用 `skills` 命令:
|
|
25
35
|
|
|
26
|
-
1.
|
|
27
|
-
2. **安装前先检查**:`info <id>`
|
|
28
|
-
3.
|
|
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
|
|
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`,务必先征得用户同意。
|
|
36
46
|
|
|
37
|
-
##
|
|
47
|
+
## MCP 配置
|
|
38
48
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
49
|
+
使用 `mcp` 命令:
|
|
50
|
+
|
|
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
|
|
45
|
-
5. `mcp list` 列出已配置的 Server
|
|
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 缺少必填的环境变量。
|
|
67
|
+
|
|
68
|
+
## 别人分享的清单
|
|
69
|
+
|
|
70
|
+
用户发来 `https://funcoding.ai/share/<id>/` 这样的链接,是别人在 funcoding.ai 上整理并公开的一组 Skill 和 MCP Server,不需要登录:
|
|
50
71
|
|
|
51
|
-
- `
|
|
52
|
-
- `
|
|
53
|
-
- `favorites`:列出收藏。
|
|
54
|
-
- `sync [--agent ...] [--project] [--dry-run]`:安装全部收藏,已装的跳过。建议先用 `--dry-run` 给用户看将要安装什么。`items[]` 里 `status` 为 `skipped` 的条目附有原因和提示,通常是 MCP 缺少必填的环境变量。
|
|
72
|
+
- `funcoding lists info <链接或 id>`:查看清单里有什么。
|
|
73
|
+
- `funcoding lists add <链接或 id> [--agent=<name>] [--project] [--dry-run]`:安装清单里的全部条目,已装的跳过。建议先用 `--dry-run` 给用户看将要安装什么;结果的 `items[]` 和 `favorites sync` 相同。链接已关闭或重置时返回退出码 3。
|
|
55
74
|
|
|
56
|
-
##
|
|
75
|
+
## AI 热点
|
|
57
76
|
|
|
58
|
-
- `news [--limit
|
|
59
|
-
- `news
|
|
60
|
-
|
|
61
|
-
|
|
77
|
+
- `funcoding news list [--limit=<n>]`:当前 AI 热榜(过去 48 小时被多个来源共同报道的事件,最多 10 个,附最新进展;数据来自 AIHOT)。
|
|
78
|
+
- `funcoding news daily [<YYYY-MM-DD>]`:某一天的 AI 日报,不写日期时为最新一期。
|
|
79
|
+
|
|
80
|
+
## 文档
|
|
81
|
+
|
|
82
|
+
`docs` 命令用来查阅 AI 编程智能体的中文文档。这些文档按官方文档的章节结构整理,并标注了官方来源和核实日期。
|
|
83
|
+
|
|
84
|
+
- `funcoding docs search <query> [--agent=<slug>]`:搜索文档。这里的 `--agent` 可以是站内任意智能体,比如 `claude-code`、`codex`、`cursor`、`gemini-cli`。
|
|
85
|
+
- `funcoding docs fetch <agent>/<path>`:读取文档全文(Markdown),开头附官方来源链接和核实日期。命令、配置和价格以官方文档为准。
|
|
86
|
+
|
|
87
|
+
## `funcoding help` 输出
|
|
88
|
+
|
|
89
|
+
```
|
|
90
|
+
Usage: funcoding [-hV] [--json] <command>
|
|
91
|
+
|
|
92
|
+
Commands:
|
|
93
|
+
init 初始化:全局安装本工具(之后可用 funcoding 或 fun),并给智能体装上 funcoding Skill
|
|
94
|
+
update 把本工具更新到最新版本,并刷新已安装的 funcoding Skill
|
|
95
|
+
info 环境信息:版本、站点、登录状态、本机的智能体和 Skill 目录、配置目录
|
|
96
|
+
help 显示帮助
|
|
97
|
+
skills 查找、安装和管理 Agent Skills(find / info / add / list / update / remove)
|
|
98
|
+
mcp 查找 MCP Server,并写入各智能体的 MCP 配置(find / info / add / list / remove)
|
|
99
|
+
favorites 网站上收藏的 Skill 和 MCP Server(list / sync)
|
|
100
|
+
lists 别人分享的 Skill 和 MCP 清单(info / add)
|
|
101
|
+
auth 登录 funcoding.ai(login / logout / status)
|
|
102
|
+
news AI 热点(list / daily)
|
|
103
|
+
docs AI 编程智能体的中文文档(search / fetch)
|
|
104
|
+
```
|