metame-cli 1.5.21 → 1.5.23

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.
@@ -30,6 +30,7 @@ function createSessionCommandHandler(deps) {
30
30
  sessionRichLabel,
31
31
  buildSessionCardElements,
32
32
  getSessionRecentContext,
33
+ getSessionRecentDialogue,
33
34
  getDefaultEngine = () => 'claude',
34
35
  } = deps;
35
36
 
@@ -430,11 +431,24 @@ function createSessionCommandHandler(deps) {
430
431
  const recentCtx = typeof getSessionRecentContext === 'function'
431
432
  ? getSessionRecentContext(target.sessionId)
432
433
  : null;
434
+ const recentDialogue = typeof getSessionRecentDialogue === 'function'
435
+ ? getSessionRecentDialogue(target.sessionId, 4)
436
+ : null;
433
437
  const title = target.customTitle || target.summary || target.sessionId.slice(0, 8);
434
438
  const lines = [`▶️ Resumed: ${title}`];
435
439
  if (attached.cwd) lines.push(`📁 ${path.basename(attached.cwd)}`);
436
- if (recentCtx && recentCtx.lastUser) lines.push(`👤 ${String(recentCtx.lastUser).replace(/\n/g, ' ').slice(0, 80)}`);
437
- if (recentCtx && recentCtx.lastAssistant) lines.push(`🤖 ${String(recentCtx.lastAssistant).replace(/\n/g, ' ').slice(0, 80)}`);
440
+ if (Array.isArray(recentDialogue) && recentDialogue.length > 0) {
441
+ lines.push('');
442
+ lines.push('最近对话:');
443
+ recentDialogue.forEach((item) => {
444
+ const marker = item.role === 'assistant' ? '🤖' : '👤';
445
+ const text = String(item.text || '').replace(/\n/g, ' ').slice(0, 120);
446
+ if (text) lines.push(`${marker} ${text}`);
447
+ });
448
+ } else {
449
+ if (recentCtx && recentCtx.lastUser) lines.push(`👤 ${String(recentCtx.lastUser).replace(/\n/g, ' ').slice(0, 80)}`);
450
+ if (recentCtx && recentCtx.lastAssistant) lines.push(`🤖 ${String(recentCtx.lastAssistant).replace(/\n/g, ' ').slice(0, 80)}`);
451
+ }
438
452
  await bot.sendMessage(chatId, lines.join('\n'));
439
453
  return true;
440
454
  }
@@ -352,6 +352,29 @@ function createSessionStore(deps) {
352
352
  return '';
353
353
  }
354
354
 
