metame-cli 1.4.34 → 1.5.1

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 (48) hide show
  1. package/README.md +136 -94
  2. package/index.js +312 -57
  3. package/package.json +8 -4
  4. package/scripts/agent-layer.js +320 -0
  5. package/scripts/daemon-admin-commands.js +328 -28
  6. package/scripts/daemon-agent-commands.js +145 -6
  7. package/scripts/daemon-agent-tools.js +163 -7
  8. package/scripts/daemon-bridges.js +110 -20
  9. package/scripts/daemon-checkpoints.js +36 -7
  10. package/scripts/daemon-claude-engine.js +849 -358
  11. package/scripts/daemon-command-router.js +31 -10
  12. package/scripts/daemon-default.yaml +28 -4
  13. package/scripts/daemon-engine-runtime.js +328 -0
  14. package/scripts/daemon-exec-commands.js +15 -7
  15. package/scripts/daemon-notify.js +37 -1
  16. package/scripts/daemon-ops-commands.js +8 -6
  17. package/scripts/daemon-runtime-lifecycle.js +129 -5
  18. package/scripts/daemon-session-commands.js +60 -25
  19. package/scripts/daemon-session-store.js +121 -13
  20. package/scripts/daemon-task-scheduler.js +129 -49
  21. package/scripts/daemon-user-acl.js +35 -9
  22. package/scripts/daemon.js +268 -33
  23. package/scripts/distill.js +327 -18
  24. package/scripts/docs/agent-guide.md +12 -0
  25. package/scripts/docs/maintenance-manual.md +155 -0
  26. package/scripts/docs/pointer-map.md +110 -0
  27. package/scripts/feishu-adapter.js +42 -13
  28. package/scripts/hooks/stop-session-capture.js +243 -0
  29. package/scripts/memory-extract.js +105 -6
  30. package/scripts/memory-nightly-reflect.js +199 -11
  31. package/scripts/memory.js +134 -3
  32. package/scripts/mentor-engine.js +405 -0
  33. package/scripts/platform.js +24 -0
  34. package/scripts/providers.js +182 -22
  35. package/scripts/schema.js +12 -0
  36. package/scripts/session-analytics.js +245 -12
  37. package/scripts/skill-changelog.js +245 -0
  38. package/scripts/skill-evolution.js +288 -5
  39. package/scripts/telegram-adapter.js +12 -8
  40. package/scripts/usage-classifier.js +1 -1
  41. package/scripts/daemon-admin-commands.test.js +0 -333
  42. package/scripts/daemon-task-envelope.test.js +0 -59
  43. package/scripts/daemon-task-scheduler.test.js +0 -106
  44. package/scripts/reliability-core.test.js +0 -280
  45. package/scripts/skill-evolution.test.js +0 -113
  46. package/scripts/task-board.test.js +0 -83
  47. package/scripts/test_daemon.js +0 -1407
  48. package/scripts/utils.test.js +0 -192
package/index.js CHANGED
@@ -9,16 +9,45 @@ const os = require('os');
9
9
  const { spawn, execSync } = require('child_process');
10
10
  const { sleepSync, findProcessesByPattern, icon } = require('./scripts/platform');
11
11
 
12
- // On Windows, .cmd files (like claude.cmd from npm global) need shell:true to spawn.
13
- // We use COMSPEC to avoid conda/PATH issues where cmd.exe can't be found.
12
+ // On Windows, resolve .cmd wrapper actual Node.js entry and spawn node directly.
13
+ // Completely bypasses cmd.exe, eliminating terminal flash.
14
+ function resolveNodeEntry(cmdPath) {
15
+ try {
16
+ const content = fs.readFileSync(cmdPath, 'utf8');
17
+ const m = content.match(/"([^"]+\.js)"\s*%\*\s*$/m);
18
+ if (m) {
19
+ const entry = m[1].replace(/%dp0%/gi, path.dirname(cmdPath) + path.sep);
20
+ if (fs.existsSync(entry)) return entry;
21
+ }
22
+ } catch { /* ignore */ }
23
+ return null;
24
+ }
25
+
26
+ function spawnViaNode(cmd, args, options) {
27
+ if (process.platform !== 'win32') return spawn(cmd, args, options);
28
+ try {
29
+ const { execSync: _es } = require('child_process');
30
+ const lines = _es(`where ${cmd}`, { encoding: 'utf8', timeout: 3000 })
31
+ .split('\n').map(l => l.trim()).filter(Boolean);
32
+ const cmdFile = lines.find(l => l.toLowerCase().endsWith(`${cmd}.cmd`)) || lines[0];
33
+ if (cmdFile) {
34
+ const entry = resolveNodeEntry(cmdFile);
35
+ if (entry) return spawn(process.execPath, [entry, ...args], { ...options, windowsHide: true });
36
+ return spawn(cmdFile, args, { ...options, shell: process.env.COMSPEC || true, windowsHide: true });
37
+ }
38
+ } catch { /* ignore */ }
39
+ return spawn(cmd, args, { ...options, shell: process.env.COMSPEC || true, windowsHide: true });
40
+ }
41
+
14
42
  function spawnClaude(args, options) {
15
- if (process.platform === 'win32') {
16
- return spawn('claude', args, {
17
- ...options,
18
- shell: process.env.COMSPEC || true,
19
- });
20
- }
21
- return spawn('claude', args, options);
43
+ return spawnViaNode('claude', args, options);
44
+ }
45
+
46
+ function spawnCodex(args, options) {
47
+ // Sanitize env: unset CODEX_HOME if it points to a non-existent path (corrupted registry value)
48
+ const env = { ...(options && options.env ? options.env : process.env) };
49
+ if (env.CODEX_HOME && !fs.existsSync(env.CODEX_HOME)) delete env.CODEX_HOME;
50
+ return spawnViaNode('codex', args, { ...options, env });
22
51
  }
