cotutor 0.1.0 → 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.
Files changed (109) hide show
  1. package/README.md +35 -24
  2. package/agents/chinese-tutor.md +67 -0
  3. package/agents/{homework-aide.md → homework-tutor.md} +4 -4
  4. package/agents/math-tutor.md +68 -0
  5. package/agents/reading-tutor.md +65 -0
  6. package/agents/scene-maker.md +41 -0
  7. package/dist/cards/canvas.d.ts +22 -0
  8. package/dist/cards/canvas.js +65 -0
  9. package/dist/cards/choice.d.ts +20 -0
  10. package/dist/cards/choice.js +76 -0
  11. package/dist/cards/code.d.ts +9 -0
  12. package/dist/cards/code.js +13 -0
  13. package/dist/cards/fill.d.ts +14 -0
  14. package/dist/cards/fill.js +64 -0
  15. package/dist/cards/image.d.ts +13 -0
  16. package/dist/cards/image.js +35 -0
  17. package/dist/cards/index.d.ts +48 -0
  18. package/dist/cards/index.js +115 -0
  19. package/dist/cards/kind.d.ts +27 -0
  20. package/dist/cards/kind.js +7 -0
  21. package/dist/cards/read.d.ts +8 -0
  22. package/dist/cards/read.js +27 -0
  23. package/dist/cards/scene.d.ts +24 -0
  24. package/dist/cards/scene.js +67 -0
  25. package/dist/cards/text.d.ts +18 -0
  26. package/dist/cards/text.js +41 -0
  27. package/dist/cli/cert.d.ts +7 -0
  28. package/dist/cli/cert.js +36 -0
  29. package/dist/cli/doctor.d.ts +5 -2
  30. package/dist/cli/doctor.js +183 -18
  31. package/dist/cli/init.d.ts +1 -1
  32. package/dist/cli/init.js +41 -39
  33. package/dist/cli/main.js +128 -8
  34. package/dist/cli/serve.d.ts +15 -4
  35. package/dist/cli/serve.js +40 -6
  36. package/dist/cli/skeleton.d.ts +23 -5
  37. package/dist/cli/skeleton.js +131 -29
  38. package/dist/cli/skills.d.ts +40 -0
  39. package/dist/cli/skills.js +170 -0
  40. package/dist/cli/tutors.d.ts +84 -0
  41. package/dist/cli/tutors.js +274 -0
  42. package/dist/cli/workspace.d.ts +9 -1
  43. package/dist/cli/workspace.js +25 -4
  44. package/dist/lib/agent-file.d.ts +1 -1
  45. package/dist/lib/board.d.ts +15 -0
  46. package/dist/lib/board.js +107 -0
  47. package/dist/lib/context-pack.js +10 -1
  48. package/dist/lib/conversation.d.ts +43 -6
  49. package/dist/lib/conversation.js +53 -9
  50. package/dist/lib/index.d.ts +4 -0
  51. package/dist/lib/index.js +4 -0
  52. package/dist/lib/kid-board.d.ts +148 -0
  53. package/dist/lib/kid-board.js +294 -0
  54. package/dist/lib/kid-view.d.ts +44 -7
  55. package/dist/lib/kid-view.js +63 -8
  56. package/dist/lib/ledger.d.ts +8 -0
  57. package/dist/lib/ledger.js +8 -0
  58. package/dist/lib/plan.d.ts +2 -0
  59. package/dist/lib/plan.js +9 -0
  60. package/dist/lib/run-plan.d.ts +30 -0
  61. package/dist/lib/run-plan.js +32 -0
  62. package/dist/lib/stream.d.ts +15 -0
  63. package/dist/lib/stream.js +69 -0
  64. package/dist/lib/timetable.d.ts +22 -0
  65. package/dist/lib/timetable.js +106 -0
  66. package/dist/schema/board.d.ts +59 -0
  67. package/dist/schema/board.js +30 -0
  68. package/dist/schema/config.d.ts +76 -18
  69. package/dist/schema/config.js +67 -37
  70. package/dist/schema/context-pack.d.ts +7 -0
  71. package/dist/schema/context-pack.js +6 -0
  72. package/dist/schema/conversation.d.ts +114 -4
  73. package/dist/schema/conversation.js +29 -5
  74. package/dist/schema/index.d.ts +6 -2
  75. package/dist/schema/index.js +6 -2
  76. package/dist/schema/json-schema.d.ts +2 -0
  77. package/dist/schema/json-schema.js +23 -0
  78. package/dist/schema/ledger.d.ts +1 -1
  79. package/dist/schema/timetable.d.ts +26 -0
  80. package/dist/schema/timetable.js +18 -0
  81. package/dist/server/app.d.ts +67 -6
  82. package/dist/server/app.js +399 -38
  83. package/dist/server/kid-page.d.ts +1 -0
  84. package/dist/server/kid-page.js +768 -0
  85. package/dist/server/mock.d.ts +58 -0
  86. package/dist/server/mock.js +403 -0
  87. package/dist/server/parent-page.d.ts +8 -0
  88. package/dist/server/parent-page.js +393 -0
  89. package/dist/server/runner.d.ts +64 -0
  90. package/dist/server/runner.js +277 -0
  91. package/dist/server/scene-props.d.ts +16 -0
  92. package/dist/server/scene-props.js +39 -0
  93. package/dist/server/stage.d.ts +15 -0
  94. package/dist/server/stage.js +69 -0
  95. package/dist/server/store.d.ts +43 -0
  96. package/dist/server/store.js +210 -0
  97. package/dist/server/tts.d.ts +51 -0
  98. package/dist/server/tts.js +99 -0
  99. package/dist/stage/Assistant-Bold-ZDZZ6JHA.woff2 +0 -0
  100. package/dist/stage/Assistant-Medium-DZ25RZU3.woff2 +0 -0
  101. package/dist/stage/Assistant-Regular-PLF2XOGW.woff2 +0 -0
  102. package/dist/stage/Assistant-SemiBold-CZ5MX6FK.woff2 +0 -0
  103. package/dist/stage/index.html +10 -0
  104. package/dist/stage/stage.css +1 -0
  105. package/dist/stage/stage.js +4383 -0
  106. package/package.json +13 -4
  107. package/agents/chinese-teacher.md +0 -19
  108. package/agents/math-teacher.md +0 -19
  109. package/agents/reading-teacher.md +0 -19
package/dist/cli/serve.js CHANGED
@@ -1,22 +1,56 @@
1
1
  /**
2
2
  * cotutor serve:一 workspace 一进程(2026-09-08 拍板),端口读 cotutor.json(--port 覆盖)。
3
3
  * 启动打印解析结果与体检警告(配置坏了直接不起,骨架缺失只警告)。
4
+ * HTTPS(iPad 上录音要):server.https 指了证书就用它(单个 workspace 的例外);没指就看机器级 ~/.config/cotutor/certs/(cotutor cert 用 mkcert 建);都没有走 HTTP。
4
5
  */
