metame-cli 1.4.34 → 1.5.1
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 +136 -94
- package/index.js +312 -57
- package/package.json +8 -4
- package/scripts/agent-layer.js +320 -0
- package/scripts/daemon-admin-commands.js +328 -28
- package/scripts/daemon-agent-commands.js +145 -6
- package/scripts/daemon-agent-tools.js +163 -7
- package/scripts/daemon-bridges.js +110 -20
- package/scripts/daemon-checkpoints.js +36 -7
- package/scripts/daemon-claude-engine.js +849 -358
- package/scripts/daemon-command-router.js +31 -10
- package/scripts/daemon-default.yaml +28 -4
- package/scripts/daemon-engine-runtime.js +328 -0
- package/scripts/daemon-exec-commands.js +15 -7
- package/scripts/daemon-notify.js +37 -1
- package/scripts/daemon-ops-commands.js +8 -6
- package/scripts/daemon-runtime-lifecycle.js +129 -5
- package/scripts/daemon-session-commands.js +60 -25
- package/scripts/daemon-session-store.js +121 -13
- package/scripts/daemon-task-scheduler.js +129 -49
- package/scripts/daemon-user-acl.js +35 -9
- package/scripts/daemon.js +268 -33
- package/scripts/distill.js +327 -18
- package/scripts/docs/agent-guide.md +12 -0
- package/scripts/docs/maintenance-manual.md +155 -0
- package/scripts/docs/pointer-map.md +110 -0
- package/scripts/feishu-adapter.js +42 -13
- package/scripts/hooks/stop-session-capture.js +243 -0
- package/scripts/memory-extract.js +105 -6
- package/scripts/memory-nightly-reflect.js +199 -11
- package/scripts/memory.js +134 -3
- package/scripts/mentor-engine.js +405 -0
- package/scripts/platform.js +24 -0
- package/scripts/providers.js +182 -22
- package/scripts/schema.js +12 -0
- package/scripts/session-analytics.js +245 -12
- package/scripts/skill-changelog.js +245 -0
- package/scripts/skill-evolution.js +288 -5
- package/scripts/telegram-adapter.js +12 -8
- package/scripts/usage-classifier.js +1 -1
- package/scripts/daemon-admin-commands.test.js +0 -333
- package/scripts/daemon-task-envelope.test.js +0 -59
- package/scripts/daemon-task-scheduler.test.js +0 -106
- package/scripts/reliability-core.test.js +0 -280
- package/scripts/skill-evolution.test.js +0 -113
- package/scripts/task-board.test.js +0 -83
- package/scripts/test_daemon.js +0 -1407
- package/scripts/utils.test.js +0 -192
|
@@ -26,6 +26,7 @@ function createCommandRouter(deps) {
|
|
|
26
26
|
pendingAgentFlows,
|
|
27
27
|
pendingActivations,
|
|
28
28
|
agentFlowTtlMs,
|
|
29
|
+
getDefaultEngine,
|
|
29
30
|
} = deps;
|
|
30
31
|
|
|
31
32
|
function resolveFlowTtlMs() {
|
|
@@ -154,8 +155,8 @@ function createCommandRouter(deps) {
|
|
|
154
155
|
const explicit = extractAgentName(input);
|
|
155
156
|
if (explicit) return explicit;
|
|
156
157
|
if (workspaceDir) {
|
|
157
|
-
const
|
|
158
|
-
if (
|
|
158
|
+
const basename = workspaceDir.split(/[/\\]/).filter(Boolean).pop();
|
|
159
|
+
if (basename) return basename;
|
|
159
160
|
}
|
|
160
161
|
return 'workspace-agent';
|
|
161
162
|
}
|
|
@@ -178,6 +179,13 @@ function createCommandRouter(deps) {
|
|
|
178
179
|
return '';
|
|
179
180
|
}
|
|
180
181
|
|
|
182
|
+
function inferAgentEngineFromText(input) {
|
|
183
|
+
const text = String(input || '').trim().toLowerCase();
|
|
184
|
+
if (!text) return null;
|
|
185
|
+
if (/\bcodex\b/.test(text) || /柯德|科德/.test(text)) return 'codex';
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
|
|
181
189
|
function isLikelyDirectAgentAction(input) {
|
|
182
190
|
const text = String(input || '').trim();
|
|
183
191
|
return /^(?:请|帮我|麻烦|给我|给这个群|给当前群|在这个群|把这个群|把当前群|将这个群|这个群|当前群|本群|群里|我想|我要|我需要|创建|新建|新增|搞一个|加一个|create|bind|绑定|列出|查看|显示|有哪些|解绑|取消绑定|断开绑定|修改|调整)/i.test(text);
|
|
@@ -414,6 +422,10 @@ function createCommandRouter(deps) {
|
|
|
414
422
|
await bot.sendMessage(chatId, '❌ 当前群未绑定 Agent。先说“给这个群绑定一个 Agent,目录是 ~/xxx”。');
|
|
415
423
|
return true;
|
|
416
424
|
}
|
|
425
|
+
// Lazy migration: ensure soul layer exists for agents created before this feature
|
|
426
|
+
if (agentTools && typeof agentTools.repairAgentSoul === 'function') {
|
|
427
|
+
await agentTools.repairAgentSoul(bound.project.cwd).catch(() => {});
|
|
428
|
+
}
|
|
417
429
|
const roleDelta = deriveRoleDelta(input);
|
|
418
430
|
const res = await agentTools.editAgentRoleDefinition(bound.project.cwd, roleDelta);
|
|
419
431
|
if (!res.ok) {
|
|
@@ -435,14 +447,19 @@ function createCommandRouter(deps) {
|
|
|
435
447
|
}
|
|
436
448
|
const agentName = deriveAgentName(input, workspaceDir);
|
|
437
449
|
const roleDelta = deriveCreateRoleDelta(input);
|
|
450
|
+
const inferredEngine = inferAgentEngineFromText(input);
|
|
438
451
|
// Always skip binding creating chat — new group activates via /activate
|
|
439
|
-
const res = await agentTools.createNewWorkspaceAgent(agentName, workspaceDir, roleDelta, chatId, {
|
|
452
|
+
const res = await agentTools.createNewWorkspaceAgent(agentName, workspaceDir, roleDelta, chatId, {
|
|
453
|
+
skipChatBinding: true,
|
|
454
|
+
engine: inferredEngine,
|
|
455
|
+
});
|
|
440
456
|
if (!res.ok) {
|
|
441
457
|
await bot.sendMessage(chatId, `❌ 创建 Agent 失败: ${res.error}`);
|
|
442
458
|
return true;
|
|
443
459
|
}
|
|
444
460
|
const data = res.data || {};
|
|
445
461
|
const projName = projectNameFromResult(data, agentName);
|
|
462
|
+
const engineTip = data.project && data.project.engine ? `\n引擎: ${data.project.engine}` : '';
|
|
446
463
|
if (data.projectKey && pendingActivations) {
|
|
447
464
|
pendingActivations.set(data.projectKey, {
|
|
448
465
|
agentKey: data.projectKey, agentName: projName, cwd: data.cwd,
|
|
@@ -450,7 +467,7 @@ function createCommandRouter(deps) {
|
|
|
450
467
|
});
|
|
451
468
|
}
|
|
452
469
|
await bot.sendMessage(chatId,
|
|
453
|
-
`✅ Agent「${projName}」已创建\n目录: ${data.cwd || '(未知)'}\n\n` +
|
|
470
|
+
`✅ Agent「${projName}」已创建\n目录: ${data.cwd || '(未知)'}${engineTip}\n\n` +
|
|
454
471
|
`**下一步**: 在新群里发送 \`/activate\` 完成绑定(30分钟内有效)`
|
|
455
472
|
);
|
|
456
473
|
return true;
|
|
@@ -458,14 +475,15 @@ function createCommandRouter(deps) {
|
|
|
458
475
|
|
|
459
476
|
if (wantsBind) {
|
|
460
477
|
const agentName = deriveAgentName(input, workspaceDir);
|
|
461
|
-
const
|
|
478
|
+
const inferredEngine = inferAgentEngineFromText(input);
|
|
479
|
+
const res = await agentTools.bindAgentToChat(chatId, agentName, workspaceDir || null, { engine: inferredEngine });
|
|
462
480
|
if (!res.ok) {
|
|
463
481
|
await bot.sendMessage(chatId, `❌ 绑定失败: ${res.error}`);
|
|
464
482
|
return true;
|
|
465
483
|
}
|
|
466
484
|
const data = res.data || {};
|
|
467
485
|
const projName = projectNameFromResult(data, agentName);
|
|
468
|
-
if (data.cwd) attachOrCreateSession(chatId, normalizeCwd(data.cwd), projName);
|
|
486
|
+
if (data.cwd) attachOrCreateSession(chatId, normalizeCwd(data.cwd), projName, (data.project && data.project.engine) || getDefaultEngine());
|
|
469
487
|
await bot.sendMessage(chatId, `✅ 已绑定 Agent\n名称: ${projName}\n目录: ${data.cwd || '(未知)'}`);
|
|
470
488
|
return true;
|
|
471
489
|
}
|
|
@@ -504,8 +522,10 @@ function createCommandRouter(deps) {
|
|
|
504
522
|
const proj = config.projects[mappedKey];
|
|
505
523
|
const projCwd = normalizeCwd(proj.cwd);
|
|
506
524
|
const cur = loadState().sessions?.[chatId];
|
|
507
|
-
|
|
508
|
-
|
|
525
|
+
const curEngine = String((cur && cur.engine) || getDefaultEngine()).toLowerCase();
|
|
526
|
+
const projEngine = String((proj && proj.engine) || getDefaultEngine()).toLowerCase();
|
|
527
|
+
if (!cur || cur.cwd !== projCwd || curEngine !== projEngine) {
|
|
528
|
+
attachOrCreateSession(chatId, projCwd, proj.name || mappedKey, proj.engine || getDefaultEngine());
|
|
509
529
|
}
|
|
510
530
|
}
|
|
511
531
|
|
|
@@ -551,6 +571,7 @@ function createCommandRouter(deps) {
|
|
|
551
571
|
'/agent edit — 编辑当前 Agent 角色',
|
|
552
572
|
'/agent unbind — 解绑当前群',
|
|
553
573
|
'/agent reset — 重置当前 Agent 角色',
|
|
574
|
+
'/agent soul [repair] — 查看/修复 Agent Soul 身份层',
|
|
554
575
|
'',
|
|
555
576
|
'📂 Session 管理:',
|
|
556
577
|
'/new [path] [name] — 新建会话',
|
|
@@ -564,7 +585,7 @@ function createCommandRouter(deps) {
|
|
|
564
585
|
'/undo <hash> — 回退到指定 git checkpoint',
|
|
565
586
|
'/quit — 结束会话,重新加载 MCP/配置',
|
|
566
587
|
'',
|
|
567
|
-
`⚙️ /model [${currentModel}] /provider [${currentProvider}] /status /tasks /run /budget /reload`,
|
|
588
|
+
`⚙️ /model [${currentModel}] /engine [${getDefaultEngine()}] /provider [${currentProvider}] /distill-model /status /tasks /run /budget /reload /mentor`,
|
|
568
589
|
'🧩 /TeamTask create <agent> <目标> [--scope <id>] · /TeamTask · /TeamTask <id>',
|
|
569
590
|
'🧠 /memory — 记忆统计 · /memory <关键词> — 搜索事实',
|
|
570
591
|
'🧬 /skill-evo — 查看/处理技能演化队列',
|
|
@@ -604,7 +625,7 @@ function createCommandRouter(deps) {
|
|
|
604
625
|
if (quickAgent && !quickAgent.rest) {
|
|
605
626
|
const { key, proj } = quickAgent;
|
|
606
627
|
const projCwd = normalizeCwd(proj.cwd);
|
|
607
|
-
attachOrCreateSession(chatId, projCwd, proj.name || key);
|
|
628
|
+
attachOrCreateSession(chatId, projCwd, proj.name || key, proj.engine || getDefaultEngine());
|
|
608
629
|
log('INFO', `Agent switch via nickname: ${key} (${projCwd})`);
|
|
609
630
|
await bot.sendMessage(chatId, `${proj.icon || '🤖'} ${proj.name || key} 在线`);
|
|
610
631
|
return;
|
|
@@ -33,22 +33,31 @@ projects:
|
|
|
33
33
|
|
|
34
34
|
heartbeat:
|
|
35
35
|
tasks:
|
|
36
|
-
# 认知蒸馏:有偏好信号才触发,
|
|
36
|
+
# 认知蒸馏:有偏好信号才触发,2小时冷却,仅在用户闲置时执行
|
|
37
37
|
- name: cognitive-distill
|
|
38
38
|
type: script
|
|
39
39
|
command: node ~/.metame/distill.js
|
|
40
|
-
interval:
|
|
40
|
+
interval: 2h
|
|
41
41
|
precondition: "test -s ~/.metame/raw_signals.jsonl"
|
|
42
42
|
require_idle: true
|
|
43
43
|
notify: false
|
|
44
44
|
enabled: true
|
|
45
45
|
|
|
46
|
-
# 记忆提取:扫描未分析 session,提取长期事实和会话标签,4
|
|
46
|
+
# 记忆提取:扫描未分析 session,提取长期事实和会话标签,4小时冷却
|
|
47
47
|
- name: memory-extract
|
|
48
48
|
type: script
|
|
49
49
|
command: node ~/.metame/memory-extract.js
|
|
50
50
|
interval: 4h
|
|
51
|
-
timeout:
|
|
51
|
+
timeout: 1800
|
|
52
|
+
require_idle: true
|
|
53
|
+
notify: false
|
|
54
|
+
enabled: true
|
|
55
|
+
|
|
56
|
+
# 记忆垃圾回收:每天 02:00 清理过期/重复记忆
|
|
57
|
+
- name: memory-gc
|
|
58
|
+
type: script
|
|
59
|
+
command: node ~/.metame/memory-gc.js
|
|
60
|
+
at: "02:00"
|
|
52
61
|
require_idle: true
|
|
53
62
|
notify: false
|
|
54
63
|
enabled: true
|
|
@@ -63,6 +72,15 @@ heartbeat:
|
|
|
63
72
|
notify: false
|
|
64
73
|
enabled: true
|
|
65
74
|
|
|
75
|
+
# 自我反思:每天 23:00 扫描纠错信号,提炼成长模式
|
|
76
|
+
- name: self-reflect
|
|
77
|
+
type: script
|
|
78
|
+
command: node ~/.metame/self-reflect.js
|
|
79
|
+
at: "23:00"
|
|
80
|
+
require_idle: true
|
|
81
|
+
notify: false
|
|
82
|
+
enabled: true
|
|
83
|
+
|
|
66
84
|
# 夜间记忆蒸馏:每天 01:00 提炼热区事实为决策与经验文档
|
|
67
85
|
- name: nightly-reflect
|
|
68
86
|
type: script
|
|
@@ -134,3 +152,9 @@ daemon:
|
|
|
134
152
|
# Mobile users can't click "allow" — so we pre-authorize everything.
|
|
135
153
|
# Security relies on allowed_chat_ids whitelist, not tool restrictions.
|
|
136
154
|
dangerously_skip_permissions: true
|
|
155
|
+
mentor:
|
|
156
|
+
enabled: false
|
|
157
|
+
friction_level: 3
|
|
158
|
+
mode: gentle
|
|
159
|
+
exclude_agents: [personal, xianyu]
|
|
160
|
+
emotion_keywords_extra: []
|
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const os = require('os');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const { execSync } = require('child_process');
|
|
7
|
+
|
|
8
|
+
const CODEX_TOOL_MAP = Object.freeze({
|
|
9
|
+
command_execution: 'Bash',
|
|
10
|
+
file_change: 'Write',
|
|
11
|
+
file_read: 'Read',
|
|
12
|
+
mcp_tool_call: 'MCP',
|
|
13
|
+
web_search: 'WebSearch',
|
|
14
|
+
web_fetch: 'WebFetch',
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
function normalizeEngineName(name) {
|
|
18
|
+
const text = String(name || '').trim().toLowerCase();
|
|
19
|
+
return text === 'codex' ? 'codex' : 'claude';
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function resolveBinary(engineName, deps = {}) {
|
|
23
|
+
const engine = normalizeEngineName(engineName);
|
|
24
|
+
const home = deps.HOME || os.homedir();
|
|
25
|
+
const fsMod = deps.fs || fs;
|
|
26
|
+
const pathMod = deps.path || path;
|
|
27
|
+
const execSyncFn = deps.execSync || execSync;
|
|
28
|
+
|
|
29
|
+
const key = engine === 'codex' ? 'codex' : 'claude';
|
|
30
|
+
const cmd = process.platform === 'win32' ? `where ${key}` : `which ${key} 2>/dev/null`;
|
|
31
|
+
try {
|
|
32
|
+
const lines = execSyncFn(cmd, { encoding: 'utf8', timeout: 3000, ...(process.platform === 'win32' ? { windowsHide: true } : {}) })
|
|
33
|
+
.split('\n').map(l => l.trim()).filter(Boolean);
|
|
34
|
+
// On Windows prefer .cmd wrapper (reliably executable by spawn)
|
|
35
|
+
const preferred = process.platform === 'win32'
|
|
36
|
+
? (lines.find(l => l.toLowerCase().endsWith(`${key}.cmd`)) || lines[0])
|
|
37
|
+
: lines[0];
|
|
38
|
+
if (preferred) return preferred;
|
|
39
|
+
} catch { /* fallback */ }
|
|
40
|
+
|
|
41
|
+
const candidates = engine === 'codex'
|
|
42
|
+
? [
|
|
43
|
+
pathMod.join(home, '.local', 'bin', 'codex'),
|
|
44
|
+
'/usr/local/bin/codex',
|
|
45
|
+
'/opt/homebrew/bin/codex',
|
|
46
|
+
]
|
|
47
|
+
: [
|
|
48
|
+
pathMod.join(home, '.local', 'bin', 'claude'),
|
|
49
|
+
pathMod.join(home, '.npm-global', 'bin', 'claude'),
|
|
50
|
+
'/usr/local/bin/claude',
|
|
51
|
+
'/opt/homebrew/bin/claude',
|
|
52
|
+
];
|
|
53
|
+
for (const p of candidates) {
|
|
54
|
+
if (fsMod.existsSync(p)) return p;
|
|
55
|
+
}
|
|
56
|
+
return key;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Single source of truth for all per-engine model config.
|
|
60
|
+
// All other code should read from here — no scattered hardcodes.
|
|
61
|
+
const ENGINE_MODEL_CONFIG = Object.freeze({
|
|
62
|
+
claude: {
|
|
63
|
+
main: 'sonnet', // default session model
|
|
64
|
+
distill: 'haiku', // background/cheap tasks
|
|
65
|
+
options: [ // /model button list
|
|
66
|
+
{ value: 'opus', label: 'opus · 最强' },
|
|
67
|
+
{ value: 'sonnet', label: 'sonnet · 均衡' },
|
|
68
|
+
{ value: 'haiku', label: 'haiku · 轻量' },
|
|
69
|
+
],
|
|
70
|
+
provider: 'anthropic',
|
|
71
|
+
hint: null,
|
|
72
|
+
},
|
|
73
|
+
codex: {
|
|
74
|
+
main: 'gpt-5.4', // recommended for most tasks (official default)
|
|
75
|
+
distill: 'gpt-5.1-codex-mini', // cost-effective mini
|
|
76
|
+
options: [ // quick-pick buttons (official model names)
|
|
77
|
+
{ value: 'gpt-5.4', label: 'gpt-5.4 · 推荐' },
|
|
78
|
+
{ value: 'gpt-5.3-codex', label: 'gpt-5.3-codex · 最新 Codex 专用' },
|
|
79
|
+
{ value: 'gpt-5.1-codex-max', label: 'gpt-5.1-codex-max · 长任务' },
|
|
80
|
+
{ value: 'gpt-5.1-codex-mini', label: 'gpt-5.1-codex-mini · 轻量' },
|
|
81
|
+
],
|
|
82
|
+
provider: 'openai',
|
|
83
|
+
hint: '或直接发送任意 OpenAI 模型名切换',
|
|
84
|
+
},
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
// Backward-compat aliases (derived, do not edit directly)
|
|
88
|
+
const ENGINE_DISTILL_MAP = Object.freeze(
|
|
89
|
+
Object.fromEntries(Object.entries(ENGINE_MODEL_CONFIG).map(([k, v]) => [k, v.distill]))
|
|
90
|
+
);
|
|
91
|
+
const ENGINE_DEFAULT_MODEL = Object.freeze(
|
|
92
|
+
Object.fromEntries(Object.entries(ENGINE_MODEL_CONFIG).map(([k, v]) => [k, v.main]))
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
function detectDefaultEngine(deps = {}) {
|
|
96
|
+
for (const engine of ['claude', 'codex']) {
|
|
97
|
+
const bin = resolveBinary(engine, deps);
|
|
98
|
+
if (bin !== engine) return engine; // resolveBinary found a real path
|
|
99
|
+
}
|
|
100
|
+
return 'claude'; // ultimate fallback
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function classifyEngineError(text) {
|
|
104
|
+
const msg = String(text || '').trim();
|
|
105
|
+
if (!msg) return null;
|
|
106
|
+
if (/(auth|unauthorized|login|api key|authentication|permission denied|forbidden|401|403)/i.test(msg)) {
|
|
107
|
+
return {
|
|
108
|
+
code: 'AUTH_REQUIRED',
|
|
109
|
+
message: '认证失败,请先执行 `codex login`(或配置 OPENAI_API_KEY)后重试。',
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
if (/(rate.?limit|too many requests|quota|429)/i.test(msg)) {
|
|
113
|
+
return {
|
|
114
|
+
code: 'RATE_LIMIT',
|
|
115
|
+
message: '请求频率或配额受限,请稍后重试。',
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
return {
|
|
119
|
+
code: 'EXEC_FAILURE',
|
|
120
|
+
message: msg,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function parseJsonLine(line) {
|
|
125
|
+
try {
|
|
126
|
+
return JSON.parse(line);
|
|
127
|
+
} catch {
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function parseClaudeStreamEvent(line) {
|
|
133
|
+
const raw = parseJsonLine(line);
|
|
134
|
+
if (!raw || typeof raw !== 'object') return [];
|
|
135
|
+
|
|
136
|
+
const out = [];
|
|
137
|
+
if (raw.type === 'assistant' && raw.message && Array.isArray(raw.message.content)) {
|
|
138
|
+
for (const block of raw.message.content) {
|
|
139
|
+
if (!block) continue;
|
|
140
|
+
if (block.type === 'text' && block.text) {
|
|
141
|
+
out.push({ type: 'text', text: String(block.text), raw });
|
|
142
|
+
} else if (block.type === 'tool_use') {
|
|
143
|
+
out.push({
|
|
144
|
+
type: 'tool_use',
|
|
145
|
+
toolName: block.name || 'Tool',
|
|
146
|
+
toolInput: block.input || {},
|
|
147
|
+
raw,
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
if (raw.type === 'system' && raw.subtype === 'init' && raw.session_id) {
|
|
153
|
+
out.push({ type: 'session', sessionId: String(raw.session_id), raw });
|
|
154
|
+
}
|
|
155
|
+
if (raw.type === 'result') {
|
|
156
|
+
if (raw.session_id) out.push({ type: 'session', sessionId: String(raw.session_id), raw });
|
|
157
|
+
if (raw.result) out.push({ type: 'text', text: String(raw.result), raw });
|
|
158
|
+
out.push({ type: 'done', usage: raw.usage || null, raw });
|
|
159
|
+
}
|
|
160
|
+
if (raw.type === 'content_block_start' || raw.type === 'content_block_delta') {
|
|
161
|
+
out.push({ type: 'tool_result', raw });
|
|
162
|
+
}
|
|
163
|
+
if (raw.type === 'error') {
|
|
164
|
+
const classified = classifyEngineError(raw.error || raw.message || '');
|
|
165
|
+
if (classified) out.push({ type: 'error', ...classified, raw });
|
|
166
|
+
}
|
|
167
|
+
return out;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function parseCodexStreamEvent(line) {
|
|
171
|
+
const raw = parseJsonLine(line);
|
|
172
|
+
if (!raw || typeof raw !== 'object') return [];
|
|
173
|
+
|
|
174
|
+
const out = [];
|
|
175
|
+
if (raw.type === 'thread.started' && raw.thread_id) {
|
|
176
|
+
out.push({ type: 'session', sessionId: String(raw.thread_id), raw });
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if ((raw.type === 'item.started' || raw.type === 'item.completed') && raw.item && raw.item.type) {
|
|
180
|
+
const itemType = String(raw.item.type);
|
|
181
|
+
const mapped = CODEX_TOOL_MAP[itemType] || itemType;
|
|
182
|
+
if (mapped && mapped !== 'reasoning' && itemType !== 'agent_message') {
|
|
183
|
+
if (raw.type === 'item.started') {
|
|
184
|
+
out.push({
|
|
185
|
+
type: 'tool_use',
|
|
186
|
+
toolName: mapped,
|
|
187
|
+
toolInput: {
|
|
188
|
+
command: raw.item.command || '',
|
|
189
|
+
file_path: raw.item.path || raw.item.file_path || '',
|
|
190
|
+
},
|
|
191
|
+
raw,
|
|
192
|
+
});
|
|
193
|
+
} else {
|
|
194
|
+
out.push({ type: 'tool_result', toolName: mapped, raw });
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
if (raw.type === 'item.completed' && itemType === 'agent_message' && raw.item.text) {
|
|
198
|
+
out.push({ type: 'text', text: String(raw.item.text), raw });
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (raw.type === 'turn.completed') {
|
|
203
|
+
out.push({ type: 'done', usage: raw.usage || null, raw });
|
|
204
|
+
}
|
|
205
|
+
if (raw.type === 'error') {
|
|
206
|
+
const classified = classifyEngineError(raw.error || raw.message || '');
|
|
207
|
+
if (classified) out.push({ type: 'error', ...classified, raw });
|
|
208
|
+
}
|
|
209
|
+
return out;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function buildClaudeArgs(options = {}) {
|
|
213
|
+
const { model = ENGINE_MODEL_CONFIG.claude.main, readOnly = false, daemonCfg = {}, session = {} } = options;
|
|
214
|
+
const args = ['-p', '--model', model];
|
|
215
|
+
if (readOnly) {
|
|
216
|
+
const readOnlyTools = ['Read', 'Glob', 'Grep', 'WebSearch', 'WebFetch', 'Task'];
|
|
217
|
+
for (const tool of readOnlyTools) args.push('--allowedTools', tool);
|
|
218
|
+
} else {
|
|
219
|
+
// Always bypass permission prompts — desktop users run in trusted local context,
|
|
220
|
+
// mobile users cannot click dialogs. Security relies on allowed_chat_ids whitelist.
|
|
221
|
+
args.push('--dangerously-skip-permissions');
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (session.id === '__continue__') {
|
|
225
|
+
args.push('--continue');
|
|
226
|
+
} else if (session.started && session.id) {
|
|
227
|
+
args.push('--resume', session.id);
|
|
228
|
+
} else if (session.id) {
|
|
229
|
+
args.push('--session-id', session.id);
|
|
230
|
+
}
|
|
231
|
+
return args;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function buildCodexArgs(options = {}) {
|
|
235
|
+
const { model = ENGINE_MODEL_CONFIG.codex.main, readOnly = false, daemonCfg = {}, session = {}, cwd } = options;
|
|
236
|
+
const isResume = (session && session.started && session.id && session.id !== '__continue__');
|
|
237
|
+
const args = isResume
|
|
238
|
+
? ['exec', 'resume', session.id]
|
|
239
|
+
: ['exec'];
|
|
240
|
+
|
|
241
|
+
args.push('--json', '--skip-git-repo-check');
|
|
242
|
+
if (model) args.push('-m', model);
|
|
243
|
+
// -C (cwd) is only supported on fresh exec, not resume
|
|
244
|
+
if (cwd && !isResume) args.push('-C', cwd);
|
|
245
|
+
|
|
246
|
+
// Permission flags are only valid on fresh exec, not resume.
|
|
247
|
+
// `codex exec resume` does not accept -s or --dangerously-bypass-approvals-and-sandbox.
|
|
248
|
+
if (!isResume) {
|
|
249
|
+
if (readOnly) {
|
|
250
|
+
args.push('-s', 'read-only');
|
|
251
|
+
} else {
|
|
252
|
+
// Mobile sessions: user cannot click permission dialogs.
|
|
253
|
+
// Security relies on allowed_chat_ids whitelist, not tool restrictions.
|
|
254
|
+
args.push('--dangerously-bypass-approvals-and-sandbox');
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// "-" means prompt is read from stdin.
|
|
259
|
+
args.push('-');
|
|
260
|
+
return args;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function createEngineRuntimeFactory(deps = {}) {
|
|
264
|
+
const home = deps.HOME || os.homedir();
|
|
265
|
+
const claudeBin = deps.CLAUDE_BIN || resolveBinary('claude', { ...deps, HOME: home });
|
|
266
|
+
const codexBin = deps.CODEX_BIN || resolveBinary('codex', { ...deps, HOME: home });
|
|
267
|
+
const getActiveProviderEnv = typeof deps.getActiveProviderEnv === 'function'
|
|
268
|
+
? deps.getActiveProviderEnv
|
|
269
|
+
: (() => ({}));
|
|
270
|
+
|
|
271
|
+
return function getEngineRuntime(engineName) {
|
|
272
|
+
const engine = normalizeEngineName(engineName);
|
|
273
|
+
if (engine === 'codex') {
|
|
274
|
+
return {
|
|
275
|
+
name: 'codex',
|
|
276
|
+
binary: codexBin,
|
|
277
|
+
defaultModel: ENGINE_MODEL_CONFIG.codex.main,
|
|
278
|
+
stdinBehavior: 'write-and-close',
|
|
279
|
+
killSignal: 'SIGTERM',
|
|
280
|
+
timeouts: { idleMs: 10 * 60 * 1000, toolMs: 25 * 60 * 1000, ceilingMs: 60 * 60 * 1000 },
|
|
281
|
+
buildArgs: buildCodexArgs,
|
|
282
|
+
buildEnv: ({ metameProject = '' } = {}) => {
|
|
283
|
+
const env = { ...process.env, METAME_PROJECT: metameProject };
|
|
284
|
+
// Unset CODEX_HOME if it points to a non-existent path (corrupted env var)
|
|
285
|
+
if (env.CODEX_HOME && !fs.existsSync(env.CODEX_HOME)) delete env.CODEX_HOME;
|
|
286
|
+
return env;
|
|
287
|
+
},
|
|
288
|
+
parseStreamEvent: parseCodexStreamEvent,
|
|
289
|
+
classifyError: classifyEngineError,
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
return {
|
|
293
|
+
name: 'claude',
|
|
294
|
+
binary: claudeBin,
|
|
295
|
+
defaultModel: ENGINE_MODEL_CONFIG.claude.main,
|
|
296
|
+
stdinBehavior: 'write-and-close',
|
|
297
|
+
killSignal: 'SIGTERM',
|
|
298
|
+
timeouts: { idleMs: 5 * 60 * 1000, toolMs: 25 * 60 * 1000, ceilingMs: 60 * 60 * 1000 },
|
|
299
|
+
buildArgs: buildClaudeArgs,
|
|
300
|
+
buildEnv: ({ metameProject = '' } = {}) => ({
|
|
301
|
+
...(() => {
|
|
302
|
+
const env = { ...process.env, ...getActiveProviderEnv(), METAME_PROJECT: metameProject };
|
|
303
|
+
delete env.CLAUDECODE;
|
|
304
|
+
return env;
|
|
305
|
+
})(),
|
|
306
|
+
}),
|
|
307
|
+
parseStreamEvent: parseClaudeStreamEvent,
|
|
308
|
+
classifyError: classifyEngineError,
|
|
309
|
+
};
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
module.exports = {
|
|
314
|
+
createEngineRuntimeFactory,
|
|
315
|
+
normalizeEngineName,
|
|
316
|
+
resolveBinary,
|
|
317
|
+
detectDefaultEngine,
|
|
318
|
+
ENGINE_MODEL_CONFIG,
|
|
319
|
+
ENGINE_DISTILL_MAP,
|
|
320
|
+
ENGINE_DEFAULT_MODEL,
|
|
321
|
+
_private: {
|
|
322
|
+
classifyEngineError,
|
|
323
|
+
parseClaudeStreamEvent,
|
|
324
|
+
parseCodexStreamEvent,
|
|
325
|
+
buildClaudeArgs,
|
|
326
|
+
buildCodexArgs,
|
|
327
|
+
},
|
|
328
|
+
};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const { classifyTaskUsage } = require('./usage-classifier');
|
|
4
|
+
const { normalizeModel } = require('./daemon-task-scheduler');
|
|
4
5
|
|
|
5
6
|
function createExecCommandHandler(deps) {
|
|
6
7
|
const {
|
|
@@ -23,6 +24,7 @@ function createExecCommandHandler(deps) {
|
|
|
23
24
|
createSession,
|
|
24
25
|
findSessionFile,
|
|
25
26
|
loadConfig,
|
|
27
|
+
getDistillModel,
|
|
26
28
|
} = deps;
|
|
27
29
|
|
|
28
30
|
function truncateOutput(output, maxLen = 4000) {
|
|
@@ -178,7 +180,7 @@ function createExecCommandHandler(deps) {
|
|
|
178
180
|
let taskPrompt = task.prompt;
|
|
179
181
|
if (precheck.context) taskPrompt += `\n\n以下是相关原始数据:\n\`\`\`\n${precheck.context}\n\`\`\``;
|
|
180
182
|
const fullPrompt = preamble + taskPrompt;
|
|
181
|
-
const model = task.model ||
|
|
183
|
+
const model = normalizeModel(task.model || getDistillModel());
|
|
182
184
|
const claudeArgs = ['-p', '--model', model, '--dangerously-skip-permissions'];
|
|
183
185
|
for (const t of (task.allowedTools || [])) claudeArgs.push('--allowedTools', t);
|
|
184
186
|
|
|
@@ -209,8 +211,9 @@ function createExecCommandHandler(deps) {
|
|
|
209
211
|
const proc = activeProcesses.get(chatId);
|
|
210
212
|
if (proc && proc.child) {
|
|
211
213
|
proc.aborted = true;
|
|
212
|
-
|
|
213
|
-
|
|
214
|
+
const signal = proc.killSignal || 'SIGTERM';
|
|
215
|
+
try { process.kill(-proc.child.pid, signal); } catch { proc.child.kill(signal); }
|
|
216
|
+
await bot.sendMessage(chatId, '⏹ Stopping current engine task...');
|
|
214
217
|
} else {
|
|
215
218
|
await bot.sendMessage(chatId, 'No active task to stop.');
|
|
216
219
|
}
|
|
@@ -228,7 +231,8 @@ function createExecCommandHandler(deps) {
|
|
|
228
231
|
const proc = activeProcesses.get(chatId);
|
|
229
232
|
if (proc && proc.child) {
|
|
230
233
|
proc.aborted = true;
|
|
231
|
-
|
|
234
|
+
const signal = proc.killSignal || 'SIGTERM';
|
|
235
|
+
try { process.kill(-proc.child.pid, signal); } catch { proc.child.kill(signal); }
|
|
232
236
|
}
|
|
233
237
|
const session = getSession(chatId);
|
|
234
238
|
const name = session ? getSessionName(session.id) : null;
|
|
@@ -244,6 +248,10 @@ function createExecCommandHandler(deps) {
|
|
|
244
248
|
await bot.sendMessage(chatId, '❌ No active session to compact.');
|
|
245
249
|
return true;
|
|
246
250
|
}
|
|
251
|
+
if (String(session.engine || '').toLowerCase() === 'codex') {
|
|
252
|
+
await bot.sendMessage(chatId, '⚠️ Codex 会话暂不支持 /compact,请继续在同一会话里对话。');
|
|
253
|
+
return true;
|
|
254
|
+
}
|
|
247
255
|
await bot.sendMessage(chatId, '🗜 Compacting session...');
|
|
248
256
|
|
|
249
257
|
// Step 1: Read conversation from JSONL (fast, no Claude needed)
|
|
@@ -298,9 +306,9 @@ function createExecCommandHandler(deps) {
|
|
|
298
306
|
digest = entry + digest;
|
|
299
307
|
}
|
|
300
308
|
|
|
301
|
-
// Step 3: Summarize with
|
|
309
|
+
// Step 3: Summarize with distill model (new process, no --resume, fast)
|
|
302
310
|
const daemonCfg = loadConfig().daemon || {};
|
|
303
|
-
const compactArgs = ['-p', '--model',
|
|
311
|
+
const compactArgs = ['-p', '--model', getDistillModel(), '--no-session-persistence'];
|
|
304
312
|
if (daemonCfg.dangerously_skip_permissions) compactArgs.push('--dangerously-skip-permissions');
|
|
305
313
|
const { output, error } = await spawnClaudeAsync(
|
|
306
314
|
compactArgs,
|
|
@@ -316,7 +324,7 @@ function createExecCommandHandler(deps) {
|
|
|
316
324
|
// Step 4: Create new session with the summary
|
|
317
325
|
const model = daemonCfg.model || 'opus';
|
|
318
326
|
const oldName = getSessionName(session.id);
|
|
319
|
-
const newSession = createSession(chatId, session.cwd, oldName ? oldName + ' (compacted)' : '');
|
|
327
|
+
const newSession = createSession(chatId, session.cwd, oldName ? oldName + ' (compacted)' : '', session.engine || 'claude');
|
|
320
328
|
const initArgs = ['-p', '--session-id', newSession.id, '--model', model];
|
|
321
329
|
if (daemonCfg.dangerously_skip_permissions) initArgs.push('--dangerously-skip-permissions');
|
|
322
330
|
const preamble = buildProfilePreamble();
|
package/scripts/daemon-notify.js
CHANGED
|
@@ -58,7 +58,43 @@ function createNotifier(deps) {
|
|
|
58
58
|
}
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
-
|
|
61
|
+
/**
|
|
62
|
+
* Send only to personal (non-agent-bound) chat IDs.
|
|
63
|
+
* Agent-bound group chats (those in chat_agent_map) are excluded.
|
|
64
|
+
* Falls back to fsIds[0] if no personal chats are found.
|
|
65
|
+
* Used for system notifications that should not spam every agent group.
|
|
66
|
+
*/
|
|
67
|
+
async function notifyPersonal(message) {
|
|
68
|
+
const config = getConfig();
|
|
69
|
+
const { telegramBridge, feishuBridge } = getBridges();
|
|
70
|
+
|
|
71
|
+
if (feishuBridge && feishuBridge.bot) {
|
|
72
|
+
const chatAgentMap = (config.feishu && config.feishu.chat_agent_map) || {};
|
|
73
|
+
const fsIds = (config.feishu && config.feishu.allowed_chat_ids) || [];
|
|
74
|
+
// Personal chats = allowed IDs not bound to any agent
|
|
75
|
+
const personalIds = fsIds.filter(id => !chatAgentMap[id]);
|
|
76
|
+
const targetIds = personalIds.length > 0 ? personalIds : fsIds.slice(0, 1);
|
|
77
|
+
for (const chatId of targetIds) {
|
|
78
|
+
try { await feishuBridge.bot.sendMessage(chatId, message); } catch (e) {
|
|
79
|
+
log('ERROR', `Feishu personal notify failed ${chatId}: ${e.message}`);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (telegramBridge && telegramBridge.bot) {
|
|
85
|
+
const tgAgentMap = (config.telegram && config.telegram.chat_agent_map) || {};
|
|
86
|
+
const tgIds = (config.telegram && config.telegram.allowed_chat_ids) || [];
|
|
87
|
+
const personalIds = tgIds.filter(id => !tgAgentMap[id]);
|
|
88
|
+
const targetIds = personalIds.length > 0 ? personalIds : tgIds.slice(0, 1);
|
|
89
|
+
for (const chatId of targetIds) {
|
|
90
|
+
try { await telegramBridge.bot.sendMarkdown(chatId, message); } catch (e) {
|
|
91
|
+
log('ERROR', `Telegram personal notify failed ${chatId}: ${e.message}`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return { notify, notifyAdmin, notifyPersonal };
|
|
62
98
|
}
|
|
63
99
|
|
|
64
100
|
module.exports = { createNotifier };
|