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 +36 -0
- package/package.json +1 -1
- package/scripts/bin/dispatch_to +2 -1
- package/scripts/daemon-admin-commands.js +164 -9
- package/scripts/daemon-agent-commands.js +17 -8
- package/scripts/daemon-bridges.js +385 -6
- package/scripts/daemon-claude-engine.js +172 -51
- package/scripts/daemon-command-router.js +33 -0
- package/scripts/daemon-default.yaml +4 -4
- package/scripts/daemon-engine-runtime.js +33 -2
- package/scripts/daemon-exec-commands.js +2 -2
- package/scripts/daemon-remote-dispatch.js +60 -0
- package/scripts/daemon-session-commands.js +19 -11
- package/scripts/daemon-session-store.js +26 -8
- package/scripts/daemon-task-scheduler.js +2 -2
- package/scripts/daemon.js +272 -6
- package/scripts/daemon.yaml +349 -0
- package/scripts/distill.js +35 -16
- package/scripts/docs/maintenance-manual.md +62 -1
- package/scripts/feishu-adapter.js +127 -58
- package/scripts/memory-extract.js +1 -1
- package/scripts/memory-write.js +21 -4
- package/scripts/publish-public.sh +24 -35
- package/scripts/qmd-client.js +1 -1
- package/scripts/signal-capture.js +14 -0
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
package/scripts/bin/dispatch_to
CHANGED
|
@@ -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
|
-
|
|
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 = (
|
|
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 = (
|
|
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
|
|
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
|
-
|
|
302
|
-
|
|
303
|
-
|
|
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;
|