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.
- package/README.md +60 -18
- package/index.js +352 -79
- package/package.json +2 -2
- package/scripts/agent-layer.js +4 -2
- package/scripts/bin/dispatch_to +178 -90
- package/scripts/daemon-admin-commands.js +353 -105
- package/scripts/daemon-agent-commands.js +434 -66
- package/scripts/daemon-bridges.js +477 -68
- package/scripts/daemon-claude-engine.js +1267 -674
- package/scripts/daemon-command-router.js +205 -27
- package/scripts/daemon-command-session-route.js +118 -0
- package/scripts/daemon-default.yaml +7 -0
- package/scripts/daemon-engine-runtime.js +96 -20
- package/scripts/daemon-exec-commands.js +108 -49
- package/scripts/daemon-file-browser.js +64 -7
- package/scripts/daemon-notify.js +18 -4
- package/scripts/daemon-ops-commands.js +16 -2
- package/scripts/daemon-remote-dispatch.js +55 -1
- package/scripts/daemon-runtime-lifecycle.js +87 -0
- 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 +697 -179
- package/scripts/daemon.yaml +7 -0
- package/scripts/docs/agent-guide.md +36 -3
- package/scripts/docs/hook-config.md +134 -0
- package/scripts/docs/maintenance-manual.md +162 -5
- package/scripts/docs/pointer-map.md +60 -5
- package/scripts/feishu-adapter.js +7 -15
- package/scripts/hooks/doc-router.js +29 -0
- package/scripts/hooks/hook-utils.js +61 -0
- package/scripts/hooks/intent-doc-router.js +54 -0
- package/scripts/hooks/intent-engine.js +72 -0
- package/scripts/hooks/intent-file-transfer.js +51 -0
- package/scripts/hooks/intent-memory-recall.js +35 -0
- package/scripts/hooks/intent-ops-assist.js +54 -0
- package/scripts/hooks/intent-task-create.js +35 -0
- package/scripts/hooks/intent-team-dispatch.js +106 -0
- package/scripts/hooks/team-context.js +143 -0
- package/scripts/intent-registry.js +59 -0
- package/scripts/memory-extract.js +59 -0
- package/scripts/memory-nightly-reflect.js +109 -43
- package/scripts/memory.js +55 -17
- 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 +315 -0
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
const { classifyTaskUsage } = require('./usage-classifier');
|
|
4
4
|
const { normalizeModel } = require('./daemon-task-scheduler');
|
|
5
|
+
const { createCommandSessionResolver } = require('./daemon-command-session-route');
|
|
5
6
|
|
|
6
7
|
function createExecCommandHandler(deps) {
|
|
7
8
|
const {
|
|
@@ -20,12 +21,23 @@ function createExecCommandHandler(deps) {
|
|
|
20
21
|
loadState,
|
|
21
22
|
saveState,
|
|
22
23
|
getSession,
|
|
24
|
+
getSessionForEngine,
|
|
23
25
|
getSessionName,
|
|
24
26
|
createSession,
|
|
25
27
|
findSessionFile,
|
|
28
|
+
findCodexSessionFile = null,
|
|
26
29
|
loadConfig,
|
|
27
30
|
getDistillModel,
|
|
31
|
+
getDefaultEngine = () => 'claude',
|
|
28
32
|
} = deps;
|
|
33
|
+
const { getActiveSession } = createCommandSessionResolver({
|
|
34
|
+
path,
|
|
35
|
+
loadConfig,
|
|
36
|
+
loadState,
|
|
37
|
+
getSession,
|
|
38
|
+
getSessionForEngine,
|
|
39
|
+
getDefaultEngine,
|
|
40
|
+
});
|
|
29
41
|
|
|
30
42
|
function truncateOutput(output, maxLen = 4000) {
|
|
31
43
|
const text = (output || '').trim() || '(no output)';
|
|
@@ -234,7 +246,7 @@ function createExecCommandHandler(deps) {
|
|
|
234
246
|
const signal = proc.killSignal || 'SIGTERM';
|
|
235
247
|
try { process.kill(-proc.child.pid, signal); } catch { try { proc.child.kill(signal); } catch { /* */ } }
|
|
236
248
|
}
|
|
237
|
-
const session =
|
|
249
|
+
const { session } = getActiveSession(chatId);
|
|
238
250
|
const name = session ? getSessionName(session.id) : null;
|
|
239
251
|
const label = name || (session ? session.id.slice(0, 8) : 'none');
|
|
240
252
|
await bot.sendMessage(chatId, `🔄 Session restarted. MCP/config reloaded.\n📁 ${session ? path.basename(session.cwd) : '~'} [${label}]`);
|
|
@@ -243,50 +255,84 @@ function createExecCommandHandler(deps) {
|
|
|
243
255
|
|
|
244
256
|
// /compact — compress current session context to save tokens
|
|
245
257
|
if (text === '/compact') {
|
|
246
|
-
const session =
|
|
247
|
-
if (!session || !session.started) {
|
|
258
|
+
const { sessionKey, engine, session } = getActiveSession(chatId);
|
|
259
|
+
if (!session || !session.id || !session.started) {
|
|
248
260
|
await bot.sendMessage(chatId, '❌ No active session to compact.');
|
|
249
261
|
return true;
|
|
250
262
|
}
|
|
251
|
-
if (String(session.engine || '').toLowerCase() === 'codex') {
|
|
252
|
-
await bot.sendMessage(chatId, '⚠️ Codex 会话暂不支持 /compact,请继续在同一会话里对话。');
|
|
253
|
-
return true;
|
|
254
|
-
}
|
|
255
263
|
await bot.sendMessage(chatId, '🗜 Compacting session...');
|
|
256
264
|
|
|
257
|
-
// Step 1: Read conversation
|
|
258
|
-
const
|
|
259
|
-
if (!jsonlPath) {
|
|
260
|
-
await bot.sendMessage(chatId, '❌ Session file not found.');
|
|
261
|
-
return true;
|
|
262
|
-
}
|
|
265
|
+
// Step 1: Read conversation (JSONL for Claude, rollout JSONL for Codex)
|
|
266
|
+
const isCodex = String(engine).toLowerCase() === 'codex';
|
|
263
267
|
const messages = [];
|
|
264
|
-
|
|
265
|
-
const
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
268
|
+
if (isCodex) {
|
|
269
|
+
const codexFile = findCodexSessionFile && findCodexSessionFile(session.id);
|
|
270
|
+
if (!codexFile) {
|
|
271
|
+
await bot.sendMessage(chatId, '❌ Codex session file not found.');
|
|
272
|
+
return true;
|
|
273
|
+
}
|
|
274
|
+
try {
|
|
275
|
+
const lines = fs.readFileSync(codexFile, 'utf8').split('\n').filter(Boolean);
|
|
276
|
+
for (const line of lines) {
|
|
277
|
+
try {
|
|
278
|
+
const obj = JSON.parse(line);
|
|
279
|
+
if (obj.type !== 'response_item') continue;
|
|
280
|
+
const p = obj.payload;
|
|
281
|
+
if (!p || p.type !== 'message') continue;
|
|
282
|
+
const role = String(p.role || '').toLowerCase();
|
|
283
|
+
if (role !== 'user' && role !== 'assistant') continue;
|
|
284
|
+
const content = p.content || p;
|
|
272
285
|
let textContent = '';
|
|
273
286
|
if (typeof content === 'string') {
|
|
274
287
|
textContent = content;
|
|
275
288
|
} else if (Array.isArray(content)) {
|
|
276
|
-
textContent = content
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
289
|
+
textContent = content.map(c => {
|
|
290
|
+
if (typeof c === 'string') return c;
|
|
291
|
+
if (c && typeof c.text === 'string') return c.text;
|
|
292
|
+
return '';
|
|
293
|
+
}).filter(Boolean).join(' ');
|
|
280
294
|
}
|
|
281
|
-
|
|
282
|
-
|
|
295
|
+
textContent = textContent.trim();
|
|
296
|
+
if (textContent) messages.push({ role, text: textContent });
|
|
297
|
+
} catch { /* skip malformed lines */ }
|
|
298
|
+
}
|
|
299
|
+
} catch (e) {
|
|
300
|
+
await bot.sendMessage(chatId, `❌ Cannot read Codex session: ${e.message}`);
|
|
301
|
+
return true;
|
|
302
|
+
}
|
|
303
|
+
} else {
|
|
304
|
+
const jsonlPath = findSessionFile(session.id);
|
|
305
|
+
if (!jsonlPath) {
|
|
306
|
+
await bot.sendMessage(chatId, '❌ Session file not found.');
|
|
307
|
+
return true;
|
|
308
|
+
}
|
|
309
|
+
try {
|
|
310
|
+
const lines = fs.readFileSync(jsonlPath, 'utf8').split('\n').filter(Boolean);
|
|
311
|
+
for (const line of lines) {
|
|
312
|
+
try {
|
|
313
|
+
const obj = JSON.parse(line);
|
|
314
|
+
if (obj.type === 'user' || obj.type === 'assistant') {
|
|
315
|
+
const msg = obj.message || {};
|
|
316
|
+
const content = msg.content;
|
|
317
|
+
let textContent = '';
|
|
318
|
+
if (typeof content === 'string') {
|
|
319
|
+
textContent = content;
|
|
320
|
+
} else if (Array.isArray(content)) {
|
|
321
|
+
textContent = content
|
|
322
|
+
.filter(c => c.type === 'text')
|
|
323
|
+
.map(c => c.text || '')
|
|
324
|
+
.join(' ');
|
|
325
|
+
}
|
|
326
|
+
if (textContent.trim()) {
|
|
327
|
+
messages.push({ role: obj.type, text: textContent.trim() });
|
|
328
|
+
}
|
|
283
329
|
}
|
|
284
|
-
}
|
|
285
|
-
}
|
|
330
|
+
} catch { /* skip malformed lines */ }
|
|
331
|
+
}
|
|
332
|
+
} catch (e) {
|
|
333
|
+
await bot.sendMessage(chatId, `❌ Cannot read session: ${e.message}`);
|
|
334
|
+
return true;
|
|
286
335
|
}
|
|
287
|
-
} catch (e) {
|
|
288
|
-
await bot.sendMessage(chatId, `❌ Cannot read session: ${e.message}`);
|
|
289
|
-
return true;
|
|
290
336
|
}
|
|
291
337
|
|
|
292
338
|
if (messages.length === 0) {
|
|
@@ -322,23 +368,36 @@ function createExecCommandHandler(deps) {
|
|
|
322
368
|
}
|
|
323
369
|
|
|
324
370
|
// Step 4: Create new session with the summary
|
|
325
|
-
const model = daemonCfg.model || 'opus';
|
|
326
371
|
const oldName = getSessionName(session.id);
|
|
327
|
-
const newSession = createSession(
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
return true;
|
|
336
|
-
}
|
|
337
|
-
// Mark as started
|
|
338
|
-
const state2 = loadState();
|
|
339
|
-
if (state2.sessions[chatId]) {
|
|
340
|
-
state2.sessions[chatId].started = true;
|
|
372
|
+
const newSession = createSession(sessionKey, session.cwd, oldName ? oldName + ' (compacted)' : '', engine);
|
|
373
|
+
if (isCodex) {
|
|
374
|
+
// Codex doesn't support --session-id init; store context for first-message injection in engine
|
|
375
|
+
const state2 = loadState();
|
|
376
|
+
if (!state2.sessions[sessionKey]) state2.sessions[sessionKey] = {};
|
|
377
|
+
if (!state2.sessions[sessionKey].engines) state2.sessions[sessionKey].engines = {};
|
|
378
|
+
if (!state2.sessions[sessionKey].engines[engine]) state2.sessions[sessionKey].engines[engine] = {};
|
|
379
|
+
state2.sessions[sessionKey].engines[engine].compactContext = output;
|
|
341
380
|
saveState(state2);
|
|
381
|
+
} else {
|
|
382
|
+
// Claude: warm up the new session immediately via --session-id
|
|
383
|
+
const model = daemonCfg.model || 'opus';
|
|
384
|
+
const initArgs = ['-p', '--session-id', newSession.id, '--model', model];
|
|
385
|
+
if (daemonCfg.dangerously_skip_permissions) initArgs.push('--dangerously-skip-permissions');
|
|
386
|
+
const preamble = buildProfilePreamble();
|
|
387
|
+
const initPrompt = preamble + `Here is the context from our previous session (compacted):\n\n${output}\n\nContext loaded. Ready to continue.`;
|
|
388
|
+
const { error: initErr } = await spawnClaudeAsync(initArgs, initPrompt, session.cwd, 60000);
|
|
389
|
+
if (initErr) {
|
|
390
|
+
await bot.sendMessage(chatId, `⚠️ Summary saved but new session init failed: ${initErr}`);
|
|
391
|
+
return true;
|
|
392
|
+
}
|
|
393
|
+
const state2 = loadState();
|
|
394
|
+
if (state2.sessions[sessionKey]) {
|
|
395
|
+
state2.sessions[sessionKey].started = true;
|
|
396
|
+
if (state2.sessions[sessionKey].engines && state2.sessions[sessionKey].engines[engine]) {
|
|
397
|
+
state2.sessions[sessionKey].engines[engine].started = true;
|
|
398
|
+
}
|
|
399
|
+
saveState(state2);
|
|
400
|
+
}
|
|
342
401
|
}
|
|
343
402
|
const tokenEst = Math.round(output.length / 3.5);
|
|
344
403
|
await bot.sendMessage(chatId, `✅ Compacted! ~${tokenEst} tokens of context carried over.\nNew session: ${newSession.id.slice(0, 8)}`);
|
|
@@ -352,8 +411,8 @@ function createExecCommandHandler(deps) {
|
|
|
352
411
|
await bot.sendMessage(chatId, '用法: /publish 123456');
|
|
353
412
|
return true;
|
|
354
413
|
}
|
|
355
|
-
const session =
|
|
356
|
-
const cwd = session?.cwd || HOME;
|
|
414
|
+
const { route, session } = getActiveSession(chatId);
|
|
415
|
+
const cwd = session?.cwd || route.cwd || HOME;
|
|
357
416
|
await bot.sendMessage(chatId, `📦 npm publish --otp=${otp} ...`);
|
|
358
417
|
try {
|
|
359
418
|
const result = await runCommand('npm', ['publish', `--otp=${otp}`], { cwd, timeout: 60000 });
|
|
@@ -58,15 +58,71 @@ function createFileBrowser(deps) {
|
|
|
58
58
|
return entry.path;
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
+
function appendDebugLog(message) {
|
|
62
|
+
try {
|
|
63
|
+
fs.appendFileSync(path.join(HOME, '.metame', 'daemon.log'), `[${new Date().toISOString()}] [INFO] [file-browser] ${message}\n`);
|
|
64
|
+
} catch {}
|
|
65
|
+
}
|
|
66
|
+
|
|
61
67
|
async function sendFileButtons(bot, chatId, files) {
|
|
62
|
-
if (
|
|
68
|
+
if (files.size === 0) return;
|
|
63
69
|
const validFiles = [...files].filter(f => fs.existsSync(f));
|
|
64
|
-
if (validFiles.length === 0) return;
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
+
if (validFiles.length === 0) return [];
|
|
71
|
+
const fallbackText = `📂 文件已生成:\n${validFiles.map(filePath => `- ${filePath}`).join('\n')}`;
|
|
72
|
+
const sentMessages = [];
|
|
73
|
+
appendDebugLog(`sendFileButtons chat=${chatId} hasSendFile=${typeof bot.sendFile === 'function'} hasSendButtons=${typeof bot.sendButtons === 'function'} hasEditMessage=${typeof bot.editMessage === 'function'} hasSendCard=${typeof bot.sendCard === 'function'} files=${validFiles.map(filePath => path.basename(filePath)).join(',')}`);
|
|
74
|
+
|
|
75
|
+
// Prefer direct file delivery when the adapter supports it.
|
|
76
|
+
// This matches the user's expectation on mobile and avoids brittle
|
|
77
|
+
// button-card transport on platforms like Feishu.
|
|
78
|
+
if (bot.sendFile) {
|
|
79
|
+
try {
|
|
80
|
+
appendDebugLog(`attempting direct sendFile chat=${chatId}`);
|
|
81
|
+
for (const filePath of validFiles) {
|
|
82
|
+
const msg = await bot.sendFile(chatId, filePath);
|
|
83
|
+
if (msg && msg.message_id) sentMessages.push(msg);
|
|
84
|
+
}
|
|
85
|
+
appendDebugLog(`direct sendFile success chat=${chatId} count=${sentMessages.length}`);
|
|
86
|
+
return sentMessages;
|
|
87
|
+
} catch (err) {
|
|
88
|
+
const detail = err && err.stack ? err.stack : String(err && err.message ? err.message : err);
|
|
89
|
+
appendDebugLog(`sendFile failed chat=${chatId} err=${detail.split('\n')[0]}`);
|
|
90
|
+
if (bot.sendMessage) {
|
|
91
|
+
const msg = await bot.sendMessage(chatId, fallbackText);
|
|
92
|
+
if (msg && msg.message_id) sentMessages.push(msg);
|
|
93
|
+
}
|
|
94
|
+
appendDebugLog(`sendFile fallback text chat=${chatId} count=${sentMessages.length}`);
|
|
95
|
+
return sentMessages;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const isStreamCardBot = typeof bot.editMessage === 'function' || typeof bot.sendCard === 'function';
|
|
100
|
+
if (!bot.sendButtons || isStreamCardBot) {
|
|
101
|
+
appendDebugLog(`button fallback skipped chat=${chatId} hasSendButtons=${typeof bot.sendButtons === 'function'} isStreamCardBot=${isStreamCardBot}`);
|
|
102
|
+
if (bot.sendMessage) {
|
|
103
|
+
const msg = await bot.sendMessage(chatId, fallbackText);
|
|
104
|
+
if (msg && msg.message_id) sentMessages.push(msg);
|
|
105
|
+
}
|
|
106
|
+
return sentMessages;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
try {
|
|
110
|
+
appendDebugLog(`attempting sendButtons chat=${chatId}`);
|
|
111
|
+
const buttons = validFiles.map(filePath => {
|
|
112
|
+
const shortId = cacheFile(filePath);
|
|
113
|
+
return [{ text: `📎 ${path.basename(filePath)}`, callback_data: `/file ${shortId}` }];
|
|
114
|
+
});
|
|
115
|
+
const msg = await bot.sendButtons(chatId, '📂 文件:', buttons);
|
|
116
|
+
if (msg && msg.message_id) sentMessages.push(msg);
|
|
117
|
+
appendDebugLog(`sendButtons success chat=${chatId}`);
|
|
118
|
+
} catch {
|
|
119
|
+
appendDebugLog(`sendButtons failed chat=${chatId}`);
|
|
120
|
+
if (bot.sendMessage) {
|
|
121
|
+
const msg = await bot.sendMessage(chatId, fallbackText);
|
|
122
|
+
if (msg && msg.message_id) sentMessages.push(msg);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return sentMessages;
|
|
70
126
|
}
|
|
71
127
|
|
|
72
128
|
async function sendDirPicker(bot, chatId, mode, title) {
|
|
@@ -87,6 +143,7 @@ function createFileBrowser(deps) {
|
|
|
87
143
|
const cmd = mode === 'new' ? '/new'
|
|
88
144
|
: mode === 'bind' ? '/agent-bind-dir'
|
|
89
145
|
: mode === 'agent-new' ? '/agent-dir'
|
|
146
|
+
: mode === 'team-new' ? '/agent-team-dir'
|
|
90
147
|
: '/cd';
|
|
91
148
|
|
|
92
149
|
const PAGE_SIZE = 10;
|
package/scripts/daemon-notify.js
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
function resolveAdminChatId(adapterConfig = {}) {
|
|
4
|
+
const explicitId = String(adapterConfig.admin_chat_id || '').trim();
|
|
5
|
+
if (explicitId) return explicitId;
|
|
6
|
+
const ids = Array.isArray(adapterConfig.allowed_chat_ids) ? adapterConfig.allowed_chat_ids : [];
|
|
7
|
+
return ids[0] || null;
|
|
8
|
+
}
|
|
9
|
+
|
|
3
10
|
function createNotifier(deps) {
|
|
4
11
|
const { log, getConfig, getBridges } = deps;
|
|
5
12
|
|
|
@@ -46,16 +53,23 @@ function createNotifier(deps) {
|
|
|
46
53
|
|
|
47
54
|
async function notifyAdmin(message) {
|
|
48
55
|
const config = getConfig();
|
|
49
|
-
const { feishuBridge } = getBridges();
|
|
56
|
+
const { feishuBridge, telegramBridge } = getBridges();
|
|
50
57
|
if (feishuBridge && feishuBridge.bot) {
|
|
51
|
-
const
|
|
52
|
-
const adminId = fsIds[0];
|
|
58
|
+
const adminId = resolveAdminChatId(config.feishu || {});
|
|
53
59
|
if (adminId) {
|
|
54
60
|
try { await feishuBridge.bot.sendMessage(adminId, message); } catch (e) {
|
|
55
61
|
log('ERROR', `Feishu admin notify failed ${adminId}: ${e.message}`);
|
|
56
62
|
}
|
|
57
63
|
}
|
|
58
64
|
}
|
|
65
|
+
if (telegramBridge && telegramBridge.bot) {
|
|
66
|
+
const adminId = resolveAdminChatId(config.telegram || {});
|
|
67
|
+
if (adminId) {
|
|
68
|
+
try { await telegramBridge.bot.sendMarkdown(adminId, message); } catch (e) {
|
|
69
|
+
log('ERROR', `Telegram admin notify failed ${adminId}: ${e.message}`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
59
73
|
}
|
|
60
74
|
|
|
61
75
|
/**
|
|
@@ -97,4 +111,4 @@ function createNotifier(deps) {
|
|
|
97
111
|
return { notify, notifyAdmin, notifyPersonal };
|
|
98
112
|
}
|
|
99
113
|
|
|
100
|
-
module.exports = { createNotifier };
|
|
114
|
+
module.exports = { createNotifier, resolveAdminChatId };
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
const { createCommandSessionResolver } = require('./daemon-command-session-route');
|
|
4
|
+
|
|
3
5
|
function createOpsCommandHandler(deps) {
|
|
4
6
|
const {
|
|
5
7
|
fs,
|
|
@@ -7,9 +9,12 @@ function createOpsCommandHandler(deps) {
|
|
|
7
9
|
spawn,
|
|
8
10
|
execSync,
|
|
9
11
|
log,
|
|
12
|
+
loadConfig,
|
|
13
|
+
loadState,
|
|
10
14
|
messageQueue,
|
|
11
15
|
activeProcesses,
|
|
12
16
|
getSession,
|
|
17
|
+
getSessionForEngine,
|
|
13
18
|
listCheckpoints,
|
|
14
19
|
cpDisplayLabel,
|
|
15
20
|
truncateSessionToCheckpoint,
|
|
@@ -20,7 +25,16 @@ function createOpsCommandHandler(deps) {
|
|
|
20
25
|
cleanupCheckpoints,
|
|
21
26
|
getNoSleepProcess,
|
|
22
27
|
setNoSleepProcess,
|
|
28
|
+
getDefaultEngine = () => 'claude',
|
|
23
29
|
} = deps;
|
|
30
|
+
const { getActiveSession } = createCommandSessionResolver({
|
|
31
|
+
path,
|
|
32
|
+
loadConfig,
|
|
33
|
+
loadState,
|
|
34
|
+
getSession,
|
|
35
|
+
getSessionForEngine,
|
|
36
|
+
getDefaultEngine,
|
|
37
|
+
});
|
|
24
38
|
|
|
25
39
|
function clearMessageQueue(chatId) {
|
|
26
40
|
if (messageQueue.has(chatId)) {
|
|
@@ -45,7 +59,7 @@ function createOpsCommandHandler(deps) {
|
|
|
45
59
|
clearMessageQueue(chatId);
|
|
46
60
|
interruptActiveProcess(chatId);
|
|
47
61
|
|
|
48
|
-
const session =
|
|
62
|
+
const { session } = getActiveSession(chatId);
|
|
49
63
|
if (!session || !session.id) {
|
|
50
64
|
await bot.sendMessage(chatId, 'No active session to undo.');
|
|
51
65
|
return true;
|
|
@@ -168,7 +182,7 @@ function createOpsCommandHandler(deps) {
|
|
|
168
182
|
clearMessageQueue(chatId);
|
|
169
183
|
interruptActiveProcess(chatId);
|
|
170
184
|
|
|
171
|
-
const session2 =
|
|
185
|
+
const { session: session2 } = getActiveSession(chatId);
|
|
172
186
|
if (!session2 || !session2.id) {
|
|
173
187
|
await bot.sendMessage(chatId, 'No active session.');
|
|
174
188
|
return true;
|
|
@@ -46,10 +46,58 @@ function decodePacket(text) {
|
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
function verifyPacket(packet, secret) {
|
|
49
|
-
if (!packet || typeof packet !== 'object' || !packet.sig) return false;
|
|
49
|
+
if (!packet || typeof packet !== 'object' || !packet.sig || !secret) return false;
|
|
50
50
|
return signPacket(packet, secret) === packet.sig;
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
+
function isValidPairCode(code) {
|
|
54
|
+
return /^\d{6}$/.test(String(code || '').trim());
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function generatePairCode() {
|
|
58
|
+
return String(Math.floor(Math.random() * 1000000)).padStart(6, '0');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function deriveSecretFromPairCode(code, chatId) {
|
|
62
|
+
const normalizedCode = String(code || '').trim();
|
|
63
|
+
const normalizedChatId = String(chatId || '').trim();
|
|
64
|
+
if (!isValidPairCode(normalizedCode) || !normalizedChatId) return null;
|
|
65
|
+
return crypto
|
|
66
|
+
.createHash('sha256')
|
|
67
|
+
.update(`metame-remote-dispatch|${normalizedChatId}|${normalizedCode}`)
|
|
68
|
+
.digest('hex');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function getRemoteDispatchStatus(config) {
|
|
72
|
+
const rd = normalizeRemoteDispatchConfig(config);
|
|
73
|
+
if (!rd) return null;
|
|
74
|
+
return {
|
|
75
|
+
selfPeer: rd.selfPeer,
|
|
76
|
+
chatId: rd.chatId,
|
|
77
|
+
mode: 'pair-code',
|
|
78
|
+
hasSecret: !!rd.secret,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// TTL dedup map — prevents replayed packets (5 min window)
|
|
83
|
+
const _seenPackets = new Map();
|
|
84
|
+
const DEDUP_TTL_MS = 5 * 60 * 1000;
|
|
85
|
+
|
|
86
|
+
function isDuplicate(packetId) {
|
|
87
|
+
if (!packetId) return false;
|
|
88
|
+
const now = Date.now();
|
|
89
|
+
for (const [id, ts] of _seenPackets) {
|
|
90
|
+
if (now - ts > DEDUP_TTL_MS) _seenPackets.delete(id);
|
|
91
|
+
}
|
|
92
|
+
if (_seenPackets.has(packetId)) return true;
|
|
93
|
+
_seenPackets.set(packetId, now);
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function isRemoteMember(member) {
|
|
98
|
+
return !!(member && member.peer);
|
|
99
|
+
}
|
|
100
|
+
|
|
53
101
|
module.exports = {
|
|
54
102
|
REMOTE_DISPATCH_PREFIX,
|
|
55
103
|
normalizeRemoteDispatchConfig,
|
|
@@ -57,4 +105,10 @@ module.exports = {
|
|
|
57
105
|
encodePacket,
|
|
58
106
|
decodePacket,
|
|
59
107
|
verifyPacket,
|
|
108
|
+
isValidPairCode,
|
|
109
|
+
generatePairCode,
|
|
110
|
+
deriveSecretFromPairCode,
|
|
111
|
+
getRemoteDispatchStatus,
|
|
112
|
+
isDuplicate,
|
|
113
|
+
isRemoteMember,
|
|
60
114
|
};
|
|
@@ -58,9 +58,92 @@ function setupRuntimeWatchers(deps) {
|
|
|
58
58
|
getHeartbeatTimer,
|
|
59
59
|
setHeartbeatTimer,
|
|
60
60
|
onRestartRequested,
|
|
61
|
+
// Agent soul layer auto-repair — optional, gracefully skipped if absent
|
|
62
|
+
repairAgentLayer,
|
|
63
|
+
writeConfigSafe,
|
|
64
|
+
expandPath,
|
|
65
|
+
HOME,
|
|
61
66
|
} = deps;
|
|
62
67
|
|
|
68
|
+
/**
|
|
69
|
+
* After every config reload: ensure all project agent soul layers are healthy.
|
|
70
|
+
*
|
|
71
|
+
* For each project:
|
|
72
|
+
* 1. If cwd changed vs oldConfig → remove stale SOUL.md/MEMORY.md symlinks from old dir
|
|
73
|
+
* 2. Call repairAgentLayer (idempotent)
|
|
74
|
+
* 3. Persist missing agent_id back to daemon.yaml
|
|
75
|
+
*/
|
|
76
|
+
function autoRepairAgentLayers(oldConfig, newConfig) {
|
|
77
|
+
if (typeof repairAgentLayer !== 'function') return;
|
|
78
|
+
const projects = newConfig && newConfig.projects;
|
|
79
|
+
if (!projects || typeof projects !== 'object') return;
|
|
80
|
+
|
|
81
|
+
const normCwd = (raw) => {
|
|
82
|
+
if (!raw) return null;
|
|
83
|
+
try {
|
|
84
|
+
const expanded = typeof expandPath === 'function'
|
|
85
|
+
? expandPath(String(raw))
|
|
86
|
+
: String(raw).replace(/^~/, HOME || require('os').homedir());
|
|
87
|
+
return path.resolve(expanded);
|
|
88
|
+
} catch { return null; }
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
let repaired = 0;
|
|
92
|
+
let agentIdFixed = 0;
|
|
93
|
+
let needsWrite = false;
|
|
94
|
+
|
|
95
|
+
for (const [projectKey, project] of Object.entries(projects)) {
|
|
96
|
+
if (!project || !project.cwd) continue;
|
|
97
|
+
const newCwd = normCwd(project.cwd);
|
|
98
|
+
if (!newCwd) continue;
|
|
99
|
+
|
|
100
|
+
// Clean stale symlinks when cwd changed
|
|
101
|
+
const oldProject = oldConfig && oldConfig.projects && oldConfig.projects[projectKey];
|
|
102
|
+
if (oldProject && oldProject.cwd) {
|
|
103
|
+
const oldCwd = normCwd(oldProject.cwd);
|
|
104
|
+
if (oldCwd && oldCwd !== newCwd) {
|
|
105
|
+
for (const fname of ['SOUL.md', 'MEMORY.md']) {
|
|
106
|
+
const stale = path.join(oldCwd, fname);
|
|
107
|
+
try {
|
|
108
|
+
if (fs.existsSync(stale) && fs.lstatSync(stale).isSymbolicLink()) {
|
|
109
|
+
fs.unlinkSync(stale);
|
|
110
|
+
log('INFO', `[agent-repair] Removed stale ${fname} from ${oldCwd}`);
|
|
111
|
+
}
|
|
112
|
+
} catch { /* non-critical */ }
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Repair soul layer (idempotent — safe every reload)
|
|
118
|
+
try {
|
|
119
|
+
const ensured = repairAgentLayer(projectKey, project, HOME);
|
|
120
|
+
if (ensured) {
|
|
121
|
+
repaired++;
|
|
122
|
+
if (!project.agent_id && ensured.agentId) {
|
|
123
|
+
newConfig.projects[projectKey] = { ...project, agent_id: ensured.agentId };
|
|
124
|
+
needsWrite = true;
|
|
125
|
+
agentIdFixed++;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
} catch (e) {
|
|
129
|
+
log('WARN', `[agent-repair] ${projectKey}: ${e.message}`);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (needsWrite && typeof writeConfigSafe === 'function') {
|
|
134
|
+
try {
|
|
135
|
+
writeConfigSafe(newConfig);
|
|
136
|
+
log('INFO', `[agent-repair] Persisted ${agentIdFixed} agent_id(s) to daemon.yaml`);
|
|
137
|
+
} catch (e) {
|
|
138
|
+
log('WARN', `[agent-repair] writeConfigSafe failed: ${e.message}`);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (repaired > 0) log('INFO', `[agent-repair] ${repaired} layer(s) ensured${agentIdFixed ? `, ${agentIdFixed} agent_id(s) added` : ''}`);
|
|
143
|
+
}
|
|
144
|
+
|
|
63
145
|
function reloadConfig() {
|
|
146
|
+
const oldConfig = typeof getConfig === 'function' ? getConfig() : null;
|
|
64
147
|
const strict = typeof loadConfigStrict === 'function'
|
|
65
148
|
? loadConfigStrict()
|
|
66
149
|
: { ok: true, config: loadConfig() };
|
|
@@ -74,6 +157,10 @@ function setupRuntimeWatchers(deps) {
|
|
|
74
157
|
const { general, project } = getAllTasks(newConfig);
|
|
75
158
|
const totalCount = general.length + project.length;
|
|
76
159
|
log('INFO', `Config reloaded: ${totalCount} tasks (${project.length} in projects)`);
|
|
160
|
+
// Auto-repair agent soul layers on every config change (idempotent, fire-and-forget)
|
|
161
|
+
try { autoRepairAgentLayers(oldConfig, newConfig); } catch (e) {
|
|
162
|
+
log('WARN', `[agent-repair] Unexpected error: ${e.message}`);
|
|
163
|
+
}
|
|
77
164
|
return { success: true, tasks: totalCount };
|
|
78
165
|
}
|
|
79
166
|
|