23
52
 
24
53
  // Quick flags (before heavy init)
@@ -88,7 +117,7 @@ function syncDirFiles(srcDir, destDir, { fileList, chmod } = {}) {
88
117
  // Auto-deploy bundled scripts to ~/.metame/
89
118
  // IMPORTANT: daemon.yaml is USER CONFIG — never overwrite it. Only daemon-default.yaml (template) is synced.
90
119
  const scriptsDir = path.join(__dirname, 'scripts');
91
- const BUNDLED_BASE_SCRIPTS = ['platform.js', 'signal-capture.js', 'distill.js', 'schema.js', 'pending-traits.js', 'migrate-v2.js', 'daemon.js', 'telegram-adapter.js', 'feishu-adapter.js', 'daemon-default.yaml', 'providers.js', 'session-analytics.js', 'resolve-yaml.js', 'utils.js', 'skill-evolution.js', 'memory.js', 'memory-extract.js', 'memory-search.js', 'memory-gc.js', 'qmd-client.js', 'session-summarize.js', 'check-macos-control-capabilities.sh', 'usage-classifier.js', 'task-board.js', 'memory-nightly-reflect.js', 'memory-index.js'];
120
+ const BUNDLED_BASE_SCRIPTS = ['platform.js', 'signal-capture.js', 'distill.js', 'schema.js', 'pending-traits.js', 'daemon.js', 'telegram-adapter.js', 'feishu-adapter.js', 'daemon-default.yaml', 'providers.js', 'session-analytics.js', 'resolve-yaml.js', 'utils.js', 'skill-evolution.js', 'memory.js', 'memory-extract.js', 'memory-search.js', 'memory-write.js', 'memory-gc.js', 'qmd-client.js', 'session-summarize.js', 'mentor-engine.js', 'check-macos-control-capabilities.sh', 'usage-classifier.js', 'task-board.js', 'memory-nightly-reflect.js', 'memory-index.js', 'skill-changelog.js', 'agent-layer.js'];
92
121
  const DAEMON_MODULE_SCRIPTS = (() => {
93
122
  try {
94
123
  return fs.readdirSync(scriptsDir).filter((f) => /^daemon-[\w-]+\.js$/.test(f));
@@ -110,20 +139,45 @@ try {
110
139
  }
111
140
  } catch { /* non-fatal */ }
112
141
 
113
- const scriptsUpdated = syncDirFiles(scriptsDir, METAME_DIR, { fileList: BUNDLED_SCRIPTS });
142
+ // Pre-deploy syntax validation: check all .js files before syncing to ~/.metame/
143
+ // Catches bad merges and careless agent edits BEFORE they can crash the daemon.
144
+ const { execSync: _execSync } = require('child_process');
145
+ const syntaxErrors = [];
146
+ for (const f of BUNDLED_SCRIPTS) {
147
+ if (!f.endsWith('.js')) continue;
148
+ const fp = path.join(scriptsDir, f);
149
+ if (!fs.existsSync(fp)) continue;
150
+ try {
151
+ _execSync(`"${process.execPath}" -c "${fp}"`, { timeout: 5000, stdio: 'pipe', windowsHide: true });
152
+ } catch (e) {
153
+ const msg = (e.stderr ? e.stderr.toString().trim() : e.message).split('\n')[0];
154
+ syntaxErrors.push(`${f}: ${msg}`);
155
+ }
156
+ }
114
157
 
115
- // Daemon restart on script update:
116
- // Don't kill daemon here — daemon's own file watcher detects ~/.metame/daemon.js changes
117
- // and has defer logic (waits for active Claude tasks to finish before restarting).
118
- // Killing here bypasses that and interrupts ongoing conversations.
119
- if (scriptsUpdated) {
120
- console.log(`${icon("pkg")} Scripts synced to ~/.metame/ — daemon will auto-restart when idle.`);
158
+ let scriptsUpdated = false;
159
+ if (syntaxErrors.length > 0) {
160
+ console.error(`${icon("warn")} DEPLOY BLOCKED syntax errors in ${syntaxErrors.length} file(s):`);
161
+ for (const err of syntaxErrors) console.error(` ${err}`);
162
+ console.error('Fix the errors before deploying. Daemon continues running with old code.');
163
+ } else {
164
+ scriptsUpdated = syncDirFiles(scriptsDir, METAME_DIR, { fileList: BUNDLED_SCRIPTS });
165
+
166
+ // Daemon restart on script update:
167
+ // Don't kill daemon here — daemon's own file watcher detects ~/.metame/daemon.js changes
168
+ // and has defer logic (waits for active Claude tasks to finish before restarting).
169
+ // Killing here bypasses that and interrupts ongoing conversations.
170
+ if (scriptsUpdated) {
171
+ console.log(`${icon("pkg")} Scripts synced to ~/.metame/ — daemon will auto-restart when idle.`);
172
+ }
121
173
  }
122
174
 
123
175
  // Docs: lazy-load references for CLAUDE.md pointer instructions
124
176
  syncDirFiles(path.join(__dirname, 'scripts', 'docs'), path.join(METAME_DIR, 'docs'));
125
177
  // Bin: CLI tools (dispatch_to etc.)
126
178
  syncDirFiles(path.join(__dirname, 'scripts', 'bin'), path.join(METAME_DIR, 'bin'), { chmod: 0o755 });
179
+ // Hooks: Claude Code event hooks (Stop, PostToolUse, etc.)
180
+ syncDirFiles(path.join(__dirname, 'scripts', 'hooks'), path.join(METAME_DIR, 'hooks'));
127
181
 
128
182
  // ---------------------------------------------------------
129
183
  // Deploy bundled skills to ~/.claude/skills/
@@ -163,6 +217,32 @@ if (fs.existsSync(bundledSkillsDir)) {
163
217
  }
164
218
  }
165
219
 
220
+ // Ensure ~/.codex/skills and ~/.agents/skills are symlinks to ~/.claude/skills
221
+ // This keeps skill evolution unified across all engines.
222
+ for (const altDir of [
223
+ path.join(HOME_DIR, '.codex', 'skills'),
224
+ path.join(HOME_DIR, '.agents', 'skills'),
225
+ ]) {
226
+ try {
227
+ const stat = fs.lstatSync(altDir);
228
+ if (stat.isSymbolicLink()) continue; // already a symlink, good
229
+ // Physical directory exists — back it up, then replace with symlink
230
+ const backupDir = altDir + '.bak.' + Date.now();
231
+ fs.renameSync(altDir, backupDir);
232
+ console.log(`[metame] Backed up existing ${altDir} → ${backupDir}`);
233
+ fs.symlinkSync(CLAUDE_SKILLS_DIR, altDir);
234
+ } catch (e) {
235
+ if (e.code === 'ENOENT') {
236
+ // Parent dir or target doesn't exist — try creating symlink
237
+ try {
238
+ fs.mkdirSync(path.dirname(altDir), { recursive: true });
239
+ fs.symlinkSync(CLAUDE_SKILLS_DIR, altDir);
240
+ } catch { /* non-fatal */ }
241
+ }
242
+ // Other errors (e.g. engine not installed): non-fatal, skip
243
+ }
244
+ }
245
+
166
246
  // Load daemon config for local launch flags
167
247
  let daemonCfg = {};
168
248
  try {
@@ -231,6 +311,8 @@ function ensureHookInstalled() {
231
311
  entry.hooks?.some(h => h.command && h.command.includes('signal-capture.js'))
232
312
  );
233
313
 
314
+ let modified = false;
315
+
234
316
  if (!stillInstalled) {
235
317
  if (!settings.hooks) settings.hooks = {};
236
318
  if (!settings.hooks.UserPromptSubmit) settings.hooks.UserPromptSubmit = [];
@@ -241,9 +323,33 @@ function ensureHookInstalled() {
241
323
  command: hookCommand
242
324
  }]
243
325
  });
326
+ modified = true;
327
+ console.log(`${icon("hook")} MetaMe: Signal capture hook installed.`);
328
+ }
329
+
330
+ // Ensure Stop hook (session-logger + tool-failure capture) is installed
331
+ const stopHookScript = path.join(METAME_DIR, 'hooks', 'stop-session-capture.js').replace(/\\/g, '/');
332
+ const stopHookCommand = `node "${stopHookScript}"`;
333
+ const stopHookInstalled = (settings.hooks?.Stop || []).some(entry =>
334
+ entry.hooks?.some(h => h.command && h.command.includes('stop-session-capture.js'))
335
+ );
336
+
337
+ if (!stopHookInstalled) {
338
+ if (!settings.hooks) settings.hooks = {};
339
+ if (!settings.hooks.Stop) settings.hooks.Stop = [];
340
+
341
+ settings.hooks.Stop.push({
342
+ hooks: [{
343
+ type: 'command',
344
+ command: stopHookCommand
345
+ }]
346
+ });
347
+ modified = true;
348
+ console.log(`${icon("hook")} MetaMe: Stop session capture hook installed.`);
349
+ }
244
350
 
351
+ if (modified) {
245
352
  fs.writeFileSync(CLAUDE_SETTINGS, JSON.stringify(settings, null, 2), 'utf8');
246
- console.log(`${icon("hook")} MetaMe: Signal capture hook installed.`);
247
353
  }
248
354
  } catch (e) {
249
355
  // Non-fatal: hook install failure shouldn't block launch
@@ -294,7 +400,7 @@ function needsBootstrap() {
294
400
  } catch { return true; }
295
401
  }