5
- import { createServer } from 'node:http';
6
- import { hostname } from 'node:os';
6
+ import { readFileSync } from 'node:fs';
7
+ import { createServer as createHttp } from 'node:http';
8
+ import { createServer as createHttps } from 'node:https';
9
+ import { hostname, networkInterfaces } from 'node:os';
10
+ import { join } from 'node:path';
7
11
  import { doctorWorkspace } from "./doctor.js";
8
- import { createHandler } from "../server/app.js";
9
- import { loadWorkspace } from "./workspace.js";
12
+ import { createContext, createHandler } from "../server/app.js";
13
+ import { USER_CERT_DIR, expandPath, loadWorkspace } from "./workspace.js";
14
+ /** 证书路径:cotutor.json 的 server.https 优先(相对 workspace 根),否则机器级 ~/.config/cotutor/certs/;都没有 → null */
15
+ export function httpsFiles(ws) {
16
+ const h = ws.config.server.https;
17
+ if (h)
18
+ return { cert: expandPath(h.cert, ws.root), key: expandPath(h.key, ws.root) };
19
+ const cert = join(USER_CERT_DIR, 'cert.pem');
20
+ const key = join(USER_CERT_DIR, 'key.pem');
21
+ try {
22
+ readFileSync(cert);
23
+ readFileSync(key);
24
+ return { cert, key };
25
+ }
26
+ catch {
27
+ return null;
28
+ }
29
+ }
30
+ /** 局域网 IPv4(iPad 用它访问;.local 名不一定解析得到) */
31
+ export function lanAddresses() {
32
+ const out = [];
33
+ for (const list of Object.values(networkInterfaces()))
34
+ for (const i of list ?? [])
35
+ if (i.family === 'IPv4' && !i.internal)
36
+ out.push(i.address);
37
+ return out;
38
+ }
10
39
  export async function serveWorkspace(opts = {}) {
11
40
  const ws = loadWorkspace(opts.workspace, opts);
12
41
  const report = await doctorWorkspace(ws.root, { ...opts, probeEnv: false });
13
42
  const warnings = report.checks.filter((c) => !c.ok).map((c) => `${c.name}:${c.detail}${c.fix ? `(${c.fix})` : ''}`);
14
43
  const port = opts.port ?? ws.config.server.port;
15
- const server = createServer(createHandler(ws));
44
+ const ctx = createContext(ws);
45
+ const handler = createHandler(ctx);
46
+ const tls = opts.http ? null : httpsFiles(ws);
47
+ const server = tls ? createHttps({ cert: readFileSync(tls.cert), key: readFileSync(tls.key) }, handler) : createHttp(handler);
16
48
  await new Promise((resolveListen, reject) => {
17
49
  server.once('error', reject);
18
50
  server.listen(port, opts.host ?? '0.0.0.0', () => resolveListen());
19
51
  });
20
52
  const actual = server.address().port;
21
- return { server, ws, port: actual, url: `http://${hostname()}:${actual}/`, warnings };
53
+ const scheme = tls ? 'https' : 'http';
54
+ const urls = [hostname(), ...lanAddresses()].map((h) => `${scheme}://${h}:${actual}/`);
55
+ return { server, ctx, ws, port: actual, https: Boolean(tls), urls, url: urls[0], warnings };
22
56
  }
@@ -1,21 +1,39 @@
1
- export declare const DIRS: readonly ["agents", "ledger", "conversations", ".claude/agents", ".qwen/agents"];
1
+ export declare const DIRS: readonly ["agents", "ledger", "conversations", ".claude/agents", ".qwen/agents", "scenes", "bundles", "snaps"];
2
2
  export declare const LEDGER_FILES: readonly ["ledger/observations.jsonl", "ledger/artifacts.jsonl"];
3
- /** 本包自带的老师定义目录(仓库检出与 npm 安装都在包根 agents/) */
3
+ /** 本包自带的老师文件目录(仓库检出与 npm 安装都在包根 agents/) */
4
4
  export declare const PACKAGE_AGENTS_DIR: string;
5
+ /** 本包版本(出厂件的 hash 记录带它,升级时知道基于哪版) */
6
+ export declare const PACKAGE_VERSION: string;
5
7
  export interface ShippedAgent {
6
8
  name: string;
7
9
  file: string;
8
10
  description: string;
9
11
  }
10
12
  export declare function shippedAgents(dir?: string): Promise<ShippedAgent[]>;
11
- export declare const GITIGNORE = "# \u8F6C\u5F55\u65E5\u5FD7\u4F53\u79EF\u5927\u3001\u53EF\u4ECE\u4F1A\u8BDD\u7D22\u5F15\u91CD\u5EFA\u5173\u952E\u4FE1\u606F;\u8981\u7559\u5168\u6863\u5C31\u5220\u6389\u4E0B\u9762\u8FD9\u884C\nconversations/**/*.log\nnode_modules/\n.DS_Store\n";
13
+ export declare const GITIGNORE = "# \u8F6C\u5F55\u65E5\u5FD7\u4F53\u79EF\u5927\u3001\u53EF\u4ECE\u5BF9\u8BDD\u7D22\u5F15\u91CD\u5EFA\u5173\u952E\u4FE1\u606F;\u8981\u7559\u5168\u6863\u5C31\u5220\u6389\u4E0B\u9762\u8FD9\u884C\nconversations/**/*.log\n# \u914D\u97F3\u53EF\u91CD\u65B0\u5408\u6210\nconversations/**/*.mp3\n# \u8BFE\u5305\u4E0E\u622A\u56FE\u662F drawtell build / snap \u7684\u6D3E\u751F\u7269,\u573A\u666F\u6E90\u5728 scenes/\nbundles/\nsnaps/\nnode_modules/\n.DS_Store\n";
12
14
  /** 家规:所有老师常驻(CLAUDE.md 与 QWEN.md 同一份)。故意短。 */
