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.
Files changed (40) hide show
  1. package/README.md +6 -1
  2. package/index.js +277 -55
  3. package/package.json +2 -2
  4. package/scripts/agent-layer.js +4 -2
  5. package/scripts/bin/dispatch_to +17 -5
  6. package/scripts/daemon-admin-commands.js +264 -62
  7. package/scripts/daemon-agent-commands.js +188 -66
  8. package/scripts/daemon-bridges.js +447 -48
  9. package/scripts/daemon-claude-engine.js +650 -103
  10. package/scripts/daemon-command-router.js +134 -27
  11. package/scripts/daemon-command-session-route.js +118 -0
  12. package/scripts/daemon-default.yaml +2 -0
  13. package/scripts/daemon-engine-runtime.js +96 -20
  14. package/scripts/daemon-exec-commands.js +106 -50
  15. package/scripts/daemon-file-browser.js +63 -7
  16. package/scripts/daemon-notify.js +18 -4
  17. package/scripts/daemon-ops-commands.js +16 -2
  18. package/scripts/daemon-remote-dispatch.js +34 -2
  19. package/scripts/daemon-session-commands.js +102 -45
  20. package/scripts/daemon-session-store.js +497 -66
  21. package/scripts/daemon-siri-bridge.js +234 -0
  22. package/scripts/daemon-siri-imessage.js +209 -0
  23. package/scripts/daemon-task-scheduler.js +10 -2
  24. package/scripts/daemon.js +610 -181
  25. package/scripts/docs/hook-config.md +7 -4
  26. package/scripts/docs/maintenance-manual.md +8 -1
  27. package/scripts/feishu-adapter.js +7 -15
  28. package/scripts/hooks/doc-router.js +29 -0
  29. package/scripts/hooks/intent-doc-router.js +54 -0
  30. package/scripts/hooks/intent-engine.js +9 -40
  31. package/scripts/intent-registry.js +59 -0
  32. package/scripts/memory-extract.js +59 -0
  33. package/scripts/mentor-engine.js +6 -0
  34. package/scripts/schema.js +1 -0
  35. package/scripts/self-reflect.js +110 -12
  36. package/scripts/session-analytics.js +160 -0
  37. package/scripts/signal-capture.js +1 -1
  38. package/scripts/team-dispatch.js +150 -11
  39. package/scripts/hooks/intent-agent-manage.js +0 -50
  40. package/scripts/hooks/intent-hook-config.js +0 -28
