funcoding-cli 0.2.1 → 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 CHANGED
@@ -59,6 +59,8 @@ fun --help
59
59
  | `mcp list` / `mcp remove <name>...` | 列出或删除 MCP 配置,修改前备份原文件 |
60
60
  | `favorites list` | 列出网站上收藏的 Skill 和 MCP Server |
61
61
  | `favorites sync [--dry-run]` | 安装全部收藏,已安装的跳过;需要密钥的 MCP 会列出缺少哪些值 |
62
+ | `lists info <link>` | 查看别人分享的清单(`https://funcoding.ai/share/<id>/`),不需要登录 |
63
+ | `lists add <link> [--dry-run]` | 安装清单里的全部 Skill 和 MCP Server,已安装的跳过 |
62
64
  | `auth login` / `auth logout` / `auth status` | 在浏览器中授权登录(设备码)、退出登录并使登录凭证失效、查看当前账号 |
63
65
  | `news list` / `news daily [<YYYY-MM-DD>]` | AI 热榜,或某一天的 AI 日报 |
64
66
  | `docs search <query> [--agent=<slug>]` | 搜索中文文档 |
@@ -70,7 +72,7 @@ fun --help
70
72
  - 成功时为 `{ "ok": true, ... }`
71
73
  - 失败时为 `{ "ok": false, "error": { "code", "message", "hint", ... } }`
72
74
  - 过程提示一律写到 stderr。
73
- - **不交互**:需要确认的操作通过参数表达,比如覆盖时加 `--force`,`favorites sync` 可以先加 `--dry-run` 预览。
75
+ - **不交互**:需要确认的操作通过参数表达,比如覆盖时加 `--force`,`favorites sync`、`lists add` 可以先加 `--dry-run` 预览。
74
76
  - **`--agent`**:可选 `claude-code`、`codex`、`cursor`,多个用逗号分隔。`all` 表示本机检测到的全部智能体,检测方法是看用户目录下有没有对应的配置目录。不指定时,在 Claude Code 中运行(`CLAUDECODE=1`)默认装给 Claude Code,其他情况也默认 Claude Code。Codex 和 Cursor 没有文档化的环境变量,CLI 不去猜,需要显式指定。
75
77
  - **范围**:`--project` 装到当前项目,不加时装到用户目录。`skills list`、`skills update`、`skills remove`、`mcp list`、`mcp remove` 默认同时查看两处,可以用 `--project` 或 `--user` 缩小范围。
76
78
 
package/dist/cli.js CHANGED
@@ -13,6 +13,7 @@ import { envInfo } from './info.js';
13
13
  import { init, selfUpdate } from './init.js';
14
14
  import { install, list, remove, skillInfo, update } from './install.js';
15
15
  import { mcpAdd, mcpInfo, mcpList, mcpRemove } from './mcp.js';
16
+ import { listAdd, listInfo } from './lists.js';
16
17
  import { favorites, sync } from './sync.js';
17
18
  import { bold, CliError, dim, fail, info, reportError, setJsonMode } from './ui.js';
18
19
  /** 帮助和提示里统一写全名;fun 是同一个命令的简写 */
@@ -182,6 +183,25 @@ const COMMANDS = [
182
183
  { name: 'sync', summary: '安装全部收藏,已安装的跳过', options: ['agent', 'project', 'dry-run'], run: ({ values: v }) => sync(targets(v), !!v['dry-run']) },
183
184
  ],
184
185
  },
186
+ {
187
+ name: 'lists',
188
+ summary: '别人分享的 Skill 和 MCP 清单(不需要登录)',
189
+ commands: [
190
+ {
191
+ name: 'info',
192
+ summary: '查看清单里的 Skill 和 MCP Server',
193
+ params: [['<link>', '清单链接(https://funcoding.ai/share/<id>/)或 id']],
194
+ run: ({ args }) => listInfo(need(args, '清单链接', `${BIN} lists info https://funcoding.ai/share/k3x9q2abcd/`)[0]),
195
+ },
196
+ {
197
+ name: 'add',
198
+ summary: '安装清单里的全部 Skill 和 MCP Server,已安装的跳过',
199
+ params: [['<link>', '清单链接(https://funcoding.ai/share/<id>/)或 id']],
200
+ options: ['agent', 'project', 'dry-run'],
201
+ run: ({ args, values: v }) => listAdd(need(args, '清单链接', `${BIN} lists add https://funcoding.ai/share/k3x9q2abcd/`)[0], targets(v), !!v['dry-run']),
202
+ },
203
+ ],
204
+ },
185
205
  {
186
206
  name: 'auth',
187
207
  summary: '登录 funcoding.ai',
@@ -195,7 +215,7 @@ const COMMANDS = [
195
215
  name: 'news',
196
216
  summary: 'AI 热点',
197
217
  commands: [
198
- { name: 'list', summary: '当前 AI 热榜:多家来源报道的事件,按热度排序', options: ['limit'], run: ({ values }) => newsList(limitOf(values)) },
218
+ { name: 'list', summary: '当前 AI 热榜:过去 48 小时被多个来源共同报道的事件,前 10 名', options: ['limit'], run: ({ values }) => newsList(limitOf(values)) },
199
219
  { name: 'daily', summary: '某一天的 AI 日报', params: [['<date>', 'YYYY-MM-DD;不写时为最新一期']], run: ({ args }) => newsDaily(args[0]) },
200
220
  ],
201
221
  },
package/dist/content.js CHANGED
@@ -26,35 +26,38 @@ export async function find(kind, query, limit) {
26
26
  printCards(kind === 'skill' ? 'Skills' : 'MCP Server', items, (c) => `funcoding ${cmd} add ${c.id}`);
27
27
  });
