metame-cli 1.5.3 → 1.5.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/README.md +60 -18
  2. package/index.js +352 -79
  3. package/package.json +2 -2
  4. package/scripts/agent-layer.js +4 -2
  5. package/scripts/bin/dispatch_to +178 -90
  6. package/scripts/daemon-admin-commands.js +353 -105
  7. package/scripts/daemon-agent-commands.js +434 -66
  8. package/scripts/daemon-bridges.js +477 -68
  9. package/scripts/daemon-claude-engine.js +1267 -674
  10. package/scripts/daemon-command-router.js +205 -27
  11. package/scripts/daemon-command-session-route.js +118 -0
  12. package/scripts/daemon-default.yaml +7 -0
  13. package/scripts/daemon-engine-runtime.js +96 -20
  14. package/scripts/daemon-exec-commands.js +108 -49
  15. package/scripts/daemon-file-browser.js +64 -7
  16. package/scripts/daemon-notify.js +18 -4
  17. package/scripts/daemon-ops-commands.js +16 -2
  18. package/scripts/daemon-remote-dispatch.js +55 -1
  19. package/scripts/daemon-runtime-lifecycle.js +87 -0
  20. package/scripts/daemon-session-commands.js +102 -45
  21. package/scripts/daemon-session-store.js +497 -66
  22. package/scripts/daemon-siri-bridge.js +234 -0
  23. package/scripts/daemon-siri-imessage.js +209 -0
  24. package/scripts/daemon-task-scheduler.js +10 -2
  25. package/scripts/daemon.js +697 -179
  26. package/scripts/daemon.yaml +7 -0
  27. package/scripts/docs/agent-guide.md +36 -3
  28. package/scripts/docs/hook-config.md +134 -0
  29. package/scripts/docs/maintenance-manual.md +162 -5
  30. package/scripts/docs/pointer-map.md +60 -5
  31. package/scripts/feishu-adapter.js +7 -15
  32. package/scripts/hooks/doc-router.js +29 -0
  33. package/scripts/hooks/hook-utils.js +61 -0
  34. package/scripts/hooks/intent-doc-router.js +54 -0
  35. package/scripts/hooks/intent-engine.js +72 -0
  36. package/scripts/hooks/intent-file-transfer.js +51 -0
  37. package/scripts/hooks/intent-memory-recall.js +35 -0
  38. package/scripts/hooks/intent-ops-assist.js +54 -0
  39. package/scripts/hooks/intent-task-create.js +35 -0
  40. package/scripts/hooks/intent-team-dispatch.js +106 -0
  41. package/scripts/hooks/team-context.js +143 -0
  42. package/scripts/intent-registry.js +59 -0
  43. package/scripts/memory-extract.js +59 -0
  44. package/scripts/memory-nightly-reflect.js +109 -43
  45. package/scripts/memory.js +55 -17
  46. package/scripts/mentor-engine.js +6 -0
  47. package/scripts/schema.js +1 -0
  48. package/scripts/self-reflect.js +110 -12
  49. package/scripts/session-analytics.js +160 -0
  50. package/scripts/signal-capture.js +1 -1
  51. package/scripts/team-dispatch.js +315 -0
package/index.js CHANGED
@@ -50,6 +50,96 @@ function spawnCodex(args, options) {
50
50
  return spawnViaNode('codex', args, { ...options, env });
51
51
  }
52
52
 