296
402
 
297
- function spawnDistillBackground() {
403
+ function spawnDistillBackground(engine) {
298
404
  const distillPath = path.join(METAME_DIR, 'distill.js');
299
405
  if (!fs.existsSync(distillPath)) return;
300
406
 
@@ -333,14 +439,16 @@ function spawnDistillBackground() {
333
439
  }
334
440
 
335
441
 
336
- // Spawn as detached background process — won't block Claude launch
337
- // Remove CLAUDECODE env var so distill.js can call `claude -p` without nested-session rejection
442
+ // Spawn as detached background process — won't block session launch
443
+ // Remove CLAUDECODE env var so distill.js can call the engine without nested-session rejection
338
444
  const distillEnvClean = { ...process.env };
339
445
  delete distillEnvClean.CLAUDECODE;
446
+ if (engine) distillEnvClean.METAME_ENGINE = engine;
340
447
  const bg = spawn('node', [distillPath], {
341
448
  detached: true,
342
449
  stdio: 'ignore',
343
450
  env: distillEnvClean,
451
+ windowsHide: true,
344
452
  });
345
453
  bg.unref();
346
454
  }
@@ -519,18 +627,45 @@ This step connects the bot to the user's PRIVATE chat — this is the admin chan
519
627
  6. Tell user to run \`metame start\` to activate.
520
628
 
521
629
  - If **Feishu:**
522
- 1. Guide through: open.feishu.cn/app create app get App ID + Secret enable bot → add event subscription (long connection mode) → add permissions (im:message, im:message.p2p_msg:readonly, im:message.group_at_msg:readonly, im:message:send_as_bot, im:resource) → publish.
523
- **${icon("warn")} 重要:** 在「事件订阅」页面,必须开启「接收消息 im.message.receive_v1」事件。然后在该事件的配置中,勾选「获取群组中所有消息」(否则 bot 在群聊中只能收到 @它 的消息,无法接收普通群消息)。
524
- 2. Ask user to paste App ID and App Secret.
525
- 3. Write \`app_id\` and \`app_secret\` into \`~/.metame/daemon.yaml\` under \`feishu:\` section, set \`enabled: true\`.
526
- 4. Tell user: "Now open Feishu and send any message to your new bot (private chat), then tell me you're done."
527
- 5. After user confirms, auto-fetch the chat ID:
630
+ Walk the user through these steps IN ORDER. Confirm each step before proceeding to the next.
631
+
632
+ **阶段一:创建应用,获取凭证(先拿到钥匙)**
633
+ 1. 打开 open.feishu.cn 开发者后台 创建企业自建应用,填写名称和描述(随意)。
634
+ 2. 进入应用 →「凭证与基础信息」→ 复制 App ID App Secret。
635
+ 3. 进入「应用功能」→「机器人」→ 开启机器人功能(点击启用)。
636
+ 4. 进入「权限管理」→ 依次搜索并开通以下权限:
637
+ - \`im:message\`
638
+ - \`im:message.p2p_msg:readonly\`
639
+ - \`im:message.group_at_msg:readonly\`
640
+ - \`im:message:send_as_bot\`
641
+ - \`im:resource\`
642
+ 5. Ask user to paste their App ID and App Secret.
643
+ 6. Write \`app_id\` and \`app_secret\` into \`~/.metame/daemon.yaml\` under \`feishu:\` section, set \`enabled: true\`.
644
+
645
+ **阶段二:启动 daemon,建立长连接(必须先跑起来)**
646
+ 7. Tell user to run \`metame start\`.
647
+ 8. Run \`metame status\` and confirm the output contains "Feishu bot connected". **${icon("warn")} 必须看到这行才能继续** — 飞书控制台只有在检测到活跃连接后才允许保存事件配置。
648
+
649
+ **阶段三:飞书控制台完成事件订阅(回去点保存)**
650
+ 9. 回到飞书开放平台 →「事件与回调」→「事件配置」→ 选择「使用长连接接收事件」。
651
+ 10. 点击「添加事件」→ 搜索并添加「接收消息 im.message.receive_v1」。
652
+ 11. **${icon("warn")} 关键:** 点击该事件右侧「申请权限」→ 勾选「获取群组中所有消息」。不勾选则 bot 在群聊中只能收到 @ 它的消息。
653
+ 12. 点击「保存配置」。此时控制台检测长连接,daemon 已在线,保存会通过。
654
+ 13. 进入「版本管理与发布」→ 创建版本 → 申请发布(企业自建应用可直接发布,无需审核)。
655
+
656
+ **阶段四:获取 chat_id,完成私聊绑定**
657
+ 14. Tell user: 在飞书里搜索刚创建的机器人名称,打开私聊,发送任意一条消息(如"你好")。
658
+ 15. After user confirms, auto-fetch the private chat ID:
528
659
  \`\`\`bash
529
- TOKEN=$(curl -s -X POST https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal -H "Content-Type: application/json" -d '{"app_id":"<APP_ID>","app_secret":"<APP_SECRET>"}' | jq -r '.tenant_access_token')
530
- curl -s -H "Authorization: Bearer $TOKEN" https://open.feishu.cn/open-apis/im/v1/chats | jq '.data.items[] | {chat_id, name, chat_type}'
660
+ TOKEN=$(curl -s -X POST https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal \\
661
+ -H "Content-Type: application/json" \\
662
+ -d '{"app_id":"<APP_ID>","app_secret":"<APP_SECRET>"}' | jq -r '.tenant_access_token')
663
+ curl -s -H "Authorization: Bearer $TOKEN" \\
664
+ "https://open.feishu.cn/open-apis/im/v1/chats?chat_type=p2p" | jq '.data.items[] | {chat_id, name}'
531
665
  \`\`\`
532
- 6. Write the discovered \`chat_id\`(s) into \`allowed_chat_ids\` in \`~/.metame/daemon.yaml\`.
533
- 7. Tell user to run \`metame start\` to activate.
666
+ 16. Write the discovered \`chat_id\` into \`allowed_chat_ids\` in \`~/.metame/daemon.yaml\`.
667
+ 17. Run \`metame stop && metame start\` to reload config.
668
+ 18. Tell user to send a message in the Feishu private chat — they should receive a reply from MetaMe. Setup complete.
534
669
 
535
670
  - If **Skip:** Say "No problem. You can run \`metame daemon init\` anytime to set this up later." Then begin normal work.
536
671
 
@@ -577,25 +712,39 @@ if (fs.existsSync(PROJECT_FILE)) {
577
712
  }
578
713
 
579
714
  // Determine if this is a known (calibrated) user
715
+ // Cache the parsed doc to avoid re-reading BRAIN_FILE in mirror/reflection sections below.
580
716
  const yaml = require('js-yaml');
581
717
  let isKnownUser = false;
718
+ let _brainDoc = null;
582
719
  try {
583
720
  if (fs.existsSync(BRAIN_FILE)) {
584
- const doc = yaml.load(fs.readFileSync(BRAIN_FILE, 'utf8')) || {};
585
- if (doc.identity && doc.identity.locale && doc.identity.locale !== 'null') {
586
- isKnownUser = true;
587
- }
721
+ _brainDoc = yaml.load(fs.readFileSync(BRAIN_FILE, 'utf8')) || {};
722
+ const id = _brainDoc.identity || {};
723
+ const hasLocale = id.locale && id.locale !== 'null' && id.locale !== null;
724
+ // Exclude default placeholder values written by genesis scaffolding
725
+ const hasName = id.name && id.name !== 'Unknown' && id.name !== 'null';
726
+ const hasRole = id.role && id.role !== 'Unknown' && id.role !== 'null';
727
+ const hasOtherFields = hasName || hasRole || id.timezone ||
728
+ (_brainDoc.status && _brainDoc.status.focus && _brainDoc.status.focus !== 'Initializing');
729
+ if (hasLocale || hasOtherFields) isKnownUser = true;
588
730
  }
589
731
  } catch (e) {
590
732
  // Ignore error, treat as unknown
591
733
  }
592
734
 
735
+ // Non-session commands (daemon ops, version, help) should not show genesis message
736
+ const _arg2 = process.argv[2];
737
+ const _isNonSessionCmd = ['daemon', 'start', 'stop', 'status', 'logs', 'codex',
738
+ 'sync', 'continue', '-v', '--version', '-h', '--help', 'distill', 'evolve'].includes(_arg2);
739
+
593
740
  let finalProtocol;
594
741
  if (isKnownUser) {
595
742
  finalProtocol = PROTOCOL_NORMAL;
596
743
  } else {
597
744
  finalProtocol = PROTOCOL_ONBOARDING;
598
- console.log(`${icon("new")} New user detected — entering Genesis interview mode...`);
745
+ if (!_isNonSessionCmd) {
746
+ console.log(`${icon("new")} New user detected — entering Genesis interview mode...`);
747
+ }
599
748
  }
600
749
 
601
750
  // ---------------------------------------------------------
@@ -603,8 +752,8 @@ if (isKnownUser) {
603
752
  // ---------------------------------------------------------
604
753
  let mirrorLine = '';
605
754
  try {
606
- if (isKnownUser && fs.existsSync(BRAIN_FILE)) {
607
- const brainDoc = yaml.load(fs.readFileSync(BRAIN_FILE, 'utf8')) || {};
755
+ if (isKnownUser && _brainDoc) {
756
+ const brainDoc = _brainDoc;
608
757
 
609
758
  // Check quiet mode
610
759
  const quietUntil = brainDoc.growth && brainDoc.growth.quiet_until;
@@ -664,8 +813,8 @@ try {
664
813
  // This ensures reflections don't fire every session.
665
814
  let reflectionLine = '';
666
815
  try {
667
- if (isKnownUser && fs.existsSync(BRAIN_FILE)) {
668
- const refDoc = yaml.load(fs.readFileSync(BRAIN_FILE, 'utf8')) || {};
816
+ if (isKnownUser && _brainDoc) {
817
+ const refDoc = _brainDoc;
669
818
 
670
819
  // Check quiet mode
671
820
  const quietUntil = refDoc.growth && refDoc.growth.quiet_until;
@@ -743,7 +892,8 @@ const GLOBAL_MARKER_END = '<!-- METAME-GLOBAL:END -->';
743
892
  // Build dynamic Agent dispatch table from daemon.yaml projects.
744
893
  // Only include agents whose cwd actually exists on disk — test/stale agents
745
894
  // with deleted paths are automatically excluded, no manual cleanup needed.
746
- let dispatchTable = '';
895
+ // The table is written to ~/.metame/docs/dispatch-table.md (NOT inlined into CLAUDE.md).
896
+ const DISPATCH_TABLE_PATH = path.join(METAME_DIR, 'docs', 'dispatch-table.md');
747
897
  try {
748
898
  const daemonYamlPath = path.join(os.homedir(), '.metame', 'daemon.yaml');
749
899
  if (fs.existsSync(daemonYamlPath)) {
@@ -752,13 +902,29 @@ try {
752
902
  const rows = Object.entries(projects)
753
903
  .filter(([, p]) => {
754
904
  if (!p || !p.name || !p.cwd) return false;
755
- // Expand ~ to home directory
756
905
  const expandedCwd = String(p.cwd).replace(/^~/, os.homedir());
757
906
  return fs.existsSync(expandedCwd);
758
907
  })
759
908
  .map(([key, p]) => `| \`${key}\` | ${p.name} |`);