28
28
  }
29
- function printStory(s, n) {
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
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 日报 · 第 ${issue.no} 期 · ${issue.day}`));
36
+ info(bold(`AI 日报 · ${issue.day}`));
42
37
  info(dim(issue.url));
43
- info('');
44
- info(bold('头条'));
45
- printStory(issue.headline);
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 s of sec.stories)
50
- printStory(s);
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.flash.length > 0) {
53
+ if (issue.flashes.length > 0) {
53
54
  info('');
54
55
  info(bold('快讯'));
55
- for (const s of issue.flash)
56
- info(` · ${s.title} ${dim(s.url)}`);
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) {
@@ -63,7 +66,14 @@ export async function newsList(limit) {
63
66
  if (r.stories.length === 0)
64
67
  return info('暂无热点。');
65
68
  info(bold('AI 热榜') + dim(` ${siteUrl('/news/')}`));
66
- r.stories.forEach((s, i) => printStory(s, i + 1));
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) {
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/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));
@@ -28,11 +28,20 @@ export async function favorites() {
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 me.favorites.skills) {
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 me.favorites.mcp) {
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
- 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
- });
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,6 +1,6 @@
1
1
  {
2
2
  "name": "funcoding-cli",
3
- "version": "0.2.1",
3
+ "version": "0.3.0",
4
4
  "description": "funcoding.ai 的命令行:给 AI 编程智能体用,查找和安装 Agent Skills、配置 MCP Server、同步收藏、查看 AI 热点和中文文档",
5
5
  "type": "module",
6
6
  "bin": {
@@ -13,7 +13,7 @@
13
13
  "skill"
14
14
  ],
15
15
  "scripts": {
16
- "build": "tsc -p tsconfig.json",
16
+ "build": "rm -rf dist && tsc -p tsconfig.json",
17
17
  "test": "node --experimental-strip-types --no-warnings --test 'src/**/*.test.ts'",
18
18
  "prepublishOnly": "pnpm build && pnpm test"
19
19
  },
@@ -65,9 +65,16 @@ npx --yes funcoding-cli@latest init --json
65
65
  - `funcoding favorites list`:列出收藏。
66
66
  - `funcoding favorites sync [--agent=<name>] [--project] [--dry-run]`:安装全部收藏,已装的跳过。建议先用 `--dry-run` 给用户看将要安装什么。`items[]` 里 `status` 为 `skipped` 的条目附有原因和提示,通常是 MCP 缺少必填的环境变量。
67
67
 
68
+ ## 别人分享的清单
69
+
70
+ 用户发来 `https://funcoding.ai/share/<id>/` 这样的链接,是别人在 funcoding.ai 上整理并公开的一组 Skill 和 MCP Server,不需要登录:
71
+
72
+ - `funcoding lists info <链接或 id>`:查看清单里有什么。
73
+ - `funcoding lists add <链接或 id> [--agent=<name>] [--project] [--dry-run]`:安装清单里的全部条目,已装的跳过。建议先用 `--dry-run` 给用户看将要安装什么;结果的 `items[]` 和 `favorites sync` 相同。链接已关闭或重置时返回退出码 3。
74
+
68
75
  ## AI 热点
69
76
 
70
- - `funcoding news list [--limit=<n>]`:当前 AI 热榜(多家来源报道的事件,按热度排序,附中文标题和摘要)。
77
+ - `funcoding news list [--limit=<n>]`:当前 AI 热榜(过去 48 小时被多个来源共同报道的事件,最多 10 个,附最新进展;数据来自 AIHOT)。
71
78
  - `funcoding news daily [<YYYY-MM-DD>]`:某一天的 AI 日报,不写日期时为最新一期。
72
79
 
73
80
  ## 文档
@@ -90,6 +97,7 @@ Commands:
90
97
  skills 查找、安装和管理 Agent Skills(find / info / add / list / update / remove)
91
98
  mcp 查找 MCP Server,并写入各智能体的 MCP 配置(find / info / add / list / remove)
92
99
  favorites 网站上收藏的 Skill 和 MCP Server(list / sync)
100
+ lists 别人分享的 Skill 和 MCP 清单(info / add)
93
101
  auth 登录 funcoding.ai(login / logout / status)
94
102
  news AI 热点(list / daily)
95
103
  docs AI 编程智能体的中文文档(search / fetch)
package/dist/setup.js DELETED
@@ -1,78 +0,0 @@
1
- /**
2
- * setup:
3
- * 1. 没有全局安装时用 npm 全局安装本 CLI,之后终端里可以直接用 funcoding 或简写 fun(--no-global 跳过)
4
- * 2. 把随 CLI 发布的 funcoding Skill(skill/funcoding/SKILL.md)装到智能体,让它知道什么时候、怎么用这个 CLI。
5
- * 这个 Skill 不在网站目录里,来源记录标成 builtin,update 时从当前版本的 CLI 重新复制
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
- /** 全局安装当前版本;npm 的输出写到 stderr,不影响 --json 的 stdout */
26
- function installGlobal() {
27
- if (onPath('funcoding'))
28
- return { status: 'present' };
29
- const pkg = `funcoding-cli@${VERSION}`;
30
- info(dim(`正在全局安装 ${pkg},之后可以直接使用 funcoding 或 fun 命令…`));
31
- const r = spawnSync('npm', ['install', '--global', pkg], { stdio: ['ignore', process.stderr, process.stderr], shell: isWindows });
32
- if (r.status === 0)
33
- return { status: 'installed' };
34
- return { status: 'failed', hint: `手动运行 npm install -g ${pkg}(权限不足时参考 https://docs.npmjs.com/resolving-eacces-permissions-errors-when-installing-packages-globally)` };
35
- }
36
- export const BUILTIN_ID = 'funcoding';
37
- const SOURCE = new URL('../skill/funcoding/SKILL.md', import.meta.url);
38
- export const builtinContent = () => readFile(SOURCE, 'utf8');
39
- export async function installBuiltin(targets, force) {
40
- const content = await builtinContent();
41
- const dirs = targets.map((t) => ({ ...t, dir: join(skillsRoot(t.agent, t.scope), BUILTIN_ID) }));
42
- const foreign = [];
43
- for (const d of dirs) {
44
- const meta = await readFile(join(d.dir, META_FILE), 'utf8').catch(() => null);
45
- const present = meta !== null || (await stat(d.dir).then(() => true, () => false));
46
- // 之前 setup 装的直接换成新版;同名的其他 Skill 要 --force,并且先备份
47
- if (present && !(meta && JSON.parse(meta).builtin))
48
- foreign.push(d);
49
- }
50
- if (foreign.length > 0 && !force)
51
- fail('exists', `${foreign.map((d) => d.dir).join('、')} 已存在,且不是 funcoding setup 安装的。`, '确认要覆盖就加 --force(会先备份到配置目录)');
52
- for (const d of dirs) {
53
- const tmp = join(dirname(d.dir), `.funcoding-tmp-${randomBytes(4).toString('hex')}`);
54
- await mkdir(tmp, { recursive: true });
55
- await writeFile(join(tmp, 'SKILL.md'), content);
56
- 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`);
57
- if (foreign.includes(d))
58
- await moveAway(d.dir, d.agent, BUILTIN_ID);
59
- else
60
- await rm(d.dir, { recursive: true, force: true });
61
- await rename(tmp, d.dir);
62
- }
63
- return dirs;
64
- }
65
- export async function setup(targets, force, global) {
66
- const cli = global ? installGlobal() : { status: 'skipped' };
67
- const dirs = await installBuiltin(targets, force);
68
- done({ cli, installed: dirs.map((d) => ({ id: BUILTIN_ID, agent: d.agent, scope: d.scope, dir: d.dir })) }, () => {
69
- if (cli.status === 'installed')
70
- info(green(`✓ 已全局安装 funcoding-cli,在终端里输入 ${cyan('funcoding')} 或 ${cyan('fun')} 即可使用`));
71
- if (cli.status === 'failed')
72
- warn(`全局安装失败,${cli.hint}`);
73
- for (const d of dirs)
74
- info(green(`✓ 已把 funcoding Skill 装到 ${d.dir}`));
75
- info(`之后可以直接让 ${dirs.map((d) => AGENTS[d.agent].label).join('、')} 帮你查找和安装 Skill、配置 MCP,例如「找一个处理 PDF 的 Skill 装上」。`);
76
- info(dim(`也可以输入 ${[...new Set(dirs.map((d) => cyan(AGENTS[d.agent].invoke(BUILTIN_ID))))].join(' / ')} 主动调用。新开会话后生效。`));
77
- });
78
- }