@@ -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 {
@@ -24,9 +25,19 @@ function createExecCommandHandler(deps) {
24
25
  getSessionName,
25
26
  createSession,
26
27
  findSessionFile,
28
+ findCodexSessionFile = null,
27
29
  loadConfig,
28
30
  getDistillModel,
31
+ getDefaultEngine = () => 'claude',
29
32
  } = deps;
33
+ const { getActiveSession } = createCommandSessionResolver({
34
+ path,
35
+ loadConfig,
36
+ loadState,
37
+ getSession,
38
+ getSessionForEngine,
39
+ getDefaultEngine,
40
+ });
30
41
 
31
42
  function truncateOutput(output, maxLen = 4000) {
32
43
  const text = (output || '').trim() || '(no output)';
@@ -235,7 +246,7 @@ function createExecCommandHandler(deps) {
235
246
  const signal = proc.killSignal || 'SIGTERM';
236
247
  try { process.kill(-proc.child.pid, signal); } catch { try { proc.child.kill(signal); } catch { /* */ } }
237
248
  }
238
- const session = getSession(chatId);
249
+ const { session } = getActiveSession(chatId);
239
250
  const name = session ? getSessionName(session.id) : null;
240
251
  const label = name || (session ? session.id.slice(0, 8) : 'none');
241
252
  await bot.sendMessage(chatId, `🔄 Session restarted. MCP/config reloaded.\n📁 ${session ? path.basename(session.cwd) : '~'} [${label}]`);
@@ -244,52 +255,84 @@ function createExecCommandHandler(deps) {
244
255
 
245
256
  // /compact — compress current session context to save tokens
246
257
  if (text === '/compact') {
247
- const rawSession = getSession(chatId);
248
- const engine = rawSession?.engine || 'claude';
249
- const session = getSessionForEngine(chatId, engine);
258
+ const { sessionKey, engine, session } = getActiveSession(chatId);
250
259
  if (!session || !session.id || !session.started) {
251
260
  await bot.sendMessage(chatId, '❌ No active session to compact.');
252
261
  return true;
253
262
  }
254
- if (String(engine).toLowerCase() === 'codex') {
255
- await bot.sendMessage(chatId, '⚠️ Codex 会话暂不支持 /compact,请继续在同一会话里对话。');
256
- return true;
257
- }
258
263
  await bot.sendMessage(chatId, '🗜 Compacting session...');
259
264
 
260
- // Step 1: Read conversation from JSONL (fast, no Claude needed)
261
- const jsonlPath = findSessionFile(session.id);
262
- if (!jsonlPath) {
263
- await bot.sendMessage(chatId, '❌ Session file not found.');
264
- return true;
265
- }
265
+ // Step 1: Read conversation (JSONL for Claude, rollout JSONL for Codex)
266
+ const isCodex = String(engine).toLowerCase() === 'codex';
266
267
  const messages = [];
267
- try {
268
- const lines = fs.readFileSync(jsonlPath, 'utf8').split('\n').filter(Boolean);
269
- for (const line of lines) {
270
- try {
271
- const obj = JSON.parse(line);
272
- if (obj.type === 'user' || obj.type === 'assistant') {
273
- const msg = obj.message || {};
274
- const content = msg.content;
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;
275
285
  let textContent = '';
276
286
  if (typeof content === 'string') {
277
287
  textContent = content;
278
288
  } else if (Array.isArray(content)) {
279
- textContent = content
280
- .filter(c => c.type === 'text')
281
- .map(c => c.text || '')
282
- .join(' ');
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(' ');
283
294
  }
284
- if (textContent.trim()) {
285
- messages.push({ role: obj.type, text: textContent.trim() });
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
+ }
286
329
  }
287
- }
288
- } catch { /* skip malformed lines */ }
330
+ } catch { /* skip malformed lines */ }
331
+ }
332
+ } catch (e) {
333
+ await bot.sendMessage(chatId, `❌ Cannot read session: ${e.message}`);
334
+ return true;
289
335
  }
290
- } catch (e) {
291
- await bot.sendMessage(chatId, `❌ Cannot read session: ${e.message}`);
292
- return true;
293
336
  }
294
337
 
295
338
  if (messages.length === 0) {
@@ -325,23 +368,36 @@ function createExecCommandHandler(deps) {
325
368
  }
326
369
 
327
370
  // Step 4: Create new session with the summary
328
- const model = daemonCfg.model || 'opus';
329
371
  const oldName = getSessionName(session.id);
330
- const newSession = createSession(chatId, session.cwd, oldName ? oldName + ' (compacted)' : '', session.engine || 'claude');
331
- const initArgs = ['-p', '--session-id', newSession.id, '--model', model];
332
- if (daemonCfg.dangerously_skip_permissions) initArgs.push('--dangerously-skip-permissions');
333
- const preamble = buildProfilePreamble();
334
- const initPrompt = preamble + `Here is the context from our previous session (compacted):\n\n${output}\n\nContext loaded. Ready to continue.`;
335
- const { error: initErr } = await spawnClaudeAsync(initArgs, initPrompt, session.cwd, 60000);
336
- if (initErr) {
337
- await bot.sendMessage(chatId, `⚠️ Summary saved but new session init failed: ${initErr}`);
338
- return true;
339
- }
340
- // Mark as started
341
- const state2 = loadState();
342
- if (state2.sessions[chatId]) {
343
- 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;
344
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
+ }
345
401
  }
346
402
  const tokenEst = Math.round(output.length / 3.5);
347
403
  await bot.sendMessage(chatId, `✅ Compacted! ~${tokenEst} tokens of context carried over.\nNew session: ${newSession.id.slice(0, 8)}`);
@@ -355,8 +411,8 @@ function createExecCommandHandler(deps) {
355
411
  await bot.sendMessage(chatId, '用法: /publish 123456');
356
412
  return true;
357
413
  }
358
- const session = getSession(chatId);
359
- const cwd = session?.cwd || HOME;
414
+ const { route, session } = getActiveSession(chatId);
415
+ const cwd = session?.cwd || route.cwd || HOME;
360
416
  await bot.sendMessage(chatId, `📦 npm publish --otp=${otp} ...`);
361
417
  try {
362
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 (!bot.sendButtons || files.size === 0) return;
68
+ if (files.size === 0) return;
63
69
  const validFiles = [...files].filter(f => fs.existsSync(f));
64
- if (validFiles.length === 0) return;
65
- const buttons = validFiles.map(filePath => {
66
- const shortId = cacheFile(filePath);
67
- return [{ text: `📎 ${path.basename(filePath)}`, callback_data: `/file ${shortId}` }];
68
- });
69
- await bot.sendButtons(chatId, '📂 文件:', buttons);
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) {
@@ -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 fsIds = (config.feishu && config.feishu.allowed_chat_ids) || [];
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 = getSession(chatId);
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 = getSession(chatId);
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,39 @@ 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
+
53
82
  // TTL dedup map — prevents replayed packets (5 min window)
54
83
  const _seenPackets = new Map();
55
84
  const DEDUP_TTL_MS = 5 * 60 * 1000;
@@ -57,7 +86,6 @@ const DEDUP_TTL_MS = 5 * 60 * 1000;
57
86
  function isDuplicate(packetId) {
58
87
  if (!packetId) return false;
59
88
  const now = Date.now();
60
- // Evict expired entries on each check
61
89
  for (const [id, ts] of _seenPackets) {
62
90
  if (now - ts > DEDUP_TTL_MS) _seenPackets.delete(id);
63
91
  }
@@ -77,6 +105,10 @@ module.exports = {
77
105
  encodePacket,
78
106
  decodePacket,
79
107
  verifyPacket,
108
+ isValidPairCode,
109
+ generatePairCode,
110
+ deriveSecretFromPairCode,
111
+ getRemoteDispatchStatus,
80
112
  isDuplicate,
81
113
  isRemoteMember,
82
114
  };