metame-cli 1.4.33 → 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 +187 -48
  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 +704 -268
  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 +87 -28
  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
package/index.js CHANGED
@@ -88,7 +88,7 @@ function syncDirFiles(srcDir, destDir, { fileList, chmod } = {}) {
88
88
  // Auto-deploy bundled scripts to ~/.metame/
89
89
  // IMPORTANT: daemon.yaml is USER CONFIG — never overwrite it. Only daemon-default.yaml (template) is synced.
90
90
  const scriptsDir = path.join(__dirname, 'scripts');
91
- const BUNDLED_BASE_SCRIPTS = ['platform.js', 'signal-capture.js', 'distill.js', 'schema.js', 'pending-traits.js', 'migrate-v2.js', 'daemon.js', 'telegram-adapter.js', 'feishu-adapter.js', 'daemon-default.yaml', 'providers.js', 'session-analytics.js', 'resolve-yaml.js', 'utils.js', 'skill-evolution.js', 'memory.js', 'memory-extract.js', 'memory-search.js', 'memory-gc.js', 'qmd-client.js', 'session-summarize.js', 'check-macos-control-capabilities.sh', 'usage-classifier.js', 'task-board.js', 'memory-nightly-reflect.js', 'memory-index.js'];
91
+ const BUNDLED_BASE_SCRIPTS = ['platform.js', 'signal-capture.js', 'distill.js', 'schema.js', 'pending-traits.js', 'daemon.js', 'telegram-adapter.js', 'feishu-adapter.js', 'daemon-default.yaml', 'providers.js', 'session-analytics.js', 'resolve-yaml.js', 'utils.js', 'skill-evolution.js', 'memory.js', 'memory-extract.js', 'memory-search.js', 'memory-write.js', 'memory-gc.js', 'qmd-client.js', 'session-summarize.js', 'mentor-engine.js', 'check-macos-control-capabilities.sh', 'usage-classifier.js', 'task-board.js', 'memory-nightly-reflect.js', 'memory-index.js', 'skill-changelog.js'];
92
92
  const DAEMON_MODULE_SCRIPTS = (() => {
93
93
  try {
94
94
  return fs.readdirSync(scriptsDir).filter((f) => /^daemon-[\w-]+\.js$/.test(f));
@@ -124,6 +124,8 @@ if (scriptsUpdated) {
124
124
  syncDirFiles(path.join(__dirname, 'scripts', 'docs'), path.join(METAME_DIR, 'docs'));
125
125
  // Bin: CLI tools (dispatch_to etc.)
126
126
  syncDirFiles(path.join(__dirname, 'scripts', 'bin'), path.join(METAME_DIR, 'bin'), { chmod: 0o755 });
127
+ // Hooks: Claude Code event hooks (Stop, PostToolUse, etc.)
128
+ syncDirFiles(path.join(__dirname, 'scripts', 'hooks'), path.join(METAME_DIR, 'hooks'));
127
129
 
128
130
  // ---------------------------------------------------------
129
131
  // Deploy bundled skills to ~/.claude/skills/
@@ -163,6 +165,29 @@ if (fs.existsSync(bundledSkillsDir)) {
163
165
  }
164
166
  }
165
167
 
168
+ // Ensure ~/.codex/skills and ~/.agents/skills are symlinks to ~/.claude/skills
169
+ // This keeps skill evolution unified across all engines.
170
+ for (const altDir of [
171
+ path.join(HOME_DIR, '.codex', 'skills'),
172
+ path.join(HOME_DIR, '.agents', 'skills'),
173
+ ]) {
174
+ try {
175
+ const parentDir = path.dirname(altDir);
176
+ if (!fs.existsSync(parentDir)) continue; // engine not installed, skip
177
+ const stat = fs.lstatSync(altDir);
178
+ if (stat.isSymbolicLink()) continue; // already a symlink, good
179
+ // Physical directory exists — replace with symlink
180
+ fs.rmSync(altDir, { recursive: true, force: true });
181
+ fs.symlinkSync(CLAUDE_SKILLS_DIR, altDir);
182
+ } catch (e) {
183
+ if (e.code === 'ENOENT') {
184
+ // Directory doesn't exist — create symlink
185
+ try { fs.symlinkSync(CLAUDE_SKILLS_DIR, altDir); } catch { /* non-fatal */ }
186
+ }
187
+ // Other errors: non-fatal, skip
188
+ }
189
+ }
190
+
166
191
  // Load daemon config for local launch flags
167
192
  let daemonCfg = {};
168
193
  try {
@@ -231,6 +256,8 @@ function ensureHookInstalled() {
231
256
  entry.hooks?.some(h => h.command && h.command.includes('signal-capture.js'))
232
257
  );
233
258
 
259
+ let modified = false;
260
+
234
261
  if (!stillInstalled) {
235
262
  if (!settings.hooks) settings.hooks = {};
236
263
  if (!settings.hooks.UserPromptSubmit) settings.hooks.UserPromptSubmit = [];
@@ -241,9 +268,33 @@ function ensureHookInstalled() {
241
268
  command: hookCommand
242
269
  }]
243
270
  });
271
+ modified = true;
272
+ console.log(`${icon("hook")} MetaMe: Signal capture hook installed.`);
273
+ }
274
+
275
+ // Ensure Stop hook (session-logger + tool-failure capture) is installed
276
+ const stopHookScript = path.join(METAME_DIR, 'hooks', 'stop-session-capture.js').replace(/\\/g, '/');
277
+ const stopHookCommand = `node "${stopHookScript}"`;
278
+ const stopHookInstalled = (settings.hooks?.Stop || []).some(entry =>
279
+ entry.hooks?.some(h => h.command && h.command.includes('stop-session-capture.js'))
280
+ );
281
+
282
+ if (!stopHookInstalled) {
283
+ if (!settings.hooks) settings.hooks = {};
284
+ if (!settings.hooks.Stop) settings.hooks.Stop = [];
285
+
286
+ settings.hooks.Stop.push({
287
+ hooks: [{
288
+ type: 'command',
289
+ command: stopHookCommand
290
+ }]
291
+ });
292
+ modified = true;
293
+ console.log(`${icon("hook")} MetaMe: Stop session capture hook installed.`);
294
+ }
244
295
 
296
+ if (modified) {
245
297
  fs.writeFileSync(CLAUDE_SETTINGS, JSON.stringify(settings, null, 2), 'utf8');
246
- console.log(`${icon("hook")} MetaMe: Signal capture hook installed.`);
247
298
  }
248
299
  } catch (e) {
249
300
  // Non-fatal: hook install failure shouldn't block launch
@@ -743,7 +794,8 @@ const GLOBAL_MARKER_END = '<!-- METAME-GLOBAL:END -->';
743
794
  // Build dynamic Agent dispatch table from daemon.yaml projects.
744
795
  // Only include agents whose cwd actually exists on disk — test/stale agents
745
796
  // with deleted paths are automatically excluded, no manual cleanup needed.
746
- let dispatchTable = '';
797
+ // The table is written to ~/.metame/docs/dispatch-table.md (NOT inlined into CLAUDE.md).
798
+ const DISPATCH_TABLE_PATH = path.join(METAME_DIR, 'docs', 'dispatch-table.md');
747
799
  try {
748
800
  const daemonYamlPath = path.join(os.homedir(), '.metame', 'daemon.yaml');
749
801
  if (fs.existsSync(daemonYamlPath)) {
@@ -752,13 +804,29 @@ try {
752
804
  const rows = Object.entries(projects)
753
805
  .filter(([, p]) => {
754
806
  if (!p || !p.name || !p.cwd) return false;
755
- // Expand ~ to home directory
756
807
  const expandedCwd = String(p.cwd).replace(/^~/, os.homedir());
757
808
  return fs.existsSync(expandedCwd);
758
809
  })
759
810
  .map(([key, p]) => `| \`${key}\` | ${p.name} |`);
760
811
  if (rows.length > 0) {
761
- dispatchTable = '\n\n| project_key | 昵称 |\n|-------------|------|\n' + rows.join('\n') + '\n\n`--new` 强制新建会话(用户说"新开会话"时加此参数)。';
812
+ const tableContent = [
813
+ '# Agent Dispatch 路由表',
814
+ '',
815
+ '> 自动生成,来源:daemon.yaml。勿手动编辑。',
816
+ '',
817
+ '| project_key | 昵称 |',
818
+ '|-------------|------|',
819
+ ...rows,
820
+ '',
821
+ '## 使用方法',
822
+ '```bash',
823
+ '~/.metame/bin/dispatch_to [--new] <project_key> "内容"',
824
+ '```',
825
+ '`--new` 强制新建会话(用户说"新开会话"时加此参数)。',
826
+ '新增 Agent:`/agent bind <名称> <工作目录>`',
827
+ ].join('\n') + '\n';
828
+ fs.mkdirSync(path.dirname(DISPATCH_TABLE_PATH), { recursive: true });
829
+ fs.writeFileSync(DISPATCH_TABLE_PATH, tableContent);
762
830
  }
763
831
  }
764
832
  } catch { /* daemon.yaml missing or invalid — skip dispatch table */ }
@@ -771,11 +839,11 @@ const KERNEL_BODY = PROTOCOL_NORMAL
771
839
 
772
840
  const CAPABILITY_SECTIONS = [
773
841
  '## Agent Dispatch',
774
- `"告诉X/让X" → \`~/.metame/bin/dispatch_to <project_key> "内容"\`,手机端 \`/dispatch to <key> <消息>\`。` + dispatchTable,
775
- '新增 Agent:`/agent bind <名称> <工作目录>`',
842
+ '识别到"告诉X/让X/通知X"等转发意图时先 `cat ~/.metame/docs/dispatch-table.md` 获取路由表(昵称→project_key),再执行转发。不要凭记忆猜测昵称对应关系。',
776
843
  '',
777
844
  '## Agent 创建与管理',
778
845
  '用户问创建/管理/绑定 Agent 时 → 先 `cat ~/.metame/docs/agent-guide.md` 再回答。',
846
+ '用户问代码结构/升级进度/脚本入口时 → 先 `cat ~/.metame/docs/pointer-map.md` 再回答。',
779
847
  '',
780
848
  '## 手机端文件交互',
781
849
  '用户要文件("发给我"/"发过来"/"导出")→ 先 `cat ~/.metame/docs/file-transfer.md` 再执行。',
@@ -882,7 +950,37 @@ try {
882
950
  }
883
951
  } catch { /* non-fatal */ }
884
952
 
953
+ // Skill evolution status
954
+ try {
955
+ const skillChangelog = require('./scripts/skill-changelog');
956
+ const skillCount = skillChangelog.countInstalledSkills();
957
+ const lastSession = skillChangelog.getLastSessionStart();
958
+ const recentChanges = skillChangelog.getRecentChanges(lastSession);
959
+
960
+ if (recentChanges.length === 0) {
961
+ console.log(`${icon("tool")} Skills: ${skillCount} installed · 无新变更`);
962
+ } else {
963
+ const evolved = recentChanges.filter(c => c.action === 'evolved');
964
+ const others = recentChanges.filter(c => c.action !== 'evolved');
965
+ const parts = [`${skillCount} installed`];
966
+ if (evolved.length > 0) parts.push(`${evolved.length} evolved since last session`);
967
+ if (others.length > 0) parts.push(`${others.length} other event${others.length > 1 ? 's' : ''}`);
968
+ console.log(`${icon("tool")} Skills: ${parts.join(' · ')}`);
969
+
970
+ // Show up to 3 details
971
+ const shown = recentChanges.slice(0, 3);
972
+ for (const c of shown) {
973
+ const actionIcon = skillChangelog.getActionIcon(c.action);
974
+ console.log(` ${actionIcon} ${c.skill || 'system'}: ${c.summary}`);
975
+ }
976
+ if (recentChanges.length > 3) {
977
+ console.log(` +${recentChanges.length - 3} more`);
978
+ }
979
+ }
885
980
 
981
+ // Write session start marker for next time
982
+ skillChangelog.writeSessionStart();
983
+ } catch { /* non-fatal */ }
886
984
 
887
985
  // ---------------------------------------------------------
888
986
  // 4.9 AUTO-UPDATE CHECK (non-blocking)
@@ -1806,7 +1904,12 @@ WantedBy=default.target
1806
1904
  }
1807
1905
 
1808
1906
  // Unknown subcommand
1809
- console.log(`${icon("book")} MetaMe Daemon Commands:`);
1907
+ console.log(`${icon("book")} MetaMe Commands:`);
1908
+ console.log(" metame — launch Claude with MetaMe init");
1909
+ console.log(" metame codex [args] — launch Codex with MetaMe init");
1910
+ console.log(" metame continue — resume latest session");
1911
+ console.log("");
1912
+ console.log(`${icon("book")} Daemon Commands:`);
1810
1913
  console.log(" metame start — start background daemon");
1811
1914
  console.log(" metame stop — stop daemon");
1812
1915
  console.log(" metame status — show status & budget");
@@ -1824,7 +1927,43 @@ WantedBy=default.target
1824
1927
  }
1825
1928
 
1826
1929
  // ---------------------------------------------------------
1827
- // 5.8 CONTINUE/SYNCresume latest session from terminal
1930
+ // 5.8 CODEXlaunch Codex with MetaMe initialization
1931
+ // ---------------------------------------------------------
1932
+ const isCodex = process.argv[2] === 'codex';
1933
+ if (isCodex) {
1934
+ // spawn() resolves PATH automatically; error event handles missing binary
1935
+ const codexBin = 'codex';
1936
+
1937
+ // Build codex args: remaining user args after 'codex'
1938
+ const codexUserArgs = process.argv.slice(3);
1939
+ let codexArgs;
1940
+ if (codexUserArgs.length === 0) {
1941
+ // Interactive mode: `codex --full-auto`
1942
+ codexArgs = ['--full-auto'];
1943
+ } else {
1944
+ // Non-interactive: `codex exec --full-auto <user args>`
1945
+ codexArgs = ['exec', '--full-auto', ...codexUserArgs];
1946
+ }
1947
+
1948
+ const codexChild = spawn(codexBin, codexArgs, {
1949
+ stdio: 'inherit',
1950
+ cwd: process.cwd(),
1951
+ env: { ...process.env, METAME_ACTIVE_SESSION: 'true' },
1952
+ });
1953
+
1954
+ codexChild.on('error', () => {
1955
+ console.error(`\n${icon("fail")} Error: Could not launch 'codex'.`);
1956
+ console.error(" Please install: npm install -g @openai/codex");
1957
+ });
1958
+ codexChild.on('close', (code) => process.exit(code || 0));
1959
+
1960
+ // Background distillation
1961
+ spawnDistillBackground();
1962
+ return;
1963
+ }
1964
+
1965
+ // ---------------------------------------------------------
1966
+ // 5.9 CONTINUE/SYNC — resume latest session from terminal
1828
1967
  // ---------------------------------------------------------
1829
1968
  // Usage: exit Claude first, then run `metame continue` from terminal.
1830
1969
  // Finds the most recent session and launches Claude with --resume.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "metame-cli",
3
- "version": "1.4.33",
3
+ "version": "1.5.0",
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": {
@@ -8,12 +8,15 @@
8
8
  },