13
- export declare const RULES = "# \u5BB6\u89C4\n\n- \u5173\u4E8E\u5B69\u5B50\u7684\u89C2\u5BDF,\u5199\u4E00\u6761\u5230 ledger/observations.jsonl(\u4E00\u884C JSON:id\u3001date\u3001author=\u4F60\u7684\u540D\u5B57\u3001claim \u4E00\u53E5\u8BDD\u3001evidence \u6307\u5411\u4F1A\u8BDD\u6216\u4EA7\u7269),\u522B\u7684\u8001\u5E08\u548C\u89C4\u5212\u8001\u5E08\u4ECE\u90A3\u91CC\u8BFB\u3002\n- \u4F60\u81EA\u5DF1\u7684\u7ECF\u9A8C\u8BB0\u8FDB\u4F60\u7684\u8BB0\u5FC6\u76EE\u5F55(MEMORY.md \u4E00\u884C\u7D22\u5F15 + \u4E3B\u9898\u6587\u4EF6)\u3002\n- \u4F60\u8BF4\u7ED9\u5B69\u5B50\u542C\u7684\u8BDD\u653E\u5728\u56DE\u590D\u7684**\u6700\u540E\u4E00\u6BB5**,\u5B69\u5B50\u53EA\u770B\u5F97\u5230\u6700\u540E\u4E00\u6BB5;\u4E2D\u95F4\u7684\u8BDD\u548C\u5DE5\u5177\u8FC7\u7A0B\u53EA\u6709\u5BB6\u957F\u770B\u3002\n- \u9700\u8981\u5BB6\u957F\u62CD\u677F\u7684\u4E8B\u5199\u6210\u4E00\u6BB5\u300C## \u5F85\u88C1\u91CF\u300D(question: \u4E00\u53E5\u8BDD;options: \u5217\u8868),\u4E0D\u8981\u505C\u4E0B\u6765\u7B49\u3002\n- \u8981\u4EA4\u7ED9\u522B\u7684\u8001\u5E08\u7684\u4E8B\u5199\u6210\u4E00\u6BB5\u300C## \u8F6C\u4EA4\u300D(to: \u8001\u5E08\u540D;why: \u4E00\u53E5\u8BDD;refs: \u76F8\u5173\u6587\u4EF6),\u7B2C\u4E00\u671F\u53EA\u5141\u8BB8\u4E00\u8DF3\u3002\n- \u4F1A\u8BDD\u6309\u5929,\u660E\u5929\u4ECE\u8D26\u672C\u548C\u4F60\u7684\u8BB0\u5FC6\u63A5\u7740\u6765,\u4E0D\u8981\u6307\u671B\u4ECA\u5929\u7684\u5BF9\u8BDD\u8FD8\u5728\u3002\n";
15
+ export declare const RULES = "# \u5BB6\u89C4\n\n- \u5173\u4E8E\u5B69\u5B50\u7684\u89C2\u5BDF,\u5199\u4E00\u6761\u5230 ledger/observations.jsonl(\u4E00\u884C JSON:id\u3001date\u3001author=\u4F60\u7684\u540D\u5B57\u3001claim \u4E00\u53E5\u8BDD\u3001evidence \u6307\u5411\u5BF9\u8BDD\u6216\u4EA7\u7269),\u522B\u7684\u8001\u5E08\u548C\u89C4\u5212\u8001\u5E08\u4ECE\u90A3\u91CC\u8BFB\u3002\n- \u4F60\u81EA\u5DF1\u7684\u7ECF\u9A8C\u8BB0\u8FDB\u4F60\u7684\u8BB0\u5FC6\u76EE\u5F55(MEMORY.md \u4E00\u884C\u7D22\u5F15 + \u4E3B\u9898\u6587\u4EF6)\u3002\n- \u56DE\u590D\u6B63\u6587\u5C31\u662F\u5B69\u5B50\u770B\u5230\u7684\u677F\u4E66:\u666E\u901A\u6BB5\u843D\u662F\u4F60\u8BF4\u7ED9\u5B69\u5B50\u542C\u7684\u8BDD(\u4E00\u884C\u4E00\u53E5,\u4F1A\u88AB\u5FF5\u51FA\u6765;\u65B9\u62EC\u53F7\u6807\u51FA\u8981\u5728\u677F\u4E0A\u6572\u7684\u8BCD;\u672B\u53E5\u662F\u95EE\u53E5\u5C31\u505C\u4E0B\u7B49\u5B69\u5B50),\u56F4\u680F\u662F\u677F\u4E0A\u7684\u5361(\u56F4\u680F\u6807\u7B7E\u662F\u5361\u7684\u79CD\u7C7B;\u5361\u7684\u5199\u6CD5\u5728\u8001\u5E08\u6587\u4EF6\u91CC)\u3002\u968F\u53E3\u95EE\u7B54\u5C31\u4E00\u4E24\u53E5\u8BDD,\u6CA1\u6709\u5361\u3002\u677F\u4E66\u5199\u5B8C\u5C31\u505C,\u4E0D\u518D\u8865\u8BDD\u3001\u4E0D\u518D\u7528\u5DE5\u5177\u2014\u2014\u5B69\u5B50\u770B\u5230\u7684\u53EA\u662F\u8FD9\u8F6E\u6700\u540E\u4E00\u6BB5\u8BDD\u3002\n- \u5BF9\u5BB6\u957F\u8BF4\u7684\u8BDD\u5199\u6210\u4E00\u6BB5\u300C## \u5BB6\u957F\u300D;\u9700\u8981\u5BB6\u957F\u62CD\u677F\u7684\u4E8B\u5199\u6210\u4E00\u6BB5\u300C## \u5F85\u88C1\u91CF\u300D(question: \u4E00\u53E5\u8BDD;options: \u5217\u8868),\u4E0D\u8981\u505C\u4E0B\u6765\u7B49;\u8981\u4EA4\u7ED9\u522B\u7684\u8001\u5E08\u7684\u4E8B\u5199\u6210\u4E00\u6BB5\u300C## \u8F6C\u4EA4\u300D(to: \u8001\u5E08\u540D;why: \u4E00\u53E5\u8BDD;refs: \u76F8\u5173\u6587\u4EF6),\u7B2C\u4E00\u671F\u53EA\u5141\u8BB8\u4E00\u8DF3;**\u8F6C\u4EA4\u53EA\u5199\u8FD9\u4E2A\u6BB5,\u4E0D\u8981\u81EA\u5DF1\u7528 Task / \u5B50\u4EE3\u7406\u53BB\u53EB\u90A3\u4F4D\u8001\u5E08**\u2014\u2014\u5E94\u7528\u770B\u5230\u6BB5\u4F1A\u81EA\u52A8\u8D77\u5979\u7684\u4E00\u8F6E\u3002\u8FD9\u4E09\u4E2A\u6BB5\u653E\u5728\u6B63\u6587\u672B\u5C3E,\u5B69\u5B50\u770B\u4E0D\u5230\u3002\n- \u5BF9\u8BDD\u6309\u5929,\u660E\u5929\u4ECE\u8D26\u672C\u548C\u4F60\u7684\u8BB0\u5FC6\u63A5\u7740\u6765,\u4E0D\u8981\u6307\u671B\u4ECA\u5929\u7684\u5BF9\u8BDD\u8FD8\u5728\u3002\n";
16
+ export interface TutorTemplateInput {
17
+ name: string;
18
+ display: string;
19
+ subject?: string;
20
+ description?: string;
21
+ }
22
+ /**
23
+ * 家长自己加老师时的文件模板:与出厂老师同一套约定(cwd、账本、家规、上下文包、问答 / 讲解 / 任务三种回复、
24
+ * 板书写法、不评判、不造课件、家长 / 待裁量 / 转交段),只有第一句人设是这位老师自己的。
25
+ */
26
+ export declare function tutorTemplate(t: TutorTemplateInput): string;
14
27
  export interface ConfigTemplateInput {
15
28
  slug: string;
16
29
  name?: string;
17
30
  port?: number;
18
- teachers: ShippedAgent[];
31
+ tutors: ShippedAgent[];
19
32
  }
20
33
  /** cotutor.json 模板:只在文件不存在时写入;政策文件永不自动重建或覆盖(家长的决定不由机器替她拍板)。 */
21
34
  export declare function configTemplate(input: ConfigTemplateInput): string;
