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,291 @@
1
+ #!/usr/bin/env bash
2
+ # AI Growth — 一键安装(macOS)
3
+ #
4
+ # 核心原则:**开发与生产用同一条全局命令,数据始终在 ~/.ai-growth**。
5
+ # CLI / MCP / daemon / 面板 都指向"全局安装的那一份"代码,所以更新 = 替换全局包。
6
+ #
7
+ # 完成:环境检查 → 依赖 → 构建 → 全局安装 → 数据库迁移
8
+ # → launchd 常驻服务(指向全局 CLI)→ MCP 配置 → Agent 入口说明 → doctor
9
+ #
10
+ # 用法:
11
+ # ./scripts/install.sh # 默认:npm link(开发态)+ 安装 Skill
12
+ # ./scripts/install.sh --global # 生产态:npm pack + npm install -g(实体包,与检出解耦)
13
+ # ./scripts/install.sh --with-mcp # 额外把 MCP 写入 ~/.workbuddy/mcp.json(会先备份)
14
+ # ./scripts/install.sh --no-skill # 不安装 Skill 到 ~/.workbuddy/skills/
15
+ # ./scripts/install.sh --no-daemon # 不安装 launchd 常驻服务
16
+ # ./scripts/install.sh --device-lan # 常驻服务以局域网模式跑(AI Passport 设备要从 Wi-Fi 访问)
17
+
18
+ set -euo pipefail
19
+
20
+ PROJECT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
21
+ DATA_DIR="${AI_GROWTH_DATA_DIR:-$HOME/.ai-growth}"
22
+ PLIST_LABEL="com.lairey.ai-growth.daemon"
23
+ PLIST_DEST="$HOME/Library/LaunchAgents/${PLIST_LABEL}.plist"
24
+ NODE_BIN="$(command -v node || true)"
25
+ NPM_GLOBAL_PREFIX="$(npm prefix -g 2>/dev/null || true)"
26
+ GLOBAL_BIN="$NPM_GLOBAL_PREFIX/bin"
27
+ GLOBAL_CLI="$NPM_GLOBAL_PREFIX/lib/node_modules/ai-growth/dist/cli/main.js"
28
+
29
+ MODE="link" # link | global
30
+ WITH_MCP=0
31
+ WITH_SKILL=1
32
+ WITH_DAEMON=1
33
+ DEVICE_LAN=0 # 1 = daemon 以局域网模式常驻(AI Passport 设备需要)
34
+ for arg in "$@"; do
35
+ case "$arg" in
36
+ --global) MODE="global" ;;
37
+ --link) MODE="link" ;;
38
+ --with-mcp) WITH_MCP=1 ;;
39
+ --no-skill) WITH_SKILL=0 ;;
40
+ --no-daemon) WITH_DAEMON=0 ;;
41
+ --device-lan) DEVICE_LAN=1 ;;
42
+ --no-device-lan) DEVICE_LAN=0 ;;
43
+ *) echo "未知参数:$arg"; exit 1 ;;
44
+ esac
45
+ done
46
+
47
+ step() { printf "\n\033[1;34m▶ %s\033[0m\n" "$1"; }
48
+ ok() { printf " \033[0;32m✓\033[0m %s\n" "$1"; }
49
+ warn() { printf " \033[0;33m!\033[0m %s\n" "$1"; }
50
+ die() { printf " \033[0;31m✗\033[0m %s\n" "$1"; exit 1; }
51
+
52
+ # ---------------------------------------------------------------- 1. 依赖检查
53
+ step "1/9 检查运行环境"
54
+ [ -n "$NODE_BIN" ] || die "未找到 node,请先安装 Node.js >= 20(https://nodejs.org)"
55
+ NODE_MAJOR="$("$NODE_BIN" -p 'process.versions.node.split(".")[0]')"
56
+ [ "$NODE_MAJOR" -ge 20 ] || die "Node 版本过低(当前 $("$NODE_BIN" -v)),需要 >= 20"
57
+ ok "node $("$NODE_BIN" -v) @ $NODE_BIN"
58
+ command -v npm >/dev/null || die "未找到 npm"
59
+ ok "npm $(npm -v)"
60
+ [ -n "$NPM_GLOBAL_PREFIX" ] || die "无法确定 npm 全局前缀(npm prefix -g 失败)"
61
+ ok "npm 全局前缀:$NPM_GLOBAL_PREFIX"
62
+ case ":$PATH:" in
63
+ *":$GLOBAL_BIN:"*) ok "全局 bin 已在 PATH 中(终端里可直接用 ai-growth)" ;;
64
+ *) warn "全局 bin 不在当前 PATH:$GLOBAL_BIN"
65
+ warn "请在 ~/.zshrc 加:export PATH=\"$GLOBAL_BIN:\$PATH\"(你的交互式 shell 可能已配好)" ;;
66
+ esac
67
+ if [[ "$(uname -s)" != "Darwin" ]]; then
68
+ warn "当前非 macOS:daemon 自启动将跳过(launchd 仅限 macOS)"
69
+ WITH_DAEMON=0
70
+ fi
71
+
72
+ # ---------------------------------------------------------------- 2. 安装依赖
73
+ step "2/9 安装依赖"
74
+ cd "$PROJECT_DIR"
75
+ need_install=0
76
+ [ -d node_modules ] || need_install=1
77
+ # 关键依赖缺失也要补装(避免"目录在但内容不全"的假象)
78
+ for dep in better-sqlite3 @modelcontextprotocol/sdk zod typescript; do
79
+ [ -e "node_modules/$dep" ] || need_install=1
80
+ done
81
+
82
+ if [ "$need_install" -eq 0 ]; then
83
+ ok "依赖已就绪(node_modules 完整)"
84
+ else
85
+ # npm 偶发 "Tracker idealTree already exists"(并发/缓存态),重试一次即可
86
+ if npm install --no-audit --no-fund; then
87
+ ok "依赖安装完成"
88
+ else
89
+ warn "npm install 首次失败,清理状态后重试…"
90
+ rm -rf node_modules/.package-lock.json
91
+ if npm install --no-audit --no-fund; then
92
+ ok "依赖安装完成(重试成功)"
93
+ else
94
+ die "依赖安装失败。请手动排查:cd \"$PROJECT_DIR\" && npm install"
95
+ fi
96
+ fi
97
+ fi
98
+ # 装完再确认一次,不允许带着残缺依赖继续
99
+ for dep in better-sqlite3 @modelcontextprotocol/sdk zod; do
100
+ [ -e "node_modules/$dep" ] || die "依赖缺失:${dep}(请手动运行 npm install)"
101
+ done
102
+
103
+ # ---------------------------------------------------------------- 3. 构建
104
+ step "3/9 构建 TypeScript"
105
+ npm run build >/dev/null
106
+ ok "构建产物:dist/"
107
+
108
+ # ---------------------------------------------------------------- 3.5 全局安装
109
+ step "4/9 全局安装(CLI / MCP / daemon 都指向这一份)"
110
+ if [ "$MODE" = "global" ]; then
111
+ TGZ="$(npm pack --silent | tail -1)"
112
+ [ -f "$PROJECT_DIR/$TGZ" ] || die "npm pack 失败"
113
+ npm install -g "$PROJECT_DIR/$TGZ" >/dev/null 2>&1 || die "npm install -g 失败"
114
+ rm -f "$PROJECT_DIR/$TGZ"
115
+ ok "生产态:已全局安装实体包(与源码检出解耦)"
116
+ else
117
+ npm link >/dev/null 2>&1 || die "npm link 失败"
118
+ ok "开发态:全局命令已软链到本检出(npm link)"
119
+ fi
120
+ # 用全局 CLI 自我确认(这也是 daemon / MCP 将要使用的那一份)
121
+ if [ -x "$GLOBAL_CLI" ] || [ -f "$GLOBAL_CLI" ]; then
122
+ V="$("$NODE_BIN" "$GLOBAL_CLI" version --json 2>/dev/null | "$NODE_BIN" -e "let s='';process.stdin.on('data',c=>s+=c).on('end',()=>{try{console.log(JSON.parse(s).data.version)}catch{console.log('?')}})" 2>/dev/null || echo '?')"
123
+ ok "全局入口可用:${GLOBAL_CLI}(版本 ${V})"
124
+ else
125
+ warn "全局入口未就位:$GLOBAL_CLI"
126
+ fi
127
+
128
+ # ---------------------------------------------------------------- 5. 数据目录 + 迁移
129
+ step "5/9 初始化数据库(数据始终在 ${DATA_DIR},与代码解耦)"
130
+ mkdir -p "$DATA_DIR"
131
+ AI_GROWTH_DATA_DIR="$DATA_DIR" "$NODE_BIN" "$GLOBAL_CLI" migrate 2>/dev/null \
132
+ || AI_GROWTH_DATA_DIR="$DATA_DIR" "$NODE_BIN" dist/db/cli.js migrate
133
+
134
+ # ---------------------------------------------------------------- 6. launchd
135
+ step "6/9 安装常驻服务(launchd → 指向全局 CLI)"
136
+ if [ "$WITH_DAEMON" -eq 1 ]; then
137
+ mkdir -p "$HOME/Library/LaunchAgents"
138
+ TMP_PLIST="$(mktemp)"
139
+ sed -e "s|__NODE_BIN__|${NODE_BIN}|g" \
140
+ -e "s|__AI_GROWTH_CLI__|${GLOBAL_CLI}|g" \
141
+ -e "s|__DATA_DIR__|${DATA_DIR}|g" \
142
+ -e "s|__AI_GROWTH_DEVICE_LAN__|${DEVICE_LAN}|g" \
143
+ "$PROJECT_DIR/launchd/${PLIST_LABEL}.plist" > "$TMP_PLIST"
144
+ cp "$TMP_PLIST" "$PLIST_DEST"
145
+ rm -f "$TMP_PLIST"
146
+ ok "plist 已写入:$PLIST_DEST"
147
+ ok "它调用的是全局 CLI:$GLOBAL_CLI serve(更新全局包即更新 daemon)"
148
+ if [ "$DEVICE_LAN" -eq 1 ]; then
149
+ ok "已开启设备局域网访问(AI_GROWTH_DEVICE_LAN=1):AI Passport 可从 Wi-Fi 访问;令牌用 ai-growth device 查看"
150
+ else
151
+ warn "未开启设备局域网访问:AI Passport 设备连不上(需要时重跑 ./scripts/install.sh --device-lan)"
152
+ fi
153
+
154
+ # 先卸载可能存在的旧实例,保证幂等。
155
+ # 注意:bootout 是异步的,紧接着 bootstrap 会偶发失败(服务尚未完全拆除),
156
+ # 因此必须 sleep + 重试 —— 否则重装会把本来在跑的服务搞停。
157
+ UID_NUM="$(id -u)"
158
+ launchctl bootout "gui/$UID_NUM/$PLIST_LABEL" >/dev/null 2>&1 || true
159
+ launchctl unload "$PLIST_DEST" >/dev/null 2>&1 || true
160
+ sleep 1
161
+
162
+ LOADED=0
163
+ for attempt in 1 2 3; do
164
+ # 现代方式(macOS 11+)
165
+ if launchctl bootstrap "gui/$UID_NUM" "$PLIST_DEST" >/dev/null 2>&1; then LOADED=1; break; fi
166
+ # 旧版回退
167
+ if launchctl load "$PLIST_DEST" >/dev/null 2>&1 && launchctl list 2>/dev/null | grep -q "$PLIST_LABEL"; then LOADED=1; break; fi
168
+ sleep 2
169
+ done
170
+
171
+ # bootstrap 返回成功不等于进程活着 —— 直接看它在不在跑
172
+ if [ "$LOADED" -eq 1 ]; then
173
+ sleep 2
174
+ if pgrep -f "ai-growth/dist/cli/main.js serve" >/dev/null 2>&1; then
175
+ ok "daemon 已加载并运行(开机自动恢复)"
176
+ else
177
+ LOADED=0
178
+ fi
179
+ fi
180
+
181
+ if [ "$LOADED" -eq 0 ]; then
182
+ warn "launchd 未能启动服务(plist 已正确就位,不影响后续开机自启)。"
183
+ printf " \033[0;33m?\033[0m 三种方式任选:\n"
184
+ printf " · 重启电脑:~/Library/LaunchAgents/ 里的服务会在登录时自动加载\n"
185
+ printf " · 立刻生效(在「终端」执行):\n\n"
186
+ printf " launchctl bootout gui/%s/%s 2>/dev/null; launchctl bootstrap gui/%s \"%s\"\n\n" "$UID_NUM" "$PLIST_LABEL" "$UID_NUM" "$PLIST_DEST"
187
+ printf " · 或先不管它 —— 手动前台跑:ai-growth serve\n\n"
188
+ printf " \033[0;33m?\033[0m 未跑 daemon 不影响核心闭环:Agent 每次进入会调用跨天检查,\n"
189
+ printf " 跨天状态依旧正确;只有「定时提醒」需要它常驻。\n"
190
+ fi
191
+ else
192
+ warn "已跳过 daemon 安装。手动启动:ai-growth serve"
193
+ fi
194
+
195
+ # ---------------------------------------------------------------- 7. MCP 配置
196
+ step "7/9 MCP 配置(指向全局安装,不指向开发目录)"
197
+ SNIPPET="$PROJECT_DIR/mcp-config.snippet.json"
198
+ MCP_SERVER_JS="$NPM_GLOBAL_PREFIX/lib/node_modules/ai-growth/dist/mcp/server.js"
199
+ cat > "$SNIPPET" <<JSON
200
+ {
201
+ "mcpServers": {
202
+ "ai-growth": {
203
+ "command": "${NODE_BIN}",
204
+ "args": ["${MCP_SERVER_JS}"],
205
+ "env": { "AI_GROWTH_DATA_DIR": "${DATA_DIR}" }
206
+ }
207
+ }
208
+ }
209
+ JSON
210
+ ok "片段已写入:$SNIPPET"
211
+ ok "MCP 入口:$MCP_SERVER_JS"
212
+
213
+ if [ "$WITH_MCP" -eq 1 ]; then
214
+ MCP_JSON="$HOME/.workbuddy/mcp.json"
215
+ mkdir -p "$(dirname "$MCP_JSON")"
216
+ if [ -f "$MCP_JSON" ]; then
217
+ cp "$MCP_JSON" "${MCP_JSON}.bak.$(date +%Y%m%d%H%M%S)"
218
+ ok "已备份原配置"
219
+ else
220
+ echo '{"mcpServers":{}}' > "$MCP_JSON"
221
+ fi
222
+ "$NODE_BIN" - "$MCP_JSON" "$NODE_BIN" "$MCP_SERVER_JS" "$DATA_DIR" <<'NODE'
223
+ const [file, nodeBin, serverJs, dataDir] = process.argv.slice(2);
224
+ const fs = require('fs');
225
+ const cfg = JSON.parse(fs.readFileSync(file, 'utf8'));
226
+ cfg.mcpServers = cfg.mcpServers || {};
227
+ cfg.mcpServers['ai-growth'] = {
228
+ command: nodeBin,
229
+ args: [serverJs],
230
+ env: { AI_GROWTH_DATA_DIR: dataDir },
231
+ };
232
+ fs.writeFileSync(file, JSON.stringify(cfg, null, 2));
233
+ console.log(' \x1b[0;32m✓\x1b[0m 已合并进 ' + file);
234
+ NODE
235
+ warn "新 MCP 不会自动激活:请在连接器管理页右上角「自定义连接器」中对该服务点击「信任」"
236
+ else
237
+ warn "未自动写入 Agent 配置(安全默认)。两种方式二选一:"
238
+ echo " a) 重跑:./scripts/install.sh --with-mcp"
239
+ echo " b) 手动把 $SNIPPET 的内容合并进 ~/.workbuddy/mcp.json"
240
+ fi
241
+
242
+ # ---------------------------------------------------------------- 8. Agent 入口
243
+ step "8/9 Agent 入口与 Skill"
244
+ echo " 仓库根目录有三个 agent 可识别文件:"
245
+ echo " $PROJECT_DIR/AGENTS.md ← Agent 入口(安装/接入/能力/铁律)"
246
+ echo " $PROJECT_DIR/llms.txt ← 精简机器可读索引"
247
+ echo " $PROJECT_DIR/server.json ← MCP Registry 清单"
248
+ echo ""
249
+
250
+ # Skill 决定 Agent「什么时候」用这套系统。不装的话,Agent 不知道何时该走 AI Growth。
251
+ SKILL_DEST="$HOME/.workbuddy/skills/ai-growth"
252
+ if [ "$WITH_SKILL" -eq 1 ]; then
253
+ mkdir -p "$SKILL_DEST"
254
+ cp "$PROJECT_DIR/SKILL.md" "$SKILL_DEST/SKILL.md"
255
+ # frontmatter 的 name+description 是 always-in-context 的元数据,Agent 靠它路由
256
+ if head -1 "$SKILL_DEST/SKILL.md" | grep -q '^---$'; then
257
+ ok "Skill 已安装:$SKILL_DEST/SKILL.md(含 frontmatter,可被 Agent 识别触发时机)"
258
+ else
259
+ warn "Skill 已复制,但缺少 frontmatter(name/description),Agent 可能不会自动触发"
260
+ fi
261
+ echo " 移除:rm -rf \"$SKILL_DEST\""
262
+ else
263
+ warn "已跳过 Skill 安装(--no-skill)。手动安装:"
264
+ echo " mkdir -p \"$SKILL_DEST\" && cp \"$PROJECT_DIR/SKILL.md\" \"$SKILL_DEST/\""
265
+ fi
266
+ echo ""
267
+ if [ -x "$GLOBAL_BIN/ai-growth" ] || [ -f "$GLOBAL_BIN/ai-growth" ]; then
268
+ ok "全局命令就绪:ai-growth(在终端里可直接用)"
269
+ echo " 自检:ai-growth where # 当前跑的是哪一份、怎么更新"
270
+ echo " Agent 自举:ai-growth agent-info --json"
271
+ else
272
+ warn "全局命令未就绪,检查 $GLOBAL_BIN 是否在 PATH"
273
+ fi
274
+
275
+ # ---------------------------------------------------------------- 9. doctor
276
+ step "9/9 运行 doctor 验证"
277
+ bash "$PROJECT_DIR/scripts/doctor.sh" || warn "doctor 报告了问题,请查看上方输出"
278
+
279
+ printf "\n\033[1;32m安装完成。\033[0m\n"
280
+ printf " 代码位置:%s(%s)\n" "$GLOBAL_CLI" "$([ "$MODE" = global ] && echo '生产态:实体包' || echo '开发态:npm link 到本检出')"
281
+ printf " 数据位置:%s(更新代码不会动它)\n" "$DATA_DIR"
282
+ printf "\n 更新方式:\n"
283
+ if [ "$MODE" = global ]; then
284
+ printf " npm install -g ai-growth@latest && ai-growth migrate\n"
285
+ else
286
+ printf " git pull && npm install && npm run build # 全局命令自动指向新代码\n"
287
+ fi
288
+ printf " launchctl kickstart -k gui/$(id -u)/%s # 让 daemon 加载新代码\n" "$PLIST_LABEL"
289
+ printf "\n 下一步:打开你的 Agent,让它加载 SKILL.md 并连接 ai-growth MCP,\n"
290
+
291
+ printf " 然后说一句「开启今日任务」——首次会主动开始访谈。\n\n"
@@ -0,0 +1,120 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * 把设备会显示的所有卡面拼成一张「对照图」,用于设计评审。
4
+ *
5
+ * 为什么需要它:
6
+ * 设备卡只有 240×320,单独看一张很难判断整体视觉是否成立(层级、留白、字号、
7
+ * 状态色的可辨度)。把「开机/离线/未配置」三张固件内置兜底卡 + 运行时五种卡面
8
+ * 放在同一张图上按 2× 像素放大对照,就能一眼看出是否成体系。
9
+ *
10
+ * 它渲染的是**与运行时完全相同**的 buildCardHtml(),不是另外画的示意图 ——
11
+ * 所以这张图里看到什么,设备上就是什么(除了被放大 2 倍)。
12
+ *
13
+ * 用法:
14
+ * npm run build && node scripts/preview-cards.mjs [--out <png路径>] [--scale 2]
15
+ * 默认输出:<项目>/.workbuddy/previews/device-cards.png
16
+ */
17
+ import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
18
+ import { dirname, join, resolve } from 'node:path';
19
+ import { fileURLToPath } from 'node:url';
20
+
21
+ const here = dirname(fileURLToPath(import.meta.url));
22
+ const root = resolve(here, '..');
23
+ const distDir = join(root, 'dist', 'device');
24
+
25
+ function argValue(flag, fallback) {
26
+ const i = process.argv.indexOf(flag);
27
+ return i >= 0 && process.argv[i + 1] ? process.argv[i + 1] : fallback;
28
+ }
29
+
30
+ const outPath = resolve(argValue('--out', join(root, '.workbuddy', 'previews', 'device-cards.png')));
31
+ const scale = Math.max(1, Math.min(3, Number(argValue('--scale', '2')) || 2));
32
+
33
+ if (!existsSync(join(distDir, 'card.js'))) {
34
+ console.error('未找到 dist/device/card.js —— 先跑 `npm run build`。');
35
+ process.exit(2);
36
+ }
37
+
38
+ const { buildCardHtml } = await import(join(distDir, 'card.js'));
39
+ const { screenshotPng, CARD_WIDTH, CARD_HEIGHT } = await import(join(distDir, 'render.js'));
40
+
41
+ const ITEMS = [
42
+ { id: 'a1', kind: 'MAIN_QUEST', title: '实现最小 Agent Memory Demo', status: 'PLANNED', done: false, minutes: 60, why: 'Memory 是当前阶段的出口标准', completionCriteria: 'demo 能写入并召回' },
43
+ { id: 'a2', kind: 'DAILY', title: '散步 30 分钟', status: 'COMPLETED', done: true, minutes: 30, why: '保持运动习惯' },
44
+ { id: 'a3', kind: 'DAILY', title: '读 20 页书', status: 'PLANNED', done: false, minutes: 20 },
45
+ ];
46
+
47
+ const BASE = {
48
+ dateLabel: '9月20日 周日',
49
+ dateKey: '2026-09-20',
50
+ stage: { title: '跑通端到端最小闭环', order: 3, total: 6 },
51
+ items: ITEMS,
52
+ doneCount: 1,
53
+ total: ITEMS.length,
54
+ battery: 76,
55
+ facts: { evidence: 14, mainDone: 3 },
56
+ };
57
+
58
+ const BLANK = { ...BASE, dateLabel: '', dateKey: '', items: [], total: 0, doneCount: 0, battery: null, stage: null };
59
+
60
+ /** 顺序 = 设备上实际会遇到的顺序:先固件兜底,再服务端正常卡面 */
61
+ const CARDS = [
62
+ ['固件兜底 · 开机', BLANK, { kind: 'booting', title: '正在连接' }],
63
+ ['固件兜底 · 离线', BLANK, { kind: 'offline', title: '连不上电脑' }],
64
+ ['固件兜底 · 未配置', BLANK, { kind: 'setup', title: '还没配置' }],
65
+ ['运行时 · 今日总览', BASE, { kind: 'overview', title: '今日总览' }],
66
+ ['运行时 · 主线任务', BASE, { kind: 'task', itemIndex: 0, title: '主线' }],
67
+ ['运行时 · 已完成的小事', BASE, { kind: 'task', itemIndex: 1, title: '小事' }],
68
+ ['运行时 · 全部做完', { ...BASE, doneCount: 3, items: ITEMS.map((i) => ({ ...i, done: true, status: 'COMPLETED' })) }, { kind: 'alldone', title: '今天做完了' }],
69
+ ['运行时 · 今天没排计划', { ...BASE, items: [], total: 0, doneCount: 0, stage: null }, { kind: 'noplan', title: '今天还没排计划' }],
70
+ ];
71
+
72
+ async function renderCard(state, spec) {
73
+ const html = buildCardHtml(state, spec);
74
+ return screenshotPng(html, { width: CARD_WIDTH, height: CARD_HEIGHT });
75
+ }
76
+
77
+ function sheetHtml(entries) {
78
+ const cols = 4;
79
+ const cellW = CARD_WIDTH * scale;
80
+ const imgH = CARD_HEIGHT * scale;
81
+ const labelH = 26;
82
+ const gap = 20;
83
+ const pad = 26;
84
+ const rows = Math.ceil(entries.length / cols);
85
+ const w = pad * 2 + cols * cellW + (cols - 1) * gap;
86
+ const h = pad * 2 + rows * (labelH + imgH) + (rows - 1) * gap;
87
+
88
+ const cells = entries.map(([label, png]) => `
89
+ <figure>
90
+ <figcaption>${label}</figcaption>
91
+ <img src="data:image/png;base64,${png.toString('base64')}" alt="${label}">
92
+ </figure>`).join('');
93
+
94
+ return { html: `<!DOCTYPE html><html><head><meta charset="utf-8"><style>
95
+ *{margin:0;padding:0;box-sizing:border-box}
96
+ html,body{width:${w}px;height:${h}px;background:#0b0f14;
97
+ font-family:"PingFang SC","Hiragino Sans GB","Microsoft YaHei",sans-serif;
98
+ -webkit-font-smoothing:antialiased}
99
+ .sheet{display:grid;grid-template-columns:repeat(${cols},${cellW}px);gap:${gap}px;padding:${pad}px}
100
+ figure{display:flex;flex-direction:column;gap:6px}
101
+ figcaption{height:${labelH - 6}px;line-height:${labelH - 6}px;font-size:13px;color:#9fb6c9;letter-spacing:.2px}
102
+ img{width:${cellW}px;height:${imgH}px;display:block;border-radius:${30 * scale}px;image-rendering:pixelated;
103
+ box-shadow:0 0 0 1px rgba(140,190,225,.18),0 10px 28px rgba(0,0,0,.55)}
104
+ </style></head><body><div class="sheet">${cells}</div></body></html>`, width: w, height: h };
105
+ }
106
+
107
+ const entries = [];
108
+ for (const [label, state, spec] of CARDS) {
109
+ entries.push([label, await renderCard(state, spec)]);
110
+ console.log(`✓ 渲染 ${label}`);
111
+ }
112
+
113
+ const { html, width, height } = sheetHtml(entries);
114
+ console.log(`… 合成对照图 ${width}×${height}(每张卡放大 ${scale}×)`);
115
+ const sheet = await screenshotPng(html, { width, height });
116
+
117
+ mkdirSync(dirname(outPath), { recursive: true });
118
+ writeFileSync(outPath, sheet);
119
+ console.log(`\n✓ 对照图:${outPath}`);
120
+ console.log(' 这张图里的卡面与运行时 buildCardHtml() 完全同源,只是被像素级放大了。');
@@ -0,0 +1,92 @@
1
+ #!/usr/bin/env bash
2
+ # AI Growth — 重置「测试数据」
3
+ #
4
+ # 安全设计(§21 / 附录 C):
5
+ # · 默认把当前数据目录**改名备份**而不是删除,绝不静默销毁真实数据。
6
+ # · 若检测到疑似真实使用痕迹(有已确认 Goal 或较多样本),需要 --force 才继续。
7
+ #
8
+ # 用法:
9
+ # ./scripts/reset-dev-data.sh # 备份现有数据 → 建一个干净的新库
10
+ # ./scripts/reset-dev-data.sh --force # 跳过"疑似真实数据"的拦截
11
+ # ./scripts/reset-dev-data.sh --list # 只查看当前数据概况,不做任何修改
12
+
13
+ set -uo pipefail
14
+
15
+ PROJECT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
16
+ DATA_DIR="${AI_GROWTH_DATA_DIR:-$HOME/.ai-growth}"
17
+ NODE_BIN="$(command -v node || true)"
18
+
19
+ FORCE=0
20
+ case "${1:-}" in
21
+ --force) FORCE=1 ;;
22
+ --list) ;;
23
+ "") ;;
24
+ *) echo "未知参数:$1"; exit 1 ;;
25
+ esac
26
+
27
+ ok() { printf " \033[0;32m✓\033[0m %s\n" "$1"; }
28
+ warn() { printf " \033[0;33m!\033[0m %s\n" "$1"; }
29
+ die() { printf " \033[0;31m✗\033[0m %s\n" "$1"; exit 1; }
30
+
31
+ printf "\033[1mAI Growth 数据概况\033[0m\n数据目录:%s\n\n" "$DATA_DIR"
32
+
33
+ if [ ! -f "$DATA_DIR/growth.db" ]; then
34
+ warn "尚无数据库,无需重置"
35
+ exit 0
36
+ fi
37
+
38
+ [ -n "$NODE_BIN" ] || die "未找到 node"
39
+
40
+ STATS="$("$NODE_BIN" -e "
41
+ const P='$PROJECT_DIR/node_modules/better-sqlite3';
42
+ const D=require(P);
43
+ const db=new D('$DATA_DIR/growth.db',{readonly:true});
44
+ const q=(s)=>{try{return db.prepare(s).get().n}catch(e){return 0}};
45
+ console.log(JSON.stringify({
46
+ goals:q(\"SELECT COUNT(*) n FROM goals WHERE deleted_at IS NULL\"),
47
+ activeGoals:q(\"SELECT COUNT(*) n FROM goals WHERE status='ACTIVE'\"),
48
+ actions:q('SELECT COUNT(*) n FROM actions'),
49
+ evidence:q('SELECT COUNT(*) n FROM evidence'),
50
+ events:q('SELECT COUNT(*) n FROM growth_events'),
51
+ confirmed:q('SELECT COUNT(*) n FROM goals WHERE status IN (\\'ACTIVE\\',\\'CONFIRMED\\',\\'COMPLETED\\')')
52
+ }));
53
+ " 2>/dev/null)"
54
+
55
+ if [ -z "$STATS" ]; then
56
+ warn "无法读取统计(数据库可能损坏)"
57
+ STATS='{"goals":0,"activeGoals":0,"actions":0,"evidence":0,"events":0,"confirmed":0}'
58
+ fi
59
+
60
+ printf " Goal: %s(其中活跃 %s)\n Action: %s\n Evidence: %s\n Growth Event: %s\n\n" \
61
+ "$(echo "$STATS" | "$NODE_BIN" -p 'JSON.parse(require("fs").readFileSync(0,"utf8")).goals')" \
62
+ "$(echo "$STATS" | "$NODE_BIN" -p 'JSON.parse(require("fs").readFileSync(0,"utf8")).activeGoals')" \
63
+ "$(echo "$STATS" | "$NODE_BIN" -p 'JSON.parse(require("fs").readFileSync(0,"utf8")).actions')" \
64
+ "$(echo "$STATS" | "$NODE_BIN" -p 'JSON.parse(require("fs").readFileSync(0,"utf8")).evidence')" \
65
+ "$(echo "$STATS" | "$NODE_BIN" -p 'JSON.parse(require("fs").readFileSync(0,"utf8")).events')"
66
+
67
+ if [ "${1:-}" = "--list" ]; then
68
+ exit 0
69
+ fi
70
+
71
+ CONFIRMED="$(echo "$STATS" | "$NODE_BIN" -p 'JSON.parse(require("fs").readFileSync(0,"utf8")).confirmed')"
72
+ if [ "$CONFIRMED" -gt 0 ] && [ "$FORCE" -ne 1 ]; then
73
+ warn "检测到 $CONFIRMED 个已确认/活跃的 Goal —— 这看起来是真实使用数据,不是测试数据。"
74
+ warn "如需继续,请显式运行:./scripts/reset-dev-data.sh --force"
75
+ exit 1
76
+ fi
77
+
78
+ STAMP="$(date +%Y%m%d-%H%M%S)"
79
+ BACKUP="$DATA_DIR.backup-$STAMP"
80
+ printf "\033[1;33m▸ 将现有数据备份为:%s\033[0m\n" "$BACKUP"
81
+ mv "$DATA_DIR" "$BACKUP" || die "备份失败,已中止(原数据未改动)"
82
+ ok "备份完成(原数据完整保留在备份目录)"
83
+
84
+ mkdir -p "$DATA_DIR"
85
+ cd "$PROJECT_DIR"
86
+ if [ -f dist/db/cli.js ]; then
87
+ AI_GROWTH_DATA_DIR="$DATA_DIR" "$NODE_BIN" dist/db/cli.js migrate >/dev/null && ok "已创建干净的数据库"
88
+ else
89
+ warn "dist/db/cli.js 不存在,请先 npm run build,然后运行 npm run migrate"
90
+ fi
91
+
92
+ printf "\n完成。回滚方式:rm -rf \"%s\" && mv \"%s\" \"%s\"\n" "$DATA_DIR" "$BACKUP" "$DATA_DIR"