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
@@ -13,6 +13,7 @@ function createAgentCommandHandler(deps) {
13
13
  sendBrowse,
14
14
  sendDirPicker,
15
15
  getSession,
16
+ getSessionForEngine,
16
17
  listRecentSessions,
17
18
  buildSessionCardElements,
18
19
  sessionLabel,
@@ -40,6 +41,103 @@ function createAgentCommandHandler(deps) {
40
41
  return n === 'codex' ? 'codex' : getDefaultEngine();
41
42
  }
42
43
 
44
+ function inferStoredEngine(rawSession) {
45
+ if (!rawSession || typeof rawSession !== 'object') return getDefaultEngine();
46
+ if (rawSession.engine) return normalizeEngineName(rawSession.engine);
47
+ const slots = rawSession.engines && typeof rawSession.engines === 'object' ? rawSession.engines : null;
48
+ if (!slots) return getDefaultEngine();
49
+ const started = Object.entries(slots).find(([, slot]) => slot && slot.started);
50
+ if (started) return normalizeEngineName(started[0]);
51
+ const available = Object.keys(slots);
52
+ return available.length === 1 ? normalizeEngineName(available[0]) : getDefaultEngine();
53
+ }
54
+
55
+ function buildBoundSessionChatId(projectKey) {
56
+ const key = String(projectKey || '').trim();
57
+ return key ? `_bound_${key}` : '';
58
+ }
59
+
60
+ function getSessionRoute(chatId) {
61
+ const cfg = loadConfig();
62
+ const state = loadState();
63
+ const chatKey = String(chatId);
64
+ const agentMap = { ...(cfg.telegram ? cfg.telegram.chat_agent_map : {}), ...(cfg.feishu ? cfg.feishu.chat_agent_map : {}) };
65
+ const boundKey = agentMap[chatKey] || null;
66
+ const boundProj = boundKey && cfg.projects ? cfg.projects[boundKey] : null;
67
+ const stickyKey = state && state.team_sticky ? state.team_sticky[chatKey] : null;
68
+ const stickyMember = stickyKey && boundProj && Array.isArray(boundProj.team)
69
+ ? boundProj.team.find((m) => m && m.key === stickyKey)
70
+ : null;
71
+
72
+ if (stickyMember) {
73
+ return {
74
+ sessionChatId: `_agent_${stickyMember.key}`,
75
+ cwd: stickyMember.cwd ? normalizeCwd(stickyMember.cwd) : (boundProj && boundProj.cwd ? normalizeCwd(boundProj.cwd) : null),
76
+ engine: normalizeEngineName(stickyMember.engine || (boundProj && boundProj.engine)),
77
+ };
78
+ }
79
+
80
+ if (boundProj) {
81
+ return {
82
+ sessionChatId: buildBoundSessionChatId(boundKey),
83
+ cwd: boundProj.cwd ? normalizeCwd(boundProj.cwd) : null,
84
+ engine: normalizeEngineName(boundProj.engine),
85
+ };
86
+ }
87
+
88
+ const rawSession = getSession(chatId);
89
+ return {
90
+ sessionChatId: String(chatId),
91
+ cwd: rawSession && rawSession.cwd ? normalizeCwd(rawSession.cwd) : null,
92
+ engine: inferStoredEngine(rawSession),
93
+ };
94
+ }
95
+
96
+ function getCurrentEngine(chatId) {
97
+ return getSessionRoute(chatId).engine;
98
+ }
99
+
100
+ function getLogicalSessionForRoute(route) {
101
+ if (!route || !route.sessionChatId) return null;
102
+ if (typeof getSessionForEngine === 'function') {
103
+ const engineSession = getSessionForEngine(route.sessionChatId, route.engine);
104
+ if (engineSession && engineSession.id) return engineSession;
105
+ }
106
+ const raw = getSession(route.sessionChatId);
107
+ if (!raw) return null;
108
+ const slot = raw.engines && raw.engines[route.engine];
109
+ if (slot && slot.id) return { cwd: raw.cwd, engine: route.engine, ...slot };
110
+ if (raw.id) return { cwd: raw.cwd, engine: route.engine, id: raw.id, started: !!raw.started };
111
+ return null;
112
+ }
113
+
114
+ function buildResumeChoices({ recentSessions, currentLogical, curCwd, currentEngine, isLogicalRoute }) {
115
+ const items = [];
116
+ const seen = new Set();
117
+ if (
118
+ isLogicalRoute
119
+ && currentLogical
120
+ && currentLogical.id
121
+ && currentLogical.started
122
+ ) {
123
+ items.push({
124
+ sessionId: currentLogical.id,
125
+ projectPath: currentLogical.cwd || curCwd || HOME,
126
+ engine: currentEngine,
127
+ customTitle: '当前会话',
128
+ summary: '优先续接当前智能体会话',
129
+ });
130
+ seen.add(String(currentLogical.id));
131
+ }
132
+ for (const session of recentSessions || []) {
133
+ const key = String(session && session.sessionId || '');
134
+ if (!key || seen.has(key)) continue;
135
+ items.push(session);
136
+ seen.add(key);
137
+ }
138
+ return items;
139
+ }
140
+
43
141
  function inferEngineByCwd(cfg, cwd) {
44
142
  if (!cfg || !cfg.projects || !cwd) return null;
45
143
  const targetCwd = normalizeCwd(cwd);
@@ -52,18 +150,20 @@ function createAgentCommandHandler(deps) {
52
150
  return null;
53
151
  }
54
152
 
153
+ async function autoCreateSessionOnEmptyResume(bot, chatId, cwd, engine) {
154
+ const resolvedCwd = cwd ? normalizeCwd(cwd) : null;
155
+ if (!resolvedCwd || !fs.existsSync(resolvedCwd) || typeof attachOrCreateSession !== 'function') {
156
+ await bot.sendMessage(chatId, `No sessions found${resolvedCwd ? ' in ' + path.basename(resolvedCwd) : ''}. Try /new first.`);
157
+ return true;
158
+ }
159
+ attachOrCreateSession(getSessionRoute(chatId).sessionChatId, resolvedCwd, '', engine || getDefaultEngine());
160
+ await bot.sendMessage(chatId, `📁 ${path.basename(resolvedCwd)}\n✅ 已自动创建新会话`);
161
+ return true;
162
+ }
163
+
55
164
  // Pending activations have no TTL — they persist until consumed.
56
165
  // The creating chatId is stored to prevent self-activation.
57
166
 
58
- function storePendingActivation(agentKey, agentName, cwd, createdByChatId) {
59
- if (!pendingActivations) return;
60
- pendingActivations.set(agentKey, {
61
- agentKey, agentName, cwd,
62
- createdByChatId: String(createdByChatId),
63
- createdAt: Date.now(),
64
- });
65
- }
66
-
67
167
  // Returns the latest pending activation, excluding the creating chat
68
168
  function getLatestActivationForChat(chatId) {
69
169
  if (!pendingActivations || pendingActivations.size === 0) return null;
@@ -188,25 +288,6 @@ function createAgentCommandHandler(deps) {
188
288
  return { ok: true, data: legacy };
189
289
  }
190
290
 
191
- async function createAgentViaUnifiedApi(chatId, name, dir, roleDesc, opts = {}) {
192
- // Default: skip binding the creating chat — let the target group activate via /activate
193
- const { skipChatBinding = true, engine = null } = opts;
194
- if (agentTools && typeof agentTools.createNewWorkspaceAgent === 'function') {
195
- const res = await agentTools.createNewWorkspaceAgent(name, dir, roleDesc, chatId, { skipChatBinding, engine });
196
- if (res.ok && skipChatBinding && res.data && res.data.projectKey) {
197
- storePendingActivation(res.data.projectKey, name, res.data.cwd, chatId);
198
- }
199
- return res;
200
- }
201
- const bound = await doBindAgent({ sendMessage: async () => {} }, chatId, name, dir);
202
- if (!bound || bound.ok === false) {
203
- return { ok: false, error: (bound && bound.error) || 'bind failed' };
204
- }
205
- const merged = await mergeAgentRole(dir, roleDesc);
206
- if (merged.error) return { ok: false, error: merged.error };
207
- return { ok: true, data: { cwd: dir, project: { name }, role: merged } };
208
- }
209
-
210
291
  async function listAgentsViaUnifiedApi(chatId) {
211
292
  if (agentTools && typeof agentTools.listAllAgents === 'function') {
212
293
  return agentTools.listAllAgents(chatId);
@@ -271,43 +352,66 @@ function createAgentCommandHandler(deps) {
271
352
  const arg = text.slice(7).trim();
272
353
 
273
354
  // Get current workdir to scope session list — prefer bound project cwd over session cwd
274
- const cfgForResume = loadConfig();
275
- const chatAgentMapForResume = { ...(cfgForResume.telegram ? cfgForResume.telegram.chat_agent_map : {}), ...(cfgForResume.feishu ? cfgForResume.feishu.chat_agent_map : {}) };
276
- const boundKeyForResume = chatAgentMapForResume[String(chatId)];
277
- const boundProjForResume = boundKeyForResume && cfgForResume.projects ? cfgForResume.projects[boundKeyForResume] : null;
278
- const boundCwdForResume = (boundProjForResume && boundProjForResume.cwd) ? normalizeCwd(boundProjForResume.cwd) : null;
279
- const curSession = getSession(chatId);
280
- const curCwd = boundCwdForResume || (curSession ? curSession.cwd : null);
281
- const recentSessions = listRecentSessions(5, curCwd);
355
+ const route = getSessionRoute(chatId);
356
+ const isLogicalRoute = route.sessionChatId !== String(chatId);
357
+ const currentLogical = getLogicalSessionForRoute(route);
358
+ const curSession = getSession(route.sessionChatId) || getSession(chatId);
359
+ const curCwd = route.cwd || (curSession ? curSession.cwd : null);
360
+ const currentEngine = getCurrentEngine(chatId);
361
+ const recentSessions = listRecentSessions(5, curCwd, currentEngine);
362
+ const resumeChoices = buildResumeChoices({
363
+ recentSessions,
364
+ currentLogical,
365
+ curCwd,
366
+ currentEngine,
367
+ isLogicalRoute,
368
+ });
282
369
 
283
370
  if (!arg) {
284
- if (recentSessions.length === 0) {
285
- await bot.sendMessage(chatId, `No sessions found${curCwd ? ' in ' + path.basename(curCwd) : ''}. Try /new first.`);
286
- return true;
371
+ if (resumeChoices.length === 0) {
372
+ return autoCreateSessionOnEmptyResume(bot, chatId, curCwd, currentEngine);
287
373
  }
288
374
  const headerTitle = curCwd ? `📋 Sessions in ${path.basename(curCwd)}` : '📋 Recent Sessions';
289
- if (bot.sendRawCard) {
290
- await bot.sendRawCard(chatId, headerTitle, buildSessionCardElements(recentSessions));
291
- } else if (bot.sendButtons) {
292
- const buttons = recentSessions.map(s => {
293
- return [{ text: sessionLabel(s), callback_data: `/resume ${s.sessionId}` }];
294
- });
295
- await bot.sendButtons(chatId, headerTitle, buttons);
296
- } else {
297
- const _tags2 = loadSessionTags();
298
- let msg = `${headerTitle}\n\n`;
299
- recentSessions.forEach((s, i) => {
300
- msg += sessionRichLabel(s, i + 1, _tags2) + '\n';
301
- });
302
- await bot.sendMessage(chatId, msg);
375
+ try {
376
+ if (bot.sendRawCard) {
377
+ await bot.sendRawCard(chatId, headerTitle, buildSessionCardElements(resumeChoices));
378
+ } else {
379
+ throw new Error('raw-card-unavailable');
380
+ }
381
+ } catch {
382
+ try {
383
+ if (bot.sendButtons) {
384
+ const buttons = resumeChoices.map(s => {
385
+ return [{ text: sessionLabel(s), callback_data: `/resume ${s.sessionId}` }];
386
+ });
387
+ await bot.sendButtons(chatId, headerTitle, buttons);
388
+ } else {
389
+ throw new Error('buttons-unavailable');
390
+ }
391
+ } catch {
392
+ const _tags2 = loadSessionTags();
393
+ let msg = `${headerTitle}\n`;
394
+ msg += '\n';
395
+ resumeChoices.forEach((s, i) => {
396
+ msg += sessionRichLabel(s, i + 1, _tags2) + '\n';
397
+ });
398
+ await bot.sendMessage(chatId, msg);
399
+ }
303
400
  }
304
401
  return true;
305
402
  }
306
403
 
307
- // Argument given -> match by name, then by session ID prefix
308
- const allSessions = listRecentSessions(50);
404
+ // Argument given -> match current resume choices first (includes synthetic
405
+ // "当前会话" entry for logical routes), then fall back to global history.
406
+ const allSessions = listRecentSessions(50, null, currentEngine);
309
407
  const argLower = arg.toLowerCase();
310
- let fullMatch = allSessions.find(s => s.customTitle && s.customTitle.toLowerCase() === argLower);
408
+ let fullMatch = resumeChoices.find(s => s.customTitle && s.customTitle.toLowerCase() === argLower);
409
+ if (!fullMatch) {
410
+ fullMatch = allSessions.find(s => s.customTitle && s.customTitle.toLowerCase() === argLower);
411
+ }
412
+ if (!fullMatch) {
413
+ fullMatch = resumeChoices.find(s => s.customTitle && s.customTitle.toLowerCase().includes(argLower));
414
+ }
311
415
  if (!fullMatch) {
312
416
  fullMatch = allSessions.find(s => s.customTitle && s.customTitle.toLowerCase().includes(argLower));
313
417
  }
@@ -315,35 +419,53 @@ function createAgentCommandHandler(deps) {
315
419
  fullMatch = recentSessions.find(s => s.sessionId.startsWith(arg))
316
420
  || allSessions.find(s => s.sessionId.startsWith(arg));
317
421
  }
422
+ if (!fullMatch) {
423
+ fullMatch = resumeChoices.find(s => s.sessionId.startsWith(arg));
424
+ }
318
425
  if (!fullMatch) {
319
426
  // keep historical behavior:
320
427
  // "/resume 看到的session信息太少了" should be treated as normal text
321
428
  return null;
322
429
  }
323
430
  const sessionId = fullMatch.sessionId;
324
- const cwd = fullMatch.projectPath || (getSession(chatId) && getSession(chatId).cwd) || HOME;
431
+ const cwd = fullMatch.projectPath || (curSession && curSession.cwd) || HOME;
325
432
 
326
433
  const state2 = loadState();
327
434
  const cfgForEngine = loadConfig();
328
- const engineByTargetCwd = inferEngineByCwd(cfgForEngine, cwd) || getDefaultEngine();
329
- // For bound chats, write session to virtual chatId (_agent_{key}) so askClaude picks it up
330
- const resumeChatAgentMap = { ...(cfgForEngine.telegram ? cfgForEngine.telegram.chat_agent_map : {}), ...(cfgForEngine.feishu ? cfgForEngine.feishu.chat_agent_map : {}) };
331
- const resumeBoundKey = resumeChatAgentMap[String(chatId)];
332
- const sessionKey = resumeBoundKey ? `_agent_${resumeBoundKey}` : String(chatId);
435
+ const sessionKey = route.sessionChatId;
333
436
  const existing = state2.sessions[sessionKey] || {};
437
+ const existingEngine = normalizeEngineName(
438
+ existing.engine
439
+ || (existing.engines && Object.entries(existing.engines).find(([, slot]) => slot && slot.started)?.[0])
440
+ );
441
+ const engineByTargetCwd = normalizeEngineName(fullMatch.engine)
442
+ || inferEngineByCwd(cfgForEngine, cwd)
443
+ || existingEngine;
444
+ const selectedLogicalCurrent = isLogicalRoute
445
+ && currentLogical
446
+ && currentLogical.id
447
+ && sessionId === currentLogical.id;
448
+ const targetSessionId = sessionId;
449
+ const targetCwd = cwd;
334
450
  const existingEngines = existing.engines || {};
335
451
  state2.sessions[sessionKey] = {
336
452
  ...existing,
337
- cwd,
338
- engines: { ...existingEngines, [engineByTargetCwd]: { id: sessionId, started: true } },
453
+ cwd: targetCwd,
454
+ id: targetSessionId,
455
+ started: true,
456
+ engine: engineByTargetCwd,
457
+ engines: { ...existingEngines, [engineByTargetCwd]: { id: targetSessionId, started: true } },
339
458
  };
340
459
  saveState(state2);
341
460
  const name = fullMatch.customTitle;
342
- const label = name || (fullMatch.summary || fullMatch.firstPrompt || '').slice(0, 40) || sessionId.slice(0, 8);
461
+ const label = name || (fullMatch.summary || fullMatch.firstPrompt || '').slice(0, 40) || targetSessionId.slice(0, 8);
343
462
 
344
463
  // 读取最近对话片段,帮助确认是否切换到正确的 session
345
- const recentCtx = getSessionRecentContext ? getSessionRecentContext(sessionId) : null;
464
+ const recentCtx = getSessionRecentContext ? getSessionRecentContext(targetSessionId) : null;
346
465
  let msg = `✅ 已切换: **${label}**\n📁 ${path.basename(cwd)}`;
466
+ if (selectedLogicalCurrent) {
467
+ msg += '\n\n已恢复当前智能体会话。';
468
+ }
347
469
  if (recentCtx) {
348
470
  if (recentCtx.lastUser) {
349
471
  const snippet = recentCtx.lastUser.replace(/\n/g, ' ').slice(0, 80);