355
+ function extractRecentClaudeDialogueFromLines(lines, maxMessages = 4) {
356
+ const collected = [];
357
+ for (const line of lines) {
358
+ if (!line) continue;
359
+ try {
360
+ const d = JSON.parse(line);
361
+ if (d.type === 'user' && d.message && d.userType !== 'internal') {
362
+ const raw = _extractMessageText(d);
363
+ if (raw.length > 2) {
364
+ collected.push({ role: 'user', text: raw.slice(0, 160) });
365
+ }
366
+ } else if (d.type === 'assistant' && d.message) {
367
+ const raw = _extractMessageText(d);
368
+ if (raw.length > 2) {
369
+ collected.push({ role: 'assistant', text: raw.slice(0, 160) });
370
+ }
371
+ }
372
+ } catch { /* skip */ }
373
+ if (collected.length >= maxMessages) break;
374
+ }
375
+ return collected.reverse();
376
+ }
377
+
355
378
  function scanClaudeSessions() {
356
379
  try {
357
380
  if (!fs.existsSync(CLAUDE_PROJECTS_DIR)) return [];
@@ -638,6 +661,59 @@ function createSessionStore(deps) {
638
661
  }
639
662
  }
640
663
 
664
+ function parseCodexSessionRecentDialogue(sessionFile, maxMessages = 4) {
665
+ try {
666
+ if (!sessionFile || !fs.existsSync(sessionFile)) return [];
667
+ const lines = fs.readFileSync(sessionFile, 'utf8').split('\n').filter(Boolean);
668
+ const items = [];
669
+ let pendingAssistant = '';
670
+
671
+ function pushDialogueItem(role, text) {
672
+ const clean = String(text || '').trim();
673
+ if (!clean) return;
674
+ const clipped = clean.slice(0, 160);
675
+ const prev = items[items.length - 1];
676
+ if (prev && prev.role === role) {
677
+ prev.text = clipped;
678
+ return;
679
+ }
680
+ items.push({ role, text: clipped });
681
+ }
682
+
683
+ for (const line of lines) {
684
+ let entry;
685
+ try {
686
+ entry = JSON.parse(line);
687
+ } catch {
688
+ continue;
689
+ }
690
+ if (entry.type === 'response_item' && entry.payload && entry.payload.type === 'message') {
691
+ const role = String(entry.payload.role || '').toLowerCase();
692
+ if (role !== 'user' && role !== 'assistant') continue;
693
+ const text = stripCodexInjectedHints(extractCodexMessageText(entry.payload.content || entry.payload));
694
+ if (!text) continue;
695
+ if (role === 'user') {
696
+ if (pendingAssistant) {
697
+ pushDialogueItem('assistant', pendingAssistant);
698
+ pendingAssistant = '';
699
+ }
700
+ pushDialogueItem('user', text);
701
+ } else {
702
+ pendingAssistant = '';
703
+ pushDialogueItem('assistant', text);
704
+ }
705
+ } else if (entry.type === 'event_msg' && entry.payload && entry.payload.type === 'agent_message') {
706
+ const text = stripCodexInjectedHints(extractCodexMessageText(entry.payload.message));
707
+ if (text) pendingAssistant = text;
708
+ }
709
+ }
710
+ if (pendingAssistant) pushDialogueItem('assistant', pendingAssistant);
711
+ return items.slice(-maxMessages);
712
+ } catch {
713
+ return [];
714
+ }
715
+ }
716
+
641
717
  function enrichCodexSession(session) {
642
718
  if (!session || session._enriched) return session;
643
719
  try {
@@ -1173,6 +1249,32 @@ function createSessionStore(deps) {
1173
1249
  } catch { return null; }
1174
1250
  }
1175
1251
 
1252
+ function getSessionRecentDialogue(sessionId, maxMessages = 4) {
1253
+ try {
1254
+ const limit = Math.max(1, Math.min(Number(maxMessages) || 4, 8));
1255
+ const sessionFile = findSessionFile(sessionId);
1256
+ if (sessionFile) {
1257
+ const stat = fs.statSync(sessionFile);
1258
+ const tailSize = Math.min(262144, stat.size);
1259
+ const buf = Buffer.alloc(tailSize);
1260
+ const fd = fs.openSync(sessionFile, 'r');
1261
+ try {
1262
+ fs.readSync(fd, buf, 0, tailSize, stat.size - tailSize);
1263
+ } finally {
1264
+ fs.closeSync(fd);
1265
+ }
1266
+ const lines = buf.toString('utf8').split('\n').reverse();
1267
+ const dialogue = extractRecentClaudeDialogueFromLines(lines, limit);
1268
+ return dialogue.length ? dialogue : null;
1269
+ }
1270
+ const codexFile = findCodexSessionFile(sessionId);
1271
+ const dialogue = parseCodexSessionRecentDialogue(codexFile, limit);
1272
+ return dialogue.length ? dialogue : null;
1273
+ } catch {
1274
+ return null;
1275
+ }
1276
+ }
1277
+
1176
1278
  function markSessionStarted(chatId, engine) {
1177
1279
  const state = loadState();
1178
1280
  const s = state.sessions[chatId];
@@ -1368,6 +1470,7 @@ function createSessionStore(deps) {
1368
1470
  writeSessionName,
1369
1471
  markSessionStarted,
1370
1472
  getSessionRecentContext,
1473
+ getSessionRecentDialogue,
1371
1474
  isEngineSessionValid,
1372
1475
  getCodexSessionSandboxProfile,
1373
1476
  getCodexSessionPermissionMode,
@@ -1378,6 +1481,7 @@ function createSessionStore(deps) {
1378
1481
  stripCodexInjectedHints,
1379
1482
  looksLikeInternalCodexPrompt,
1380
1483
  parseCodexSessionPreview,
1484
+ parseCodexSessionRecentDialogue,
1381
1485
  buildPendingStateSessions,
1382
1486
  },
1383
1487
  };
@@ -0,0 +1,146 @@
1
+ 'use strict';
2
+
3
+ const VALID_TEAM_COLORS = ['green', 'yellow', 'red', 'blue', 'purple', 'orange', 'pink', 'indigo'];
4
+
5
+ function parseTeamMembers(input, teamName) {
6
+ const memberLines = String(input || '').split(/[,,\n]/).filter(line => line.trim());
7
+ const members = [];
8
+
9
+ for (const line of memberLines) {
10
+ const parts = line.trim().split(':');
11
+ const name = parts[0] && parts[0].trim();
12
+ if (!name) continue;
13
+
14
+ const icon = (parts[1] && parts[1].trim()) || '🤖';
15
+ const rawColor = parts[2] && parts[2].trim().toLowerCase();
16
+ const color = VALID_TEAM_COLORS.includes(rawColor)
17
+ ? rawColor
18
+ : VALID_TEAM_COLORS[members.length % VALID_TEAM_COLORS.length];
19
+
20
+ members.push({
21
+ key: name,
22
+ name: `${teamName} · ${name}`,
23
+ icon,
24
+ color,
25
+ nicknames: [name],
26
+ });
27
+ }
28
+
29
+ return members;
30
+ }
31
+
32
+ function findParentProjectKey({ projects, dirPath, normalizeCwd }) {
33
+ if (!projects || !dirPath) return null;
34
+ const targetDir = normalizeCwd(dirPath);
35
+
36
+ for (const [projKey, proj] of Object.entries(projects)) {
37
+ if (normalizeCwd(proj && proj.cwd ? proj.cwd : '') === targetDir) {
38
+ return projKey;
39
+ }
40
+ }
41
+
42
+ return null;
43
+ }
44
+
45
+ function ensureTeamMemberWorkspace({ fs, path, execSync, teamDir, teamName, member }) {
46
+ const memberDir = path.join(teamDir, member.key);
47
+ if (!fs.existsSync(memberDir)) fs.mkdirSync(memberDir, { recursive: true });
48
+
49
+ const claudeMdPath = path.join(memberDir, 'CLAUDE.md');
50
+ if (!fs.existsSync(claudeMdPath)) {
51
+ fs.writeFileSync(claudeMdPath, `# ${member.name}\n\n(团队成员:${teamName})\n`, 'utf8');
52
+ }
53
+
54
+ try {
55
+ if (typeof execSync === 'function') execSync('git init -q', { cwd: memberDir, stdio: 'ignore' });
56
+ } catch {
57
+ // Git init is a best-effort enhancement for checkpoints.
58
+ }
59
+
60
+ return {
61
+ ...member,
62
+ cwd: memberDir,
63
+ };
64
+ }
65
+
66
+ function registerTeamMembers({
67
+ cfg,
68
+ parentProjectKey,
69
+ members,
70
+ writeConfigSafe,
71
+ backupConfig,
72
+ }) {
73
+ if (!parentProjectKey || !cfg.projects || !cfg.projects[parentProjectKey]) return null;
74
+
75
+ const proj = cfg.projects[parentProjectKey];
76
+ if (!Array.isArray(proj.team)) proj.team = [];
77
+
78
+ for (const member of members) {
79
+ if (proj.team.some(existing => existing && existing.key === member.key)) continue;
80
+ proj.team.push({
81
+ key: member.key,
82
+ name: member.name,
83
+ icon: member.icon,
84
+ color: member.color,
85
+ cwd: member.cwd,
86
+ nicknames: member.nicknames,
87
+ });
88
+ }
89
+
90
+ if (typeof writeConfigSafe === 'function') writeConfigSafe(cfg);
91
+ if (typeof backupConfig === 'function') backupConfig();
92
+ return parentProjectKey;
93
+ }
94
+
95
+ function createTeamWorkspace({
96
+ fs,
97
+ path,
98
+ execSync,
99
+ dirPath,
100
+ teamName,
101
+ members,
102
+ loadConfig,
103
+ normalizeCwd,
104
+ writeConfigSafe,
105
+ backupConfig,
106
+ HOME,
107
+ }) {
108
+ const teamDir = path.join(dirPath, 'team');
109
+ if (!fs.existsSync(teamDir)) fs.mkdirSync(teamDir, { recursive: true });
110
+
111
+ const createdMembers = members.map((member) => ensureTeamMemberWorkspace({
112
+ fs,
113
+ path,
114
+ execSync,
115
+ teamDir,
116
+ teamName,
117
+ member,
118
+ }));
119
+
120
+ const cfg = typeof loadConfig === 'function' ? loadConfig() : { projects: {} };
121
+ const parentProjectKey = findParentProjectKey({
122
+ projects: cfg.projects,
123
+ dirPath,
124
+ normalizeCwd,
125
+ });
126
+
127
+ registerTeamMembers({
128
+ cfg,
129
+ parentProjectKey,
130
+ members: createdMembers,
131
+ writeConfigSafe,
132
+ backupConfig,
133
+ });
134
+
135
+ return {
136
+ teamDir,
137
+ parentProjectKey,
138
+ memberLines: createdMembers.map((member) => `${member.icon} ${member.key}: ${member.cwd.replace(HOME, '~')}`),
139
+ };
140
+ }
141
+
142
+ module.exports = {
143
+ VALID_TEAM_COLORS,
144
+ parseTeamMembers,
145
+ createTeamWorkspace,
146
+ };
package/scripts/daemon.js CHANGED
@@ -36,9 +36,11 @@ const fs = require('fs');
36
36
  const path = require('path');
37
37
  const os = require('os');
38
38
  const { execSync, execFileSync, execFile, spawn } = require('child_process');
39
+ const { bootstrapRuntimeModulePaths } = require('./runtime-bootstrap');
39
40
 
40
41
  const HOME = os.homedir();
41
42
  const METAME_DIR = path.join(HOME, '.metame');
43
+ bootstrapRuntimeModulePaths(METAME_DIR);
42
44
  const CONFIG_FILE = path.join(METAME_DIR, 'daemon.yaml');
43
45
  const STATE_FILE = path.join(METAME_DIR, 'daemon_state.json');
44
46
  const PID_FILE = path.join(METAME_DIR, 'daemon.pid');
@@ -507,6 +509,7 @@ function createNullBot(onOutput) {
507
509
  deleteMessage: async () => { },
508
510
  sendFile: noop,
509
511
  downloadFile: noop,
512
+ notifyFinalOutput: async (text) => { if (onOutput) onOutput({ body: text, final: true }); return { message_id: '_virtual' }; },
510
513
  };
511
514
  }
512
515
 
@@ -610,6 +613,7 @@ function createStreamForwardBot(realBot, chatId, onOutput = null, opts = {}) {
610
613
  deleteMessage: async (_, msgId) => { await waitUntilReady(); return realBot.deleteMessage(chatId, msgId); },
611
614
  sendFile: async (_, filePath, caption) => { await waitUntilReady(); return realBot.sendFile(chatId, filePath, caption); },
612
615
  downloadFile: async (...args) => realBot.downloadFile(...args),
616
+ notifyFinalOutput: async (text) => { if (onOutput) onOutput({ body: text, final: true }); return { message_id: '_virtual' }; },
613
617
  };
614
618
  }
615
619
 
@@ -897,10 +901,11 @@ function dispatchTask(targetProject, message, config, replyFn, streamOptions = n
897
901
 
898
902
  let _taskFinalized = false;
899
903
  const outputHandler = (output) => {
904
+ const isFinalOutput = !!(output && typeof output === 'object' && output.final);
900
905
  const outStr = typeof output === 'object' ? (output.body || JSON.stringify(output)) : String(output);
901
906
  const displayOut = envelope ? appendTeamTaskResumeHint(outStr, envelope.task_id, envelope.scope_id) : outStr;
902
907
  log('INFO', `Dispatch output from ${targetProject}: ${outStr.slice(0, 200)}`);
903
- if (envelope && taskBoard && !_taskFinalized && outStr.trim().length > 2) {
908
+ if (!isFinalOutput && envelope && taskBoard && !_taskFinalized && outStr.trim().length > 2) {
904
909
  const status = inferTaskStatusFromOutput(outStr);
905
910
  const artifacts = extractArtifactPaths(outStr);
906
911
  const update = {
@@ -916,9 +921,9 @@ function dispatchTask(targetProject, message, config, replyFn, streamOptions = n
916
921
  });
917
922
  _taskFinalized = true;
918
923
  }
919
- if (replyFn && outStr.trim().length > 2) {
924
+ if (!isFinalOutput && replyFn && outStr.trim().length > 2) {
920
925
  replyFn(displayOut);
921
- } else if (!replyFn && fullMsg.callback && fullMsg.from && config) {
926
+ } else if (!isFinalOutput && !replyFn && fullMsg.callback && fullMsg.from && config) {
922
927
  // Write result to sender's inbox before dispatching callback
923
928
  try {
924
929
  const inboxDir = path.join(os.homedir(), '.metame', 'memory', 'inbox', fullMsg.from);
@@ -954,6 +959,7 @@ function dispatchTask(targetProject, message, config, replyFn, streamOptions = n
954
959
 
955
960
  // ── Reactive lifecycle hook ──
956
961
  try {
962
+ if (!isFinalOutput) return;
957
963
  handleReactiveOutput(targetProject, outStr, loadConfig(), {
958
964
  log,
959
965
  loadState,
@@ -1761,6 +1767,7 @@ const {
1761
1767
  sessionLabel,
1762
1768
  sessionRichLabel,
1763
1769
  getSessionRecentContext,
1770
+ getSessionRecentDialogue,
1764
1771
  buildSessionCardElements,
1765
1772
  getSession,
1766
1773
  getSessionForEngine,
@@ -2010,6 +2017,8 @@ const { handleSessionCommand } = createSessionCommandHandler({
2010
2017
  loadSessionTags,
2011
2018
  sessionRichLabel,
2012
2019
  buildSessionCardElements,
2020
+ getSessionRecentContext,
2021
+ getSessionRecentDialogue,
2013
2022
  sessionLabel,
2014
2023
  getDefaultEngine,
2015
2024
  });
@@ -2047,6 +2056,7 @@ const { spawnClaudeAsync, askClaude } = createClaudeEngine({
2047
2056
  findSessionFile,
2048
2057
  listRecentSessions,
2049
2058
  getSessionRecentContext,
2059
+ getSessionRecentDialogue,
2050
2060
  isEngineSessionValid,
2051
2061
  getCodexSessionSandboxProfile,
2052
2062
  getCodexSessionPermissionMode,
@@ -2118,6 +2128,7 @@ const { handleAgentCommand } = createAgentCommandHandler({
2118
2128
  loadSessionTags,
2119
2129
  sessionRichLabel,
2120
2130
  getSessionRecentContext,
2131
+ getSessionRecentDialogue,
2121
2132
  pendingBinds,
2122
2133
  pendingAgentFlows,
2123
2134
  pendingTeamFlows,
@@ -1,4 +1,4 @@
1
- # MetaMe Hook / Intent Engine 配置手册
1
+ # MetaMe Intent 配置手册
2
2
 
3
3
  > 自动部署到 `~/.metame/docs/hook-config.md`。源文件:`scripts/docs/hook-config.md`。只编辑 `scripts/`,不要直接改 `~/.metame/`。
4
4
 
@@ -8,15 +8,17 @@
8
8
 
9
9
  ```
10
10
  UserPromptSubmit (每轮用户输入)
11
- ├── signal-capture.js → 捕获用户偏好信号(写文件,不注入)
12
- └── intent-engine.js → 意图检测 + 按需注入 additionalSystemPrompt
11
+ └── signal-capture.js → 捕获用户偏好信号(写文件,不注入)
12
+
13
+ Daemon runtime (每轮真正发给引擎前)
14
+ └── intent-registry.js → 意图检测 + 按需拼接提示块
13
15
 
14
16
  Stop (每轮结束)
15
17
  └── stop-session-capture.js → session 事件日志 + 工具失败捕获
16
18
  ```
17
19
 
18
20
  `scripts/intent-registry.js` 是单一维护源,负责调用各意图模块并返回提示块。
19
- `intent-engine.js` Claude hook adapter;daemon 里的 Codex 路径也复用同一 registry。
21
+ daemon 在运行时直接复用同一 registry,Claude / Codex 共用这一条注入路径。
20
22
  零匹配 → 零输出(不浪费 token)。
21
23
 
22
24
  ---
@@ -25,13 +27,20 @@ Stop (每轮结束)
25
27
 
26
28
  | 模块 key | 文件 | 触发条件 | 注入内容 |
27
29
  |---------|------|---------|---------|
30
+ | `agent_capability` | `intent-agent-capability.js` | 明确的 Agent 创建/绑定/激活/分身/团队/角色/Soul 管理语境 | 当前真实支持的 `/agent ...` `/activate` 能力提示 |
28
31
  | `team_dispatch` | `intent-team-dispatch.js` | 检测到"告诉/让/发给 + 成员名"等联络意图 | `dispatch_to` 命令提示(仅匹配成员) |
29
32
  | `ops_assist` | `intent-ops-assist.js` | 回退/日志/重启/gc/状态 相关语境 | `/undo` `/restart` `/logs` `/gc` `/status` 命令提示 |
30
- | `task_create` | `intent-task-create.js` | 定时/提醒/每天X点 等调度语境 | `/task-add` 命令用法提示 |
31
33
  | `file_transfer` | `intent-file-transfer.js` | "发给我/发过来/导出" 等文件传输语境 | `[[FILE:...]]` 协议 + 收发规则 |
32
34
  | `weixin_bridge` | `intent-weixin-bridge.js` | "帮我绑定微信/配置微信桥接/开启微信接入/开始微信扫码登录" 等明确桥接语境 | 开启 `weixin.enabled` + `/weixin` 绑定流程提示 |
33
- | `memory_recall` | `intent-memory-recall.js` | "上次/之前/还记得" 等跨会话回忆语境 | `memory-search.js` 命令用法 |
34
- | `doc_router` | `intent-doc-router.js` | "创建/绑定 Agent"、"代码结构/脚本入口"、"hook/intent 配置" 等文档导向语境 | 统一 doc-router 文档指引 |
35
+ | `memory_recall` | `intent-memory-recall.js` | "上次/之前/还记得" 等跨会话回忆语境 | `memory-search.js` CLI 召回提示 |
36
+ | `doc_router` | `intent-doc-router.js` | "代码结构/脚本入口"、"hook/intent 配置" 等文档导向语境 | 统一 doc-router 文档指引 |
37
+ | `perpetual` | `intent-perpetual.js` | 永续/reactive 任务语境 | `dispatch_to` / `/status perpetual` 等永续协议提示 |
38
+ | `research` | `intent-research.js` | `paper_rev` 项目下的研究语境 | 研究方法与文件落点提示 |
39
+
40
+ 注入策略:
41
+ - 零匹配 → 零输出
42
+ - 多模块命中时按优先级裁剪,默认最多注入 2 个 hint、总长不超过约 1200 字符
43
+ - `doc_router` 只作为 fallback;只要已有真实能力提示命中,就不再额外塞文档路由
35
44
  ---
36
45
 
37
46
  ## 开关控制
@@ -40,13 +49,13 @@ Stop (每轮结束)
40
49
 
41
50
  ```yaml
42
51
  hooks:
52
+ agent_capability: true
43
53
  team_dispatch: true # 改为 false 可禁用
44
54
  ops_assist: true
45
- task_create: false # 禁用任务调度提示
46
55
  weixin_bridge: true # 默认开启;只匹配明确的微信配置/绑定语境
47
56
  ```
48
57
 
49
- 改完立即生效(intent-engine 每次运行时读取)。
58
+ 改完立即生效(daemon 每次发请求前都会读取)。
50
59
  **不需要重启 daemon。**
51
60
 
52
61
  ---
@@ -70,19 +79,20 @@ module.exports = function detect<Name>(prompt, config, projectKey) {
70
79
  };
71
80
  ```
72
81
 
73
- 2. **注册到 intent-engine.js**:
82
+ 2. **注册到 intent-registry.js**:
74
83
 
75
84
  ```js
76
- // scripts/hooks/intent-engine.js
85
+ // scripts/intent-registry.js
77
86
  const INTENT_MODULES = {
78
- team_dispatch: './intent-team-dispatch',
79
- ops_assist: './intent-ops-assist',
80
- task_create: './intent-task-create',
81
- your_name: './intent-<name>', // ← 加这行
87
+ agent_capability: require('./hooks/intent-agent-capability'),
88
+ team_dispatch: require('./hooks/intent-team-dispatch'),
89
+ ops_assist: require('./hooks/intent-ops-assist'),
90
+ your_name: require('./hooks/intent-<name>'), // ← 加这行
82
91
  };
83
92
 
84
93
  const DEFAULTS = {
85
94
  // ...
95
+ agent_capability: true,
86
96
  your_name: true, // ← 加这行(默认开启)
87
97
  };
88
98
  ```
@@ -96,11 +106,14 @@ hooks:
96
106
  your_name: true
97
107
  ```
98
108
 
99
- 4. **部署**:`node index.js`(同步文件到 `~/.metame/hooks/`)
109
+ 4. **部署**:`node index.js`
100
110
 
101
111
  5. **验证**:
102
112
  ```bash
103
- echo '{"prompt":"触发词"}' | node ~/.metame/hooks/intent-engine.js
113
+ node - <<'EOF'
114
+ const { buildIntentHintBlock } = require('./scripts/intent-registry');
115
+ console.log(buildIntentHintBlock('触发词', {}, ''));
116
+ EOF
104
117
  ```
105
118
 
106
119
  ---
@@ -109,7 +122,16 @@ echo '{"prompt":"触发词"}' | node ~/.metame/hooks/intent-engine.js
109
122
 
110
123
  ```bash
111
124
  # 测试某个 prompt 是否触发意图
112
- echo '{"prompt":"告诉工匠去做这个"}' | METAME_PROJECT=business node ~/.metame/hooks/intent-engine.js | python3 -m json.tool
125
+ node - <<'EOF'
126
+ const { buildIntentHintBlock } = require('./scripts/intent-registry');
127
+ console.log(buildIntentHintBlock('告诉工匠去做这个', {
128
+ projects: {
129
+ business: {
130
+ team: [{ key: 'builder', name: '工匠', nicknames: ['工匠'] }],
131
+ },
132
+ },
133
+ }, 'business'));
134
+ EOF
113
135
 
114
136
  # 查看当前已注册的 hooks
115
137
  python3 -c "
@@ -128,9 +150,7 @@ for k, v in s.get('hooks', {}).items():
128
150
 
129
151
  | 文件 | 说明 |
130
152
  |------|------|
131
- | `scripts/intent-registry.js` | 共享意图注册表(Claude hook / Codex runtime 共用) |
132
- | `scripts/hooks/intent-engine.js` | Claude hook adapter(源文件) |
133
- | `~/.metame/hooks/intent-engine.js` | 部署副本(copy) |
153
+ | `scripts/intent-registry.js` | 共享意图注册表(daemon 运行时唯一入口) |
134
154
  | `scripts/hooks/intent-*.js` | 各意图模块(源文件) |
135
155
  | `~/.metame/daemon.yaml` | 用户配置(包含 `hooks:` 开关) |
136
156
  | `~/.claude/settings.json` | Claude Code hook 注册表 |
@@ -179,7 +179,7 @@ feishu:
179
179
 
180
180
  在手机端(飞书/Telegram)发送以下任一方式触发创建向导:
181
181
 
182
- - 自然语言:`创建团队`、`新建工作组`、`建个团队` 等(`_detectTeamIntent` 识别,位于 `daemon-command-router.js`)
182
+ - 自然语言:`创建团队`、`新建工作组`、`建个团队` 等(`daemon-agent-intent.js` 统一识别并路由)
183
183
  - 命令:`/agent new team`
184
184
 
185
185
  向导分三步,全部在 `daemon-agent-commands.js` 中实现:
@@ -354,7 +354,7 @@ Claude 看到 hook 注入:
354
354
  | `daemon-admin-commands.js` | `/dispatch peers` 查看配置 + `/dispatch to peer:project` 手动派发 |
355
355
  | `scripts/bin/dispatch_to` | 支持 `peer:project` 格式 → 写 `remote-pending.jsonl` |
356
356
  | `daemon-team-dispatch.js` | `buildTeamRosterHint()` 为远端成员生成 `peer:key` 格式命令 |
357
- | `hooks/team-context.js` | intent hook 注入远端 `peer:key` dispatch 命令 |
357
+ | `hooks/intent-team-dispatch.js` | daemon 意图注入远端 `peer:key` dispatch 命令 |
358
358
 
359
359
  ### 管理命令
360
360
 
@@ -13,7 +13,7 @@
13
13
  |------|--------|------|
14
14
  | `session-analytics.js` | daemon-claude-engine, distill, memory-extract | 会话分析核心库 |
15
15
  | `mentor-engine.js` | daemon-claude-engine, daemon-admin-commands | AI 导师引擎 |
16
- | `intent-registry.js` | daemon-claude-engine, hooks/intent-engine | 意图识别注册表 |
16
+ | `intent-registry.js` | daemon-claude-engine | 意图识别注册表(daemon 运行时注入) |
17
17
  | `daemon-command-session-route.js` | daemon-exec-commands, daemon-ops-commands | 会话路由解析 |
18
18
  | `daemon-siri-bridge.js` | daemon-bridges.js | Siri HTTP 桥接 |
19
19
  | `daemon-siri-imessage.js` | daemon-siri-bridge.js | iMessage 数据库读取 |
@@ -53,7 +53,7 @@
53
53
  - `scripts/daemon-agent-tools.js`
54
54
  - 关键点:自然语言提取 `codex` 关键词;默认 `claude` 不写 `engine` 字段,仅 `codex` 持久化 `engine: codex`;
55
55
  `bindAgentToChat()` 自动调用 `ensureAgentMetadata()` 建立 soul 层;
56
- `_detectTeamIntent()` 自然语言意图识别(含负样本过滤),识别"建团队"意图后自动路由到 `/agent new team` 向导
56
+ `daemon-agent-intent.js` 统一处理 Agent/团队自然语言入口(含负样本过滤、Windows 路径识别、显式动作优先)
57
57
 
58
58
  - 会话命令与兼容边界:
59
59
  - `scripts/daemon-exec-commands.js`
@@ -113,7 +113,7 @@
113
113
  按昵称解析到远端 member 时自动走 `sendRemoteDispatch`
114
114
 
115
115
  - Intent Hook:
116
- - `scripts/hooks/team-context.js`
116
+ - `scripts/hooks/intent-team-dispatch.js`
117
117
  - 关键点:检测通信意图 → 注入 dispatch_to 命令提示;远端成员自动带 `peer:key` 前缀
118
118
 
119
119
  ## Mentor Mode(Step 1-4)定位
@@ -0,0 +1,51 @@
1
+ 'use strict';
2
+
3
+ const { classifyAgentIntent } = require('../agent-intent-shared');
4
+
5
+ function buildLines(action) {
6
+ switch (action) {
7
+ case 'create':
8
+ return [
9
+ '- 创建并绑定当前群:直接用自然语言说“给这个群创建一个 Agent,目录是 ~/repo”',
10
+ '- 创建待激活 Agent:说“创建一个 codex agent,目录是 ~/repo”,再去目标群发 `/activate`',
11
+ ];
12
+ case 'bind':
13
+ return [
14
+ '- 绑定现有 Agent:`/agent bind <名称> <目录>`',
15
+ '- 也可直接说“给这个群绑定一个 Agent,目录是 ~/repo”',
16
+ ];
17
+ case 'list':
18
+ return ['- 查看已配置 Agent:`/agent list`'];
19
+ case 'unbind':
20
+ return ['- 解绑当前群:`/agent unbind`'];
21
+ case 'edit_role':
22
+ return ['- 修改当前 Agent 角色:`/agent edit <描述>`,或直接用自然语言说“把当前 agent 角色改成 ...”'];
23
+ case 'reset':
24
+ return ['- 清空当前 Agent 的角色定义:`/agent reset`'];
25
+ case 'soul':
26
+ return [
27
+ '- 查看当前 Soul:`/agent soul`',
28
+ '- 修复 Soul 文件:`/agent soul repair`',
29
+ '- 覆盖编辑 Soul:`/agent soul edit <内容>`',
30
+ ];
31
+ case 'activate':
32
+ return ['- 在新群完成绑定:进入目标群发送 `/activate`'];
33
+ case 'wizard_clone':
34
+ return ['- 创建当前 Agent 的分身:`/agent new clone`'];
35
+ case 'wizard_team':
36
+ return ['- 创建团队工作区:`/agent new team`'];
37
+ case 'agent_doc':
38
+ return ['- Agent 配置/管理文档:先看 `~/.metame/docs/agent-guide.md`'];
39
+ default:
40
+ return [];
41
+ }
42
+ }
43
+
44
+ module.exports = function detectAgentCapability(prompt) {
45
+ const intent = classifyAgentIntent(prompt);
46
+ if (!intent) return null;
47
+
48
+ const lines = buildLines(intent.action);
49
+ if (lines.length === 0) return null;
50
+ return ['[Agent 能力提示]', ...lines].join('\n');
51
+ };