metame-cli 1.4.34 → 1.5.0

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 (44) hide show
  1. package/README.md +146 -32
  2. package/index.js +148 -9
  3. package/package.json +6 -3
  4. package/scripts/daemon-admin-commands.js +254 -9
  5. package/scripts/daemon-agent-commands.js +64 -6
  6. package/scripts/daemon-agent-tools.js +26 -5
  7. package/scripts/daemon-bridges.js +110 -20
  8. package/scripts/daemon-claude-engine.js +698 -239
  9. package/scripts/daemon-command-router.js +24 -8
  10. package/scripts/daemon-default.yaml +28 -4
  11. package/scripts/daemon-engine-runtime.js +275 -0
  12. package/scripts/daemon-exec-commands.js +10 -4
  13. package/scripts/daemon-notify.js +37 -1
  14. package/scripts/daemon-runtime-lifecycle.js +2 -1
  15. package/scripts/daemon-session-commands.js +52 -4
  16. package/scripts/daemon-session-store.js +2 -1
  17. package/scripts/daemon-task-scheduler.js +68 -38
  18. package/scripts/daemon-user-acl.js +26 -9
  19. package/scripts/daemon.js +81 -17
  20. package/scripts/distill.js +323 -18
  21. package/scripts/docs/agent-guide.md +12 -0
  22. package/scripts/docs/maintenance-manual.md +119 -0
  23. package/scripts/docs/pointer-map.md +88 -0
  24. package/scripts/feishu-adapter.js +6 -1
  25. package/scripts/hooks/stop-session-capture.js +243 -0
  26. package/scripts/memory-extract.js +100 -5
  27. package/scripts/memory-nightly-reflect.js +196 -11
  28. package/scripts/memory.js +134 -3
  29. package/scripts/mentor-engine.js +405 -0
  30. package/scripts/platform.js +2 -0
  31. package/scripts/providers.js +169 -21
  32. package/scripts/schema.js +12 -0
  33. package/scripts/session-analytics.js +245 -12
  34. package/scripts/skill-changelog.js +245 -0
  35. package/scripts/skill-evolution.js +288 -5
  36. package/scripts/usage-classifier.js +1 -1
  37. package/scripts/daemon-admin-commands.test.js +0 -333
  38. package/scripts/daemon-task-envelope.test.js +0 -59
  39. package/scripts/daemon-task-scheduler.test.js +0 -106
  40. package/scripts/reliability-core.test.js +0 -280
  41. package/scripts/skill-evolution.test.js +0 -113
  42. package/scripts/task-board.test.js +0 -83
  43. package/scripts/test_daemon.js +0 -1407
  44. package/scripts/utils.test.js +0 -192
@@ -5,6 +5,9 @@ const {
5
5
  CORE_USAGE_CATEGORIES,
6
6
  USAGE_CATEGORY_LABEL,
7
7
  } = require('./usage-classifier');
8
+ const { IS_WIN } = require('./platform');
9
+ let mentorEngine = null;
10
+ try { mentorEngine = require('./mentor-engine'); } catch { /* optional */ }
8
11
 
