metame-cli 1.5.2 → 1.5.3

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/README.md CHANGED
@@ -316,6 +316,7 @@ systemctl --user start metame
316
316
  | **Auto-Provisioning** | First run deploys default CLAUDE.md, documentation, and `dispatch_to` to `~/.metame/`. Subsequent runs sync scripts without overwriting user config. |
317
317
  | **Heartbeat System** | Three-layer programmable nervous system. Layer 0 kernel always-on (zero config). Layer 1 system evolution built-in (5 tasks: distill + memory + skills + nightly reflection + memory index). Layer 2 your custom scheduled tasks with `require_idle`, `precondition`, `notify`, workflows. |
318
318
  | **Multi-Agent** | Multiple projects with dedicated chat groups. `/agent bind` for one-tap setup. True parallel execution. |
319
+ | **Team Routing** | Project-level team clones: multiple AI agents work in parallel within a single chat group. Nickname routing, sticky follow, `/stop` per member, broadcast visibility. |
319
320
  | **Browser Automation** | Built-in Playwright MCP. Browser control out of the box for every user. |
320
321
  | **Cross-Platform** | Native support for macOS and Windows. Platform abstraction layer handles spawn, IPC, process management, and terminal encoding automatically. |
321
322
  | **Provider Relay** | Route through any Anthropic-compatible API. Use GPT-4, DeepSeek, Gemini — zero config file mutation. |
@@ -416,6 +417,41 @@ All agents share your cognitive profile (`~/.claude_profile.yaml`) — they all
416
417
  ~/.metame/bin/dispatch_to coder "Run the test suite and report results"