35
+ /** 把 JSON Schema 写进 workspace(机器文件,每次 init / upgrade 都刷新) */
36
+ /** 给老师看的板书语法表:从卡的注册表现生成,机器文件,init / upgrade 每次刷新;老师文件正文让老师讲解前读它 */
37
+ export declare const SYNTAX_FILE = ".cotutor/\u677F\u4E66\u8BED\u6CD5.md";
38
+ export declare function writeSyntaxFile(root: string): Promise<string>;
39
+ export declare function writeSchemaFile(root: string): Promise<string>;
@@ -1,17 +1,23 @@
1
1
  /**
2
- * 工作区骨架清单:init 建、doctor 查,同一张清单(两边各写一遍必然漂移)。
2
+ * workspace骨架清单:init 建、doctor 查,同一张清单(两边各写一遍必然漂移)。
3
3
  * 布局见《cotutor-agent层设计.md》§2:
4
- * cotutor.json / CLAUDE.md QWEN.md(家规)/ .claude/agents .qwen/agents(老师定义链自本包 agents/)
5
- * agents/<name>/(老师的家 = 会话 cwd)/ ledger/(两本账)/ conversations/(会话索引与转录)
4
+ * cotutor.json / CLAUDE.md QWEN.md(家规)/ .claude/agents(老师文件,拷自本包 agents/,是家长的)/ .qwen/agents(相对链)/ .cotutor/shipped.json(出厂 hash)
5
+ * agents/<name>/(老师的家 = 会话 cwd)/ ledger/(两本账)/ conversations/(对话索引与转录)
6
6
  */
7
+ import { readFileSync } from 'node:fs';
7
8
  import { readFile, readdir } from 'node:fs/promises';
8
9
  import { join } from 'node:path';
9
10
  import { fileURLToPath } from 'node:url';
10
11
  import { parseAgentFile } from "../lib/agent-file.js";
11
- export const DIRS = ['agents', 'ledger', 'conversations', '.claude/agents', '.qwen/agents'];
12
+ import { CONFIG_SCHEMA_FILE, TTS_DEFAULT, cotutorJsonSchema } from "../schema/index.js";
13
+ import { boardSyntaxDoc } from "../cards/index.js";
14
+ import { mkdir, writeFile } from 'node:fs/promises';
15
+ export const DIRS = ['agents', 'ledger', 'conversations', '.claude/agents', '.qwen/agents', 'scenes', 'bundles', 'snaps'];
12
16
  export const LEDGER_FILES = ['ledger/observations.jsonl', 'ledger/artifacts.jsonl'];
13
- /** 本包自带的老师定义目录(仓库检出与 npm 安装都在包根 agents/) */
17
+ /** 本包自带的老师文件目录(仓库检出与 npm 安装都在包根 agents/) */
14
18
  export const PACKAGE_AGENTS_DIR = fileURLToPath(new URL('../../agents/', import.meta.url));
19
+ /** 本包版本(出厂件的 hash 记录带它,升级时知道基于哪版) */
20
+ export const PACKAGE_VERSION = JSON.parse(readFileSync(fileURLToPath(new URL('../../package.json', import.meta.url)), 'utf8')).version;
15
21
  export async function shippedAgents(dir = PACKAGE_AGENTS_DIR) {
16
22
  const out = [];
17
23
  for (const f of (await readdir(dir)).filter((f) => f.endsWith('.md')).sort()) {
@@ -22,61 +28,157 @@ export async function shippedAgents(dir = PACKAGE_AGENTS_DIR) {
22
28
  }
23
29
  return out;
24
30
  }
25
- export const GITIGNORE = `# 转录日志体积大、可从会话索引重建关键信息;要留全档就删掉下面这行
31
+ export const GITIGNORE = `# 转录日志体积大、可从对话索引重建关键信息;要留全档就删掉下面这行
26
32
  conversations/**/*.log
33
+ # 配音可重新合成
34
+ conversations/**/*.mp3
35
+ # 课包与截图是 drawtell build / snap 的派生物,场景源在 scenes/
36
+ bundles/
37
+ snaps/
27
38
  node_modules/
28
39
  .DS_Store
29
40
  `;
30
41
  /** 家规:所有老师常驻(CLAUDE.md 与 QWEN.md 同一份)。故意短。 */
31
42
  export const RULES = `# 家规
32
43
 
33
- - 关于孩子的观察,写一条到 ledger/observations.jsonl(一行 JSON:id、date、author=你的名字、claim 一句话、evidence 指向会话或产物),别的老师和规划老师从那里读。
44
+ - 关于孩子的观察,写一条到 ledger/observations.jsonl(一行 JSON:id、date、author=你的名字、claim 一句话、evidence 指向对话或产物),别的老师和规划老师从那里读。
34
45
  - 你自己的经验记进你的记忆目录(MEMORY.md 一行索引 + 主题文件)。
35
- - 你说给孩子听的话放在回复的**最后一段**,孩子只看得到最后一段;中间的话和工具过程只有家长看。
36
- - 需要家长拍板的事写成一段「## 待裁量」(question: 一句话;options: 列表),不要停下来等。
37
- - 要交给别的老师的事写成一段「## 转交」(to: 老师名;why: 一句话;refs: 相关文件),第一期只允许一跳。
38
- - 会话按天,明天从账本和你的记忆接着来,不要指望今天的对话还在。
46
+ - 回复正文就是孩子看到的板书:普通段落是你说给孩子听的话(一行一句,会被念出来;方括号标出要在板上敲的词;末句是问句就停下等孩子),围栏是板上的卡(围栏标签是卡的种类;卡的写法在老师文件里)。随口问答就一两句话,没有卡。板书写完就停,不再补话、不再用工具——孩子看到的只是这轮最后一段话。
47
+ - 对家长说的话写成一段「## 家长」;需要家长拍板的事写成一段「## 待裁量」(question: 一句话;options: 列表),不要停下来等;要交给别的老师的事写成一段「## 转交」(to: 老师名;why: 一句话;refs: 相关文件),第一期只允许一跳;**转交只写这个段,不要自己用 Task / 子代理去叫那位老师**——应用看到段会自动起她的一轮。这三个段放在正文末尾,孩子看不到。
48
+ - 对话按天,明天从账本和你的记忆接着来,不要指望今天的对话还在。
39
49
  `;
40
- const TEACHER_PRESETS = {
41
- 'math-teacher': { display: '数学老师', subject: '数学', avatar: '🧮' },
42
- 'chinese-teacher': { display: '语文老师', subject: '语文', avatar: '📚' },
43
- 'reading-teacher': { display: '朗读老师', subject: '英语', avatar: '📖' },
44
- 'homework-aide': { display: '作业助教', avatar: '📷' },
50
+ /**
51
+ * 家长自己加老师时的文件模板:与出厂老师同一套约定(cwd、账本、家规、上下文包、问答 / 讲解 / 任务三种回复、
52
+ * 板书写法、不评判、不造课件、家长 / 待裁量 / 转交段),只有第一句人设是这位老师自己的。
53
+ */
54
+ export function tutorTemplate(t) {
55
+ const what = t.subject ? `${t.subject}的事` : '孩子问的事';
56
+ return `---
57
+ name: ${t.name}
58
+ description: ${t.description ?? `${t.display}。${what}都找它;随口问的一两句答完,要讲的在板书上一节一节讲`}
59
+ maxTurns: 40
60
+ permissionMode: bypassPermissions
61
+ memory: project
62
+ ---
63
+ 你是这个家的${t.display},面对的是一个小学生和他的家长。cwd 是你的家(agents/${t.name}/),账本在 ../../ledger/,家规在 ../../CLAUDE.md。消息前面有一段 \`cotutor:\` 开头的上下文包(谁在说、几点、正在看什么、本周计划、最近观察),先看它再答。
64
+
65
+ (在这里写这位老师自己的性子和讲法:比如「说话慢一点,爱打比方」「英文后面跟中文」。一两句就够。)
66
+
67
+ 回复有三种,先分清:
68
+ - **问答**(缺省,孩子或家长随口问):一两句大白话,不超过三句,没有卡;不用工具,不读账本(上下文包里已经给了),想到的就直接说。
69
+ - **讲解**(孩子说「讲讲 / 教我 / 不会」、家长发来一道题、或者一句话答不完):写一节板书,格式见下面;讲完一节停下问孩子,孩子答了或按「继续」你再写下一节。
70
+ - **任务**(上下文包里 \`from: system\`,或家长明确写了「记账 / 做计划」):按任务做,做完在正文里报告结果。
71
+
72
+ 现在没有别的课件通道:**不要写文件、不要造课件**,板书就是你的课件。
73
+
74
+ ## 板书怎么写(讲解时)
75
+
76
+ 回复正文就是孩子看到的板书,只有两种东西:
77
+
78
+ - **普通段落 = 你说的话。** 一行一句,每句会被念出来、显示在字幕行,所以不要写标题、列表、粗体、括号注释。句子里用方括号标出要在板上敲的词,那个词要在某张卡上出现。末句写成问句就停下等孩子。
79
+ - **围栏 = 板上的卡。** 围栏的语言标签是卡的种类,正文按各种卡的写法。卡写在讲它的那句话前面,卡与话交错。
80
+ - **板书写完就停。** 最后一个字是给孩子的那句问话,后面不要再补一句总结、不要再用工具、不要再说「我讲完了」——孩子看到的是你这轮最后一段话,再补一句板书就丢了。
81
+
82
+ 卡有哪几种、各自怎么写,在 ../../.cotutor/板书语法.md(text / read / choice / fill / image…);第一次讲解前读一遍,之后不用再读。上下文包里 \`board: off\` 时只说话不出卡。
83
+
84
+ 一节 4–8 张卡、6–12 句话;一次只讲一个想法,讲完就问。孩子的话不上板;孩子答了,你的口头回应直接写成一句话。不打分、不说「错了」:答对了直接往下讲,答偏了就从他的答案往回讲一步。
85
+
86
+ 例子(一节的骨架):
87
+
88
+ \`\`\`\`
89
+ 开场的一句话。
90
+
91
+ \`\`\`text cover
92
+ 标题
93
+ 副标题
94
+ \`\`\`
95
+
96
+ 讲这张卡的一句话,[要敲的词]在卡上。
97
+
98
+ \`\`\`choice
99
+ 问题?
100
+ - [ ] 选项一
101
+ - [x] 选项二
102
+ \`\`\`
103
+
104
+ 末句是问句?
105
+ \`\`\`\`
106
+
107
+ 对家长说的话、要拍板的事、要转交的事,用「## 家长」「## 待裁量」(question: 一句话;options: 列表)「## 转交」三个段放在正文末尾,孩子看不到;不要停下来等家长。
108
+
109
+ 记账与记忆(只在任务里做,问答和讲解不做):值得别的老师知道的观察,追加一行到 ../../ledger/observations.jsonl;你自己的经验记进你的记忆目录。
110
+ `;
111
+ }
112
+ const TUTOR_DEFAULTS = {
113
+ 'math-tutor': { display: '数学老师', subject: '数学', avatar: '🧮' },
114
+ 'chinese-tutor': { display: '语文老师', subject: '语文', avatar: '📚' },
115
+ 'reading-tutor': { display: '朗读老师', subject: '英语', avatar: '📖' },
116
+ 'homework-tutor': { display: '作业老师', avatar: '📷' },
45
117
  planner: { display: '规划老师', avatar: '🗓', hidden: true },
118
+ 'scene-maker': { display: '画图老师', avatar: '🎨', hidden: true, runtime: 'claude-scene' },
46
119
  };
