metame-cli 1.5.3 → 1.5.4

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
@@ -26,15 +26,17 @@ curl -fsSL https://raw.githubusercontent.com/Yaron9/MetaMe/main/install.sh | bas
26
26
 
27
27
  ---
28
28
 
29
- > ### 🚀 v1.5.0Dynamic Engine Default + Distill Coupling
29
+ > ### 🚀 v1.5.4Agent Soul Layer Auto-Repair & Intent Engine
30
30
  >
31
+ > - **Cross-device dispatch**: Team members can run on remote machines. Add `peer: windows` to a member and messages route automatically via a Feishu relay chat — HMAC-signed, dedup-protected, zero manual routing.
32
+ > - **`/dispatch peers`**: View remote dispatch config, relay chat, and all remote team members from mobile.
33
+ > - **`dispatch_to peer:project`**: Dispatch tasks to remote peers from CLI, admin commands, or Claude sessions.
34
+ > - **Unified team dispatch**: Shared `team-dispatch.js` module — single source of truth for project/member resolution, roster hints, and prompt enrichment.
35
+ > - **Team broadcast**: Real-time cross-agent visibility in shared group chats with nickname routing and sticky follow.
36
+ > - **Unified intent engine**: Config-driven intent dispatcher replacing standalone hooks for team communication, ops assist, and task creation.
37
+ > - **Modular agent wizards**: New streamlined CLI flows for creating teams and cloning agents.
31
38
  > - **Dynamic default engine**: auto-detects installed CLI (claude/codex) at startup; pure-codex users work out of the box with zero config.
32
- > - **`/engine` command**: switch global default engine from mobile (`/engine codex`), with three-layer priority: `project.engine > /engine setting > auto-detect`.
33
- > - **Engine–distill coupling**: switching engine auto-pairs the distill model (claude→haiku, codex→gpt-5.1-codex-mini) and distill binary.
34
- > - **Engine-aware distill**: `callDistillModel` now routes through the correct CLI binary and parses codex JSON stream output.
35
- > - **`/doctor` engine checks**: health check now validates CLI availability against the configured default engine.
36
39
  > - **Multi-engine runtime adapter**: daemon supports engine routing by project (`project.engine`) with shared execution flow for Claude/Codex.
37
- > - **Codex session continuity**: supports `exec`/`resume`, thread id backfill, one-shot resume fallback, and auth/rate-limit error mapping.
38
40
  > - **Mentor mode hooks**: pre-flight emotion breaker, context-time mentor prompt, and post-flight reflection debt registration.
39
41
  > - **Multi-user ACL**: role-based permissions (admin / member / stranger) with binding protection.
40
42
  > - **Windows native support**: cross-platform path handling, Named Pipes IPC, GBK-safe encoding.
@@ -317,6 +319,7 @@ systemctl --user start metame
317
319
  | **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
320
  | **Multi-Agent** | Multiple projects with dedicated chat groups. `/agent bind` for one-tap setup. True parallel execution. |
319
321
  | **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. |
322
+ | **Cross-Device Dispatch** | Team members can run on different machines. `member.peer` marks remote agents — messages route via a Feishu relay chat with HMAC-SHA256 signing and 5-minute TTL dedup. `/dispatch peers` to view config, `dispatch_to peer:project` for explicit routing. |
320
323
  | **Browser Automation** | Built-in Playwright MCP. Browser control out of the box for every user. |
321
324
  | **Cross-Platform** | Native support for macOS and Windows. Platform abstraction layer handles spawn, IPC, process management, and terminal encoding automatically. |
322
325
  | **Provider Relay** | Route through any Anthropic-compatible API. Use GPT-4, DeepSeek, Gemini — zero config file mutation. |
@@ -419,7 +422,7 @@ All agents share your cognitive profile (`~/.claude_profile.yaml`) — they all
419
422
 
420
423
  ## Team Routing
421
424
 
422
- MetaMe supports project-level team clones — multiple AI agents (digital twins) sharing the same workspace, working in parallel within a single Feishu group.
425
+ MetaMe supports project-level team clones — multiple AI agents (digital twins) sharing the same workspace, working in parallel within a single Feishu group. Team members can run locally or on remote machines.
423
426
 
424
427
  ### Configuration
425
428
 
@@ -440,6 +443,12 @@ projects:
440
443
  nicknames:
