cotutor 0.4.4 → 0.4.5

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.
Files changed (55) hide show
  1. package/CHANGELOG.md +9 -1
  2. package/INSTALL.md +159 -0
  3. package/README.md +35 -19
  4. package/dist/app/cli/activities.d.ts +1 -1
  5. package/dist/app/cli/activities.js +2 -2
  6. package/dist/app/cli/cert.js +2 -1
  7. package/dist/app/cli/chalk/main.d.ts +0 -1
  8. package/dist/app/cli/chalk/main.js +8 -12
  9. package/dist/app/cli/commands.d.ts +50 -0
  10. package/dist/app/cli/commands.js +353 -0
  11. package/dist/app/cli/doctor.js +32 -17
  12. package/dist/app/cli/guide.d.ts +6 -0
  13. package/dist/app/cli/guide.js +17 -0
  14. package/dist/app/cli/init.d.ts +2 -0
  15. package/dist/app/cli/init.js +60 -6
  16. package/dist/app/cli/main.js +109 -46
  17. package/dist/app/cli/migrate.js +1 -1
  18. package/dist/app/cli/port.js +2 -10
  19. package/dist/app/cli/sandbox.js +2 -7
  20. package/dist/app/cli/service.d.ts +4 -1
  21. package/dist/app/cli/service.js +5 -2
  22. package/dist/app/cli/shipped.js +1 -1
  23. package/dist/app/cli/skeleton.d.ts +2 -0
  24. package/dist/app/cli/skeleton.js +3 -2
  25. package/dist/app/cli/skills.js +2 -2
  26. package/dist/app/cli/themes.js +2 -2
  27. package/dist/app/cli/tray.d.ts +6 -1
  28. package/dist/app/cli/tray.js +8 -5
  29. package/dist/app/cli/tutors.d.ts +2 -2
  30. package/dist/app/cli/tutors.js +4 -4
  31. package/dist/app/cli/update-cmd.js +4 -4
  32. package/dist/app/cli/usage.d.ts +26 -2
  33. package/dist/app/cli/usage.js +167 -38
  34. package/dist/app/http/app.js +1 -1
  35. package/dist/app/skills/analyze-doc.js +4 -8
  36. package/dist/app/skills/home-doc.js +3 -7
  37. package/dist/app/skills/tune-doc.js +4 -8
  38. package/dist/app/workspace.d.ts +16 -3
  39. package/dist/app/workspace.js +31 -9
  40. package/dist/runtime/home/home.js +1 -1
  41. package/dist/runtime/runner.js +2 -2
  42. package/dist/shared/home.d.ts +2 -0
  43. package/dist/shared/home.js +9 -0
  44. package/dist/ui/dev/dev-page.js +5 -5
  45. package/dist/ui/parent/parent-home-page.js +1 -1
  46. package/install.ps1 +5 -4
  47. package/install.sh +3 -3
  48. package/package.json +7 -5
  49. package/skills/cotutor-analyze/SKILL.md +1 -1
  50. package/skills/cotutor-analyze/references//345/221/275/344/273/244/344/270/216/346/226/207/344/273/266.md +168 -10
  51. package/skills/cotutor-home/SKILL.md +1 -1
  52. package/skills/cotutor-home/references//345/221/275/344/273/244.md +75 -7
  53. package/skills/cotutor-tune/SKILL.md +8 -8
  54. package/skills/cotutor-tune/references//345/255/227/346/256/265.md +164 -10
  55. package/skills/cotutor-tutor/SKILL.md +1 -1
@@ -42,6 +42,6 @@ export async function writeManifest(root, m) {
42
42
  const file = join(root, SHIPPED_FILE);
43
43
  await mkdir(dirname(file), { recursive: true });
44
44
  const tmp = `${file}.tmp`;
45
- await writeFile(tmp, `${JSON.stringify({ ...m, _note: '出厂时老师文件的 hash,cotutor upgrade 据此分辨「没改过」与「改过」;机器写,不用手改' }, null, 2)}\n`);
45
+ await writeFile(tmp, `${JSON.stringify({ ...m, _note: '出厂时老师文件的 hash,cotutor refresh 据此分辨「没改过」与「改过」;机器写,不用手改' }, null, 2)}\n`);
46
46
  await rename(tmp, file);
47
47
  }
@@ -30,6 +30,8 @@ export interface ConfigTemplateInput {
30
30
  slug: string;
31
31
  name?: string;
32
32
  port?: number;
33
+ /** 写进 paths.vault 的(~/… 或绝对路径);不给就不写,vault 落在 workspace 根(旧版的布局) */
34
+ vault?: string;
33
35
  tutors: ShippedAgent[];
34
36
  }
35
37
  /** cotutor.json 模板:只在文件不存在时写入;政策文件永不自动重建或覆盖(家长的决定不由机器代做)。 */
@@ -4,7 +4,8 @@
4
4
  * cotutor.json / .cotutor/shipped.json(出厂 hash)/ .agents/skills/(技能;.claude/skills 是指过去的链,给 Claude Code)
5
5
  * agents/<name>.md(老师文件,拷自本包 grades/grade-<n>/agents/,是家长的;所有运行时都从这里拿老师正文)与 agents/<name>/(老师的家 = 会话 cwd)
6
6
  * ledger/(产物账本)/ conversations/(对话索引与转录)
7
- * vault 侧(《obsidian仓库设计.md》):档案 / 课程表 / 日记 / 教材 / 计划 / 参考,init 只建档案与参考 README(缺了才写)
7
+ * vault 侧(《obsidian仓库设计.md》):档案 / 课程表 / 日记 / 教材 / 计划 / 参考,init 只建档案与参考 README(缺了才写);
8
+ * vault 不在 workspace 里,缺省是旁边的 <目录名>-vault/(2026-11-09;旧版没写 paths.vault 的落在 workspace 根)
8
9
  */
9
10
  import { readFileSync } from 'node:fs';
10
11
  import { readFile, readdir } from 'node:fs/promises';
@@ -140,7 +141,7 @@ export function configTemplate(input) {
140
141
  title: input.name ? `${input.name}的老师们` : 'cotutor',
141
142
  kid: { slug: input.slug, ...(input.name ? { name: input.name } : {}) },
142
143
  server: { port: input.port ?? DEFAULT_PORT },
143
- paths: {},
144
+ paths: input.vault ? { vault: input.vault } : {},
144
145
  policyDefaults: {},
145
146
  tutors,
146
147
  // 缺省 api:应用直连百炼,家长端「首页」填一次按量 key + 业务空间 id 就能用(2026-10-31);2026-11-02 起只有 API 运行时
@@ -124,7 +124,7 @@ export async function installSkills(root) {
124
124
  steps.push({ item, action: 'exists', note: s.state === 'latest' ? '已按本包刷新(机器件)' : '已按本包换新(机器件,不认改动)' });
125
125
  }
126
126
  else
127
- steps.push({ item, action: 'exists', note: s.state === 'custom' ? `自定义(基于 ${s.basedOn})` : s.state === 'upgradable' ? '可升级(cotutor upgrade)' : s.state === 'untracked' ? '已有(没有出厂记录);升级时当自定义对待' : undefined });
127
+ steps.push({ item, action: 'exists', note: s.state === 'custom' ? `自定义(基于 ${s.basedOn})` : s.state === 'upgradable' ? '可升级(cotutor refresh)' : s.state === 'untracked' ? '已有(没有出厂记录);升级时当自定义对待' : undefined });
128
128
  }
129
129
  if (touched)
130
130
  await writeManifest(root, { ...manifest, version: PACKAGE_VERSION });
@@ -187,7 +187,7 @@ export async function linkClaudeSkills(root) {
187
187
  return key(resolve(dirname(link), to.replace(/^\\\\\?\\/, ''))) === key(resolve(root, SKILLS_DIR)) ? { item, action: 'exists' } : { item, action: 'kept', note: `已经是指向 ${to} 的链,没动` };
188
188
  }
189
189
  if (st)
190
- return { item, action: 'kept', note: `${CLAUDE_SKILLS_LINK} 是实体目录,没建链:把里面的技能挪进 ${SKILLS_DIR}/、删掉它,再跑 cotutor upgrade` };
190
+ return { item, action: 'kept', note: `${CLAUDE_SKILLS_LINK} 是实体目录,没建链:把里面的技能挪进 ${SKILLS_DIR}/、删掉它,再跑 cotutor refresh` };
191
191
  await mkdir(dirname(link), { recursive: true });
192
192
  try {
193
193
  await symlink(target, link, process.platform === 'win32' ? 'junction' : undefined);
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * 主题(themes/<name>/theme.json + kid.css):孩子端板书的样子。样子的真相在主题里,不在卡的协议里。
3
3
  * 出厂主题 default 在包根 themes/default/;init **拷贝**进 workspace 的 themes/default/(拷进来就是家长的,改了刷新就有),
4
- * 目录 hash 记 .cotutor/shipped.json 的 themes,与老师文件 / skill 同一套:没改过的随 cotutor upgrade 换新,改过的报 diff 保留。
4
+ * 目录 hash 记 .cotutor/shipped.json 的 themes,与老师文件 / skill 同一套:没改过的随 cotutor refresh 换新,改过的报 diff 保留。
5
5
  * 自家的主题(cotutor add-theme <name> 拷一份 default 来改,或直接建目录)永远 untracked,upgrade 不碰。
6
6
  */
7
7
  import { cp, lstat, mkdir, readFile, readdir, rm, writeFile } from 'node:fs/promises';
@@ -103,7 +103,7 @@ export async function installThemes(root) {
103
103
  steps.push({ item, action: 'created', note: '孩子端的主题(theme.json 槽的清单 + kid.css 样式),拷自本包,改了刷新就有' });
104
104
  }
105
105
  else
106
- steps.push({ item, action: 'exists', note: s.state === 'custom' ? `自定义(基于 ${s.basedOn})` : s.state === 'upgradable' ? '可升级(cotutor upgrade)' : s.state === 'untracked' ? '已有(没有出厂记录);升级时当自定义对待' : undefined });
106
+ steps.push({ item, action: 'exists', note: s.state === 'custom' ? `自定义(基于 ${s.basedOn})` : s.state === 'upgradable' ? '可升级(cotutor refresh)' : s.state === 'untracked' ? '已有(没有出厂记录);升级时当自定义对待' : undefined });
107
107
  }