53
+ function mergeReflectionDisplayEntries(entries) {
54
+ const merged = new Map();
55
+ for (const entry of entries) {
56
+ const normalized = typeof entry === 'string'
57
+ ? { summary: entry, detected: null }
58
+ : (entry && typeof entry.summary === 'string' ? { summary: entry.summary, detected: entry.detected || null } : null);
59
+ if (!normalized) continue;
60
+
61
+ const existing = merged.get(normalized.summary);
62
+ if (!existing) {
63
+ merged.set(normalized.summary, normalized);
64
+ continue;
65
+ }
66
+
67
+ const existingDetected = existing.detected ? new Date(existing.detected).getTime() : 0;
68
+ const normalizedDetected = normalized.detected ? new Date(normalized.detected).getTime() : 0;
69
+ const shouldReplace =
70
+ normalizedDetected > 0 && (
71
+ existingDetected === 0
72
+ || normalizedDetected < existingDetected
73
+ );
74
+ if (shouldReplace) merged.set(normalized.summary, { ...existing, ...normalized });
75
+ }
76
+ return [...merged.values()];
77
+ }
78
+
79
+ function readLatestClaudeSession(projectsRoot, cwd) {
80
+ let bestSession = null;
81
+ const findLatest = (dir) => {
82
+ try {
83
+ return fs.readdirSync(dir)
84
+ .filter(f => f.endsWith('.jsonl'))
85
+ .map(f => ({ id: f.replace('.jsonl', ''), mtime: fs.statSync(path.join(dir, f)).mtimeMs }))
86
+ .sort((a, b) => b.mtime - a.mtime)[0] || null;
87
+ } catch { return null; }
88
+ };
89
+
90
+ try {
91
+ const projDir = path.join(projectsRoot, cwd.replace(/\//g, '-'));
92
+ const localBest = findLatest(projDir);
93
+ let globalBest = null;
94
+ try {
95
+ for (const d of fs.readdirSync(projectsRoot)) {
96
+ const s = findLatest(path.join(projectsRoot, d));
97
+ if (s && (!globalBest || s.mtime > globalBest.mtime)) globalBest = s;
98
+ }
99
+ } catch { /* ignore */ }
100
+ if (localBest && globalBest && globalBest.mtime > localBest.mtime) {
101
+ bestSession = { ...globalBest, scope: 'global' };
102
+ } else {
103
+ bestSession = localBest ? { ...localBest, scope: 'local' } : (globalBest ? { ...globalBest, scope: 'global' } : null);
104
+ }
105
+ } catch { /* ignore */ }
106
+
107
+ return bestSession ? { ...bestSession, engine: 'claude' } : null;
108
+ }
109
+
110
+ function readLatestCodexSession(cwd) {
111
+ let db;
112
+ try {
113
+ const codeDb = path.join(HOME_DIR, '.codex', 'state_5.sqlite');
114
+ if (!fs.existsSync(codeDb)) return null;
115
+ const { DatabaseSync } = require('node:sqlite');
116
+ db = new DatabaseSync(codeDb, { readonly: true });
117
+ const row = db.prepare(`
118
+ SELECT id, cwd, updated_at, created_at
119
+ FROM threads
120
+ WHERE COALESCE(has_user_event, 1) = 1
121
+ AND archived = 0
122
+ ORDER BY
123
+ CASE WHEN cwd = ? THEN 0 ELSE 1 END ASC,
124
+ COALESCE(updated_at, created_at, 0) DESC
125
+ LIMIT 1
126
+ `).get(cwd);
127
+ db.close();
128
+ db = null;
129
+ if (!row || !row.id) return null;
130
+ const ts = Number(row.updated_at || row.created_at || 0) * 1000;
131
+ return {
132
+ id: String(row.id),
133
+ mtime: ts || 0,
134
+ engine: 'codex',
135
+ scope: String(row.cwd || '') === String(cwd) ? 'local' : 'global',
136
+ };
137
+ } catch {
138
+ if (db) { try { db.close(); } catch { /* ignore */ } }
139
+ return null;
140
+ }
141
+ }
142
+
53
143
  // Quick flags (before heavy init)
54
144
  const pkgVersion = require('./package.json').version;
55
145
  if (process.argv.includes('-V') || process.argv.includes('--version')) {
@@ -83,8 +173,15 @@ if (!fs.existsSync(METAME_DIR)) {
83
173
  // DEPLOY PHASE: sync scripts, docs, bin to ~/.metame/
84
174
  // ---------------------------------------------------------
85
175
 
176
+ // Dev mode: when running from git repo, symlink instead of copy.
177
+ // This ensures source files and runtime files are always the same,
178
+ // preventing agents from accidentally editing copies instead of source.
179
+ const IS_DEV_MODE = fs.existsSync(path.join(__dirname, '.git'));
180
+
86
181
  /**
87
- * Sync files from srcDir to destDir. Only writes when content differs.
182
+ * Sync files from srcDir to destDir.
183
+ * - Dev mode (git repo): creates symlinks so source === runtime.
184
+ * - Production (npm install): copies files, only writes when content differs.
88
185
  * @param {string} srcDir - source directory
89
186
  * @param {string} destDir - destination directory
90
187
  * @param {object} [opts]
@@ -102,22 +199,111 @@ function syncDirFiles(srcDir, destDir, { fileList, chmod } = {}) {
102
199
  const dest = path.join(destDir, f);
103
200
  try {
104
201
  if (!fs.existsSync(src)) continue;
105
- const srcContent = fs.readFileSync(src, 'utf8');
106
- const destContent = fs.existsSync(dest) ? fs.readFileSync(dest, 'utf8') : '';
107
- if (srcContent !== destContent) {
108
- fs.writeFileSync(dest, srcContent, 'utf8');
109
- if (chmod) try { fs.chmodSync(dest, chmod); } catch { /* Windows */ }
110
- updated = true;
202
+
203
+ if (IS_DEV_MODE) {
204
+ // Dev mode: symlink dest → src (replace copy/stale symlink if needed)
205
+ const srcReal = fs.realpathSync(src);
206
+ let needLink = true;
207
+ try {
208
+ const existing = fs.lstatSync(dest);
209
+ if (existing.isSymbolicLink()) {
210
+ if (fs.realpathSync(dest) === srcReal) needLink = false;
211
+ else fs.unlinkSync(dest);
212
+ } else {
213
+ // Replace regular file with symlink
214
+ fs.unlinkSync(dest);
215
+ }
216
+ } catch { /* dest doesn't exist */ }
217
+ if (needLink) {
218
+ fs.symlinkSync(srcReal, dest);
219
+ if (chmod) try { fs.chmodSync(dest, chmod); } catch { /* Windows */ }
220
+ updated = true;
221
+ }
222
+ } else {
223
+ // Production: copy when content differs
224
+ const srcContent = fs.readFileSync(src, 'utf8');
225
+ const destContent = fs.existsSync(dest) ? fs.readFileSync(dest, 'utf8') : '';
226
+ if (srcContent !== destContent) {
227
+ fs.writeFileSync(dest, srcContent, 'utf8');
228
+ if (chmod) try { fs.chmodSync(dest, chmod); } catch { /* Windows */ }
229
+ updated = true;
230
+ }
111
231
  }
112
232
  } catch { /* non-fatal per file */ }
113
233
  }
114
234
  return updated;
115
235
  }