9
12
  function createAdminCommandHandler(deps) {
10
13
  const {
@@ -30,6 +33,8 @@ function createAdminCommandHandler(deps) {
30
33
  getMessageQueue,
31
34
  loadState,
32
35
  saveState,
36
+ getDefaultEngine = () => 'claude',
37
+ setDefaultEngine = () => {},
33
38
  } = deps;
34
39
 
35
40
  function resolveProjectKey(targetName, projects) {
@@ -92,6 +97,61 @@ function createAdminCommandHandler(deps) {
92
97
  return 'unspecified';
93
98
  }
94
99
 
100
+ function modeFromLevel(level) {
101
+ const n = Number(level);
102
+ if (!Number.isFinite(n)) return 'gentle';
103
+ if (n >= 8) return 'intense';
104
+ if (n >= 4) return 'active';
105
+ return 'gentle';
106
+ }
107
+
108
+ function parseDistillModelIntent(input) {
109
+ const text = String(input || '').trim();
110
+ if (!text || text.startsWith('/')) return null;
111
+ if (!/(蒸馏|distill|提炼|提纯)/i.test(text)) return null;
112
+ const setVerb = '(?:改成|改为|设为|设置|切到|切换到|换成|改用|使用|用|set|switch|use)';
113
+ if (!(new RegExp(setVerb, 'i')).test(text)) return null;
114
+
115
+ const explicitModel = text.match(new RegExp(`(?:蒸馏模型|模型|distill\\s*model|model)\\s*(?:${setVerb}|to|is)?\\s*[::]?\\s*([a-zA-Z0-9._-]{2,80})`, 'i'));
116
+ if (explicitModel) return { model: explicitModel[1] };
117
+
118
+ if (/(蒸馏模型|模型|distill\s*model|model)/i.test(text)) {
119
+ const quotedModel = text.match(/[“"'「]([a-zA-Z0-9._-]{2,80})[”"'」]/);
120
+ if (quotedModel) return { model: quotedModel[1] };
121
+ }
122
+
123
+ const knownToken = text.match(new RegExp(`${setVerb}\\s*(?:为|成|到|to)?\\s*[::]?\\s*(gpt-5\\.1-codex-mini|gpt-5-mini|haiku|sonnet|opus|5\\.1mini|5mini|codex-mini)\\b`, 'i'));
124
+ if (knownToken) return { model: knownToken[1] };
125
+
126
+ return null;
127
+ }
128
+
129
+ function ensureMentorConfig(cfg) {
130
+ if (!cfg.daemon) cfg.daemon = {};
131
+ if (!cfg.daemon.mentor || typeof cfg.daemon.mentor !== 'object') {
132
+ cfg.daemon.mentor = {};
133
+ }
134
+ const mentor = cfg.daemon.mentor;
135
+ if (typeof mentor.enabled !== 'boolean') mentor.enabled = false;
136
+ if (!Number.isFinite(Number(mentor.friction_level))) mentor.friction_level = 3;
137
+ if (!mentor.mode || !['gentle', 'active', 'intense'].includes(String(mentor.mode))) {
138
+ mentor.mode = modeFromLevel(mentor.friction_level);
139
+ }
140
+ if (!Array.isArray(mentor.exclude_agents)) mentor.exclude_agents = ['personal', 'xianyu'];
141
+ if (!Array.isArray(mentor.emotion_keywords_extra)) mentor.emotion_keywords_extra = [];
142
+ return mentor;
143
+ }
144
+
145
+ function hasCli(execSyncFn, bin) {
146
+ try {
147
+ const cmd = process.platform === 'win32' ? `where ${bin}` : `which ${bin}`;
148
+ execSyncFn(cmd, { encoding: 'utf8' });
149
+ return true;
150
+ } catch {
151
+ return false;
152
+ }
153
+ }
154
+
95
155
  async function handleAdminCommand(ctx) {
96
156
  const { bot, chatId, text } = ctx;
97
157
  const state = ctx.state || {};
@@ -195,17 +255,69 @@ function createAdminCommandHandler(deps) {
195
255
  return { handled: true, config };
196
256
  }
197
257
 
258
+ // /skill-evo approve <id> — approve a workflow_proposal and dispatch skill creation
259
+ const approveMatch = arg.match(/^approve\s+(\S+)$/i);
260
+ if (approveMatch) {
261
+ const id = approveMatch[1];
262
+ // Find the queue item (search both pending and notified states)
263
+ const item = skillEvolution.listQueueItems({ status: ['pending', 'notified'], limit: 200 })
264
+ .find(i => i.id === id && i.type === 'workflow_proposal');
265
+ if (!item) {
266
+ await bot.sendMessage(chatId, `❌ 未找到 workflow_proposal: ${id}`);
267
+ return { handled: true, config };
268
+ }
269
+ // Build skill-creator prefilled prompt
270
+ const toolsSig = (item.tools_signature || []).join(', ');
271
+ const prefilledPrompt = [
272
+ '/skill-creator',
273
+ `创建一个新技能,自动化以下工作流:`,
274
+ `工作流模式: ${item.search_hint || item.reason}`,
275
+ toolsSig ? `常用工具: ${toolsSig}` : '',
276
+ item.example_prompt ? `用户示例: "${item.example_prompt}"` : '',
277
+ `该技能应封装这个多步工作流为单一可调用技能。`,
278
+ ].filter(Boolean).join('\n');
279
+ // Dispatch to metame agent for skill creation (async — must not block event loop)
280
+ try {
281
+ const HOME = require('os').homedir();
282
+ const dispatchBin = require('path').join(HOME, '.metame', 'bin', 'dispatch_to');
283
+ const { execFile } = require('child_process');
284
+ const { promisify } = require('util');
285
+ const execFileAsync = promisify(execFile);
286
+ // dispatch_to is a Node.js script; on Windows shebang resolution is unavailable,
287
+ // so invoke via node explicitly for cross-platform safety
288
+ const cmd = IS_WIN ? process.execPath : dispatchBin;
289
+ const cmdArgs = IS_WIN ? [dispatchBin, 'metame', prefilledPrompt] : ['metame', prefilledPrompt];
290
+ await execFileAsync(cmd, cmdArgs, { encoding: 'utf8', timeout: 15000 });
291
+ // Mark installed only after successful dispatch
292
+ skillEvolution.resolveQueueItemById(id, 'installed');
293
+ await bot.sendMessage(chatId, `✅ 已派发给 Jarvis 创建技能,完成后会通知你\n工作流: ${item.search_hint || item.reason}`);
294
+ } catch (e) {
295
+ // Dispatch failed — don't mark installed, keep in queue
296
+ await bot.sendMessage(chatId, `⚠️ 自动派发失败: ${e.message}\n提案仍在队列中,可重试: /skill-evo approve ${id}`);
297
+ }
298
+ return { handled: true, config };
299
+ }
300
+
198
301
  const dismissMatch = arg.match(/^(?:dismiss|skip|ignored?)\s+(\S+)$/i);
199
302
  if (dismissMatch) {
200
303
  const id = dismissMatch[1];
304
+ // Check if this is a workflow_proposal — if so, reset the sketch
305
+ const item = skillEvolution.listQueueItems({ status: ['pending', 'notified'], limit: 200 })
306
+ .find(i => i.id === id);
201
307
  const ok = skillEvolution.resolveQueueItemById
202
308
  ? skillEvolution.resolveQueueItemById(id, 'dismissed')
203
309
  : false;
310
+ // Reset workflow sketch so it can re-accumulate
311
+ if (ok && item && item.type === 'workflow_proposal' && item.workflow_sketch_id) {
312
+ if (skillEvolution.resetWorkflowSketch) {
313
+ skillEvolution.resetWorkflowSketch(item.workflow_sketch_id);
314
+ }
315
+ }
204
316
  await bot.sendMessage(chatId, ok ? `✅ 已标记 dismissed: ${id}` : `❌ 未找到可处理项: ${id}`);
205
317
  return { handled: true, config };
206
318
  }
207
319
 
208
- await bot.sendMessage(chatId, '用法: /skill-evo list | /skill-evo done <id> | /skill-evo dismiss <id>');
320
+ await bot.sendMessage(chatId, '用法: /skill-evo list | /skill-evo done <id> | /skill-evo dismiss <id> | /skill-evo approve <id>');
209
321
  return { handled: true, config };
210
322
  }
211
323
 
@@ -635,6 +747,68 @@ function createAdminCommandHandler(deps) {
635
747
  return { handled: true, config };
636
748
  }
637
749
 
750
+ if (text === '/mentor' || text.startsWith('/mentor ')) {
751
+ try {
752
+ backupConfig();
753
+ const cfg = yaml.load(fs.readFileSync(CONFIG_FILE, 'utf8')) || {};
754
+ const mentorCfg = ensureMentorConfig(cfg);
755
+ const arg = text.slice('/mentor'.length).trim();
756
+
757
+ if (!arg || arg === 'status') {
758
+ const status = mentorEngine && typeof mentorEngine.getRuntimeStatus === 'function'
759
+ ? mentorEngine.getRuntimeStatus()
760
+ : { debt_count: 0, cooldown_remaining_ms: 0 };
761
+ const mode = String(mentorCfg.mode || modeFromLevel(mentorCfg.friction_level));
762
+ const level = Number(mentorCfg.friction_level || 0);
763
+ const cooldownSec = Math.ceil((Number(status.cooldown_remaining_ms) || 0) / 1000);
764
+ const lines = [
765
+ `Mentor: ${mentorCfg.enabled ? 'ON' : 'OFF'}`,
766
+ `Mode: ${mode}`,
767
+ `Friction level: ${level}`,
768
+ `Debts: ${status.debt_count || 0}`,
769
+ `Emotion cooldown: ${cooldownSec > 0 ? `${cooldownSec}s` : '0s'}`,
770
+ 'Zone: n/a (runtime)',
771
+ ];
772
+ await bot.sendMessage(chatId, lines.join('\n'));
773
+ return { handled: true, config };
774
+ }
775
+
776
+ if (arg === 'on' || arg === 'off') {
777
+ mentorCfg.enabled = arg === 'on';
778
+ writeConfigSafe(cfg);
779
+ config = loadConfig();
780
+ await bot.sendMessage(chatId, mentorCfg.enabled
781
+ ? '✅ Mentor mode enabled.'
782
+ : '✅ Mentor mode disabled.');
783
+ return { handled: true, config };
784
+ }
785
+
786
+ const mLevel = arg.match(/^level\s+(-?\d{1,2})$/i);
787
+ if (mLevel) {
788
+ let level = Number(mLevel[1]);
789
+ if (!Number.isFinite(level)) level = 3;
790
+ level = Math.max(0, Math.min(10, Math.floor(level)));
791
+ mentorCfg.friction_level = level;
792
+ mentorCfg.mode = modeFromLevel(level);
793
+ writeConfigSafe(cfg);
794
+ config = loadConfig();
795
+ await bot.sendMessage(chatId, `✅ Mentor level set to ${level} (${mentorCfg.mode}).`);
796
+ return { handled: true, config };
797
+ }
798
+
799
+ await bot.sendMessage(chatId, [
800
+ '用法:',
801
+ '/mentor on',
802
+ '/mentor off',
803
+ '/mentor level <0-10>',
804
+ '/mentor status',
805
+ ].join('\n'));
806
+ } catch (e) {
807
+ await bot.sendMessage(chatId, `❌ Mentor command failed: ${e.message}`);
808
+ }
809
+ return { handled: true, config };
810
+ }
811
+
638
812
  if (text === '/reload') {
639
813
  if (global._metameReload) {
640
814
  const r = global._metameReload();
@@ -727,6 +901,10 @@ function createAdminCommandHandler(deps) {
727
901
  const validModels = ['sonnet', 'opus', 'haiku'];
728
902
  const checks = [];
729
903
  let issues = 0;
904
+ const activeProvider = providerMod && typeof providerMod.getActiveName === 'function'
905
+ ? providerMod.getActiveName()
906
+ : 'anthropic';
907
+ const isCustomProvider = activeProvider !== 'anthropic';
730
908
 
731
909
  let cfg = null;
732
910
  try {
@@ -738,21 +916,34 @@ function createAdminCommandHandler(deps) {
738
916
  }
739
917
 
740
918
  const m = (cfg && cfg.daemon && cfg.daemon.model) || 'opus';
741
- if (validModels.includes(m)) {
919
+ const modelOk = isCustomProvider
920
+ ? /^[a-zA-Z0-9._-]{2,80}$/.test(String(m || '').trim())
921
+ : validModels.includes(m);
922
+ if (modelOk) {
742
923
  checks.push(`✅ 模型: ${m}`);
743
924
  } else {
744
- checks.push(`❌ 模型: ${m} (无效)`);
925
+ checks.push(`❌ 模型: ${m} (${isCustomProvider ? '格式无效' : '无效'})`);
745
926
  issues++;
746
927
  }
747
928
 
748
- try {
749
- execSync(process.platform === 'win32' ? 'where claude' : 'which claude', { encoding: 'utf8' });
750
- checks.push('✅ Claude CLI');
751
- } catch {
752
- checks.push('❌ Claude CLI 未找到');
929
+ const hasClaude = hasCli(execSync, 'claude');
930
+ const hasCodex = hasCli(execSync, 'codex');
931
+ checks.push(hasClaude ? '✅ Claude CLI' : '⚠️ Claude CLI 未找到');
932
+ checks.push(hasCodex ? '✅ Codex CLI' : '⚠️ Codex CLI 未找到');
933
+
934
+ const currentEngine = getDefaultEngine() === 'codex' ? 'codex' : 'claude';
935
+ if (currentEngine === 'claude' && !hasClaude) {
936
+ checks.push('❌ 当前默认引擎是 claude,但 Claude CLI 不可用');
937
+ issues++;
938
+ }
939
+ if (currentEngine === 'codex' && !hasCodex) {
940
+ checks.push('❌ 当前默认引擎是 codex,但 Codex CLI 不可用');
753
941
  issues++;
754
942
  }
755
943
 
944
+ checks.push(`✅ 默认引擎: ${currentEngine}`);
945
+ checks.push(`✅ Provider: ${activeProvider}${isCustomProvider ? ' (custom)' : ''}`);
946
+
756
947
  const bakFile = CONFIG_FILE + '.bak';
757
948
  const hasBak = fs.existsSync(bakFile);
758
949
  checks.push(hasBak ? '✅ 有备份' : '⚠️ 无备份');
@@ -867,10 +1058,64 @@ function createAdminCommandHandler(deps) {
867
1058
  return { handled: true, config };
868
1059
  }
869
1060
 
1061
+ // /engine [name] — show or switch default engine (claude/codex)
1062
+ if (text === '/engine' || text.startsWith('/engine ')) {
1063
+ const arg = text.slice('/engine'.length).trim().toLowerCase();
1064
+ if (!arg) {
1065
+ const cur = getDefaultEngine();
1066
+ const distill = providerMod ? providerMod.getDistillModel() : '(unknown)';
1067
+ await bot.sendMessage(chatId, `🔧 当前引擎: ${cur}\n🧪 蒸馏模型: ${distill}\n\n用法: /engine claude 或 /engine codex`);
1068
+ return { handled: true, config };
1069
+ }
1070
+ if (arg !== 'claude' && arg !== 'codex') {
1071
+ await bot.sendMessage(chatId, `❌ 不支持的引擎: ${arg}\n可选: claude, codex`);
1072
+ return { handled: true, config };
1073
+ }
1074
+ setDefaultEngine(arg);
1075
+ const distill = providerMod ? providerMod.getDistillModel() : '(unknown)';
1076
+ await bot.sendMessage(chatId, `✅ 默认引擎: ${arg}\n🧪 蒸馏模型已同步: ${distill}`);
1077
+ return { handled: true, config };
1078
+ }
1079
+
1080
+ // /distill-model [name] — show or update distill model
1081
+ if (text === '/distill-model' || text.startsWith('/distill-model ')) {
1082
+ if (!providerMod || typeof providerMod.getDistillModel !== 'function' || typeof providerMod.setDistillModel !== 'function') {
1083
+ await bot.sendMessage(chatId, '❌ Distill model config is not available.');
1084
+ return { handled: true, config };
1085
+ }
1086
+ const arg = text.slice('/distill-model'.length).trim();
1087
+ if (!arg) {
1088
+ await bot.sendMessage(chatId, `🧪 当前蒸馏模型: ${providerMod.getDistillModel()}\n用法: /distill-model <model>\n示例: /distill-model gpt-5.1-codex-mini`);
1089
+ return { handled: true, config };
1090
+ }
1091
+ try {
1092
+ providerMod.setDistillModel(arg);
1093
+ await bot.sendMessage(chatId, `✅ 蒸馏模型已更新为: ${providerMod.getDistillModel()}`);
1094
+ } catch (e) {
1095
+ await bot.sendMessage(chatId, `❌ 设置失败: ${e.message}`);
1096
+ }
1097
+ return { handled: true, config };
1098
+ }
1099
+
1100
+ const nlDistillIntent = parseDistillModelIntent(text);
1101
+ if (nlDistillIntent) {
1102
+ if (!providerMod || typeof providerMod.setDistillModel !== 'function' || typeof providerMod.getDistillModel !== 'function') {
1103
+ await bot.sendMessage(chatId, '❌ Distill model config is not available.');
1104
+ return { handled: true, config };
1105
+ }
1106
+ try {
1107
+ providerMod.setDistillModel(nlDistillIntent.model);
1108
+ await bot.sendMessage(chatId, `✅ 已按自然语言请求更新蒸馏模型: ${providerMod.getDistillModel()}`);
1109
+ } catch (e) {
1110
+ await bot.sendMessage(chatId, `❌ 设置失败: ${e.message}`);
1111
+ }
1112
+ return { handled: true, config };
1113
+ }
1114
+
870
1115
  return { handled: false, config };
871
1116
  }
872
1117
 
873
- return { handleAdminCommand };
1118
+ return { handleAdminCommand, _private: { parseDistillModelIntent } };
874
1119
  }
875
1120
 
876
1121
  module.exports = { createAdminCommandHandler };
@@ -28,8 +28,26 @@ function createAgentCommandHandler(deps) {
28
28
  attachOrCreateSession,
29
29
  agentFlowTtlMs,
30
30
  agentBindTtlMs,
31
+ getDefaultEngine = () => 'claude',
31
32
  } = deps;
32
33
 
34
+ function normalizeEngineName(name) {
35
+ const n = String(name || '').trim().toLowerCase();
36
+ return n === 'codex' ? 'codex' : getDefaultEngine();
37
+ }
38
+
39
+ function inferEngineByCwd(cfg, cwd) {
40
+ if (!cfg || !cfg.projects || !cwd) return null;
41
+ const targetCwd = normalizeCwd(cwd);
42
+ for (const proj of Object.values(cfg.projects || {})) {
43
+ if (!proj || !proj.cwd) continue;
44
+ if (normalizeCwd(proj.cwd) === targetCwd) {
45
+ return normalizeEngineName(proj.engine);
46
+ }
47
+ }
48
+ return null;
49
+ }
50
+
33
51
  // Pending activations have no TTL — they persist until consumed.
34
52
  // The creating chatId is stored to prevent self-activation.
35
53
 
@@ -127,7 +145,12 @@ function createAgentCommandHandler(deps) {
127
145
  const action = res.data.isNewProject ? '绑定成功' : '重新绑定';
128
146
  const displayCwd = String(res.data.cwd || '').replace(HOME, '~');
129
147
  if (res.data.cwd && typeof attachOrCreateSession === 'function') {
130
- attachOrCreateSession(chatId, normalizeCwd(res.data.cwd), p.name || agentName || res.data.projectKey || '');
148
+ attachOrCreateSession(
149
+ chatId,
150
+ normalizeCwd(res.data.cwd),
151
+ p.name || agentName || res.data.projectKey || '',
152
+ p.engine || getDefaultEngine()
153
+ );
131
154
  }
132
155
  await bot.sendMessage(chatId, `${icon} ${p.name || agentName} ${action}\n目录: ${displayCwd}`);
133
156
  return { ok: true, data: res.data };
@@ -140,7 +163,7 @@ function createAgentCommandHandler(deps) {
140
163
  }
141
164
  const fallbackCwd = (fallback.data && fallback.data.cwd) || agentCwd;
142
165
  if (fallbackCwd && typeof attachOrCreateSession === 'function') {
143
- attachOrCreateSession(chatId, normalizeCwd(fallbackCwd), agentName || '');
166
+ attachOrCreateSession(chatId, normalizeCwd(fallbackCwd), agentName || '', getDefaultEngine());
144
167
  }
145
168
  return {
146
169
  ok: true,
@@ -163,9 +186,9 @@ function createAgentCommandHandler(deps) {
163
186
 
164
187
  async function createAgentViaUnifiedApi(chatId, name, dir, roleDesc, opts = {}) {
165
188
  // Default: skip binding the creating chat — let the target group activate via /activate
166
- const { skipChatBinding = true } = opts;
189
+ const { skipChatBinding = true, engine = null } = opts;
167
190
  if (agentTools && typeof agentTools.createNewWorkspaceAgent === 'function') {
168
- const res = await agentTools.createNewWorkspaceAgent(name, dir, roleDesc, chatId, { skipChatBinding });
191
+ const res = await agentTools.createNewWorkspaceAgent(name, dir, roleDesc, chatId, { skipChatBinding, engine });
169
192
  if (res.ok && skipChatBinding && res.data && res.data.projectKey) {
170
193
  storePendingActivation(res.data.projectKey, name, res.data.cwd, chatId);
171
194
  }
@@ -273,10 +296,14 @@ function createAgentCommandHandler(deps) {
273
296
  const cwd = fullMatch.projectPath || (getSession(chatId) && getSession(chatId).cwd) || HOME;
274
297
 
275
298
  const state2 = loadState();
299
+ const cfgForEngine = loadConfig();
300
+ const engineByTargetCwd = inferEngineByCwd(cfgForEngine, cwd);
301
+ const currentEngine = normalizeEngineName(state2.sessions[chatId] && state2.sessions[chatId].engine);
276
302
  state2.sessions[chatId] = {
277
303
  id: sessionId,
278
304
  cwd,
279
305
  started: true,
306
+ engine: engineByTargetCwd || currentEngine,
280
307
  };
281
308
  saveState(state2);
282
309
  const name = fullMatch.customTitle;
@@ -485,7 +512,36 @@ function createAgentCommandHandler(deps) {
485
512
  }
486
513
  }
487
514
  }
488
- // No pending activation at all guide to manual bind
515
+ // No pending activation fall back to scanning daemon.yaml for unbound projects
516
+ const allBoundKeys = new Set(Object.values({
517
+ ...(cfg.telegram ? cfg.telegram.chat_agent_map : {}),
518
+ ...(cfg.feishu ? cfg.feishu.chat_agent_map : {}),
519
+ }));
520
+ const unboundProjects = Object.entries(cfg.projects || {})
521
+ .filter(([key, p]) => p && p.cwd && !allBoundKeys.has(key))
522
+ .map(([key, p]) => ({ key, name: p.name || key, cwd: p.cwd, icon: p.icon || '🤖' }));
523
+
524
+ if (unboundProjects.length === 1) {
525
+ // Exactly one unbound project — auto-bind using project KEY (not display name)
526
+ // to ensure toProjectKey() resolves to the correct existing key in daemon.yaml
527
+ const proj = unboundProjects[0];
528
+ const bindRes2 = await bindViaUnifiedApi(bot, chatId, proj.key, proj.cwd);
529
+ if (bindRes2.ok) pendingActivations && pendingActivations.delete(proj.key);
530
+ return true;
531
+ }
532
+
533
+ if (unboundProjects.length > 1) {
534
+ // Multiple unbound projects — show pick list using project keys
535
+ const lines = ['请选择要激活的 Agent:', ''];
536
+ for (const p of unboundProjects) {
537
+ lines.push(`${p.icon} ${p.name} → \`/agent bind ${p.key} ${p.cwd}\``);
538
+ }
539
+ lines.push('\n发送对应命令即可绑定此群。');
540
+ await bot.sendMessage(chatId, lines.join('\n'));
541
+ return true;
542
+ }
543
+
544
+ // Truly nothing to activate
489
545
  await bot.sendMessage(chatId,
490
546
  '没有待激活的 Agent。\n\n如果已创建过 Agent,直接用:\n`/agent bind <名称> <目录>`\n即可绑定,不需要重新创建。'
491
547
  );
@@ -517,4 +573,6 @@ function createAgentCommandHandler(deps) {
517
573
  return { handleAgentCommand };
518
574
  }
519
575
 
520
- module.exports = { createAgentCommandHandler };
576
+ module.exports = {
577
+ createAgentCommandHandler,
578
+ };
@@ -31,13 +31,17 @@ function createAgentTools(deps) {
31
31
  return (String(agentName || '').replace(/[^a-zA-Z0-9_]/g, '_').toLowerCase() || String(chatId));
32
32
  }
33
33
 
34
+ function normalizeEngine(engine) {
35
+ return String(engine || '').trim().toLowerCase() === 'codex' ? 'codex' : 'claude';
36
+ }
37
+
34
38
  function ensureAdapterConfig(cfg, adapterKey) {
35
39
  if (!cfg[adapterKey]) cfg[adapterKey] = {};
36
40
  if (!cfg[adapterKey].allowed_chat_ids) cfg[adapterKey].allowed_chat_ids = [];
37
41
  if (!cfg[adapterKey].chat_agent_map) cfg[adapterKey].chat_agent_map = {};
38
42
  }
39
43
 
40
- async function bindAgentToChat(chatId, agentName, workspaceDir, { force = false } = {}) {
44
+ async function bindAgentToChat(chatId, agentName, workspaceDir, { force = false, engine = null } = {}) {
41
45
  try {
42
46
  const safeName = sanitizeText(agentName, 120);
43
47
  if (!safeName) return { ok: false, error: 'agentName is required' };
@@ -49,6 +53,7 @@ function createAgentTools(deps) {
49
53
 
50
54
  const projectKey = toProjectKey(safeName, chatId);
51
55
  let resolvedDir = resolveWorkspaceDir(workspaceDir);
56
+ const normalizedEngine = engine ? normalizeEngine(engine) : null;
52
57
 
53
58
  if (!resolvedDir) {
54
59
  const existing = cfg.projects[projectKey];
@@ -80,7 +85,12 @@ function createAgentTools(deps) {
80
85
  cfg[adapterKey].chat_agent_map[String(chatId)] = projectKey;
81
86
  const existed = !!cfg.projects[projectKey];
82
87
  if (!existed) {
83
- cfg.projects[projectKey] = { name: safeName, cwd: resolvedDir, nicknames: [safeName] };
88
+ cfg.projects[projectKey] = {
89
+ name: safeName,
90
+ cwd: resolvedDir,
91
+ nicknames: [safeName],
92
+ ...(normalizedEngine === 'codex' ? { engine: 'codex' } : {}),
93
+ };
84
94
  } else {
85
95
  const nicknames = Array.isArray(cfg.projects[projectKey].nicknames)
86
96
  ? cfg.projects[projectKey].nicknames
@@ -91,6 +101,7 @@ function createAgentTools(deps) {
91
101
  name: safeName,
92
102
  cwd: resolvedDir,
93
103
  nicknames,
104
+ ...(normalizedEngine === 'codex' ? { engine: 'codex' } : {}),
94
105
  };
95
106
  }
96
107
 
@@ -175,8 +186,9 @@ ${safeDelta}
175
186
  }
176
187
  }
177
188
 
178
- async function createNewWorkspaceAgent(agentName, workspaceDir, roleDescription, chatId, { skipChatBinding = false } = {}) {
189
+ async function createNewWorkspaceAgent(agentName, workspaceDir, roleDescription, chatId, { skipChatBinding = false, engine = null } = {}) {
179
190
  let bindData;
191
+ const normalizedEngine = engine ? normalizeEngine(engine) : null;
180
192
 
181
193
  if (skipChatBinding) {
182
194
  // Create the project entry without touching chat_agent_map
@@ -192,7 +204,16 @@ ${safeDelta}
192
204
  const projectKey = toProjectKey(safeName, chatId);
193
205
  const existed = !!cfg.projects[projectKey];
194
206
  if (!existed) {
195
- cfg.projects[projectKey] = { name: safeName, cwd: resolvedDir, nicknames: [safeName] };
207
+ cfg.projects[projectKey] = {
208
+ name: safeName,
209
+ cwd: resolvedDir,
210
+ nicknames: [safeName],
211
+ ...(normalizedEngine === 'codex' ? { engine: 'codex' } : {}),
212
+ };
213
+ writeConfigSafe(cfg);
214
+ backupConfig();
215
+ } else if (normalizedEngine === 'codex') {
216
+ cfg.projects[projectKey] = { ...cfg.projects[projectKey], engine: 'codex' };
196
217
  writeConfigSafe(cfg);
197
218
  backupConfig();
198
219
  }
@@ -204,7 +225,7 @@ ${safeDelta}
204
225
  project: cfg.projects[projectKey],
205
226
  };
206
227
  } else {
207
- const bindResult = await bindAgentToChat(chatId, agentName, workspaceDir);
228
+ const bindResult = await bindAgentToChat(chatId, agentName, workspaceDir, { engine: normalizedEngine });
208
229
  if (!bindResult.ok) return bindResult;
209
230
  bindData = bindResult.data;
210
231
  }