metame-cli 1.5.3 → 1.5.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/README.md +60 -18
  2. package/index.js +352 -79
  3. package/package.json +2 -2
  4. package/scripts/agent-layer.js +4 -2
  5. package/scripts/bin/dispatch_to +178 -90
  6. package/scripts/daemon-admin-commands.js +353 -105
  7. package/scripts/daemon-agent-commands.js +434 -66
  8. package/scripts/daemon-bridges.js +477 -68
  9. package/scripts/daemon-claude-engine.js +1267 -674
  10. package/scripts/daemon-command-router.js +205 -27
  11. package/scripts/daemon-command-session-route.js +118 -0
  12. package/scripts/daemon-default.yaml +7 -0
  13. package/scripts/daemon-engine-runtime.js +96 -20
  14. package/scripts/daemon-exec-commands.js +108 -49
  15. package/scripts/daemon-file-browser.js +64 -7
  16. package/scripts/daemon-notify.js +18 -4
  17. package/scripts/daemon-ops-commands.js +16 -2
  18. package/scripts/daemon-remote-dispatch.js +55 -1
  19. package/scripts/daemon-runtime-lifecycle.js +87 -0
  20. package/scripts/daemon-session-commands.js +102 -45
  21. package/scripts/daemon-session-store.js +497 -66
  22. package/scripts/daemon-siri-bridge.js +234 -0
  23. package/scripts/daemon-siri-imessage.js +209 -0
  24. package/scripts/daemon-task-scheduler.js +10 -2
  25. package/scripts/daemon.js +697 -179
  26. package/scripts/daemon.yaml +7 -0
  27. package/scripts/docs/agent-guide.md +36 -3
  28. package/scripts/docs/hook-config.md +134 -0
  29. package/scripts/docs/maintenance-manual.md +162 -5
  30. package/scripts/docs/pointer-map.md +60 -5
  31. package/scripts/feishu-adapter.js +7 -15
  32. package/scripts/hooks/doc-router.js +29 -0
  33. package/scripts/hooks/hook-utils.js +61 -0
  34. package/scripts/hooks/intent-doc-router.js +54 -0
  35. package/scripts/hooks/intent-engine.js +72 -0
  36. package/scripts/hooks/intent-file-transfer.js +51 -0
  37. package/scripts/hooks/intent-memory-recall.js +35 -0
  38. package/scripts/hooks/intent-ops-assist.js +54 -0
  39. package/scripts/hooks/intent-task-create.js +35 -0
  40. package/scripts/hooks/intent-team-dispatch.js +106 -0
  41. package/scripts/hooks/team-context.js +143 -0
  42. package/scripts/intent-registry.js +59 -0
  43. package/scripts/memory-extract.js +59 -0
  44. package/scripts/memory-nightly-reflect.js +109 -43
  45. package/scripts/memory.js +55 -17
  46. package/scripts/mentor-engine.js +6 -0
  47. package/scripts/schema.js +1 -0
  48. package/scripts/self-reflect.js +110 -12
  49. package/scripts/session-analytics.js +160 -0
  50. package/scripts/signal-capture.js +1 -1
  51. package/scripts/team-dispatch.js +315 -0