116
236
 
237
+ function readRunningDaemonPid({ pidFile, lockFile }) {
238
+ if (fs.existsSync(pidFile)) {
239
+ try {
240
+ const pid = parseInt(fs.readFileSync(pidFile, 'utf8').trim(), 10);
241
+ if (pid && pid !== process.pid) {
242
+ process.kill(pid, 0);
243
+ return pid;
244
+ }
245
+ } catch { /* stale pid file */ }
246
+ }
247
+ if (fs.existsSync(lockFile)) {
248
+ try {
249
+ const lock = JSON.parse(fs.readFileSync(lockFile, 'utf8'));
250
+ const pid = parseInt(lock && lock.pid, 10);
251
+ if (pid && pid !== process.pid) {
252
+ process.kill(pid, 0);
253
+ return pid;
254
+ }
255
+ } catch { /* stale or invalid lock */ }
256
+ }
257
+ return null;
258
+ }
259
+
260
+ function requestDaemonRestart({
261
+ reason = 'manual-restart',
262
+ daemonPidFile = path.join(METAME_DIR, 'daemon.pid'),
263
+ daemonLockFile = path.join(METAME_DIR, 'daemon.lock'),
264
+ daemonScript = path.join(METAME_DIR, 'daemon.js'),
265
+ } = {}) {
266
+ const pid = readRunningDaemonPid({ pidFile: daemonPidFile, lockFile: daemonLockFile });
267
+ if (!pid) return { ok: false, status: 'not_running' };
268
+
269
+ if (process.platform !== 'win32') {
270
+ try {
271
+ process.kill(pid, 'SIGUSR2');
272
+ return { ok: true, status: 'signaled', pid };
273
+ } catch (e) {
274
+ return { ok: false, status: 'signal_failed', pid, error: e.message };
275
+ }
276
+ }
277
+
278
+ try {
279
+ process.kill(pid, 'SIGTERM');
280
+ } catch (e) {
281
+ return { ok: false, status: 'stop_failed', pid, error: e.message };
282
+ }
283
+
284
+ let stopped = false;
285
+ for (let i = 0; i < 12; i++) {
286
+ sleepSync(500);
287
+ try { process.kill(pid, 0); } catch { stopped = true; break; }
288
+ }
289
+ if (!stopped) {
290
+ try { process.kill(pid, 'SIGKILL'); } catch { /* already gone */ }
291
+ }
292
+
293
+ const bg = spawn(process.execPath, [daemonScript], {
294
+ detached: true,
295
+ stdio: 'ignore',
296
+ windowsHide: true,
297
+ env: { ...process.env, HOME: HOME_DIR, METAME_ROOT: __dirname, METAME_DEPLOY_RESTART_REASON: reason },
298
+ });
299
+ bg.unref();
300
+ return { ok: true, status: 'restarted', pid, nextPid: bg.pid };
301
+ }
302
+
117
303
  // Auto-deploy bundled scripts to ~/.metame/