760
909
  if (rows.length > 0) {
761
- dispatchTable = '\n\n| project_key | 昵称 |\n|-------------|------|\n' + rows.join('\n') + '\n\n`--new` 强制新建会话(用户说"新开会话"时加此参数)。';
910
+ const tableContent = [
911
+ '# Agent Dispatch 路由表',
912
+ '',
913
+ '> 自动生成,来源:daemon.yaml。勿手动编辑。',
914
+ '',
915
+ '| project_key | 昵称 |',
916
+ '|-------------|------|',
917
+ ...rows,
918
+ '',
919
+ '## 使用方法',
920
+ '```bash',
921
+ '~/.metame/bin/dispatch_to [--new] <project_key> "内容"',
922
+ '```',
923
+ '`--new` 强制新建会话(用户说"新开会话"时加此参数)。',
924
+ '新增 Agent:`/agent bind <名称> <工作目录>`',
925
+ ].join('\n') + '\n';
926
+ fs.mkdirSync(path.dirname(DISPATCH_TABLE_PATH), { recursive: true });
927
+ fs.writeFileSync(DISPATCH_TABLE_PATH, tableContent);
762
928
  }
763
929
  }
764
930
  } catch { /* daemon.yaml missing or invalid — skip dispatch table */ }
@@ -771,11 +937,11 @@ const KERNEL_BODY = PROTOCOL_NORMAL
771
937
 
