metame-cli 1.5.4 → 1.5.6

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 (44) hide show
  1. package/README.md +6 -1
  2. package/index.js +277 -55
  3. package/package.json +3 -2
  4. package/scripts/agent-layer.js +4 -2
  5. package/scripts/bin/dispatch_to +18 -6
  6. package/scripts/bin/push-clean.sh +72 -0
  7. package/scripts/daemon-admin-commands.js +266 -64
  8. package/scripts/daemon-agent-commands.js +188 -66
  9. package/scripts/daemon-bridges.js +475 -50
  10. package/scripts/daemon-checkpoints.js +84 -30
  11. package/scripts/daemon-claude-engine.js +651 -103
  12. package/scripts/daemon-command-router.js +134 -27
  13. package/scripts/daemon-command-session-route.js +118 -0
  14. package/scripts/daemon-default.yaml +2 -0
  15. package/scripts/daemon-dispatch-cards.js +185 -0
  16. package/scripts/daemon-engine-runtime.js +96 -20
  17. package/scripts/daemon-exec-commands.js +106 -50
  18. package/scripts/daemon-file-browser.js +63 -7
  19. package/scripts/daemon-notify.js +18 -4
  20. package/scripts/daemon-ops-commands.js +28 -6
  21. package/scripts/daemon-remote-dispatch.js +34 -2
  22. package/scripts/daemon-session-commands.js +102 -45
  23. package/scripts/daemon-session-store.js +497 -66
  24. package/scripts/daemon-siri-bridge.js +234 -0
  25. package/scripts/daemon-siri-imessage.js +209 -0
  26. package/scripts/daemon-task-scheduler.js +10 -2
  27. package/scripts/{team-dispatch.js → daemon-team-dispatch.js} +150 -11
  28. package/scripts/daemon.js +484 -181
  29. package/scripts/docs/hook-config.md +7 -4
  30. package/scripts/docs/maintenance-manual.md +10 -3
  31. package/scripts/docs/pointer-map.md +2 -2
  32. package/scripts/feishu-adapter.js +7 -15
  33. package/scripts/hooks/doc-router.js +29 -0
  34. package/scripts/hooks/intent-doc-router.js +54 -0
  35. package/scripts/hooks/intent-engine.js +9 -40
  36. package/scripts/intent-registry.js +59 -0
  37. package/scripts/memory-extract.js +59 -0
  38. package/scripts/mentor-engine.js +6 -0
  39. package/scripts/schema.js +1 -0
  40. package/scripts/self-reflect.js +110 -12
  41. package/scripts/session-analytics.js +160 -0
  42. package/scripts/signal-capture.js +1 -1
  43. package/scripts/hooks/intent-agent-manage.js +0 -50
  44. package/scripts/hooks/intent-hook-config.js +0 -28
package/README.md CHANGED
@@ -464,7 +464,7 @@ Each team member runs on a virtual chatId (`_agent_{key}`) and appears with its
464
464
 
465
465
  ### Cross-Device Dispatch
466
466
 
467
- Team members with `peer` field run on a different machine. Configure `feishu.remote_dispatch` on both machines with the same relay chat and shared secret:
467
+ Team members with `peer` field run on a different machine. Configure `feishu.remote_dispatch` on both machines with the same relay chat and shared secret, but do not share the same Feishu bot between machines. Each machine must use its own Feishu app/bot credentials.
468
468
 
469
469
  ```yaml
470
470
  feishu:
@@ -475,6 +475,11 @@ feishu:
475
475
  secret: shared-secret-key # HMAC signing key
476
476
  ```
477
477
 
478
+ Why separate bots are required:
479
+ - Feishu may deliver relay-chat events to either online client for the same bot.
480
+ - Current remote-dispatch handling drops packets addressed to a different `self` peer.
481
+ - If Windows and Mac share one bot, the wrong machine can consume and discard the packet.
482
+
478
483
  Use from mobile: `/dispatch to windows:hunter research competitors` or just mention by nickname — routing is automatic. Use `/dispatch peers` to check remote config status.
