funcoding-cli 0.0.0-stage → 0.2.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/dist/paths.js ADDED
@@ -0,0 +1,18 @@
1
+ /** 下载的文件路径来自网络,写盘前必须确认不会跑出目标目录 */
2
+ import { isAbsolute, normalize, relative, resolve, sep } from 'node:path';
3
+ /** 合法的相对路径:不能是绝对路径、不能有 ..、不能有反斜杠和控制字符 */
4
+ export function isSafeRelativePath(p) {
5
+ if (!p || p.length > 300 || isAbsolute(p) || /[\\\0-\x1f]/.test(p) || /^[a-zA-Z]:/.test(p))
6
+ return false;
7
+ return p.split('/').every((seg) => seg !== '' && seg !== '.' && seg !== '..');
8
+ }
9
+ /** 把相对路径落到 root 下;越界返回 null */
10
+ export function resolveInside(root, p) {
11
+ if (!isSafeRelativePath(p))
12
+ return null;
13
+ const full = resolve(root, normalize(p));
14
+ const rel = relative(root, full);
15
+ return rel && !rel.startsWith('..') && !isAbsolute(rel) && !rel.split(sep).includes('..') ? full : null;
16
+ }
17
+ /** 目录名只允许小写字母、数字和连字符 */
18
+ export const isSafeFolderName = (name) => /^[a-z0-9][a-z0-9-]{0,63}$/.test(name);
package/dist/setup.js ADDED
@@ -0,0 +1,49 @@
1
+ /**
2
+ * setup:把随 CLI 发布的 funcoding Skill(skill/funcoding/SKILL.md)装到智能体,让它知道什么时候、怎么用这个 CLI。
3
+ * 这个 Skill 不在网站目录里,来源记录标成 builtin,update 时从当前版本的 CLI 重新复制。
4
+ */
5
+ import { randomBytes } from 'node:crypto';
6
+ import { mkdir, readFile, rename, rm, stat, writeFile } from 'node:fs/promises';
7
+ import { dirname, join } from 'node:path';
8
+ import { AGENTS, skillsRoot } from './agents.js';
9
+ import { USER_AGENT } from './api.js';
10
+ import { META_FILE, moveAway } from './install.js';
11
+ import { cyan, dim, done, fail, green, info } from './ui.js';
12
+ export const BUILTIN_ID = 'funcoding';
13
+ const SOURCE = new URL('../skill/funcoding/SKILL.md', import.meta.url);
14
+ export const builtinContent = () => readFile(SOURCE, 'utf8');
15
+ export async function installBuiltin(targets, force) {
16
+ const content = await builtinContent();
17
+ const dirs = targets.map((t) => ({ ...t, dir: join(skillsRoot(t.agent, t.scope), BUILTIN_ID) }));
18
+ const foreign = [];
19
+ for (const d of dirs) {
20
+ const meta = await readFile(join(d.dir, META_FILE), 'utf8').catch(() => null);
21
+ const present = meta !== null || (await stat(d.dir).then(() => true, () => false));
22
+ // 之前 setup 装的直接换成新版;同名的其他 Skill 要 --force,并且先备份
23
+ if (present && !(meta && JSON.parse(meta).builtin))
24
+ foreign.push(d);
25
+ }
26
+ if (foreign.length > 0 && !force)
27
+ fail('exists', `${foreign.map((d) => d.dir).join('、')} 已存在,且不是 funcoding setup 安装的。`, '确认要覆盖就加 --force(会先备份到配置目录)');
28
+ for (const d of dirs) {
29
+ const tmp = join(dirname(d.dir), `.funcoding-tmp-${randomBytes(4).toString('hex')}`);
30
+ await mkdir(tmp, { recursive: true });
31
+ await writeFile(join(tmp, 'SKILL.md'), content);
32
+ 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`);
33
+ if (foreign.includes(d))
34
+ await moveAway(d.dir, d.agent, BUILTIN_ID);
35
+ else
36
+ await rm(d.dir, { recursive: true, force: true });
37
+ await rename(tmp, d.dir);
38
+ }
39
+ return dirs;
40
+ }
41
+ export async function setup(targets, force) {
42
+ const dirs = await installBuiltin(targets, force);
43
+ done({ installed: dirs.map((d) => ({ id: BUILTIN_ID, agent: d.agent, scope: d.scope, dir: d.dir })) }, () => {
44
+ for (const d of dirs)
45
+ info(green(`✓ 已把 funcoding Skill 装到 ${d.dir}`));
46
+ info(`之后可以直接让 ${dirs.map((d) => AGENTS[d.agent].label).join('、')} 帮你查找和安装 Skill、配置 MCP,例如「找一个处理 PDF 的 Skill 装上」。`);
47
+ info(dim(`也可以输入 ${[...new Set(dirs.map((d) => cyan(AGENTS[d.agent].invoke(BUILTIN_ID))))].join(' / ')} 主动调用。新开会话后生效。`));
48
+ });
49
+ }
package/dist/sync.js ADDED
@@ -0,0 +1,86 @@
1
+ import { siteUrl } from './api.js';
2
+ import { fetchMe, requireToken } from './auth.js';
3
+ import { fetchSkill, installSkill, scanAll } from './install.js';
4
+ import { addServer, fetchMcp } from './mcp.js';
5
+ import { bold, CliError, cyan, dim, done, green, info, stars, truncate, yellow } from './ui.js';
6
+ function printCards(title, cards, hrefOf) {
7
+ if (cards.length === 0)
8
+ return;
9
+ info(bold(title));
10
+ for (const c of cards) {
11
+ info(` ${cyan(c.id)} ${dim(`★ ${stars(c.stars)}${c.official ? ' · 官方' : ''}`)}`);
12
+ if (c.description)
13
+ info(` ${truncate(c.description, (process.stdout.columns || 100) - 6)}`);
14
+ info(dim(` ${hrefOf(c)}`));
15
+ }
16
+ info('');
17
+ }
18
+ export async function favorites() {
19
+ const me = await fetchMe(await requireToken());
20
+ const { skills, mcp } = me.favorites;
21
+ done({ skills, mcp }, () => {
22
+ if (skills.length + mcp.length === 0)
23
+ return info(`还没有收藏。可以在 ${siteUrl('/skills/')} 收藏 Skill 和 MCP Server。`);
24
+ printCards('收藏的 Skill', skills, (c) => `funcoding install ${c.id}`);
25
+ printCards('收藏的 MCP Server', mcp, (c) => `funcoding mcp add ${c.id}`);
26
+ info(dim(`运行 ${cyan('funcoding sync')} 一次安装全部收藏。`));
27
+ });
28
+ }
29
+ export async function sync(targets, dryRun) {
30
+ const me = await fetchMe(await requireToken());
31
+ const items = [];
32
+ const agents = [...new Set(targets.map((t) => t.agent))];
33
+ const scopes = [...new Set(targets.map((t) => t.scope))];
34
+ const installed = await scanAll({ agents, scopes });
35
+ for (const card of me.favorites.skills) {
36
+ const missing = targets.filter((t) => !installed.some((s) => s.agent === t.agent && s.scope === t.scope && s.source?.id === card.id));
37
+ if (missing.length === 0) {
38
+ items.push({ kind: 'skill', id: card.id, status: 'present', agents });
39
+ continue;
40
+ }
41
+ if (dryRun) {
42
+ items.push({ kind: 'skill', id: card.id, status: 'planned', agents: missing.map((t) => t.agent) });
43
+ continue;
44
+ }
45
+ try {
46
+ // 同名目录可能是手动放的或别的来源,不覆盖
47
+ await installSkill(await fetchSkill(card.id), missing, false);
48
+ items.push({ kind: 'skill', id: card.id, status: 'installed', agents: missing.map((t) => t.agent) });
49
+ }
50
+ catch (err) {
51
+ if (!(err instanceof CliError))
52
+ throw err;
53
+ items.push({ kind: 'skill', id: card.id, status: 'skipped', agents: missing.map((t) => t.agent), reason: err.message, hint: err.hint });
54
+ }
55
+ }
56
+ for (const card of me.favorites.mcp) {
57
+ try {
58
+ const server = await fetchMcp(card.id);
59
+ if (dryRun) {
60
+ items.push({ kind: 'mcp', id: card.id, status: 'planned', agents });
61
+ continue;
62
+ }
63
+ await addServer(server, { targets, env: {}, headers: {}, force: false });
64
+ items.push({ kind: 'mcp', id: card.id, status: 'installed', agents });
65
+ }
66
+ catch (err) {
67
+ if (!(err instanceof CliError))
68
+ throw err;
69
+ // 已经配置过的算已装;缺少必填项、没有安装包等记为跳过
70
+ if (err.code === 'exists')
71
+ items.push({ kind: 'mcp', id: card.id, status: 'present', agents });
72
+ else
73
+ 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
+ }
75
+ }
76
+ done({ dryRun, items }, () => {
77
+ if (items.length === 0)
78
+ return info(`还没有收藏。可以在 ${siteUrl('/skills/')} 收藏 Skill 和 MCP Server。`);
79
+ const label = { installed: green('新装'), present: dim('已装'), skipped: yellow('跳过'), planned: cyan('将安装') };
80
+ for (const i of items) {
81
+ info(`${label[i.status]} ${i.kind === 'skill' ? 'Skill' : 'MCP'} ${i.id}${i.reason ? dim(`:${i.reason}`) : ''}`);
82
+ if (i.hint)
83
+ info(dim(` ${i.hint}`));
84
+ }
85
+ });
86
+ }
package/dist/ui.js ADDED
@@ -0,0 +1,65 @@
1
+ /**
2
+ * 输出层。CLI 主要给智能体用,约定:
3
+ * - 加 --json 时 stdout 只输出一个 JSON 对象(成功 { ok: true, ... },失败 { ok: false, error: { code, message, hint } }),
4
+ * 过程提示一律写到 stderr;不加时是给人看的文字
5
+ * - 退出码固定(见 EXIT),智能体可以据此决定下一步
6
+ * - 颜色只在交互终端里用,NO_COLOR 或 --json 时关闭
7
+ */
8
+ export const EXIT = { ok: 0, error: 1, usage: 2, not_found: 3, exists: 4, auth: 5 };
9
+ export class CliError extends Error {
10
+ code;
11
+ /** 下一步可以运行的命令或可以做的事 */
12
+ hint;
13
+ details;
14
+ constructor(code, message, hint, details) {
15
+ super(message);
16
+ this.code = code;
17
+ this.hint = hint;
18
+ this.details = details;
19
+ }
20
+ }
21
+ let json = false;
22
+ export const setJsonMode = (v) => {
23
+ json = v;
24
+ };
25
+ export const isJsonMode = () => json;
26
+ const color = () => !json && !!process.stdout.isTTY && !process.env.NO_COLOR;
27
+ const wrap = (code) => (s) => (color() ? `\x1b[${code}m${s}\x1b[0m` : s);
28
+ export const bold = wrap('1');
29
+ export const dim = wrap('2');
30
+ export const green = wrap('32');
31
+ export const yellow = wrap('33');
32
+ export const red = wrap('31');
33
+ export const cyan = wrap('36');
34
+ /** 给人看的输出:--json 时改写到 stderr,不污染 stdout 的 JSON */
35
+ export const info = (msg) => (json ? console.error(msg) : console.log(msg));
36
+ export const warn = (msg) => console.error(yellow(`! ${msg}`));
37
+ export function fail(code, message, hint, details) {
38
+ throw new CliError(code, message, hint, details);
39
+ }
40
+ /** 命令的最终结果:--json 时输出 JSON,否则调用 human 打印文字 */
41
+ export function done(data, human) {
42
+ if (json)
43
+ console.log(JSON.stringify({ ok: true, ...data }, null, 2));
44
+ else
45
+ human();
46
+ }
47
+ export function reportError(err) {
48
+ if (json) {
49
+ console.log(JSON.stringify({ ok: false, error: { code: err.code, message: err.message, ...(err.hint ? { hint: err.hint } : {}), ...(err.details ?? {}) } }, null, 2));
50
+ }
51
+ else {
52
+ console.error(red(`✗ ${err.message}`));
53
+ if (err.hint)
54
+ console.error(dim(` ${err.hint}`));
55
+ }
56
+ process.exitCode = EXIT[err.code];
57
+ }
58
+ export function stars(n) {
59
+ return n >= 1000 ? `${(n / 1000).toFixed(n >= 10_000 ? 0 : 1)}k` : String(n);
60
+ }
61
+ /** 截断到终端宽度 */
62
+ export function truncate(s, max = (process.stdout.columns || 100) - 4) {
63
+ const flat = s.replace(/\s+/g, ' ').trim();
64
+ return flat.length > max ? `${flat.slice(0, max - 1)}…` : flat;
65
+ }
package/package.json CHANGED
@@ -1,6 +1,42 @@
1
1
  {
2
2
  "name": "funcoding-cli",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.2.0",
4
+ "description": "funcoding.ai 的命令行:给 AI 编程智能体用,查找和安装 Agent Skills、配置 MCP Server、同步收藏、查看 AI 热点和中文文档",
5
+ "type": "module",
6
+ "bin": {
7
+ "funcoding": "dist/cli.js",
8
+ "funcoding-cli": "dist/cli.js"
9
+ },
10
+ "files": [
11
+ "dist",
12
+ "skill"
13
+ ],
14
+ "scripts": {
15
+ "build": "tsc -p tsconfig.json",
16
+ "test": "node --experimental-strip-types --no-warnings --test 'src/**/*.test.ts'",
17
+ "prepublishOnly": "pnpm build && pnpm test"
18
+ },
19
+ "engines": {
20
+ "node": ">=18.17"
21
+ },
22
+ "keywords": [
23
+ "agent-skills",
24
+ "mcp",
25
+ "claude-code",
26
+ "codex",
27
+ "cursor",
28
+ "skills",
29
+ "cli"
30
+ ],
31
+ "homepage": "https://funcoding.ai",
32
+ "license": "MIT",
33
+ "devDependencies": {
34
+ "@types/node": "^26.6.4",
35
+ "typescript": "^7.0.2"
36
+ },
37
+ "repository": {
38
+ "type": "git",
39
+ "url": "git+https://github.com/funcodingdev/funcoding_ai.git",
40
+ "directory": "packages/funcoding-cli"
41
+ }
42
+ }
@@ -0,0 +1,61 @@
1
+ ---
2
+ name: funcoding
3
+ description: 用 funcoding-cli 从 funcoding.ai 查找并安装 Agent Skills 和 MCP Server,管理已安装的 Skill,同步用户在网站上的收藏,查看 AI 热点和日报,搜索和阅读 Claude Code、Codex、Cursor 等智能体的中文文档。当用户想找某类 Skill 或 MCP Server、安装或更新 Skill、配置 MCP、同步收藏、了解 AI 圈最近的新闻,或查阅 AI 编程工具的中文文档时使用。
4
+ ---
5
+
6
+ # funcoding-cli
7
+
8
+ funcoding-cli 是 funcoding.ai 的命令行工具。运行方式:
9
+
10
+ ```bash
11
+ npx --yes funcoding-cli@latest <命令> --json
12
+ ```
13
+
14
+ 已全局安装时(`npm i -g funcoding-cli`)可以直接用 `funcoding <命令>`。需要 Node.js 18.17 及以上。
15
+
16
+ ## 使用约定
17
+
18
+ - **始终加 `--json`**:stdout 只输出一个 JSON 对象。成功时是 `{ "ok": true, ... }`,失败时是 `{ "ok": false, "error": { "code", "message", "hint" } }`。过程提示写在 stderr,可以忽略。
19
+ - 出错时先看 `error.hint`,里面通常是下一步该运行的命令或该补的参数。
20
+ - 退出码:0 成功,1 一般错误,2 参数错误或缺少必填项,3 未找到,4 目标已存在(需要 `--force`),5 未登录或登录已过期。
21
+ - `--agent` 指定装给哪个智能体:`claude-code`、`codex`、`cursor`,多个用逗号分隔;`all` 表示本机检测到的全部智能体。不指定时,在 Claude Code 里运行默认装给 Claude Code,其他情况也默认 Claude Code。如果你不是 Claude Code,请显式加上 `--agent`。
22
+ - `--project` 装到当前项目(可以随仓库提交,团队共用);不加时装到用户目录,对所有项目生效。
23
+
24
+ ## 安装 Skill
25
+
26
+ 1. 查找:`search <关键词>`。结果里的 `skills[].id` 形如 `owner/repo/skill`。
27
+ 2. **安装前先检查**:`info <id>` 会返回 SKILL.md 全文(`skill.content`)和文件清单(`skill.files`,`skill.scripts` 是其中的可执行脚本)。用一两句话告诉用户这个 Skill 做什么;如果有执行任意命令、读取或外传密钥和个人数据、修改系统设置等危险操作,要明确指出。**得到用户确认后再安装。**
28
+ 3. 安装:`install <id> [--agent ...] [--project]`。目录已存在时会以退出码 4 停下;向用户确认后再加 `--force`,旧版本会备份到配置目录。
29
+ 4. 告诉用户装到了哪个目录(`installed[].dir`),以及如何调用:Claude Code 和 Cursor 输入 `/<目录名>`,Codex 输入 `$<目录名>`,也可以直接描述任务,让智能体自动选用。新装的 Skill 可能需要新开会话才会加载。
30
+
31
+ 管理已安装的 Skill:
32
+
33
+ - `list`:列出所有智能体在用户目录和当前项目里的 Skill。`source` 不为空的是通过 funcoding 安装的。
34
+ - `update --check`:检查远端 SKILL.md 是否有变化,不安装。`update [名称]`:按原来源重新安装。
35
+ - `remove <名称>`:删除前会先备份。只能删除通过 funcoding 安装的 Skill;删除其他 Skill 需要加 `--force`,务必先征得用户同意。
36
+
37
+ ## 配置 MCP Server
38
+
39
+ 1. `search <关键词>` 结果里的 `mcp[].id` 形如 `owner/repo`。
40
+ 2. `mcp info <id>`:查看可用的启动方式(`server.launches`),每种方式需要的环境变量(`env`)或请求头(`headers`),以及是否必填。
41
+ 3. `mcp add <id> [--agent ...] [--project] [--env KEY=VALUE ...] [--header KEY=VALUE ...] [--via remote|npm|pypi]`。
42
+ - 缺少必填项时,会以退出码 2 返回,`error.missing` 列出缺少的项。**向用户索取这些值,不要编造。** 密钥只通过参数传入,不要写进其他文件。
43
+ - 没有登记标准安装包时,会以退出码 3 返回,`error.readme` 是 README 地址。这时按 README 手动配置。
44
+ 4. 配置在重启智能体或新开会话后生效。Claude Code 的项目范围 Server 需要用户在 Claude Code 里确认后才会启用。
45
+ 5. `mcp list` 列出已配置的 Server,`mcp remove <配置名>` 删除,删除前会备份原文件。
46
+
47
+ ## 收藏
48
+
49
+ 用户在 funcoding.ai 上收藏的 Skill 和 MCP Server:
50
+
51
+ - `whoami`:查看是否已登录。返回退出码 5 表示未登录。
52
+ - `login`:会显示验证码和授权地址(在 stderr 中),并尝试打开浏览器。把地址和验证码告诉用户,请用户在浏览器中确认;命令会一直等到用户确认后才返回。
53
+ - `favorites`:列出收藏。
54
+ - `sync [--agent ...] [--project] [--dry-run]`:安装全部收藏,已装的跳过。建议先用 `--dry-run` 给用户看将要安装什么。`items[]` 里 `status` 为 `skipped` 的条目附有原因和提示,通常是 MCP 缺少必填的环境变量。
55
+
56
+ ## 热点与文档
57
+
58
+ - `news [--limit N]`:当前 AI 热榜(多家来源报道的事件,按热度排序,中文标题和摘要)。
59
+ - `news --daily [YYYY-MM-DD]`:某天的 AI 日报,不带日期时为最新一期。
60
+ - `docs search <关键词> [--agent <智能体>]`:搜索中文文档。这里的 `--agent` 可以是站内任意智能体,比如 `claude-code`、`codex`、`cursor`、`gemini-cli`。
61
+ - `docs read <id>`:读取文档全文(Markdown),开头附官方来源链接和核实日期。命令、配置和价格以官方文档为准。