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
|
@@ -20,7 +20,6 @@ function createCommandRouter(deps) {
|
|
|
20
20
|
getNoSleepProcess,
|
|
21
21
|
activeProcesses,
|
|
22
22
|
messageQueue,
|
|
23
|
-
sleep,
|
|
24
23
|
log,
|
|
25
24
|
agentTools,
|
|
26
25
|
pendingAgentFlows,
|
|
@@ -29,6 +28,71 @@ function createCommandRouter(deps) {
|
|
|
29
28
|
getDefaultEngine,
|
|
30
29
|
} = deps;
|
|
31
30
|
|
|
31
|
+
function clearQueuedTimer(chatId) {
|
|
32
|
+
const q = messageQueue && messageQueue.get(chatId);
|
|
33
|
+
if (q && q.timer) {
|
|
34
|
+
clearTimeout(q.timer);
|
|
35
|
+
q.timer = null;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function interruptActiveProcess(chatId) {
|
|
40
|
+
const proc = activeProcesses.get(chatId);
|
|
41
|
+
if (proc && proc.child) {
|
|
42
|
+
proc.aborted = true;
|
|
43
|
+
const signal = proc.killSignal || 'SIGTERM';
|
|
44
|
+
try { process.kill(-proc.child.pid, signal); } catch { try { proc.child.kill(signal); } catch { /* */ } }
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function shouldPauseAndMergeFollowUps(chatId) {
|
|
51
|
+
const proc = activeProcesses.get(chatId);
|
|
52
|
+
return !!(proc && proc.engine === 'codex');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function getFollowUpDebounceMs(config) {
|
|
56
|
+
const raw = Number(config && config.daemon && config.daemon.follow_up_debounce_ms);
|
|
57
|
+
if (Number.isFinite(raw) && raw >= 300) return raw;
|
|
58
|
+
return 2500;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function buildMergedFollowUpPrompt(messages) {
|
|
62
|
+
return [
|
|
63
|
+
'继续上面的工作,并结合我刚刚连续补充的消息统一处理:',
|
|
64
|
+
'',
|
|
65
|
+
messages.join('\n'),
|
|
66
|
+
].join('\n');
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function scheduleQueuedResume(bot, chatId, config, readOnly, senderId) {
|
|
70
|
+
const q = messageQueue.get(chatId);
|
|
71
|
+
if (!q || q.mode !== 'resume-after-pause') return;
|
|
72
|
+
clearQueuedTimer(chatId);
|
|
73
|
+
const delay = getFollowUpDebounceMs(config);
|
|
74
|
+
q.timer = setTimeout(async () => {
|
|
75
|
+
const pending = messageQueue.get(chatId);
|
|
76
|
+
if (!pending || pending.mode !== 'resume-after-pause') return;
|
|
77
|
+
if (activeProcesses.has(chatId)) {
|
|
78
|
+
scheduleQueuedResume(bot, chatId, config, readOnly, senderId);
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
const msgs = pending.messages.splice(0);
|
|
82
|
+
messageQueue.delete(chatId);
|
|
83
|
+
if (msgs.length === 0) return;
|
|
84
|
+
log('INFO', `Follow-up: resuming with ${msgs.length} merged queued message(s) for ${chatId}`);
|
|
85
|
+
resetCooldown(chatId);
|
|
86
|
+
try {
|
|
87
|
+
await askClaude(bot, chatId, buildMergedFollowUpPrompt(msgs), config, readOnly, senderId);
|
|
88
|
+
} catch (err) {
|
|
89
|
+
log('WARN', `Follow-up resume failed for ${chatId}: ${err.message}`);
|
|
90
|
+
try { await bot.sendMessage(chatId, `⚠️ 继续处理补充消息失败:${err.message}`); } catch { /* */ }
|
|
91
|
+
}
|
|
92
|
+
}, delay);
|
|
93
|
+
if (typeof q.timer.unref === 'function') q.timer.unref();
|
|
94
|
+
}
|
|
95
|
+
|
|
32
96
|
function resolveFlowTtlMs() {
|
|
33
97
|
const raw = typeof agentFlowTtlMs === 'function' ? agentFlowTtlMs() : agentFlowTtlMs;
|
|
34
98
|
const num = Number(raw);
|
|
@@ -214,10 +278,19 @@ function createCommandRouter(deps) {
|
|
|
214
278
|
return null;
|
|
215
279
|
}
|
|
216
280
|
|
|
281
|
+
function buildSessionChatId(chatId, projectKey = null) {
|
|
282
|
+
const rawChatId = String(chatId || '');
|
|
283
|
+
const inferredKey = projectKey || projectKeyFromVirtualChatId(rawChatId);
|
|
284
|
+
if (rawChatId.startsWith('_agent_') || rawChatId.startsWith('_scope_')) return rawChatId;
|
|
285
|
+
return inferredKey ? `_bound_${inferredKey}` : rawChatId;
|
|
286
|
+
}
|
|
287
|
+
|
|
217
288
|
function getBoundProjectForChat(chatId, cfg) {
|
|
218
289
|
const map = {
|
|
219
290
|
...(cfg.telegram ? cfg.telegram.chat_agent_map : {}),
|
|
220
291
|
...(cfg.feishu ? cfg.feishu.chat_agent_map : {}),
|
|
292
|
+
...(cfg.imessage ? cfg.imessage.chat_agent_map : {}),
|
|
293
|
+
...(cfg.siri_bridge ? cfg.siri_bridge.chat_agent_map : {}),
|
|
221
294
|
};
|
|
222
295
|
const key = map[String(chatId)];
|
|
223
296
|
const proj = key && cfg.projects ? cfg.projects[key] : null;
|
|
@@ -350,12 +423,83 @@ function createCommandRouter(deps) {
|
|
|
350
423
|
});
|
|
351
424
|
}
|
|
352
425
|
|
|
426
|
+
function _detectCloneIntent(text) {
|
|
427
|
+
if (!text || text.startsWith('/') || text.length < 3) return false;
|
|
428
|
+
const cloneKeywords = ['分身', '再造', '克隆', '副本', '另一个自己', '另一个我'];
|
|
429
|
+
const hasCloneKeyword = cloneKeywords.some(k => text.includes(k));
|
|
430
|
+
if (hasCloneKeyword) {
|
|
431
|
+
const excludePatterns = [/已经/, /存在/, /有了/, /好了/, /完成/, /搞定/, /配置好/, /怎么建/, /如何建/, /方法/, /步骤/];
|
|
432
|
+
if (excludePatterns.some(p => p.test(text))) return false;
|
|
433
|
+
return true;
|
|
434
|
+
}
|
|
435
|
+
const actionKeywords = ['新建', '创建', '造', '做一个', '加一个', '增加', '添加'];
|
|
436
|
+
const hasAction = actionKeywords.some(k => text.includes(k));
|
|
437
|
+
if (hasAction && /分身|数字/.test(text)) return true;
|
|
438
|
+
if (/让.*做分身|叫.*做分身|甲.*做分身/.test(text)) return true;
|
|
439
|
+
return false;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
function _detectNewAgentIntent(text) {
|
|
443
|
+
if (!text || text.startsWith('/') || text.length < 3) return false;
|
|
444
|
+
if (_detectCloneIntent(text)) return false;
|
|
445
|
+
if (_detectTeamIntent(text)) return false;
|
|
446
|
+
const agentKeywords = ['agent', '助手', '机器人', '小助手'];
|
|
447
|
+
const hasAgentKeyword = agentKeywords.some(k => text.toLowerCase().includes(k.toLowerCase()));
|
|
448
|
+
const actionKeywords = ['新建', '创建', '造', '做一个', '加一个', '增加', '添加', '开一个'];
|
|
449
|
+
const hasAction = actionKeywords.some(k => text.includes(k));
|
|
450
|
+
if (hasAgentKeyword && hasAction) {
|
|
451
|
+
const excludePatterns = [/已经/, /存在/, /有了/, /好了/, /完成/, /搞定/, /配置好/, /怎么建/, /如何建/, /方法/, /步骤/, /是什么/, /哪个/];
|
|
452
|
+
if (excludePatterns.some(p => p.test(text))) return false;
|
|
453
|
+
return true;
|
|
454
|
+
}
|
|
455
|
+
if (/^(给我|帮我|我要|我想|给我加|帮我加)/.test(text) && hasAgentKeyword) return true;
|
|
456
|
+
return false;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
function _detectTeamIntent(text) {
|
|
460
|
+
if (!text || text.startsWith('/') || text.length < 4) return false;
|
|
461
|
+
// Exclude: only mentioning team, no creation intent
|
|
462
|
+
if (/走team|用team|通过team|team里|team中|团队里|团队中|走团队|用团队|在team|在团队|team.*已经|团队.*已经|team.*讨论|团队.*讨论/.test(text)) return false;
|
|
463
|
+
// Positive match: team + action word
|
|
464
|
+
if ((text.includes('团队') || text.includes('工作组'))) {
|
|
465
|
+
if (/(新建|创建|造一个|加一个|组建|设置|建|搞)/.test(text)) {
|
|
466
|
+
if (/怎么|如何|方法|步骤/.test(text)) return false;
|
|
467
|
+
return true;
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
// Pattern: "建个团队" / "搞个团队"
|
|
471
|
+
if (/^(新建|创建|建|搞).*团队/.test(text)) return true;
|
|
472
|
+
return false;
|
|
473
|
+
}
|
|
474
|
+
|
|
353
475
|
async function tryHandleAgentIntent(bot, chatId, text, config) {
|
|
354
476
|
if (!agentTools || !text || text.startsWith('/')) return false;
|
|
355
477
|
const key = String(chatId);
|
|
356
478
|
if (hasFreshPendingFlow(key) || hasFreshPendingFlow(key + ':edit')) return false;
|
|
357
479
|
const input = text.trim();
|
|
358
480
|
if (!input) return false;
|
|
481
|
+
|
|
482
|
+
// Clone intent — route to /agent new clone wizard
|
|
483
|
+
if (_detectCloneIntent(input)) {
|
|
484
|
+
log('INFO', `[CloneIntent] "${input.slice(0, 80)}" → /agent new clone`);
|
|
485
|
+
await handleAgentCommand({ bot, chatId, text: '/agent new clone', config });
|
|
486
|
+
return true;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// New agent intent — route to /agent new wizard
|
|
490
|
+
if (_detectNewAgentIntent(input)) {
|
|
491
|
+
log('INFO', `[NewAgentIntent] "${input.slice(0, 80)}" → /agent new`);
|
|
492
|
+
await handleAgentCommand({ bot, chatId, text: '/agent new', config });
|
|
493
|
+
return true;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// Team creation intent — route to /agent new team wizard
|
|
497
|
+
if (_detectTeamIntent(input)) {
|
|
498
|
+
log('INFO', `[TeamIntent] "${input.slice(0, 80)}" → /agent new team`);
|
|
499
|
+
await handleAgentCommand({ bot, chatId, text: '/agent new team', config });
|
|
500
|
+
return true;
|
|
501
|
+
}
|
|
502
|
+
|
|
359
503
|
const directAction = isLikelyDirectAgentAction(input);
|
|
360
504
|
const issueReport = looksLikeAgentIssueReport(input);
|
|
361
505
|
if (issueReport && !directAction) return false;
|
|
@@ -514,18 +658,28 @@ function createCommandRouter(deps) {
|
|
|
514
658
|
// --- chat_agent_map: auto-switch agent based on dedicated chatId ---
|
|
515
659
|
// Configure in daemon.yaml: feishu.chat_agent_map or telegram.chat_agent_map
|
|
516
660
|
// e.g. chat_agent_map: { "oc_xxx": "personal", "oc_yyy": "metame" }
|
|
517
|
-
const chatAgentMap = {
|
|
661
|
+
const chatAgentMap = {
|
|
662
|
+
...(config.telegram ? config.telegram.chat_agent_map : {}),
|
|
663
|
+
...(config.feishu ? config.feishu.chat_agent_map : {}),
|
|
664
|
+
...(config.imessage ? config.imessage.chat_agent_map : {}),
|
|
665
|
+
...(config.siri_bridge ? config.siri_bridge.chat_agent_map : {}),
|
|
666
|
+
};
|
|
518
667
|
const _chatIdStr = String(chatId);
|
|
519
668
|
const mappedKey = chatAgentMap[_chatIdStr] ||
|
|
520
669
|
projectKeyFromVirtualChatId(_chatIdStr);
|
|
521
670
|
if (mappedKey && config.projects && config.projects[mappedKey]) {
|
|
522
671
|
const proj = config.projects[mappedKey];
|
|
523
672
|
const projCwd = normalizeCwd(proj.cwd);
|
|
524
|
-
const
|
|
525
|
-
const
|
|
673
|
+
const sessionChatId = buildSessionChatId(chatId, mappedKey);
|
|
674
|
+
const cur = loadState().sessions?.[sessionChatId];
|
|
526
675
|
const projEngine = String((proj && proj.engine) || getDefaultEngine()).toLowerCase();
|
|
527
|
-
|
|
528
|
-
|
|
676
|
+
// Multi-engine format stores engines in cur.engines object; legacy format uses cur.engine string.
|
|
677
|
+
// Check whether the session already has a slot for the project's configured engine.
|
|
678
|
+
const curHasEngine = cur && (
|
|
679
|
+
cur.engines ? !!cur.engines[projEngine] : String(cur.engine || '').toLowerCase() === projEngine
|
|
680
|
+
);
|
|
681
|
+
if (!cur || cur.cwd !== projCwd || !curHasEngine) {
|
|
682
|
+
attachOrCreateSession(sessionChatId, projCwd, proj.name || mappedKey, proj.engine || getDefaultEngine());
|
|
529
683
|
}
|
|
530
684
|
}
|
|
531
685
|
|
|
@@ -538,7 +692,7 @@ function createCommandRouter(deps) {
|
|
|
538
692
|
return;
|
|
539
693
|
}
|
|
540
694
|
|
|
541
|
-
const adminResult = await handleAdminCommand({ bot, chatId, text, config, state });
|
|
695
|
+
const adminResult = await handleAdminCommand({ bot, chatId, text, config, state, senderId });
|
|
542
696
|
if (adminResult.handled) {
|
|
543
697
|
config = adminResult.config || config;
|
|
544
698
|
return;
|
|
@@ -602,16 +756,10 @@ function createCommandRouter(deps) {
|
|
|
602
756
|
if (activeProcesses.has(chatId) && INTERRUPT_RE.test(text.trim())) {
|
|
603
757
|
// Kill current process but preserve session for resume
|
|
604
758
|
if (messageQueue.has(chatId)) {
|
|
605
|
-
|
|
606
|
-
if (q.timer) clearTimeout(q.timer);
|
|
759
|
+
clearQueuedTimer(chatId);
|
|
607
760
|
messageQueue.delete(chatId);
|
|
608
761
|
}
|
|
609
|
-
|
|
610
|
-
if (proc && proc.child) {
|
|
611
|
-
proc.aborted = true;
|
|
612
|
-
const signal = proc.killSignal || 'SIGTERM';
|
|
613
|
-
try { process.kill(-proc.child.pid, signal); } catch { try { proc.child.kill(signal); } catch { /* */ } }
|
|
614
|
-
}
|
|
762
|
+
interruptActiveProcess(chatId);
|
|
615
763
|
await bot.sendMessage(chatId, '⏸ 好的,听你说');
|
|
616
764
|
return;
|
|
617
765
|
}
|
|
@@ -624,31 +772,60 @@ function createCommandRouter(deps) {
|
|
|
624
772
|
if (handled) {
|
|
625
773
|
// /last attached the session — now send "继续" to actually resume the conversation
|
|
626
774
|
resetCooldown(chatId);
|
|
627
|
-
await askClaude(bot, chatId, '继续上面的工作', config, readOnly);
|
|
775
|
+
await askClaude(bot, chatId, '继续上面的工作', config, readOnly, senderId);
|
|
628
776
|
return;
|
|
629
777
|
}
|
|
630
778
|
// No session found — fall through to normal askClaude
|
|
631
779
|
}
|
|
632
780
|
|
|
633
|
-
//
|
|
781
|
+
// While collecting follow-up messages after a pause, keep merging them until the debounce window closes.
|
|
782
|
+
if (messageQueue.has(chatId)) {
|
|
783
|
+
const q = messageQueue.get(chatId);
|
|
784
|
+
if (q && q.mode === 'resume-after-pause') {
|
|
785
|
+
q.messages.push(text);
|
|
786
|
+
scheduleQueuedResume(bot, chatId, config, readOnly, senderId);
|
|
787
|
+
return;
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
// If a task is running: pause it, collect the user's burst of messages, then resume with a merged follow-up.
|
|
634
792
|
if (activeProcesses.has(chatId)) {
|
|
793
|
+
if (!shouldPauseAndMergeFollowUps(chatId)) {
|
|
794
|
+
const isFirst = !messageQueue.has(chatId);
|
|
795
|
+
if (isFirst) {
|
|
796
|
+
messageQueue.set(chatId, { messages: [] });
|
|
797
|
+
}
|
|
798
|
+
const q = messageQueue.get(chatId);
|
|
799
|
+
if (q.messages.length >= 10) {
|
|
800
|
+
await bot.sendMessage(chatId, '⚠️ 排队已满(10条),请等当前任务完成');
|
|
801
|
+
return;
|
|
802
|
+
}
|
|
803
|
+
q.messages.push(text);
|
|
804
|
+
if (isFirst) {
|
|
805
|
+
await bot.sendMessage(chatId, '📝 收到,完成后继续处理');
|
|
806
|
+
}
|
|
807
|
+
return;
|
|
808
|
+
}
|
|
635
809
|
const isFirst = !messageQueue.has(chatId);
|
|
636
810
|
if (isFirst) {
|
|
637
|
-
messageQueue.set(chatId, { messages: [] });
|
|
811
|
+
messageQueue.set(chatId, { messages: [], mode: 'resume-after-pause', timer: null });
|
|
638
812
|
}
|
|
639
813
|
const q = messageQueue.get(chatId);
|
|
640
|
-
if (q.messages.length >= 10) {
|
|
641
|
-
await bot.sendMessage(chatId, '⚠️ 排队已满(10条),请等当前任务完成');
|
|
642
|
-
return;
|
|
643
|
-
}
|
|
644
814
|
q.messages.push(text);
|
|
645
815
|
if (isFirst) {
|
|
646
|
-
|
|
816
|
+
interruptActiveProcess(chatId);
|
|
817
|
+
await bot.sendMessage(chatId, '⏸ 已暂停当前任务,你可以继续连发,我会自动合并后续内容再继续');
|
|
647
818
|
}
|
|
819
|
+
scheduleQueuedResume(bot, chatId, config, readOnly, senderId);
|
|
648
820
|
return;
|
|
649
821
|
}
|
|
650
822
|
// Strict mode: chats with a fixed agent in chat_agent_map must not cross-dispatch
|
|
651
|
-
const _strictChatAgentMap = {
|
|
823
|
+
const _strictChatAgentMap = {
|
|
824
|
+
...(config.telegram ? config.telegram.chat_agent_map : {}),
|
|
825
|
+
...(config.feishu ? config.feishu.chat_agent_map : {}),
|
|
826
|
+
...(config.imessage ? config.imessage.chat_agent_map : {}),
|
|
827
|
+
...(config.siri_bridge ? config.siri_bridge.chat_agent_map : {}),
|
|
828
|
+
};
|
|
652
829
|
const _isStrictChat = !!(_strictChatAgentMap[String(chatId)] || projectKeyFromVirtualChatId(String(chatId)));
|
|
653
830
|
|
|
654
831
|
// Nickname-only switch: bypass cooldown + budget (no Claude call)
|
|
@@ -658,7 +835,7 @@ function createCommandRouter(deps) {
|
|
|
658
835
|
if (quickAgent && !quickAgent.rest) {
|
|
659
836
|
const { key, proj } = quickAgent;
|
|
660
837
|
const projCwd = normalizeCwd(proj.cwd);
|
|
661
|
-
attachOrCreateSession(chatId, projCwd, proj.name || key, proj.engine || getDefaultEngine());
|
|
838
|
+
attachOrCreateSession(buildSessionChatId(chatId, key), projCwd, proj.name || key, proj.engine || getDefaultEngine());
|
|
662
839
|
log('INFO', `Agent switch via nickname: ${key} (${projCwd})`);
|
|
663
840
|
await bot.sendMessage(chatId, `${proj.icon || '🤖'} ${proj.name || key} 在线`);
|
|
664
841
|
return;
|
|
@@ -684,7 +861,7 @@ function createCommandRouter(deps) {
|
|
|
684
861
|
await bot.sendMessage(chatId, 'Daily token budget exceeded.');
|
|
685
862
|
return;
|
|
686
863
|
}
|
|
687
|
-
const claudeResult = await askClaude(bot, chatId, text, config, readOnly);
|
|
864
|
+
const claudeResult = await askClaude(bot, chatId, text, config, readOnly, senderId);
|
|
688
865
|
const claudeFailed = !!(claudeResult && claudeResult.ok === false);
|
|
689
866
|
const claudeAborted = !!(claudeResult && claudeResult.error === 'Stopped by user');
|
|
690
867
|
if (claudeFailed && !claudeAborted && !macLocalFirst && macFallbackEnabled && allowLocalMacControl) {
|
|
@@ -702,13 +879,14 @@ function createCommandRouter(deps) {
|
|
|
702
879
|
// Use while-loop instead of recursion to avoid unbounded stack growth
|
|
703
880
|
while (messageQueue.has(chatId)) {
|
|
704
881
|
const q = messageQueue.get(chatId);
|
|
882
|
+
if (q && q.mode === 'resume-after-pause') break;
|
|
705
883
|
const msgs = q.messages.splice(0);
|
|
706
884
|
messageQueue.delete(chatId);
|
|
707
885
|
if (msgs.length === 0) break;
|
|
708
886
|
const combined = msgs.join('\n');
|
|
709
887
|
log('INFO', `Follow-up: processing ${msgs.length} queued message(s) for ${chatId}`);
|
|
710
888
|
resetCooldown(chatId);
|
|
711
|
-
const followUp = await askClaude(bot, chatId, combined, config, readOnly);
|
|
889
|
+
const followUp = await askClaude(bot, chatId, combined, config, readOnly, senderId);
|
|
712
890
|
if (followUp && followUp.error === 'Stopped by user') break;
|
|
713
891
|
}
|
|
714
892
|
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
function createCommandSessionResolver(deps) {
|
|
4
|
+
const {
|
|
5
|
+
path,
|
|
6
|
+
loadConfig,
|
|
7
|
+
loadState,
|
|
8
|
+
getSession,
|
|
9
|
+
getSessionForEngine,
|
|
10
|
+
getDefaultEngine = () => 'claude',
|
|
11
|
+
} = deps;
|
|
12
|
+
|
|
13
|
+
function normalizeEngineName(name) {
|
|
14
|
+
return String(name || '').trim().toLowerCase() === 'codex' ? 'codex' : getDefaultEngine();
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function inferStoredEngine(rawSession) {
|
|
18
|
+
if (!rawSession || typeof rawSession !== 'object') return getDefaultEngine();
|
|
19
|
+
if (rawSession.engine) return normalizeEngineName(rawSession.engine);
|
|
20
|
+
const slots = rawSession.engines && typeof rawSession.engines === 'object' ? rawSession.engines : null;
|
|
21
|
+
if (!slots) return getDefaultEngine();
|
|
22
|
+
const started = Object.entries(slots).find(([, slot]) => slot && slot.started);
|
|
23
|
+
if (started) return normalizeEngineName(started[0]);
|
|
24
|
+
const available = Object.keys(slots);
|
|
25
|
+
return available.length === 1 ? normalizeEngineName(available[0]) : getDefaultEngine();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function buildBoundSessionChatId(projectKey) {
|
|
29
|
+
const key = String(projectKey || '').trim();
|
|
30
|
+
return key ? `_bound_${key}` : '';
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function normalizeRouteCwd(cwd) {
|
|
34
|
+
if (!cwd) return null;
|
|
35
|
+
try {
|
|
36
|
+
return path.resolve(String(cwd));
|
|
37
|
+
} catch {
|
|
38
|
+
return String(cwd);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function getSessionRoute(chatId) {
|
|
43
|
+
const cfg = loadConfig();
|
|
44
|
+
const state = loadState();
|
|
45
|
+
const chatKey = String(chatId);
|
|
46
|
+
const agentMap = { ...(cfg.telegram ? cfg.telegram.chat_agent_map : {}), ...(cfg.feishu ? cfg.feishu.chat_agent_map : {}) };
|
|
47
|
+
const boundKey = agentMap[chatKey] || null;
|
|
48
|
+
const boundProj = boundKey && cfg.projects ? cfg.projects[boundKey] : null;
|
|
49
|
+
const stickyKey = state && state.team_sticky ? state.team_sticky[chatKey] : null;
|
|
50
|
+
const stickyMember = stickyKey && boundProj && Array.isArray(boundProj.team)
|
|
51
|
+
? boundProj.team.find((m) => m && m.key === stickyKey)
|
|
52
|
+
: null;
|
|
53
|
+
|
|
54
|
+
if (stickyMember) {
|
|
55
|
+
return {
|
|
56
|
+
sessionChatId: `_agent_${stickyMember.key}`,
|
|
57
|
+
cwd: normalizeRouteCwd(stickyMember.cwd || (boundProj && boundProj.cwd) || null),
|
|
58
|
+
engine: normalizeEngineName(stickyMember.engine || (boundProj && boundProj.engine)),
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (boundProj) {
|
|
63
|
+
return {
|
|
64
|
+
sessionChatId: buildBoundSessionChatId(boundKey),
|
|
65
|
+
cwd: normalizeRouteCwd(boundProj.cwd || null),
|
|
66
|
+
engine: normalizeEngineName(boundProj.engine),
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const rawSession = getSession(chatId);
|
|
71
|
+
return {
|
|
72
|
+
sessionChatId: String(chatId),
|
|
73
|
+
cwd: rawSession && rawSession.cwd ? normalizeRouteCwd(rawSession.cwd) : null,
|
|
74
|
+
engine: inferStoredEngine(rawSession),
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function getActiveSession(chatId) {
|
|
79
|
+
const route = getSessionRoute(chatId);
|
|
80
|
+
const rawSession = getSession(route.sessionChatId) || getSession(chatId);
|
|
81
|
+
const engine = normalizeEngineName((rawSession && rawSession.engine) || route.engine);
|
|
82
|
+
const engineSession = getSessionForEngine(route.sessionChatId, engine)
|
|
83
|
+
|| getSessionForEngine(chatId, engine);
|
|
84
|
+
if (engineSession && engineSession.id) {
|
|
85
|
+
return {
|
|
86
|
+
route,
|
|
87
|
+
sessionKey: route.sessionChatId,
|
|
88
|
+
engine,
|
|
89
|
+
session: engineSession,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
if (rawSession && rawSession.id) {
|
|
93
|
+
return {
|
|
94
|
+
route,
|
|
95
|
+
sessionKey: route.sessionChatId,
|
|
96
|
+
engine,
|
|
97
|
+
session: { cwd: rawSession.cwd, engine, id: rawSession.id, started: !!rawSession.started },
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
return {
|
|
101
|
+
route,
|
|
102
|
+
sessionKey: route.sessionChatId,
|
|
103
|
+
engine,
|
|
104
|
+
session: null,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
normalizeEngineName,
|
|
110
|
+
inferStoredEngine,
|
|
111
|
+
buildBoundSessionChatId,
|
|
112
|
+
normalizeRouteCwd,
|
|
113
|
+
getSessionRoute,
|
|
114
|
+
getActiveSession,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
module.exports = { createCommandSessionResolver };
|
|
@@ -4,13 +4,20 @@
|
|
|
4
4
|
telegram:
|
|
5
5
|
enabled: false
|
|
6
6
|
bot_token: null
|
|
7
|
+
admin_chat_id: null
|
|
7
8
|
allowed_chat_ids: []
|
|
8
9
|
|
|
9
10
|
feishu:
|
|
10
11
|
enabled: false
|
|
11
12
|
app_id: null
|
|
12
13
|
app_secret: null
|
|
14
|
+
admin_chat_id: null
|
|
13
15
|
allowed_chat_ids: []
|
|
16
|
+
remote_dispatch:
|
|
17
|
+
enabled: false
|
|
18
|
+
self: ""
|
|
19
|
+
chat_id: ""
|
|
20
|
+
secret: ""
|
|
14
21
|
|
|
15
22
|
projects:
|
|
16
23
|
# Per-project heartbeat tasks. Each project's tasks are isolated and
|
|
@@ -239,7 +239,7 @@ function parseCodexStreamEvent(line) {
|
|
|
239
239
|
}
|
|
240
240
|
|
|
241
241
|
function buildClaudeArgs(options = {}) {
|
|
242
|
-
const { model = ENGINE_MODEL_CONFIG.claude.main, readOnly = false,
|
|
242
|
+
const { model = ENGINE_MODEL_CONFIG.claude.main, readOnly = false, session = {} } = options;
|
|
243
243
|
const args = ['-p', '--model', model];
|
|
244
244
|
if (readOnly) {
|
|
245
245
|
const readOnlyTools = ['Read', 'Glob', 'Grep', 'WebSearch', 'WebFetch', 'Task'];
|
|
@@ -260,8 +260,76 @@ function buildClaudeArgs(options = {}) {
|
|
|
260
260
|
return args;
|
|
261
261
|
}
|
|
262
262
|
|
|
263
|
+
function normalizeCodexSandboxMode(value, fallback = 'danger-full-access') {
|
|
264
|
+
const text = String(value || '').trim().toLowerCase();
|
|
265
|
+
if (!text) return fallback;
|
|
266
|
+
if (text === 'read-only' || text === 'readonly') return 'read-only';
|
|
267
|
+
if (text === 'workspace-write' || text === 'workspace') return 'workspace-write';
|
|
268
|
+
if (
|
|
269
|
+
text === 'danger-full-access'
|
|
270
|
+
|| text === 'dangerous'
|
|
271
|
+
|| text === 'full-access'
|
|
272
|
+
|| text === 'full'
|
|
273
|
+
|| text === 'bypass'
|
|
274
|
+
|| text === 'writable'
|
|
275
|
+
) return 'danger-full-access';
|
|
276
|
+
return fallback;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function normalizeCodexApprovalPolicy(value, fallback = 'never') {
|
|
280
|
+
const text = String(value || '').trim().toLowerCase();
|
|
281
|
+
if (!text) return fallback;
|
|
282
|
+
if (text === 'never' || text === 'no' || text === 'none') return 'never';
|
|
283
|
+
if (text === 'on-failure' || text === 'on_failure' || text === 'failure') return 'on-failure';
|
|
284
|
+
if (text === 'on-request' || text === 'on_request' || text === 'request') return 'on-request';
|
|
285
|
+
if (text === 'untrusted') return 'untrusted';
|
|
286
|
+
return fallback;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function resolveCodexPermissionProfile(options = {}) {
|
|
290
|
+
const { readOnly = false, daemonCfg = {}, session = {} } = options;
|
|
291
|
+
if (readOnly) {
|
|
292
|
+
return {
|
|
293
|
+
sandboxMode: 'read-only',
|
|
294
|
+
approvalPolicy: 'never',
|
|
295
|
+
permissionMode: 'read-only',
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const codexCfg = (daemonCfg && daemonCfg.codex && typeof daemonCfg.codex === 'object') ? daemonCfg.codex : {};
|
|
300
|
+
const sandboxMode = normalizeCodexSandboxMode(
|
|
301
|
+
codexCfg.sandbox_mode
|
|
302
|
+
|| codexCfg.sandboxMode
|
|
303
|
+
|| codexCfg.sandbox
|
|
304
|
+
|| codexCfg.permission_mode
|
|
305
|
+
|| codexCfg.permissionMode
|
|
306
|
+
|| session.sandboxMode
|
|
307
|
+
|| session.permissionMode,
|
|
308
|
+
'danger-full-access'
|
|
309
|
+
);
|
|
310
|
+
const approvalPolicy = normalizeCodexApprovalPolicy(
|
|
311
|
+
codexCfg.approval_policy
|
|
312
|
+
|| codexCfg.approvalPolicy
|
|
313
|
+
|| session.approvalPolicy,
|
|
314
|
+
sandboxMode === 'danger-full-access' ? 'never' : 'on-failure'
|
|
315
|
+
);
|
|
316
|
+
|
|
317
|
+
return {
|
|
318
|
+
sandboxMode,
|
|
319
|
+
approvalPolicy,
|
|
320
|
+
permissionMode: sandboxMode,
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
|
|
263
324
|
function buildCodexArgs(options = {}) {
|
|
264
|
-
const {
|
|
325
|
+
const {
|
|
326
|
+
model = ENGINE_MODEL_CONFIG.codex.main,
|
|
327
|
+
readOnly = false,
|
|
328
|
+
daemonCfg = {},
|
|
329
|
+
session = {},
|
|
330
|
+
cwd,
|
|
331
|
+
permissionProfile = null,
|
|
332
|
+
} = options;
|
|
265
333
|
const isResume = (session && session.started && session.id && session.id !== '__continue__');
|
|
266
334
|
const args = isResume
|
|
267
335
|
? ['exec', 'resume', session.id]
|
|
@@ -272,16 +340,13 @@ function buildCodexArgs(options = {}) {
|
|
|
272
340
|
// -C (cwd) is only supported on fresh exec, not resume
|
|
273
341
|
if (cwd && !isResume) args.push('-C', cwd);
|
|
274
342
|
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
// Security relies on allowed_chat_ids whitelist, not tool restrictions.
|
|
283
|
-
args.push('--dangerously-bypass-approvals-and-sandbox');
|
|
284
|
-
}
|
|
343
|
+
const effectivePermissionProfile = permissionProfile || resolveCodexPermissionProfile({ readOnly, daemonCfg, session });
|
|
344
|
+
if (effectivePermissionProfile.sandboxMode === 'danger-full-access' && effectivePermissionProfile.approvalPolicy === 'never') {
|
|
345
|
+
// Keep the legacy shortcut for the fully-trusted mobile/default path.
|
|
346
|
+
args.push('--dangerously-bypass-approvals-and-sandbox');
|
|
347
|
+
} else {
|
|
348
|
+
// codex 0.114.0 removed --ask-for-approval; only -s <sandboxMode> is needed
|
|
349
|
+
args.push('-s', effectivePermissionProfile.sandboxMode);
|
|
285
350
|
}
|
|
286
351
|
|
|
287
352
|
// "-" means prompt is read from stdin.
|
|
@@ -289,6 +354,18 @@ function buildCodexArgs(options = {}) {
|
|
|
289
354
|
return args;
|
|
290
355
|
}
|
|
291
356
|
|
|
357
|
+
function buildCodexEnv(baseEnv = {}, { metameProject = '', metameSenderId = '' } = {}) {
|
|
358
|
+
const env = { ...baseEnv, METAME_PROJECT: metameProject, METAME_SENDER_ID: String(metameSenderId || '') };
|
|
359
|
+
const strippedKeys = [
|
|
360
|
+
'CODEX_THREAD_ID',
|
|
361
|
+
'METAME_ACTIVE_SESSION',
|
|
362
|
+
'CLAUDE_CODE_SSE_PORT',
|
|
363
|
+
];
|
|
364
|
+
for (const key of strippedKeys) delete env[key];
|
|
365
|
+
if (env.CODEX_HOME && !fs.existsSync(env.CODEX_HOME)) delete env.CODEX_HOME;
|
|
366
|
+
return env;
|
|
367
|
+
}
|
|
368
|
+
|
|
292
369
|
function createEngineRuntimeFactory(deps = {}) {
|
|
293
370
|
const home = deps.HOME || os.homedir();
|
|
294
371
|
const claudeBin = deps.CLAUDE_BIN || resolveBinary('claude', { ...deps, HOME: home });
|
|
@@ -308,12 +385,7 @@ function createEngineRuntimeFactory(deps = {}) {
|
|
|
308
385
|
killSignal: 'SIGTERM',
|
|
309
386
|
timeouts: { idleMs: 10 * 60 * 1000, toolMs: 25 * 60 * 1000, ceilingMs: 60 * 60 * 1000 },
|
|
310
387
|
buildArgs: buildCodexArgs,
|
|
311
|
-
buildEnv: ({ metameProject = '' } = {}) => {
|
|
312
|
-
const env = { ...process.env, METAME_PROJECT: metameProject };
|
|
313
|
-
// Unset CODEX_HOME if it points to a non-existent path (corrupted env var)
|
|
314
|
-
if (env.CODEX_HOME && !fs.existsSync(env.CODEX_HOME)) delete env.CODEX_HOME;
|
|
315
|
-
return env;
|
|
316
|
-
},
|
|
388
|
+
buildEnv: ({ metameProject = '', metameSenderId = '' } = {}) => buildCodexEnv(process.env, { metameProject, metameSenderId }),
|
|
317
389
|
parseStreamEvent: parseCodexStreamEvent,
|
|
318
390
|
classifyError: classifyEngineError,
|
|
319
391
|
};
|
|
@@ -326,9 +398,9 @@ function createEngineRuntimeFactory(deps = {}) {
|
|
|
326
398
|
killSignal: 'SIGTERM',
|
|
327
399
|
timeouts: { idleMs: 5 * 60 * 1000, toolMs: 25 * 60 * 1000, ceilingMs: 60 * 60 * 1000 },
|
|
328
400
|
buildArgs: buildClaudeArgs,
|
|
329
|
-
buildEnv: ({ metameProject = '' } = {}) => ({
|
|
401
|
+
buildEnv: ({ metameProject = '', metameSenderId = '' } = {}) => ({
|
|
330
402
|
...(() => {
|
|
331
|
-
const env = { ...process.env, ...getActiveProviderEnv(), METAME_PROJECT: metameProject };
|
|
403
|
+
const env = { ...process.env, ...getActiveProviderEnv(), METAME_PROJECT: metameProject, METAME_SENDER_ID: String(metameSenderId || '') };
|
|
332
404
|
delete env.CLAUDECODE;
|
|
333
405
|
return env;
|
|
334
406
|
})(),
|
|
@@ -354,6 +426,10 @@ module.exports = {
|
|
|
354
426
|
parseCodexStreamEvent,
|
|
355
427
|
buildClaudeArgs,
|
|
356
428
|
buildCodexArgs,
|
|
429
|
+
buildCodexEnv,
|
|
430
|
+
normalizeCodexSandboxMode,
|
|
431
|
+
normalizeCodexApprovalPolicy,
|
|
432
|
+
resolveCodexPermissionProfile,
|
|
357
433
|
BUILTIN_CLAUDE_MODEL_VALUES,
|
|
358
434
|
},
|
|
359
435
|
};
|