479
484
 
480
485
  ## Mobile Commands
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')) {
@@ -144,10 +234,76 @@ function syncDirFiles(srcDir, destDir, { fileList, chmod } = {}) {
144
234
  return updated;
145
235
  }
146
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
+
147
303
  // Auto-deploy bundled scripts to ~/.metame/
148
304
  // IMPORTANT: daemon.yaml is USER CONFIG — never overwrite it. Only daemon-default.yaml (template) is synced.
149
305
  const scriptsDir = path.join(__dirname, 'scripts');
150
- 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'];
151
307
  const DAEMON_MODULE_SCRIPTS = (() => {
152
308
  try {
153
309
  return fs.readdirSync(scriptsDir).filter((f) => /^daemon-[\w-]+\.js$/.test(f));
@@ -192,22 +348,36 @@ if (syntaxErrors.length > 0) {
192
348
  console.error('Fix the errors before deploying. Daemon continues running with old code.');
193
349
  } else {
194
350
  scriptsUpdated = syncDirFiles(scriptsDir, METAME_DIR, { fileList: BUNDLED_SCRIPTS });
195
-
196
- // Daemon restart on script update:
197
- // Don't kill daemon here — daemon's own file watcher detects ~/.metame/daemon.js changes
198
- // and has defer logic (waits for active Claude tasks to finish before restarting).
199
- // Killing here bypasses that and interrupts ongoing conversations.
200
351
  if (scriptsUpdated) {
201
- console.log(`${icon("pkg")} Scripts ${IS_DEV_MODE ? 'symlinked' : 'synced'} to ~/.metame/ — daemon will auto-restart when idle.`);
352
+ console.log(`${icon("pkg")} Scripts ${IS_DEV_MODE ? 'symlinked' : 'synced'} to ~/.metame/.`);
202
353
  }
203
354
  }
204
355
 
205
356
  // Docs: lazy-load references for CLAUDE.md pointer instructions
206
357
  syncDirFiles(path.join(__dirname, 'scripts', 'docs'), path.join(METAME_DIR, 'docs'));
207
358
  // Bin: CLI tools (dispatch_to etc.)
208
- 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 });
209
360
  // Hooks: Claude Code event hooks (Stop, PostToolUse, etc.)
210
- 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
+ }
211
381
 
212
382
  // ---------------------------------------------------------
213
383
  // Deploy bundled skills to ~/.claude/skills/
