ai-growth 1.1.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 (75) hide show
  1. package/.env.example +19 -0
  2. package/AGENTS.md +232 -0
  3. package/IMPLEMENTATION_PLAN.md +207 -0
  4. package/LICENSE +21 -0
  5. package/PRODUCT_SPEC.md +1776 -0
  6. package/README.md +384 -0
  7. package/SKILL.md +121 -0
  8. package/START-HERE.md +156 -0
  9. package/dist/api/app.js +206 -0
  10. package/dist/api/server.js +42 -0
  11. package/dist/cli/main.js +799 -0
  12. package/dist/core/actions.js +264 -0
  13. package/dist/core/context.js +7 -0
  14. package/dist/core/directions.js +26 -0
  15. package/dist/core/evidence.js +155 -0
  16. package/dist/core/goals.js +188 -0
  17. package/dist/core/lifeContext.js +41 -0
  18. package/dist/core/memory.js +35 -0
  19. package/dist/core/metrics.js +74 -0
  20. package/dist/core/profile.js +95 -0
  21. package/dist/core/progress.js +244 -0
  22. package/dist/core/prompts/index.js +40 -0
  23. package/dist/core/replan.js +100 -0
  24. package/dist/core/rollover.js +68 -0
  25. package/dist/core/rolloverTypes.js +1 -0
  26. package/dist/core/stateMachine.js +61 -0
  27. package/dist/core/status.js +57 -0
  28. package/dist/core/types.js +9 -0
  29. package/dist/daemon/main.js +64 -0
  30. package/dist/db/cli.js +19 -0
  31. package/dist/db/client.js +36 -0
  32. package/dist/db/migrate.js +34 -0
  33. package/dist/db/migrations/V1__init.sql +249 -0
  34. package/dist/device/card.js +257 -0
  35. package/dist/device/deck.js +157 -0
  36. package/dist/device/fontCoverage.js +162 -0
  37. package/dist/device/http.js +170 -0
  38. package/dist/device/png.js +111 -0
  39. package/dist/device/provision.js +185 -0
  40. package/dist/device/render.js +194 -0
  41. package/dist/device/renderCard.js +48 -0
  42. package/dist/device/report.js +146 -0
  43. package/dist/device/serial.js +201 -0
  44. package/dist/device/token.js +78 -0
  45. package/dist/device/types.js +2 -0
  46. package/dist/mcp/ctx.js +48 -0
  47. package/dist/mcp/server.js +15 -0
  48. package/dist/mcp/tools.js +586 -0
  49. package/dist/passport/builder.js +83 -0
  50. package/dist/passport/state.js +81 -0
  51. package/dist/passport/sync.js +61 -0
  52. package/dist/reminder/notify.js +30 -0
  53. package/dist/reminder/scheduler.js +87 -0
  54. package/dist/reminder/service.js +102 -0
  55. package/dist/shared/config.js +50 -0
  56. package/dist/shared/result.js +27 -0
  57. package/dist/shared/time.js +67 -0
  58. package/dist/shared/util.js +21 -0
  59. package/dist/ui/app.js +500 -0
  60. package/dist/ui/index.html +38 -0
  61. package/dist/ui/style.css +367 -0
  62. package/docs/images/cards.png +0 -0
  63. package/docs/images/cover.png +0 -0
  64. package/launchd/com.lairey.ai-growth.daemon.plist +45 -0
  65. package/llms.txt +52 -0
  66. package/package.json +79 -0
  67. package/scripts/check-device-cards.mjs +177 -0
  68. package/scripts/doctor.sh +194 -0
  69. package/scripts/gen-device-assets.mjs +188 -0
  70. package/scripts/install.sh +291 -0
  71. package/scripts/preview-cards.mjs +120 -0
  72. package/scripts/reset-dev-data.sh +92 -0
  73. package/scripts/smoke-device.mjs +401 -0
  74. package/scripts/uninstall.sh +63 -0
  75. package/server.json +56 -0
