metame-cli 1.4.18 → 1.4.20

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.
@@ -18,8 +18,10 @@ function createAgentCommandHandler(deps) {
18
18
  sessionLabel,
19
19
  loadSessionTags,
20
20
  sessionRichLabel,
21
+ getSessionRecentContext,
21
22
  pendingBinds,
22
23
  pendingAgentFlows,
24
+ pendingActivations,
23
25
  doBindAgent,
24
26
  mergeAgentRole,
25
27
  agentTools,
@@ -28,6 +30,30 @@ function createAgentCommandHandler(deps) {
28
30
  agentBindTtlMs,
29
31
  } = deps;
30
32
 
33
+ // Pending activations have no TTL — they persist until consumed.
34
+ // The creating chatId is stored to prevent self-activation.
35
+
36
+ function storePendingActivation(agentKey, agentName, cwd, createdByChatId) {
37
+ if (!pendingActivations) return;
38
+ pendingActivations.set(agentKey, {
39
+ agentKey, agentName, cwd,
40
+ createdByChatId: String(createdByChatId),
41
+ createdAt: Date.now(),
42
+ });
43
+ }
44
+
45
+ // Returns the latest pending activation, excluding the creating chat
46
+ function getLatestActivationForChat(chatId) {
47
+ if (!pendingActivations || pendingActivations.size === 0) return null;
48
+ const cid = String(chatId);
49
+ let latest = null;
50
+ for (const rec of pendingActivations.values()) {
51
+ if (rec.createdByChatId === cid) continue; // creating chat cannot self-activate
52
+ if (!latest || rec.createdAt > latest.createdAt) latest = rec;
53
+ }
54
+ return latest;
55
+ }
56
+
31
57
  function resolveTtl(valueOrGetter, fallbackMs) {
32
58
  const raw = typeof valueOrGetter === 'function' ? valueOrGetter() : valueOrGetter;
33
59
  const num = Number(raw);
@@ -135,9 +161,15 @@ function createAgentCommandHandler(deps) {
135
161
  return { ok: true, data: legacy };
136
162
  }
137
163
 
138
- async function createAgentViaUnifiedApi(chatId, name, dir, roleDesc) {
164
+ async function createAgentViaUnifiedApi(chatId, name, dir, roleDesc, opts = {}) {
165
+ // Default: skip binding the creating chat — let the target group activate via /activate
166
+ const { skipChatBinding = true } = opts;
139
167
  if (agentTools && typeof agentTools.createNewWorkspaceAgent === 'function') {
140
- return agentTools.createNewWorkspaceAgent(name, dir, roleDesc, chatId);
168
+ const res = await agentTools.createNewWorkspaceAgent(name, dir, roleDesc, chatId, { skipChatBinding });
169
+ if (res.ok && skipChatBinding && res.data && res.data.projectKey) {
170
+ storePendingActivation(res.data.projectKey, name, res.data.cwd, chatId);
171
+ }
172
+ return res;
141
173
  }
142
174
  const bound = await doBindAgent({ sendMessage: async () => {} }, chatId, name, dir);
143
175
  if (!bound || bound.ok === false) {
@@ -249,45 +281,30 @@ function createAgentCommandHandler(deps) {
249
281
  saveState(state2);
250
282
  const name = fullMatch.customTitle;
251
283
  const label = name || (fullMatch.summary || fullMatch.firstPrompt || '').slice(0, 40) || sessionId.slice(0, 8);
252
- await bot.sendMessage(chatId, `Resumed: ${label}\nWorkdir: ${cwd}`);
253
- return true;
254
- }
255
284
 
256
- // /agent new wizard state machine (kept for command compatibility)
257
- {
258
- const flow = getFreshFlow(String(chatId));
259
- if (flow && flow.step === 'name' && text && !text.startsWith('/')) {
260
- flow.name = text.trim();
261
- flow.step = 'desc';
262
- setFlow(String(chatId), flow);
263
- await bot.sendMessage(chatId, `好的,Agent 名称是「${flow.name}」\n\n请描述这个 Agent 的角色和职责(用自然语言):`);
264
- return true;
265
- }
266
- if (flow && flow.step === 'desc' && text && !text.startsWith('/')) {
267
- pendingAgentFlows.delete(String(chatId));
268
- const { dir, name } = flow;
269
- const description = text.trim();
270
- await bot.sendMessage(chatId, `⏳ 正在配置 Agent「${name}」,稍等...`);
271
- const created = await createAgentViaUnifiedApi(chatId, name, dir, description);
272
- if (!created.ok) {
273
- await bot.sendMessage(chatId, `❌ 创建 Agent 失败: ${created.error}`);
274
- return true;
285
+ // 读取最近对话片段,帮助确认是否切换到正确的 session
286
+ const recentCtx = getSessionRecentContext ? getSessionRecentContext(sessionId) : null;
287
+ let msg = `✅ 已切换: **${label}**\n📁 ${path.basename(cwd)}`;
288
+ if (recentCtx) {
289
+ if (recentCtx.lastUser) {
290
+ const snippet = recentCtx.lastUser.replace(/\n/g, ' ').slice(0, 80);
291
+ msg += `\n\n💬 上次你说: _${snippet}${recentCtx.lastUser.length > 80 ? '…' : ''}_`;
275
292
  }
276
- if (created.data && created.data.cwd && typeof attachOrCreateSession === 'function') {
277
- attachOrCreateSession(chatId, normalizeCwd(created.data.cwd), name || '');
278
- }
279
- const roleInfo = created.data.role || {};
280
- if (roleInfo.skipped) {
281
- await bot.sendMessage(chatId, '✅ Agent 创建成功');
282
- } else if (roleInfo.created) {
283
- await bot.sendMessage(chatId, '📝 已创建 CLAUDE.md 并写入角色定义');
284
- } else {
285
- await bot.sendMessage(chatId, '📝 已将角色定义合并进现有 CLAUDE.md');
293
+ if (recentCtx.lastAssistant) {
294
+ const snippet = recentCtx.lastAssistant.replace(/\n/g, ' ').slice(0, 80);
295
+ msg += `\n🤖 上次回复: ${snippet}${recentCtx.lastAssistant.length > 80 ? '…' : ''}`;
286
296
  }
287
- return true;
288
297
  }
298
+ if (bot.sendMarkdown) {
299
+ await bot.sendMarkdown(chatId, msg);
300
+ } else {
301
+ await bot.sendMessage(chatId, msg.replace(/[_*`]/g, ''));
302
+ }
303
+ return true;
289
304
  }
290
305
 
306
+ // wizard state machine removed — use natural language to create agents
307
+
291
308
  // /agent edit wait-input flow (kept for command compatibility)
292
309
  {
293
310
  const editFlow = getFreshFlow(String(chatId) + ':edit');
@@ -336,7 +353,7 @@ function createAgentCommandHandler(deps) {
336
353
  }
337
354
  const agents = res.data.agents || [];
338
355
  if (agents.length === 0) {
339
- await bot.sendMessage(chatId, '暂无已配置的 Agent。\n使用 /agent new 创建,或 /agent bind <名称> 绑定目录。');
356
+ await bot.sendMessage(chatId, '暂无已配置的 Agent。\n用自然语言说"创建一个agent,目录是~/xxx",或 /agent bind <名称> <目录>。');
340
357
  return true;
341
358
  }
342
359
  const lines = ['📋 已配置的 Agent:', ''];
@@ -354,19 +371,12 @@ function createAgentCommandHandler(deps) {
354
371
  return true;
355
372
  }
356
373
 
357
- // /agent new (wizard)
358
- if (agentSub === 'new') {
359
- setFlow(String(chatId), { step: 'dir' });
360
- await sendBrowse(bot, chatId, 'agent-new', HOME, '步骤1/3:选择这个 Agent 的工作目录');
361
- return true;
362
- }
363
-
364
374
  // /agent edit [描述]
365
375
  if (agentSub === 'edit') {
366
376
  const cfg = loadConfig();
367
377
  const { boundProj } = getBoundProject(chatId, cfg);
368
378
  if (!boundProj || !boundProj.cwd) {
369
- await bot.sendMessage(chatId, '❌ 当前群未绑定 Agent,请先使用 /agent bind /agent new');
379
+ await bot.sendMessage(chatId, '❌ 当前群未绑定 Agent,请先用自然语言创建 Agent 或 /agent bind <名称> <目录>');
370
380
  return true;
371
381
  }
372
382
  const cwd = normalizeCwd(boundProj.cwd);
@@ -413,7 +423,7 @@ function createAgentCommandHandler(deps) {
413
423
  const cfg = loadConfig();
414
424
  const { boundProj } = getBoundProject(chatId, cfg);
415
425
  if (!boundProj || !boundProj.cwd) {
416
- await bot.sendMessage(chatId, '❌ 当前群未绑定 Agent,请先使用 /agent bind /agent new');
426
+ await bot.sendMessage(chatId, '❌ 当前群未绑定 Agent,请先用自然语言创建 Agent 或 /agent bind <名称> <目录>');
417
427
  return true;
418
428
  }
419
429
  const cwd = normalizeCwd(boundProj.cwd);
@@ -438,7 +448,7 @@ function createAgentCommandHandler(deps) {
438
448
  const projects = config.projects || {};
439
449
  const entries = Object.entries(projects).filter(([, p]) => p.cwd);
440
450
  if (entries.length === 0) {
441
- await bot.sendMessage(chatId, '暂无已配置的 Agent。\n使用 /agent new 新建,或 /agent bind <名称> 绑定目录。');
451
+ await bot.sendMessage(chatId, '暂无已配置的 Agent。\n用自然语言说"创建一个agent,目录是~/xxx",或 /agent bind <名称> <目录>。');
442
452
  return true;
443
453
  }
444
454
  const currentSession = getSession(chatId);
@@ -453,6 +463,41 @@ function createAgentCommandHandler(deps) {
453
463
  }
454
464
  }
455
465
 
466
+ // /activate — bind this unbound chat to the most recently created pending agent
467
+ if (text === '/activate' || text.startsWith('/activate ')) {
468
+ const cfg = loadConfig();
469
+ const { boundKey } = getBoundProject(chatId, cfg);
470
+ if (boundKey) {
471
+ await bot.sendMessage(chatId, `此群已绑定到「${boundKey}」,无需激活。如需更换请先 /agent unbind`);
472
+ return true;
473
+ }
474
+ const activation = getLatestActivationForChat(chatId);
475
+ if (!activation) {
476
+ // Check if this chat was the creator (self-activate attempt)
477
+ if (pendingActivations) {
478
+ for (const rec of pendingActivations.values()) {
479
+ if (rec.createdByChatId === String(chatId)) {
480
+ await bot.sendMessage(chatId,
481
+ `❌ 不能在创建来源群激活。\n请在你新建的目标群里发送 \`/activate\`\n\n` +
482
+ `或在任意群用: \`/agent bind ${rec.agentName} ${rec.cwd}\``
483
+ );
484
+ return true;
485
+ }
486
+ }
487
+ }
488
+ // No pending activation at all — guide to manual bind
489
+ await bot.sendMessage(chatId,
490
+ '没有待激活的 Agent。\n\n如果已创建过 Agent,直接用:\n`/agent bind <名称> <目录>`\n即可绑定,不需要重新创建。'
491
+ );
492
+ return true;
493
+ }
494
+ const bindRes = await bindViaUnifiedApi(bot, chatId, activation.agentName, activation.cwd);
495
+ if (bindRes.ok) {
496
+ pendingActivations && pendingActivations.delete(activation.agentKey);
497
+ }
498
+ return true;
499
+ }
500
+
456
501
  // /agent-bind-dir <path>: internal callback for bind picker
457
502
  if (text.startsWith('/agent-bind-dir ')) {
458
503
  const dirPath = expandPath(text.slice(16).trim());
@@ -466,22 +511,6 @@ function createAgentCommandHandler(deps) {
466
511
  return true;
467
512
  }
468
513
 
469
- // /agent-dir <path>: internal callback for /agent new wizard
470
- if (text.startsWith('/agent-dir ')) {
471
- const dirPath = expandPath(text.slice(11).trim());
472
- const flow = getFreshFlow(String(chatId));
473
- if (!flow || flow.step !== 'dir') {
474
- await bot.sendMessage(chatId, '❌ 没有待完成的 /agent new,请重新发送 /agent new');
475
- return true;
476
- }
477
- flow.dir = dirPath;
478
- flow.step = 'name';
479
- setFlow(String(chatId), flow);
480
- const displayPath = dirPath.replace(HOME, '~');
481
- await bot.sendMessage(chatId, `✓ 已选择目录:${displayPath}\n\n步骤2/3:给这个 Agent 起个名字?`);
482
- return true;
483
- }
484
-
485
514
  return false;
486
515
  }
487
516
 
@@ -37,7 +37,7 @@ function createAgentTools(deps) {
37
37
  if (!cfg[adapterKey].chat_agent_map) cfg[adapterKey].chat_agent_map = {};
38
38
  }
39
39
 
40
- async function bindAgentToChat(chatId, agentName, workspaceDir) {
40
+ async function bindAgentToChat(chatId, agentName, workspaceDir, { force = false } = {}) {
41
41
  try {
42
42
  const safeName = sanitizeText(agentName, 120);
43
43
  if (!safeName) return { ok: false, error: 'agentName is required' };
@@ -64,6 +64,16 @@ function createAgentTools(deps) {
64
64
  return { ok: false, error: `workspaceDir is not a directory: ${resolvedDir}` };
65
65
  }
66
66
 
67
+ // Overwrite protection: reject if chat is already bound to a different agent
68
+ const existingKey = cfg[adapterKey].chat_agent_map[String(chatId)];
69
+ if (existingKey && existingKey !== projectKey && !force) {
70
+ return {
71
+ ok: false,
72
+ error: `此群已绑定到 "${existingKey}",如需覆盖请使用 force:true`,
73
+ data: { existingKey },
74
+ };
75
+ }
76
+
67
77
  const idVal = typeof chatId === 'number' ? chatId : String(chatId);
68
78
  if (!cfg[adapterKey].allowed_chat_ids.includes(idVal)) cfg[adapterKey].allowed_chat_ids.push(idVal);
69
79
 
@@ -165,30 +175,57 @@ ${safeDelta}
165
175
  }
166
176
  }
167
177
 
168
- async function createNewWorkspaceAgent(agentName, workspaceDir, roleDescription, chatId) {
169
- const bindResult = await bindAgentToChat(chatId, agentName, workspaceDir);
170
- if (!bindResult.ok) return bindResult;
178
+ async function createNewWorkspaceAgent(agentName, workspaceDir, roleDescription, chatId, { skipChatBinding = false } = {}) {
179
+ let bindData;
180
+
181
+ if (skipChatBinding) {
182
+ // Create the project entry without touching chat_agent_map
183
+ const safeName = sanitizeText(agentName, 120);
184
+ if (!safeName) return { ok: false, error: 'agentName is required' };
185
+ const resolvedDir = resolveWorkspaceDir(workspaceDir);
186
+ if (!resolvedDir) return { ok: false, error: 'workspaceDir is required' };
187
+ if (!fs.existsSync(resolvedDir) || !fs.statSync(resolvedDir).isDirectory()) {
188
+ return { ok: false, error: `workspaceDir not found or not a directory: ${resolvedDir}` };
189
+ }
190
+ const cfg = loadConfig();
191
+ if (!cfg.projects) cfg.projects = {};
192
+ const projectKey = toProjectKey(safeName, chatId);
193
+ const existed = !!cfg.projects[projectKey];
194
+ if (!existed) {
195
+ cfg.projects[projectKey] = { name: safeName, cwd: resolvedDir, nicknames: [safeName] };
196
+ writeConfigSafe(cfg);
197
+ backupConfig();
198
+ }
199
+ bindData = {
200
+ projectKey,
201
+ cwd: resolvedDir,
202
+ isNewProject: !existed,
203
+ chatId: null, // not bound to any chat
204
+ project: cfg.projects[projectKey],
205
+ };
206
+ } else {
207
+ const bindResult = await bindAgentToChat(chatId, agentName, workspaceDir);
208
+ if (!bindResult.ok) return bindResult;
209
+ bindData = bindResult.data;
210
+ }
171
211
 
172
212
  const roleText = sanitizeText(roleDescription, 1200);
173
213
  if (!roleText) {
174
- return { ok: true, data: { ...bindResult.data, role: { skipped: true } } };
214
+ return { ok: true, data: { ...bindData, role: { skipped: true } } };
175
215
  }
176
216
 
177
- const roleResult = await editAgentRoleDefinition(bindResult.data.cwd, roleText);
217
+ const roleResult = await editAgentRoleDefinition(bindData.cwd, roleText);
178
218
  if (!roleResult.ok) {
179
219
  return {
180
220
  ok: false,
181
- error: `agent bound but role update failed: ${roleResult.error}`,
182
- data: { ...bindResult.data, roleError: roleResult.error },
221
+ error: `agent created but role update failed: ${roleResult.error}`,
222
+ data: { ...bindData, roleError: roleResult.error },
183
223
  };
184
224
  }
185
225
 
186
226
  return {
187
227
  ok: true,
188
- data: {
189
- ...bindResult.data,
190
- role: roleResult.data,
191
- },
228
+ data: { ...bindData, role: roleResult.data },
192
229
  };
193
230
  }
194
231
 
@@ -12,8 +12,29 @@ function createBridgeStarter(deps) {
12
12
  saveState,
13
13
  getSession,
14
14
  handleCommand,
15
+ pendingActivations, // optional — used to show smart activation hint
15
16
  } = deps;
16
17
 
18
+ // Returns the best pending activation for a given chatId (excludes self-created)
19
+ function getPendingActivationForChat(chatId) {
20
+ if (!pendingActivations || pendingActivations.size === 0) return null;
21
+ const cid = String(chatId);
22
+ let latest = null;
23
+ for (const rec of pendingActivations.values()) {
24
+ if (rec.createdByChatId === cid) continue;
25
+ if (!latest || rec.createdAt > latest.createdAt) latest = rec;
26
+ }
27
+ return latest;
28
+ }
29
+
30
+ function unauthorizedMsg(chatId, useSend) {
31
+ const pending = getPendingActivationForChat(chatId);
32
+ if (pending) {
33
+ return `⚠️ 此群未授权\n\n发送以下命令激活 Agent「${pending.agentName}」:\n\`/activate\``;
34
+ }
35
+ return '⚠️ 此群未授权\n\n如已创建 Agent,发送 `/activate` 完成绑定。\n否则请先在主群创建 Agent。';
36
+ }
37
+
17
38
  async function startTelegramBridge(config, executeTaskByName) {
18
39
  if (!config.telegram || !config.telegram.enabled) return null;
19
40
  if (!config.telegram.bot_token) {
@@ -68,13 +89,13 @@ function createBridgeStarter(deps) {
68
89
  const trimmedText = msg.text && msg.text.trim();
69
90
  const isBindCmd = trimmedText && (
70
91
  trimmedText.startsWith('/agent bind')
71
- || trimmedText.startsWith('/agent new')
72
92
  || trimmedText.startsWith('/agent-bind-dir')
73
93
  || trimmedText.startsWith('/browse bind')
94
+ || trimmedText === '/activate'
74
95
  );
75
96
  if (!allowedIds.includes(chatId) && !isBindCmd) {
76
97
  log('WARN', `Rejected message from unauthorized chat: ${chatId}`);
77
- bot.sendMessage(chatId, '⚠️ This chat is not authorized.\n\nCopy and send this command to register:\n\n/agent bind personal').catch(() => {});
98
+ bot.sendMessage(chatId, unauthorizedMsg(chatId)).catch(() => {});
78
99
  continue;
79
100
  }
80
101
 
@@ -157,15 +178,14 @@ function createBridgeStarter(deps) {
157
178
  const trimmedText = text && text.trim();
158
179
  const isBindCmd = trimmedText && (
159
180
  trimmedText.startsWith('/agent bind')
160
- || trimmedText.startsWith('/agent new')
161
181
  || trimmedText.startsWith('/agent-bind-dir')
162
182
  || trimmedText.startsWith('/browse bind')
183
+ || trimmedText === '/activate'
163
184
  );
164
185
  if (!allowedIds.includes(chatId) && !isBindCmd) {
165
186
  log('WARN', `Feishu: rejected message from ${chatId}`);
166
- (bot.sendMarkdown
167
- ? bot.sendMarkdown(chatId, '⚠️ 此会话未授权\n\n复制发送以下命令注册:\n\n/agent bind personal')
168
- : bot.sendMessage(chatId, '⚠️ 此会话未授权\n\n复制发送以下命令注册:\n\n/agent bind personal')).catch(() => {});
187
+ const msg = unauthorizedMsg(chatId);
188
+ (bot.sendMarkdown ? bot.sendMarkdown(chatId, msg) : bot.sendMessage(chatId, msg)).catch(() => {});
169
189
  return;
170
190
  }
171
191