772
938
  const CAPABILITY_SECTIONS = [
773
939
  '## Agent Dispatch',
774
- `"告诉X/让X" → \`~/.metame/bin/dispatch_to <project_key> "内容"\`,手机端 \`/dispatch to <key> <消息>\`。` + dispatchTable,
775
- '新增 Agent:`/agent bind <名称> <工作目录>`',
940
+ '识别到"告诉X/让X/通知X"等转发意图时先 `cat ~/.metame/docs/dispatch-table.md` 获取路由表(昵称→project_key),再执行转发。不要凭记忆猜测昵称对应关系。',
776
941
  '',
777
942
  '## Agent 创建与管理',
778
943
  '用户问创建/管理/绑定 Agent 时 → 先 `cat ~/.metame/docs/agent-guide.md` 再回答。',
944
+ '用户问代码结构/升级进度/脚本入口时 → 先 `cat ~/.metame/docs/pointer-map.md` 再回答。',
779
945
  '',
780
946
  '## 手机端文件交互',
781
947
  '用户要文件("发给我"/"发过来"/"导出")→ 先 `cat ~/.metame/docs/file-transfer.md` 再执行。',
@@ -882,7 +1048,37 @@ try {
882
1048
  }
883
1049
  } catch { /* non-fatal */ }
884
1050
 