441
444
  - 甲
442
445
  auto_dispatch: true
446
+ - key: hunter
447
+ name: 猎手
448
+ icon: 🎯
449
+ peer: windows # runs on another machine
450
+ nicknames:
451
+ - 猎手
443
452
  ```
444
453
 
445
454
  ### Key Features
@@ -449,9 +458,25 @@ projects:
449
458
  - **`/stop` precision**: `/stop 乙` stops a specific member; `/stop` stops the sticky member; reply-quote `/stop` stops the quoted member
450
459
  - **Auto-dispatch**: when the main agent is busy, messages are automatically routed to idle `auto_dispatch` members
451
460
  - **Broadcast**: with `broadcast: true`, inter-member `dispatch_to` messages are shown as cards in the group chat
461
+ - **Cross-device members**: add `peer: <device>` to a team member — messages route via a Feishu relay chat with HMAC signing and dedup protection
452
462
 
453
463
  Each team member runs on a virtual chatId (`_agent_{key}`) and appears with its own card title (e.g. `🤖 Jarvis · 乙`).
454
464
 
465
+ ### Cross-Device Dispatch
466
+
467
+ Team members with `peer` field run on a different machine. Configure `feishu.remote_dispatch` on both machines with the same relay chat and shared secret:
468
+
469
+ ```yaml
470
+ feishu:
471
+ remote_dispatch:
472
+ enabled: true
473
+ self: mac # unique peer name per machine
474
+ chat_id: oc_relay_xxx # shared relay group
475
+ secret: shared-secret-key # HMAC signing key
476
+ ```
477
+
478
+ Use from mobile: `/dispatch to windows:hunter research competitors` or just mention by nickname — routing is automatic. Use `/dispatch peers` to check remote config status.
479
+
455
480
  ## Mobile Commands
456
481
 
457
482
  | Command | Action |
@@ -469,7 +494,14 @@ Each team member runs on a virtual chatId (`_agent_{key}`) and appears with its
469
494
  | `/distill-model` | Show/update background distill model (default: `haiku`) |
470
495
  | `/mentor` | Mentor mode control: on/off/level/status |
471
496
  | `/activate` | Activate and bind the most recently created pending agent in a new group |
497
+ | `/agent new` | Interactive wizard to create a new agent |
498
+ | `/agent new team` | Team wizard: create multiple parallel agent clones under a project |
499
+ | `/agent new clone` | Clone wizard: create a clone sharing the current agent's role |
472
500
  | `/agent bind <name> [dir]` | Manually register group as dedicated agent |
501
+ | `/agent soul repair` | Idempotent rebuild of agent soul layer (links SOUL.md / MEMORY.md) |
502
+ | `/msg <agent> <message>` | Send a direct message to a team member or agent (e.g. `/msg 乙 check this`) |
503
+ | `/broadcast [on\|off]` | Toggle team broadcast for the current project (show inter-agent dispatches as cards) |
504
+ | `/stop <nickname>` | Stop a specific team member (e.g. `/stop 乙`) |
473
505
  | `/mac` | macOS control helper: permissions check/open + AppleScript/JXA execution |
474
506
  | `/sh <cmd>` | Raw shell — bypasses Claude |
475
507
  | `/memory` | Memory stats: fact count, session tags, DB size |
@@ -480,6 +512,8 @@ Each team member runs on a virtual chatId (`_agent_{key}`) and appears with its
480
512
  | `/user list` | List all configured users |
481
513
  | `/user remove <open_id>` | Remove a user |
482
514
  | `/sessions` | Browse recent sessions with last message preview |
515
+ | `/dispatch peers` | View remote dispatch configuration and remote team members |
516
+ | `/dispatch to <target> <prompt>` | Dispatch task to agent or remote peer (`peer:project` format supported) |
483
517
  | `/teamtask create <agent> <goal>` | Create a cross-agent collaboration task |
484
518
  | `/teamtask` | List recent TeamTasks (last 10) |
485
519
  | `/teamtask <task_id>` | View task detail |
@@ -506,8 +540,9 @@ Level mapping:
506
540
 
507
541
  ## Hook Optimizations (Default On)
508
542
 
509
- MetaMe installs and maintains two core Claude hooks automatically on launch:
543
+ MetaMe installs and maintains core Claude hooks automatically on launch:
510
544
 
545
+ - `UserPromptSubmit` hook (`scripts/hooks/intent-engine.js`): Unified intent engine for team dispatch, ops assist, and task creation hints.
511
546
  - `UserPromptSubmit` hook (`scripts/signal-capture.js`): captures high-signal preference/task traces with layered filtering.
512
547
  - `Stop` hook (`scripts/hooks/stop-session-capture.js`): records session-end/tool-failure signals with watermark protection.
513
548
 
@@ -517,7 +552,7 @@ If hook installation fails, MetaMe logs and continues the session (non-blocking
517
552
 
518
553
  ```