417
418
  ```
418
419
 
420
+ ## Team Routing
421
+
422
+ MetaMe supports project-level team clones — multiple AI agents (digital twins) sharing the same workspace, working in parallel within a single Feishu group.
423
+
424
+ ### Configuration
425
+
426
+ Add a `team` array and `broadcast: true` under any project in `daemon.yaml`:
427
+
428
+ ```yaml
429
+ projects:
430
+ metame:
431
+ name: 超级总管 Jarvis
432
+ icon: 🤖
433
+ broadcast: true
434
+ team:
435
+ - key: jia
436
+ name: Jarvis · 甲
437
+ icon: 🤖
438
+ color: green
439
+ cwd: ~/AGI/MetaMe
440
+ nicknames:
441
+ - 甲
442
+ auto_dispatch: true
443
+ ```
444
+
445
+ ### Key Features
446
+
447
+ - **Nickname routing**: mention a member by nickname (e.g. "乙 check this") to route directly to them
448
+ - **Sticky follow**: once you address a member, subsequent messages without a nickname continue going to the same member
449
+ - **`/stop` precision**: `/stop 乙` stops a specific member; `/stop` stops the sticky member; reply-quote `/stop` stops the quoted member
450
+ - **Auto-dispatch**: when the main agent is busy, messages are automatically routed to idle `auto_dispatch` members
451
+ - **Broadcast**: with `broadcast: true`, inter-member `dispatch_to` messages are shown as cards in the group chat
452
+
453
+ Each team member runs on a virtual chatId (`_agent_{key}`) and appears with its own card title (e.g. `🤖 Jarvis · 乙`).
454
+
419
455
  ## Mobile Commands
420
456
 
421
457
  | Command | Action |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "metame-cli",
3
- "version": "1.5.2",
3
+ "version": "1.5.3",
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": {
@@ -15,7 +15,8 @@ const args = process.argv.slice(2);
15
15
  const newSession = args[0] === '--new' ? (args.shift(), true) : false;
16
16
 
17
17
  // --from <project_key>: identifies the calling agent for callback routing
18
- let fromKey = '_claude_session';
18
+ // Auto-detect from METAME_PROJECT env var (set by daemon when spawning agent sessions)
19
+ let fromKey = process.env.METAME_PROJECT || '_claude_session';
19
20
  const fromIdx = args.indexOf('--from');
20
21
  if (fromIdx !== -1 && args[fromIdx + 1]) {
21
22
  fromKey = args.splice(fromIdx, 2)[1];
@@ -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 } = require('./daemon-engine-runtime');
9
+ const { ENGINE_MODEL_CONFIG, resolveEngineModel } = require('./daemon-engine-runtime');
10
10
  let mentorEngine = null;
11
11
  try { mentorEngine = require('./mentor-engine'); } catch { /* optional */ }
12
12
 
@@ -46,6 +46,17 @@ function createAdminCommandHandler(deps) {
46
46
  ? proj.nicknames
47
47
  : (proj.nicknames ? [proj.nicknames] : []);
48
48
  if (key === targetName || nicknames.some(n => n === targetName)) return key;
49
+
50
+ // Also search team members (nested projects)
51
+ if (Array.isArray(proj.team)) {
52
+ for (const member of proj.team) {
53
+ const memberNicks = Array.isArray(member.nicknames) ? member.nicknames : [];
54
+ if (member.key === targetName || memberNicks.some(n => n === targetName)) {
55
+ // Return full path: parentKey/teamMemberKey
56
+ return `${key}/${member.key}`;
57
+ }
58
+ }
59
+ }
49
60
  }
50
61
  return null;
51
62
  }
@@ -669,6 +680,59 @@ function createAdminCommandHandler(deps) {
669
680
  return { handled: true, config };
670
681
  }
671
682
 
683
+ // /msg — team internal messaging (like sessions_send)
684
+ if (text.startsWith('/msg ')) {
685
+ const args = text.slice('/msg '.length).trim();
686
+ const msgMatch = args.match(/^(\S+)\s+(.+)$/s);
687
+ if (!msgMatch) {
688
+ await bot.sendMessage(chatId, '用法: /msg <agent> <消息内容>\n示例: /msg 甲 帮我看看这个文档');
689
+ return { handled: true, config };
690
+ }
691
+ const targetName = msgMatch[1];
692
+ const message = msgMatch[2].trim();
693
+
694
+ // Resolve target - check team members first, then projects
695
+ let targetKey = null;
696
+ const senderKey = resolveSenderKey(chatId, config);
697
+ const senderProj = config.projects ? config.projects[senderKey] : null;
698
+
699
+ // Check if sender has a team
700
+ if (senderProj && Array.isArray(senderProj.team)) {
701
+ for (const member of senderProj.team) {
702
+ const nicks = Array.isArray(member.nicknames) ? member.nicknames : [];
703
+ if (member.key === targetName || nicks.some(n => n === targetName)) {
704
+ targetKey = member.key;
705
+ break;
706
+ }
707
+ }
708
+ }
709
+ // Fall back to project lookup
710
+ if (!targetKey) {
711
+ targetKey = resolveProjectKey(targetName, config.projects || {});
712
+ }
713
+
714
+ if (!targetKey) {
715
+ await bot.sendMessage(chatId, `未找到 agent: ${targetName}`);
716
+ return { handled: true, config };
717
+ }
718
+
719
+ const toProj = config.projects[targetKey] || {};
720
+ const result = dispatchTask(targetKey, {
721
+ from: senderKey,
722
+ type: 'message',
723
+ priority: 'normal',
724
+ payload: { title: 'team message', prompt: `[来自团队的消息]\n\n${message}` },
725
+ callback: false,
726
+ }, config, null, null);
727
+
728
+ if (result.success) {
729
+ await bot.sendMessage(chatId, `📬 已发送消息给 ${toProj.icon || '🤖'} ${toProj.name || targetKey}`);
730
+ } else {
731
+ await bot.sendMessage(chatId, `❌ 发送失败: ${result.error}`);
732
+ }
733
+ return { handled: true, config };
734
+ }
735
+
672
736
  if (text === '/budget') {
673
737
  const limit = (config.budget && config.budget.daily_limit) || 50000;
674
738
  const used = state.budget.tokens_used;
@@ -676,6 +740,102 @@ function createAdminCommandHandler(deps) {
676
740
  return { handled: true, config };
677
741
  }
678
742
 
743
+ if (text === '/reset-budget') {
744
+ if (!state.budget) state.budget = {};
745
+ state.budget.tokens_used = 0;
746
+ state.budget.date = new Date().toISOString().slice(0, 10);
747
+ await bot.sendMessage(chatId, `✅ Budget 已重置 (${state.budget.date})`);
748
+ return { handled: true, config };
749
+ }
750
+
751
+ if (text === '/toggle' || text.startsWith('/toggle ')) {
752
+ const arg = text.slice('/toggle'.length).trim();
753
+ const cfg = config;
754
+ const tasks = (cfg.heartbeat && cfg.heartbeat.tasks) || [];
755
+
756
+ // Group mapping: friendly name → task names
757
+ const groups = {
758
+ cognition: ['cognitive-distill', 'self-reflect'],
759
+ memory: ['memory-extract', 'nightly-reflect', 'memory-gc', 'memory-index'],
760
+ skill: ['skill-evolve'],
761
+ };
762
+
763
+ if (!arg) {
764
+ // Show status
765
+ const lines = ['⚙️ 后台任务开关:'];
766
+ for (const [group, names] of Object.entries(groups)) {
767
+ const statuses = names.map(n => {
768
+ const t = tasks.find(t2 => t2.name === n);
769
+ return t ? (t.enabled !== false ? '✅' : '❌') : '⚠️';
770
+ });
771
+ const allOn = statuses.every(s => s === '✅');
772
+ const allOff = statuses.every(s => s === '❌');
773
+ lines.push(` ${allOn ? '✅' : allOff ? '❌' : '⚠️'} ${group}`);
774
+ }
775
+ lines.push('', '用法: /toggle <cognition|memory|skill> <on|off>');
776
+ await bot.sendMessage(chatId, lines.join('\n'));
777
+ return { handled: true, config };
778
+ }
779
+
780
+ const parts = arg.split(/\s+/);
781
+ const groupName = parts[0];
782
+ const action = parts[1];
783
+
784
+ if (!groups[groupName]) {
785
+ await bot.sendMessage(chatId, `未知分组: ${groupName}\n可选: cognition, memory, skill`);
786
+ return { handled: true, config };
787
+ }
788
+ if (action !== 'on' && action !== 'off') {
789
+ await bot.sendMessage(chatId, `用法: /toggle ${groupName} <on|off>`);
790
+ return { handled: true, config };
791
+ }
792
+
793
+ const enabled = action === 'on';
794
+ const affected = [];
795
+ for (const name of groups[groupName]) {
796
+ const t = tasks.find(t2 => t2.name === name);
797
+ if (t) {
798
+ t.enabled = enabled;
799
+ affected.push(name);
800
+ }
801
+ }
802
+ if (affected.length === 0) {
803
+ await bot.sendMessage(chatId, `⚠️ 未找到 ${groupName} 相关任务,请检查 heartbeat 配置`);
804
+ return { handled: true, config };
805
+ }
806
+ writeConfigSafe(cfg);
807
+ config = loadConfig();
808
+ await bot.sendMessage(chatId, `${enabled ? '✅' : '❌'} ${groupName} ${enabled ? 'ON' : 'OFF'} (${affected.join(', ')})`);
809
+ return { handled: true, config };
810
+ }
811
+
812
+ // /broadcast [on|off] — toggle team broadcast for the current chat's bound project
813
+ if (text === '/broadcast' || text.startsWith('/broadcast ')) {
814
+ const arg = text.slice('/broadcast'.length).trim();
815
+ const cfg = config;
816
+ const feishuMap = { ...(cfg.feishu ? cfg.feishu.chat_agent_map : {}), ...(cfg.telegram ? cfg.telegram.chat_agent_map : {}) };
817
+ const boundKey = feishuMap[String(chatId)];
818
+ const boundProj = boundKey && cfg.projects ? cfg.projects[boundKey] : null;
819
+ if (!boundProj || !Array.isArray(boundProj.team) || boundProj.team.length === 0) {
820
+ await bot.sendMessage(chatId, '⚠️ 当前群没有绑定 team 项目');
821
+ return { handled: true, config };
822
+ }
823
+ if (!arg) {
824
+ const status = boundProj.broadcast ? '✅ ON' : '❌ OFF';
825
+ await bot.sendMessage(chatId, `📢 团队广播: ${status}\n\n用法: /broadcast on|off\n开启后 team 成员间的传话会在群里可见`);
826
+ return { handled: true, config };
827
+ }
828
+ if (arg !== 'on' && arg !== 'off') {
829
+ await bot.sendMessage(chatId, '用法: /broadcast on|off');
830
+ return { handled: true, config };
831
+ }
832
+ cfg.projects[boundKey].broadcast = arg === 'on';
833
+ writeConfigSafe(cfg);
834
+ config = loadConfig();
835
+ await bot.sendMessage(chatId, `📢 团队广播已${arg === 'on' ? '开启' : '关闭'}`);
836
+ return { handled: true, config };
837
+ }
838
+
679
839
  if (text === '/usage' || text.startsWith('/usage ')) {
680
840
  const arg = text.slice('/usage'.length).trim() || 'today';
681
841
  const usage = state.usage || {};
@@ -999,9 +1159,7 @@ function createAdminCommandHandler(deps) {
999
1159
  );
1000
1160
  const optionValues = optionEntries.map(o => o.value);
1001
1161
  const daemonCfg = config.daemon || {};
1002
- const currentModel = (daemonCfg.models && daemonCfg.models[currentEngine])
1003
- || daemonCfg.model // legacy fallback
1004
- || engineCfg.main;
1162
+ const currentModel = resolveEngineModel(currentEngine, daemonCfg);
1005
1163
  // providerMod manages Claude providers only — for codex use engineCfg.provider
1006
1164
  const activeProvider = (currentEngine === 'claude' && providerMod)
1007
1165
  ? providerMod.getActiveName()
@@ -1089,8 +1247,7 @@ function createAdminCommandHandler(deps) {
1089
1247
  : curEngineCfg.provider;
1090
1248
  const distill = getDistillModel();
1091
1249
  const daemonCfg = config.daemon || {};
1092
- const currentModel = (daemonCfg.models && daemonCfg.models[cur])
1093
- || daemonCfg.model || curEngineCfg.main;
1250
+ const currentModel = resolveEngineModel(cur, daemonCfg);
1094
1251
  await bot.sendMessage(chatId, [
1095
1252
  `🔧 引擎: ${cur} | Provider: ${activeProvider}`,
1096
1253
  `🤖 会话模型: ${currentModel} | 后台轻量: ${distill}`,
@@ -1111,9 +1268,7 @@ function createAdminCommandHandler(deps) {
1111
1268
  const distill = getDistillModel();
1112
1269
  const freshCfg = loadConfig();
1113
1270
  const freshDaemon = freshCfg.daemon || {};
1114
- const targetEngineCfg = ENGINE_MODEL_CONFIG[arg] || ENGINE_MODEL_CONFIG.claude;
1115
- const syncedModel = (freshDaemon.models && freshDaemon.models[arg])
1116
- || freshDaemon.model || targetEngineCfg.main;
1271
+ const syncedModel = resolveEngineModel(arg, freshDaemon);
1117
1272
 
1118
1273
  // Auto-switch provider if the preferred one exists in providers.yaml
1119
1274
  let providerNote = '';
@@ -247,9 +247,14 @@ function createAgentCommandHandler(deps) {
247
247
  if (text === '/resume' || text.startsWith('/resume ')) {
248
248
  const arg = text.slice(7).trim();
249
249
 
250
- // Get current workdir to scope session list
250
+ // Get current workdir to scope session list — prefer bound project cwd over session cwd
251
+ const cfgForResume = loadConfig();
252
+ const chatAgentMapForResume = { ...(cfgForResume.telegram ? cfgForResume.telegram.chat_agent_map : {}), ...(cfgForResume.feishu ? cfgForResume.feishu.chat_agent_map : {}) };
253
+ const boundKeyForResume = chatAgentMapForResume[String(chatId)];
254
+ const boundProjForResume = boundKeyForResume && cfgForResume.projects ? cfgForResume.projects[boundKeyForResume] : null;
255
+ const boundCwdForResume = (boundProjForResume && boundProjForResume.cwd) ? normalizeCwd(boundProjForResume.cwd) : null;
251
256
  const curSession = getSession(chatId);
252
- const curCwd = curSession ? curSession.cwd : null;
257
+ const curCwd = boundCwdForResume || (curSession ? curSession.cwd : null);
253
258
  const recentSessions = listRecentSessions(5, curCwd);
254
259
 
255
260
  if (!arg) {
@@ -297,13 +302,17 @@ function createAgentCommandHandler(deps) {
297
302
 
298
303
  const state2 = loadState();
299
304
  const cfgForEngine = loadConfig();
300
- const engineByTargetCwd = inferEngineByCwd(cfgForEngine, cwd);
301
- const currentEngine = normalizeEngineName(state2.sessions[chatId] && state2.sessions[chatId].engine);
302
- state2.sessions[chatId] = {
303
- id: sessionId,
305
+ const engineByTargetCwd = inferEngineByCwd(cfgForEngine, cwd) || getDefaultEngine();
306
+ // For bound chats, write session to virtual chatId (_agent_{key}) so askClaude picks it up
307
+ const resumeChatAgentMap = { ...(cfgForEngine.telegram ? cfgForEngine.telegram.chat_agent_map : {}), ...(cfgForEngine.feishu ? cfgForEngine.feishu.chat_agent_map : {}) };
308
+ const resumeBoundKey = resumeChatAgentMap[String(chatId)];
309
+ const sessionKey = resumeBoundKey ? `_agent_${resumeBoundKey}` : String(chatId);
310
+ const existing = state2.sessions[sessionKey] || {};
311
+ const existingEngines = existing.engines || {};
312
+ state2.sessions[sessionKey] = {
313
+ ...existing,
304
314
  cwd,
305
- started: true,
306
- engine: engineByTargetCwd || currentEngine,
315
+ engines: { ...existingEngines, [engineByTargetCwd]: { id: sessionId, started: true } },
307
316
  };
308
317
  saveState(state2);
309
318
  const name = fullMatch.customTitle;