@@ -14,7 +14,6 @@ function createSessionCommandHandler(deps) {
14
14
  sendBrowse,
15
15
  sendDirPicker,
16
16
  createSession,
17
- getSessionForEngine,
18
17
  getCachedFile,
19
18
  getSession,
20
19
  listRecentSessions,
@@ -26,7 +25,6 @@ function createSessionCommandHandler(deps) {
26
25
  loadSessionTags,
27
26
  sessionRichLabel,
28
27
  buildSessionCardElements,
29
- sessionLabel,
30
28
  getDefaultEngine = () => 'claude',
31
29
  } = deps;
32
30
 
@@ -35,23 +33,71 @@ function createSessionCommandHandler(deps) {
35
33
  return n === 'codex' ? 'codex' : getDefaultEngine();
36
34
  }
37
35
 
36
+ function inferStoredEngine(rawSession) {
37
+ if (!rawSession || typeof rawSession !== 'object') return getDefaultEngine();
38
+ if (rawSession.engine) return normalizeEngineName(rawSession.engine);
39
+ const slots = rawSession.engines && typeof rawSession.engines === 'object' ? rawSession.engines : null;
40
+ if (!slots) return getDefaultEngine();
41
+ const started = Object.entries(slots).find(([, slot]) => slot && slot.started);
42
+ if (started) return normalizeEngineName(started[0]);
43
+ const available = Object.keys(slots);
44
+ return available.length === 1 ? normalizeEngineName(available[0]) : getDefaultEngine();
45
+ }
46
+
47
+ function buildBoundSessionChatId(projectKey) {
48
+ const key = String(projectKey || '').trim();
49
+ return key ? `_bound_${key}` : '';
50
+ }
51
+
52
+ function getSessionRoute(chatId) {
53
+ const cfg = loadConfig();
54
+ const state = loadState();
55
+ const chatKey = String(chatId);
56
+ const agentMap = { ...(cfg.telegram ? cfg.telegram.chat_agent_map : {}), ...(cfg.feishu ? cfg.feishu.chat_agent_map : {}) };
57
+ const boundKey = agentMap[chatKey] || null;
58
+ const boundProj = boundKey && cfg.projects ? cfg.projects[boundKey] : null;
59
+ const stickyKey = state && state.team_sticky ? state.team_sticky[chatKey] : null;
60
+ const stickyMember = stickyKey && boundProj && Array.isArray(boundProj.team)
61
+ ? boundProj.team.find((m) => m && m.key === stickyKey)
62
+ : null;
63
+
64
+ if (stickyMember) {
65
+ return {
66
+ sessionChatId: `_agent_${stickyMember.key}`,
67
+ cwd: stickyMember.cwd ? normalizeCwd(stickyMember.cwd) : (boundProj && boundProj.cwd ? normalizeCwd(boundProj.cwd) : null),
68
+ engine: normalizeEngineName(stickyMember.engine || (boundProj && boundProj.engine)),
69
+ };
70
+ }
71
+
72
+ if (boundProj) {
73
+ return {
74
+ sessionChatId: buildBoundSessionChatId(boundKey),
75
+ cwd: boundProj.cwd ? normalizeCwd(boundProj.cwd) : null,
76
+ engine: normalizeEngineName(boundProj.engine),
77
+ };
78
+ }
79
+
80
+ const rawSession = getSession(chatId);
81
+ return {
82
+ sessionChatId: String(chatId),
83
+ cwd: rawSession && rawSession.cwd ? normalizeCwd(rawSession.cwd) : null,
84
+ engine: inferStoredEngine(rawSession),
85
+ };
86
+ }
87
+
88
+ function getCurrentEngine(chatId) {
89
+ return getSessionRoute(chatId).engine;
90
+ }
91
+
38
92
  function getBoundCwd(chatId) {
39
93
  try {
40
- const cfg = loadConfig();
41
- const chatAgentMap = { ...(cfg.telegram ? cfg.telegram.chat_agent_map : {}), ...(cfg.feishu ? cfg.feishu.chat_agent_map : {}) };
42
- const mappedKey = chatAgentMap[String(chatId)];
43
- const proj = mappedKey && cfg.projects ? cfg.projects[mappedKey] : null;
44
- return (proj && proj.cwd) ? normalizeCwd(proj.cwd) : null;
94
+ return getSessionRoute(chatId).cwd;
45
95
  } catch { return null; }
46
96
  }
47
97
 
48
98
  // Write per-engine session slot, preserving cwd and other engine slots.
49
99
  function attachEngineSession(state, chatId, engine, sessionId, cwd) {
50
- // For bound chats, resolve to virtual chatId so askClaude picks up the session
51
- const cfg = loadConfig();
52
- const agentMap = { ...(cfg.telegram ? cfg.telegram.chat_agent_map : {}), ...(cfg.feishu ? cfg.feishu.chat_agent_map : {}) };
53
- const boundKey = agentMap[String(chatId)];
54
- const effectiveId = boundKey ? `_agent_${boundKey}` : String(chatId);
100
+ const effectiveId = getSessionRoute(chatId).sessionChatId;
55
101
  const existing = state.sessions[effectiveId] || {};
56
102
  const existingEngines = existing.engines || {};
57
103
  state.sessions[effectiveId] = {
@@ -71,6 +117,18 @@ function createSessionCommandHandler(deps) {
71
117
  return null;
72
118
  }
73
119
 
120
+ async function autoCreateSessionWhenEmpty(bot, chatId, cwd, engine) {
121
+ const resolvedCwd = cwd ? normalizeCwd(cwd) : null;
122
+ if (!resolvedCwd || !fs.existsSync(resolvedCwd)) {
123
+ await bot.sendMessage(chatId, `No sessions found${resolvedCwd ? ' in ' + path.basename(resolvedCwd) : ''}. Try /new first.`);
124
+ return true;
125
+ }
126
+ const route = getSessionRoute(chatId);
127
+ const session = createSession(route.sessionChatId, resolvedCwd, '', engine || getDefaultEngine());
128
+ await bot.sendMessage(chatId, `📁 ${path.basename(resolvedCwd)}\n✅ 已自动创建新会话\nWorkdir: ${session.cwd}`);
129
+ return true;
130
+ }
131
+
74
132
  async function handleSessionCommand(ctx) {
75
133
  const { bot, chatId, text } = ctx;
76
134
 
@@ -98,13 +156,9 @@ function createSessionCommandHandler(deps) {
98
156
  const arg = text.slice(4).trim();
99
157
  if (!arg) {
100
158
  // In a dedicated agent group, use the agent's bound cwd directly
101
- const newCfg = loadConfig();
102
- const agentMap = { ...(newCfg.telegram ? newCfg.telegram.chat_agent_map : {}), ...(newCfg.feishu ? newCfg.feishu.chat_agent_map : {}) };
103
- const boundKey = agentMap[String(chatId)];
104
- const boundProj = boundKey && newCfg.projects && newCfg.projects[boundKey];
105
- if (boundProj && boundProj.cwd) {
106
- const boundCwd = normalizeCwd(boundProj.cwd);
107
- const session = createSession(chatId, boundCwd, '', normalizeEngineName(boundProj.engine));
159
+ const route = getSessionRoute(chatId);
160
+ if (route.cwd) {
161
+ const session = createSession(route.sessionChatId, route.cwd, '', route.engine);
108
162
  await bot.sendMessage(chatId, `✅ 新会话已创建\nWorkdir: ${session.cwd}`);
109
163
  return true;
110
164
  }
@@ -136,7 +190,7 @@ function createSessionCommandHandler(deps) {
136
190
  const mappedProjForEngine = mappedKeyForEngine && cfgForEngine.projects ? cfgForEngine.projects[mappedKeyForEngine] : null;
137
191
  const currentEngine = getDefaultEngine();
138
192
  const sessionEngine = normalizeEngineName((mappedProjForEngine && mappedProjForEngine.engine) || currentEngine);
139
- const session = createSession(chatId, dirPath, sessionName || '', sessionEngine);
193
+ const session = createSession(getSessionRoute(chatId).sessionChatId, dirPath, sessionName || '', sessionEngine);
140
194
  const label = sessionName ? `[${sessionName}]` : '';
141
195
  await bot.sendMessage(chatId, `New session ${label}\nWorkdir: ${session.cwd}`);
142
196
  return true;
@@ -174,17 +228,19 @@ function createSessionCommandHandler(deps) {
174
228
 
175
229
  // /last — smart resume: prefer current cwd, then most recent globally
176
230
  if (text === '/last') {
177
- const curSession = getSession(chatId);
231
+ const currentEngine = getCurrentEngine(chatId);
232
+ const route = getSessionRoute(chatId);
233
+ const curSession = getSession(route.sessionChatId) || getSession(chatId);
178
234
  const curCwd = curSession ? curSession.cwd : null;
179
235
 
180
236
  // Strategy: try current cwd first, then fall back to global
181
237
  let s = null;
182
238
  if (curCwd) {
183
- const cwdSessions = listRecentSessions(1, curCwd);
239
+ const cwdSessions = listRecentSessions(1, curCwd, currentEngine);
184
240
  if (cwdSessions.length > 0) s = cwdSessions[0];
185
241
  }
186
242
  if (!s) {
187
- const globalSessions = listRecentSessions(1);
243
+ const globalSessions = listRecentSessions(1, null, currentEngine);
188
244
  if (globalSessions.length > 0) s = globalSessions[0];
189
245
  }
190
246
 
@@ -192,7 +248,7 @@ function createSessionCommandHandler(deps) {
192
248
  // Last resort: use __continue__ to resume whatever Claude thinks is last
193
249
  const state2 = loadState();
194
250
  const cfgForEngine = loadConfig();
195
- const engineByCwd = inferEngineByCwd(cfgForEngine, curCwd || HOME) || getDefaultEngine();
251
+ const engineByCwd = inferEngineByCwd(cfgForEngine, curCwd || HOME) || currentEngine;
196
252
  attachEngineSession(state2, chatId, engineByCwd, '__continue__', curCwd || HOME);
197
253
  saveState(state2);
198
254
  await bot.sendMessage(chatId, `⚡ Resuming last session in ${path.basename(curCwd || HOME)}`);
@@ -201,7 +257,7 @@ function createSessionCommandHandler(deps) {
201
257
 
202
258
  const state2 = loadState();
203
259
  const cfgForEngine = loadConfig();
204
- const engineByCwd = inferEngineByCwd(cfgForEngine, s.projectPath || HOME) || getDefaultEngine();
260
+ const engineByCwd = normalizeEngineName(s.engine) || inferEngineByCwd(cfgForEngine, s.projectPath || HOME) || getDefaultEngine();
205
261
  attachEngineSession(state2, chatId, engineByCwd, s.sessionId, s.projectPath || HOME);
206
262
  saveState(state2);
207
263
  // Display: name/summary + id on separate lines
@@ -267,13 +323,14 @@ function createSessionCommandHandler(deps) {
267
323
 
268
324
  // /sessions — compact list, tap to see details, then tap to switch
269
325
  if (text === '/sessions') {
270
- const currentEngine = getDefaultEngine();
271
- const codexLimitTip = '⚠️ 当前为 Codex 会话:`/sessions` 列表暂仅展示 Claude 本地会话,Codex 会话暂不可见。';
272
- const allSessions = listRecentSessions(15, getBoundCwd(chatId));
326
+ const allSessions = listRecentSessions(15, getBoundCwd(chatId), getCurrentEngine(chatId));
273
327
  if (allSessions.length === 0) {
274
- const base = 'No sessions found. Try /new first.';
275
- await bot.sendMessage(chatId, currentEngine === 'codex' ? `${base}\n\n${codexLimitTip}` : base);
276
- return true;
328
+ return autoCreateSessionWhenEmpty(
329
+ bot,
330
+ chatId,
331
+ getBoundCwd(chatId) || (getSession(chatId) && getSession(chatId).cwd),
332
+ getCurrentEngine(chatId)
333
+ );
277
334
  }
278
335
  if (bot.sendButtons) {
279
336
  await bot.sendRawCard(chatId, '📋 Recent Sessions', buildSessionCardElements(allSessions));
@@ -283,19 +340,15 @@ function createSessionCommandHandler(deps) {
283
340
  allSessions.forEach((s, i) => {
284
341
  msg += sessionRichLabel(s, i + 1, _tags1) + '\n';
285
342
  });
286
- if (currentEngine === 'codex') msg += `\n${codexLimitTip}\n`;
287
343
  await bot.sendMessage(chatId, msg);
288
344
  }
289
- if (bot.sendButtons && currentEngine === 'codex') {
290
- await bot.sendMessage(chatId, codexLimitTip);
291
- }
292
345
  return true;
293
346
  }
294
347
 
295
348
  // /sess <id> — show session detail card with switch button
296
349
  if (text.startsWith('/sess ')) {
297
350
  const sid = text.slice(6).trim();
298
- const allSessions = listRecentSessions(50);
351
+ const allSessions = listRecentSessions(50, null, getCurrentEngine(chatId));
299
352
  const s = allSessions.find(x => x.sessionId === sid || x.sessionId.startsWith(sid));
300
353
  if (!s) {
301
354
  await bot.sendMessage(chatId, `Session not found: ${sid.slice(0, 8)}`);
@@ -373,9 +426,10 @@ function createSessionCommandHandler(deps) {
373
426
  }
374
427
  // /cd last — sync to computer: switch to most recent session AND its directory
375
428
  if (newCwd === 'last') {
376
- const currentSession = getSession(chatId);
429
+ const route = getSessionRoute(chatId);
430
+ const currentSession = getSession(route.sessionChatId) || getSession(chatId);
377
431
  const excludeId = currentSession?.id;
378
- const recent = listRecentSessions(10);
432
+ const recent = listRecentSessions(10, null, getCurrentEngine(chatId));
379
433
  const filtered = excludeId ? recent.filter(s => s.sessionId !== excludeId) : recent;
380
434
 
381
435
  // For bound chats, prefer sessions from the same project to avoid
@@ -416,7 +470,7 @@ function createSessionCommandHandler(deps) {
416
470
  }
417
471
  const state2 = loadState();
418
472
  // Try to find existing session in this directory
419
- const recentInDir = listRecentSessions(1, newCwd);
473
+ const recentInDir = listRecentSessions(1, newCwd, getCurrentEngine(chatId));
420
474
  if (recentInDir.length > 0 && recentInDir[0].sessionId) {
421
475
  // Attach to existing session in this directory
422
476
  const target = recentInDir[0];
@@ -426,14 +480,14 @@ function createSessionCommandHandler(deps) {
426
480
  saveState(state2);
427
481
  const label = target.customTitle || target.summary?.slice(0, 30) || target.sessionId.slice(0, 8);
428
482
  await bot.sendMessage(chatId, `📁 ${path.basename(newCwd)}\n🔄 Attached: ${label}`);
429
- } else if (!state2.sessions[chatId]) {
483
+ } else if (!state2.sessions[getSessionRoute(chatId).sessionChatId]) {
430
484
  const cfgForEngine = loadConfig();
431
485
  const engineByCwd = inferEngineByCwd(cfgForEngine, newCwd);
432
486
  const currentEngine = getDefaultEngine();
433
- createSession(chatId, newCwd, '', engineByCwd || currentEngine);
487
+ createSession(getSessionRoute(chatId).sessionChatId, newCwd, '', engineByCwd || currentEngine);
434
488
  await bot.sendMessage(chatId, `📁 ${path.basename(newCwd)} (new session)`);
435
489
  } else {
436
- state2.sessions[chatId].cwd = newCwd;
490
+ state2.sessions[getSessionRoute(chatId).sessionChatId].cwd = newCwd;
437
491
  saveState(state2);
438
492
  await bot.sendMessage(chatId, `📁 ${path.basename(newCwd)}`);
439
493
  }
@@ -443,7 +497,8 @@ function createSessionCommandHandler(deps) {
443
497
 
444
498
  // /list [subdir|glob|fullpath] — list files (zero token, daemon-only)
445
499
  if (text === '/list' || text.startsWith('/list ')) {
446
- const session = getSession(chatId);
500
+ const route = getSessionRoute(chatId);
501
+ const session = getSession(route.sessionChatId) || getSession(chatId);
447
502
  const cwd = session?.cwd || HOME;
448
503
  const arg = text.slice(5).trim();
449
504
  // If arg is an absolute or ~ path, list that directly
@@ -466,7 +521,8 @@ function createSessionCommandHandler(deps) {
466
521
  await bot.sendMessage(chatId, 'Usage: /name <session name>');
467
522
  return true;
468
523
  }
469
- const session = getSession(chatId);
524
+ const route = getSessionRoute(chatId);
525
+ const session = getSession(route.sessionChatId) || getSession(chatId);
470
526
  if (!session) {
471
527
  await bot.sendMessage(chatId, 'No active session. Start one first.');
472
528
  return true;
@@ -482,7 +538,8 @@ function createSessionCommandHandler(deps) {
482
538
  }
483
539
 
484
540
  if (text === '/session') {
485
- const session = getSession(chatId);
541
+ const route = getSessionRoute(chatId);
542
+ const session = getSession(route.sessionChatId) || getSession(chatId);
486
543
  if (!session) {
487
544
  await bot.sendMessage(chatId, 'No active session. Send any message to start one.');
488
545
  } else {