1051
+ // Skill evolution status
1052
+ try {
1053
+ const skillChangelog = require('./scripts/skill-changelog');
1054
+ const skillCount = skillChangelog.countInstalledSkills();
1055
+ const lastSession = skillChangelog.getLastSessionStart();
1056
+ const recentChanges = skillChangelog.getRecentChanges(lastSession);
885
1057
 
1058
+ if (recentChanges.length === 0) {
1059
+ console.log(`${icon("tool")} Skills: ${skillCount} installed · 无新变更`);
1060
+ } else {
1061
+ const evolved = recentChanges.filter(c => c.action === 'evolved');
1062
+ const others = recentChanges.filter(c => c.action !== 'evolved');
1063
+ const parts = [`${skillCount} installed`];
1064
+ if (evolved.length > 0) parts.push(`${evolved.length} evolved since last session`);
1065
+ if (others.length > 0) parts.push(`${others.length} other event${others.length > 1 ? 's' : ''}`);
1066
+ console.log(`${icon("tool")} Skills: ${parts.join(' · ')}`);
1067
+
1068
+ // Show up to 3 details
1069
+ const shown = recentChanges.slice(0, 3);
1070
+ for (const c of shown) {
1071
+ const actionIcon = skillChangelog.getActionIcon(c.action);
1072
+ console.log(` ${actionIcon} ${c.skill || 'system'}: ${c.summary}`);
1073
+ }
1074
+ if (recentChanges.length > 3) {
1075
+ console.log(` +${recentChanges.length - 3} more`);
1076
+ }
1077
+ }
1078
+
1079
+ // Write session start marker for next time
1080
+ skillChangelog.writeSessionStart();
1081
+ } catch { /* non-fatal */ }
886
1082
 
887
1083
  // ---------------------------------------------------------
888
1084
  // 4.9 AUTO-UPDATE CHECK (non-blocking)
@@ -1352,6 +1548,7 @@ if (isDaemon) {
1352
1548
  const DAEMON_CONFIG = path.join(METAME_DIR, 'daemon.yaml');
1353
1549
  const DAEMON_STATE = path.join(METAME_DIR, 'daemon_state.json');
1354
1550
  const DAEMON_PID = path.join(METAME_DIR, 'daemon.pid');
1551
+ const DAEMON_LOCK = path.join(METAME_DIR, 'daemon.lock');
1355
1552
  const DAEMON_LOG = path.join(METAME_DIR, 'daemon.log');
1356
1553
  const DAEMON_DEFAULT = path.join(__dirname, 'scripts', 'daemon-default.yaml');
1357
1554
  const DAEMON_SCRIPT = path.join(METAME_DIR, 'daemon.js');
@@ -1682,11 +1879,10 @@ WantedBy=default.target
1682
1879
  }
1683
1880
  // Use caffeinate on macOS to prevent sleep while daemon is running
1684
1881
  const isMac = process.platform === 'darwin';
1685
- const isWin = process.platform === 'win32';
1686
1882
  const cmd = isMac ? 'caffeinate' : process.execPath;
1687
1883
  const args = isMac ? ['-i', process.execPath, DAEMON_SCRIPT] : [DAEMON_SCRIPT];
