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.
- package/README.md +6 -1
- package/index.js +277 -55
- package/package.json +2 -2
- package/scripts/agent-layer.js +4 -2
- package/scripts/bin/dispatch_to +17 -5
- package/scripts/daemon-admin-commands.js +264 -62
- package/scripts/daemon-agent-commands.js +188 -66
- package/scripts/daemon-bridges.js +447 -48
- package/scripts/daemon-claude-engine.js +650 -103
- package/scripts/daemon-command-router.js +134 -27
- package/scripts/daemon-command-session-route.js +118 -0
- package/scripts/daemon-default.yaml +2 -0
- package/scripts/daemon-engine-runtime.js +96 -20
- package/scripts/daemon-exec-commands.js +106 -50
- package/scripts/daemon-file-browser.js +63 -7
- package/scripts/daemon-notify.js +18 -4
- package/scripts/daemon-ops-commands.js +16 -2
- package/scripts/daemon-remote-dispatch.js +34 -2
- 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 +610 -181
- package/scripts/docs/hook-config.md +7 -4
- package/scripts/docs/maintenance-manual.md +8 -1
- package/scripts/feishu-adapter.js +7 -15
- package/scripts/hooks/doc-router.js +29 -0
- package/scripts/hooks/intent-doc-router.js +54 -0
- package/scripts/hooks/intent-engine.js +9 -40
- package/scripts/intent-registry.js +59 -0
- package/scripts/memory-extract.js +59 -0
- 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 +150 -11
- package/scripts/hooks/intent-agent-manage.js +0 -50
- package/scripts/hooks/intent-hook-config.js +0 -28
|
@@ -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;
|
|
@@ -585,18 +658,28 @@ function createCommandRouter(deps) {
|
|
|
585
658
|
// --- chat_agent_map: auto-switch agent based on dedicated chatId ---
|
|
586
659
|
// Configure in daemon.yaml: feishu.chat_agent_map or telegram.chat_agent_map
|
|
587
660
|
// e.g. chat_agent_map: { "oc_xxx": "personal", "oc_yyy": "metame" }
|
|
588
|
-
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
|
+
};
|
|
589
667
|
const _chatIdStr = String(chatId);
|
|
590
668
|
const mappedKey = chatAgentMap[_chatIdStr] ||
|
|
591
669
|
projectKeyFromVirtualChatId(_chatIdStr);
|
|
592
670
|
if (mappedKey && config.projects && config.projects[mappedKey]) {
|
|
593
671
|
const proj = config.projects[mappedKey];
|
|
594
672
|
const projCwd = normalizeCwd(proj.cwd);
|
|
595
|
-
const
|
|
596
|
-
const
|
|
673
|
+
const sessionChatId = buildSessionChatId(chatId, mappedKey);
|
|
674
|
+
const cur = loadState().sessions?.[sessionChatId];
|
|
597
675
|
const projEngine = String((proj && proj.engine) || getDefaultEngine()).toLowerCase();
|
|
598
|
-
|
|
599
|
-
|
|
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());
|
|
600
683
|
}
|
|
601
684
|
}
|
|
602
685
|
|
|
@@ -609,7 +692,7 @@ function createCommandRouter(deps) {
|
|
|
609
692
|
return;
|
|
610
693
|
}
|
|
611
694
|
|
|
612
|
-
const adminResult = await handleAdminCommand({ bot, chatId, text, config, state });
|
|
695
|
+
const adminResult = await handleAdminCommand({ bot, chatId, text, config, state, senderId });
|
|
613
696
|
if (adminResult.handled) {
|
|
614
697
|
config = adminResult.config || config;
|
|
615
698
|
return;
|
|
@@ -673,16 +756,10 @@ function createCommandRouter(deps) {
|
|
|
673
756
|
if (activeProcesses.has(chatId) && INTERRUPT_RE.test(text.trim())) {
|
|
674
757
|
// Kill current process but preserve session for resume
|
|
675
758
|
if (messageQueue.has(chatId)) {
|
|
676
|
-
|
|
677
|
-
if (q.timer) clearTimeout(q.timer);
|
|
759
|
+
clearQueuedTimer(chatId);
|
|
678
760
|
messageQueue.delete(chatId);
|
|
679
761
|
}
|
|
680
|
-
|
|
681
|
-
if (proc && proc.child) {
|
|
682
|
-
proc.aborted = true;
|
|
683
|
-
const signal = proc.killSignal || 'SIGTERM';
|
|
684
|
-
try { process.kill(-proc.child.pid, signal); } catch { try { proc.child.kill(signal); } catch { /* */ } }
|
|
685
|
-
}
|
|
762
|
+
interruptActiveProcess(chatId);
|
|
686
763
|
await bot.sendMessage(chatId, '⏸ 好的,听你说');
|
|
687
764
|
return;
|
|
688
765
|
}
|
|
@@ -695,31 +772,60 @@ function createCommandRouter(deps) {
|
|
|
695
772
|
if (handled) {
|
|
696
773
|
// /last attached the session — now send "继续" to actually resume the conversation
|
|
697
774
|
resetCooldown(chatId);
|
|
698
|
-
await askClaude(bot, chatId, '继续上面的工作', config, readOnly);
|
|
775
|
+
await askClaude(bot, chatId, '继续上面的工作', config, readOnly, senderId);
|
|
699
776
|
return;
|
|
700
777
|
}
|
|
701
778
|
// No session found — fall through to normal askClaude
|
|
702
779
|
}
|
|
703
780
|
|
|
704
|
-
//
|
|
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.
|
|
705
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
|
+
}
|
|
706
809
|
const isFirst = !messageQueue.has(chatId);
|
|
707
810
|
if (isFirst) {
|
|
708
|
-
messageQueue.set(chatId, { messages: [] });
|
|
811
|
+
messageQueue.set(chatId, { messages: [], mode: 'resume-after-pause', timer: null });
|
|
709
812
|
}
|
|
710
813
|
const q = messageQueue.get(chatId);
|
|
711
|
-
if (q.messages.length >= 10) {
|
|
712
|
-
await bot.sendMessage(chatId, '⚠️ 排队已满(10条),请等当前任务完成');
|
|
713
|
-
return;
|
|
714
|
-
}
|
|
715
814
|
q.messages.push(text);
|
|
716
815
|
if (isFirst) {
|
|
717
|
-
|
|
816
|
+
interruptActiveProcess(chatId);
|
|
817
|
+
await bot.sendMessage(chatId, '⏸ 已暂停当前任务,你可以继续连发,我会自动合并后续内容再继续');
|
|
718
818
|
}
|
|
819
|
+
scheduleQueuedResume(bot, chatId, config, readOnly, senderId);
|
|
719
820
|
return;
|
|
720
821
|
}
|
|
721
822
|
// Strict mode: chats with a fixed agent in chat_agent_map must not cross-dispatch
|
|
722
|
-
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
|
+
};
|
|
723
829
|
const _isStrictChat = !!(_strictChatAgentMap[String(chatId)] || projectKeyFromVirtualChatId(String(chatId)));
|
|
724
830
|
|
|
725
831
|
// Nickname-only switch: bypass cooldown + budget (no Claude call)
|
|
@@ -729,7 +835,7 @@ function createCommandRouter(deps) {
|
|
|
729
835
|
if (quickAgent && !quickAgent.rest) {
|
|
730
836
|
const { key, proj } = quickAgent;
|
|
731
837
|
const projCwd = normalizeCwd(proj.cwd);
|
|
732
|
-
attachOrCreateSession(chatId, projCwd, proj.name || key, proj.engine || getDefaultEngine());
|
|
838
|
+
attachOrCreateSession(buildSessionChatId(chatId, key), projCwd, proj.name || key, proj.engine || getDefaultEngine());
|
|
733
839
|
log('INFO', `Agent switch via nickname: ${key} (${projCwd})`);
|
|
734
840
|
await bot.sendMessage(chatId, `${proj.icon || '🤖'} ${proj.name || key} 在线`);
|
|
735
841
|
return;
|
|
@@ -755,7 +861,7 @@ function createCommandRouter(deps) {
|
|
|
755
861
|
await bot.sendMessage(chatId, 'Daily token budget exceeded.');
|
|
756
862
|
return;
|
|
757
863
|
}
|
|
758
|
-
const claudeResult = await askClaude(bot, chatId, text, config, readOnly);
|
|
864
|
+
const claudeResult = await askClaude(bot, chatId, text, config, readOnly, senderId);
|
|
759
865
|
const claudeFailed = !!(claudeResult && claudeResult.ok === false);
|
|
760
866
|
const claudeAborted = !!(claudeResult && claudeResult.error === 'Stopped by user');
|
|
761
867
|
if (claudeFailed && !claudeAborted && !macLocalFirst && macFallbackEnabled && allowLocalMacControl) {
|
|
@@ -773,13 +879,14 @@ function createCommandRouter(deps) {
|
|
|
773
879
|
// Use while-loop instead of recursion to avoid unbounded stack growth
|
|
774
880
|
while (messageQueue.has(chatId)) {
|
|
775
881
|
const q = messageQueue.get(chatId);
|
|
882
|
+
if (q && q.mode === 'resume-after-pause') break;
|
|
776
883
|
const msgs = q.messages.splice(0);
|
|
777
884
|
messageQueue.delete(chatId);
|
|
778
885
|
if (msgs.length === 0) break;
|
|
779
886
|
const combined = msgs.join('\n');
|
|
780
887
|
log('INFO', `Follow-up: processing ${msgs.length} queued message(s) for ${chatId}`);
|
|
781
888
|
resetCooldown(chatId);
|
|
782
|
-
const followUp = await askClaude(bot, chatId, combined, config, readOnly);
|
|
889
|
+
const followUp = await askClaude(bot, chatId, combined, config, readOnly, senderId);
|
|
783
890
|
if (followUp && followUp.error === 'Stopped by user') break;
|
|
784
891
|
}
|
|
785
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 };
|
|
@@ -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
|
};
|