@@ -834,6 +1004,7 @@ try {
834
1004
 
835
1005
  // Find a pattern that hasn't been surfaced in 14 days
836
1006
  const candidate = brainDoc.growth.patterns.find(p => {
1007
+ if (!p || typeof p.summary !== 'string') return false;
837
1008
  if (!p.surfaced) return true;
838
1009
  return (now - new Date(p.surfaced).getTime()) > COOLDOWN_MS;
839
1010
  });
@@ -1307,17 +1478,34 @@ if (isInsights) {
1307
1478
  try {
1308
1479
  const doc = yaml.load(fs.readFileSync(BRAIN_FILE, 'utf8')) || {};
1309
1480
  const patterns = (doc.growth && doc.growth.patterns) || [];
1481
+ const reflectionPatterns = (doc.growth && doc.growth.self_reflection_patterns) || [];
1310
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]);
1311
1488
 
1312
- if (patterns.length === 0) {
1489
+ if (userPatterns.length === 0 && aiReflections.length === 0) {
1313
1490
  console.log(`${icon("search")} MetaMe: No patterns detected yet. Keep using MetaMe and patterns will emerge after ~5 sessions.`);
1314
1491
  } else {
1315
1492
  console.log(`${icon("mirror")} MetaMe Insights:\n`);
1316
- patterns.forEach((p, i) => {
1493
+ if (userPatterns.length > 0) {
1494
+ console.log('User observation:');
1495
+ }
1496
+ userPatterns.forEach((p, i) => {
1317
1497
  const sym = p.type === 'avoidance' ? icon("warn") : p.type === 'growth' ? '+' : p.type === 'energy' ? '*' : icon("reload");
1318
1498
  console.log(` ${sym} [${p.type}] ${p.summary} (confidence: ${(p.confidence * 100).toFixed(0)}%)`);
1319
1499
  console.log(` Detected: ${p.detected}${p.surfaced ? `, Last shown: ${p.surfaced}` : ''}`);
1320
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
+ }
1321
1509
  if (zoneHistory.length > 0) {
1322
1510
  console.log(`\n ${icon("chart")} Recent zone history: ${zoneHistory.join(' → ')}`);
1323
1511
  console.log(` (C=Comfort, S=Stretch, P=Panic)`);
@@ -1525,7 +1713,7 @@ if (isProvider) {
1525
1713
  // 5.7 DAEMON SUBCOMMANDS
1526
1714
  // ---------------------------------------------------------
1527
1715
  // Shorthand aliases: `metame start` → `metame daemon start`, etc.
1528
- const DAEMON_SHORTCUTS = ['start', 'stop', 'status', 'logs'];
1716
+ const DAEMON_SHORTCUTS = ['start', 'stop', 'restart', 'status', 'logs'];
1529
1717
  if (DAEMON_SHORTCUTS.includes(process.argv[2])) {
1530
1718
  process.argv.splice(2, 0, 'daemon');
1531
1719
  }
@@ -1907,6 +2095,48 @@ WantedBy=default.target
1907
2095
  process.exit(0);
1908
2096
  }
1909
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
+
1910
2140
  if (subCmd === 'status') {
1911
2141
  let state = {};
1912
2142
  try { state = JSON.parse(fs.readFileSync(DAEMON_STATE, 'utf8')); } catch { /* empty */ }
@@ -2010,6 +2240,7 @@ WantedBy=default.target
2010
2240
  console.log(`${icon("book")} Daemon Commands:`);
2011
2241
  console.log(" metame start — start background daemon");
2012
2242
  console.log(" metame stop — stop daemon");
2243
+ console.log(" metame restart — graceful restart daemon");
2013
2244
  console.log(" metame status — show status & budget");
2014
2245
  console.log(" metame logs — tail log file");
2015
2246
  console.log(" metame daemon init — initialize config");
@@ -2073,58 +2304,48 @@ if (isCodex) {
2073
2304
  // ---------------------------------------------------------
2074
2305
  // 5.9 CONTINUE/SYNC — resume latest session from terminal
2075
2306
  // ---------------------------------------------------------
2076
- // Usage: exit Claude first, then run `metame continue` from terminal.
2077
- // 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.
2078
2309
  const isSync = process.argv.includes('sync') || process.argv.includes('continue');
2079
2310
  if (isSync) {
2080
2311
  const projectsRoot = path.join(HOME_DIR, '.claude', 'projects');
2081
- let bestSession = null;
2082
- try {
2083
- const cwd = process.cwd();
2084
- const projDir = path.join(projectsRoot, cwd.replace(/\//g, '-'));
2085
- const findLatest = (dir) => {
2086
- try {
2087
- return fs.readdirSync(dir)
2088
- .filter(f => f.endsWith('.jsonl'))
2089
- .map(f => ({ id: f.replace('.jsonl', ''), mtime: fs.statSync(path.join(dir, f)).mtimeMs }))
2090
- .sort((a, b) => b.mtime - a.mtime)[0] || null;
2091
- } catch { return null; }
2092
- };
2093
- const localBest = findLatest(projDir);
2094
- // Always scan globally to find the absolute most recent session
2095
- // (phone /continue may have worked in a different project's session)
2096
- let globalBest = null;
2097
- try {
2098
- for (const d of fs.readdirSync(projectsRoot)) {
2099
- const s = findLatest(path.join(projectsRoot, d));
2100
- if (s && (!globalBest || s.mtime > globalBest.mtime)) globalBest = s;
2101
- }
2102
- } catch { /* ignore */ }
2103
- // Use global best if it's more recent than local; prefer local otherwise
2104
- if (localBest && globalBest && globalBest.mtime > localBest.mtime) {
2105
- bestSession = globalBest;
2106
- console.log(` (global session is newer than local — using global)`);
2107
- } else {
2108
- bestSession = localBest || globalBest;
2109
- }
2110
- } 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;
2111
2318
 
2112
2319
  if (!bestSession) {
2113
2320
  console.error('No session found.');
2114
2321
  process.exit(1);
2115
2322
  }
2116
2323
 
2324
+ if (bestSession.scope === 'global') {
2325
+ console.log(' (global session is newer than local — using global)');
2326
+ }
2117
2327
  console.log(`\n${icon("reload")} Resuming session ${bestSession.id.slice(0, 8)}...\n`);
2118
- const providerEnv = (() => { try { return require(path.join(__dirname, 'scripts', 'providers.js')).buildActiveEnv(); } catch { return {}; } })();
2119
- const resumeArgs = ['--resume', bestSession.id];
2120
- if (daemonCfg.dangerously_skip_permissions) resumeArgs.push('--dangerously-skip-permissions');
2121
- const syncChild = spawnClaude(resumeArgs, {
2122
- stdio: 'inherit',
2123
- env: { ...process.env, ...providerEnv, METAME_ACTIVE_SESSION: 'true' }
2124
- });
2125
- syncChild.on('error', () => {
2126
- console.error("Could not launch 'claude'. Is Claude Code installed?");
2127
- });
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
+ }
2128
2349
  syncChild.on('close', (c) => process.exit(c || 0));
2129
2350
  return;
2130
2351
  }
@@ -2137,7 +2358,8 @@ if (isSync) {
2137
2358
  if (process.env.METAME_ACTIVE_SESSION === 'true') {
2138
2359
  console.error(`\n${icon("stop")} ACTION BLOCKED: Nested Session Detected`);
2139
2360
  console.error(" You are actively running inside a MetaMe session.");
2140
- 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");
2141
2363
  process.exit(1);
2142
2364
  }
2143
2365
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "metame-cli",
3
- "version": "1.5.4",
3
+ "version": "1.5.6",
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,8 @@
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
+ "push": "bash scripts/bin/push-clean.sh",
21
+ "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/daemon-team-dispatch.js scripts/daemon-dispatch-cards.js scripts/daemon-remote-dispatch.js scripts/daemon-siri-bridge.js scripts/daemon-siri-imessage.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
22
  "sync:readme": "node scripts/sync-readme.js",
22
23
  "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
24
  "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();
@@ -16,8 +16,12 @@ const crypto = require('crypto');
16
16
  const os = require('os');
17
17
  const { socketPath } = require('../platform');
18
18
  const yaml = require('../resolve-yaml');
19
- const { buildEnrichedPrompt, buildTeamRosterHint } = require('../team-dispatch');
20
- const { parseRemoteTargetRef, normalizeRemoteDispatchConfig, encodePacket } = require('../daemon-remote-dispatch');
19
+ const { buildEnrichedPrompt, buildTeamRosterHint } = require('../daemon-team-dispatch');
20
+ const {
21
+ parseRemoteTargetRef,
22
+ normalizeRemoteDispatchConfig,
23
+ encodePacket,
24
+ } = require('../daemon-remote-dispatch');
21
25
 
22
26
  const METAME_DIR = path.join(os.homedir(), '.metame');
23
27
  const DISPATCH_DIR = path.join(METAME_DIR, 'dispatch');
@@ -30,6 +34,7 @@ const args = process.argv.slice(2);
30
34
  const newSession = args[0] === '--new' ? (args.shift(), true) : false;
31
35
 
32
36
  let fromKey = process.env.METAME_PROJECT || '_claude_session';
37
+ const sourceSenderId = String(process.env.METAME_SENDER_ID || '').trim();
33
38
  const fromIdx = args.indexOf('--from');
34
39
  if (fromIdx !== -1 && args[fromIdx + 1]) {
35
40
  fromKey = args.splice(fromIdx, 2)[1];
@@ -64,7 +69,9 @@ function sendOne(memberTarget, memberPrompt, opts = {}) {
64
69
  return new Promise((resolve) => {
65
70
  const ts = new Date().toISOString();
66
71
  const secret = getDispatchSecret();
67
- const enriched = opts.skipEnrich ? memberPrompt : buildEnrichedPrompt(memberTarget, memberPrompt, METAME_DIR);
72
+ const enriched = opts.skipEnrich
73
+ ? memberPrompt
74
+ : buildEnrichedPrompt(memberTarget, memberPrompt, METAME_DIR, { includeShared: !!opts.includeShared });
68
75
  const sigPayload = JSON.stringify({ target: memberTarget, prompt: enriched, ts });
69
76
  const sig = crypto.createHmac('sha256', secret).update(sigPayload).digest('hex');
70
77
 
@@ -73,6 +80,7 @@ function sendOne(memberTarget, memberPrompt, opts = {}) {
73
80
  target: memberTarget,
74
81
  prompt: enriched,
75
82
  from: fromKey,
83
+ source_sender_id: sourceSenderId,
76
84
  new_session: newSession,
77
85
  created_at: ts,
78
86
  ts,
@@ -141,6 +149,10 @@ function sendRemoteViaRelay(peer, project, memberPrompt) {
141
149
  console.error('dispatch_to: feishu.remote_dispatch not configured or disabled');
142
150
  process.exit(1);
143
151
  }
152
+ if (!rd.secret) {
153
+ console.error('dispatch_to: remote dispatch secret missing; run /dispatch code then /dispatch pair <code>');
154
+ process.exit(1);
155
+ }
144
156
  const ts = new Date().toISOString();
145
157
  const id = `${rd.selfPeer}_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
146
158
  const body = encodePacket({
@@ -151,11 +163,11 @@ function sendRemoteViaRelay(peer, project, memberPrompt) {
151
163
  target_project: project,
152
164
  prompt: memberPrompt,
153
165
  source_sender_key: fromKey,
166
+ source_sender_id: sourceSenderId,
154
167
  }, rd.secret);
155
168
 
156
169
  // Write to dispatch/remote-pending.jsonl for daemon to pick up and send via bot
157
170
  const remotePending = path.join(DISPATCH_DIR, 'remote-pending.jsonl');
158
- fs.mkdirSync(DISPATCH_DIR, { recursive: true });
159
171
  fs.appendFileSync(remotePending, JSON.stringify({ relay_chat_id: rd.chatId, body }) + '\n');
160
172
  console.log(`DISPATCH_OK(remote): ${peer}:${project} → ${memberPrompt.slice(0, 60)}`);
161
173
  }
@@ -187,7 +199,7 @@ if (teamMode) {
187
199
  // Await all dispatches before exiting so async socket/file ops complete
188
200
  Promise.all(team.map((member) => {
189
201
  const roster = buildTeamRosterHint(target, member.key, config.projects);
190
- const enriched = buildEnrichedPrompt(member.key, prompt, METAME_DIR);
202
+ const enriched = buildEnrichedPrompt(member.key, prompt, METAME_DIR, { includeShared: true });
191
203
  const memberPrompt = roster ? `${roster}\n\n---\n${enriched}` : enriched;
192
204
  // Remote member → relay dispatch
193
205
  if (member.peer) {
@@ -201,7 +213,7 @@ if (teamMode) {
201
213
  // ── Normal single-target dispatch ─────────────────────────────────────────────
202
214
  const remoteTarget = parseRemoteTargetRef(target);
203
215
  if (remoteTarget) {
204
- const enriched = buildEnrichedPrompt(remoteTarget.project, prompt, METAME_DIR);
216
+ const enriched = buildEnrichedPrompt(remoteTarget.project, prompt, METAME_DIR, { includeShared: false });
205
217
  sendRemoteViaRelay(remoteTarget.peer, remoteTarget.project, enriched);
206
218
  process.exit(0);
207
219
  } else {
@@ -0,0 +1,72 @@
1
+ #!/usr/bin/env bash
2
+ # push-clean.sh — Push to remote, stripping local-only checkpoint/safety commits.
3
+ #
4
+ # Usage: npm run push
5
+ #
6
+ # What it does:
7
+ # 1. Collects non-checkpoint commits between origin/main and HEAD (in order).
8
+ # 2. If none exist → plain `git push` (nothing to strip).
9
+ # 3. Otherwise → cherry-picks them onto a temp branch based at origin/main,
10
+ # pushes that branch as origin/main, then resets local main to match remote.
11
+
12
+ set -euo pipefail
13
+
14
+ REMOTE="${METAME_PUSH_REMOTE:-origin}"
15
+ BRANCH="${METAME_PUSH_BRANCH:-main}"
16
+ TEMP_BRANCH="_push-clean-$(date +%s)"
17
+
18
+ upstream="$REMOTE/$BRANCH"
19
+
20
+ # ── 1. Fetch so we have an up-to-date upstream ref ────────────────────────────
21
+ echo "[push] Fetching $upstream …"
22
+ git fetch "$REMOTE" "$BRANCH" --quiet
23
+
24
+ # ── 2. Collect ALL commits ahead of upstream (oldest first) ──────────────────
25
+ all_commits=()
26
+ while IFS= read -r sha; do
27
+ [ -n "$sha" ] && all_commits+=("$sha")
28
+ done < <(git log --reverse --format="%H" "$upstream"..HEAD 2>/dev/null)
29
+
30
+ if [ ${#all_commits[@]} -eq 0 ]; then
31
+ echo "[push] Nothing ahead of $upstream — already up to date."
32
+ exit 0
33
+ fi
34
+
35
+ # ── 3. Filter out checkpoint/safety commits ───────────────────────────────────
36
+ clean_commits=()
37
+ cp_count=0
38
+ for sha in "${all_commits[@]}"; do
39
+ subject=$(git log -1 --format="%s" "$sha")
40
+ if echo "$subject" | grep -qE '^\[metame-checkpoint\]|^\[metame-safety\]'; then
41
+ cp_count=$((cp_count + 1))
42
+ echo "[push] Skipping checkpoint: $sha ${subject:0:60}"
43
+ else
44
+ clean_commits+=("$sha")
45
+ fi
46
+ done
47
+
48
+ if [ ${#clean_commits[@]} -eq 0 ]; then
49
+ echo "[push] Only checkpoint commits ahead — nothing to push."
50
+ exit 0
51
+ fi
52
+
53
+ echo "[push] Pushing ${#clean_commits[@]} commit(s) (skipping $cp_count checkpoint(s)) …"
54
+
55
+ # ── 4. Cherry-pick onto a temp branch at upstream ────────────────────────────
56
+ git checkout -q -b "$TEMP_BRANCH" "$upstream"
57
+
58
+ for sha in "${clean_commits[@]}"; do
59
+ subject=$(git log -1 --format="%s" "$sha")
60
+ echo "[push] cherry-pick $sha ${subject:0:60}"
61
+ git cherry-pick --allow-empty --keep-redundant-commits "$sha" --quiet
62
+ done
63
+
64
+ # ── 5. Push temp branch → remote main ────────────────────────────────────────
65
+ git push "$REMOTE" "$TEMP_BRANCH:$BRANCH"
66
+
67
+ # ── 6. Reset local main to match remote (fast-forward) ───────────────────────
68
+ git checkout -q "$BRANCH"
69
+ git reset --hard "$REMOTE/$BRANCH"
70
+ git branch -D "$TEMP_BRANCH"
71
+
72
+ echo "[push] Done. Local $BRANCH is now aligned with $REMOTE/$BRANCH."