1688
1884
  const bg = spawn(cmd, args, {
1689
- detached: !isWin,
1885
+ detached: true,
1690
1886
  stdio: 'ignore',
1691
1887
  windowsHide: true,
1692
1888
  env: { ...process.env, HOME: HOME_DIR, METAME_ROOT: __dirname },
@@ -1729,14 +1925,26 @@ WantedBy=default.target
1729
1925
 
1730
1926
  // Check if running
1731
1927
  let isRunning = false;
1928
+ let runningPid = null;
1732
1929
  if (fs.existsSync(DAEMON_PID)) {
1733
1930
  const pid = parseInt(fs.readFileSync(DAEMON_PID, 'utf8').trim(), 10);
1734
- try { process.kill(pid, 0); isRunning = true; } catch { /* dead */ }
1931
+ try { process.kill(pid, 0); isRunning = true; runningPid = pid; } catch { /* dead */ }
1932
+ }
1933
+ if (!isRunning && fs.existsSync(DAEMON_LOCK)) {
1934
+ try {
1935
+ const lock = JSON.parse(fs.readFileSync(DAEMON_LOCK, 'utf8'));
1936
+ const pid = parseInt(lock && lock.pid, 10);
1937
+ if (pid) {
1938
+ process.kill(pid, 0);
1939
+ isRunning = true;
1940
+ runningPid = pid;
1941
+ }
1942
+ } catch { /* lock stale or invalid */ }
1735
1943
  }
1736
1944
 
1737
1945
  console.log(`${icon("bot")} MetaMe Daemon: ${isRunning ? icon("green") + ' Running' : icon("red") + ' Stopped'}`);
1738
1946
  if (state.started_at) console.log(` Started: ${state.started_at}`);
1739
- if (state.pid) console.log(` PID: ${state.pid}`);
1947
+ if (runningPid || state.pid) console.log(` PID: ${runningPid || state.pid}`);
1740
1948
 
1741
1949
  // Budget
1742
1950
  const budget = state.budget || {};
@@ -1806,7 +2014,12 @@ WantedBy=default.target
1806
2014
  }
1807
2015
 
1808
2016
  // Unknown subcommand
1809
- console.log(`${icon("book")} MetaMe Daemon Commands:`);
2017
+ console.log(`${icon("book")} MetaMe Commands:`);
2018
+ console.log(" metame — launch Claude with MetaMe init");
2019
+ console.log(" metame codex [args] — launch Codex with MetaMe init");
2020
+ console.log(" metame continue — resume latest session");
2021
+ console.log("");
2022
+ console.log(`${icon("book")} Daemon Commands:`);
1810
2023
  console.log(" metame start — start background daemon");
1811
2024
  console.log(" metame stop — stop daemon");
1812
2025
  console.log(" metame status — show status & budget");
@@ -1823,8 +2036,54 @@ WantedBy=default.target
1823
2036
  process.exit(0);
1824
2037
  }
1825
2038
 
2039
+ const GENESIS_TRIGGER_PROMPT = 'MANDATORY FIRST ACTION: The user has not been calibrated yet. You MUST start the Genesis Protocol interview from CLAUDE.md IMMEDIATELY — do NOT answer any other question first. Begin with the Trust Contract.';
2040
+
2041
+ // ---------------------------------------------------------
2042
+ // 5.8 CODEX — launch Codex with MetaMe initialization
2043
+ // ---------------------------------------------------------
2044
+ const isCodex = process.argv[2] === 'codex';
2045
+ if (isCodex) {
2046
+ const codexUserArgs = process.argv.slice(3);
2047
+ const codexProviderEnv = (() => { try { return require(path.join(__dirname, 'scripts', 'providers.js')).buildActiveEnv(); } catch { return {}; } })();
2048
+
2049
+ // Genesis: new user + interactive mode — trigger profile interview within the same Codex session.
2050
+ // CLAUDE.md (already written to disk above) contains the full genesis protocol; Codex reads it.
2051
+ // We pass the trigger as the opening [PROMPT] argument so genesis flows into normal work seamlessly.
2052
+ const codexArgs = codexUserArgs.length === 0
2053
+ ? ['--dangerously-bypass-approvals-and-sandbox']
2054
+ : ['exec', '--dangerously-bypass-approvals-and-sandbox', ...codexUserArgs];
2055
+
2056
+ // Codex reads AGENTS.md (not CLAUDE.md); create symlink so genesis protocol is visible.
2057
+ // Also ensure global ~/AGENTS.md → ~/.claude/CLAUDE.md for identity context.
2058
+ // Use try-catch on symlinkSync directly (avoids TOCTOU race from existsSync pre-check).
2059
+ try {
2060
+ if (fs.existsSync(path.join(process.cwd(), 'CLAUDE.md')))
2061
+ fs.symlinkSync('CLAUDE.md', path.join(process.cwd(), 'AGENTS.md'));
2062
+ } catch { /* EEXIST or other — non-critical */ }
2063
+ try {
2064
+ const globalClaudeMd = path.join(HOME_DIR, '.claude', 'CLAUDE.md');
2065
+ if (fs.existsSync(globalClaudeMd))
2066
+ fs.symlinkSync(globalClaudeMd, path.join(HOME_DIR, 'AGENTS.md'));
2067
+ } catch { /* EEXIST or other — non-critical */ }
2068
+
2069
+ const child = spawnCodex(codexArgs, {
2070
+ stdio: 'inherit',
2071
+ cwd: process.cwd(),
2072
+ env: { ...process.env, ...codexProviderEnv, METAME_ACTIVE_SESSION: 'true' },
2073
+ });
2074
+ let launchError = false;
2075
+ child.on('error', (err) => {
2076
+ launchError = true;
2077
+ console.error(`\n${icon("fail")} Error: Could not launch 'codex': ${err.message}`);
2078
+ console.error(" Please install: npm install -g @openai/codex");
2079
+ });
2080
+ child.on('close', (code) => process.exit(launchError ? 127 : (code || 0)));
2081
+ spawnDistillBackground('codex');
2082
+ return;
2083
+ }
2084
+
1826
2085
  // ---------------------------------------------------------
1827
- // 5.8 CONTINUE/SYNC — resume latest session from terminal
2086
+ // 5.9 CONTINUE/SYNC — resume latest session from terminal
1828
2087
  // ---------------------------------------------------------
1829
2088
  // Usage: exit Claude first, then run `metame continue` from terminal.
1830
2089
  // Finds the most recent session and launches Claude with --resume.
