metame-cli 1.5.0 → 1.5.2

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.
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', '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'];
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,14 +139,37 @@ 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
@@ -172,19 +224,22 @@ for (const altDir of [
172
224
  path.join(HOME_DIR, '.agents', 'skills'),
173
225
  ]) {
174
226
  try {
175
- const parentDir = path.dirname(altDir);
176
- if (!fs.existsSync(parentDir)) continue; // engine not installed, skip
177
227
  const stat = fs.lstatSync(altDir);
178
228
  if (stat.isSymbolicLink()) continue; // already a symlink, good
179
- // Physical directory exists — replace with symlink
180
- fs.rmSync(altDir, { recursive: true, force: true });
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}`);
181
233
  fs.symlinkSync(CLAUDE_SKILLS_DIR, altDir);
182
234
  } catch (e) {
183
235
  if (e.code === 'ENOENT') {
184
- // Directory doesn't exist — create symlink
185
- try { fs.symlinkSync(CLAUDE_SKILLS_DIR, altDir); } catch { /* non-fatal */ }
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 */ }
186
241
  }
187
- // Other errors: non-fatal, skip
242
+ // Other errors (e.g. engine not installed): non-fatal, skip
188
243
  }
189
244
  }
190
245
 
@@ -345,7 +400,7 @@ function needsBootstrap() {
345
400
  } catch { return true; }
346
401
  }
347
402
 
348
- function spawnDistillBackground() {
403
+ function spawnDistillBackground(engine) {
349
404
  const distillPath = path.join(METAME_DIR, 'distill.js');
350
405
  if (!fs.existsSync(distillPath)) return;
351
406
 
@@ -384,14 +439,16 @@ function spawnDistillBackground() {
384
439
  }
385
440
 
386
441
 
387
- // Spawn as detached background process — won't block Claude launch
388
- // 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
389
444
  const distillEnvClean = { ...process.env };
390
445
  delete distillEnvClean.CLAUDECODE;
446
+ if (engine) distillEnvClean.METAME_ENGINE = engine;
391
447
  const bg = spawn('node', [distillPath], {
392
448
  detached: true,
393
449
  stdio: 'ignore',
394
450
  env: distillEnvClean,
451
+ windowsHide: true,
395
452
  });
396
453
  bg.unref();
397
454
  }
@@ -570,18 +627,45 @@ This step connects the bot to the user's PRIVATE chat — this is the admin chan
570
627
  6. Tell user to run \`metame start\` to activate.
571
628
 
572
629
  - If **Feishu:**
573
- 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.
574
- **${icon("warn")} 重要:** 在「事件订阅」页面,必须开启「接收消息 im.message.receive_v1」事件。然后在该事件的配置中,勾选「获取群组中所有消息」(否则 bot 在群聊中只能收到 @它 的消息,无法接收普通群消息)。
575
- 2. Ask user to paste App ID and App Secret.
576
- 3. Write \`app_id\` and \`app_secret\` into \`~/.metame/daemon.yaml\` under \`feishu:\` section, set \`enabled: true\`.
577
- 4. Tell user: "Now open Feishu and send any message to your new bot (private chat), then tell me you're done."
578
- 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:
579
659
  \`\`\`bash
580
- 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')
581
- 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}'
582
665
  \`\`\`
583
- 6. Write the discovered \`chat_id\`(s) into \`allowed_chat_ids\` in \`~/.metame/daemon.yaml\`.
584
- 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.
585
669
 
586
670
  - If **Skip:** Say "No problem. You can run \`metame daemon init\` anytime to set this up later." Then begin normal work.
587
671
 
@@ -628,25 +712,39 @@ if (fs.existsSync(PROJECT_FILE)) {
628
712
  }
629
713
 
630
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.
631
716
  const yaml = require('js-yaml');
632
717
  let isKnownUser = false;
718
+ let _brainDoc = null;
633
719
  try {
634
720
  if (fs.existsSync(BRAIN_FILE)) {
635
- const doc = yaml.load(fs.readFileSync(BRAIN_FILE, 'utf8')) || {};
636
- if (doc.identity && doc.identity.locale && doc.identity.locale !== 'null') {
637
- isKnownUser = true;
638
- }
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;
639
730
  }
640
731
  } catch (e) {
641
732
  // Ignore error, treat as unknown
642
733
  }
643
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
+
644
740
  let finalProtocol;
645
741
  if (isKnownUser) {
646
742
  finalProtocol = PROTOCOL_NORMAL;
647
743
  } else {
648
744
  finalProtocol = PROTOCOL_ONBOARDING;
649
- 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
+ }
650
748
  }
651
749
 
652
750
  // ---------------------------------------------------------
@@ -654,8 +752,8 @@ if (isKnownUser) {
654
752
  // ---------------------------------------------------------
655
753
  let mirrorLine = '';
656
754
  try {
657
- if (isKnownUser && fs.existsSync(BRAIN_FILE)) {
658
- const brainDoc = yaml.load(fs.readFileSync(BRAIN_FILE, 'utf8')) || {};
755
+ if (isKnownUser && _brainDoc) {
756
+ const brainDoc = _brainDoc;
659
757
 
660
758
  // Check quiet mode
661
759
  const quietUntil = brainDoc.growth && brainDoc.growth.quiet_until;
@@ -708,73 +806,10 @@ try {
708
806
  // Non-fatal
709
807
  }
710
808
 
711
- // ---------------------------------------------------------
712
- // 4.6 REFLECTION PROMPT (Phase C — conditional, NOT static)
713
- // ---------------------------------------------------------
714
- // Only inject when trigger conditions are met at startup.
715
- // This ensures reflections don't fire every session.
716
- let reflectionLine = '';
717
- try {
718
- if (isKnownUser && fs.existsSync(BRAIN_FILE)) {
719
- const refDoc = yaml.load(fs.readFileSync(BRAIN_FILE, 'utf8')) || {};
720
-
721
- // Check quiet mode
722
- const quietUntil = refDoc.growth && refDoc.growth.quiet_until;
723
- const isQuietForRef = quietUntil && new Date(quietUntil).getTime() > Date.now();
724
-
725
- if (!isQuietForRef) {
726
- const distillCount = (refDoc.evolution && refDoc.evolution.distill_count) || 0;
727
- const zoneHistory = (refDoc.growth && refDoc.growth.zone_history) || [];
728
-
729
- // Trigger 1: Every 7th session
730
- const trigger7th = distillCount > 0 && distillCount % 7 === 0;
731
-
732
- // Trigger 2: Three consecutive comfort-zone sessions
733
- const lastThree = zoneHistory.slice(-3);
734
- const triggerComfort = lastThree.length === 3 && lastThree.every(z => z === 'C');
735
-
736
- // Trigger 3: Persistent goal drift (2+ drifted in last 3 sessions)
737
- let triggerDrift = false;
738
- let driftDeclaredFocus = null;
739
- try {
740
- const sessionLogFile = path.join(METAME_DIR, 'session_log.yaml');
741
- if (fs.existsSync(sessionLogFile)) {
742
- const driftLog = yaml.load(fs.readFileSync(sessionLogFile, 'utf8'));
743
- if (driftLog && Array.isArray(driftLog.sessions)) {
744
- const recentSessions = driftLog.sessions.slice(-3);
745
- const driftCount = recentSessions.filter(s =>
746
- s.goal_alignment === 'drifted' || s.goal_alignment === 'partial'
747
- ).length;
748
- if (driftCount >= 2 && recentSessions.length >= 2) {
749
- driftDeclaredFocus = refDoc.status?.focus || refDoc.context?.focus;
750
- if (driftDeclaredFocus) triggerDrift = true;
751
- }
752
- }
753
- }
754
- } catch { /* non-fatal */ }
755
-
756
- if (triggerDrift || triggerComfort || trigger7th) {
757
- let hint = '';
758
- if (triggerDrift) {
759
- hint = `最近几个session的方向和"${driftDeclaredFocus}"有偏差。请在对话开始时温和地问:${icon("mirror")} 是方向有意调整了,还是不小心偏了?`;
760
- } else if (triggerComfort) {
761
- hint = `连续几次都在熟悉领域。如果用户在session结束时自然停顿,可以温和地问:${icon("mirror")} 准备好探索拉伸区了吗?`;
762
- } else {
763
- hint = '这是第' + distillCount + `次session。如果session自然结束,可以附加一句:${icon("mirror")} 一个词形容这次session的感受?`;
764
- }
765
- const timing = triggerDrift ? '在对话开始时就问一次' : '只在session即将结束时说一次';
766
- reflectionLine = `\n[MetaMe reflection: ${hint} ${timing}。如果用户没回应就不要追问。]\n`;
767
- }
768
- }
769
- }
770
- } catch {
771
- // Non-fatal
772
- }
773
-
774
809
  // Project-level CLAUDE.md: KERNEL has moved to global ~/.claude/CLAUDE.md.
775
- // Only inject dynamic per-session observations (mirror / reflection).
810
+ // Only inject dynamic per-session observations (mirror).
776
811
  // If nothing dynamic, write the cleaned file with no METAME block at all.
777
- const dynamicContent = mirrorLine + reflectionLine;
812
+ const dynamicContent = mirrorLine;
778
813
  const newContent = dynamicContent.trim()
779
814
  ? METAME_START + '\n' + dynamicContent + METAME_END + '\n' + fileContent
780
815
  : fileContent;
@@ -1450,6 +1485,7 @@ if (isDaemon) {
1450
1485
  const DAEMON_CONFIG = path.join(METAME_DIR, 'daemon.yaml');
1451
1486
  const DAEMON_STATE = path.join(METAME_DIR, 'daemon_state.json');
1452
1487
  const DAEMON_PID = path.join(METAME_DIR, 'daemon.pid');
1488
+ const DAEMON_LOCK = path.join(METAME_DIR, 'daemon.lock');
1453
1489
  const DAEMON_LOG = path.join(METAME_DIR, 'daemon.log');
1454
1490
  const DAEMON_DEFAULT = path.join(__dirname, 'scripts', 'daemon-default.yaml');
1455
1491
  const DAEMON_SCRIPT = path.join(METAME_DIR, 'daemon.js');
@@ -1780,11 +1816,10 @@ WantedBy=default.target
1780
1816
  }
1781
1817
  // Use caffeinate on macOS to prevent sleep while daemon is running
1782
1818
  const isMac = process.platform === 'darwin';
1783
- const isWin = process.platform === 'win32';
1784
1819
  const cmd = isMac ? 'caffeinate' : process.execPath;
1785
1820
  const args = isMac ? ['-i', process.execPath, DAEMON_SCRIPT] : [DAEMON_SCRIPT];
1786
1821
  const bg = spawn(cmd, args, {
1787
- detached: !isWin,
1822
+ detached: true,
1788
1823
  stdio: 'ignore',
1789
1824
  windowsHide: true,
1790
1825
  env: { ...process.env, HOME: HOME_DIR, METAME_ROOT: __dirname },
@@ -1827,14 +1862,26 @@ WantedBy=default.target
1827
1862
 
1828
1863
  // Check if running
1829
1864
  let isRunning = false;
1865
+ let runningPid = null;
1830
1866
  if (fs.existsSync(DAEMON_PID)) {
1831
1867
  const pid = parseInt(fs.readFileSync(DAEMON_PID, 'utf8').trim(), 10);
1832
- try { process.kill(pid, 0); isRunning = true; } catch { /* dead */ }
1868
+ try { process.kill(pid, 0); isRunning = true; runningPid = pid; } catch { /* dead */ }
1869
+ }
1870
+ if (!isRunning && fs.existsSync(DAEMON_LOCK)) {
1871
+ try {
1872
+ const lock = JSON.parse(fs.readFileSync(DAEMON_LOCK, 'utf8'));
1873
+ const pid = parseInt(lock && lock.pid, 10);
1874
+ if (pid) {
1875
+ process.kill(pid, 0);
1876
+ isRunning = true;
1877
+ runningPid = pid;
1878
+ }
1879
+ } catch { /* lock stale or invalid */ }
1833
1880
  }
1834
1881
 
1835
1882
  console.log(`${icon("bot")} MetaMe Daemon: ${isRunning ? icon("green") + ' Running' : icon("red") + ' Stopped'}`);
1836
1883
  if (state.started_at) console.log(` Started: ${state.started_at}`);
1837
- if (state.pid) console.log(` PID: ${state.pid}`);
1884
+ if (runningPid || state.pid) console.log(` PID: ${runningPid || state.pid}`);
1838
1885
 
1839
1886
  // Budget
1840
1887
  const budget = state.budget || {};
@@ -1926,39 +1973,49 @@ WantedBy=default.target
1926
1973
  process.exit(0);
1927
1974
  }
1928
1975
 
1976
+ 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.';
1977
+
1929
1978
  // ---------------------------------------------------------
1930
1979
  // 5.8 CODEX — launch Codex with MetaMe initialization
1931
1980
  // ---------------------------------------------------------
1932
1981
  const isCodex = process.argv[2] === 'codex';
1933
1982
  if (isCodex) {
1934
- // spawn() resolves PATH automatically; error event handles missing binary
1935
- const codexBin = 'codex';
1936
-
1937
- // Build codex args: remaining user args after 'codex'
1938
1983
  const codexUserArgs = process.argv.slice(3);
1939
- let codexArgs;
1940
- if (codexUserArgs.length === 0) {
1941
- // Interactive mode: `codex --full-auto`
1942
- codexArgs = ['--full-auto'];
1943
- } else {
1944
- // Non-interactive: `codex exec --full-auto <user args>`
1945
- codexArgs = ['exec', '--full-auto', ...codexUserArgs];
1946
- }
1984
+ const codexProviderEnv = (() => { try { return require(path.join(__dirname, 'scripts', 'providers.js')).buildActiveEnv(); } catch { return {}; } })();
1985
+
1986
+ // Genesis: new user + interactive mode trigger profile interview within the same Codex session.
1987
+ // CLAUDE.md (already written to disk above) contains the full genesis protocol; Codex reads it.
1988
+ // We pass the trigger as the opening [PROMPT] argument so genesis flows into normal work seamlessly.
1989
+ const codexArgs = codexUserArgs.length === 0
1990
+ ? ['--dangerously-bypass-approvals-and-sandbox']
1991
+ : ['exec', '--dangerously-bypass-approvals-and-sandbox', ...codexUserArgs];
1992
+
1993
+ // Codex reads AGENTS.md (not CLAUDE.md); create symlink so genesis protocol is visible.
1994
+ // Also ensure global ~/AGENTS.md → ~/.claude/CLAUDE.md for identity context.
1995
+ // Use try-catch on symlinkSync directly (avoids TOCTOU race from existsSync pre-check).
1996
+ try {
1997
+ if (fs.existsSync(path.join(process.cwd(), 'CLAUDE.md')))
1998
+ fs.symlinkSync('CLAUDE.md', path.join(process.cwd(), 'AGENTS.md'));
1999
+ } catch { /* EEXIST or other — non-critical */ }
2000
+ try {
2001
+ const globalClaudeMd = path.join(HOME_DIR, '.claude', 'CLAUDE.md');
2002
+ if (fs.existsSync(globalClaudeMd))
2003
+ fs.symlinkSync(globalClaudeMd, path.join(HOME_DIR, 'AGENTS.md'));
2004
+ } catch { /* EEXIST or other — non-critical */ }
1947
2005
 
1948
- const codexChild = spawn(codexBin, codexArgs, {
2006
+ const child = spawnCodex(codexArgs, {
1949
2007
  stdio: 'inherit',
1950
2008
  cwd: process.cwd(),
1951
- env: { ...process.env, METAME_ACTIVE_SESSION: 'true' },
2009
+ env: { ...process.env, ...codexProviderEnv, METAME_ACTIVE_SESSION: 'true' },
1952
2010
  });
1953
-
1954
- codexChild.on('error', () => {
1955
- console.error(`\n${icon("fail")} Error: Could not launch 'codex'.`);
2011
+ let launchError = false;
2012
+ child.on('error', (err) => {
2013
+ launchError = true;
2014
+ console.error(`\n${icon("fail")} Error: Could not launch 'codex': ${err.message}`);
1956
2015
  console.error(" Please install: npm install -g @openai/codex");
1957
2016
  });
1958
- codexChild.on('close', (code) => process.exit(code || 0));
1959
-
1960
- // Background distillation
1961
- spawnDistillBackground();
2017
+ child.on('close', (code) => process.exit(launchError ? 127 : (code || 0)));
2018
+ spawnDistillBackground('codex');
1962
2019
  return;
1963
2020
  }
1964
2021
 
@@ -2049,10 +2106,7 @@ if (daemonCfg.dangerously_skip_permissions && !launchArgs.includes('--dangerousl
2049
2106
  launchArgs.push('--dangerously-skip-permissions');
2050
2107
  }
2051
2108
  if (!isKnownUser) {
2052
- launchArgs.push(
2053
- '--append-system-prompt',
2054
- '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.'
2055
- );
2109
+ launchArgs.push('--append-system-prompt', GENESIS_TRIGGER_PROMPT);
2056
2110
  }
2057
2111
 
2058
2112
  // RAG: inject relevant facts based on current project (desktop-side equivalent of daemon RAG)
@@ -2100,11 +2154,10 @@ try {
2100
2154
  }
2101
2155
  if (!daemonRunning) {
2102
2156
  const _isMac = process.platform === 'darwin';
2103
- const _isWin = process.platform === 'win32';
2104
2157
  const dCmd = _isMac ? 'caffeinate' : process.execPath;
2105
2158
  const dArgs = _isMac ? ['-i', process.execPath, _daemonScript] : [_daemonScript];
2106
2159
  const bg = spawn(dCmd, dArgs, {
2107
- detached: !_isWin,
2160
+ detached: true,
2108
2161
  stdio: 'ignore',
2109
2162
  windowsHide: true,
2110
2163
  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.5.0",
3
+ "version": "1.5.2",
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": {
@@ -15,10 +15,11 @@
15
15
  ],
16
16
  "scripts": {
17
17
  "test": "node --test scripts/*.test.js",
18
+ "test:daemon-status": "node --test scripts/daemon-restart-status.test.js",
18
19
  "start": "node index.js",
19
- "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/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/self-reflect.js scripts/check-macos-control-capabilities.sh plugin/scripts/ && echo 'Plugin scripts synced'",
20
21
  "sync:readme": "node scripts/sync-readme.js",
21
- "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'",
22
23
  "precommit": "npm run sync:plugin && npm run restart:daemon"
23
24
  },
24
25
  "keywords": [