9
9
  "files": [
10
10
  "index.js",
11
- "scripts/"
11
+ "scripts/",
12
+ "!scripts/*.test.js",
13
+ "!scripts/test_daemon.js",
14
+ "!scripts/hooks/test-*.js"
12
15
  ],
13
16
  "scripts": {
14
17
  "test": "node --test scripts/*.test.js",
15
18
  "start": "node index.js",
16
- "sync:plugin": "cp scripts/platform.js scripts/schema.js scripts/pending-traits.js scripts/signal-capture.js scripts/distill.js scripts/daemon.js scripts/daemon-agent-commands.js scripts/daemon-session-commands.js scripts/daemon-admin-commands.js scripts/daemon-exec-commands.js scripts/daemon-ops-commands.js scripts/daemon-session-store.js scripts/daemon-checkpoints.js scripts/daemon-bridges.js scripts/daemon-file-browser.js scripts/daemon-runtime-lifecycle.js scripts/daemon-notify.js scripts/daemon-claude-engine.js scripts/daemon-command-router.js scripts/daemon-user-acl.js scripts/daemon-agent-tools.js scripts/daemon-task-scheduler.js scripts/daemon-task-envelope.js scripts/task-board.js scripts/telegram-adapter.js scripts/feishu-adapter.js scripts/daemon-default.yaml scripts/providers.js scripts/utils.js scripts/usage-classifier.js scripts/resolve-yaml.js scripts/memory.js scripts/memory-extract.js scripts/qmd-client.js scripts/session-summarize.js scripts/session-analytics.js scripts/skill-evolution.js scripts/check-macos-control-capabilities.sh plugin/scripts/ && echo '✅ Plugin scripts synced'",
19
+ "sync:plugin": "cp scripts/platform.js scripts/schema.js scripts/pending-traits.js scripts/signal-capture.js scripts/distill.js scripts/daemon.js scripts/daemon-agent-commands.js scripts/daemon-session-commands.js scripts/daemon-admin-commands.js scripts/daemon-exec-commands.js scripts/daemon-ops-commands.js scripts/daemon-session-store.js scripts/daemon-checkpoints.js scripts/daemon-bridges.js scripts/daemon-file-browser.js scripts/daemon-runtime-lifecycle.js scripts/daemon-notify.js scripts/daemon-claude-engine.js scripts/daemon-engine-runtime.js scripts/daemon-command-router.js scripts/daemon-user-acl.js scripts/daemon-agent-tools.js scripts/daemon-task-scheduler.js scripts/daemon-task-envelope.js scripts/task-board.js scripts/telegram-adapter.js scripts/feishu-adapter.js scripts/daemon-default.yaml scripts/providers.js scripts/utils.js scripts/usage-classifier.js scripts/resolve-yaml.js scripts/memory.js scripts/memory-write.js scripts/memory-extract.js scripts/memory-search.js scripts/memory-gc.js scripts/memory-nightly-reflect.js scripts/memory-index.js scripts/qmd-client.js scripts/session-summarize.js scripts/session-analytics.js scripts/mentor-engine.js scripts/skill-evolution.js scripts/skill-changelog.js scripts/check-macos-control-capabilities.sh plugin/scripts/ && echo '✅ Plugin scripts synced'",
17
20
  "sync:readme": "node scripts/sync-readme.js",
18
21
  "restart:daemon": "node index.js stop 2>/dev/null; sleep 1; node index.js start 2>/dev/null || echo '⚠️ Daemon not running or restart failed'",
19
22
  "precommit": "npm run sync:plugin && npm run restart:daemon"
@@ -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 };