@@ -0,0 +1,799 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * ai-growth CLI — 让 Agent 与人都能直接驱动这套系统。
4
+ *
5
+ * 设计要点:
6
+ * 1. **自描述**:`ai-growth help --json` 会输出全部命令、参数与规则,
7
+ * Agent 不需要读源码就知道怎么用;`ai-growth agent-info --json` 给出安装/接入所需的全部路径。
8
+ * 2. **统一契约**:所有命令的输出与 MCP 工具一致 —— `{ ok, data, error? }`。
9
+ * 3. **非 TTY 自动 JSON**:被管道/子进程调用时默认输出 JSON,避免 Agent 忘记加 --json。
10
+ * 4. 所有读写都走 Core Service,与 MCP / 面板完全同一条路径。
11
+ */
12
+ import { readFileSync, existsSync, writeFileSync } from 'node:fs';
13
+ import { join, dirname, resolve } from 'node:path';
14
+ import { fileURLToPath } from 'node:url';
15
+ import { spawn, execFileSync } from 'node:child_process';
16
+ import { networkInterfaces } from 'node:os';
17
+ import { loadConfig } from '../shared/config.js';
18
+ import { openDb, dbPath } from '../db/client.js';
19
+ import { makeCtx } from '../mcp/ctx.js';
20
+ import { ok, err, DomainError } from '../shared/result.js';
21
+ import { growthStatus } from '../core/status.js';
22
+ import { getTodayPlan, completeAction, skipAction, delayAction } from '../core/actions.js';
23
+ import { listStages, getCurrentStage, listGoals } from '../core/goals.js';
24
+ import { listGrowthEvents, listEvidence } from '../core/evidence.js';
25
+ import { runDailyRollover } from '../core/rollover.js';
26
+ import { listRemindersForDate } from '../reminder/service.js';
27
+ import { getActiveLifeContext } from '../core/lifeContext.js';
28
+ import { computeMetrics } from '../core/metrics.js';
29
+ import { renderProgress } from '../core/progress.js';
30
+ import { todayIn, resolveTimezone } from '../core/profile.js';
31
+ import { latestSnapshot } from '../passport/state.js';
32
+ import { isDeviceLanEnabled, setDeviceLanEnabled, getOrCreateDeviceToken, rotateDeviceToken } from '../device/token.js';
33
+ import { provisionDevice, probeService } from '../device/provision.js';
34
+ const VERSION = '1.1.0';
35
+ /** 包根目录:dist/cli/main.js → 上两级 */
36
+ const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..', '..');
37
+ function sh(cmd, args) {
38
+ try {
39
+ return execFileSync(cmd, args, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
40
+ }
41
+ catch {
42
+ return '';
43
+ }
44
+ }
45
+ function detectInstall() {
46
+ // 注意:Node 默认解析符号链接,import.meta.url 报的是**真实路径**。
47
+ // 因此不能用"路径里是否含 node_modules/ai-growth"判断,
48
+ // 必须比较「全局入口的 realpath」与「当前代码根目录」。
49
+ const selfPath = fileURLToPath(import.meta.url);
50
+ const invokedAs = process.argv[1] ?? '';
51
+ const globalPrefix = sh('npm', ['prefix', '-g']);
52
+ const globalBin = globalPrefix ? join(globalPrefix, 'bin') : '';
53
+ const globalPkgDir = globalPrefix ? join(globalPrefix, 'lib', 'node_modules', 'ai-growth') : '';
54
+ const globalCliPath = globalPkgDir ? join(globalPkgDir, 'dist', 'cli', 'main.js') : '';
55
+ let globalCliResolvesHere = false;
56
+ if (globalCliPath && existsSync(globalCliPath)) {
57
+ const real = sh('node', ['-e', `process.stdout.write(require('fs').realpathSync(${JSON.stringify(globalCliPath)}))`]);
58
+ globalCliResolvesHere = !!real && resolve(dirname(dirname(dirname(real)))) === resolve(ROOT);
59
+ }
60
+ let kind = 'checkout';
61
+ if (globalCliResolvesHere) {
62
+ // 全局入口存在;再看它是软链到本检出,还是独立的一份实体
63
+ const realPkg = globalPkgDir
64
+ ? sh('node', ['-e', `process.stdout.write(require('fs').realpathSync(${JSON.stringify(globalPkgDir)}))`])
65
+ : '';
66
+ kind = realPkg && resolve(realPkg) === resolve(ROOT) ? 'linked' : 'installed';
67
+ }
68
+ const invokedVia = globalBin && invokedAs.startsWith(globalBin) ? 'global-command' : 'direct-path';
69
+ const pathDirs = (process.env.PATH ?? '').split(':');
70
+ return {
71
+ kind, selfPath, invokedVia, globalPrefix, globalBin, globalCliPath, globalCliResolvesHere,
72
+ binOnPath: pathDirs.includes(globalBin),
73
+ };
74
+ }
75
+ function parseArgv(argv) {
76
+ const args = [];
77
+ const flags = {};
78
+ for (let i = 0; i < argv.length; i++) {
79
+ const a = argv[i];
80
+ if (a.startsWith('--')) {
81
+ const eq = a.indexOf('=');
82
+ if (eq > 0) {
83
+ flags[a.slice(2, eq)] = a.slice(eq + 1);
84
+ }
85
+ else if (argv[i + 1] !== undefined && !argv[i + 1].startsWith('-')) {
86
+ flags[a.slice(2)] = argv[++i];
87
+ }
88
+ else {
89
+ flags[a.slice(2)] = true;
90
+ }
91
+ }
92
+ else if (a === '-h') {
93
+ flags.help = true;
94
+ }
95
+ else if (!a.startsWith('-')) {
96
+ args.push(a);
97
+ }
98
+ }
99
+ return { cmd: args[0] ?? 'help', args: args.slice(1), flags };
100
+ }
101
+ // ---------------------------------------------------------------- 输出
102
+ function wantsJson(flags) {
103
+ if (flags.json === true)
104
+ return true;
105
+ if (flags.json === 'false' || flags['no-json'] === true)
106
+ return false; // 逃生口
107
+ // 非 TTY(被 Agent/管道调用)默认 JSON,避免忘记加 --json
108
+ return !process.stdout.isTTY;
109
+ }
110
+ function emit(result, flags, human) {
111
+ if (wantsJson(flags)) {
112
+ process.stdout.write(JSON.stringify(result, null, 2) + '\n');
113
+ if (!result.ok)
114
+ process.exitCode = 1;
115
+ return;
116
+ }
117
+ if (!result.ok) {
118
+ process.stderr.write(`✗ ${result.error?.code}: ${result.error?.message}\n`);
119
+ process.exitCode = 1;
120
+ return;
121
+ }
122
+ process.stdout.write((human ? human() : JSON.stringify(result.data, null, 2)) + '\n');
123
+ }
124
+ function run(fn, flags, human) {
125
+ try {
126
+ const data = fn();
127
+ emit(ok(data), flags, human ? () => human(data) : undefined);
128
+ }
129
+ catch (e) {
130
+ if (e instanceof DomainError)
131
+ emit(err(e.code, e.message, e.retryable), flags);
132
+ else
133
+ emit(err('INTERNAL', e instanceof Error ? e.message : String(e)), flags);
134
+ }
135
+ }
136
+ /**
137
+ * run 的异步版。配置设备要真的走串口(异步 IO + 超时等待),同步签名套不住,
138
+ * 所以错误映射必须在这里再做一遍,保持与 run 完全一致的输出契约。
139
+ */
140
+ function runAsync(fn, flags, human) {
141
+ fn().then((data) => emit(ok(data), flags, human ? () => human(data) : undefined), (e) => {
142
+ if (e instanceof DomainError)
143
+ emit(err(e.code, e.message, e.retryable), flags);
144
+ else
145
+ emit(err('INTERNAL', e instanceof Error ? e.message : String(e)), flags);
146
+ });
147
+ }
148
+ const COMMANDS = [
149
+ { name: 'help', summary: '列出全部命令、参数与使用规则(自描述入口)', usage: 'ai-growth help [--json]', example: 'ai-growth help --json' },
150
+ { name: 'agent-info', summary: 'Agent 引导信息:安装路径、数据目录、MCP 配置、Skill 位置、下一步动作', usage: 'ai-growth agent-info [--json]', example: 'ai-growth agent-info --json' },
151
+ { name: 'info', summary: '环境与运行状态:版本、数据目录、时区、安装形态与更新方式', usage: 'ai-growth info [--json]' },
152
+ { name: 'where', summary: '当前跑的是哪一份代码、数据在哪、怎么更新(开发/生产统一入口)', usage: 'ai-growth where [--json]' },
153
+ { name: 'status', summary: '当前成长状态与建议的下一步操作(等价于 MCP growth_status)', usage: 'ai-growth status [--json]' },
154
+ { name: 'start', summary: '「开启今日任务」的确定性入口:跨天检查 + 状态 + 今日计划 + 明确该做什么(未访谈时不会生成任务)', usage: 'ai-growth start [--json]', example: 'ai-growth start --json' },
155
+ { name: 'today', summary: '今日计划:Main Quest + Daily Actions + 提醒', usage: 'ai-growth today [--json]' },
156
+ { name: 'goals', summary: '主线目标与阶段计划', usage: 'ai-growth goals [--json]' },
157
+ { name: 'timeline', summary: '成长记录与证据流', usage: 'ai-growth timeline [--json] [--limit N]' },
158
+ { name: 'passport', summary: 'AI Passport 快照', usage: 'ai-growth passport [--json]' },
159
+ { name: 'progress', summary: '产出带样式的进度报告:阶段轨道 + 进度条 + Mermaid 甘特图 + 事实表(可 --out 存成 .md/.html)', usage: 'ai-growth progress [--html] [--out FILE]', example: 'ai-growth progress --no-json' },
160
+ { name: 'metrics', summary: '本地规划指标(用于改进推荐,不是自律分)', usage: 'ai-growth metrics [--json] [--days N]' },
161
+ { name: 'rollover', summary: '执行跨天检查(幂等):昨日未完成 Daily → 过期,Main → 待重排', usage: 'ai-growth rollover [--json]', writes: true },
162
+ { name: 'complete', summary: '完成一个任务', usage: 'ai-growth complete <actionId> [--note "..."]', args: ['actionId'], writes: true, example: 'ai-growth complete ab12-... --note "已跑通"' },
163
+ { name: 'skip', summary: '跳过任务(可带原因)', usage: 'ai-growth skip <actionId> [--reason "..."]', args: ['actionId'], writes: true },
164
+ { name: 'delay', summary: '延期任务(不跨天作废,与 Daily 的过期不同)', usage: 'ai-growth delay <actionId> [--reason "..."]', args: ['actionId'], writes: true },
165
+ { name: 'mcp-config', summary: '输出可直接粘贴的 MCP 配置(含绝对路径)', usage: 'ai-growth mcp-config [--json]' },
166
+ { name: 'skill', summary: '输出 SKILL.md 全文 —— Agent 应按它行事', usage: 'ai-growth skill [--json]' },
167
+ { name: 'doctor', summary: '环境自检(依赖/构建/数据库/launchd/MCP/面板)', usage: 'ai-growth doctor' },
168
+ { name: 'device', summary: 'AI Passport 设备接入:`device` 看局域网状态/本机地址/令牌;`device setup` 一条命令配好 Wi-Fi 并自动确认取卡成功', usage: 'ai-growth device [setup --ssid <Wi-Fi名> [--pass <密码>] [--port <串口>] [--no-verify] [--no-enable-lan]] [--rotate] [--json]', writes: true, example: 'ai-growth device setup --ssid "我家的WiFi" --pass "12345678"' },
169
+ { name: 'init', summary: '初始化数据库并执行 migration(幂等)', usage: 'ai-growth init', writes: true },
170
+ { name: 'migrate', summary: 'init 的别名 —— 更新后跑它执行数据库迁移(幂等、不丢数据)', usage: 'ai-growth migrate', writes: true },
171
+ { name: 'serve', summary: '前台启动常驻服务(调度 + 面板 API)', usage: 'ai-growth serve' },
172
+ { name: 'version', summary: '打印版本', usage: 'ai-growth version' },
173
+ ];
174
+ /** 产品铁律:Agent 驱动本系统前必须知道 */
175
+ const RULES = [
176
+ 'AI 管路线,人决定目的地:不得替人决定人生方向。',
177
+ '首次使用必须先做访谈(onboarding),不允许直接生成任务。',
178
+ 'Daily Action 只当天有效,次日自动过期,绝不顺延成跨天债务,也不 clone 到第二天。',
179
+ 'Main Quest 未完成不作废,进入 PENDING_REPLAN 由 Replan 解决。',
180
+ '默认负荷 1 个 Main Quest + 0~2 个 Daily Action;长期提高负荷必须用户确认。',
181
+ '不产生虚构属性值 / 掌握度百分比 / 自律分;进度只用可核对事实(如 Stage 3 / 6、Evidence)。',
182
+ '所有状态变更必须经 Core Service;不要直接写数据库。',
183
+ '"哪一天"按配置时区计算,绝不按 UTC 截断。',
184
+ '不要为了展示功能而生成大量任务;宁可少而准。',
185
+ ];
186
+ const NEXT_STEPS = [
187
+ '1) ai-growth agent-info --json # 拿到数据目录、MCP 配置片段、Skill 路径',
188
+ '2) ai-growth start --json # 启动今日:跨天检查 + 状态 + 今日计划 + 该做什么(推荐入口)',
189
+ '3) 按 instruction / suggestedNextOperation 行动(先访谈 / 解决 Replan / 生成今日计划 / 继续推进)',
190
+ '4) 需要完整工具集时接入 MCP(ai-growth mcp-config --json)',
191
+ ];
192
+ // ---------------------------------------------------------------- 主流程
193
+ function main() {
194
+ const { cmd, args, flags } = parseArgv(process.argv.slice(2));
195
+ switch (cmd) {
196
+ case 'help':
197
+ return run(() => ({
198
+ name: 'ai-growth',
199
+ version: VERSION,
200
+ description: 'Agent-native、Local-first 的个人成长执行系统:AI 维护计划,人保留目标与确认权。',
201
+ outputContract: '所有命令加 --json 输出 { ok, data, error? };非 TTY(被 Agent/管道调用)时默认即 JSON,加 --no-json 可强制人类可读输出。',
202
+ rules: RULES,
203
+ commands: COMMANDS,
204
+ nextStepsForAgent: NEXT_STEPS,
205
+ docs: ['AGENTS.md', 'README.md', 'SKILL.md', 'PRODUCT_SPEC.md'],
206
+ }), flags, (d) => {
207
+ const lines = [
208
+ `ai-growth ${d.version} — ${d.description}`,
209
+ '',
210
+ '命令:',
211
+ ...COMMANDS.map((c) => ` ${c.name.padEnd(12)} ${c.summary}${c.writes ? ' [写入]' : ''}`),
212
+ '',
213
+ '产品铁律(Agent 必读):',
214
+ ...RULES.map((r) => ` · ${r}`),
215
+ '',
216
+ 'Agent 上手步骤:',
217
+ ...NEXT_STEPS.map((s) => ` ${s}`),
218
+ ];
219
+ return lines.join('\n');
220
+ });
221
+ case 'version':
222
+ return run(() => ({ name: 'ai-growth', version: VERSION }), flags, (d) => `ai-growth ${d.version}`);
223
+ case 'agent-info':
224
+ return run(() => agentInfo(flags), flags, (d) => agentInfoHuman(d));
225
+ case 'info':
226
+ return run(() => envInfo(), flags, (d) => envInfoHuman(d));
227
+ case 'where': {
228
+ const inst = detectInstall();
229
+ return run(() => ({
230
+ kind: inst.kind,
231
+ kindLabel: INSTALL_LABEL[inst.kind],
232
+ codePath: inst.selfPath,
233
+ invokedVia: inst.invokedVia,
234
+ globalCommand: inst.globalBin ? join(inst.globalBin, 'ai-growth') : null,
235
+ globalCliPath: inst.globalCliPath || null,
236
+ globalCommandPointsHere: inst.globalCliResolvesHere,
237
+ dataDir: loadConfig().dataDir,
238
+ dataIndependentOfCode: true,
239
+ note: 'CLI / MCP / daemon / 面板 都指向「正在运行的这一份」代码。更新就是替换这一份;数据在 ~/.ai-growth,与代码解耦。',
240
+ howToUpdate: updateSteps(inst.kind),
241
+ }), flags, (d) => [
242
+ `安装形态:${d.kindLabel}`,
243
+ `代码位置:${d.codePath}`,
244
+ `调用方式:${d.invokedVia === 'global-command' ? '全局命令' : '直接指定路径'}`,
245
+ d.globalCommand ? `全局命令:${d.globalCommand}${d.globalCommandPointsHere ? ' ✓ 指向这份代码' : ''}` : '',
246
+ `数据目录:${d.dataDir} (与代码解耦,更新不动它)`,
247
+ '',
248
+ '更新方式:',
249
+ ...d.howToUpdate.map((s) => ` ${s}`),
250
+ ].filter(Boolean).join('\n'));
251
+ }
252
+ case 'status':
253
+ return run(() => withCtx((ctx) => growthStatus(ctx)), flags, (d) => statusHuman(d));
254
+ /**
255
+ * 「开启今日任务」的确定性入口:一次调用完成 跨天检查 → 状态 → 今日计划。
256
+ * Agent 听到「开启今日任务 / 今天要做什么 / 开工」时,直接调它即可,不必先猜状态。
257
+ * 注意:未完成访谈时不会生成任何任务,只返回 START_ONBOARDING(核心原则 2)。
258
+ */
259
+ case 'start':
260
+ return run(() => withCtx((ctx) => {
261
+ const rollover = runDailyRollover(ctx);
262
+ const status = growthStatus(ctx);
263
+ const { plan, actions } = getTodayPlan(ctx);
264
+ const needsOnboarding = status.onboarding !== 'COMPLETED';
265
+ return {
266
+ today: todayIn(ctx),
267
+ rollover: { alreadyRan: rollover.alreadyRan, expired: rollover.expiredDailyActionIds.length, pendingReplan: rollover.pendingReplanMainQuestIds.length },
268
+ status,
269
+ plan,
270
+ actions,
271
+ reminders: listRemindersForDate(ctx),
272
+ // 明确告知调用方:现在该干什么。未访谈时不产出任务,先访谈。
273
+ instruction: needsOnboarding
274
+ ? 'ONBOARDING_REQUIRED:先与用户做访谈(不要生成任何任务),完成后再说 GOAL_SETUP'
275
+ : {
276
+ START_ONBOARDING: '先访谈',
277
+ CONTINUE_ONBOARDING: '继续访谈,不要从头问',
278
+ DISCUSS_GOAL: '与用户讨论目标,不要自己编一个',
279
+ RESOLVE_REPLAN: '先解决待重排的主线,再规划今天',
280
+ RUN_DAILY_PLANNING: '生成今日计划(1 Main + 0~2 Daily)并请用户确认',
281
+ CONFIRM_TODAY_PLAN: '把今日计划给用户确认',
282
+ CONTINUE_TODAY: '帮用户推进手上的任务',
283
+ }[status.suggestedNextOperation] ?? '按 suggestedNextOperation 行动',
284
+ };
285
+ }), flags, (d) => [
286
+ `今天 ${d.today}`,
287
+ d.rollover.alreadyRan ? '跨天检查:今天已执行过(幂等)' : `跨天检查:过期 ${d.rollover.expired} 个 Daily,${d.rollover.pendingReplan} 个 Main 待重排`,
288
+ `状态:${d.status.onboarding} · 待重排 ${d.status.pendingReplan ? '有' : '无'}`,
289
+ '',
290
+ `该做什么:${d.instruction}`,
291
+ ].join('\n'));
292
+ case 'today':
293
+ return run(() => withCtx((ctx) => {
294
+ runDailyRollover(ctx); // 幂等:保证"今天"的视角正确
295
+ const { plan, actions } = getTodayPlan(ctx);
296
+ return {
297
+ today: todayIn(ctx),
298
+ plan,
299
+ actions,
300
+ reminders: listRemindersForDate(ctx),
301
+ lifeContext: getActiveLifeContext(ctx),
302
+ };
303
+ }), flags, (d) => todayHuman(d));
304
+ case 'goals':
305
+ return run(() => withCtx((ctx) => listGoals(ctx).map((g) => ({ ...g, stages: listStages(ctx, g.id), currentStage: getCurrentStage(ctx, g.id) }))), flags, (d) => d.map((g) => `${g.title} [${g.status}] — ${g.stages.length} 个阶段`).join('\n') || '(还没有 Goal)');
306
+ case 'timeline':
307
+ return run(() => withCtx((ctx) => {
308
+ const limit = Number(flags.limit ?? 20);
309
+ const tz = resolveTimezone(ctx);
310
+ return {
311
+ today: todayIn(ctx),
312
+ events: listGrowthEvents(ctx, undefined, limit),
313
+ evidence: listEvidence(ctx, { limit }).map((e) => ({ ...e, dateKey: localDateKeySafe(e.createdAt, tz) })),
314
+ };
315
+ }), flags, (d) => d.events.map((e) => ` ${e.dateKey} ${e.title}`).join('\n') || '(还没有成长记录)');
316
+ case 'passport':
317
+ return run(() => withCtx((ctx) => {
318
+ const snap = latestSnapshot(ctx);
319
+ return { snapshot: snap?.passport ?? null, generatedAt: snap?.generatedAt, dirty: snap?.dirty };
320
+ }), flags);
321
+ case 'metrics':
322
+ return run(() => withCtx((ctx) => computeMetrics(ctx, Number(flags.days ?? 14))), flags);
323
+ /** 产出**带样式的 Markdown**(含阶段轨道、进度条、Mermaid 甘特图、事实表),不是纯文字流水账 */
324
+ case 'progress':
325
+ return run(() => {
326
+ const cfg = loadConfig();
327
+ const db = openDb(cfg.dataDir);
328
+ const report = renderProgress(makeCtx(db, cfg, 'cli'));
329
+ if (flags.out) {
330
+ // --out 指定文件时按扩展名决定格式
331
+ const target = String(flags.out);
332
+ const isHtml = target.endsWith('.html');
333
+ writeFileSync(target, isHtml ? report.html : report.markdown, 'utf8');
334
+ return { saved: target, format: isHtml ? 'html' : 'markdown', title: report.title, facts: report.facts };
335
+ }
336
+ return {
337
+ format: flags.html ? 'html' : 'markdown',
338
+ title: report.title,
339
+ facts: report.facts,
340
+ markdown: report.markdown,
341
+ html: report.html,
342
+ };
343
+ }, flags, (d) => {
344
+ const x = d;
345
+ if (x.saved)
346
+ return `✓ 已保存:${x.saved}(${x.format})`;
347
+ return (x.format === 'html' || flags.html) ? (x.html ?? '') : (x.markdown ?? '');
348
+ });
349
+ case 'rollover':
350
+ return run(() => withCtx((ctx) => runDailyRollover(ctx)), flags, (d) => d.alreadyRan
351
+ ? '今天已经执行过跨天检查(幂等)'
352
+ : `已过期 ${d.expiredDailyActionIds.length} 个 Daily,${d.pendingReplanMainQuestIds.length} 个 Main 转入待重排`);
353
+ case 'complete': {
354
+ const id = args[0];
355
+ if (!id)
356
+ return failMissing('complete', '<actionId>', flags);
357
+ return run(() => withCtx((ctx) => completeAction(ctx, id)), flags, (d) => `✓ 已完成:${d.title}`);
358
+ }
359
+ case 'skip': {
360
+ const id = args[0];
361
+ if (!id)
362
+ return failMissing('skip', '<actionId>', flags);
363
+ return run(() => withCtx((ctx) => skipAction(ctx, id, str(flags.reason))), flags, (d) => `已跳过:${d.title}`);
364
+ }
365
+ case 'delay': {
366
+ const id = args[0];
367
+ if (!id)
368
+ return failMissing('delay', '<actionId>', flags);
369
+ return run(() => withCtx((ctx) => delayAction(ctx, id, str(flags.reason))), flags, (d) => `已延期:${d.title}`);
370
+ }
371
+ case 'mcp-config':
372
+ return run(() => mcpConfig(), flags, (d) => JSON.stringify(d.mcpServers, null, 2));
373
+ case 'skill': {
374
+ const file = join(ROOT, 'SKILL.md');
375
+ return run(() => ({ path: file, content: existsSync(file) ? readFileSync(file, 'utf8') : null }), flags, (d) => d.content ?? `未找到 SKILL.md(预期位置:${d.path})`);
376
+ }
377
+ case 'doctor':
378
+ return runDoctor(flags);
379
+ /**
380
+ * 设备接入。
381
+ *
382
+ * 存在的理由:设备要取卡,必须让 daemon 以局域网模式跑(AI_GROWTH_DEVICE_LAN=1),
383
+ * 而局域网一开,/api/device/* 对【所有】来源都强制校验令牌(见 src/device/token.ts)。
384
+ * 于是「本机局域网地址 + 完整令牌」这两样缺一不可 —— 以前它们只散落在
385
+ * daemon 启动日志的前 8 位里,用户拿不到完整值(固件、错误提示、启动日志
386
+ * 三处却都在让用户跑 `ai-growth device`)。这个命令就是补上那个断掉的接口。
387
+ */
388
+ // 两个子命令:device(只读看状态) / device setup(一条命令配好并自己确认成功)。
389
+ // setup 是发布出去的应用该有的体验:用户不需要跑脚本,Agent 也不需要逐步排查。
390
+ case 'device': {
391
+ if (args[0] !== 'setup') {
392
+ return run(() => deviceInfo(flags.rotate === true), flags, (d) => deviceHuman(d));
393
+ }
394
+ const ssid = str(flags.ssid);
395
+ if (!ssid)
396
+ return failMissing('device setup', '--ssid <Wi-Fi 名>', flags);
397
+ const password = str(flags.pass) ?? '';
398
+ return runAsync(async () => {
399
+ const info = deviceInfo(false);
400
+ // ① 局域网开关:默认是关的,而设备只能走局域网才够得到电脑。
401
+ // 作为要发布出去的应用,这条命令应该**把它打开并记住**,
402
+ // 而不是让用户回去查文档再手动带环境变量。
403
+ let justEnabledLan = false;
404
+ if (!info.lanEnabled) {
405
+ if (flags['no-enable-lan'] === true) {
406
+ throw new DomainError('LAN_DISABLED', '服务只绑在 127.0.0.1,设备在 Wi-Fi 上够不到它。按 --no-enable-lan 未自动开启;'
407
+ + '去掉该参数重跑即可自动开启并记住。');
408
+ }
409
+ withCtx((ctx) => setDeviceLanEnabled(ctx, true));
410
+ justEnabledLan = true;
411
+ }
412
+ // ② 服务活着吗。刚刚才打开开关的话,正在跑的那个实例仍绑在 127.0.0.1,
413
+ // 必须重启一次才生效 —— 这点必须明说,否则用户会以为已经好了。
414
+ if (!(await probeService(info.apiPort))) {
415
+ throw new DomainError('SERVICE_NOT_RUNNING', `本机 ${info.apiPort} 端口没有服务在跑,设备取不到卡。\n`
416
+ + ' 启动:ai-growth serve'
417
+ + (justEnabledLan ? '\n(局域网访问已开启并记住,直接 serve 就行,不用再带环境变量)' : ''));
418
+ }
419
+ if (justEnabledLan) {
420
+ throw new DomainError('LAN_JUST_ENABLED_RESTART_NEEDED', '已开启并记住「局域网访问」,但当前这个服务实例还是绑在 127.0.0.1,重启一次才生效'
421
+ + '(重启后不需要任何环境变量)。\n 重启:ai-growth serve');
422
+ }
423
+ if (!info.host) {
424
+ throw new DomainError('NO_LAN_ADDRESS', '没找到本机的局域网地址:确认这台电脑连着 Wi-Fi 或网线。');
425
+ }
426
+ return provisionDevice({
427
+ ssid,
428
+ password,
429
+ host: info.host,
430
+ apiPort: info.apiPort,
431
+ token: info.token,
432
+ serialPath: str(flags.port),
433
+ skipVerify: flags['no-verify'] === true,
434
+ });
435
+ }, flags, (d) => provisionHuman(d));
436
+ }
437
+ case 'init':
438
+ case 'migrate':
439
+ return run(() => {
440
+ const cfg = loadConfig();
441
+ openDb(cfg.dataDir);
442
+ return { dataDir: cfg.dataDir, db: dbPath(cfg.dataDir), migrated: true };
443
+ }, flags, (d) => `✓ 数据库就绪(migration 幂等):${d.db}`);
444
+ case 'serve': {
445
+ const child = spawn(process.execPath, [join(ROOT, 'dist', 'daemon', 'main.js')], { stdio: 'inherit' });
446
+ child.on('exit', (code) => process.exit(code ?? 0));
447
+ return;
448
+ }
449
+ default:
450
+ return emit(err('UNKNOWN_COMMAND', `未知命令:${cmd}。运行 ai-growth help 查看全部命令。`), flags);
451
+ }
452
+ }
453
+ // ---------------------------------------------------------------- 辅助
454
+ function str(v) {
455
+ return typeof v === 'string' ? v : undefined;
456
+ }
457
+ function failMissing(cmd, arg, flags) {
458
+ emit(err('VALIDATION_ERROR', `${cmd} 需要参数 ${arg}。用法见 ai-growth help --json`), flags);
459
+ }
460
+ function withCtx(fn) {
461
+ const cfg = loadConfig();
462
+ const db = openDb(cfg.dataDir);
463
+ return fn(makeCtx(db, cfg, 'cli'));
464
+ }
465
+ // ---------------------------------------------------------------- 设备接入
466
+ /**
467
+ * 找出「设备应该连哪个地址」。
468
+ *
469
+ * 为什么不能直接取第一个非回环地址:本机装了 Tailscale,会多出 utun* 隧道网卡;
470
+ * 热点/虚拟机还会带来 awdl / llw / bridge / ap 这些前缀的网卡。设备是同一台 Wi-Fi 上的
471
+ * 另一台机器,只有真实局域网地址(192.168 或 10 或 172.16-31)才连得上 —— 给错了它
472
+ * 永远连不上,而设备上只显示"连不上电脑",用户在电脑这边看不到任何线索。
473
+ */
474
+ function lanIPv4() {
475
+ const ifaceRank = (n) => (/^en\d/.test(n) ? 0 : /^eth/.test(n) ? 1 : 2);
476
+ const all = [];
477
+ for (const [name, addrs] of Object.entries(networkInterfaces())) {
478
+ if (/^(utun|llw|awdl|bridge|ap\d|gif|stf|lo)/.test(name))
479
+ continue;
480
+ for (const a of addrs ?? []) {
481
+ if (a.family !== 'IPv4' || a.internal)
482
+ continue;
483
+ all.push({
484
+ address: a.address,
485
+ iface: name,
486
+ isPrivate: /^192\.168\.|^10\.|^172\.(1[6-9]|2\d|3[01])\./.test(a.address),
487
+ });
488
+ }
489
+ }
490
+ const lan = all.filter((c) => c.isPrivate).sort((a, b) => ifaceRank(a.iface) - ifaceRank(b.iface));
491
+ return lan[0] ?? all[0] ?? null;
492
+ }
493
+ /**
494
+ * 设备接入所需的一切。只读 + 幂等创建令牌(没有就生成,不覆盖已有的)。
495
+ * 令牌是设备与电脑之间的共享密钥,只在局域网开启时参与校验。
496
+ */
497
+ function deviceInfo(rotate) {
498
+ const cfg = loadConfig();
499
+ const lan = lanIPv4();
500
+ // 令牌与"局域网开关"都落在 settings 表里(开关仍允许环境变量临时覆盖,见 token.ts)
501
+ const { token, lanEnabled, lanFromEnv } = withCtx((ctx) => ({
502
+ token: rotate ? rotateDeviceToken(ctx) : getOrCreateDeviceToken(ctx),
503
+ lanEnabled: isDeviceLanEnabled(cfg, ctx),
504
+ lanFromEnv: (cfg.deviceLan ?? '').trim() !== '',
505
+ }));
506
+ const host = lan?.address ?? null;
507
+ const enableLanCommand = 'ai-growth device setup --ssid <Wi-Fi名> --pass <密码>(会自动开启并记住)';
508
+ return {
509
+ lanEnabled,
510
+ lanFromEnv,
511
+ lanSwitchEnvVar: 'AI_GROWTH_DEVICE_LAN',
512
+ enableLanCommand,
513
+ token,
514
+ tokenCreatedOrRotated: rotate,
515
+ host,
516
+ iface: lan?.iface ?? null,
517
+ apiPort: cfg.apiPort,
518
+ deviceApiBase: host ? `http://${host}:${cfg.apiPort}/api/device` : null,
519
+ /** 现在就差什么才能真正跑通 */
520
+ ready: lanEnabled && !!host,
521
+ blockedBy: !lanEnabled ? 'LAN_DISABLED' : !host ? 'NO_LAN_ADDRESS' : null,
522
+ /**
523
+ * 这一行要原样粘进设备串口。设备侧解析是 `growth_set <ssid> <pass> <host> <port> [token]`
524
+ * (见固件 main/growth_config.c),<你的Wi-Fi名>/<Wi-Fi密码> 由用户替换;
525
+ * 密码不能含空格,开放网络可以写 ""。
526
+ */
527
+ growthSetCommand: `growth_set <你的Wi-Fi名> <Wi-Fi密码> ${host ?? '<本机局域网IP>'} ${cfg.apiPort} ${token}`,
528
+ /**
529
+ * ⚠ Wi-Fi 名含中文(或空格)时必须用这一条,并把名字/密码换成 hex。
530
+ * 原因:esp_console 交给命令前会过 sanitize(),只保留 isprint() 为真的字节,
531
+ * 中文(UTF-8 的 0x80~0xFF)会被整段丢掉 —— 用 growth_set 只会存进一个残缺的尾巴。
532
+ * (日常直接用 `ai-growth device setup`,它会自动判断该走哪条。)
533
+ */
534
+ growthSetHexCommand: `growth_set_hex <Wi-Fi名的hex> <密码的hex> ${host ?? '<本机局域网IP>'} ${cfg.apiPort} ${token}`,
535
+ deviceConsoleCommands: ['growth_status', 'growth_set', 'growth_set_hex', 'growth_clear'],
536
+ steps: [
537
+ lanEnabled
538
+ ? '服务已在局域网模式:确认 ai-growth serve(或已装好的 daemon)正在跑'
539
+ : '先按上面那行把服务切成局域网模式再重启(否则设备永远连不上)',
540
+ '确保设备连的是跟电脑同一个 Wi-Fi',
541
+ '插上 USB、打开设备串口,粘上面那行 growth_set(保存后设备会自动重启)',
542
+ '重启后敲 growth_status 看配置,再按键让它取卡',
543
+ ],
544
+ note: '局域网关闭时不校验令牌;开启后所有来源(含本机)都必须带令牌。',
545
+ };
546
+ }
547
+ function deviceHuman(d) {
548
+ const out = ['设备接入(AI Passport)', ''];
549
+ out.push(`本地服务 :http://127.0.0.1:${d.apiPort}`);
550
+ if (d.host)
551
+ out.push(`设备要连的地址:http://${d.host}:${d.apiPort}${d.iface ? ` (网卡 ${d.iface})` : ''}`);
552
+ else
553
+ out.push('设备要连的地址:没找到局域网地址 —— 确认这台电脑连着 Wi-Fi 或网线');
554
+ out.push(`局域网访问 :${d.lanEnabled ? '已开启 ✓' : '未开启 ✗ 设备在 Wi-Fi 上够不到这台电脑'}`);
555
+ if (!d.lanEnabled)
556
+ out.push(` 用这个命令重启服务即可开启:${d.enableLanCommand}`);
557
+ out.push(`设备令牌 :${d.token}${d.tokenCreatedOrRotated ? ' (刚刚重新生成,设备需要重新配置)' : ''}`);
558
+ out.push('');
559
+ out.push('在设备串口里粘这一行(把两个尖括号换成你的 Wi-Fi;密码不能含空格):');
560
+ out.push(` ${d.growthSetCommand}`);
561
+ out.push('');
562
+ out.push('顺序:');
563
+ for (const [i, s] of d.steps.entries())
564
+ out.push(` ${i + 1}. ${s}`);
565
+ if (!d.lanEnabled) {
566
+ out.push('');
567
+ out.push('⚠ 不先开局域网的话,设备会一直停在「连不上电脑」。');
568
+ }
569
+ return out.join('\n');
570
+ }
571
+ /** device setup 的结果,照"每步成/败 + 判定依据 + 下一步"三段给,方便人和 Agent 都直接用 */
572
+ function provisionHuman(d) {
573
+ const out = ['AI Passport 连接', ''];
574
+ for (const s of d.steps) {
575
+ out.push(` ${s.ok ? '✓' : '✗'} ${s.name}${s.detail ? ` —— ${s.detail}` : ''}`);
576
+ }
577
+ if (d.evidence.length > 0) {
578
+ out.push('', '设备日志(判定依据):');
579
+ for (const line of d.evidence.slice(0, 6))
580
+ out.push(` ${line}`);
581
+ }
582
+ out.push('');
583
+ out.push(d.ok
584
+ ? `✓ 完成:设备已连上 Wi-Fi 并成功取到卡(写入方式 ${d.usedCommand};串口 ${d.serialPath})`
585
+ : `✗ 未完成:设备给出的结论是 ${d.verdict}`);
586
+ if (d.nextAction)
587
+ out.push('', `下一步:${d.nextAction}`);
588
+ return out.join('\n');
589
+ }
590
+ /** 局部导入避免与 core/time 循环依赖写法分散 */
591
+ function localDateKeySafe(iso, tz) {
592
+ return new Intl.DateTimeFormat('en-CA', { timeZone: tz, year: 'numeric', month: '2-digit', day: '2-digit' })
593
+ .format(new Date(iso));
594
+ }
595
+ function envInfo() {
596
+ const cfg = loadConfig();
597
+ const inst = detectInstall();
598
+ return {
599
+ version: VERSION,
600
+ node: process.version,
601
+ platform: process.platform,
602
+ dataDir: cfg.dataDir,
603
+ database: dbPath(cfg.dataDir),
604
+ databaseExists: existsSync(dbPath(cfg.dataDir)),
605
+ timezone: cfg.timezone,
606
+ apiPort: cfg.apiPort,
607
+ install: {
608
+ kind: inst.kind,
609
+ kindLabel: INSTALL_LABEL[inst.kind],
610
+ selfPath: inst.selfPath,
611
+ invokedVia: inst.invokedVia,
612
+ projectRoot: ROOT,
613
+ globalPrefix: inst.globalPrefix,
614
+ globalBin: inst.globalBin,
615
+ globalBinOnPath: inst.binOnPath,
616
+ globalCliPath: inst.globalCliPath,
617
+ globalCliResolvesHere: inst.globalCliResolvesHere,
618
+ },
619
+ mcpEntry: join(ROOT, 'dist', 'mcp', 'server.js'),
620
+ daemonEntry: join(ROOT, 'dist', 'daemon', 'main.js'),
621
+ howToUpdate: updateSteps(inst.kind),
622
+ };
623
+ }
624
+ const INSTALL_LABEL = {
625
+ checkout: '仅从源码检出运行 —— 全局命令尚未建立',
626
+ linked: '全局命令指向当前源码检出(npm link)—— 开发与生产共用同一入口',
627
+ installed: '全局安装的实体包(与源码检出解耦)',
628
+ };
629
+ function updateSteps(kind) {
630
+ if (kind === 'linked') {
631
+ return [
632
+ '开发态(npm link):全局命令已指向源码检出,改代码后重新构建即生效。',
633
+ ' 更新:cd <项目> && git pull && npm install && npm run build',
634
+ ' 无需重新 link;daemon 重启即加载新代码,数据不受影响。',
635
+ '要切成生产态(与检出解耦):./scripts/install.sh --global',
636
+ ];
637
+ }
638
+ if (kind === 'installed') {
639
+ return [
640
+ '生产态(全局实体包)。更新三步:',
641
+ ' 1) npm install -g ai-growth@latest',
642
+ ' 2) ai-growth migrate # 幂等,不丢数据',
643
+ ' 3) launchctl kickstart -k gui/$(id -u)/com.lairey.ai-growth.daemon',
644
+ '数据在 ~/.ai-growth,与代码包完全分离,更新不触碰它。',
645
+ ];
646
+ }
647
+ return [
648
+ '当前只能从源码检出目录运行,全局命令还没建立。二选一:',
649
+ ' 开发态:cd <项目> && npm link (全局 ai-growth → 当前源码)',
650
+ ' 生产态:cd <项目> && ./scripts/install.sh --global',
651
+ ];
652
+ }
653
+ function envInfoHuman(d) {
654
+ return [
655
+ `ai-growth ${d.version} (node ${d.node}, ${d.platform})`,
656
+ `安装形态:${d.install.kindLabel}`,
657
+ `代码位置:${d.install.selfPath}`,
658
+ d.install.globalPrefix ? `全局前缀:${d.install.globalPrefix}${d.install.globalBinOnPath ? '' : ' ⚠ 该 bin 不在当前 PATH 中'}` : '',
659
+ `数据目录:${d.dataDir}`,
660
+ `数据库: ${d.database}${d.databaseExists ? '' : ' (尚未初始化,运行 ai-growth init)'}`,
661
+ `时区: ${d.timezone}`,
662
+ `面板端口:${d.apiPort}`,
663
+ '',
664
+ '更新方式:',
665
+ ...d.howToUpdate.map((s) => ` ${s}`),
666
+ ].filter(Boolean).join('\n');
667
+ }
668
+ /** Agent 引导:一条命令拿到接入所需的全部信息 */
669
+ function agentInfo(flags) {
670
+ const cfg = loadConfig();
671
+ const inst = detectInstall();
672
+ const skillPath = join(ROOT, 'SKILL.md');
673
+ const agentsPath = join(ROOT, 'AGENTS.md');
674
+ return {
675
+ name: 'ai-growth',
676
+ version: VERSION,
677
+ whatItIs: 'Agent-native、Local-first 的个人成长执行系统。AI 维护计划,人保留目标与最终确认权。',
678
+ whatItIsNot: '它不是 Todo List,也不是虚构属性值的 RPG;不要替用户决定人生方向。',
679
+ install: {
680
+ fromNpm: ['npm install -g ai-growth', 'ai-growth init'],
681
+ fromSource: ['git clone <repo>', 'cd ai-growth', 'npm install', 'npm run build', 'ai-growth init'],
682
+ oneShot: 'bash scripts/install.sh(开发:npm link;生产:--global)',
683
+ currentKind: inst.kind,
684
+ currentKindLabel: INSTALL_LABEL[inst.kind],
685
+ howToUpdate: updateSteps(inst.kind),
686
+ },
687
+ runtime: {
688
+ dataDir: cfg.dataDir,
689
+ database: dbPath(cfg.dataDir),
690
+ databaseReady: existsSync(dbPath(cfg.dataDir)),
691
+ timezone: cfg.timezone,
692
+ apiPort: cfg.apiPort,
693
+ },
694
+ connect: {
695
+ mcp: mcpConfig(),
696
+ mcpNote: '把上面的配置写进 Agent 的 MCP 配置后需「信任」该服务;MCP 暴露 63 个工具,含写操作。',
697
+ skill: skillPath,
698
+ skillNote: '把 SKILL.md 交给 Agent 作为行为准则;ai-growth skill 可直接打印其内容。',
699
+ cliFallback: 'Agent 不支持 MCP 时可用 CLI:ai-growth status/today/complete/rollover 等,全部支持 --json。',
700
+ },
701
+ rules: RULES,
702
+ nextSteps: NEXT_STEPS,
703
+ docs: {
704
+ agents: agentsPath,
705
+ readme: join(ROOT, 'README.md'),
706
+ spec: join(ROOT, 'PRODUCT_SPEC.md'),
707
+ serverJson: join(ROOT, 'server.json'),
708
+ llmsTxt: join(ROOT, 'llms.txt'),
709
+ },
710
+ flags: { jsonRequested: flags.json === true },
711
+ };
712
+ }
713
+ function agentInfoHuman(d) {
714
+ return [
715
+ `ai-growth ${d.version}`,
716
+ d.whatItIs,
717
+ '',
718
+ '安装(二选一):',
719
+ ...d.install.fromNpm.map((c) => ` ${c}`),
720
+ ' 或从源码:',
721
+ ...d.install.fromSource.map((c) => ` ${c}`),
722
+ '',
723
+ `数据目录:${d.runtime.dataDir}${d.runtime.databaseReady ? '' : ' ← 尚未初始化,先跑 ai-growth init'}`,
724
+ `面板:http://127.0.0.1:${d.runtime.apiPort}`,
725
+ `Skill:${d.connect.skill}`,
726
+ '',
727
+ '接入 Agent:',
728
+ ' ai-growth mcp-config # 打印可粘贴的 MCP 配置',
729
+ ' ai-growth skill # 打印 SKILL.md 全文,交给 Agent 作为行为准则',
730
+ '',
731
+ '产品铁律(Agent 必读):',
732
+ ...RULES.map((r) => ` · ${r}`),
733
+ '',
734
+ '下一步:',
735
+ ...NEXT_STEPS.map((s) => ` ${s}`),
736
+ ].join('\n');
737
+ }
738
+ function mcpConfig() {
739
+ const cfg = loadConfig();
740
+ return {
741
+ mcpServers: {
742
+ 'ai-growth': {
743
+ command: process.execPath,
744
+ args: [join(ROOT, 'dist', 'mcp', 'server.js')],
745
+ env: { AI_GROWTH_DATA_DIR: cfg.dataDir },
746
+ },
747
+ },
748
+ };
749
+ }
750
+ function statusHuman(d) {
751
+ const onboarding = {
752
+ NOT_STARTED: '未开始(需要先做访谈)',
753
+ IN_PROGRESS: '访谈进行中',
754
+ COMPLETED: '已完成',
755
+ };
756
+ return [
757
+ `引导状态:${onboarding[d.onboarding] ?? d.onboarding}`,
758
+ `活跃目标:${d.activeGoalCount}${d.currentStageTitle ? `(当前阶段:${d.currentStageTitle})` : ''}`,
759
+ `今日计划:${d.todayPlanStatus}`,
760
+ `待重排: ${d.pendingReplan ? '有 —— 先解决 Replan 再规划新主线' : '无'}`,
761
+ `生活情境:${d.lifeContextMode}`,
762
+ `建议下一步:${d.suggestedNextOperation}`,
763
+ ].join('\n');
764
+ }
765
+ function todayHuman(d) {
766
+ const main = d.actions.filter((a) => a.kind === 'MAIN_QUEST');
767
+ const dailies = d.actions.filter((a) => a.kind === 'DAILY');
768
+ const line = (a) => ` [${a.status}] ${a.title}${a.estimatedMinutes ? ` · ${a.estimatedMinutes}min` : ''}\n id=${a.id}`;
769
+ return [
770
+ `今天 ${d.today}`,
771
+ '',
772
+ 'Main Quest:',
773
+ main.length ? main.map(line).join('\n') : ' (无)',
774
+ '',
775
+ 'Daily:',
776
+ dailies.length ? dailies.map(line).join('\n') : ' (无)',
777
+ '',
778
+ `提醒:${d.reminders.length} 条`,
779
+ ].join('\n');
780
+ }
781
+ function runDoctor(flags) {
782
+ const script = join(ROOT, 'scripts', 'doctor.sh');
783
+ if (!existsSync(script)) {
784
+ return emit(err('NOT_FOUND', `未找到 ${script}`), flags);
785
+ }
786
+ const child = spawn('bash', [script], { stdio: wantsJson(flags) ? 'pipe' : 'inherit' });
787
+ let out = '';
788
+ if (wantsJson(flags))
789
+ child.stdout?.on('data', (c) => { out += c; });
790
+ child.on('exit', (code) => {
791
+ if (wantsJson(flags)) {
792
+ emit({ ok: code === 0, data: { exitCode: code, report: out } }, flags);
793
+ }
794
+ else {
795
+ process.exitCode = code ?? 0;
796
+ }
797
+ });
798
+ }
799
+ main();