47
120
  /** cotutor.json 模板:只在文件不存在时写入;政策文件永不自动重建或覆盖(家长的决定不由机器替她拍板)。 */
48
121
  export function configTemplate(input) {
49
- const teachers = {};
50
- for (const a of input.teachers) {
51
- const p = TEACHER_PRESETS[a.name];
52
- teachers[a.name] = p
53
- ? { display: p.display, ...(p.subject ? { subject: p.subject } : {}), avatar: p.avatar, enabled: true, ...(p.hidden ? { hidden: true } : {}) }
122
+ const tutors = {};
123
+ for (const a of input.tutors) {
124
+ const p = TUTOR_DEFAULTS[a.name];
125
+ tutors[a.name] = p
126
+ ? { display: p.display, ...(p.subject ? { subject: p.subject } : {}), avatar: p.avatar, enabled: true, ...(p.hidden ? { hidden: true } : {}), ...(p.runtime ? { runtime: p.runtime } : {}) }
54
127
  : { display: a.name, enabled: true };
55
128
  }
56
129
  const cfg = {
130
+ $schema: CONFIG_SCHEMA_FILE,
57
131
  version: 1,
58
- title: input.name ? `${input.name}的书房` : 'cotutor',
132
+ title: input.name ? `${input.name}的老师们` : 'cotutor',
59
133
  kid: { slug: input.slug, ...(input.name ? { name: input.name } : {}) },
60
134
  server: { port: input.port ?? 5180 },
61
135
  paths: {},
62
136
  policyDefaults: {},
63
- teachers,
64
- agents: {
137
+ tutors,
138
+ runtimes: {
65
139
  default: 'claude',
140
+ // 普通老师不许派子代理(--disallowedTools Agent):claude 会把 .claude/agents/ 里的老师文件当可派的子代理,老师自己去叫 scene-maker 就把预算烧在自己这轮里;转交只写「## 转交」段
66
141
  claude: {
67
- run: ['claude', '--agent', '{agent}', '-p', '{prompt}', '--dangerously-skip-permissions', '--output-format', 'stream-json', '--verbose', '--max-budget-usd', '2'],
68
- resume: ['claude', '--agent', '{agent}', '-p', '--resume', '{session}', '{prompt}', '--dangerously-skip-permissions', '--output-format', 'stream-json', '--verbose', '--max-budget-usd', '2'],
142
+ run: ['claude', '--agent', '{agent}', '-p', '{prompt}', '--dangerously-skip-permissions', '--disallowedTools', 'Agent', '--output-format', 'stream-json', '--verbose', '--include-partial-messages', '--max-budget-usd', '2'],
143
+ resume: ['claude', '--agent', '{agent}', '-p', '--resume', '{session}', '{prompt}', '--dangerously-skip-permissions', '--disallowedTools', 'Agent', '--output-format', 'stream-json', '--verbose', '--include-partial-messages', '--max-budget-usd', '2'],
69
144
  },
70
145
  qwen: {
71
146
  run: ['qwen', '-p', '{prompt}', '--append-system-prompt', '{agentBody}', '--yolo', '--output-format', 'stream-json', '--max-wall-time', '10m'],
72
147
  resume: ['qwen', '-p', '{prompt}', '--resume', '{session}', '--append-system-prompt', '{agentBody}', '--yolo', '--output-format', 'stream-json', '--max-wall-time', '10m'],
73
148
  },
149
+ // 场景作业(scene-maker):分钟级、几美元一个,预算与时限比问答大;老师条目 runtime 指到它
150
+ 'claude-scene': {
151
+ run: ['claude', '--agent', '{agent}', '-p', '{prompt}', '--dangerously-skip-permissions', '--output-format', 'stream-json', '--verbose', '--max-budget-usd', '8'],
152
+ resume: ['claude', '--agent', '{agent}', '-p', '--resume', '{session}', '{prompt}', '--dangerously-skip-permissions', '--output-format', 'stream-json', '--verbose', '--max-budget-usd', '8'],
153
+ },
154
+ 'qwen-scene': {
155
+ run: ['qwen', '-p', '{prompt}', '--append-system-prompt', '{agentBody}', '--yolo', '--output-format', 'stream-json', '--max-wall-time', '25m'],
156
+ resume: ['qwen', '-p', '{prompt}', '--resume', '{session}', '--append-system-prompt', '{agentBody}', '--yolo', '--output-format', 'stream-json', '--max-wall-time', '25m'],
157
+ },
74
158
  },
159
+ tts: TTS_DEFAULT,
75
160
  _note: '一孩一 workspace 的政策文件,家长改这里;机器不会自动重建或覆盖,写坏了靠 git 回退,cotutor doctor 可体检。' +
76
- 'teachers = 老师表(key 与 .claude/agents/<key>.md 的 name 一致):display 显示名、avatar、voice 用 voxtell 音色 id、enabled 开关、hidden 孩子端不露、policy 覆盖 policyDefaults。' +
77
- 'policyDefaults 缺省:replyMaxChars 60、dailyMessages 30、dailyRegen 3、reviewGate false、contextPack {recent 10, planLines 10}。' +
161
+ 'tutors = 老师表(key 与 .claude/agents/<key>.md 的 name 一致):display 显示名、avatar、voice 用 voxtell 音色 id、enabled 开关、hidden 孩子端不露、policy 覆盖 policyDefaults。' +
162
+ 'policyDefaults 缺省:replyMaxChars 60(每句)、dailyMessages 30、dailyRegen 3、reviewGate false、contextPack {recent 10, planLines 10}、board auto(off = 只说话不出卡)。' +
78
163
  'paths = 角色映射:vault 指 Obsidian vault 根(一孩一 vault),photos/diary/plans/profile/timetable 相对 vault。' +
79
- 'agents = 运行时预设,占位 {agent} {agentBody} {prompt} {session};政策旋钮(预算、时限)写进模板。',
164
+ 'runtimes = 运行时,占位 {agent} {agentBody} {prompt} {session};政策旋钮(预算、时限、模型)写进模板;老师条目的 runtime 可指定用哪个(scene-maker 用 claude-scene:预算 8 美元)。scenes.dailyMax(缺省 2)= 每天最多起几个场景作业。' +
165
+ 'tts = 配音命令,占位 {text} {voice} {out};老师没配 voice 就不合成,孩子端用浏览器的声。' +
166
+ 'server.https = {cert, key} 只给这个 workspace 用的证书路径(相对 workspace 根);不配则用机器级 ~/.config/cotutor/certs/(cotutor cert 用 mkcert 建,所有 workspace 共用)。',
80
167
  };
81
168
  return `${JSON.stringify(cfg, null, 2)}\n`;
82
169
  }
170
+ /** 把 JSON Schema 写进 workspace(机器文件,每次 init / upgrade 都刷新) */
171
+ /** 给老师看的板书语法表:从卡的注册表现生成,机器文件,init / upgrade 每次刷新;老师文件正文让老师讲解前读它 */
172
+ export const SYNTAX_FILE = '.cotutor/板书语法.md';
173
+ export async function writeSyntaxFile(root) {
174
+ const file = join(root, SYNTAX_FILE);
175
+ await mkdir(join(root, '.cotutor'), { recursive: true });
176
+ await writeFile(file, boardSyntaxDoc());
177
+ return file;
178
+ }
179
+ export async function writeSchemaFile(root) {
180
+ const file = join(root, CONFIG_SCHEMA_FILE);
181
+ await mkdir(join(root, '.cotutor'), { recursive: true });
182
+ await writeFile(file, `${JSON.stringify(cotutorJsonSchema(), null, 2)}\n`);
183
+ return file;
184
+ }
@@ -0,0 +1,40 @@
1
+ export declare const SHIPPED_SKILLS: readonly ["drawtell-scene", "drawtell-teaching", "drawtell-cli", "drawtell-verify"];
2
+ export type ShippedSkillName = (typeof SHIPPED_SKILLS)[number];
3
+ /** drawtell-skills 包的 skills/ 目录;包没装 → null(doctor 点名,init 跳过) */
4
+ export declare function packageSkillsDir(): string | null;
5
+ /** drawtell CLI 的入口(本包 node_modules 里);没装 → null */
6
+ export declare function drawtellBin(): string | null;
7
+ export declare const TOOL_SHIM = ".cotutor/drawtell";
8
+ /** 一个目录的内容 hash:相对路径 + 内容,按路径排序 */
9
+ export declare function dirHash(dir: string): Promise<string>;
10
+ export type SkillState = 'latest' | 'upgradable' | 'custom' | 'untracked' | 'missing' | 'unavailable';
11
+ export interface SkillStatus {
12
+ name: ShippedSkillName;
13
+ state: SkillState;
14
+ dir: string;
15
+ basedOn?: string;
16
+ }
17
+ export declare function skillDirs(root: string, name: string): {
18
+ claude: string;
19
+ qwen: string;
20
+ };
21
+ export declare function skillStatuses(root: string): Promise<SkillStatus[]>;
22
+ export interface SkillStep {
23
+ item: string;
24
+ action: 'created' | 'exists' | 'kept' | 'replaced';
25
+ note?: string;
26
+ }
27
+ /** init 用:缺的拷,有的不动;drawtell-skills 没装就只报一行 */
28
+ export declare function installSkills(root: string): Promise<SkillStep[]>;
29
+ export interface SkillUpgradeStep {
30
+ name: string;
31
+ action: 'upgraded' | 'latest' | 'kept-custom' | 'installed' | 'unavailable';
32
+ basedOn?: string;
33
+ }
34
+ /** upgrade 用:latest 跳过;upgradable 换新;custom / untracked 保留只报;缺的补 */
35
+ export declare function upgradeSkills(root: string): Promise<SkillUpgradeStep[]>;
36
+ /** .cotutor/drawtell:壳脚本,老师在 agents/<name>/ 里用 ../../.cotutor/drawtell 跑 drawtell CLI;机器文件,init / upgrade 每次刷新 */
37
+ export declare function writeToolShim(root: string): Promise<{
38
+ file: string;
39
+ available: boolean;
40
+ }>;
@@ -0,0 +1,170 @@
1
+ /**
2
+ * 出厂 skill(scene-maker 用的四个领域 skill,来自 drawtell-skills 包):与老师文件同一套「拷不链」机制——
3
+ * init 拷进 workspace 的 .claude/skills/<name>/(拷进来就是家长的),hash 记 .cotutor/shipped.json 的 skills,
4
+ * upgrade 没改过的换新、改过的报 diff 保留;.qwen/skills/<name> 是相对链。工作流 skill(math-explainer 等)不拷,
5
+ * scene-maker 的工作流写在它的老师文件正文里(《drawtell接入与场景卡.md》§3)。
6
+ * 顺带一个机器文件 .cotutor/drawtell:指向本包 node_modules 里 drawtell CLI 的壳脚本,老师用相对路径就能跑它。
7
+ */
8
+ import { createHash } from 'node:crypto';
9
+ import { chmod, cp, lstat, mkdir, readFile, readdir, readlink, rm, symlink, unlink, writeFile } from 'node:fs/promises';
10
+ import { createRequire } from 'node:module';
11
+ import { dirname, join, relative } from 'node:path';
12
+ import { PACKAGE_VERSION } from "./skeleton.js";
13
+ import { readManifest, writeManifest } from "./tutors.js";
14
+ export const SHIPPED_SKILLS = ['drawtell-scene', 'drawtell-teaching', 'drawtell-cli', 'drawtell-verify'];
15
+ /** drawtell-skills 包的 skills/ 目录;包没装 → null(doctor 点名,init 跳过) */
16
+ export function packageSkillsDir() {
17
+ try {
18
+ return join(dirname(createRequire(import.meta.url).resolve('drawtell-skills/package.json')), 'skills');
19
+ }
20
+ catch {
21
+ return null;
22
+ }
23
+ }
24
+ /** drawtell CLI 的入口(本包 node_modules 里);没装 → null */
25
+ export function drawtellBin() {
26
+ try {
27
+ return join(dirname(createRequire(import.meta.url).resolve('drawtell/package.json')), 'bin', 'drawtell.js');
28
+ }
29
+ catch {
30
+ return null;
31
+ }
32
+ }
33
+ export const TOOL_SHIM = '.cotutor/drawtell';
34
+ /** 一个目录的内容 hash:相对路径 + 内容,按路径排序 */
35
+ export async function dirHash(dir) {
36
+ const files = [];
37
+ const walk = async (d) => {
38
+ for (const e of (await readdir(d, { withFileTypes: true })).sort((a, b) => a.name.localeCompare(b.name))) {
39
+ const p = join(d, e.name);
40
+ if (e.isDirectory())
41
+ await walk(p);
42
+ else if (e.isFile())
43
+ files.push(p);
44
+ }
45
+ };
46
+ await walk(dir);
47
+ const h = createHash('sha256');
48
+ for (const f of files.sort()) {
49
+ h.update(relative(dir, f));
50
+ h.update('\0');
51
+ h.update(await readFile(f));
52
+ h.update('\0');
53
+ }
54
+ return `sha256:${h.digest('hex')}`;
55
+ }
56
+ export function skillDirs(root, name) {
57
+ return { claude: join(root, '.claude', 'skills', name), qwen: join(root, '.qwen', 'skills', name) };
58
+ }
59
+ async function readState(root, name, manifest, src) {
60
+ const { claude } = skillDirs(root, name);
61
+ const st = await lstat(claude).catch(() => null);
62
+ if (!st?.isDirectory())
63
+ return { name, state: src ? 'missing' : 'unavailable', dir: claude };
64
+ const rec = manifest.skills?.[name];
65
+ const mine = await dirHash(claude);
66
+ if (src) {
67
+ const shipped = await dirHash(join(src, name)).catch(() => null);
68
+ if (shipped && mine === shipped)
69
+ return { name, state: 'latest', dir: claude, basedOn: rec?.version };
70
+ }
71
+ if (rec && rec.hash === mine)
72
+ return { name, state: src ? 'upgradable' : 'latest', dir: claude, basedOn: rec.version };
73
+ return { name, state: rec ? 'custom' : 'untracked', dir: claude, basedOn: rec?.version };
74
+ }
75
+ export async function skillStatuses(root) {
76
+ const manifest = await readManifest(root);
77
+ const src = packageSkillsDir();
78
+ const out = [];
79
+ for (const name of SHIPPED_SKILLS)
80
+ out.push(await readState(root, name, manifest, src));
81
+ return out;
82
+ }
83
+ async function installOne(root, name, src, manifest) {
84
+ const { claude } = skillDirs(root, name);
85
+ await rm(claude, { recursive: true, force: true });
86
+ await mkdir(dirname(claude), { recursive: true });
87
+ await cp(join(src, name), claude, { recursive: true });
88
+ manifest.skills ??= {};
89
+ manifest.skills[name] = { hash: await dirHash(claude), version: PACKAGE_VERSION };
90
+ }
91
+ async function ensureQwenLink(root, name) {
92
+ const { claude, qwen } = skillDirs(root, name);
93
+ await mkdir(dirname(qwen), { recursive: true });
94
+ const want = relative(dirname(qwen), claude);
95
+ const st = await lstat(qwen).catch(() => null);
96
+ if (st?.isSymbolicLink() && (await readlink(qwen).catch(() => '')) === want)
97
+ return 'exists';
98
+ if (st) {
99
+ if (!st.isSymbolicLink())
100
+ return 'exists';
101
+ await unlink(qwen);
102
+ await symlink(want, qwen);
103
+ return 'replaced';
104
+ }
105
+ await symlink(want, qwen);
106
+ return 'created';
107
+ }
108
+ /** init 用:缺的拷,有的不动;drawtell-skills 没装就只报一行 */
109
+ export async function installSkills(root) {
110
+ const steps = [];
111
+ const src = packageSkillsDir();
112
+ if (!src)
113
+ return [{ item: '.claude/skills/', action: 'kept', note: 'drawtell-skills 没装,四个领域 skill 没拷(scene-maker 作业要它们);仓库根 pnpm install' }];
114
+ const manifest = await readManifest(root);
115
+ let touched = false;
116
+ for (const name of SHIPPED_SKILLS) {
117
+ const s = await readState(root, name, manifest, src);
118
+ const item = `.claude/skills/${name}/`;
119
+ if (s.state === 'missing') {
120
+ await installOne(root, name, src, manifest);
121
+ touched = true;
122
+ steps.push({ item, action: 'created', note: `拷自 drawtell-skills` });
123
+ }
124
+ else
125
+ steps.push({ item, action: 'exists', note: s.state === 'custom' ? `自定义(基于 ${s.basedOn})` : s.state === 'upgradable' ? '可升级(cotutor upgrade)' : s.state === 'untracked' ? '已有(没有出厂记录);升级时当自定义对待' : undefined });
126
+ const q = await ensureQwenLink(root, name);
127
+ steps.push({ item: `.qwen/skills/${name}`, action: q, note: q === 'exists' ? undefined : '→ ../../.claude/skills/' });
128
+ }
129
+ if (touched)
130
+ await writeManifest(root, { ...manifest, version: PACKAGE_VERSION });
131
+ return steps;
132
+ }
133
+ /** upgrade 用:latest 跳过;upgradable 换新;custom / untracked 保留只报;缺的补 */
134
+ export async function upgradeSkills(root) {
135
+ const src = packageSkillsDir();
136
+ const steps = [];
137
+ if (!src)
138
+ return SHIPPED_SKILLS.map((name) => ({ name, action: 'unavailable' }));
139
+ const manifest = await readManifest(root);
140
+ for (const name of SHIPPED_SKILLS) {
141
+ const s = await readState(root, name, manifest, src);
142
+ if (s.state === 'latest')
143
+ steps.push({ name, action: 'latest', basedOn: s.basedOn });
144
+ else if (s.state === 'missing') {
145
+ await installOne(root, name, src, manifest);
146
+ steps.push({ name, action: 'installed' });
147
+ }
148
+ else if (s.state === 'upgradable') {
149
+ await installOne(root, name, src, manifest);
150
+ steps.push({ name, action: 'upgraded', basedOn: s.basedOn });
151
+ }
152
+ else
153
+ steps.push({ name, action: 'kept-custom', basedOn: s.basedOn });
154
+ await ensureQwenLink(root, name);
155
+ }
156
+ await writeManifest(root, { ...manifest, version: PACKAGE_VERSION });
157
+ return steps;
158
+ }
159
+ /** .cotutor/drawtell:壳脚本,老师在 agents/<name>/ 里用 ../../.cotutor/drawtell 跑 drawtell CLI;机器文件,init / upgrade 每次刷新 */
160
+ export async function writeToolShim(root) {
161
+ const file = join(root, TOOL_SHIM);
162
+ const bin = drawtellBin();
163
+ await mkdir(dirname(file), { recursive: true });
164
+ const body = bin
165
+ ? `#!/bin/sh\n# cotutor 生成的机器文件:drawtell CLI 的壳,指向本包 node_modules 里的 drawtell;init / upgrade 会刷新\nexec node "${bin}" "$@"\n`
166
+ : `#!/bin/sh\necho "cotutor: node_modules 里没有 drawtell(仓库根 pnpm install),场景作业跑不了" >&2\nexit 1\n`;
167
+ await writeFile(file, body);
168
+ await chmod(file, 0o755);
169
+ return { file, available: bin !== null };
170
+ }
@@ -0,0 +1,84 @@
1
+ import { type TutorTemplateInput } from './skeleton.ts';
2
+ export declare const SHIPPED_FILE = ".cotutor/shipped.json";
3
+ export interface ShippedManifest {
4
+ /** 出厂 skill(drawtell-skills 的四个领域 skill)的 hash,与 tutors 同一套机制(src/cli/skills.ts) */
5
+ skills?: Record<string, {
6
+ hash: string;
7
+ version: string;
8
+ }>;
9
+ version: string;
10
+ tutors: Record<string, {
11
+ hash: string;
12
+ version: string;
13
+ }>;
14
+ }
15
+ export declare const sha256: (text: string) => string;
16
+ export declare function readManifest(root: string): Promise<ShippedManifest>;
17
+ export declare function writeManifest(root: string, m: ShippedManifest): Promise<void>;
18
+ export type TutorState = 'latest' | 'upgradable' | 'custom' | 'untracked' | 'missing' | 'broken';
19
+ export interface TutorStatus {
20
+ name: string;
21
+ state: TutorState;
22
+ /** workspace 里那份 */
23
+ file: string;
24
+ /** 记录的出厂版本(custom / upgradable 时有) */
25
+ basedOn?: string;
26
+ /** 是否还是指向包的旧链(init 会换成拷贝) */
27
+ legacyLink?: boolean;
28
+ }
29
+ /** 老师文件在 workspace 里的位置:.claude/agents/<name>.md 是真相,.qwen/agents/<name>.md 是相对链 */
30
+ export declare function tutorFiles(root: string, name: string): {
31
+ claude: string;
32
+ qwen: string;
33
+ };
34
+ export declare function tutorStatuses(root: string): Promise<TutorStatus[]>;
35
+ export interface InstallStep {
36
+ item: string;
37
+ action: 'created' | 'exists' | 'kept' | 'replaced';
38
+ note?: string;
39
+ }
40
+ /**
41
+ * init 用。出厂的:缺的拷,旧的包内链换成拷贝,家长的文件不动;
42
+ * cotutor.json 里的每一位(含家长自己加的):补老师目录 agents/<name>/ 与 .qwen 链——「加老师 = 加文件 + 目录,没有注册表」,
43
+ * 自己加的老师文件不在这里生成(cotutor add 生成模板),缺了由 doctor 点名。
44
+ */
45
+ export declare function installTutors(root: string, configured?: string[]): Promise<InstallStep[]>;
46
+ export interface AddResult {
47
+ name: string;
48
+ file: string;
49
+ home: string;
50
+ }
51
+ /**
52
+ * cotutor add <name>:按出厂老师的结构写一份模板(约定都带上,人设一句留给家长填)、建目录与 .qwen 链。
53
+ * 不动 cotutor.json——那是调用方(main / 页面)用 patchConfig 加的,同一条路。已有同名文件就拒,不覆盖。
54
+ */
55
+ export declare function addTutorFile(root: string, input: TutorTemplateInput): Promise<AddResult>;
56
+ export interface UpgradeStep {
57
+ name: string;
58
+ action: 'upgraded' | 'latest' | 'kept-custom' | 'forced' | 'installed';
59
+ basedOn?: string;
60
+ /** custom 时的行级 diff(出厂新版 vs 你的) */
61
+ diff?: string[];
62
+ }
63
+ /** 最小行 diff(LCS),只为让家长看清改了哪几行;不追求 git 那么好 */
64
+ export declare function lineDiff(a: string, b: string): string[];
65
+ /**
66
+ * cotutor upgrade:latest 跳过;upgradable 换新;custom / untracked 只报 diff 并保留(--force <name> 才覆盖,原文留 .bak);缺的补。
67
+ */
68
+ export declare function upgradeTutors(root: string, opts?: {
69
+ force?: string[];
70
+ }): Promise<UpgradeStep[]>;
71
+ export interface TutorFileView {
72
+ name: string;
73
+ text: string;
74
+ /** 出厂件的状态;自家加的是 own */
75
+ state: TutorState | 'own';
76
+ basedOn?: string;
77
+ }
78
+ export declare function readTutorFile(root: string, name: string): Promise<TutorFileView>;
79
+ /** 页面上改老师正文:frontmatter 的 name 必须还是它;写完 doctor 会把它标成自定义 */
80
+ export declare function writeTutorFile(root: string, name: string, text: string): Promise<TutorFileView>;
81
+ /** 删自家加的老师文件:改名成 .removed-<时间>(不真删;记忆目录与对话不动);出厂的不让删,用开关 */
82
+ export declare function removeTutorFile(root: string, name: string): Promise<{
83
+ moved: string | null;
84
+ }>;