118
304
  // IMPORTANT: daemon.yaml is USER CONFIG — never overwrite it. Only daemon-default.yaml (template) is synced.
119
305
  const scriptsDir = path.join(__dirname, 'scripts');
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'];
306
+ const BUNDLED_BASE_SCRIPTS = ['platform.js', 'signal-capture.js', 'distill.js', 'schema.js', 'pending-traits.js', 'daemon.js', 'daemon-notify.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'];
121
307
  const DAEMON_MODULE_SCRIPTS = (() => {
122
308
  try {
123
309
  return fs.readdirSync(scriptsDir).filter((f) => /^daemon-[\w-]+\.js$/.test(f));
@@ -162,22 +348,36 @@ if (syntaxErrors.length > 0) {
162
348
  console.error('Fix the errors before deploying. Daemon continues running with old code.');
163
349
  } else {
164
350
  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
351
  if (scriptsUpdated) {
171
- console.log(`${icon("pkg")} Scripts synced to ~/.metame/ — daemon will auto-restart when idle.`);
352
+ console.log(`${icon("pkg")} Scripts ${IS_DEV_MODE ? 'symlinked' : 'synced'} to ~/.metame/.`);
172
353
  }
173
354
  }
174
355
 
175
356
  // Docs: lazy-load references for CLAUDE.md pointer instructions
176
357
  syncDirFiles(path.join(__dirname, 'scripts', 'docs'), path.join(METAME_DIR, 'docs'));
177
358
  // Bin: CLI tools (dispatch_to etc.)
178
- syncDirFiles(path.join(__dirname, 'scripts', 'bin'), path.join(METAME_DIR, 'bin'), { chmod: 0o755 });
359
+ const binUpdated = syncDirFiles(path.join(__dirname, 'scripts', 'bin'), path.join(METAME_DIR, 'bin'), { chmod: 0o755 });
179
360
  // Hooks: Claude Code event hooks (Stop, PostToolUse, etc.)
180
- syncDirFiles(path.join(__dirname, 'scripts', 'hooks'), path.join(METAME_DIR, 'hooks'));
361
+ const hooksUpdated = syncDirFiles(path.join(__dirname, 'scripts', 'hooks'), path.join(METAME_DIR, 'hooks'));
362
+
363
+ const daemonCodeUpdated = scriptsUpdated || binUpdated || hooksUpdated;
364
+ const shouldAutoRestartAfterDeploy = (() => {
365
+ const [cmd] = process.argv.slice(2);
366
+ if (!cmd) return true;
367
+ if (cmd === 'daemon') return false;
368
+ if (['start', 'stop', 'restart', 'status', 'logs'].includes(cmd)) return false;
369
+ return ['codex', 'continue', 'sync'].includes(cmd);
370
+ })();
371
+ if (daemonCodeUpdated && shouldAutoRestartAfterDeploy) {
372
+ const restartResult = requestDaemonRestart({ reason: 'deploy-sync' });
373
+ if (restartResult.ok) {
374
+ console.log(`${icon("reload")} Daemon restart requested after deploy${restartResult.pid ? ` (PID: ${restartResult.pid})` : ''}.`);
375
+ } else if (restartResult.status === 'not_running') {
376
+ console.log(`${icon("info")} Deploy finished. Daemon not running, so restart was skipped.`);
377
+ } else {
378
+ console.log(`${icon("warn")} Deploy finished, but daemon restart failed: ${restartResult.error || restartResult.status}`);
379
+ }
380
+ }
181
381
 
182
382
  // ---------------------------------------------------------
183
383
  // Deploy bundled skills to ~/.claude/skills/
@@ -217,6 +417,7 @@ if (fs.existsSync(bundledSkillsDir)) {
217
417
  }
218
418
  }
219
419
 
420
+
220
421
  // Ensure ~/.codex/skills and ~/.agents/skills are symlinks to ~/.claude/skills
221
422
  // This keeps skill evolution unified across all engines.
222
423
  for (const altDir of [
@@ -348,6 +549,41 @@ function ensureHookInstalled() {
348
549
  console.log(`${icon("hook")} MetaMe: Stop session capture hook installed.`);
349
550
  }
350
551
 
552
+ // Migrate: remove standalone team-context.js hook (superseded by intent-engine)
553
+ if (settings.hooks?.UserPromptSubmit) {
554
+ const before = settings.hooks.UserPromptSubmit.length;
555
+ for (const entry of settings.hooks.UserPromptSubmit) {
556
+ if (entry.hooks) {
557
+ entry.hooks = entry.hooks.filter(h => !(h.command && h.command.includes('team-context.js')));
558
+ }
559
+ }
560
+ settings.hooks.UserPromptSubmit = settings.hooks.UserPromptSubmit.filter(
561
+ entry => entry.hooks && entry.hooks.length > 0
562
+ );
563
+ if (settings.hooks.UserPromptSubmit.length !== before) modified = true;
564
+ }
565
+
566
+ // Ensure intent-engine hook (unified intent detection + hint injection)
567
+ const intentEngineScript = path.join(METAME_DIR, 'hooks', 'intent-engine.js').replace(/\\/g, '/');
568
+ const intentEngineCommand = `node "${intentEngineScript}"`;
569
+ const intentEngineInstalled = (settings.hooks?.UserPromptSubmit || []).some(entry =>
570
+ entry.hooks?.some(h => h.command && h.command.includes('intent-engine.js'))
571
+ );
572
+
573
+ if (!intentEngineInstalled) {
574
+ if (!settings.hooks) settings.hooks = {};
575
+ if (!settings.hooks.UserPromptSubmit) settings.hooks.UserPromptSubmit = [];
576
+
577
+ settings.hooks.UserPromptSubmit.push({
578
+ hooks: [{
579
+ type: 'command',
580
+ command: intentEngineCommand,
581
+ }]
582
+ });
583
+ modified = true;
584
+ console.log(`${icon("hook")} MetaMe: Intent engine hook installed.`);
585
+ }
586
+
351
587
  if (modified) {
352
588
  fs.writeFileSync(CLAUDE_SETTINGS, JSON.stringify(settings, null, 2), 'utf8');
353
589
  }
@@ -768,6 +1004,7 @@ try {
768
1004
 
769
1005
  // Find a pattern that hasn't been surfaced in 14 days
770
1006
  const candidate = brainDoc.growth.patterns.find(p => {
1007
+ if (!p || typeof p.summary !== 'string') return false;
771
1008
  if (!p.surfaced) return true;
772
1009
  return (now - new Date(p.surfaced).getTime()) > COOLDOWN_MS;
773
1010
  });
@@ -872,26 +1109,11 @@ const KERNEL_BODY = PROTOCOL_NORMAL
872
1109
  .replace(/^<!-- METAME:START -->\n/, '') // remove project-level marker
873
1110
  .trimEnd();
874
1111
 
1112
+ // Most capability hints migrated to intent engine (on-demand injection).
1113
+ // Only keep Skills here — it's a fallback behavior that can't be keyword-matched.
875
1114
  const CAPABILITY_SECTIONS = [
876
- '## Agent Dispatch',
877
- '识别到"告诉X/让X/通知X"等转发意图时 → 先 `cat ~/.metame/docs/dispatch-table.md` 获取路由表(昵称→project_key),再执行转发。不要凭记忆猜测昵称对应关系。',
878
- '',
879
- '## Agent 创建与管理',
880
- '用户问创建/管理/绑定 Agent 时 → 先 `cat ~/.metame/docs/agent-guide.md` 再回答。',
881
- '用户问代码结构/升级进度/脚本入口时 → 先 `cat ~/.metame/docs/pointer-map.md` 再回答。',
882
- '',
883
- '## 手机端文件交互',
884
- '用户要文件("发给我"/"发过来"/"导出")→ 先 `cat ~/.metame/docs/file-transfer.md` 再执行。',
885
- '**收**:用户发图片/文件自动存到 `upload/`,用 Read 查看。',
886
- '**发**:回复末尾加 `[[FILE:/absolute/path]]`,daemon 自动发手机。不要读内容再复述。',
887
- '',
888
- '## 跨会话记忆',
889
- '用户提"上次/之前"时搜索:`node ~/.metame/memory-search.js "关键词1" "keyword2"`',
890
- '一次传 3-4 个关键词(中文+英文+函数名),`--facts` 只搜事实,`--sessions` 只搜会话。',
891
- '',
892
1115
  '## Skills',
893
1116
  '能力不足/工具缺失/任务失败 → 先查 `cat ~/.claude/skills/skill-manager/SKILL.md`,不要自己猜。',
894
-
895
1117
  ].join('\n');
896
1118
 
897
1119
  try {
@@ -1256,17 +1478,34 @@ if (isInsights) {
1256
1478
  try {
1257
1479
  const doc = yaml.load(fs.readFileSync(BRAIN_FILE, 'utf8')) || {};
1258
1480
  const patterns = (doc.growth && doc.growth.patterns) || [];
1481
+ const reflectionPatterns = (doc.growth && doc.growth.self_reflection_patterns) || [];
1259
1482
  const zoneHistory = (doc.growth && doc.growth.zone_history) || [];
1483
+ const userPatterns = patterns.filter(p => p && typeof p.summary === 'string');
1484
+ const legacyReflectionPatterns = patterns
1485
+ .filter(p => typeof p === 'string')
1486
+ .map(p => ({ summary: p, detected: null }));
1487
+ const aiReflections = mergeReflectionDisplayEntries([...reflectionPatterns, ...legacyReflectionPatterns]);
1260
1488
 
1261
- if (patterns.length === 0) {
1489
+ if (userPatterns.length === 0 && aiReflections.length === 0) {
1262
1490
  console.log(`${icon("search")} MetaMe: No patterns detected yet. Keep using MetaMe and patterns will emerge after ~5 sessions.`);
1263
1491
  } else {
1264
1492
  console.log(`${icon("mirror")} MetaMe Insights:\n`);
1265
- patterns.forEach((p, i) => {
1493
+ if (userPatterns.length > 0) {
1494
+ console.log('User observation:');
1495
+ }
1496
+ userPatterns.forEach((p, i) => {
1266
1497
  const sym = p.type === 'avoidance' ? icon("warn") : p.type === 'growth' ? '+' : p.type === 'energy' ? '*' : icon("reload");
1267
1498
  console.log(` ${sym} [${p.type}] ${p.summary} (confidence: ${(p.confidence * 100).toFixed(0)}%)`);
1268
1499
  console.log(` Detected: ${p.detected}${p.surfaced ? `, Last shown: ${p.surfaced}` : ''}`);
1269
1500
  });
1501
+ if (aiReflections.length > 0) {
1502
+ if (userPatterns.length > 0) console.log('');
1503
+ console.log('AI self-reflection:');
1504
+ aiReflections.forEach((p) => {
1505
+ console.log(` ${icon("thought")} ${p.summary}`);
1506
+ if (p.detected) console.log(` Detected: ${p.detected}`);
1507
+ });
1508
+ }
1270
1509
  if (zoneHistory.length > 0) {
1271
1510
  console.log(`\n ${icon("chart")} Recent zone history: ${zoneHistory.join(' → ')}`);
1272
1511
  console.log(` (C=Comfort, S=Stretch, P=Panic)`);
@@ -1474,7 +1713,7 @@ if (isProvider) {
1474
1713
  // 5.7 DAEMON SUBCOMMANDS
1475
1714
  // ---------------------------------------------------------
1476
1715
  // Shorthand aliases: `metame start` → `metame daemon start`, etc.
1477
- const DAEMON_SHORTCUTS = ['start', 'stop', 'status', 'logs'];
1716
+ const DAEMON_SHORTCUTS = ['start', 'stop', 'restart', 'status', 'logs'];
1478
1717
  if (DAEMON_SHORTCUTS.includes(process.argv[2])) {
1479
1718
  process.argv.splice(2, 0, 'daemon');
1480
1719
  }
@@ -1856,6 +2095,48 @@ WantedBy=default.target
1856
2095
  process.exit(0);
1857
2096
  }
1858
2097
 
2098
+ if (subCmd === 'restart') {
2099
+ if (!fs.existsSync(DAEMON_CONFIG)) {
2100
+ console.error(`${icon("fail")} No config found. Run: metame daemon init`);
2101
+ process.exit(1);
2102
+ }
2103
+ if (!fs.existsSync(DAEMON_SCRIPT)) {
2104
+ console.error(`${icon("fail")} daemon.js not found. Reinstall MetaMe.`);
2105
+ process.exit(1);
2106
+ }
2107
+ const result = requestDaemonRestart({
2108
+ reason: 'cli-restart',
2109
+ daemonPidFile: DAEMON_PID,
2110
+ daemonLockFile: DAEMON_LOCK,
2111
+ daemonScript: DAEMON_SCRIPT,
2112
+ });
2113
+ if (result.ok) {
2114
+ if (result.status === 'restarted') {
2115
+ console.log(`${icon("ok")} Daemon restarted (old PID: ${result.pid}, new PID: ${result.nextPid})`);
2116
+ } else {
2117
+ console.log(`${icon("ok")} Daemon graceful restart requested (PID: ${result.pid})`);
2118
+ }
2119
+ process.exit(0);
2120
+ }
2121
+ if (result.status === 'not_running') {
2122
+ console.log(`${icon("info")} No daemon running. Starting a fresh daemon instead.`);
2123
+ const isMac = process.platform === 'darwin';
2124
+ const cmd = isMac ? 'caffeinate' : process.execPath;
2125
+ const args = isMac ? ['-i', process.execPath, DAEMON_SCRIPT] : [DAEMON_SCRIPT];
2126
+ const bg = spawn(cmd, args, {
2127
+ detached: true,
2128
+ stdio: 'ignore',
2129
+ windowsHide: true,
2130
+ env: { ...process.env, HOME: HOME_DIR, METAME_ROOT: __dirname, METAME_DEPLOY_RESTART_REASON: 'cli-restart' },
2131
+ });
2132
+ bg.unref();
2133
+ console.log(`${icon("ok")} MetaMe daemon started (PID: ${bg.pid})`);
2134
+ process.exit(0);
2135
+ }
2136
+ console.error(`${icon("fail")} Daemon restart failed: ${result.error || result.status}`);
2137
+ process.exit(1);
2138
+ }
2139
+
1859
2140
  if (subCmd === 'status') {
1860
2141
  let state = {};
1861
2142
  try { state = JSON.parse(fs.readFileSync(DAEMON_STATE, 'utf8')); } catch { /* empty */ }
@@ -1959,6 +2240,7 @@ WantedBy=default.target
1959
2240
  console.log(`${icon("book")} Daemon Commands:`);
1960
2241
  console.log(" metame start — start background daemon");
1961
2242
  console.log(" metame stop — stop daemon");
2243
+ console.log(" metame restart — graceful restart daemon");
1962
2244
  console.log(" metame status — show status & budget");
1963
2245
  console.log(" metame logs — tail log file");
1964
2246
  console.log(" metame daemon init — initialize config");
@@ -2022,58 +2304,48 @@ if (isCodex) {
2022
2304
  // ---------------------------------------------------------
2023
2305
  // 5.9 CONTINUE/SYNC — resume latest session from terminal
2024
2306
  // ---------------------------------------------------------
2025
- // Usage: exit Claude first, then run `metame continue` from terminal.
2026
- // Finds the most recent session and launches Claude with --resume.
2307
+ // Usage: exit current CLI first, then run `metame continue` from terminal.
2308
+ // Finds the most recent session across Claude/Codex and resumes with the matching engine.
2027
2309
  const isSync = process.argv.includes('sync') || process.argv.includes('continue');
2028
2310
  if (isSync) {
2029
2311
  const projectsRoot = path.join(HOME_DIR, '.claude', 'projects');
2030
- let bestSession = null;
2031
- try {
2032
- const cwd = process.cwd();
2033
- const projDir = path.join(projectsRoot, cwd.replace(/\//g, '-'));
2034
- const findLatest = (dir) => {
2035
- try {
2036
- return fs.readdirSync(dir)
2037
- .filter(f => f.endsWith('.jsonl'))
2038
- .map(f => ({ id: f.replace('.jsonl', ''), mtime: fs.statSync(path.join(dir, f)).mtimeMs }))
2039
- .sort((a, b) => b.mtime - a.mtime)[0] || null;
2040
- } catch { return null; }
2041
- };
2042
- const localBest = findLatest(projDir);
2043
- // Always scan globally to find the absolute most recent session
2044
- // (phone /continue may have worked in a different project's session)
2045
- let globalBest = null;
2046
- try {
2047
- for (const d of fs.readdirSync(projectsRoot)) {
2048
- const s = findLatest(path.join(projectsRoot, d));
2049
- if (s && (!globalBest || s.mtime > globalBest.mtime)) globalBest = s;
2050
- }
2051
- } catch { /* ignore */ }
2052
- // Use global best if it's more recent than local; prefer local otherwise
2053
- if (localBest && globalBest && globalBest.mtime > localBest.mtime) {
2054
- bestSession = globalBest;
2055
- console.log(` (global session is newer than local — using global)`);
2056
- } else {
2057
- bestSession = localBest || globalBest;
2058
- }
2059
- } catch { }
2312
+ const cwd = process.cwd();
2313
+ const candidates = [
2314
+ readLatestClaudeSession(projectsRoot, cwd),
2315
+ readLatestCodexSession(cwd),
2316
+ ].filter(Boolean);
2317
+ const bestSession = candidates.sort((a, b) => (b.mtime || 0) - (a.mtime || 0))[0] || null;
2060
2318
 
2061
2319
  if (!bestSession) {
2062
2320
  console.error('No session found.');
2063
2321
  process.exit(1);
2064
2322
  }
2065
2323
 
2324
+ if (bestSession.scope === 'global') {
2325
+ console.log(' (global session is newer than local — using global)');
2326
+ }
2066
2327
  console.log(`\n${icon("reload")} Resuming session ${bestSession.id.slice(0, 8)}...\n`);
2067
- const providerEnv = (() => { try { return require(path.join(__dirname, 'scripts', 'providers.js')).buildActiveEnv(); } catch { return {}; } })();
2068
- const resumeArgs = ['--resume', bestSession.id];
2069
- if (daemonCfg.dangerously_skip_permissions) resumeArgs.push('--dangerously-skip-permissions');
2070
- const syncChild = spawnClaude(resumeArgs, {
2071
- stdio: 'inherit',
2072
- env: { ...process.env, ...providerEnv, METAME_ACTIVE_SESSION: 'true' }
2073
- });
2074
- syncChild.on('error', () => {
2075
- console.error("Could not launch 'claude'. Is Claude Code installed?");
2076
- });
2328
+ let syncChild;
2329
+ if (bestSession.engine === 'codex') {
2330
+ syncChild = spawnCodex(['exec', 'resume', bestSession.id], {
2331
+ stdio: 'inherit',
2332
+ env: { ...process.env, METAME_ACTIVE_SESSION: 'true' }
2333
+ });
2334
+ syncChild.on('error', () => {
2335
+ console.error("Could not launch 'codex'. Is Codex CLI installed?");
2336
+ });
2337
+ } else {
2338
+ const providerEnv = (() => { try { return require(path.join(__dirname, 'scripts', 'providers.js')).buildActiveEnv(); } catch { return {}; } })();
2339
+ const resumeArgs = ['--resume', bestSession.id];
2340
+ if (daemonCfg.dangerously_skip_permissions) resumeArgs.push('--dangerously-skip-permissions');
2341
+ syncChild = spawnClaude(resumeArgs, {
2342
+ stdio: 'inherit',
2343
+ env: { ...process.env, ...providerEnv, METAME_ACTIVE_SESSION: 'true' }
2344
+ });
2345
+ syncChild.on('error', () => {
2346
+ console.error("Could not launch 'claude'. Is Claude Code installed?");
2347
+ });
2348
+ }
2077
2349
  syncChild.on('close', (c) => process.exit(c || 0));
2078
2350
  return;
2079
2351
  }
@@ -2086,7 +2358,8 @@ if (isSync) {
2086
2358
  if (process.env.METAME_ACTIVE_SESSION === 'true') {
2087
2359
  console.error(`\n${icon("stop")} ACTION BLOCKED: Nested Session Detected`);
2088
2360
  console.error(" You are actively running inside a MetaMe session.");
2089
- console.error(" To reload configuration, use: \x1b[36m!metame refresh\x1b[0m\n");
2361
+ console.error(" To hot-reload daemon code from this session, run: \x1b[36mtouch ~/.metame/daemon.js\x1b[0m");
2362
+ console.error(" In this dev workspace, \x1b[36mtouch scripts/daemon.js\x1b[0m works too because ~/.metame/daemon.js is symlinked.\n");
2090
2363
  process.exit(1);
2091
2364
  }
2092
2365
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "metame-cli",
3
- "version": "1.5.3",
3
+ "version": "1.5.5",
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": {
@@ -17,7 +17,7 @@
17
17
  "test": "node --test scripts/*.test.js",
18
18
  "test:daemon-status": "node --test scripts/daemon-restart-status.test.js",
19
19
  "start": "node index.js",
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
+ "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-command-session-route.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'",
21
21
  "sync:readme": "node scripts/sync-readme.js",
22
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'",
23
23
  "precommit": "npm run sync:plugin && npm run restart:daemon"
@@ -127,11 +127,13 @@ function tryRemoveExisting(filePath) {
127
127
  * 3. plain file copy (last resort; note: will not track future changes to target)
128
128
  */
129
129
  function createLinkOrMirror(targetPath, linkPath) {
130
- const relativeTarget = path.relative(path.dirname(linkPath), targetPath) || path.basename(targetPath);
131
130
  tryRemoveExisting(linkPath);
132
131
 
133
132
  try {
134
- fs.symlinkSync(relativeTarget, linkPath, 'file');
133
+ // Use absolute symlinks here: agent layer lives under ~/.metame while workspaces can
134
+ // sit on a different top-level tree (/var, /Volumes, etc). Relative links are brittle
135
+ // across those roots and have produced broken SOUL.md/MEMORY.md views.
136
+ fs.symlinkSync(targetPath, linkPath, 'file');
135
137
  return { mode: 'symlink', path: linkPath };
136
138
  } catch (symlinkErr) {
137
139
  const sameRoot = path.parse(targetPath).root.toLowerCase() === path.parse(linkPath).root.toLowerCase();