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
@@ -0,0 +1,353 @@
1
+ /**
2
+ * 命令表:cotutor 每条命令的说明、参数和例子,只在这里写一次。
3
+ * 由它生成:顶层帮助与 cotutor <命令> --help(usage.ts)、技能里的命令参考(app/skills/*-doc.ts);
4
+ * main.ts 照它拦不认识的旗标、用错子命令的旗标和多余的参数(agent 打错一个字,不能悄悄当没看见)。
5
+ * 纯数据,不引别的模块:技能生成和测试都要读它。
6
+ */
7
+ export const GROUP_TITLE = {
8
+ setup: '安装与设置',
9
+ daily: '日常使用',
10
+ custom: '定制教学',
11
+ debug: '开发与调试',
12
+ };
13
+ const DATE = { name: 'date', value: '<日期>', desc: '哪天(YYYY-MM-DD,缺省今天)' };
14
+ const LANE = { name: 'lane', value: '<道>', desc: '只看这几道的事件,逗号分开,如 main,tts' };
15
+ const ACTIVITY = { name: 'activity', value: '<名>', desc: '像孩子点了首页的 activity 按钮,新开话题' };
16
+ const PARAM = { name: 'param', value: "'名=值;名=值'", desc: 'activity 的参数(按钮下面那几行)' };
17
+ const FROM = { name: 'from', value: 'kid|system', desc: '谁说的(缺省 kid;system 不算孩子说的)' };
18
+ const OPEN_QR = { name: 'open-qr', desc: '--open 的旧名', hidden: true };
19
+ export const COMMANDS = [
20
+ // ---- 安装与设置 ----
21
+ {
22
+ name: 'guide', group: 'setup', summary: '给 AI 助手的安装说明(新电脑先跑这条)',
23
+ usage: ['cotutor guide'], maxArgs: 0,
24
+ notes: ['打印随包发的 INSTALL.md,和这个版本对得上', '还没装 cotutor 时:npx cotutor@latest guide', '第一行只用 ASCII:中文成了乱码,照它说的直接按 UTF-8 读文件'],
25
+ examples: ['npx cotutor@latest guide'],
26
+ },
27
+ {
28
+ name: 'init', group: 'setup', summary: '建孩子的 cotutor 文件夹和笔记文件夹',
29
+ usage: ['cotutor init <slug> [--name <称呼>] [--grade <1-6>] [--vault <目录>] [--dir <目录>] [--port <n>] [--json]'],
30
+ maxArgs: 1, json: true,
31
+ flags: [
32
+ { name: 'name', value: '<称呼>', desc: '老师怎么叫孩子,小名就行' },
33
+ { name: 'grade', value: '<1-6>', desc: '年级,决定装哪个教学包(cotutor grades 看有哪些)' },
34
+ { name: 'vault', value: '<目录>', desc: '用家长已有的笔记库(缺省在旁边建 <slug>-vault/);不能放在 cotutor 文件夹里' },
35
+ { name: 'dir', value: '<目录>', desc: 'cotutor 文件夹放哪(缺省 ~/cotutor/<slug>/)' },
36
+ { name: 'port', value: '<n>', desc: '端口(缺省 443,不行就 8443、8445…);选一次写进 cotutor.json,以后不变' },
37
+ ],
38
+ notes: ['<slug> 是孩子的短名:小写字母、数字、连字符,不用真名', '重复跑只补缺,已有的文件不动'],
39
+ examples: ['cotutor init ming --name 小明 --grade 2'],
40
+ },
41
+ {
42
+ name: 'grades', group: 'setup', summary: '列出能装的年级教学包',
43
+ usage: ['cotutor grades [--json]'], maxArgs: 0, json: true,
44
+ examples: ['cotutor grades --json'],
45
+ },
46
+ {
47
+ name: 'service', group: 'setup', summary: '后台服务:装、起停、状态、日志',
48
+ usage: [
49
+ 'cotutor service install [--no-open] [--no-tray] [--json]',
50
+ 'cotutor service uninstall | start | stop | restart',
51
+ 'cotutor service status [--json]',
52
+ 'cotutor service logs [--lines <n>] [--follow]',
53
+ ],
54
+ subs: ['install', 'uninstall', 'start', 'stop', 'restart', 'status', 'logs'], subRequired: true, hiddenSubs: ['run'],
55
+ maxArgs: 1, workspace: true, json: ['install', 'uninstall', 'status'],
56
+ flags: [
57
+ { name: 'no-open', desc: '装完不打开家长端(第一次装缺省会打开)', subs: ['install'] },
58
+ { name: 'open', desc: '装完打开家长端(重装缺省不开)', subs: ['install'] },
59
+ OPEN_QR,
60
+ { name: 'no-tray', desc: '不带菜单栏 / 任务栏图标', subs: ['install'] },
61
+ { name: 'lines', value: '<n>', desc: '看最后几行(缺省 50)', subs: ['logs'] },
62
+ { name: 'follow', desc: '一直跟着看新写的', subs: ['logs'] },
63
+ { name: 'log-dir', value: '<目录>', desc: '看护进程用', subs: ['run'], hidden: true },
64
+ ],
65
+ notes: [
66
+ '一个孩子一个服务,不要管理员:macOS launchd、Linux systemd 用户服务、Windows 登录项',
67
+ '登录后自己起来,崩了 10 秒后再起;日志按天留 14 天',
68
+ '换了 Node 版本或包挪了地方,再 install 一次',
69
+ ],
70
+ effects: ['install / uninstall 改这台电脑的登录项'],
71
+ examples: ['cotutor service install --no-open', 'cotutor service status --json', 'cotutor service logs --lines 100'],
72
+ },
73
+ {
74
+ name: 'cert', group: 'setup', summary: '签 HTTPS 证书(平板上说话要 HTTPS)',
75
+ usage: ['cotutor cert [<名或IP>]... [--json]'], maxArgs: Infinity, json: true,
76
+ flags: [{ name: 'host', value: '<名或IP>', desc: '多认一个名字或 IP(也可以直接写在后面)' }],
77
+ notes: ['要先装 mkcert;缺省认电脑名,换 Wi-Fi、IP 变了都不用重签', '所有孩子共用一张,在 ~/.config/cotutor/certs/', '签完 cotutor service restart 才走 HTTPS'],
78
+ effects: ['这台电脑信任根证书要人自己跑 mkcert -install(要输密码或点弹窗)'],
79
+ examples: ['cotutor cert', 'cotutor cert 192.168.1.20'],
80
+ },
81
+ {
82
+ name: 'pin', group: 'setup', summary: '家长端密码(PIN)',
83
+ usage: ['cotutor pin [--pin <4-8 位数字>]', 'cotutor pin off | status [--json]'],
84
+ subs: ['off', 'status'], maxArgs: 1, json: true,
85
+ flags: [{ name: 'pin', value: '<数字>', desc: '设成这个 PIN(不带它在终端里会问)', subs: [''] }],
86
+ notes: ['锁家长端 /parent 和工作台 /dev;没设就对局域网全开', '这台电脑上免密;所有孩子共用'],
87
+ effects: ['要人来:PIN 由家长自己设,不要说给 AI 助手'],
88
+ examples: ['cotutor pin status --json', 'cotutor pin off'],
89
+ },
90
+ // ---- 日常使用 ----
91
+ {
92
+ name: 'open', group: 'daily', summary: '打开家长端',
93
+ usage: ['cotutor open [--print] [--json]'], maxArgs: 0, workspace: true, sandbox: true, json: true,
94
+ flags: [{ name: 'print', desc: '只打印地址,不开浏览器' }],
95
+ notes: ['读 cotutor.json,不用记端口;没在跑就说怎么起,退出码 1'],
96
+ examples: ['cotutor open', 'cotutor open --print'],
97
+ },
98
+ {
99
+ name: 'doctor', group: 'daily', summary: '体检;--fix 修掉能自动修的',
100
+ usage: ['cotutor doctor [--json]', 'cotutor doctor --fix', 'cotutor doctor --live'],
101
+ maxArgs: 0, workspace: true, sandbox: true, json: true,
102
+ flags: [
103
+ { name: 'fix', desc: '修掉能自动修的(幂等,不碰孩子的数据,不用人输东西),要人来的留着' },
104
+ { name: 'live', desc: '真连一次百炼的嘴、真配一句音' },
105
+ ],
106
+ notes: ['--json 里没过的项带 cmd(能直接跑)或 human(要人来)', '有必需项没过,退出码 1'],
107
+ effects: ['--live 花一分钱'],
108
+ examples: ['cotutor doctor --fix', 'cotutor doctor --json'],
109
+ },
110
+ {
111
+ name: 'update', group: 'daily', summary: '升级程序,再更新每个孩子的出厂文件',
112
+ usage: ['cotutor update [--anyway]'], maxArgs: 0, workspace: true,
113
+ flags: [{ name: 'anyway', desc: '孩子在通话时也动(缺省等下课)' }],
114
+ notes: ['问 npm 有没有新版本 → 换程序 → 每个孩子换新出厂文件(cotutor refresh)、重起服务和图标', 'npm 没装成,服务不动', '「升级 cotutor」就是这条'],
115
+ examples: ['cotutor update'],
116
+ },
117
+ {
118
+ name: 'backup', group: 'daily', summary: '备份、看有哪些、恢复',
119
+ usage: ['cotutor backup', 'cotutor backup list [--json]', 'cotutor backup restore <名字> [--yes]'],
120
+ subs: ['list', 'restore'], maxArgs: 2, workspace: true, sandbox: true, json: true,
121
+ flags: [{ name: 'yes', desc: '真的恢复(不加只说会做什么)', subs: ['restore'] }],
122
+ notes: ['在孩子的 cotutor 文件夹旁边的 .backups/,不含录音', '服务开着时每天一份,升级前一份;恢复前先把现在的也备一份', 'restore 不加 --yes:只说会做什么,以 1 退(--json 时 needsYes: true)'],
123
+ effects: ['restore 覆盖孩子的文件:家长明确说要恢复才跑'],
124
+ examples: ['cotutor backup list', 'cotutor backup restore <名字> --yes'],
125
+ },
126
+ {
127
+ name: 'report', group: 'daily', summary: '诊断信息(不含孩子的对话和 key)',
128
+ usage: ['cotutor report'], maxArgs: 0, workspace: true, sandbox: true,
129
+ notes: ['版本、系统、服务、没过的体检、最近报错的原因;名字和 IP 已抹掉,先看再发', '给人看、贴进 issue 的,只出文字;要机器可读的用 cotutor doctor --json 和 cotutor service status --json'],
130
+ examples: ['cotutor report'],
131
+ },
132
+ {
133
+ name: 'tray', group: 'daily', summary: '菜单栏 / 任务栏图标',
134
+ usage: ['cotutor tray on | off | status [--json]'],
135
+ subs: ['on', 'off', 'status'], subRequired: true, maxArgs: 1, json: true,
136
+ notes: ['macOS、Windows;service install 时一起装', 'off 关掉并记住,以后装服务也不带'],
137
+ examples: ['cotutor tray status --json'],
138
+ },
139
+ // ---- 定制教学 ----
140
+ {
141
+ name: 'refresh', group: 'custom', summary: '换新这个孩子的出厂文件(不升级程序)',
142
+ usage: ['cotutor refresh [--json]', 'cotutor refresh --config [--dry-run] [--json]'],
143
+ maxArgs: 0, workspace: true, sandbox: true, json: true,
144
+ flags: [
145
+ { name: 'config', desc: '改的是 cotutor.json:只补缺的、删不再用的出厂段,改过的值不动' },
146
+ { name: 'dry-run', desc: '和 --config 一起:只看要改什么' },
147
+ { name: 'grade', value: '<1-6>', desc: '', hidden: true },
148
+ ],
149
+ notes: [
150
+ '出厂的老师、activity、技能、主题:没改过的换新,改过的保留并报 diff;换之前自动备份一份',
151
+ '升级程序用 cotutor update,它会对每个孩子跑这条;这条只在程序已经换过(手动 npm i -g、从仓库跑)时单独用',
152
+ '改过的想回出厂的:cotutor reset',
153
+ ],
154
+ examples: ['cotutor refresh', 'cotutor refresh --config --dry-run'],
155
+ },
156
+ {
157
+ name: 'reset', group: 'custom', summary: '把改过的出厂老师或 activity 换回出厂的',
158
+ usage: ['cotutor reset <老师>|<老师>/<activity>... [--json]'],
159
+ maxArgs: Infinity, workspace: true, sandbox: true, json: true,
160
+ notes: ['只动点名的这几份;你那份存成旁边的 .bak,换之前还自动备份一份', '自家加的老师、activity 没有出厂那份,换不了'],
161
+ effects: ['盖掉家长改过的老师文件:家长明确说要回出厂的才跑'],
162
+ examples: ['cotutor reset math-tutor', 'cotutor reset chinese-tutor/你说我画'],
163
+ },
164
+ {
165
+ // refresh 的旧名:0.4.x 的 cotutor update 换完程序,会用新程序跑 upgrade --workspace <孩子>,所以一直要认;不进帮助
166
+ name: 'upgrade', group: 'custom', hidden: true, summary: '旧名:升级程序用 cotutor update,换新出厂文件用 cotutor refresh,回出厂用 cotutor reset',
167
+ usage: ['cotutor upgrade [--force <老师>]... [--config [--dry-run]] [--json]'],
168
+ maxArgs: Infinity, workspace: true, sandbox: true, json: true,
169
+ flags: [
170
+ { name: 'force', value: '<老师>', desc: '同 cotutor reset' },
171
+ { name: 'config', desc: '同 cotutor refresh --config' },
172
+ { name: 'dry-run', desc: '和 --config 一起:只看要改什么' },
173
+ { name: 'grade', value: '<1-6>', desc: '', hidden: true },
174
+ ],
175
+ notes: ['还认,是为了旧版本的 cotutor update 和旧文档;新写的一律用 update / refresh / reset'],
176
+ examples: ['cotutor refresh'],
177
+ },
178
+ {
179
+ name: 'add', group: 'custom', summary: '加一位自家的老师',
180
+ usage: ['cotutor add <老师名> --display <显示名> [--subject <学科>] [--avatar <emoji|图片>] [--description <一句话>] [--hidden] [--json]'],
181
+ maxArgs: 1, workspace: true, sandbox: true, json: true,
182
+ flags: [
183
+ { name: 'display', value: '<显示名>', desc: '孩子看到的名字(必填)' },
184
+ { name: 'subject', value: '<学科>', desc: '学科' },
185
+ { name: 'avatar', value: '<emoji|图片>', desc: 'emoji,或 workspace 里图片的相对路径' },
186
+ { name: 'description', value: '<一句话>', desc: '写进老师文件的一句介绍' },
187
+ { name: 'hidden', desc: '孩子端不露(只和系统、家长打交道的工具人)' },
188
+ ],
189
+ notes: ['名字以 -tutor 结尾的是有脸的老师,其余是工具人', '出老师文件、写进 cotutor.json、建它的家;服务不用重启'],
190
+ examples: ['cotutor add science-tutor --display 科学老师 --subject 科学 --avatar 🔬'],
191
+ },
192
+ {
193
+ name: 'add-theme', group: 'custom', summary: '加一个自家的主题',
194
+ usage: ['cotutor add-theme <主题名> [--from <主题>] [--json]'], maxArgs: 1, workspace: true, sandbox: true, json: true,
195
+ flags: [{ name: 'from', value: '<主题>', desc: '从哪个主题拷(缺省 default)' }],
196
+ notes: ['拷到 themes/<主题名>/;cotutor.json 的 kid.theme 改成它就换过去'],
197
+ examples: ['cotutor add-theme dark'],
198
+ },
199
+ {
200
+ name: 'home', group: 'custom', summary: '检查和发布孩子端首页',
201
+ usage: ['cotutor home check [--published] [--json]', 'cotutor home publish [--force] [--from <文件>] [--json]', 'cotutor home show [--json]'],
202
+ subs: ['check', 'publish', 'show'], subRequired: true, maxArgs: 1, workspace: true, sandbox: true, json: true,
203
+ flags: [
204
+ { name: 'published', desc: '查已发布的那份(缺省查草稿 home/draft.md)', subs: ['check'] },
205
+ { name: 'force', desc: '有要改的卡,丢掉它们照发', subs: ['publish'] },
206
+ { name: 'from', value: '<文件>', desc: '发另一份(比如历史里的)', subs: ['publish'] },
207
+ ],
208
+ notes: ['check 不花钱:孩子会看到哪几张卡、每位老师的按钮、要改的(带行号)', 'publish 先检查,留历史 home/history/,写 home/published.json;孩子端刷新就是新的', 'show:现在发布的是哪份、每个按钮被点了几次'],
209
+ examples: ['cotutor home check', 'cotutor home publish'],
210
+ },
211
+ {
212
+ name: 'activity', group: 'custom', summary: '检查和评测 activity',
213
+ usage: ['cotutor activity check [<老师> [<名>]] [--json]', 'cotutor activity eval <老师> <名> [--runs <n>] [--only <例子>] [--kid-model <模型>] [--json]'],
214
+ subs: ['check', 'eval'], subRequired: true, maxArgs: 3, workspace: true, sandbox: true, json: true,
215
+ flags: [
216
+ { name: 'runs', value: '<n>', desc: '每个例子跑几次(1–10,缺省 3),次次都过才算过', subs: ['eval'] },
217
+ { name: 'only', value: '<例子>', desc: '只跑这个例子', subs: ['eval'] },
218
+ { name: 'kid-model', value: '<模型>', desc: '模拟孩子用的千问模型(缺省 qwen3.8-flash)', subs: ['eval'] },
219
+ ],
220
+ notes: ['activity 文件在 agents/<老师>/activities/<名>.md', 'check 不花钱;eval 照 <名>.例子.md 跑真老师 + 模拟孩子,落 evals/'],
221
+ effects: ['eval 花钱'],
222
+ examples: ['cotutor activity check', 'cotutor activity eval chinese-tutor 你说我画 --runs 1'],
223
+ },
224
+ {
225
+ name: 'chalk', group: 'custom', summary: '做小课堂的课包',
226
+ usage: [
227
+ 'cotutor chalk check <scene.ts|目录> [--no-types] [--json]',
228
+ 'cotutor chalk build <scene.ts|目录> [--out <目录>] [--json]',
229
+ 'cotutor chalk bake <scene.ts|课包 id> [--bundles <目录>] [--json]',
230
+ 'cotutor chalk snap <scene.ts|课包 id> [--step <n>] [--no-cursor] [--out <目录>] [--bundles <目录>] [--json]',
231
+ 'cotutor chalk dub <scene.ts|课包 id> [--voice <音色>] [--tutor <老师>] [--instruction <语气>] [--dry-run] [--bundles <目录>] [--json]',
232
+ ],
233
+ subs: ['check', 'build', 'bake', 'snap', 'dub'], maxArgs: 2, workspace: true, sandbox: true, json: true,
234
+ flags: [
235
+ { name: 'no-types', desc: '不跑类型检查', subs: ['check'] },
236
+ { name: 'out', value: '<目录>', desc: 'build:课包放哪(缺省 bundles/<id>/);snap:截图放哪', subs: ['build', 'snap'] },
237
+ { name: 'bundles', value: '<目录>', desc: '课包目录(缺省 workspace 的 bundles/)', subs: ['bake', 'snap', 'dub'] },
238
+ { name: 'step', value: '<n>', desc: '只截这一步(0 = 空板)', subs: ['snap'] },
239
+ { name: 'no-cursor', desc: '截图里不画笔尖', subs: ['snap'] },
240
+ { name: 'voice', value: '<音色>', desc: '配音的音色(缺省用 --tutor 那位老师卡的音色)', subs: ['dub'] },
241
+ { name: 'tutor', value: '<老师>', desc: '用这位老师卡的音色(缺省 math-tutor)', subs: ['dub'] },
242
+ { name: 'instruction', value: '<语气>', desc: '配音的语气', subs: ['dub'] },
243
+ { name: 'dry-run', desc: '只算账(几步、多少计费字符),不合成', subs: ['dub'] },
244
+ ],
245
+ notes: ['check 校验场景;build 出课包;bake 烤画面;snap 每步截图;dub 配音', '给 .ts 的 bake / snap / dub 先 check + build;给课包 id 的直接用 bundles 里那份', '相对路径按当前目录'],
246
+ effects: ['bake / snap 要本机的 Chrome', 'dub 花钱'],
247
+ examples: ['cotutor chalk check scene.ts', 'cotutor chalk snap scene.ts'],
248
+ },
249
+ // ---- 开发与调试(顶层帮助缺省不列,cotutor --help --all 看) ----
250
+ {
251
+ name: 'serve', group: 'debug', summary: '在终端里起服务(关了终端就停)',
252
+ usage: ['cotutor serve [--port <n>] [--http] [--open] [--trace [--lane <道>]] [--log-dir <目录>]'],
253
+ maxArgs: 0, workspace: true, sandbox: true,
254
+ flags: [
255
+ { name: 'port', value: '<n>', desc: '这次换个端口' },
256
+ { name: 'http', desc: '不走 HTTPS' },
257
+ { name: 'open', desc: '顺手打开家长端' },
258
+ OPEN_QR,
259
+ { name: 'trace', desc: '每一轮的事件按道打印' },
260
+ LANE,
261
+ { name: 'log-dir', value: '<目录>', desc: '输出按天落文件(后台服务用)' },
262
+ ],
263
+ notes: ['一直开着用 cotutor service'],
264
+ examples: ['cotutor serve --sandbox'],
265
+ },
266
+ {
267
+ name: 'mock', group: 'debug', summary: '不花钱看前端:假老师、假嘴',
268
+ usage: ['cotutor mock [--port <n>] [--scenario normal|nokey] [--delay <ms>] [--http] [--open]'], maxArgs: 0,
269
+ flags: [
270
+ { name: 'port', value: '<n>', desc: '端口' },
271
+ { name: 'scenario', value: 'normal|nokey', desc: 'nokey = 没配百炼,孩子端只出「老师们还没到」' },
272
+ { name: 'delay', value: '<ms>', desc: '假老师的延迟(毫秒)' },
273
+ { name: 'http', desc: '不走 HTTPS' },
274
+ { name: 'open', desc: '顺手打开扫码页' },
275
+ OPEN_QR,
276
+ ],
277
+ notes: ['不需要 workspace'],
278
+ examples: ['cotutor mock --scenario nokey'],
279
+ },
280
+ {
281
+ name: 'send', group: 'debug', summary: '终端里给老师发一条',
282
+ usage: [
283
+ 'cotutor send <老师> [<消息>] [--new] [--photo <图,图>] [--quiet] [--json]',
284
+ "cotutor send <老师> --activity <名> [--param '名=值;名=值']",
285
+ ],
286
+ maxArgs: Infinity, workspace: true, sandbox: true, json: true,
287
+ flags: [
288
+ { name: 'new', desc: '开新话题' },
289
+ { name: 'photo', value: '<图,图>', desc: '带作业照片(jpg / png,最多 9 张);有照片时消息可以空' },
290
+ ACTIVITY, PARAM, FROM,
291
+ { name: 'runtime', value: '<名>', desc: '换个运行时' },
292
+ LANE,
293
+ { name: 'quiet', desc: '只要结果,不打印过程' },
294
+ ],
295
+ notes: ['孩子的话照实时写(同孩子端),只是没有嘴;事件落 events.jsonl,事后 cotutor trace 看'],
296
+ effects: ['花钱', '写进孩子的对话记录(试一下用 --sandbox)'],
297
+ examples: ['cotutor send math-tutor "这一步为什么要借位"'],
298
+ },
299
+ {
300
+ name: 'pack', group: 'debug', summary: '干跑上下文包:不起模型,看老师会拿到什么',
301
+ usage: ["cotutor pack <老师> [<消息>] [--activity <名> [--param '名=值;名=值']] [--from kid|system] [--at <ISO时间>] [--json]"],
302
+ maxArgs: Infinity, workspace: true, sandbox: true, json: true,
303
+ flags: [ACTIVITY, PARAM, FROM, { name: 'at', value: '<ISO时间>', desc: '当作这个时间' }],
304
+ notes: ['每段来自哪个文件、一共几条、按政策带了几条;改了档案、日记、计划立刻看效果'],
305
+ examples: ['cotutor pack math-tutor "讲讲退位"'],
306
+ },
307
+ {
308
+ name: 'show', group: 'debug', summary: '看一轮:问了什么、上下文包、讲稿与卡、读了什么',
309
+ usage: ['cotutor show <老师> <job> [<日期>] [--evals] [--json]'], maxArgs: 3, workspace: true, sandbox: true, json: true,
310
+ flags: [{ name: 'evals', desc: '看回放的那轮(evals/ 里的)' }],
311
+ notes: ['日期缺省今天;--json 和家长端「看原文」是同一份'],
312
+ examples: ['cotutor show math-tutor 1620-1 2026-09-12 --json'],
313
+ },
314
+ {
315
+ name: 'trace', group: 'debug', summary: '回放一轮的事件',
316
+ usage: ['cotutor trace <老师> <job> [<日期>] [--lane <道>] [--json]'], maxArgs: 3, workspace: true, sandbox: true, json: true,
317
+ flags: [LANE],
318
+ notes: ['读 <日期>.<job>.events.jsonl;日期缺省今天'],
319
+ examples: ['cotutor trace math-tutor 1620-1 --lane main'],
320
+ },
321
+ {
322
+ name: 'replay', group: 'debug', summary: '同一问按现在的提示词再跑一遍,并排对照',
323
+ usage: ['cotutor replay <老师> <job> [<日期>] [--runtime <名>] [--json]'], maxArgs: 3, workspace: true, sandbox: true, json: true,
324
+ flags: [{ name: 'runtime', value: '<名>', desc: '换个运行时' }],
325
+ notes: ['落 evals/,不进孩子的对话、不配音'],
326
+ effects: ['花钱'],
327
+ examples: ['cotutor replay math-tutor 1620-1 2026-09-12'],
328
+ },
329
+ {
330
+ name: 'compare', group: 'debug', summary: '再看一次回放的对照',
331
+ usage: ['cotutor compare <老师> [<evalJob>] [<日期>] [--json]'], maxArgs: 3, workspace: true, sandbox: true, json: true,
332
+ notes: ['不给 evalJob 就列这天回放过哪些'],
333
+ examples: ['cotutor compare math-tutor'],
334
+ },
335
+ {
336
+ name: 'rate', group: 'debug', summary: '给一个话题打星(1–5)',
337
+ usage: ['cotutor rate <老师> <话题> <1-5> [--date <日期>] [--json]'], maxArgs: 3, workspace: true, sandbox: true, json: true,
338
+ flags: [DATE],
339
+ notes: ['和家长端同一条路;到 vault.keepScore 星的话题,记账时摘要才进日记'],
340
+ examples: ['cotutor rate math-tutor 1620-1 4'],
341
+ },
342
+ {
343
+ name: 'bookkeep', group: 'debug', summary: '记账:把这天的话题写进日记',
344
+ usage: ['cotutor bookkeep <老师> [--date <日期>] [--thread <话题>] [--quiet] [--json]'], maxArgs: 1, workspace: true, sandbox: true, json: true,
345
+ flags: [DATE, { name: 'thread', value: '<话题>', desc: '只记这个话题' }, { name: 'quiet', desc: '不打印过程' }],
346
+ notes: ['每个还没记过的话题起一轮记账,应用写进笔记文件夹的日记'],
347
+ effects: ['花钱', '写孩子的笔记文件夹'],
348
+ examples: ['cotutor bookkeep math-tutor'],
349
+ },
350
+ ];
351
+ export const commandSpec = (name) => COMMANDS.find((c) => c.name === name);
352
+ /** 所有命令里带值的旗标(parseArgs 要先知道哪些吃下一个词) */
353
+ export const VALUED_FLAGS = [...new Set(['workspace', ...COMMANDS.flatMap((c) => (c.flags ?? []).filter((f) => f.value).map((f) => f.name))])];
@@ -31,7 +31,7 @@ import { gradeOfSemester } from "../../memory/grade.js";
31
31
  import { DIRS } from "./skeleton.js";
