metame-cli 1.5.4 → 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.
- package/README.md +6 -1
- package/index.js +277 -55
- package/package.json +2 -2
- package/scripts/agent-layer.js +4 -2
- package/scripts/bin/dispatch_to +17 -5
- package/scripts/daemon-admin-commands.js +264 -62
- package/scripts/daemon-agent-commands.js +188 -66
- package/scripts/daemon-bridges.js +447 -48
- package/scripts/daemon-claude-engine.js +650 -103
- package/scripts/daemon-command-router.js +134 -27
- package/scripts/daemon-command-session-route.js +118 -0
- package/scripts/daemon-default.yaml +2 -0
- package/scripts/daemon-engine-runtime.js +96 -20
- package/scripts/daemon-exec-commands.js +106 -50
- package/scripts/daemon-file-browser.js +63 -7
- package/scripts/daemon-notify.js +18 -4
- package/scripts/daemon-ops-commands.js +16 -2
- package/scripts/daemon-remote-dispatch.js +34 -2
- package/scripts/daemon-session-commands.js +102 -45
- package/scripts/daemon-session-store.js +497 -66
- package/scripts/daemon-siri-bridge.js +234 -0
- package/scripts/daemon-siri-imessage.js +209 -0
- package/scripts/daemon-task-scheduler.js +10 -2
- package/scripts/daemon.js +610 -181
- package/scripts/docs/hook-config.md +7 -4
- package/scripts/docs/maintenance-manual.md +8 -1
- package/scripts/feishu-adapter.js +7 -15
- package/scripts/hooks/doc-router.js +29 -0
- package/scripts/hooks/intent-doc-router.js +54 -0
- package/scripts/hooks/intent-engine.js +9 -40
- package/scripts/intent-registry.js +59 -0
- package/scripts/memory-extract.js +59 -0
- package/scripts/mentor-engine.js +6 -0
- package/scripts/schema.js +1 -0
- package/scripts/self-reflect.js +110 -12
- package/scripts/session-analytics.js +160 -0
- package/scripts/signal-capture.js +1 -1
- package/scripts/team-dispatch.js +150 -11
- package/scripts/hooks/intent-agent-manage.js +0 -50
- 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
|
|
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 (
|
|
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
|
-
|
|
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
|
|
2077
|
-
// Finds the most recent session and
|
|
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
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
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
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
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
|
|
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.
|
|
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"
|
package/scripts/agent-layer.js
CHANGED
|
@@ -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
|
-
|
|
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();
|
package/scripts/bin/dispatch_to
CHANGED
|
@@ -17,7 +17,11 @@ const os = require('os');
|
|
|
17
17
|
const { socketPath } = require('../platform');
|
|
18
18
|
const yaml = require('../resolve-yaml');
|
|
19
19
|
const { buildEnrichedPrompt, buildTeamRosterHint } = require('../team-dispatch');
|
|
20
|
-
const {
|
|
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
|
|
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 {
|