108
108
  if (touched)
109
109
  await writeManifest(root, { ...manifest, version: PACKAGE_VERSION });
@@ -37,6 +37,8 @@ export interface TrayProbe {
37
37
  name: string;
38
38
  kid: string;
39
39
  root: string;
40
+ /** vault 在哪(cotutor.json 的 paths.vault;没写就是 root):Windows 上读不到 vault 时打开它 */
41
+ vault: string;
40
42
  logDir: string | null;
41
43
  port: number;
42
44
  health: Health | null;
@@ -142,6 +144,7 @@ export interface TrayDeps {
142
144
  inspect(s: InstalledService): Promise<{
143
145
  kid: string;
144
146
  port: number;
147
+ vault: string;
145
148
  }>;
146
149
  probe(port: number): Promise<Health | null>;
147
150
  holder(port: number): Promise<string | null>;
@@ -188,10 +191,12 @@ export declare function openCommand(platform: NodeJS.Platform, target: string):
188
191
  export declare function openTarget(platform: NodeJS.Platform, target: string): void;
189
192
  /** 日志:一个文件,过 1 MB 换一次,只留上一份 */
190
193
  export declare function trayNote(dir: string): (text: string) => void;
191
- /** 孩子叫什么、哪个端口:只读这两样,不加载 zod(托盘进程常驻,要轻);读不了就用服务名和缺省端口 */
194
+ /** 孩子叫什么、哪个端口、vault 在哪:只读这三样,不加载 zod(托盘进程常驻,要轻);读不了就用服务名、缺省端口、workspace 根。
195
+ * vault 照 workspace.ts 的 resolvePaths 算:paths.vault 相对 workspace 根,没写就是根(旧版的布局) */
192
196
  export declare function inspectService(s: InstalledService): Promise<{
193
197
  kid: string;
194
198
  port: number;
199
+ vault: string;
195
200
  }>;
196
201
  /** 真的依赖,给 runTray */
197
202
  export declare function realTrayDeps(host: ServiceHost, dir: string, file: string, version: () => string): TrayDeps;
@@ -16,6 +16,7 @@ import { join } from 'node:path';
16
16
  import { createInterface } from 'node:readline';
17
17
  import { DEFAULT_PORT } from "../../shared/default-port.js";
18
18
  import { UsageError } from "../../shared/errors.js";
19
+ import { expandPath } from "../../shared/home.js";
19
20
  import { siteUrl } from "../../shared/site-url.js";
20
21
  import { isNewer } from "../../shared/upgrade.js";
21
22
  import { portHolder } from "./port-holder.js";
@@ -60,7 +61,7 @@ export function issuesOf(p, o) {
60
61
  if (h.configError)
61
62
  out.push({ text: 'cotutor.json 写坏了:改的没生效', fix: { kind: 'open', target: p.root } });
62
63
  if (h.vault === 'denied')
63
- out.push(o.platform === 'darwin' ? { text: '读不到 vault:要给 node 开磁盘权限', fix: { kind: 'open', target: MAC_FULL_DISK } } : { text: '读不到 vault:看看这个目录的权限', fix: { kind: 'open', target: p.root } });
64
+ out.push(o.platform === 'darwin' ? { text: '读不到 vault:要给 node 开磁盘权限', fix: { kind: 'open', target: MAC_FULL_DISK } } : { text: '读不到 vault:看看这个目录的权限', fix: { kind: 'open', target: p.vault } });
64
65
  else if (h.vault === 'missing')
65
66
  out.push({ text: 'vault 不在了:看看 cotutor.json 的 paths.vault', fix: { kind: 'open', target: p.root } });
66
67
  if (h.bailian === false)
@@ -371,7 +372,7 @@ export function runTray(d) {
371
372
  }
372
373
  else
373
374
  holders.delete(s.name);
374
- probes.push({ name: s.name, kid: w.kid, root: s.root, logDir: s.logDir, port: w.port, health, holder });
375
+ probes.push({ name: s.name, kid: w.kid, root: s.root, vault: w.vault, logDir: s.logDir, port: w.port, health, holder });
375
376
  }
376
377
  const r = model.update(probes, d.now());
377
378
  rows = r.rows;
@@ -501,15 +502,17 @@ export function trayNote(dir) {
501
502
  catch { /* 写不了日志不能把图标带倒 */ }
502
503
  };
503
504
  }
504
- /** 孩子叫什么、哪个端口:只读这两样,不加载 zod(托盘进程常驻,要轻);读不了就用服务名和缺省端口 */
505
+ /** 孩子叫什么、哪个端口、vault 在哪:只读这三样,不加载 zod(托盘进程常驻,要轻);读不了就用服务名、缺省端口、workspace 根。
506
+ * vault 照 workspace.ts 的 resolvePaths 算:paths.vault 相对 workspace 根,没写就是根(旧版的布局) */
505
507
  export async function inspectService(s) {
506
508
  try {
507
509
  const c = JSON.parse(await readFile(join(s.root, 'cotutor.json'), 'utf8'));
508
510
  const kid = typeof c.kid?.name === 'string' && c.kid.name ? c.kid.name : typeof c.kid?.slug === 'string' && c.kid.slug ? c.kid.slug : s.name;
509
- return { kid, port: typeof c.server?.port === 'number' ? c.server.port : DEFAULT_PORT };
511
+ const vault = typeof c.paths?.vault === 'string' && c.paths.vault ? expandPath(c.paths.vault, s.root) : s.root;
512
+ return { kid, port: typeof c.server?.port === 'number' ? c.server.port : DEFAULT_PORT, vault };
510
513
  }
511
514
  catch {
512
- return { kid: s.name, port: DEFAULT_PORT };
515
+ return { kid: s.name, port: DEFAULT_PORT, vault: s.root };
513
516
  }
514
517
  }
515
518
  /** 真的依赖,给 runTray */
@@ -43,7 +43,7 @@ export interface UpgradeStep {
43
43
  /** 最小行 diff(LCS),只为让家长看清改了哪几行;不追求 git 那么好 */
44
44
  export declare function lineDiff(a: string, b: string): string[];
45
45
  /**
46
- * cotutor upgrade:latest 跳过;upgradable 换新;custom / untracked 只报 diff 并保留(--force <name> 才覆盖,原文留 .bak);缺的补。
46
+ * cotutor refresh:latest 跳过;upgradable 换新;custom / untracked 只报 diff 并保留(--force <name> 才覆盖,原文留 .bak);缺的补。
47
47
  */
48
48
  export declare function upgradeTutors(root: string, opts?: {
49
49
  force?: string[];
@@ -65,7 +65,7 @@ export declare function removeTutorFile(root: string, name: string): Promise<{
65
65
  /** 出厂那份老师文件的原文;不是出厂的 → null */
66
66
  export declare function shippedTutorText(name: string, root?: string): Promise<string | null>;
67
67
  /**
68
- * 只把一位出厂老师的文件换回出厂那份(工作台「恢复出厂」,与 upgrade --force <老师> 同一条路,但不碰别的老师):
68
+ * 只把一位出厂老师的文件换回出厂那份(工作台「恢复出厂」,与 cotutor reset <老师> 同一条路,但不碰别的老师):
69
69
  * 现在那份和出厂不一样就先存 .bak。不是出厂的 → UsageError。返回 .bak 的路径(没存就是 null)
70
70
  */
71
71
  export declare function restoreTutor(root: string, name: string): Promise<{
@@ -4,7 +4,7 @@
4
4
  * 和老师的家 agents/<name>/ 并排,同包里一样(2026-10-30 从 .claude/agents/ 挪来,老 workspace 由 migrate.ts 的 migrateLayout 搬)。
5
5
  * 出厂时的内容 hash 记在 .cotutor/shipped.json(机器文件,家长不用管),据此分三种状态:
6
6
  * latest 与本包一致
7
- * upgradable 与记录的出厂 hash 一致(没改过,只是包更新了)→ cotutor upgrade 直接换新
7
+ * upgradable 与记录的出厂 hash 一致(没改过,只是包更新了)→ cotutor refresh 直接换新
8
8
  * custom 改过 → upgrade 只报 diff,不动
9
9
  * untracked 没有记录(旧workspace、或家长手写的老师)→ 当 custom 对待
10
10
  */
@@ -82,7 +82,7 @@ export async function installTutors(root, configured = []) {
82
82
  steps.push({ item, action: 'kept', note: '已有(不是出厂件,或没有记录);升级时当自定义对待' });
83
83
  }
84
84
  else {
85
- steps.push({ item, action: 'exists', note: s.state === 'custom' ? `自定义(基于 ${s.basedOn})` : s.state === 'upgradable' ? `可升级(cotutor upgrade)` : undefined });
85
+ steps.push({ item, action: 'exists', note: s.state === 'custom' ? `自定义(基于 ${s.basedOn})` : s.state === 'upgradable' ? `可升级(cotutor refresh)` : undefined });
86
86
  }
87
87
  }
88
88
  for (const name of configured.filter((n) => !shipped.some((a) => a.name === n))) {
@@ -151,7 +151,7 @@ export function lineDiff(a, b) {
151
151
  return out;
152
152
  }
153
153
  /**
154
- * cotutor upgrade:latest 跳过;upgradable 换新;custom / untracked 只报 diff 并保留(--force <name> 才覆盖,原文留 .bak);缺的补。
154
+ * cotutor refresh:latest 跳过;upgradable 换新;custom / untracked 只报 diff 并保留(--force <name> 才覆盖,原文留 .bak);缺的补。
155
155
  */
156
156
  export async function upgradeTutors(root, opts = {}) {
157
157
  const manifest = await readManifest(root);
@@ -229,7 +229,7 @@ export async function shippedTutorText(name, root) {
229
229
  return a ? readFile(a.file, 'utf8') : null;
230
230
  }
231
231
  /**
232
- * 只把一位出厂老师的文件换回出厂那份(工作台「恢复出厂」,与 upgrade --force <老师> 同一条路,但不碰别的老师):
232
+ * 只把一位出厂老师的文件换回出厂那份(工作台「恢复出厂」,与 cotutor reset <老师> 同一条路,但不碰别的老师):
233
233
  * 现在那份和出厂不一样就先存 .bak。不是出厂的 → UsageError。返回 .bak 的路径(没存就是 null)
234
234
  */
235
235
  export async function restoreTutor(root, name) {
@@ -21,7 +21,7 @@ const nodeTooOld = (have, min) => {
21
21
  const worthSaying = (out) => out.split('\n').map((l) => l.trimEnd()).filter((l) => l.trim() && !/已是最新|已换新/.test(l));
22
22
  async function updateOne(d, s, target) {
23
23
  let ok = true;
24
- const up = await d.cli(['upgrade', '--workspace', s.root]);
24
+ const up = await d.cli(['refresh', '--workspace', s.root]);
25
25
  for (const l of worthSaying(up.out))
26
26
  d.print(` ${l}`);
27
27
  if (up.code !== 0) {
@@ -53,14 +53,14 @@ async function updateOne(d, s, target) {
53
53
  const checks = JSON.parse(doc.out).checks;
54
54
  const migrate = checks.find((c) => c.name === 'config.migrate' && !c.ok);
55
55
  if (migrate)
56
- d.print(` ! ${s.kid} 的配置要迁移:${migrate.cmd ?? 'cotutor upgrade --config'}`);
56
+ d.print(` ! ${s.kid} 的配置要迁移:${migrate.cmd ?? 'cotutor refresh --config'}`);
57
57
  }
58
58
  catch { /* 体检读不了不影响升级 */ }
59
59
  return ok;
60
60
  }
61
61
  export async function runUpdate(d, o = {}) {
62
62
  if (d.fromRepo) {
63
- d.print('这份是从仓库跑的:git pull,再 cotutor upgrade、cotutor service restart。cotutor update 只管 npm 装的。');
63
+ d.print('这份是从仓库跑的:git pull,再 cotutor refresh、cotutor service restart。cotutor update 只管 npm 装的。');
64
64
  return 1;
65
65
  }
66
66
  const services = await d.services();
@@ -114,7 +114,7 @@ export async function runUpdate(d, o = {}) {
114
114
  if (!services.length) {
115
115
  const root = d.workspace();
116
116
  if (root) {
117
- const up = await d.cli(['upgrade', '--workspace', root]);
117
+ const up = await d.cli(['refresh', '--workspace', root]);
118
118
  for (const l of worthSaying(up.out))
119
119
  d.print(` ${l}`);
120
120
  if (up.code !== 0)
@@ -1,2 +1,26 @@
1
- /** 命令用法(cotutor --help;技能 cotutor-analyze 的 references/命令与文件.md 从这里取分析用的那几行,所以放单独文件,别的模块 import 不会跑到 CLI) */
2
- export declare const USAGE = "\u7528\u6CD5:\n cotutor init <slug> [--dir <path>] [--name <\u5B69\u5B50\u540D>] [--grade <1-6>] [--port <n>] \u5EFA ~/cotutor/<slug>/ \u9AA8\u67B6(\u5E42\u7B49\u8865\u7F3A;\u9ED8\u8BA4\u4E8C\u5E74\u7EA7\u6559\u5B66\u5305;\u7AEF\u53E3\u5728\u8FD9\u91CC\u9009\u4E00\u6B21\u5199\u8FDB cotutor.json:443 \u4F18\u5148,\u4E0D\u884C\u5C31 8443\u30018445\u2026,\u4EE5\u540E\u4E0D\u53D8)\n cotutor grades [--json] \u5217\u51FA\u672C\u5305\u53EF\u5B89\u88C5\u7684\u5E74\u7EA7\u6559\u5B66\u5305\n cotutor doctor [--workspace <dir>] [--json] [--live] [--fix] \u9010\u9879\u4F53\u68C0;--live \u771F\u8FDE\u4E00\u6B21\u767E\u70BC\u5B9E\u65F6\u6A21\u578B\u3001\u771F\u5408\u6210\u4E00\u53E5\u914D\u97F3(\u82B1\u4E00\u5206\u94B1)\u628A API \u5C42\u7684\u5751\u6446\u51FA\u6765(\u771F\u8DD1\u8001\u5E08\u7528 cotutor send);--json \u91CC\u4E0D\u8FC7\u7684\u9879\u5E26 cmd(\u80FD\u76F4\u63A5\u8DD1)\u6216 human(\u8981\u4EBA\u6765);--fix \u628A\u5E26 cmd \u7684\u4E00\u6761\u6761\u4FEE\u6389,\u8981\u4EBA\u6765\u7684\u7559\u7740\n cotutor report [--workspace <dir>] \u8BCA\u65AD\u4FE1\u606F:\u7248\u672C\u3001\u7CFB\u7EDF\u3001\u670D\u52A1\u3001\u4F53\u68C0\u6CA1\u8FC7\u7684\u3001\u6700\u8FD1\u62A5\u9519\u7684\u539F\u56E0;\u4E0D\u542B\u5B69\u5B50\u7684\u5BF9\u8BDD\u3001\u7167\u7247\u3001\u5F55\u97F3\u3001\u6863\u6848\u548C key,\u540D\u5B57\u548C IP \u5DF2\u62B9\u6389,\u5148\u770B\u518D\u53D1\n cotutor backup [list | restore <\u540D\u5B57> [--yes]] [--workspace <dir>] \u5907\u4EFD\u5230 workspace \u65C1\u8FB9\u7684 .backups/:\u4E0D\u542B\u5F55\u97F3;\u670D\u52A1\u5F00\u7740\u65F6\u6BCF\u5929\u4E00\u4EFD(\u7559 14 \u4EFD),cotutor upgrade \u524D\u4E00\u4EFD(\u7559 5 \u4EFD);\u6CA1\u53D8\u7684\u6587\u4EF6\u786C\u94FE\u63A5,\u4E0D\u5360\u7B2C\u4E8C\u4EFD\u5730\u65B9;restore \u5148\u628A\u73B0\u5728\u7684\u4E5F\u5907\u4E00\u4EFD\u518D\u62F7\u56DE\u53BB,\u4E0D\u52A0 --yes \u53EA\u8BF4\u4F1A\u505A\u4EC0\u4E48\n cotutor update [--anyway] \u5347\u7EA7\u4E00\u6761\u547D\u4EE4\u7BA1\u5230\u5E95:\u95EE npm \u6709\u6CA1\u6709\u65B0\u7248\u672C,\u6362\u7A0B\u5E8F,\u6BCF\u4E2A\u5B69\u5B50\u66F4\u65B0\u51FA\u5382\u6587\u4EF6\u3001\u91CD\u8D77\u670D\u52A1\u548C\u56FE\u6807;\u5B69\u5B50\u5728\u901A\u8BDD\u65F6\u4E0D\u52A8(--anyway \u624D\u52A8),npm \u6CA1\u6210\u670D\u52A1\u4E0D\u52A8\n cotutor upgrade [--workspace <dir>] [--force <\u8001\u5E08>]... \u8001\u5E08\u6587\u4EF6\u3001activity \u4E0E skill \u6362\u65B0\u7248:\u6CA1\u6539\u8FC7\u7684\u76F4\u63A5\u6362,\u6539\u8FC7\u7684\u53EA\u62A5 diff(--force <\u8001\u5E08> \u6216 --force <\u8001\u5E08>/<\u540D> \u624D\u8986\u76D6,\u539F\u6587\u7559 .bak)\n cotutor upgrade --config [--dry-run] [--workspace <dir>] cotutor.json \u8865\u7F3A:\u65B0\u51FA\u5382\u8001\u5E08 / \u8FD0\u884C\u65F6 / \u547D\u4EE4\u6A21\u677F\u65D7\u6807(\u53EA\u52A0\u7F3A\u7684,\u4F60\u6539\u8FC7\u7684\u503C\u4E0D\u52A8)\n cotutor add <\u8001\u5E08\u540D> --display <\u663E\u793A\u540D> [--subject <\u5B66\u79D1>] [--avatar <emoji|\u56FE\u7247\u76F8\u5BF9\u8DEF\u5F84>] [--hidden] \u52A0\u4E00\u4F4D\u81EA\u5BB6\u7684\u8001\u5E08:\u51FA\u6A21\u677F\u6587\u4EF6\u3001\u8FDB cotutor.json\u3001\u5EFA\u76EE\u5F55(\u540D\u5B57\u4EE5 -tutor \u7ED3\u5C3E\u7684\u662F\u8001\u5E08,figshot \u7ED9\u5B83\u505A\u5934\u50CF)\n cotutor add-theme <\u4E3B\u9898\u540D> [--from <\u4E3B\u9898>] [--workspace <dir>] \u52A0\u4E00\u4E2A\u81EA\u5BB6\u7684\u4E3B\u9898:\u62F7\u4E00\u4EFD(\u7F3A\u7701\u51FA\u5382\u7684 default)\u5230 themes/<\u4E3B\u9898\u540D>/,\u6539 cotutor.json \u7684 kid.theme \u6362\u8FC7\u53BB\n cotutor serve [--workspace <dir>] [--port <n>] [--http] [--trace] [--open] [--log-dir <\u76EE\u5F55>] \u5728\u7EC8\u7AEF\u91CC\u8D77\u670D\u52A1(\u4E00 workspace \u4E00\u8FDB\u7A0B;~/.config/cotutor/certs/ \u6709\u8BC1\u4E66\u5C31\u8D70 HTTPS;--trace \u6BCF\u4E00\u8F6E\u7684\u4E8B\u4EF6\u6309\u9053\u6253\u5370;\u53EA\u6253\u5370\u5BB6\u957F\u7AEF\u9996\u9875\u7684\u5730\u5740,\u5B69\u5B50\u7684\u5E73\u677F\u5728\u90A3\u91CC\u626B\u7801;--open \u987A\u624B\u6253\u5F00,\u65E7\u540D --open-qr \u7167\u6837\u8BA4;--log-dir \u8F93\u51FA\u6309\u5929\u843D\u6587\u4EF6,\u540E\u53F0\u670D\u52A1\u7528;\u5173\u4E86\u7EC8\u7AEF\u5C31\u505C,\u4E00\u76F4\u5F00\u7740\u7528 cotutor service)\n cotutor open [--workspace <dir>] [--sandbox] [--print] [--json] \u6253\u5F00\u5BB6\u957F\u7AEF(\u8BFB cotutor.json,\u4E0D\u7528\u8BB0\u7AEF\u53E3);--sandbox \u5F00\u8BD5\u7528\u7684\u90A3\u4E2A(\u7AEF\u53E3\u6C38\u8FDC\u662F\u771F\u7684 +1);--print \u53EA\u6253\u5370\u5730\u5740;\u6CA1\u5728\u8DD1\u5C31\u8BF4\u600E\u4E48\u8D77,\u4EE5 1 \u9000\n cotutor service install | uninstall | start | stop | restart [--workspace <dir>] [--no-tray] [--no-open] \u628A serve \u88C5\u6210\u8FD9\u53F0\u7535\u8111\u7684\u540E\u53F0\u670D\u52A1(macOS launchd\u3001Linux systemd \u7528\u6237\u670D\u52A1\u3001Windows \u7528\u6237\u7684\u767B\u5F55\u9879,\u90FD\u4E0D\u8981\u7BA1\u7406\u5458):\u767B\u5F55\u540E\u81EA\u5DF1\u8D77\u6765\u3001\u5D29\u4E86 10 \u79D2\u540E\u518D\u8D77\u3001\u65E5\u5FD7\u6309\u5929\u843D\u6587\u4EF6\u7559 14 \u5929;\u4E00\u4E2A workspace \u4E00\u4E2A;\u7B2C\u4E00\u6B21 install \u6253\u5F00\u4E00\u6B21\u5BB6\u957F\u7AEF(--no-open \u4E0D\u5F00),\u4EE5\u540E\u7684\u542F\u52A8\u90FD\u4E0D\u5F00;\u6362\u4E86 node \u7248\u672C\u6216\u5305\u632A\u4E86\u5730\u65B9\u518D install \u4E00\u6B21\n cotutor tray on | off | status [--json] \u83DC\u5355\u680F / \u4EFB\u52A1\u680F\u56FE\u6807(macOS\u3001Windows;service install \u65F6\u4E00\u8D77\u88C5,--no-tray \u4E0D\u5E26):\u6B63\u5E38\u65F6\u53EA\u6709 logo,\u6709\u4E8B\u51FA\u6A59\u70B9,\u6CA1\u5728\u8DD1\u65F6\u53D8\u6DE1\u52A0\u7EA2\u70B9\u5E76\u901A\u77E5\u4E00\u6B21;\u70B9\u5B83\u770B\u72B6\u6001\u3001\u6253\u5F00\u5BB6\u957F\u7AEF\u3001\u91CD\u542F\u3001\u770B\u65E5\u5FD7\u3001\u9000\u51FA;off \u5173\u6389\u5E76\u8BB0\u4F4F\n cotutor service status [--json] | logs [--lines <n>] [--follow] \u540E\u53F0\u670D\u52A1\u5728\u4E0D\u5728\u8DD1\u3001\u7AEF\u53E3\u7B54\u4E0D\u7B54\u5E94\u3001\u8DD1\u7684\u54EA\u4E2A\u7248\u672C\u3001vault \u8BFB\u4E0D\u8BFB\u5F97\u5230\u3001\u65E5\u5FD7\u5728\u54EA;--json \u8FD8\u7B54\u5E73\u677F\u5730\u5740\u3001\u8BC1\u4E66\u3001\u767E\u70BC\u914D\u6CA1\u914D\u3001\u56FE\u6807;logs \u770B\u6700\u8FD1\u7684\u65E5\u5FD7\n cotutor pin [off | status] [--pin <\u6570\u5B57>] [--json] \u5BB6\u957F\u7AEF /parent \u4E0E\u5DE5\u4F5C\u53F0 /dev \u7684\u9501:\u673A\u5668\u7EA7 PIN(\u6240\u6709 workspace \u5171\u7528),iPad \u4E0A\u8F93\u4E00\u6B21\u3001\u8FD9\u53F0\u7535\u8111\u514D\u5BC6;\u6CA1\u8BBE\u5C31\u5168\u5F00(\u5BB6\u957F\u5728\u65C1\u8FB9\u53EF\u4EE5\u4E0D\u8BBE);off \u64A4\u6389\n cotutor cert [--host <\u540D\u6216IP>]... \u7528 mkcert \u5EFA\u8FD9\u53F0\u673A\u5668\u7684\u81EA\u7B7E\u8BC1\u4E66\u5230 ~/.config/cotutor/certs/(\u5E73\u677F\u3001\u624B\u673A\u4E0A\u5F55\u97F3\u8981 HTTPS;\u6240\u6709 workspace \u5171\u7528)\n cotutor send <\u8001\u5E08> [<\u6D88\u606F>] [--from kid|system] [--runtime <\u540D>] [--new] [--photo a.jpg,b.png] [--activity <\u540D> [--param '\u540D=\u503C;\u540D=\u503C']] [--lane main,tts] [--quiet] \u7EC8\u7AEF\u91CC\u53D1\u4E00\u6761,\u73B0\u573A\u6309\u9053\u6253\u5370\u6BCF\u9053\u5DE5\u5E8F\u7684\u4E8B\u4EF6,\u8BF4\u5B8C\u6253\u5370\u7ED3\u679C(\u5B69\u5B50\u7684\u8BDD\u7167\u5B9E\u65F6\u5199,\u540C\u5B69\u5B50\u7AEF:\u4E0A\u4E0B\u6587\u5305\u5E26 live:\u3001\u4E0D\u8865\u63D0\u95EE\u5361,\u53EA\u662F\u6CA1\u6709\u5634;--from system \u4E0D\u7B97;--new \u5F00\u65B0\u8BDD\u9898;--photo \u5E26\u4F5C\u4E1A\u7167\u7247,\u62F7\u8FDB captures/,\u6709\u7167\u7247\u65F6\u6D88\u606F\u53EF\u4EE5\u7A7A;--activity \u50CF\u5B69\u5B50\u70B9\u4E86\u9996\u9875\u7684 activity \u6309\u94AE,\u65B0\u5F00\u8BDD\u9898,\u53C2\u6570\u540C\u6309\u94AE\u4E0B\u9762\u90A3\u51E0\u884C;--quiet \u53EA\u8981\u7ED3\u679C)\n cotutor pack <\u8001\u5E08> [<\u6D88\u606F>] [--from kid|system] [--at <ISO\u65F6\u95F4>] [--activity <\u540D> [--param '\u540D=\u503C;\u540D=\u503C']] [--json] \u5E72\u8DD1\u4E0A\u4E0B\u6587\u5305(--activity \u770B activity \u8BDD\u9898\u7B2C\u4E00\u6761\u8001\u5E08\u62FF\u5230\u4EC0\u4E48):\u4E0D\u8D77\u6A21\u578B,\u6253\u5370\u73B0\u5728\u4F1A\u53D1\u7ED9\u8001\u5E08\u7684\u90A3\u4EFD + \u6BCF\u6BB5\u6765\u81EA\u54EA\u4E2A\u6587\u4EF6\u3001\u90A3\u91CC\u4E00\u5171\u51E0\u6761\u3001\u6309\u653F\u7B56\u5E26\u4E86\u51E0\u6761(\u6539\u4E86\u6863\u6848 / \u65E5\u8BB0 / \u8BA1\u5212\u7ACB\u523B\u770B\u6548\u679C)\n cotutor replay <\u8001\u5E08> <job> [<\u65E5\u671F>] [--runtime <\u540D>] [--json] \u56DE\u653E\u4E00\u8F6E:\u540C\u4E00\u95EE\u6309\u73B0\u5728\u7684 vault \u4E0E\u63D0\u793A\u8BCD\u518D\u8DD1\u4E00\u904D(\u843D evals/,\u4E0D\u8FDB\u5B69\u5B50\u7684\u5BF9\u8BDD\u3001\u4E0D\u914D\u97F3),\u8DD1\u5B8C\u5E76\u6392\u6253\u5370\u4E0A\u4E0B\u6587\u5305 / \u8BB2\u7A3F / \u5361 / \u8BFB\u4E86\u4EC0\u4E48\u7684 diff\n cotutor compare <\u8001\u5E08> [<evalJob>] [<\u65E5\u671F>] [--json] \u518D\u770B\u4E00\u6B21\u56DE\u653E\u7684\u5BF9\u7167(\u4E0D\u7ED9 evalJob \u5C31\u5217\u8FD9\u5929\u56DE\u653E\u8FC7\u54EA\u4E9B)\n cotutor show <\u8001\u5E08> <job> [<\u65E5\u671F>] [--json] [--evals] [--workspace <dir>] \u770B\u4E00\u8F6E:\u95EE\u4E86\u4EC0\u4E48\u3001\u4E0A\u4E0B\u6587\u5305\u3001\u5F53\u65F6\u7684\u8001\u5E08\u6587\u4EF6\u4E0E\u6280\u80FD hash\u3001\u8BB2\u7A3F\u4E0E\u5361\u3001\u8BFB\u4E86\u4EC0\u4E48\u3001token \u4E0E\u7528\u65F6\u3001\u7ED9\u5BB6\u957F\u7684\u5C3E\u5DF4;--json \u662F\u5BB6\u957F\u7AEF\u300C\u770B\u539F\u6587\u300D\u540C\u4E00\u4EFD\u6570\u636E,\u7ED9\u7F16\u7A0B\u52A9\u624B\u5206\u6790\u7528\n cotutor trace <\u8001\u5E08> <job> [<\u65E5\u671F>] [--lane \u2026] [--workspace <dir>] \u56DE\u653E\u4E00\u8F6E\u7684\u4E8B\u4EF6(<\u65E5\u671F>.<job>.events.jsonl;\u6392\u67E5\u6628\u5929\u90A3\u8F6E\u7528)\n cotutor rate <\u8001\u5E08> <\u8BDD\u9898> <1-5> [--date <\u65E5\u671F>] \u7ED9\u4E00\u4E2A\u8BDD\u9898\u6253\u661F(\u4E0E\u5BB6\u957F\u7AEF\u540C\u4E00\u6761\u8DEF;\u2265 vault.keepScore \u7684\u8BDD\u9898\u8BB0\u8D26\u65F6\u6458\u8981\u624D\u8FDB\u65E5\u8BB0)\n cotutor bookkeep <\u8001\u5E08> [--date <\u65E5\u671F>] [--thread <\u8BDD\u9898>]... \u8BB0\u8D26:\u8FD9\u5929\u6BCF\u4E2A\u8FD8\u6CA1\u8BB0\u8FC7\u7684\u8BDD\u9898\u5404\u8D77\u4E00\u8F6E\u8BB0\u8D26\u4EFB\u52A1,\u8001\u5E08\u56DE\u300C## \u8BB0\u8D26\u300D,\u5E94\u7528\u5199\u8FDB vault \u7684\u65E5\u8BB0(\u8BDD\u9898\u540D\u3001\u5B69\u5B50\u95EE\u3001\u6458\u8981\u3001\u89C2\u5BDF)\n cotutor home check [--published] [--json] \u68C0\u67E5\u9996\u9875\u8349\u7A3F home/draft.md(\u6216\u5DF2\u53D1\u5E03\u7684\u90A3\u4EFD):\u5B69\u5B50\u4F1A\u770B\u5230\u54EA\u51E0\u5F20\u5361\u3001\u6BCF\u4F4D\u8001\u5E08\u7684\u6309\u94AE\u3001\u8981\u6539\u7684\u4E0E\u63D0\u9192(\u5E26\u884C\u53F7);\u4E0D\u82B1\u94B1\n cotutor home publish [--force] [--from <\u6587\u4EF6>] [--json] \u53D1\u5E03\u9996\u9875:\u68C0\u67E5 \u2192 \u7559\u5386\u53F2 home/history/<id>.md \u2192 \u5199 home/published.json(\u5B69\u5B50\u7AEF\u53EA\u8BFB\u5B83);\u6709\u8981\u6539\u7684\u4E0D\u53D1,--force \u4E22\u6389\u90A3\u51E0\u5F20\u7167\u53D1;--from \u53D1\u53E6\u4E00\u4EFD(\u5982\u5386\u53F2)\n cotutor home show [--json] \u73B0\u5728\u53D1\u5E03\u7684\u662F\u54EA\u4EFD\u3001\u51E0\u5929\u524D\u3001\u6BCF\u4E2A\u6309\u94AE\u88AB\u70B9\u4E86\u51E0\u6B21(\u54EA\u5929\u3001\u54EA\u4E2A\u8BDD\u9898)\n cotutor activity check [<\u8001\u5E08> [<\u540D>]] [--json] \u68C0\u67E5 activity \u6587\u4EF6(agents/<\u8001\u5E08>/activities/<\u540D>.md):\u5BB6\u957F\u586B\u7684\u3001\u5E94\u7528\u7B97\u7684\u3001\u5E94\u7528\u62E6\u7684\u3001\u88C1\u6BB5\u3001\u63D2\u69FD\u3001\u6B63\u6587\u7528\u7684\u5361\u3001\u7B2C\u4E00\u53E5\u4E0E\u6536(\u5E26\u884C\u53F7);\u4E0D\u82B1\u94B1\n cotutor activity eval <\u8001\u5E08> <\u540D> [--runs <n>] [--only <\u4F8B\u5B50>] [--kid-model <\u5343\u95EE\u6A21\u578B>] [--json] \u7167 <\u540D>.\u4F8B\u5B50.md \u8DD1:\u771F\u8001\u5E08 + \u6A21\u62DF\u5B69\u5B50(\u5343\u95EE,\u7F3A\u7701 qwen3.8-flash,\u6709 Token Plan \u8D70\u5B83)\u591A\u8F6E(\u7167\u5B9E\u65F6\u5199,\u540C\u5B69\u5B50\u7AEF),\u5224\u53EA\u770B\u677F\u4E66;\u4E00\u4E2A\u4F8B\u5B50\u8DD1 n \u6B21(\u7F3A\u7701 3)\u6B21\u6B21\u90FD\u8FC7\u624D\u7B97\u8FC7;\u843D evals/,\u753B\u5BB6\u662F\u5047\u7684;\u82B1\u94B1\n cotutor chalk <check|build|bake|snap|dub> <scene.ts|\u8BFE\u5305 id> [...] \u505A\u5C0F\u8BFE\u5802\u7684\u8BFE\u5305:check \u6821\u9A8C\u573A\u666F\u3001build \u51FA\u8BFE\u5305\u5230 bundles/\u3001bake \u70E4\u753B\u9762\u3001snap \u6BCF\u6B65\u622A\u56FE\u3001dub \u914D\u97F3(cotutor chalk \u770B\u5404\u81EA\u7684\u9009\u9879;bake / snap \u8981\u672C\u673A\u7684 Chrome)\n cotutor mock [--port <n>] [--scenario normal|nokey] [--delay <ms>] [--http] \u4E0D\u7ECF\u771F\u5B9E\u8001\u5E08\u4E0E\u767E\u70BC,\u7528\u56FA\u5B9A\u7684\u677F\u4E66 JSON \u8D77\u5B69\u5B50\u7AEF,\u6D4B\u524D\u7AEF\u4EA4\u4E92\u4E0E\u6E32\u67D3(\u4E0D\u9700\u8981 workspace;\u8001\u5E08\u90FD\u8D70\u5B9E\u65F6\u8001\u5E08\u9875,\u5047\u5634\u7167\u811A\u672C\u5FF5\u9759\u97F3\u7684\u58F0\u97F3;nokey = \u6CA1\u914D\u767E\u70BC,\u5B69\u5B50\u7AEF\u53EA\u51FA\u300C\u8001\u5E08\u4EEC\u8FD8\u6CA1\u5230\u300D)\n cotutor --version | --help\nworkspace\u89E3\u6790:--workspace > COTUTOR_WORKSPACE > cwd \u6216\u7956\u5148\u6709 cotutor.json > ~/.config/cotutor/config.json > ~/cotutor/ \u4E0B\u552F\u4E00\u7684\u5B69\u5B50\u76EE\u5F55\n\u8BD5\u7528:\u8981 workspace \u7684\u547D\u4EE4\u52A0 --sandbox,\u6362\u5230\u771F workspace \u65C1\u8FB9\u7684 .sandbox/<\u76EE\u5F55\u540D>/,\u8BD5\u7684\u5BF9\u8BDD\u4E0E vault \u7684\u5199\u5165\u4E0D\u8FDB\u5B69\u5B50\u7684\u8BB0\u5F55(\u7B2C\u4E00\u6B21\u81EA\u52A8\u5206:\u8BBE\u7F6E\u7167\u62F7,\u5BF9\u8BDD\u4E0E\u7167\u7247\u4E0D\u5E26,vault \u62F7\u4E00\u4EFD;\u6807\u9898\u524D\u52A0\u300C\u8BD5\u7528\u300D\u3001\u7AEF\u53E3 +1\u3001\u81EA\u52A8\u8BB0\u8D26\u5173\u6389);--sandbox --fresh \u91CD\u65B0\u5206\n";
1
+ /**
2
+ * 帮助与参数检查,都从命令表(commands.ts)来:顶层帮助只列命令和一句话,参数、例子在 cotutor <命令> --help。
3
+ * 纯函数、不碰 I/O:技能生成(app/skills/*-doc.ts)和测试都 import 它,不能把 CLI 带进去。
4
+ */
5
+ import { type CommandSpec } from './commands.ts';
6
+ /** 顶层帮助;all 时连开发与调试的命令也列 */
7
+ export declare function topHelp(opts?: {
8
+ all?: boolean;
9
+ }): string;
10
+ /** cotutor <命令> --help;place: false 不列 --workspace / --sandbox(技能参考里统一说一次) */
11
+ export declare function commandHelp(name: string, opts?: {
12
+ place?: boolean;
13
+ }): string;
14
+ /** 技能里的命令参考:这几条命令各自的帮助;--workspace、--sandbox 在开头说一次 */
15
+ export declare function commandsReference(names: readonly string[]): string;
16
+ /** 不认识的命令:一句话,带最像的那个 */
17
+ export declare function unknownCommand(cmd: string): string;
18
+ /**
19
+ * 照命令表检查参数:不认识的旗标、用错子命令的旗标、不认识的子命令、多余的参数。
20
+ * 通过返回 null,不通过返回一句话(调用方补「看 cotutor <命令> --help」)。命令本身认不认识由调用方先判。
21
+ */
22
+ export declare function checkArgs(c: CommandSpec, positionals: readonly string[], flags: Readonly<Record<string, string | true>>): string | null;
23
+ /** 一行 cotutor 命令(文档、测试里写的)照命令表对不对得上;对得上返回 null */
24
+ export declare function checkCommandLine(line: string): string | null;
25
+ /** 旧名:顶层帮助 */
26
+ export declare const USAGE: string;
@@ -1,38 +1,167 @@
1
- /** 命令用法(cotutor --help;技能 cotutor-analyze 的 references/命令与文件.md 从这里取分析用的那几行,所以放单独文件,别的模块 import 不会跑到 CLI) */
2
- export const USAGE = `用法:
3
- cotutor init <slug> [--dir <path>] [--name <孩子名>] [--grade <1-6>] [--port <n>] 建 ~/cotutor/<slug>/ 骨架(幂等补缺;默认二年级教学包;端口在这里选一次写进 cotutor.json:443 优先,不行就 8443、8445…,以后不变)
4
- cotutor grades [--json] 列出本包可安装的年级教学包
5
- cotutor doctor [--workspace <dir>] [--json] [--live] [--fix] 逐项体检;--live 真连一次百炼实时模型、真合成一句配音(花一分钱)把 API 层的坑摆出来(真跑老师用 cotutor send);--json 里不过的项带 cmd(能直接跑)或 human(要人来);--fix 把带 cmd 的一条条修掉,要人来的留着
6
- cotutor report [--workspace <dir>] 诊断信息:版本、系统、服务、体检没过的、最近报错的原因;不含孩子的对话、照片、录音、档案和 key,名字和 IP 已抹掉,先看再发
7
- cotutor backup [list | restore <名字> [--yes]] [--workspace <dir>] 备份到 workspace 旁边的 .backups/:不含录音;服务开着时每天一份(留 14 份),cotutor upgrade 前一份(留 5 份);没变的文件硬链接,不占第二份地方;restore 先把现在的也备一份再拷回去,不加 --yes 只说会做什么
8
- cotutor update [--anyway] 升级一条命令管到底:问 npm 有没有新版本,换程序,每个孩子更新出厂文件、重起服务和图标;孩子在通话时不动(--anyway 才动),npm 没成服务不动
9
- cotutor upgrade [--workspace <dir>] [--force <老师>]... 老师文件、activity 与 skill 换新版:没改过的直接换,改过的只报 diff(--force <老师> 或 --force <老师>/<名> 才覆盖,原文留 .bak)
10
- cotutor upgrade --config [--dry-run] [--workspace <dir>] cotutor.json 补缺:新出厂老师 / 运行时 / 命令模板旗标(只加缺的,你改过的值不动)
11
- cotutor add <老师名> --display <显示名> [--subject <学科>] [--avatar <emoji|图片相对路径>] [--hidden] 加一位自家的老师:出模板文件、进 cotutor.json、建目录(名字以 -tutor 结尾的是老师,figshot 给它做头像)
12
- cotutor add-theme <主题名> [--from <主题>] [--workspace <dir>] 加一个自家的主题:拷一份(缺省出厂的 default)到 themes/<主题名>/,改 cotutor.json 的 kid.theme 换过去
13
- cotutor serve [--workspace <dir>] [--port <n>] [--http] [--trace] [--open] [--log-dir <目录>] 在终端里起服务(一 workspace 一进程;~/.config/cotutor/certs/ 有证书就走 HTTPS;--trace 每一轮的事件按道打印;只打印家长端首页的地址,孩子的平板在那里扫码;--open 顺手打开,旧名 --open-qr 照样认;--log-dir 输出按天落文件,后台服务用;关了终端就停,一直开着用 cotutor service)
14
- cotutor open [--workspace <dir>] [--sandbox] [--print] [--json] 打开家长端(读 cotutor.json,不用记端口);--sandbox 开试用的那个(端口永远是真的 +1);--print 只打印地址;没在跑就说怎么起,以 1 退
15
- cotutor service install | uninstall | start | stop | restart [--workspace <dir>] [--no-tray] [--no-open] 把 serve 装成这台电脑的后台服务(macOS launchd、Linux systemd 用户服务、Windows 用户的登录项,都不要管理员):登录后自己起来、崩了 10 秒后再起、日志按天落文件留 14 天;一个 workspace 一个;第一次 install 打开一次家长端(--no-open 不开),以后的启动都不开;换了 node 版本或包挪了地方再 install 一次
16
- cotutor tray on | off | status [--json] 菜单栏 / 任务栏图标(macOS、Windows;service install 时一起装,--no-tray 不带):正常时只有 logo,有事出橙点,没在跑时变淡加红点并通知一次;点它看状态、打开家长端、重启、看日志、退出;off 关掉并记住
17
- cotutor service status [--json] | logs [--lines <n>] [--follow] 后台服务在不在跑、端口答不答应、跑的哪个版本、vault 读不读得到、日志在哪;--json 还答平板地址、证书、百炼配没配、图标;logs 看最近的日志
18
- cotutor pin [off | status] [--pin <数字>] [--json] 家长端 /parent 与工作台 /dev 的锁:机器级 PIN(所有 workspace 共用),iPad 上输一次、这台电脑免密;没设就全开(家长在旁边可以不设);off 撤掉
19
- cotutor cert [--host <名或IP>]... 用 mkcert 建这台机器的自签证书到 ~/.config/cotutor/certs/(平板、手机上录音要 HTTPS;所有 workspace 共用)
20
- cotutor send <老师> [<消息>] [--from kid|system] [--runtime <名>] [--new] [--photo a.jpg,b.png] [--activity <名> [--param '名=值;名=值']] [--lane main,tts] [--quiet] 终端里发一条,现场按道打印每道工序的事件,说完打印结果(孩子的话照实时写,同孩子端:上下文包带 live:、不补提问卡,只是没有嘴;--from system 不算;--new 开新话题;--photo 带作业照片,拷进 captures/,有照片时消息可以空;--activity 像孩子点了首页的 activity 按钮,新开话题,参数同按钮下面那几行;--quiet 只要结果)
21
- cotutor pack <老师> [<消息>] [--from kid|system] [--at <ISO时间>] [--activity <名> [--param '名=值;名=值']] [--json] 干跑上下文包(--activity 看 activity 话题第一条老师拿到什么):不起模型,打印现在会发给老师的那份 + 每段来自哪个文件、那里一共几条、按政策带了几条(改了档案 / 日记 / 计划立刻看效果)
22
- cotutor replay <老师> <job> [<日期>] [--runtime <名>] [--json] 回放一轮:同一问按现在的 vault 与提示词再跑一遍(落 evals/,不进孩子的对话、不配音),跑完并排打印上下文包 / 讲稿 / 卡 / 读了什么的 diff
23
- cotutor compare <老师> [<evalJob>] [<日期>] [--json] 再看一次回放的对照(不给 evalJob 就列这天回放过哪些)
24
- cotutor show <老师> <job> [<日期>] [--json] [--evals] [--workspace <dir>] 看一轮:问了什么、上下文包、当时的老师文件与技能 hash、讲稿与卡、读了什么、token 与用时、给家长的尾巴;--json 是家长端「看原文」同一份数据,给编程助手分析用
25
- cotutor trace <老师> <job> [<日期>] [--lane …] [--workspace <dir>] 回放一轮的事件(<日期>.<job>.events.jsonl;排查昨天那轮用)
26
- cotutor rate <老师> <话题> <1-5> [--date <日期>] 给一个话题打星(与家长端同一条路;≥ vault.keepScore 的话题记账时摘要才进日记)
27
- cotutor bookkeep <老师> [--date <日期>] [--thread <话题>]... 记账:这天每个还没记过的话题各起一轮记账任务,老师回「## 记账」,应用写进 vault 的日记(话题名、孩子问、摘要、观察)
28
- cotutor home check [--published] [--json] 检查首页草稿 home/draft.md(或已发布的那份):孩子会看到哪几张卡、每位老师的按钮、要改的与提醒(带行号);不花钱
29
- cotutor home publish [--force] [--from <文件>] [--json] 发布首页:检查 → 留历史 home/history/<id>.md → 写 home/published.json(孩子端只读它);有要改的不发,--force 丢掉那几张照发;--from 发另一份(如历史)
30
- cotutor home show [--json] 现在发布的是哪份、几天前、每个按钮被点了几次(哪天、哪个话题)
31
- cotutor activity check [<老师> [<名>]] [--json] 检查 activity 文件(agents/<老师>/activities/<名>.md):家长填的、应用算的、应用拦的、裁段、插槽、正文用的卡、第一句与收(带行号);不花钱
32
- cotutor activity eval <老师> <名> [--runs <n>] [--only <例子>] [--kid-model <千问模型>] [--json] 照 <名>.例子.md 跑:真老师 + 模拟孩子(千问,缺省 qwen3.8-flash,有 Token Plan 走它)多轮(照实时写,同孩子端),判只看板书;一个例子跑 n 次(缺省 3)次次都过才算过;落 evals/,画家是假的;花钱
33
- cotutor chalk <check|build|bake|snap|dub> <scene.ts|课包 id> [...] 做小课堂的课包:check 校验场景、build 出课包到 bundles/、bake 烤画面、snap 每步截图、dub 配音(cotutor chalk 看各自的选项;bake / snap 要本机的 Chrome)
34
- cotutor mock [--port <n>] [--scenario normal|nokey] [--delay <ms>] [--http] 不经真实老师与百炼,用固定的板书 JSON 起孩子端,测前端交互与渲染(不需要 workspace;老师都走实时老师页,假嘴照脚本念静音的声音;nokey = 没配百炼,孩子端只出「老师们还没到」)
35
- cotutor --version | --help
36
- workspace解析:--workspace > COTUTOR_WORKSPACE > cwd 或祖先有 cotutor.json > ~/.config/cotutor/config.json > ~/cotutor/ 下唯一的孩子目录
37
- 试用:要 workspace 的命令加 --sandbox,换到真 workspace 旁边的 .sandbox/<目录名>/,试的对话与 vault 的写入不进孩子的记录(第一次自动分:设置照拷,对话与照片不带,vault 拷一份;标题前加「试用」、端口 +1、自动记账关掉);--sandbox --fresh 重新分
38
- `;
1
+ /**
2
+ * 帮助与参数检查,都从命令表(commands.ts)来:顶层帮助只列命令和一句话,参数、例子在 cotutor <命令> --help。
3
+ * 纯函数、不碰 I/O:技能生成(app/skills/*-doc.ts)和测试都 import 它,不能把 CLI 带进去。
4
+ */
5
+ import { COMMANDS, GROUP_TITLE, commandSpec } from "./commands.js";
6
+ const WORKSPACE_DESC = '哪个孩子的 cotutor 文件夹(缺省:COTUTOR_WORKSPACE → 当前目录往上找 → ~/.config/cotutor/config.json → ~/cotutor/ 下唯一的那个)';
7
+ const SANDBOX_DESC = '在试用副本里跑(旁边的 .sandbox/,写的东西不进孩子的记录;--fresh 重新分一份)';
8
+ /** 顶层帮助;all 时连开发与调试的命令也列 */
9
+ export function topHelp(opts = {}) {
10
+ const groups = opts.all ? ['setup', 'daily', 'custom', 'debug'] : ['setup', 'daily', 'custom'];
11
+ const width = Math.max(...COMMANDS.map((c) => c.name.length)) + 2;
12
+ const out = ['cotutor — 家里的 AI 老师团', '', '用法:cotutor <命令> [选项]'];
13
+ for (const g of groups) {
14
+ out.push('', GROUP_TITLE[g]);
15
+ for (const c of COMMANDS.filter((x) => x.group === g && !x.hidden))
16
+ out.push(` ${pad(c.name, width)}${c.summary}`);
17
+ }
18
+ out.push('', '常用选项:', ' --workspace <目录> 选哪个孩子(缺省自动找)', ' --json 机器可读的输出', ' --sandbox 在试用副本里跑,不进孩子的记录', ' --version 版本号', '各命令认哪些,看它的帮助;不认的选项、多余的参数都报错,不会悄悄忽略。', '退出码:0 成功 · 1 没通过(体检、检查有要改的) · 2 用法错 · 4 内部错误', '', '参数和例子:cotutor <命令> --help');
19
+ if (!opts.all)
20
+ out.push(`开发与调试的命令(${COMMANDS.filter((c) => c.group === 'debug' && !c.hidden).map((c) => c.name).join('、')}):cotutor --help --all`);
21
+ return `${out.join('\n')}\n`;
22
+ }
23
+ /** 一条命令的全部选项(连同它认的全局选项),帮助和检查共用 */
24
+ function flagsOf(c, opts = {}) {
25
+ const out = [...(c.flags ?? [])];
26
+ const place = opts.place !== false;
27
+ if (c.workspace)
28
+ out.push({ name: 'workspace', value: '<目录>', desc: WORKSPACE_DESC, ...(place ? {} : { hidden: true }) });
29
+ if (c.sandbox)
30
+ out.push({ name: 'sandbox', desc: SANDBOX_DESC, ...(place ? {} : { hidden: true }) }, { name: 'fresh', desc: '', hidden: true });
31
+ if (c.json)
32
+ out.push({ name: 'json', desc: '机器可读的输出', ...(Array.isArray(c.json) ? { subs: c.json } : {}) });
33
+ return out;
34
+ }
35
+ /** 终端里占几格:中文、全角两格 */
36
+ const cells = (s) => [...s].reduce((n, ch) => n + (/[ᄀ-ᅟ⺀-꓏가-힣豈-﫿︰-﹏＀-⦆¢-₩]/.test(ch) ? 2 : 1), 0);
37
+ const pad = (s, width) => s + ' '.repeat(Math.max(0, width - cells(s)));
38
+ /** 帮助里旗标说明前面的 [install];'' 是不带子命令的那种 */
39
+ const subsTag = (subs) => `[${subs.map((s) => (s === '' ? '不带子命令' : s)).join('、')}] `;
40
+ /** 报错里说「只给谁用」 */
41
+ const subsWho = (c, subs) => subs.map((s) => (s === '' ? `cotutor ${c.name}(不带子命令)` : `${c.name} ${s}`)).join('、');
42
+ /** cotutor <命令> --help;place: false 不列 --workspace / --sandbox(技能参考里统一说一次) */
43
+ export function commandHelp(name, opts = {}) {
44
+ const c = commandSpec(name);
45
+ if (!c)
46
+ return topHelp();
47
+ const out = [`cotutor ${c.name} — ${c.summary}`, '', '用法:', ...c.usage.map((u) => ` ${u}`)];
48
+ const shown = flagsOf(c, opts).filter((f) => !f.hidden);
49
+ if (shown.length) {
50
+ const label = (f) => `--${f.name}${f.value ? ` ${f.value}` : ''}`;
51
+ const width = Math.min(26, Math.max(...shown.map((f) => cells(label(f)))) + 2);
52
+ out.push('', '选项:');
53
+ for (const f of shown) {
54
+ const l = label(f);
55
+ out.push(` ${cells(l) >= width ? `${l}\n ${' '.repeat(width)}` : pad(l, width)}${f.subs ? subsTag(f.subs) : ''}${f.desc}`);
56
+ }
57
+ }
58
+ if (c.notes?.length)
59
+ out.push('', '说明:', ...c.notes.map((n) => ` - ${n}`));
60
+ if (c.effects?.length)
61
+ out.push('', '注意:', ...c.effects.map((n) => ` - ${n}`));
62
+ out.push('', '例子:', ...c.examples.map((e) => ` ${e}`));
63
+ return `${out.join('\n')}\n`;
64
+ }
65
+ /** 技能里的命令参考:这几条命令各自的帮助;--workspace、--sandbox 在开头说一次 */
66
+ export function commandsReference(names) {
67
+ return [
68
+ '这些命令都认 --workspace <目录>(在 workspace 根跑可以不写)和 --sandbox(在试用副本里跑,不进孩子的记录)。',
69
+ ...names.map((n) => commandHelp(n, { place: false }).trimEnd()),
70
+ ].join('\n\n');
71
+ }
72
+ /** 编辑距离,给打错的字找最像的 */
73
+ function distance(a, b) {
74
+ const d = Array.from({ length: a.length + 1 }, (_, i) => [i, ...Array(b.length).fill(0)]);
75
+ for (let j = 1; j <= b.length; j++)
76
+ d[0][j] = j;
77
+ for (let i = 1; i <= a.length; i++) {
78
+ for (let j = 1; j <= b.length; j++) {
79
+ d[i][j] = Math.min(d[i - 1][j] + 1, d[i][j - 1] + 1, d[i - 1][j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1));
80
+ // 两个字母打反了(josn → json)算一步
81
+ if (i > 1 && j > 1 && a[i - 1] === b[j - 2] && a[i - 2] === b[j - 1])
82
+ d[i][j] = Math.min(d[i][j], d[i - 2][j - 2] + 1);
83
+ }
84
+ }
85
+ return d[a.length][b.length];
86
+ }
87
+ function closest(word, candidates) {
88
+ let best;
89
+ let score = Infinity;
90
+ for (const c of candidates) {
91
+ const s = c.startsWith(word) || word.startsWith(c) ? 1 : distance(word, c);
92
+ if (s < score) {
93
+ score = s;
94
+ best = c;
95
+ }
96
+ }
97
+ return score <= Math.max(1, Math.floor(word.length / 3)) ? best : undefined;
98
+ }
99
+ /** 不认识的命令:一句话,带最像的那个 */
100
+ export function unknownCommand(cmd) {
101
+ const near = closest(cmd, COMMANDS.filter((c) => !c.hidden).map((c) => c.name));
102
+ return `没有 '${cmd}' 这条命令${near ? `,是不是 cotutor ${near}?` : ''}`;
103
+ }
104
+ /**
105
+ * 照命令表检查参数:不认识的旗标、用错子命令的旗标、不认识的子命令、多余的参数。
106
+ * 通过返回 null,不通过返回一句话(调用方补「看 cotutor <命令> --help」)。命令本身认不认识由调用方先判。
107
+ */
108
+ export function checkArgs(c, positionals, flags) {
109
+ const sub = positionals[0];
110
+ if (c.subs && (sub !== undefined || c.subRequired) && !c.subs.includes(sub ?? '') && !c.hiddenSubs?.includes(sub ?? '')) {
111
+ const near = sub === undefined ? undefined : closest(sub, c.subs);
112
+ return `${c.name} 后面跟 ${c.subs.join(' / ')}${c.subRequired ? ' 之一' : ''}${sub === undefined ? '' : `,不认识 '${sub}'`}${near ? `,是不是 ${near}?` : ''}`;
113
+ }
114
+ if (positionals.length > c.maxArgs) {
115
+ const extra = positionals[c.maxArgs];
116
+ return c.maxArgs === 0 ? `${c.name} 不带参数,多了 '${extra}'` : `多余的参数 '${extra}':${c.name} 最多 ${c.maxArgs} 个${extra.includes(' ') ? '(带空格的值要加引号)' : ''}`;
117
+ }
118
+ const known = flagsOf(c);
119
+ const actualSub = c.subs?.includes(sub ?? '') || c.hiddenSubs?.includes(sub ?? '') ? (sub ?? '') : '';
120
+ for (const key of Object.keys(flags)) {
121
+ // --sandbox / --fresh 由 main 自己说清楚(这条命令为什么不用试用副本),这里不抢
122
+ if (key === 'help' || key === 'all' || key === 'version' || key === 'sandbox' || key === 'fresh')
123
+ continue;
124
+ const f = known.find((x) => x.name === key);
125
+ if (!f) {
126
+ const near = closest(key, known.filter((x) => !x.hidden).map((x) => x.name));
127
+ return `${c.name} 不认识 --${key}${near ? `,是不是 --${near}?` : ''}`;
128
+ }
129
+ if (f.subs && !f.subs.includes(actualSub))
130
+ return `--${key} 只给 ${subsWho(c, f.subs)} 用`;
131
+ if (f.value && flags[key] === true && key !== 'force')
132
+ return `--${key} 要一个值:--${key} ${f.value}`;
133
+ }
134
+ return null;
135
+ }
136
+ /** 一行 cotutor 命令(文档、测试里写的)照命令表对不对得上;对得上返回 null */
137
+ export function checkCommandLine(line) {
138
+ const words = line.trim().split(/\s+/);
139
+ if (words[0] !== 'cotutor' && words[0] !== 'cotutor.cmd')
140
+ return `不是 cotutor 命令:${line}`;
141
+ const [cmd, ...rest] = words.slice(1);
142
+ if (cmd === '--version' || cmd === '--help')
143
+ return null;
144
+ const c = commandSpec(cmd);
145
+ if (!c)
146
+ return unknownCommand(cmd);
147
+ if (rest.includes('--help'))
148
+ return null;
149
+ const positionals = [];
150
+ const flags = {};
151
+ for (let i = 0; i < rest.length; i++) {
152
+ const w = rest[i];
153
+ if (!w.startsWith('--')) {
154
+ positionals.push(w);
155
+ continue;
156
+ }
157
+ const key = w.slice(2).split('=')[0];
158
+ const spec = flagsOf(c).find((f) => f.name === key);
159
+ if (spec?.value && rest[i + 1] !== undefined && !rest[i + 1].startsWith('--'))
160
+ flags[key] = rest[++i];
161
+ else
162
+ flags[key] = true;
163
+ }
164
+ return checkArgs(c, positionals, flags);
165
+ }
166
+ /** 旧名:顶层帮助 */
167
+ export const USAGE = topHelp();
@@ -625,7 +625,7 @@ export async function route(method, path, ctx, body, who = 'local', client = {})
625
625
  }
626
626
  return { status: 405, json: { error: 'method_not_allowed' } };
627
627
  }
628
- // 政策文件补缺(与 cotutor upgrade --config 同一条路):只加缺的出厂件,家长写过的值不动
628
+ // 政策文件补缺(与 cotutor refresh --config 同一条路):只加缺的出厂件,家长写过的值不动
629
629
  if (p === '/api/config/migrate' && method === 'POST') {
630
630
  const r = await upgradeConfig(ws.root);
631
631
  await ctx.reload();
@@ -1,18 +1,14 @@
1
1
  /**
2
- * cotutor-analyze 技能的生成部分(2026-09-15):references/命令与文件.md 从 CLI 的用法文本(app/cli/usage.ts)与对话文件的命名规则(runtime/journal/conversation.ts
2
+ * cotutor-analyze 技能的生成部分(2026-09-15):references/命令与文件.md 从命令表(app/cli/commands.ts,经 usage.ts 的 commandsReference)与对话文件的命名规则(runtime/journal/conversation.ts
3
3
  * conversationFiles)现生成,scripts/gen-skills.ts 写进包根 skills/cotutor-analyze/,tests/skills.test.ts 断言入库的和生成的一致——
4
4
  * 技能里的命令与文件名永远和代码同源。SKILL.md 本体(怎么查、禁区)是手写的。
5
5
  */
6
- import { USAGE } from "../cli/usage.js";
6
+ import { commandsReference } from "../cli/usage.js";
7
7
  import { conversationFiles } from "../../schema/index.js";
8
8
  export const ANALYZE_SKILL = 'cotutor-analyze';
9
9
  /** 分析用的那几条命令(按用法文本里的第二个词挑) */
10
10
  const ANALYZE_COMMANDS = ['show', 'pack', 'replay', 'compare', 'trace', 'doctor', 'send', 'rate', 'bookkeep'];
11
11
  export function analyzeReferenceDoc() {
12
- const cmds = USAGE.split('\n').filter((l) => {
13
- const m = /^\s+cotutor (\S+)/.exec(l);
14
- return m !== null && ANALYZE_COMMANDS.includes(m[1]);
15
- });
16
12
  const f = conversationFiles('conversations', '<老师>', '<日期>');
17
13
  const rows = [
18
14
  [f.index, '一天的索引:每条消息物化了 text / kidText / section(卡与讲稿)/ tools(读了什么)/ timing / tokens(这轮老师用的 token:输入 / 输出)/ warnings / bookkeeping;顶层 sessions / ratings / booked(老数据里还有 costUsd,是 claude 时代的美元,不用管)'],
@@ -25,10 +21,10 @@ export function analyzeReferenceDoc() {
25
21
  ];
26
22
  return `# 命令与文件(机器生成,别改)
27
23
 
28
- ## 分析用的命令(在 workspace 根跑;完整用法 cotutor --help)
24
+ ## 分析用的命令(在 workspace 根跑;每条就是 cotutor <命令> --help 的内容)
29
25
 
30
26
  \`\`\`
31
- ${cmds.join('\n')}
27
+ ${commandsReference(ANALYZE_COMMANDS)}
32
28
  \`\`\`
33
29
 
34
30
  ## 一轮的文件(conversations/ 下;回放的在 evals/ 下,同一套名字)
@@ -3,7 +3,7 @@
3
3
  * tests/skills.test.ts 断言入库的和生成的一致。首页语法.md 从首页能用的卡的 card.md 拼(首页专属的全文,和板书共用的只给一句 + 指向 cotutor-board),
4
4
  * <kind>.md 是首页专属卡的协议全文,命令.md 从 CLI 用法文本取。SKILL.md 本体(流程、铁律)是手写的。
5
5
  */
6
- import { USAGE } from "../cli/usage.js";
6
+ import { commandsReference } from "../cli/usage.js";
7
7
  import { BUTTON_LABEL_MAX, TUTOR_BUTTONS_MAX, kindsFor } from "../../board/cards/index.js";
8
8
  import { PACKAGE_CARDS_DIR, readCardDoc } from "../../board/cards/docs.js";
9
9
  export const HOME_SKILL = 'cotutor-home';
@@ -81,16 +81,12 @@ for: 2026-09-18
81
81
  `;
82
82
  }
83
83
  export function homeCommandsDoc() {
84
- const cmds = USAGE.split('\n').filter((l) => {
85
- const m = /^\s+cotutor (\S+)/.exec(l);
86
- return m !== null && HOME_COMMANDS.includes(m[1]);
87
- });
88
84
  return `# 命令(机器生成,别改)
89
85
 
90
- 在 workspace 根跑;完整用法 cotutor --help。
86
+ 在 workspace 根跑;每条就是 cotutor <命令> --help 的内容。
91
87
 
92
88
  \`\`\`
93
- ${cmds.join('\n')}
89
+ ${commandsReference(HOME_COMMANDS)}
94
90
  \`\`\`
95
91
 
96
92
  ## 文件