metame-cli 1.5.16 → 1.5.17

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "metame-cli",
3
- "version": "1.5.16",
3
+ "version": "1.5.17",
4
4
  "description": "The Cognitive Profile Layer for Claude Code. Knows how you think, not just what you said.",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -6,7 +6,7 @@ const {
6
6
  USAGE_CATEGORY_LABEL,
7
7
  } = require('./usage-classifier');
8
8
  const { IS_WIN } = require('./platform');
9
- const { ENGINE_MODEL_CONFIG, resolveEngineModel } = require('./daemon-engine-runtime');
9
+ const { ENGINE_MODEL_CONFIG, resolveEngineModel, normalizeClaudeModel } = require('./daemon-engine-runtime');
10
10
  const { resolveProjectKey: _resolveProjectKey } = require('./daemon-team-dispatch');
11
11
  const {
12
12
  parseRemoteTargetRef,
@@ -1439,7 +1439,7 @@ function createAdminCommandHandler(deps) {
1439
1439
  return { handled: true, config };
1440
1440
  }
1441
1441
 
1442
- // /doctor — diagnostics; /fix — restore backup; /reset — reset model to sonnet
1442
+ // /doctor — diagnostics; /fix — restore backup; /reset — reset Claude slot to opus
1443
1443
  if (text === '/fix') {
1444
1444
  if (restoreConfig()) {
1445
1445
  await bot.sendMessage(chatId, '✅ 已从备份恢复配置');
@@ -1453,7 +1453,8 @@ function createAdminCommandHandler(deps) {
1453
1453
  backupConfig();
1454
1454
  const cfg = yaml.load(fs.readFileSync(CONFIG_FILE, 'utf8')) || {};
1455
1455
  if (!cfg.daemon) cfg.daemon = {};
1456
- cfg.daemon.model = 'opus';
1456
+ if (!cfg.daemon.models) cfg.daemon.models = {};
1457
+ cfg.daemon.models.claude = 'opus';
1457
1458
  writeConfigSafe(cfg);
1458
1459
  config = loadConfig();
1459
1460
  await bot.sendMessage(chatId, '✅ 模型已重置为 opus');
@@ -1480,9 +1481,10 @@ function createAdminCommandHandler(deps) {
1480
1481
  issues++;
1481
1482
  }
1482
1483
 
1483
- const m = (cfg && cfg.daemon && cfg.daemon.model) || 'opus';
1484
+ const daemonCfg = (cfg && cfg.daemon) || {};
1485
+ const m = resolveEngineModel('claude', daemonCfg);
1484
1486
  const modelOk = isCustomProvider
1485
- ? /^[a-zA-Z0-9._-]{2,80}$/.test(String(m || '').trim())
1487
+ ? ['sonnet', 'opus', 'haiku'].includes(m)
1486
1488
  : validModels.includes(m);
1487
1489
  if (modelOk) {
1488
1490
  checks.push(`✅ 模型: ${m}`);
@@ -1569,7 +1571,11 @@ function createAdminCommandHandler(deps) {
1569
1571
  : engineCfg.provider;
1570
1572
  const isBuiltinProvider = activeProvider === engineCfg.provider;
1571
1573
  const distillModel = getDistillModel();
1572
- const hintLine = engineCfg.hint ? `\n💡 ${engineCfg.hint}` : (!isBuiltinProvider ? `\n💡 ${activeProvider} 可输入任意模型名` : '');
1574
+ const hintLine = engineCfg.hint
1575
+ ? `\n💡 ${engineCfg.hint}`
1576
+ : (!isBuiltinProvider && currentEngine === 'claude'
1577
+ ? `\n💡 ${activeProvider} 的后端真实模型请在 CC Switch / provider 层配置`
1578
+ : '');
1573
1579
 
1574
1580
  if (!arg) {
1575
1581
  const statusLine = `🤖 [${currentEngine}] 会话模型: ${currentModel} Provider: ${activeProvider}\n🧪 后台轻量: ${distillModel} (/distill-model 修改)${hintLine}`;
@@ -1587,10 +1593,13 @@ function createAdminCommandHandler(deps) {
1587
1593
  }
1588
1594
 
1589
1595
  const normalizedArg = arg.toLowerCase();
1590
- // Only restrict model names for claude with anthropic provider (known fixed set)
1591
- // codex always allows free-form input (OpenAI models change frequently)
1592
- if (currentEngine === 'claude' && isBuiltinProvider && !optionValues.includes(normalizedArg)) {
1593
- await bot.sendMessage(chatId, `❌ 无效模型: ${arg}\n可选: ${optionValues.join(', ')}\n💡 切换到自定义 provider 后可用任意模型名`);
1596
+ // Claude session/config layer only accepts canonical slots; provider mapping stays in CC Switch.
1597
+ if (currentEngine === 'claude' && !optionValues.includes(normalizedArg)) {
1598
+ const suggested = normalizeClaudeModel(arg, '');
1599
+ const hint = suggested
1600
+ ? `\n💡 检测到它更像 Claude 槽位 ${suggested},请直接用 /model ${suggested}`
1601
+ : '';
1602
+ await bot.sendMessage(chatId, `❌ Claude 会话模型只接受: ${optionValues.join(', ')}\n后端真实模型请在 CC Switch / provider 层配置,不要写进会话模型${hint}`);
1594
1603
  return { handled: true, config };
1595
1604
  }
1596
1605
 
@@ -434,13 +434,10 @@ function createClaudeEngine(deps) {
434
434
  const entry = JSON.parse(line);
435
435
  const sessionModel = entry && entry.message && entry.message.model;
436
436
  if (!sessionModel || sessionModel === '<synthetic>') continue;
437
- if (!sessionModel.startsWith('claude-')) {
438
- return {
439
- shouldResume: false,
440
- modelPin: null,
441
- reason: 'non-claude-session',
442
- };
443
- }
437
+ // Custom Claude-compatible providers may record backend-native model ids
438
+ // (for example MiniMax) in the JSONL. Those sessions are still resumable;
439
+ // we only skip model pinning when the family cannot be mapped back to
440
+ // Claude's alias set.
444
441
  // If the configured model is a short alias (sonnet/opus/haiku) and the JSONL model
445
442
  // belongs to the same family, do NOT pin — let the alias resolve to the latest version.
446
443
  // Only pin when the families genuinely differ (e.g. session was opus, config says sonnet).
@@ -1235,14 +1232,18 @@ function createClaudeEngine(deps) {
1235
1232
  * Reset active provider back to anthropic/opus and reload config.
1236
1233
  * Returns the freshly loaded config so callers can reassign their local variable.
1237
1234
  */
1238
- function fallbackToDefaultProvider(reason) {
1235
+ function fallbackToDefaultProvider(reason, boundProjectKey = '') {
1239
1236
  log('WARN', `Falling back to anthropic/opus — reason: ${reason}`);
1240
1237
  if (providerMod && providerMod.getActiveName() !== 'anthropic') {
1241
1238
  providerMod.setActive('anthropic');
1242
1239
  }
1243
1240
  const cfg = yaml.load(fs.readFileSync(CONFIG_FILE, 'utf8')) || {};
1244
1241
  if (!cfg.daemon) cfg.daemon = {};
1245
- cfg.daemon.model = 'opus';
1242
+ if (!cfg.daemon.models) cfg.daemon.models = {};
1243
+ cfg.daemon.models.claude = 'opus';
1244
+ if (boundProjectKey && cfg.projects && cfg.projects[boundProjectKey]) {
1245
+ cfg.projects[boundProjectKey].model = 'opus';
1246
+ }
1246
1247
  writeConfigSafe(cfg);
1247
1248
  return loadConfig();
1248
1249
  }
@@ -2269,7 +2270,7 @@ ${mentorRadarHint}
2269
2270
  const looksLikeError = output.length < 300 && /\b(not found|invalid model|unauthorized|401|403|404|error|failed)\b/i.test(output);
2270
2271
  if (looksLikeError && (activeProvCheck !== 'anthropic' || !builtinModelsCheck.includes(model))) {
2271
2272
  try {
2272
- config = fallbackToDefaultProvider(`output looks like error for ${activeProvCheck}/${model}`);
2273
+ config = fallbackToDefaultProvider(`output looks like error for ${activeProvCheck}/${model}`, boundProjectKey || '');
2273
2274
  await bot.sendMessage(chatId, `⚠️ ${activeProvCheck}/${model} 疑似失败,已回退到 anthropic/opus\n输出: ${output.slice(0, 150)}`);
2274
2275
  } catch (fbErr) {
2275
2276
  log('ERROR', `Fallback failed: ${fbErr.message}`);
@@ -2500,7 +2501,7 @@ ${mentorRadarHint}
2500
2501
  const builtinModelValues = (ENGINE_MODEL_CONFIG.claude.options || []).map(o => typeof o === 'string' ? o : o.value);
2501
2502
  if ((activeProv !== 'anthropic' || !builtinModelValues.includes(model)) && !errMsg.includes('Stopped by user')) {
2502
2503
  try {
2503
- config = fallbackToDefaultProvider(`${activeProv}/${model} error: ${errMsg.slice(0, 100)}`);
2504
+ config = fallbackToDefaultProvider(`${activeProv}/${model} error: ${errMsg.slice(0, 100)}`, boundProjectKey || '');
2504
2505
  await bot.sendMessage(chatId, `⚠️ ${activeProv}/${model} 失败,已回退到 anthropic/opus\n原因: ${errMsg.slice(0, 100)}`);
2505
2506
  } catch (fallbackErr) {
2506
2507
  log('ERROR', `Fallback failed: ${fallbackErr.message}`);
@@ -1,5 +1,7 @@
1
1
  'use strict';
2
2
 
3
+ const { resolveEngineModel } = require('./daemon-engine-runtime');
4
+
3
5
  function createCommandRouter(deps) {
4
6
  const {
5
7
  loadState,
@@ -657,7 +659,7 @@ function createCommandRouter(deps) {
657
659
  }
658
660
 
659
661
  if (text.startsWith('/')) {
660
- const currentModel = (config.daemon && config.daemon.model) || 'opus';
662
+ const currentModel = resolveEngineModel('claude', (config && config.daemon) || {});
661
663
  const currentProvider = providerMod ? providerMod.getActiveName() : 'anthropic';
662
664
  await bot.sendMessage(chatId, [
663
665
  '📱 手机端 Claude Code',
@@ -93,23 +93,58 @@ const BUILTIN_CLAUDE_MODEL_VALUES = Object.freeze(
93
93
  ).filter(Boolean)
94
94
  );
95
95
 
96
+ function normalizeClaudeModel(model, fallback = ENGINE_MODEL_CONFIG.claude.main) {
97
+ const raw = String(model || '').trim();
98
+ if (!raw) return fallback;
99
+ const normalized = raw.toLowerCase();
100
+ if (BUILTIN_CLAUDE_MODEL_VALUES.includes(normalized)) return normalized;
101
+ if (normalized.includes('opus')) return 'opus';
102
+ if (normalized.includes('sonnet')) return 'sonnet';
103
+ if (normalized.includes('haiku')) return 'haiku';
104
+ return fallback;
105
+ }
106
+
107
+ function looksLikeCodexModel(model) {
108
+ const raw = String(model || '').trim().toLowerCase();
109
+ if (!raw) return false;
110
+ return (
111
+ raw.startsWith('gpt-')
112
+ || raw.startsWith('o1')
113
+ || raw.startsWith('o3')
114
+ || raw.startsWith('o4')
115
+ || raw.includes('codex')
116
+ );
117
+ }
118
+
96
119
  function resolveEngineModel(engineName, daemonCfg = {}, overrideModel = '') {
97
120
  const engine = normalizeEngineName(engineName);
98
121
  const engineCfg = ENGINE_MODEL_CONFIG[engine] || ENGINE_MODEL_CONFIG.claude;
99
122
  const engineModels = (daemonCfg && daemonCfg.models) || {};
100
123
  const explicitModel = String(overrideModel || '').trim();
101
- if (explicitModel) return explicitModel;
124
+ if (explicitModel) {
125
+ return engine === 'claude'
126
+ ? normalizeClaudeModel(explicitModel, engineCfg.main)
127
+ : explicitModel;
128
+ }
102
129
 
103
130
  const perEngineModel = String(engineModels[engine] || '').trim();
104
- if (perEngineModel) return perEngineModel;
131
+ if (perEngineModel) {
132
+ return engine === 'claude'
133
+ ? normalizeClaudeModel(perEngineModel, engineCfg.main)
134
+ : perEngineModel;
135
+ }
105
136
 
106
137
  const legacyModel = String((daemonCfg && daemonCfg.model) || '').trim();
107
138
  if (!legacyModel) return engineCfg.main;
108
139
 
109
140
  // Legacy daemon.model historically meant a Claude model.
110
- // Preserve backward compatibility for non-Claude custom model IDs,
111
- // but do not leak Claude aliases like "opus" into Codex sessions.
112
- if (engine === 'codex' && BUILTIN_CLAUDE_MODEL_VALUES.includes(legacyModel)) {
141
+ if (engine === 'claude') {
142
+ return normalizeClaudeModel(legacyModel, engineCfg.main);
143
+ }
144
+
145
+ // Legacy daemon.model primarily belonged to Claude; only reuse it for Codex
146
+ // when it already looks like a real Codex/OpenAI model id.
147
+ if (engine === 'codex' && !looksLikeCodexModel(legacyModel)) {
113
148
  return engineCfg.main;
114
149
  }
115
150
  return legacyModel;
@@ -420,6 +455,7 @@ module.exports = {
420
455
  resolveBinary,
421
456
  detectDefaultEngine,
422
457
  resolveEngineModel,
458
+ normalizeClaudeModel,
423
459
  ENGINE_MODEL_CONFIG,
424
460
  ENGINE_DISTILL_MAP,
425
461
  ENGINE_DEFAULT_MODEL,
@@ -434,5 +470,7 @@ module.exports = {
434
470
  normalizeCodexApprovalPolicy,
435
471
  resolveCodexPermissionProfile,
436
472
  BUILTIN_CLAUDE_MODEL_VALUES,
473
+ normalizeClaudeModel,
474
+ looksLikeCodexModel,
437
475
  },
438
476
  };
@@ -296,7 +296,9 @@ function createSessionStore(deps) {
296
296
  }
297
297
  } catch {}
298
298
  }
299
- } catch { /* skip */ }
299
+ } catch (err) {
300
+ log('WARN', `scanClaudeSessions project ${proj}: ${err.message}`);
301
+ }
300
302
 
301
303
  try {
302
304
  const files = fs.readdirSync(projDir).filter(f => f.endsWith('.jsonl'));
@@ -318,7 +320,9 @@ function createSessionStore(deps) {
318
320
  });
319
321
  }
320
322
  }
321
- } catch { /* skip */ }
323
+ } catch (err) {
324
+ log('WARN', `scanClaudeSessions project ${proj}: ${err.message}`);
325
+ }
322
326
  }
323
327
 
324
328
  const all = Array.from(sessionMap.values()).map((entry) => ({ ...entry, engine: 'claude' }));
@@ -385,7 +389,8 @@ function createSessionStore(deps) {
385
389
  } catch { /* non-fatal */ }
386
390
  }
387
391
  return all;
388
- } catch {
392
+ } catch (err) {
393
+ log('WARN', `scanClaudeSessions: ${err.message}`);
389
394
  return [];
390
395
  }
391
396
  }
@@ -444,8 +449,9 @@ function createSessionStore(deps) {
444
449
  };
445
450
  })
446
451
  .map((session) => enrichCodexSession(session));
447
- } catch {
452
+ } catch (err) {
448
453
  if (db) { try { db.close(); } catch { /* ignore */ } }
454
+ log('WARN', `scanCodexSessions ${CODEX_DB}: ${err.message}`);
449
455
  return [];
450
456
  }
451
457
  }
@@ -544,19 +550,15 @@ function createSessionStore(deps) {
544
550
 
545
551
  function scanAllSessions() {
546
552
  if (_sessionCache && (Date.now() - _sessionCacheTime < SESSION_CACHE_TTL)) return _sessionCache;
547
- try {
548
- const all = [...scanClaudeSessions(), ...scanCodexSessions()];
549
- all.sort((a, b) => {
550
- const aTime = a.fileMtime || new Date(a.modified).getTime();
551
- const bTime = b.fileMtime || new Date(b.modified).getTime();
552
- return bTime - aTime;
553
- });
554
- _sessionCache = all;
555
- _sessionCacheTime = Date.now();
556
- return all;
557
- } catch {
558
- return [];
559
- }
553
+ const all = [...scanClaudeSessions(), ...scanCodexSessions()];
554
+ all.sort((a, b) => {
555
+ const aTime = a.fileMtime || new Date(a.modified).getTime();
556
+ const bTime = b.fileMtime || new Date(b.modified).getTime();
557
+ return bTime - aTime;
558
+ });
559
+ _sessionCache = all;
560
+ _sessionCacheTime = Date.now();
561
+ return all;
560
562
  }
561
563
 
562
564
  function listRecentSessions(limit, cwd, engine) {
@@ -1033,10 +1035,6 @@ function createSessionStore(deps) {
1033
1035
 
1034
1036
  // Try to read cwd/model from session JSONL file content (most reliable)
1035
1037
  const metadata = _readClaudeSessionMetadata(sessionFile);
1036
- if (metadata.model && !metadata.model.startsWith('claude-')) {
1037
- log('WARN', `[SessionValid] ${sessionId.slice(0, 8)}: non-claude model "${metadata.model}"`);
1038
- return false;
1039
- }
1040
1038
  if (metadata.cwd && path.resolve(metadata.cwd) === normCwd) return true;
1041
1039
  if (metadata.cwd) {
1042
1040
  // CWD mismatch: the session was created for a different directory.
@@ -2,6 +2,7 @@
2
2
 
3
3
  const crypto = require('crypto');
4
4
  const { classifyTaskUsage } = require('./usage-classifier');
5
+ const { resolveEngineModel } = require('./daemon-engine-runtime');
5
6
 
6
7
  const WEEKDAY_INDEX = Object.freeze({
7
8
  sun: 0,
@@ -579,7 +580,7 @@ function createTaskScheduler(deps) {
579
580
  if (steps.length === 0) return { success: false, error: 'No steps defined', output: '' };
580
581
 
581
582
  // Workflow tasks match the user's session model setting (same quality as interactive)
582
- const sessionModel = (config && config.daemon && config.daemon.model) || 'sonnet';
583
+ const sessionModel = resolveEngineModel('claude', (config && config.daemon) || {});
583
584
  const model = normalizeModel(task.model || sessionModel);
584
585
  const cwd = task.cwd ? task.cwd.replace(/^~/, HOME) : HOME;
585
586
  const sessionId = crypto.randomUUID();
package/scripts/daemon.js CHANGED
@@ -2078,16 +2078,6 @@ if (providerMod && typeof providerMod.setEngine === 'function') {
2078
2078
  }
2079
2079
  log('INFO', `Default engine: ${_defaultEngine} (detected: ${detectedEngine})`);
2080
2080
 
2081
- // One-time migration: daemon.model (legacy) → daemon.models.<engine>
2082
- try {
2083
- const _migCfg = yaml.load(fs.readFileSync(CONFIG_FILE, 'utf8')) || {};
2084
- if (_migCfg.daemon && _migCfg.daemon.model && !_migCfg.daemon.models) {
2085
- _migCfg.daemon.models = { [_defaultEngine]: _migCfg.daemon.model };
2086
- writeConfigSafe(_migCfg);
2087
- log('INFO', `Migrated daemon.model="${_migCfg.daemon.model}" → daemon.models.${_defaultEngine}`);
2088
- }
2089
- } catch { /* ignore */ }
2090
-
2091
2081
  function getDefaultEngine() {
2092
2082
  return _defaultEngine;
2093
2083
  }
@@ -2107,15 +2097,6 @@ function setDefaultEngine(engine) {
2107
2097
  try { providerMod.setEngine(engine); } catch { /* ignore */ }
2108
2098
  }
2109
2099
  }
2110
- // Migrate old daemon.model → daemon.models[engine] on first switch
2111
- try {
2112
- const cfg = yaml.load(fs.readFileSync(CONFIG_FILE, 'utf8')) || {};
2113
- if (!cfg.daemon) cfg.daemon = {};
2114
- if (cfg.daemon.model && !cfg.daemon.models) {
2115
- cfg.daemon.models = { [engine]: cfg.daemon.model };
2116
- writeConfigSafe(cfg);
2117
- }
2118
- } catch { /* ignore */ }
2119
2100
  }
2120
2101
 
2121
2102
  const getEngineRuntime = createEngineRuntimeFactory({
@@ -2609,10 +2590,6 @@ async function main() {
2609
2590
  'enable_nl_mac_control',
2610
2591
  'enable_nl_mac_fallback',
2611
2592
  ];
2612
- // All known models across all engines (for legacy daemon.model validation only)
2613
- const BUILTIN_CLAUDE_MODELS = (ENGINE_MODEL_CONFIG.claude.options || []).map(option =>
2614
- typeof option === 'string' ? option : option.value
2615
- ).filter(Boolean);
2616
2593
  for (const key of Object.keys(config)) {
2617
2594
  if (!KNOWN_SECTIONS.includes(key)) log('WARN', `Config: unknown section "${key}" (typo?)`);
2618
2595
  }
@@ -2620,14 +2597,11 @@ async function main() {
2620
2597
  for (const key of Object.keys(config.daemon)) {
2621
2598
  if (!KNOWN_DAEMON.includes(key)) log('WARN', `Config: unknown daemon.${key} (typo?)`);
2622
2599
  }
2623
- // Validate legacy daemon.model (only warn if anthropic provider + unknown Claude model)
2624
- if (config.daemon.model && !BUILTIN_CLAUDE_MODELS.includes(config.daemon.model)) {
2600
+ // Keep legacy daemon.model read-compatible, but never auto-migrate or treat it
2601
+ // as the write source of truth. The canonical writable field is daemon.models.<engine>.
2602
+ if (config.daemon.model) {
2625
2603
  const activeProv = providerMod ? providerMod.getActiveName() : 'anthropic';
2626
- if (activeProv === 'anthropic' && _defaultEngine === 'claude') {
2627
- log('WARN', `Config: daemon.model="${config.daemon.model}" is not a known Claude model`);
2628
- } else {
2629
- log('INFO', `Config: legacy daemon.model="${config.daemon.model}" retained; active ${_defaultEngine} model resolves to "${resolveEngineModel(_defaultEngine, config.daemon)}" (${activeProv})`);
2630
- }
2604
+ log('INFO', `Config: legacy daemon.model detected; active ${_defaultEngine} model resolves to "${resolveEngineModel(_defaultEngine, config.daemon)}" (${activeProv})`);
2631
2605
  }
2632
2606
  }
2633
2607