32
32
  import { tutorStatuses, readManifest } from "./tutors.js";
33
33
  import { workspaceGradePack } from "./grades.js";
34
- import { CONFIG_FILE, ConfigError, USER_CONFIG, UsageError, assembleWorkspace, expandPath, parseConfig, readJson, redactHome, resolveRoot, } from "../workspace.js";
34
+ import { CONFIG_FILE, ConfigError, USER_CONFIG, UsageError, assembleWorkspace, defaultVaultOf, expandPath, homePath, isInside, parseConfig, readJson, redactHome, resolveRoot, vaultSeparate, } from "../workspace.js";
35
35
  /** 每条不过的要么能跑(cmd)要么要人来(human),没有第三种:新加的检查项忘了写 cmd,也只是交给人,不会让 agent 瞎猜 */
36
36
  function finish(report) {
37
37
  for (const c of report.checks) {
@@ -108,7 +108,7 @@ export async function doctorWorkspace(override, opts = {}) {
108
108
  // 路径写 $HOME/…,和别的字段一样:用户名不传给 agent 的厂商;redactDeep 只换开头,管不到命令中间的路径;sh 与 PowerShell 都展开
109
109
  const quoted = `"${redactHome(root)}"`;
110
110
  const flag = source === 'flag' ? ` --workspace ${quoted}` : '';
111
- const upgrade = `cotutor upgrade${flag}`;
111
+ const refresh = `cotutor refresh${flag}`;
112
112
  // ---- cotutor.json ----
113
113
  let ws = null;
114
114
  const configFile = join(root, CONFIG_FILE);
@@ -147,7 +147,7 @@ export async function doctorWorkspace(override, opts = {}) {
147
147
  teachingPack = await workspaceGradePack(root);
148
148
  push({ name: 'teachingPack', ok: true, required: true, detail: `${teachingPack.grade} 年级教学包 · ${redactHome(teachingPack.dir)}` });
149
149
  if (!manifest.teachingPack)
150
- push({ name: 'teachingPack.origin', ok: false, required: false, detail: '旧版出厂内容按二年级来源读取,尚未补记;孩子档案不变', fix: 'cotutor upgrade 补记来源,保留自定义文件与原 hash', cmd: upgrade });
150
+ push({ name: 'teachingPack.origin', ok: false, required: false, detail: '旧版出厂内容按二年级来源读取,尚未补记;孩子档案不变', fix: 'cotutor refresh 补记来源,保留自定义文件与原 hash', cmd: refresh });
151
151
  }
152
152
  catch (err) {
153
153
  push({ name: 'teachingPack', ok: false, required: true, detail: err.message, fix: '检查 .cotutor/shipped.json 的 teachingPack 与本包 grades/;恢复原来源记录或安装包含该年级的版本,不要改成其他年级绕过' });
@@ -160,8 +160,8 @@ export async function doctorWorkspace(override, opts = {}) {
160
160
  ok: gaps.length === 0,
161
161
  required: false,
162
162
  detail: gaps.length ? `${gaps.length} 项要改:${gaps.map((g) => g.detail).join(';')}` : 'cotutor.json 有出厂模板里的全部老师、运行时与旗标,也没有不再用的段',
163
- fix: gaps.length ? 'cotutor upgrade --config --dry-run 先看,再 cotutor upgrade --config 改(只加缺的、删不再用的出厂段,你改过的值不动)' : undefined,
164
- cmd: gaps.length ? `cotutor upgrade --config${flag}` : undefined,
163
+ fix: gaps.length ? 'cotutor refresh --config --dry-run 先看,再 cotutor refresh --config 改(只加缺的、删不再用的出厂段,你改过的值不动)' : undefined,
164
+ cmd: gaps.length ? `cotutor refresh --config${flag}` : undefined,
165
165
  });
166
166
  }
167
167
  // ---- 骨架 ----
@@ -180,7 +180,7 @@ export async function doctorWorkspace(override, opts = {}) {
180
180
  // 老布局(2026-10-30 之前):老师文件在 .claude/agents/、技能在 .claude/skills/,现在都读不到了
181
181
  const legacy = await legacyLayout(root);
182
182
  if (legacy.length)
183
- push({ name: 'layout', ok: false, required: true, detail: `${legacy.join(' ')} 是老位置:老师文件现在在 agents/<老师>.md,技能在 ${SKILLS_DIR}/`, fix: 'cotutor upgrade(原样挪过去,不改内容;新位置已有同名的不挪、会点名)', cmd: upgrade });
183
+ push({ name: 'layout', ok: false, required: true, detail: `${legacy.join(' ')} 是老位置:老师文件现在在 agents/<老师>.md,技能在 ${SKILLS_DIR}/`, fix: 'cotutor refresh(原样挪过去,不改内容;新位置已有同名的不挪、会点名)', cmd: refresh });
184
184
  // 家规(根的 QWEN.md)2026-09-15 起不出厂:有就是家长自建的共同规矩,提一句;没有不缺
185
185
  const rulesThere = (await Promise.all(['QWEN.md'].map(async (f) => ((await statOrNull(join(root, f))) ? f : null)))).filter(Boolean);
186
186
  push({ name: 'rules', ok: true, required: false, detail: rulesThere.length ? `家长自建的家规 ${rulesThere.join(' ')} 在,所有老师每轮都读` : '没有根目录的 QWEN.md(不需要:老师要知道的都在老师文件与技能里)' });
@@ -215,7 +215,7 @@ export async function doctorWorkspace(override, opts = {}) {
215
215
  });
216
216
  const st = statuses.get(name);
217
217
  if (!st)
218
- push({ name: `tutor.${name}.origin`, ok: true, required: false, detail: teachingPack ? '自家加的老师(不是出厂件,upgrade 不碰)' : '出厂来源无法确认,先修复 teachingPack 检查项' });
218
+ push({ name: `tutor.${name}.origin`, ok: true, required: false, detail: teachingPack ? '自家加的老师(不是出厂件,refresh 不碰)' : '出厂来源无法确认,先修复 teachingPack 检查项' });
219
219
  if (st) {
220
220
  const label = { latest: '出厂件,最新', upgradable: `出厂件,基于 ${st.basedOn},包已更新`, custom: `自定义(基于 ${st.basedOn})`, untracked: '自定义(没有出厂记录)', missing: '缺', broken: '读不到' };
221
221
  push({
@@ -223,8 +223,8 @@ export async function doctorWorkspace(override, opts = {}) {
223
223
  ok: st.state !== 'upgradable',
224
224
  required: false,
225
225
  detail: label[st.state],
226
- fix: st.state === 'upgradable' ? 'cotutor upgrade 换新版' : undefined,
227
- cmd: st.state === 'upgradable' ? upgrade : undefined,
226
+ fix: st.state === 'upgradable' ? 'cotutor refresh 换新版' : undefined,
227
+ cmd: st.state === 'upgradable' ? refresh : undefined,
228
228
  });
229
229
  }
230
230
  const home = join(ws.dirs.agents, name);
@@ -293,11 +293,11 @@ export async function doctorWorkspace(override, opts = {}) {
293
293
  const { CLAUDE_SKILLS_LINK, skillStatuses } = await import("./skills.js");
294
294
  for (const sk of teachingPack ? await skillStatuses(root) : []) {
295
295
  const label = { latest: sk.machine ? '机器件,最新' : '出厂件,最新', upgradable: sk.machine ? '机器件,和包里不一样(改过或包已更新)' : `出厂件,基于 ${sk.basedOn},包已更新`, custom: `自定义(基于 ${sk.basedOn})`, untracked: '自定义(没有出厂记录)', missing: sk.machine ? '缺(机器件,老师或家长的技能靠它)' : '缺' };
296
- push({ name: `skill.${sk.name}`, ok: sk.state !== 'missing' && sk.state !== 'upgradable', required: Boolean(sk.machine), detail: `${SKILLS_DIR}/${sk.name}/:${label[sk.state]}`, fix: sk.state === 'missing' ? 'cotutor init 补拷' : sk.state === 'upgradable' ? 'cotutor upgrade 换新版' : undefined, cmd: sk.state === 'missing' ? initCmd() : sk.state === 'upgradable' ? upgrade : undefined });
296
+ push({ name: `skill.${sk.name}`, ok: sk.state !== 'missing' && sk.state !== 'upgradable', required: Boolean(sk.machine), detail: `${SKILLS_DIR}/${sk.name}/:${label[sk.state]}`, fix: sk.state === 'missing' ? 'cotutor init 补拷' : sk.state === 'upgradable' ? 'cotutor refresh 换新版' : undefined, cmd: sk.state === 'missing' ? initCmd() : sk.state === 'upgradable' ? refresh : undefined });
297
297
  }
298
298
  // Claude Code 只认 .claude/skills:那里该是指向 .agents/skills 的链;老师用不到,家长在 workspace 里用 Claude Code 才要
299
299
  const linked = (await lstat(join(root, CLAUDE_SKILLS_LINK)).catch(() => null))?.isSymbolicLink() ?? false;
300
- push({ name: 'skills.claude', ok: linked, required: false, detail: linked ? `${CLAUDE_SKILLS_LINK} 链到 ${SKILLS_DIR}(Claude Code 读的是同一份)` : `${CLAUDE_SKILLS_LINK} 不是链,在 workspace 里用 Claude Code 看不到 ${SKILLS_DIR}/ 的技能`, fix: linked ? undefined : 'cotutor upgrade 建链(是实体目录的话先把里面的挪进 .agents/skills/ 再删掉)', cmd: linked ? undefined : upgrade });
300
+ push({ name: 'skills.claude', ok: linked, required: false, detail: linked ? `${CLAUDE_SKILLS_LINK} 链到 ${SKILLS_DIR}(Claude Code 读的是同一份)` : `${CLAUDE_SKILLS_LINK} 不是链,在 workspace 里用 Claude Code 看不到 ${SKILLS_DIR}/ 的技能`, fix: linked ? undefined : 'cotutor refresh 建链(是实体目录的话先把里面的挪进 .agents/skills/ 再删掉)', cmd: linked ? undefined : refresh });
301
301
  // ---- activity:老师的家里 agents/<老师>/activities/<名>.md;出厂的看新旧,每份都过一遍 activity check ----
302
302
  {
303
303
  const { activityStatuses, checkActivities } = await import("./activities.js");
@@ -316,7 +316,7 @@ export async function doctorWorkspace(override, opts = {}) {
316
316
  }
317
317
  for (const r of await checkActivities(root, ws.config.tutors)) {
318
318
  const key = `${r.tutor}/${r.name}`;
319
- // 老 workspace 改过老师文件:upgrade 只报 diff,里面同名那节还在,两处说法会打架
319
+ // 老 workspace 改过老师文件:refresh 只报 diff,里面同名那节还在,两处说法会打架
320
320
  const agentText = await readFile(tutorFilePath(ws.dirs.agents, r.tutor), 'utf8').catch(() => '');
321
321
  if (new RegExp(`^#{2,4}\\s*${r.name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\s*$`, 'm').test(agentText))
322
322
  push({ name: `activity.${key}.dup`, ok: false, required: false, detail: `agents/${r.tutor}.md 里还有一节「${r.name}」,和 activity 文件两处说法会打架`, fix: `删掉老师文件里那一节(做法以 agents/${r.tutor}/activities/${r.name}.md 为准)` });
@@ -328,8 +328,8 @@ export async function doctorWorkspace(override, opts = {}) {
328
328
  ok: !fixes.length && st?.state !== 'upgradable',
329
329
  required: false,
330
330
  detail: fixes.length ? `${origin};${fixes.length} 条要改,第一条:${fixes[0].line ? `第 ${fixes[0].line} 行 ` : ''}${fixes[0].text}` : origin,
331
- fix: fixes.length ? `cotutor activity check ${r.tutor} ${r.name} 看全` : st?.state === 'upgradable' ? 'cotutor upgrade 换新版' : undefined,
332
- cmd: !fixes.length && st?.state === 'upgradable' ? upgrade : undefined,
331
+ fix: fixes.length ? `cotutor activity check ${r.tutor} ${r.name} 看全` : st?.state === 'upgradable' ? 'cotutor refresh 换新版' : undefined,
332
+ cmd: !fixes.length && st?.state === 'upgradable' ? refresh : undefined,
333
333
  });
334
334
  }
335
335
  }
@@ -343,15 +343,15 @@ export async function doctorWorkspace(override, opts = {}) {
343
343
  push({ name: 'theme.manifest', ok: true, required: false, detail: `themes/${name}/:${Object.keys(t.manifest.tints).length} 个底色槽、${Object.keys(t.manifest.looks).length} 个字形槽、${Object.keys(t.manifest.pens).length} 支笔,default = ${t.manifest.default}` });
344
344
  }
345
345
  catch (err) {
346
- push({ name: 'theme.manifest', ok: false, required: false, detail: `themes/${name}/ 用不了(服务退回出厂 default):${err instanceof Error ? err.message : String(err)}`, fix: name === 'default' ? 'cotutor init 补拷(已有的不动)或 cotutor upgrade' : `修 themes/${name}/theme.json 与 kid.css,或把 cotutor.json 的 kid.theme 改回 default`, cmd: name === 'default' ? initCmd() : undefined });
346
+ push({ name: 'theme.manifest', ok: false, required: false, detail: `themes/${name}/ 用不了(服务退回出厂 default):${err instanceof Error ? err.message : String(err)}`, fix: name === 'default' ? 'cotutor init 补拷(已有的不动)或 cotutor refresh' : `修 themes/${name}/theme.json 与 kid.css,或把 cotutor.json 的 kid.theme 改回 default`, cmd: name === 'default' ? initCmd() : undefined });
347
347
  }
348
348
  for (const th of teachingPack ? await themeStatuses(root) : []) {
349
349
  if (!th.shipped) {
350
- push({ name: `theme.${th.name}.origin`, ok: true, required: false, detail: `themes/${th.name}/:自家的主题(不是出厂件,upgrade 不碰)` });
350
+ push({ name: `theme.${th.name}.origin`, ok: true, required: false, detail: `themes/${th.name}/:自家的主题(不是出厂件,refresh 不碰)` });
351
351
  continue;
352
352
  }
353
353
  const label = { latest: '出厂件,最新', upgradable: `出厂件,基于 ${th.basedOn},包已更新`, custom: `自定义(基于 ${th.basedOn})`, untracked: '自定义(没有出厂记录)', missing: '缺' };
354
- push({ name: `theme.${th.name}.origin`, ok: th.state !== 'missing' && th.state !== 'upgradable', required: false, detail: `themes/${th.name}/:${label[th.state]}`, fix: th.state === 'missing' ? 'cotutor init 补拷' : th.state === 'upgradable' ? 'cotutor upgrade 换新版' : undefined, cmd: th.state === 'missing' ? initCmd() : th.state === 'upgradable' ? upgrade : undefined });
354
+ push({ name: `theme.${th.name}.origin`, ok: th.state !== 'missing' && th.state !== 'upgradable', required: false, detail: `themes/${th.name}/:${label[th.state]}`, fix: th.state === 'missing' ? 'cotutor init 补拷' : th.state === 'upgradable' ? 'cotutor refresh 换新版' : undefined, cmd: th.state === 'missing' ? initCmd() : th.state === 'upgradable' ? refresh : undefined });
355
355
  }
356
356
  }
357
357
  }
@@ -402,6 +402,21 @@ export async function doctorWorkspace(override, opts = {}) {
402
402
  const n = there ? (await readdir(cap).catch(() => [])).filter((d) => /^\d{4}-\d{2}-\d{2}$/.test(d)).length : 0;
403
403
  push({ name: 'captures', ok: inside && writable, required: false, detail: !inside ? `${redactHome(cap)} 在 workspace 根以外,页面看不到照片(/api/kid/image 只给根以内的)` : !writable ? `${redactHome(cap)} 写不了(最近的已有目录 ${redactHome(dir)} 没有写权限)` : there ? `${redactHome(cap)}:${n} 天的作业照片` : `${redactHome(cap)} 还没有(第一张照片时建)`, fix: !inside ? '把 cotutor.json 的 paths.captures 改回根以内(缺省 captures)' : !writable ? '改目录权限,或换 paths.captures' : undefined });
404
404
  }
405
+ // ---- vault 与 workspace 分开(2026-11-09):旧版 init 不写 paths.vault,vault 就是 workspace 根;照旧能用,这里说怎么分。sandbox 的 vault 本来就拷在里面,不查 ----
406
+ if (!ws.sandbox) {
407
+ const vault = ws.paths.vault;
408
+ const separate = vaultSeparate(ws.root, vault);
409
+ const rootInVault = !separate && !isInside(vault, ws.root);
410
+ push({
411
+ name: 'vault.separate',
412
+ ok: separate,
413
+ required: false,
414
+ detail: separate ? `笔记文件夹(vault)在 ${redactHome(vault)},和 cotutor 文件夹分开` : rootInVault ? `孩子的 cotutor 文件夹在笔记文件夹(${redactHome(vault)})里面` : vault === ws.root ? '笔记文件夹(vault)就是孩子的 cotutor 文件夹(旧版 init 的布局):照旧能用,但用 Obsidian 打开满是程序的文件,恢复备份会连家长的笔记一起换掉' : `笔记文件夹(vault)在孩子的 cotutor 文件夹里(${redactHome(vault)})`,
415
+ fix: separate ? undefined : rootInVault
416
+ ? '把孩子的 cotutor 文件夹挪到笔记文件夹外面(挪了要再 cotutor service install),或者笔记文件夹换一个地方'
417
+ : `想分开:cotutor service stop,建笔记文件夹 ${homePath(defaultVaultOf(ws.root))},把档案、日记/、记忆/、计划/、教材/、参考/ 和你自己的笔记挪过去,cotutor.json 的 paths.vault 写成它,再 cotutor service start`,
418
+ });
419
+ }
405
420
  // ---- vault(《obsidian仓库设计.md》2026-09-17):档案与每位老师的入口文件按 frontmatter 找,原文整篇进上下文包 ----
406
421
  {
407
422
  const { notes, files } = await scanVault(ws);
@@ -0,0 +1,6 @@
1
+ export declare const PACKAGE_INSTALL_MD: string;
2
+ /**
3
+ * 第一行只用 ASCII:Windows PowerShell 5.1 把管道里的 UTF-8 按系统代码页(中文系统是 936)解,
4
+ * 下面的中文会成乱码,这一行还读得懂,告诉助手直接按 UTF-8 读文件。
5
+ */
6
+ export declare function guideText(version: string): string;
@@ -0,0 +1,17 @@
1
+ /**
2
+ * cotutor guide:把随包发的 INSTALL.md 原样打印给替家长安装的 AI 助手。
3
+ * 家长只要打一句「先跑 npx cotutor@latest guide」,不用输网址;走 npm(国内能换镜像),说明和装上的版本对得上。
4
+ * src/app/cli/ 与 dist/app/cli/ 都在包根下三层,和 main.ts 读 package.json 一样往上找。
5
+ */
6
+ import { readFileSync } from 'node:fs';
7
+ import { fileURLToPath } from 'node:url';
8
+ import { redactHome } from "../workspace.js";
9
+ export const PACKAGE_INSTALL_MD = fileURLToPath(new URL('../../../INSTALL.md', import.meta.url));
10
+ /**
11
+ * 第一行只用 ASCII:Windows PowerShell 5.1 把管道里的 UTF-8 按系统代码页(中文系统是 936)解,
12
+ * 下面的中文会成乱码,这一行还读得懂,告诉助手直接按 UTF-8 读文件。
13
+ */
14
+ export function guideText(version) {
15
+ const head = `# cotutor ${version} INSTALL.md (for the AI assistant installing cotutor; if the text below is garbled, read this file as UTF-8: ${redactHome(PACKAGE_INSTALL_MD)} , $HOME is the home directory)`;
16
+ return `${head}\n\n${readFileSync(PACKAGE_INSTALL_MD, 'utf8')}`;
17
+ }
@@ -11,6 +11,8 @@ export interface InitResult {
11
11
  export interface InitOptions {
12
12
  slug: string;
13
13
  dir?: string;
14
+ /** vault 放哪(家长已有的 Obsidian 库);缺省 workspace 旁边的 <目录名>-vault。只在新建 cotutor.json 时用 */
15
+ vault?: string;
14
16
  name?: string;
15
17
  port?: number;
16
18
  /** 出厂教学包年级,不是孩子档案的覆盖值。 */