519
554
  ┌─────────────┐ Telegram/Feishu ┌──────────────────────────────┐
520
- │ Your Phone │ ◄──────────────────────► │ MetaMe Daemon
555
+ │ Your Phone │ ◄──────────────────────► │ MetaMe Daemon (Mac)
521
556
  └─────────────┘ │ (your machine, 24/7) │
522
557
  │ │
523
558
  │ ┌──────────────┐ │
@@ -526,18 +561,20 @@ If hook installation fails, MetaMe logs and continues the session (non-blocking
526
561
  │ └──────────────┘ │
527
562
  │ │
528
563
  │ ~/.claude_profile │
529
- │ (6-dim soul schema) │
530
- │ │
531
564
  │ ~/.metame/memory.db │
532
- │ session_tags.json │
533
- │ (5-layer memory) │
534
- │ │
535
565
  │ dispatch_to (auto-deployed)│
566
+ └──────────┬───────────────────┘
567
+
568
+ ┌──────────▼───────────────────┐
569
+ │ Feishu Relay Chat │
570
+ │ (HMAC-signed packets) │
571
+ └──────────┬───────────────────┘
572
+
573
+ ┌──────────▼───────────────────┐
574
+ │ MetaMe Daemon (Windows) │
575
+ │ peer: "windows" │
576
+ │ Remote team members here │
536
577
  └──────────────────────────────┘
537
-
538
- idle → summaries + memory tasks
539
- 01:00 → nightly reflection
540
- 01:30 → memory index rebuild
541
578
  ```
542
579
 
543
580
  - **Profile** (`~/.claude_profile.yaml`): 6-dimension soul schema. Injected into every Claude session via `CLAUDE.md`.
package/index.js CHANGED
@@ -83,8 +83,15 @@ if (!fs.existsSync(METAME_DIR)) {
83
83
  // DEPLOY PHASE: sync scripts, docs, bin to ~/.metame/
84
84
  // ---------------------------------------------------------
85
85
 
86
+ // Dev mode: when running from git repo, symlink instead of copy.
87
+ // This ensures source files and runtime files are always the same,
88
+ // preventing agents from accidentally editing copies instead of source.
89
+ const IS_DEV_MODE = fs.existsSync(path.join(__dirname, '.git'));
90
+
86
91
  /**
87
- * Sync files from srcDir to destDir. Only writes when content differs.
92
+ * Sync files from srcDir to destDir.
93
+ * - Dev mode (git repo): creates symlinks so source === runtime.
94
+ * - Production (npm install): copies files, only writes when content differs.
88
95
  * @param {string} srcDir - source directory
89
96
  * @param {string} destDir - destination directory
90
97
  * @param {object} [opts]
@@ -102,12 +109,35 @@ function syncDirFiles(srcDir, destDir, { fileList, chmod } = {}) {
102
109
  const dest = path.join(destDir, f);
103
110
  try {
104
111
  if (!fs.existsSync(src)) continue;
105
- const srcContent = fs.readFileSync(src, 'utf8');
106
- const destContent = fs.existsSync(dest) ? fs.readFileSync(dest, 'utf8') : '';
107
- if (srcContent !== destContent) {
108
- fs.writeFileSync(dest, srcContent, 'utf8');
109
- if (chmod) try { fs.chmodSync(dest, chmod); } catch { /* Windows */ }
110
- updated = true;
112
+
113
+ if (IS_DEV_MODE) {
114
+ // Dev mode: symlink dest → src (replace copy/stale symlink if needed)
115
+ const srcReal = fs.realpathSync(src);
116
+ let needLink = true;
117
+ try {
118
+ const existing = fs.lstatSync(dest);
119
+ if (existing.isSymbolicLink()) {
120
+ if (fs.realpathSync(dest) === srcReal) needLink = false;
121
+ else fs.unlinkSync(dest);
122
+ } else {
123
+ // Replace regular file with symlink
124
+ fs.unlinkSync(dest);
125
+ }
126
+ } catch { /* dest doesn't exist */ }
127
+ if (needLink) {
128
+ fs.symlinkSync(srcReal, dest);
129
+ if (chmod) try { fs.chmodSync(dest, chmod); } catch { /* Windows */ }
130
+ updated = true;
131
+ }
132
+ } else {
133
+ // Production: copy when content differs
134
+ const srcContent = fs.readFileSync(src, 'utf8');
135
+ const destContent = fs.existsSync(dest) ? fs.readFileSync(dest, 'utf8') : '';
136
+ if (srcContent !== destContent) {
137
+ fs.writeFileSync(dest, srcContent, 'utf8');
138
+ if (chmod) try { fs.chmodSync(dest, chmod); } catch { /* Windows */ }
139
+ updated = true;
140
+ }
111
141
  }
112
142
  } catch { /* non-fatal per file */ }
113
143
  }
@@ -168,7 +198,7 @@ if (syntaxErrors.length > 0) {
168
198
  // and has defer logic (waits for active Claude tasks to finish before restarting).
169
199
  // Killing here bypasses that and interrupts ongoing conversations.
170
200
  if (scriptsUpdated) {
171
- console.log(`${icon("pkg")} Scripts synced to ~/.metame/ — daemon will auto-restart when idle.`);
201
+ console.log(`${icon("pkg")} Scripts ${IS_DEV_MODE ? 'symlinked' : 'synced'} to ~/.metame/ — daemon will auto-restart when idle.`);
172
202
  }
173
203
  }
174
204
 
@@ -217,6 +247,7 @@ if (fs.existsSync(bundledSkillsDir)) {
217
247
  }
218
248
  }
219
249
 
250
+
220
251
  // Ensure ~/.codex/skills and ~/.agents/skills are symlinks to ~/.claude/skills
221
252
  // This keeps skill evolution unified across all engines.
222
253
  for (const altDir of [
@@ -348,6 +379,41 @@ function ensureHookInstalled() {
348
379
  console.log(`${icon("hook")} MetaMe: Stop session capture hook installed.`);
349
380
  }
350
381
 
382
+ // Migrate: remove standalone team-context.js hook (superseded by intent-engine)
383
+ if (settings.hooks?.UserPromptSubmit) {
384
+ const before = settings.hooks.UserPromptSubmit.length;
385
+ for (const entry of settings.hooks.UserPromptSubmit) {
386
+ if (entry.hooks) {
387
+ entry.hooks = entry.hooks.filter(h => !(h.command && h.command.includes('team-context.js')));
388
+ }
389
+ }
390
+ settings.hooks.UserPromptSubmit = settings.hooks.UserPromptSubmit.filter(
391
+ entry => entry.hooks && entry.hooks.length > 0
392
+ );
393
+ if (settings.hooks.UserPromptSubmit.length !== before) modified = true;
394
+ }
395
+
396
+ // Ensure intent-engine hook (unified intent detection + hint injection)
397
+ const intentEngineScript = path.join(METAME_DIR, 'hooks', 'intent-engine.js').replace(/\\/g, '/');
398
+ const intentEngineCommand = `node "${intentEngineScript}"`;
399
+ const intentEngineInstalled = (settings.hooks?.UserPromptSubmit || []).some(entry =>
400
+ entry.hooks?.some(h => h.command && h.command.includes('intent-engine.js'))
401
+ );
402
+
403
+ if (!intentEngineInstalled) {
404
+ if (!settings.hooks) settings.hooks = {};
405
+ if (!settings.hooks.UserPromptSubmit) settings.hooks.UserPromptSubmit = [];
406
+
407
+ settings.hooks.UserPromptSubmit.push({
408
+ hooks: [{
409
+ type: 'command',
410
+ command: intentEngineCommand,
411
+ }]
412
+ });
413
+ modified = true;
414
+ console.log(`${icon("hook")} MetaMe: Intent engine hook installed.`);
415
+ }
416
+
351
417
  if (modified) {
352
418
  fs.writeFileSync(CLAUDE_SETTINGS, JSON.stringify(settings, null, 2), 'utf8');
353
419
  }
@@ -872,26 +938,11 @@ const KERNEL_BODY = PROTOCOL_NORMAL
872
938
  .replace(/^<!-- METAME:START -->\n/, '') // remove project-level marker
873
939
  .trimEnd();
874
940
 
941
+ // Most capability hints migrated to intent engine (on-demand injection).
942
+ // Only keep Skills here — it's a fallback behavior that can't be keyword-matched.
875
943
  const CAPABILITY_SECTIONS = [
876
- '## Agent Dispatch',
877
- '识别到"告诉X/让X/通知X"等转发意图时 → 先 `cat ~/.metame/docs/dispatch-table.md` 获取路由表(昵称→project_key),再执行转发。不要凭记忆猜测昵称对应关系。',
878
- '',
879
- '## Agent 创建与管理',
880
- '用户问创建/管理/绑定 Agent 时 → 先 `cat ~/.metame/docs/agent-guide.md` 再回答。',
881
- '用户问代码结构/升级进度/脚本入口时 → 先 `cat ~/.metame/docs/pointer-map.md` 再回答。',
882
- '',
883
- '## 手机端文件交互',
884
- '用户要文件("发给我"/"发过来"/"导出")→ 先 `cat ~/.metame/docs/file-transfer.md` 再执行。',
885
- '**收**:用户发图片/文件自动存到 `upload/`,用 Read 查看。',
886
- '**发**:回复末尾加 `[[FILE:/absolute/path]]`,daemon 自动发手机。不要读内容再复述。',
887
- '',
888
- '## 跨会话记忆',
889
- '用户提"上次/之前"时搜索:`node ~/.metame/memory-search.js "关键词1" "keyword2"`',
890
- '一次传 3-4 个关键词(中文+英文+函数名),`--facts` 只搜事实,`--sessions` 只搜会话。',
891
- '',
892
944
  '## Skills',
893
945
  '能力不足/工具缺失/任务失败 → 先查 `cat ~/.claude/skills/skill-manager/SKILL.md`,不要自己猜。',
894
-
895
946
  ].join('\n');
896
947
 
897
948
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "metame-cli",
3
- "version": "1.5.3",
3
+ "version": "1.5.4",
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": {
@@ -1,6 +1,11 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * dispatch_to [--new] [--from <project_key>] <project_key> "<prompt>"
3
+ * dispatch_to [--new] [--from <project_key>] [--team] <project_key> "<prompt>"
4
+ *
5
+ * --team: broadcast to all members of the named project team.
6
+ * Each member receives the full task + a team roster hint so they know
7
+ * who their teammates are and how to dispatch_to them.
8
+ *
4
9
  * Tries Unix socket / Named Pipe first (low-latency), falls back to pending.jsonl.
5
10
  */
6
11
  'use strict';
@@ -10,125 +15,196 @@ const net = require('net');
10
15
  const crypto = require('crypto');
11
16
  const os = require('os');
12
17
  const { socketPath } = require('../platform');
18
+ const yaml = require('../resolve-yaml');
19
+ const { buildEnrichedPrompt, buildTeamRosterHint } = require('../team-dispatch');
20
+ const { parseRemoteTargetRef, normalizeRemoteDispatchConfig, encodePacket } = require('../daemon-remote-dispatch');
13
21
 
22
+ const METAME_DIR = path.join(os.homedir(), '.metame');
23
+ const DISPATCH_DIR = path.join(METAME_DIR, 'dispatch');
24
+ const PENDING = path.join(DISPATCH_DIR, 'pending.jsonl');
25
+ const DISPATCH_SECRET_FILE = path.join(METAME_DIR, '.dispatch_secret');
26
+ const SOCK_PATH = socketPath(METAME_DIR);
27
+
28
+ // ── Parse flags ──────────────────────────────────────────────────────────────
14
29
  const args = process.argv.slice(2);
15
30
  const newSession = args[0] === '--new' ? (args.shift(), true) : false;
16
31
 
17
- // --from <project_key>: identifies the calling agent for callback routing
18
- // Auto-detect from METAME_PROJECT env var (set by daemon when spawning agent sessions)
19
32
  let fromKey = process.env.METAME_PROJECT || '_claude_session';
20
33
  const fromIdx = args.indexOf('--from');
21
34
  if (fromIdx !== -1 && args[fromIdx + 1]) {
22
35
  fromKey = args.splice(fromIdx, 2)[1];
23
36
  }
24
37
 
38
+ const teamMode = args[0] === '--team' ? (args.shift(), true) : false;
39
+
25
40
  const [target, ...rest] = args;
26
41
  const prompt = rest.join(' ').replace(/^["']|["']$/g, '');
42
+
27
43
  if (!target || !prompt) {
28
- console.error('Usage: dispatch_to [--new] [--from <project_key>] <project_key> "<prompt>"');
44
+ console.error(
45
+ 'Usage: dispatch_to [--new] [--from <key>] [--team] <project_key> "<prompt>"\n' +
46
+ ' --team: broadcast to all members of the named project team'
47
+ );
29
48
  process.exit(1);
30
49
  }
31
50
 
32
- const METAME_DIR = path.join(os.homedir(), '.metame');
33
- const DISPATCH_DIR = path.join(METAME_DIR, 'dispatch');
34
- const PENDING = path.join(DISPATCH_DIR, 'pending.jsonl');
35
- const DISPATCH_SECRET_FILE = path.join(METAME_DIR, '.dispatch_secret');
36
- const SOCK_PATH = socketPath(METAME_DIR);
37
-
51
+ // ── Shared helpers ────────────────────────────────────────────────────────────
38
52
  function getDispatchSecret() {
39
53
  try {
40
54
  if (fs.existsSync(DISPATCH_SECRET_FILE)) {
41
55
  return fs.readFileSync(DISPATCH_SECRET_FILE, 'utf8').trim();
42
56
  }
43
- } catch { /* fall through to generate */ }
57
+ } catch { /* fall through */ }
44
58
  const secret = crypto.randomBytes(32).toString('hex');
45
- try {
46
- fs.writeFileSync(DISPATCH_SECRET_FILE, secret, { mode: 0o600 });
47
- } catch { /* ignore write errors */ }
59
+ try { fs.writeFileSync(DISPATCH_SECRET_FILE, secret, { mode: 0o600 }); } catch {}
48
60
  return secret;
49
61
  }
50
62
 
51
- const ts = new Date().toISOString();
52
- const secret = getDispatchSecret();
53
-
54
- // Auto-inject shared context: now/shared.md + target's _latest.md
55
- function buildEnrichedPrompt(rawPrompt) {
56
- const nowFile = path.join(METAME_DIR, 'memory', 'now', 'shared.md');
57
- const agentFile = path.join(METAME_DIR, 'memory', 'agents', `${target}_latest.md`);
58
- let ctx = '';
59
- try { if (fs.existsSync(nowFile)) ctx += `[共享进度 now.md]\n${fs.readFileSync(nowFile, 'utf8').trim()}\n\n`; } catch {}
60
- try { if (fs.existsSync(agentFile)) ctx += `[${target} 上次产出]\n${fs.readFileSync(agentFile, 'utf8').trim()}\n\n`; } catch {}
61
- // Push model: inject unread inbox messages and immediately archive them
62
- try {
63
- const inboxDir = path.join(METAME_DIR, 'memory', 'inbox', target);
64
- const readDir = path.join(inboxDir, 'read');
65
- const files = fs.readdirSync(inboxDir).filter(f => f.endsWith('.md')).sort();
66
- if (files.length > 0) {
67
- ctx += `[📬 Agent Inbox — ${files.length} 条未读消息]\n`;
68
- fs.mkdirSync(readDir, { recursive: true });
69
- for (const f of files) {
70
- const filePath = path.join(inboxDir, f);
71
- ctx += fs.readFileSync(filePath, 'utf8').trim() + '\n---\n';
72
- fs.renameSync(filePath, path.join(readDir, f));
73
- }
74
- ctx += '\n';
63
+ function sendOne(memberTarget, memberPrompt, opts = {}) {
64
+ return new Promise((resolve) => {
65
+ const ts = new Date().toISOString();
66
+ const secret = getDispatchSecret();
67
+ const enriched = opts.skipEnrich ? memberPrompt : buildEnrichedPrompt(memberTarget, memberPrompt, METAME_DIR);
68
+ const sigPayload = JSON.stringify({ target: memberTarget, prompt: enriched, ts });
69
+ const sig = crypto.createHmac('sha256', secret).update(sigPayload).digest('hex');
70
+
71
+ const callback = fromKey !== '_claude_session';
72
+ const msg = {
73
+ target: memberTarget,
74
+ prompt: enriched,
75
+ from: fromKey,
76
+ new_session: newSession,
77
+ created_at: ts,
78
+ ts,
79
+ sig,
80
+ team_roster_injected: opts.team_roster_injected || false,
81
+ ...(callback && { callback: true }),
82
+ };
83
+
84
+ // Ensure target inbox dir
85
+ fs.mkdirSync(path.join(METAME_DIR, 'memory', 'inbox', memberTarget, 'read'), { recursive: true });
86
+
87
+ function fallback() {
88
+ fs.mkdirSync(DISPATCH_DIR, { recursive: true });
89
+ fs.appendFileSync(PENDING, JSON.stringify(msg) + '\n');
90
+ console.log(`DISPATCH_OK(file): ${memberTarget} → ${memberPrompt.slice(0, 60)}${newSession ? ' [new session]' : ''}`);
91
+ resolve();
75
92
  }
76
- } catch {}
77
- return ctx ? `${ctx}---\n${rawPrompt}` : rawPrompt;
78
- }
79
-
80
- const enrichedPrompt = buildEnrichedPrompt(prompt);
81
- const sigPayload = JSON.stringify({ target, prompt: enrichedPrompt, ts });
82
- const sig = crypto.createHmac('sha256', secret).update(sigPayload).digest('hex');
83
93
 
84
- // Set callback: true when dispatched by another agent (not a user session)
85
- const callback = fromKey !== '_claude_session';
86
- const msg = { target, prompt: enrichedPrompt, from: fromKey, new_session: newSession, created_at: ts, ts, sig, ...(callback && { callback: true }) };
87
-
88
- // Ensure target's inbox exists — lazy init, safe for new users and new agents
89
- fs.mkdirSync(path.join(METAME_DIR, 'memory', 'inbox', target, 'read'), { recursive: true });
94
+ const sock = net.createConnection({ path: SOCK_PATH });
95
+ let done = false;
96
+
97
+ const timer = setTimeout(() => {
98
+ if (done) return;
99
+ done = true;
100
+ sock.destroy();
101
+ fallback();
102
+ }, 2000);
103
+
104
+ sock.on('connect', () => { sock.write(JSON.stringify(msg)); sock.end(); });
105
+ sock.on('data', (data) => {
106
+ if (done) return;
107
+ done = true;
108
+ clearTimeout(timer);
109
+ try {
110
+ const res = JSON.parse(data.toString().trim());
111
+ if (res.ok) {
112
+ console.log(`DISPATCH_OK(socket): ${memberTarget} → ${memberPrompt.slice(0, 60)}${newSession ? ' [new session]' : ''}`);
113
+ } else {
114
+ fallback();
115
+ return;
116
+ }
117
+ } catch { fallback(); return; }
118
+ sock.destroy();
119
+ resolve();
120
+ });
121
+ sock.on('error', () => {
122
+ if (done) return;
123
+ done = true;
124
+ clearTimeout(timer);
125
+ fallback();
126
+ });
127
+ });
128
+ }
90
129
 
91
- function fallbackToFile() {
130
+ // ── Remote dispatch helpers ───────────────────────────────────────────────────
131
+ function sendRemoteViaRelay(peer, project, memberPrompt) {
132
+ let config;
133
+ try {
134
+ config = yaml.load(fs.readFileSync(path.join(METAME_DIR, 'daemon.yaml'), 'utf8'));
135
+ } catch (e) {
136
+ console.error(`dispatch_to: failed to load daemon.yaml: ${e.message}`);
137
+ process.exit(1);
138
+ }
139
+ const rd = normalizeRemoteDispatchConfig(config);
140
+ if (!rd) {
141
+ console.error('dispatch_to: feishu.remote_dispatch not configured or disabled');
142
+ process.exit(1);
143
+ }
144
+ const ts = new Date().toISOString();
145
+ const id = `${rd.selfPeer}_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
146
+ const body = encodePacket({
147
+ v: 1, id, ts,
148
+ type: 'task',
149
+ from_peer: rd.selfPeer,
150
+ to_peer: peer,
151
+ target_project: project,
152
+ prompt: memberPrompt,
153
+ source_sender_key: fromKey,
154
+ }, rd.secret);
155
+
156
+ // Write to dispatch/remote-pending.jsonl for daemon to pick up and send via bot
157
+ const remotePending = path.join(DISPATCH_DIR, 'remote-pending.jsonl');
92
158
  fs.mkdirSync(DISPATCH_DIR, { recursive: true });
93
- fs.appendFileSync(PENDING, JSON.stringify(msg) + '\n');
94
- console.log(`DISPATCH_OK(file): ${target} → ${prompt.slice(0, 60)}${newSession ? ' [new session]' : ''}`);
159
+ fs.appendFileSync(remotePending, JSON.stringify({ relay_chat_id: rd.chatId, body }) + '\n');
160
+ console.log(`DISPATCH_OK(remote): ${peer}:${project} → ${memberPrompt.slice(0, 60)}`);
95
161
  }
