metame-cli 1.4.10 → 1.4.13

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.
@@ -0,0 +1,395 @@
1
+ 'use strict';
2
+
3
+ function createCommandRouter(deps) {
4
+ const {
5
+ loadState,
6
+ loadConfig,
7
+ checkBudget,
8
+ checkCooldown,
9
+ routeAgent,
10
+ normalizeCwd,
11
+ attachOrCreateSession,
12
+ handleSessionCommand,
13
+ handleAgentCommand,
14
+ handleAdminCommand,
15
+ handleExecCommand,
16
+ handleOpsCommand,
17
+ askClaude,
18
+ providerMod,
19
+ getNoSleepProcess,
20
+ activeProcesses,
21
+ messageQueue,
22
+ sleep,
23
+ log,
24
+ agentTools,
25
+ pendingAgentFlows,
26
+ agentFlowTtlMs,
27
+ } = deps;
28
+
29
+ function resolveFlowTtlMs() {
30
+ const raw = typeof agentFlowTtlMs === 'function' ? agentFlowTtlMs() : agentFlowTtlMs;
31
+ const num = Number(raw);
32
+ return Number.isFinite(num) && num > 0 ? num : (10 * 60 * 1000);
33
+ }
34
+
35
+ function hasFreshPendingFlow(flowKey) {
36
+ if (!pendingAgentFlows) return false;
37
+ const flow = pendingAgentFlows.get(flowKey);
38
+ if (!flow) return false;
39
+
40
+ const ttlMs = resolveFlowTtlMs();
41
+ const now = Date.now();
42
+ const ts = Number(flow && flow.__ts || 0);
43
+ if (ts > 0 && (now - ts) > ttlMs) {
44
+ pendingAgentFlows.delete(flowKey);
45
+ return false;
46
+ }
47
+
48
+ // Backfill timestamp for legacy flow objects so they can expire later.
49
+ if (!(ts > 0) && flow && typeof flow === 'object') {
50
+ pendingAgentFlows.set(flowKey, { ...flow, __ts: now });
51
+ }
52
+ return true;
53
+ }
54
+
55
+ function extractQuotedContent(input) {
56
+ const m = String(input || '').match(/[“"'「](.+?)[”"'」]/);
57
+ return m ? m[1].trim() : '';
58
+ }
59
+
60
+ function extractPathFromText(input) {
61
+ const m = String(input || '').match(/(?:~\/|\/)[^\s,。;;!!??"“”'‘’`]+/);
62
+ if (!m) return '';
63
+ return m[0].replace(/[,。;;!!??]+$/, '');
64
+ }
65
+
66
+ function extractAgentName(input) {
67
+ const text = String(input || '').trim();
68
+ const byNameField = text.match(/(?:名字|名称|叫做?|名为|named?)\s*(?:为)?\s*[“"'「]?([^\s,。;;!!??"“”'‘’`]+)[”"'」]?/i);
69
+ if (byNameField) return byNameField[1].trim();
70
+ const byBind = text.match(/(?:bind|绑定)\s*(?:到|为|成)?\s*[“"'「]?([a-zA-Z0-9_\-\u4e00-\u9fa5]+)[”"'」]?/i);
71
+ if (byBind) return byBind[1].trim();
72
+ return '';
73
+ }
74
+
75
+ function deriveAgentName(input, workspaceDir) {
76
+ const explicit = extractAgentName(input);
77
+ if (explicit) return explicit;
78
+ if (workspaceDir) {
79
+ const segs = workspaceDir.split('/').filter(Boolean);
80
+ if (segs.length > 0) return segs[segs.length - 1];
81
+ }
82
+ return 'workspace-agent';
83
+ }
84
+
85
+ function deriveRoleDelta(input) {
86
+ const text = String(input || '').trim();
87
+ const quoted = extractQuotedContent(text);
88
+ if (quoted) return quoted;
89
+ const byVerb = text.match(/(?:改成|改为|变成|设为|更新为)\s*[::]?\s*(.+)$/);
90
+ if (byVerb) return byVerb[1].trim();
91
+ return text;
92
+ }
93
+
94
+ function deriveCreateRoleDelta(input) {
95
+ const text = String(input || '').trim();
96
+ const quoted = extractQuotedContent(text);
97
+ if (quoted) return quoted;
98
+ const byRoleField = text.match(/(?:角色|职责|人设)\s*(?:是|为|:|:)?\s*(.+)$/i);
99
+ if (byRoleField) return byRoleField[1].trim();
100
+ return '';
101
+ }
102
+
103
+ function projectNameFromResult(data, fallbackName) {
104
+ if (data && data.project && data.project.name) return data.project.name;
105
+ if (data && data.projectKey) return data.projectKey;
106
+ return fallbackName || 'workspace-agent';
107
+ }
108
+
109
+ function getBoundProjectForChat(chatId, cfg) {
110
+ const map = {
111
+ ...(cfg.telegram ? cfg.telegram.chat_agent_map : {}),
112
+ ...(cfg.feishu ? cfg.feishu.chat_agent_map : {}),
113
+ };
114
+ const key = map[String(chatId)];
115
+ const proj = key && cfg.projects ? cfg.projects[key] : null;
116
+ return { key: key || null, project: proj || null };
117
+ }
118
+
119
+ async function tryHandleAgentIntent(bot, chatId, text, config) {
120
+ if (!agentTools || !text || text.startsWith('/')) return false;
121
+ const key = String(chatId);
122
+ if (hasFreshPendingFlow(key) || hasFreshPendingFlow(key + ':edit')) return false;
123
+ const input = text.trim();
124
+ if (!input) return false;
125
+
126
+ const hasAgentContext = /(agent|智能体|工作区|人设|绑定|当前群|这个群|chat|workspace)/i.test(input);
127
+ const wantsList = /(列出|查看|显示|有哪些|list|show)/i.test(input) && /(agent|智能体|工作区|绑定)/i.test(input);
128
+ const wantsUnbind = /(解绑|取消绑定|断开绑定|unbind|unassign)/i.test(input) && hasAgentContext;
129
+ const wantsEditRole =
130
+ ((/(角色|职责|人设)/i.test(input) && /(改|修改|调整|更新|变成|改成|改为)/i.test(input)) ||
131
+ /(把这个agent|把当前agent|当前群.*角色|当前群.*职责)/i.test(input));
132
+ const wantsCreate =
133
+ (/(创建|新建|新增|搞一个|加一个|create)/i.test(input) && /(agent|智能体|人设|工作区)/i.test(input));
134
+ const wantsBind =
135
+ !wantsCreate &&
136
+ (/(绑定|bind)/i.test(input) && hasAgentContext);
137
+
138
+ if (!wantsList && !wantsUnbind && !wantsEditRole && !wantsCreate && !wantsBind) {
139
+ return false;
140
+ }
141
+
142
+ if (wantsList) {
143
+ const res = await agentTools.listAllAgents(chatId);
144
+ if (!res.ok) {
145
+ await bot.sendMessage(chatId, `❌ 查询 Agent 失败: ${res.error}`);
146
+ return true;
147
+ }
148
+ const agents = res.data.agents || [];
149
+ if (agents.length === 0) {
150
+ await bot.sendMessage(chatId, '暂无已配置的 Agent。你可以直接说“给这个群创建一个 Agent,目录是 ~/xxx”。');
151
+ return true;
152
+ }
153
+ const lines = ['📋 当前 Agent 列表', ''];
154
+ for (const a of agents) {
155
+ const marker = a.key === res.data.boundKey ? ' ◀ 当前' : '';
156
+ lines.push(`${a.icon || '🤖'} ${a.name}${marker}`);
157
+ lines.push(`目录: ${a.cwd}`);
158
+ lines.push(`Key: ${a.key}`);
159
+ lines.push('');
160
+ }
161
+ await bot.sendMessage(chatId, lines.join('\n').trimEnd());
162
+ return true;
163
+ }
164
+
165
+ if (wantsUnbind) {
166
+ const res = await agentTools.unbindCurrentAgent(chatId);
167
+ if (!res.ok) {
168
+ await bot.sendMessage(chatId, `❌ 解绑失败: ${res.error}`);
169
+ return true;
170
+ }
171
+ if (res.data.unbound) {
172
+ await bot.sendMessage(chatId, `✅ 已解绑当前群(原 Agent: ${res.data.previousProjectKey})`);
173
+ } else {
174
+ await bot.sendMessage(chatId, '当前群没有绑定 Agent,无需解绑。');
175
+ }
176
+ return true;
177
+ }
178
+
179
+ if (wantsEditRole) {
180
+ const freshCfg = loadConfig();
181
+ const bound = getBoundProjectForChat(chatId, freshCfg);
182
+ if (!bound.project || !bound.project.cwd) {
183
+ await bot.sendMessage(chatId, '❌ 当前群未绑定 Agent。先说“给这个群绑定一个 Agent,目录是 ~/xxx”。');
184
+ return true;
185
+ }
186
+ const roleDelta = deriveRoleDelta(input);
187
+ const res = await agentTools.editAgentRoleDefinition(bound.project.cwd, roleDelta);
188
+ if (!res.ok) {
189
+ await bot.sendMessage(chatId, `❌ 更新角色失败: ${res.error}`);
190
+ return true;
191
+ }
192
+ await bot.sendMessage(chatId, res.data.created ? '✅ 已创建 CLAUDE.md 并写入角色定义' : '✅ 角色定义已更新到 CLAUDE.md');
193
+ return true;
194
+ }
195
+
196
+ if (wantsCreate) {
197
+ const workspaceDir = extractPathFromText(input);
198
+ if (!workspaceDir) {
199
+ await bot.sendMessage(chatId, '请补充工作目录,例如:`给这个群创建一个 Agent,目录是 ~/projects/foo`');
200
+ return true;
201
+ }
202
+ const agentName = deriveAgentName(input, workspaceDir);
203
+ const roleDelta = deriveCreateRoleDelta(input);
204
+ const res = await agentTools.createNewWorkspaceAgent(agentName, workspaceDir, roleDelta, chatId);
205
+ if (!res.ok) {
206
+ await bot.sendMessage(chatId, `❌ 创建 Agent 失败: ${res.error}`);
207
+ return true;
208
+ }
209
+ const data = res.data || {};
210
+ const projName = projectNameFromResult(data, agentName);
211
+ if (data.cwd) attachOrCreateSession(chatId, normalizeCwd(data.cwd), projName);
212
+ await bot.sendMessage(chatId, `✅ Agent 已创建并绑定\n名称: ${projName}\n目录: ${data.cwd || '(未知)'}`);
213
+ return true;
214
+ }
215
+
216
+ if (wantsBind) {
217
+ const workspaceDir = extractPathFromText(input);
218
+ const agentName = deriveAgentName(input, workspaceDir);
219
+ const res = await agentTools.bindAgentToChat(chatId, agentName, workspaceDir || null);
220
+ if (!res.ok) {
221
+ await bot.sendMessage(chatId, `❌ 绑定失败: ${res.error}`);
222
+ return true;
223
+ }
224
+ const data = res.data || {};
225
+ const projName = projectNameFromResult(data, agentName);
226
+ if (data.cwd) attachOrCreateSession(chatId, normalizeCwd(data.cwd), projName);
227
+ await bot.sendMessage(chatId, `✅ 已绑定 Agent\n名称: ${projName}\n目录: ${data.cwd || '(未知)'}`);
228
+ return true;
229
+ }
230
+
231
+ return false;
232
+ }
233
+
234
+ async function handleCommand(bot, chatId, text, config, executeTaskByName, senderId = null, readOnly = false) {
235
+ if (text && !text.startsWith('/chatid') && !text.startsWith('/myid')) log('INFO', `CMD [${String(chatId).slice(-8)}]: ${text.slice(0, 80)}`);
236
+ const state = loadState();
237
+
238
+ // --- /chatid: reply with current chatId ---
239
+ if (text === '/chatid') {
240
+ await bot.sendMessage(chatId, `Chat ID: \`${chatId}\``);
241
+ return;
242
+ }
243
+
244
+ // --- /myid: reply with sender's user open_id (for configuring operator_ids) ---
245
+ if (text === '/myid') {
246
+ await bot.sendMessage(chatId, senderId ? `Your ID: \`${senderId}\`` : 'ID not available (Telegram not supported)');
247
+ return;
248
+ }
249
+
250
+ // --- chat_agent_map: auto-switch agent based on dedicated chatId ---
251
+ // Configure in daemon.yaml: feishu.chat_agent_map or telegram.chat_agent_map
252
+ // e.g. chat_agent_map: { "oc_xxx": "personal", "oc_yyy": "metame" }
253
+ const chatAgentMap = { ...(config.telegram ? config.telegram.chat_agent_map : {}), ...(config.feishu ? config.feishu.chat_agent_map : {}) };
254
+ const _chatIdStr = String(chatId);
255
+ const mappedKey = chatAgentMap[_chatIdStr] ||
256
+ (_chatIdStr.startsWith('_agent_') ? _chatIdStr.slice(7) : null);
257
+ if (mappedKey && config.projects && config.projects[mappedKey]) {
258
+ const proj = config.projects[mappedKey];
259
+ const projCwd = normalizeCwd(proj.cwd);
260
+ const cur = loadState().sessions?.[chatId];
261
+ if (!cur || cur.cwd !== projCwd) {
262
+ attachOrCreateSession(chatId, projCwd, proj.name || mappedKey);
263
+ }
264
+ }
265
+
266
+ if (await handleSessionCommand({ bot, chatId, text })) {
267
+ return;
268
+ }
269
+
270
+ const agentResult = await handleAgentCommand({ bot, chatId, text, config });
271
+ if (agentResult === true || agentResult === null) {
272
+ return;
273
+ }
274
+
275
+ const adminResult = await handleAdminCommand({ bot, chatId, text, config, state });
276
+ if (adminResult.handled) {
277
+ config = adminResult.config || config;
278
+ return;
279
+ }
280
+
281
+ if (await handleExecCommand({ bot, chatId, text, config, executeTaskByName })) {
282
+ return;
283
+ }
284
+
285
+ if (await handleOpsCommand({ bot, chatId, text, config })) {
286
+ return;
287
+ }
288
+
289
+ if (text.startsWith('/')) {
290
+ const currentModel = (config.daemon && config.daemon.model) || 'opus';
291
+ const currentProvider = providerMod ? providerMod.getActiveName() : 'anthropic';
292
+ await bot.sendMessage(chatId, [
293
+ '📱 手机端 Claude Code',
294
+ '',
295
+ '⚡ 快速同步电脑工作:',
296
+ '/last — 继续电脑上最近的对话',
297
+ '/cd last — 切到电脑最近的项目目录',
298
+ '',
299
+ '🤖 Agent 管理:',
300
+ '/agent — 切换 Agent',
301
+ '/agent new — 向导新建 Agent',
302
+ '/agent bind <名称> [目录] — 绑定当前群',
303
+ '/agent list — 查看所有 Agent',
304
+ '/agent edit — 编辑当前 Agent 角色',
305
+ '/agent unbind — 解绑当前群',
306
+ '/agent reset — 重置当前 Agent 角色',
307
+ '',
308
+ '📂 Session 管理:',
309
+ '/new [path] [name] — 新建会话',
310
+ '/sessions — 浏览所有最近会话',
311
+ '/resume [name] — 选择/恢复会话',
312
+ '/name <name> — 命名当前会话',
313
+ '/cd <path> — 切换工作目录',
314
+ '/session — 查看当前会话',
315
+ '/stop — 中断当前任务 (ESC)',
316
+ '/undo — 选择历史消息,点击回退到该条之前',
317
+ '/undo <hash> — 回退到指定 git checkpoint',
318
+ '/quit — 结束会话,重新加载 MCP/配置',
319
+ '',
320
+ `⚙️ /model [${currentModel}] /provider [${currentProvider}] /status /tasks /run /budget /reload`,
321
+ '🧠 /memory — 记忆统计 · /memory <关键词> — 搜索事实',
322
+ `🔧 /doctor /fix /reset /sh <cmd> /nosleep [${getNoSleepProcess() ? 'ON' : 'OFF'}]`,
323
+ '',
324
+ '直接打字即可对话 💬',
325
+ ].join('\n'));
326
+ return;
327
+ }
328
+
329
+ // --- Natural language → Claude Code session ---
330
+ // If a task is running: interrupt + collect + merge
331
+ if (activeProcesses.has(chatId)) {
332
+ const isFirst = !messageQueue.has(chatId);
333
+ if (isFirst) {
334
+ messageQueue.set(chatId, { messages: [], timer: null });
335
+ }
336
+ const q = messageQueue.get(chatId);
337
+ q.messages.push(text);
338
+ // Only notify once (first message), subsequent ones silently queue
339
+ if (isFirst) {
340
+ await bot.sendMessage(chatId, '📝 收到,稍后一起处理');
341
+ }
342
+ // Interrupt the running Claude process
343
+ const proc = activeProcesses.get(chatId);
344
+ if (proc && proc.child && !proc.aborted) {
345
+ proc.aborted = true;
346
+ try { process.kill(-proc.child.pid, 'SIGINT'); } catch { proc.child.kill('SIGINT'); }
347
+ }
348
+ // Debounce: wait 5s for more messages before processing
349
+ if (q.timer) clearTimeout(q.timer);
350
+ q.timer = setTimeout(async () => {
351
+ // Wait for active process to fully exit (up to 10s)
352
+ for (let i = 0; i < 20 && activeProcesses.has(chatId); i++) {
353
+ await sleep(500);
354
+ }
355
+ const msgs = q.messages.splice(0);
356
+ messageQueue.delete(chatId);
357
+ if (msgs.length === 0) return;
358
+ const combined = msgs.join('\n');
359
+ log('INFO', `Processing ${msgs.length} queued message(s) for ${chatId}`);
360
+ try {
361
+ await handleCommand(bot, chatId, combined, config, executeTaskByName);
362
+ } catch (e) {
363
+ log('ERROR', `Queue dispatch failed: ${e.message}`);
364
+ }
365
+ }, 5000);
366
+ return;
367
+ }
368
+ // Nickname-only switch: bypass cooldown + budget (no Claude call)
369
+ const quickAgent = routeAgent(text, config);
370
+ if (quickAgent && !quickAgent.rest) {
371
+ const { key, proj } = quickAgent;
372
+ const projCwd = normalizeCwd(proj.cwd);
373
+ attachOrCreateSession(chatId, projCwd, proj.name || key);
374
+ log('INFO', `Agent switch via nickname: ${key} (${projCwd})`);
375
+ await bot.sendMessage(chatId, `${proj.icon || '🤖'} ${proj.name || key} 在线`);
376
+ return;
377
+ }
378
+
379
+ if (await tryHandleAgentIntent(bot, chatId, text, config)) {
380
+ return;
381
+ }
382
+
383
+ const cd = checkCooldown(chatId);
384
+ if (!cd.ok) { await bot.sendMessage(chatId, `${cd.wait}s`); return; }
385
+ if (!checkBudget(loadConfig(), loadState())) {
386
+ await bot.sendMessage(chatId, 'Daily token budget exceeded.');
387
+ return;
388
+ }
389
+ await askClaude(bot, chatId, text, config, readOnly);
390
+ }
391
+
392
+ return { handleCommand };
393
+ }
394
+
395
+ module.exports = { createCommandRouter };
@@ -42,11 +42,11 @@ heartbeat:
42
42
  notify: false
43
43
  enabled: true
44
44
 
45
- # 记忆提取:扫描未分析 session,提取长期事实和会话标签,2小时冷却
45
+ # 记忆提取:扫描未分析 session,提取长期事实和会话标签,4小时冷却(与 cognitive-distill 对齐)
46
46
  - name: memory-extract
47
47
  type: script
48
48
  command: node ~/.metame/memory-extract.js
49
- interval: 2h
49
+ interval: 4h
50
50
  timeout: 600
51
51
  require_idle: true
52
52
  notify: false