@@ -1910,10 +2169,7 @@ if (daemonCfg.dangerously_skip_permissions && !launchArgs.includes('--dangerousl
1910
2169
  launchArgs.push('--dangerously-skip-permissions');
1911
2170
  }
1912
2171
  if (!isKnownUser) {
1913
- launchArgs.push(
1914
- '--append-system-prompt',
1915
- 'MANDATORY FIRST ACTION: The user has not been calibrated yet. You MUST start the Genesis Protocol interview from CLAUDE.md IMMEDIATELY — do NOT answer any other question first. Begin with the Trust Contract.'
1916
- );
2172
+ launchArgs.push('--append-system-prompt', GENESIS_TRIGGER_PROMPT);
1917
2173
  }
1918
2174
 
1919
2175
  // RAG: inject relevant facts based on current project (desktop-side equivalent of daemon RAG)
@@ -1961,11 +2217,10 @@ try {
1961
2217
  }
1962
2218
  if (!daemonRunning) {
1963
2219
  const _isMac = process.platform === 'darwin';
1964
- const _isWin = process.platform === 'win32';
1965
2220
  const dCmd = _isMac ? 'caffeinate' : process.execPath;
1966
2221
  const dArgs = _isMac ? ['-i', process.execPath, _daemonScript] : [_daemonScript];
1967
2222
  const bg = spawn(dCmd, dArgs, {
1968
- detached: !_isWin,
2223
+ detached: true,
1969
2224
  stdio: 'ignore',
1970
2225
  windowsHide: true,
1971
2226
  env: { ...process.env, HOME: HOME_DIR, METAME_ROOT: __dirname },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "metame-cli",
3
- "version": "1.4.34",
3
+ "version": "1.5.1",
4
4
  "description": "The Cognitive Profile Layer for Claude Code. Knows how you think, not just what you said.",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -8,14 +8,18 @@
8
8
  },
9
9
  "files": [
10
10
  "index.js",
11
- "scripts/"
11
+ "scripts/",
12
+ "!scripts/*.test.js",
13
+ "!scripts/test_daemon.js",
14
+ "!scripts/hooks/test-*.js"
12
15
  ],
13
16
  "scripts": {
14
17
  "test": "node --test scripts/*.test.js",
18
+ "test:daemon-status": "node --test scripts/daemon-restart-status.test.js",
15
19
  "start": "node index.js",
16
- "sync:plugin": "cp scripts/platform.js scripts/schema.js scripts/pending-traits.js scripts/signal-capture.js scripts/distill.js scripts/daemon.js scripts/daemon-agent-commands.js scripts/daemon-session-commands.js scripts/daemon-admin-commands.js scripts/daemon-exec-commands.js scripts/daemon-ops-commands.js scripts/daemon-session-store.js scripts/daemon-checkpoints.js scripts/daemon-bridges.js scripts/daemon-file-browser.js scripts/daemon-runtime-lifecycle.js scripts/daemon-notify.js scripts/daemon-claude-engine.js scripts/daemon-command-router.js scripts/daemon-user-acl.js scripts/daemon-agent-tools.js scripts/daemon-task-scheduler.js scripts/daemon-task-envelope.js scripts/task-board.js scripts/telegram-adapter.js scripts/feishu-adapter.js scripts/daemon-default.yaml scripts/providers.js scripts/utils.js scripts/usage-classifier.js scripts/resolve-yaml.js scripts/memory.js scripts/memory-extract.js scripts/qmd-client.js scripts/session-summarize.js scripts/session-analytics.js scripts/skill-evolution.js scripts/check-macos-control-capabilities.sh plugin/scripts/ && echo 'Plugin scripts synced'",
20
+ "sync:plugin": "cp scripts/platform.js scripts/schema.js scripts/pending-traits.js scripts/signal-capture.js scripts/distill.js scripts/daemon.js scripts/daemon-agent-commands.js scripts/daemon-session-commands.js scripts/daemon-admin-commands.js scripts/daemon-exec-commands.js scripts/daemon-ops-commands.js scripts/daemon-session-store.js scripts/daemon-checkpoints.js scripts/daemon-bridges.js scripts/daemon-file-browser.js scripts/daemon-runtime-lifecycle.js scripts/daemon-notify.js scripts/daemon-claude-engine.js scripts/daemon-engine-runtime.js scripts/daemon-command-router.js scripts/daemon-user-acl.js scripts/daemon-agent-tools.js scripts/daemon-task-scheduler.js scripts/daemon-task-envelope.js scripts/task-board.js scripts/telegram-adapter.js scripts/feishu-adapter.js scripts/daemon-default.yaml scripts/providers.js scripts/utils.js scripts/usage-classifier.js scripts/resolve-yaml.js scripts/memory.js scripts/memory-write.js scripts/memory-extract.js scripts/memory-search.js scripts/memory-gc.js scripts/memory-nightly-reflect.js scripts/memory-index.js scripts/qmd-client.js scripts/session-summarize.js scripts/session-analytics.js scripts/mentor-engine.js scripts/skill-evolution.js scripts/skill-changelog.js scripts/agent-layer.js scripts/check-macos-control-capabilities.sh plugin/scripts/ && echo 'Plugin scripts synced'",
17
21
  "sync:readme": "node scripts/sync-readme.js",
18
- "restart:daemon": "node index.js stop 2>/dev/null; sleep 1; node index.js start 2>/dev/null || echo '⚠️ Daemon not running or restart failed'",
22
+ "restart:daemon": "node index.js stop 2>/dev/null; sleep 1; node index.js start 2>/dev/null || echo '鈿狅笍 Daemon not running or restart failed'",
19
23
  "precommit": "npm run sync:plugin && npm run restart:daemon"
20
24
  },
21
25
  "keywords": [