96
162
 
97
- const sock = net.createConnection({ path: SOCK_PATH });
98
- let done = false;
99
-
100
- const timer = setTimeout(() => {
101
- if (done) return;
102
- done = true;
103
- sock.destroy();
104
- fallbackToFile();
105
- }, 2000);
106
-
107
- sock.on('connect', () => {
108
- sock.write(JSON.stringify(msg));
109
- sock.end();
110
- });
111
-
112
- sock.on('data', (data) => {
113
- if (done) return;
114
- done = true;
115
- clearTimeout(timer);
163
+ // ── Team broadcast mode ───────────────────────────────────────────────────────
164
+ if (teamMode) {
165
+ let config = null;
116
166
  try {
117
- const res = JSON.parse(data.toString().trim());
118
- if (res.ok) {
119
- console.log(`DISPATCH_OK(socket): ${target} ${prompt.slice(0, 60)}${newSession ? ' [new session]' : ''}`);
120
- } else {
121
- fallbackToFile();
122
- }
123
- } catch {
124
- fallbackToFile();
167
+ config = yaml.load(fs.readFileSync(path.join(METAME_DIR, 'daemon.yaml'), 'utf8'));
168
+ } catch (e) {
169
+ console.error(`dispatch_to --team: failed to load daemon.yaml: ${e.message}`);
170
+ process.exit(1);
125
171
  }
126
- sock.destroy();
127
- });
128
-
129
- sock.on('error', () => {
130
- if (done) return;
131
- done = true;
132
- clearTimeout(timer);
133
- fallbackToFile();
134
- });
172
+
173
+ const project = config && config.projects && config.projects[target];
174
+ if (!project) {
175
+ console.error(`dispatch_to --team: project "${target}" not found in daemon.yaml`);
176
+ process.exit(1);
177
+ }
178
+
179
+ const team = Array.isArray(project.team) ? project.team : [];
180
+ if (team.length === 0) {
181
+ console.error(`dispatch_to --team: project "${target}" has no team members`);
182
+ process.exit(1);
183
+ }
184
+
185
+ console.log(`📢 Team broadcast → ${target} (${team.length} members): ${prompt.slice(0, 60)}`);
186
+
187
+ // Await all dispatches before exiting so async socket/file ops complete
188
+ Promise.all(team.map((member) => {
189
+ const roster = buildTeamRosterHint(target, member.key, config.projects);
190
+ const enriched = buildEnrichedPrompt(member.key, prompt, METAME_DIR);
191
+ const memberPrompt = roster ? `${roster}\n\n---\n${enriched}` : enriched;
192
+ // Remote member → relay dispatch
193
+ if (member.peer) {
194
+ sendRemoteViaRelay(member.peer, member.key, memberPrompt);
195
+ return Promise.resolve();
196
+ }
197
+ return sendOne(member.key, memberPrompt, { team_roster_injected: true, skipEnrich: true });
198
+ })).then(() => process.exit(0));
199
+ } else {
200
+
201
+ // ── Normal single-target dispatch ─────────────────────────────────────────────
202
+ const remoteTarget = parseRemoteTargetRef(target);
203
+ if (remoteTarget) {
204
+ const enriched = buildEnrichedPrompt(remoteTarget.project, prompt, METAME_DIR);
205
+ sendRemoteViaRelay(remoteTarget.peer, remoteTarget.project, enriched);
206
+ process.exit(0);
207
+ } else {
208
+ sendOne(target, prompt).then(() => process.exit(0));
209
+ }
210
+ }