metame-cli 1.5.2 โ†’ 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.
Files changed (44) hide show
  1. package/README.md +90 -17
  2. package/index.js +76 -25
  3. package/package.json +1 -1
  4. package/scripts/bin/dispatch_to +167 -90
  5. package/scripts/daemon-admin-commands.js +225 -24
  6. package/scripts/daemon-agent-commands.js +263 -8
  7. package/scripts/daemon-bridges.js +395 -6
  8. package/scripts/daemon-claude-engine.js +749 -582
  9. package/scripts/daemon-command-router.js +104 -0
  10. package/scripts/daemon-default.yaml +9 -4
  11. package/scripts/daemon-engine-runtime.js +33 -2
  12. package/scripts/daemon-exec-commands.js +8 -5
  13. package/scripts/daemon-file-browser.js +1 -0
  14. package/scripts/daemon-remote-dispatch.js +82 -0
  15. package/scripts/daemon-runtime-lifecycle.js +87 -0
  16. package/scripts/daemon-session-commands.js +19 -11
  17. package/scripts/daemon-session-store.js +26 -8
  18. package/scripts/daemon-task-scheduler.js +2 -2
  19. package/scripts/daemon.js +363 -8
  20. package/scripts/daemon.yaml +356 -0
  21. package/scripts/distill.js +35 -16
  22. package/scripts/docs/agent-guide.md +36 -3
  23. package/scripts/docs/hook-config.md +131 -0
  24. package/scripts/docs/maintenance-manual.md +214 -3
  25. package/scripts/docs/pointer-map.md +60 -5
  26. package/scripts/feishu-adapter.js +127 -58
  27. package/scripts/hooks/hook-utils.js +61 -0
  28. package/scripts/hooks/intent-agent-manage.js +50 -0
  29. package/scripts/hooks/intent-engine.js +103 -0
  30. package/scripts/hooks/intent-file-transfer.js +51 -0
  31. package/scripts/hooks/intent-hook-config.js +28 -0
  32. package/scripts/hooks/intent-memory-recall.js +35 -0
  33. package/scripts/hooks/intent-ops-assist.js +54 -0
  34. package/scripts/hooks/intent-task-create.js +35 -0
  35. package/scripts/hooks/intent-team-dispatch.js +106 -0
  36. package/scripts/hooks/team-context.js +143 -0
  37. package/scripts/memory-extract.js +1 -1
  38. package/scripts/memory-nightly-reflect.js +109 -43
  39. package/scripts/memory-write.js +21 -4
  40. package/scripts/memory.js +55 -17
  41. package/scripts/publish-public.sh +24 -35
  42. package/scripts/qmd-client.js +1 -1
  43. package/scripts/signal-capture.js +14 -0
  44. package/scripts/team-dispatch.js +176 -0
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.0 โ€” Dynamic Engine Default + Distill Coupling
29
+ > ### ๐Ÿš€ v1.5.4 โ€” Agent 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.
@@ -316,6 +318,8 @@ systemctl --user start metame
316
318
  | **Auto-Provisioning** | First run deploys default CLAUDE.md, documentation, and `dispatch_to` to `~/.metame/`. Subsequent runs sync scripts without overwriting user config. |
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. |
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. |
319
323
  | **Browser Automation** | Built-in Playwright MCP. Browser control out of the box for every user. |
320
324
  | **Cross-Platform** | Native support for macOS and Windows. Platform abstraction layer handles spawn, IPC, process management, and terminal encoding automatically. |
321
325
  | **Provider Relay** | Route through any Anthropic-compatible API. Use GPT-4, DeepSeek, Gemini โ€” zero config file mutation. |
@@ -416,6 +420,63 @@ All agents share your cognitive profile (`~/.claude_profile.yaml`) โ€” they all
416
420
  ~/.metame/bin/dispatch_to coder "Run the test suite and report results"
417
421
  ```
418
422
 
423
+ ## Team Routing
424
+
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.
426
+
427
+ ### Configuration
428
+
429
+ Add a `team` array and `broadcast: true` under any project in `daemon.yaml`:
430
+
431
+ ```yaml
432
+ projects:
433
+ metame:
434
+ name: ่ถ…็บงๆ€ป็ฎก Jarvis
435
+ icon: ๐Ÿค–
436
+ broadcast: true
437
+ team:
438
+ - key: jia
439
+ name: Jarvis ยท ็”ฒ
440
+ icon: ๐Ÿค–
441
+ color: green
442
+ cwd: ~/AGI/MetaMe
443
+ nicknames:
444
+ - ็”ฒ
445
+ auto_dispatch: true
446
+ - key: hunter
447
+ name: ็ŒŽๆ‰‹
448
+ icon: ๐ŸŽฏ
449
+ peer: windows # runs on another machine
450
+ nicknames:
451
+ - ็ŒŽๆ‰‹
452
+ ```
453
+
454
+ ### Key Features
455
+
456
+ - **Nickname routing**: mention a member by nickname (e.g. "ไน™ check this") to route directly to them
457
+ - **Sticky follow**: once you address a member, subsequent messages without a nickname continue going to the same member
458
+ - **`/stop` precision**: `/stop ไน™` stops a specific member; `/stop` stops the sticky member; reply-quote `/stop` stops the quoted member
459
+ - **Auto-dispatch**: when the main agent is busy, messages are automatically routed to idle `auto_dispatch` members
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
462
+
463
+ Each team member runs on a virtual chatId (`_agent_{key}`) and appears with its own card title (e.g. `๐Ÿค– Jarvis ยท ไน™`).
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
+
419
480
  ## Mobile Commands
420
481
 
421
482
  | Command | Action |
@@ -433,7 +494,14 @@ All agents share your cognitive profile (`~/.claude_profile.yaml`) โ€” they all
433
494
  | `/distill-model` | Show/update background distill model (default: `haiku`) |
434
495
  | `/mentor` | Mentor mode control: on/off/level/status |
435
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 |
436
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 ไน™`) |
437
505
  | `/mac` | macOS control helper: permissions check/open + AppleScript/JXA execution |
438
506
  | `/sh <cmd>` | Raw shell โ€” bypasses Claude |
439
507
  | `/memory` | Memory stats: fact count, session tags, DB size |
@@ -444,6 +512,8 @@ All agents share your cognitive profile (`~/.claude_profile.yaml`) โ€” they all
444
512
  | `/user list` | List all configured users |
445
513
  | `/user remove <open_id>` | Remove a user |
446
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) |
447
517
  | `/teamtask create <agent> <goal>` | Create a cross-agent collaboration task |
448
518
  | `/teamtask` | List recent TeamTasks (last 10) |
449
519
  | `/teamtask <task_id>` | View task detail |
@@ -470,8 +540,9 @@ Level mapping:
470
540
 
471
541
  ## Hook Optimizations (Default On)
472
542
 
473
- MetaMe installs and maintains two core Claude hooks automatically on launch:
543
+ MetaMe installs and maintains core Claude hooks automatically on launch:
474
544
 
545
+ - `UserPromptSubmit` hook (`scripts/hooks/intent-engine.js`): Unified intent engine for team dispatch, ops assist, and task creation hints.
475
546
  - `UserPromptSubmit` hook (`scripts/signal-capture.js`): captures high-signal preference/task traces with layered filtering.
476
547
  - `Stop` hook (`scripts/hooks/stop-session-capture.js`): records session-end/tool-failure signals with watermark protection.
477
548
 
@@ -481,7 +552,7 @@ If hook installation fails, MetaMe logs and continues the session (non-blocking
481
552
 
482
553
  ```
483
554
  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” Telegram/Feishu โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
484
- โ”‚ Your Phone โ”‚ โ—„โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–บ โ”‚ MetaMe Daemon โ”‚
555
+ โ”‚ Your Phone โ”‚ โ—„โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–บ โ”‚ MetaMe Daemon (Mac) โ”‚
485
556
  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ (your machine, 24/7) โ”‚
486
557
  โ”‚ โ”‚
487
558
  โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚
@@ -490,18 +561,20 @@ If hook installation fails, MetaMe logs and continues the session (non-blocking
490
561
  โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚
491
562
  โ”‚ โ”‚
492
563
  โ”‚ ~/.claude_profile โ”‚
493
- โ”‚ (6-dim soul schema) โ”‚
494
- โ”‚ โ”‚
495
564
  โ”‚ ~/.metame/memory.db โ”‚
496
- โ”‚ session_tags.json โ”‚
497
- โ”‚ (5-layer memory) โ”‚
498
- โ”‚ โ”‚
499
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 โ”‚
500
577
  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
501
- โ†‘
502
- idle โ†’ summaries + memory tasks
503
- 01:00 โ†’ nightly reflection
504
- 01:30 โ†’ memory index rebuild
505
578
  ```
506
579
 
507
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.2",
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,124 +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
- let fromKey = '_claude_session';
32
+ let fromKey = process.env.METAME_PROJECT || '_claude_session';
19
33
  const fromIdx = args.indexOf('--from');
20
34
  if (fromIdx !== -1 && args[fromIdx + 1]) {
21
35
  fromKey = args.splice(fromIdx, 2)[1];
22
36
  }
23
37
 
38
+ const teamMode = args[0] === '--team' ? (args.shift(), true) : false;
39
+
24
40
  const [target, ...rest] = args;
25
41
  const prompt = rest.join(' ').replace(/^["']|["']$/g, '');
42
+
26
43
  if (!target || !prompt) {
27
- 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
+ );
28
48
  process.exit(1);
29
49
  }
30
50
 
31
- const METAME_DIR = path.join(os.homedir(), '.metame');
32
- const DISPATCH_DIR = path.join(METAME_DIR, 'dispatch');
33
- const PENDING = path.join(DISPATCH_DIR, 'pending.jsonl');
34
- const DISPATCH_SECRET_FILE = path.join(METAME_DIR, '.dispatch_secret');
35
- const SOCK_PATH = socketPath(METAME_DIR);
36
-
51
+ // โ”€โ”€ Shared helpers โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
37
52
  function getDispatchSecret() {
38
53
  try {
39
54
  if (fs.existsSync(DISPATCH_SECRET_FILE)) {
40
55
  return fs.readFileSync(DISPATCH_SECRET_FILE, 'utf8').trim();
41
56
  }
42
- } catch { /* fall through to generate */ }
57
+ } catch { /* fall through */ }
43
58
  const secret = crypto.randomBytes(32).toString('hex');
44
- try {
45
- fs.writeFileSync(DISPATCH_SECRET_FILE, secret, { mode: 0o600 });
46
- } catch { /* ignore write errors */ }
59
+ try { fs.writeFileSync(DISPATCH_SECRET_FILE, secret, { mode: 0o600 }); } catch {}
47
60
  return secret;
48
61
  }
49
62
 
50
- const ts = new Date().toISOString();
51
- const secret = getDispatchSecret();
52
-
53
- // Auto-inject shared context: now/shared.md + target's _latest.md
54
- function buildEnrichedPrompt(rawPrompt) {
55
- const nowFile = path.join(METAME_DIR, 'memory', 'now', 'shared.md');
56
- const agentFile = path.join(METAME_DIR, 'memory', 'agents', `${target}_latest.md`);
57
- let ctx = '';
58
- try { if (fs.existsSync(nowFile)) ctx += `[ๅ…ฑไบซ่ฟ›ๅบฆ now.md]\n${fs.readFileSync(nowFile, 'utf8').trim()}\n\n`; } catch {}
59
- try { if (fs.existsSync(agentFile)) ctx += `[${target} ไธŠๆฌกไบงๅ‡บ]\n${fs.readFileSync(agentFile, 'utf8').trim()}\n\n`; } catch {}
60
- // Push model: inject unread inbox messages and immediately archive them
61
- try {
62
- const inboxDir = path.join(METAME_DIR, 'memory', 'inbox', target);
63
- const readDir = path.join(inboxDir, 'read');
64
- const files = fs.readdirSync(inboxDir).filter(f => f.endsWith('.md')).sort();
65
- if (files.length > 0) {
66
- ctx += `[๐Ÿ“ฌ Agent Inbox โ€” ${files.length} ๆกๆœช่ฏปๆถˆๆฏ]\n`;
67
- fs.mkdirSync(readDir, { recursive: true });
68
- for (const f of files) {
69
- const filePath = path.join(inboxDir, f);
70
- ctx += fs.readFileSync(filePath, 'utf8').trim() + '\n---\n';
71
- fs.renameSync(filePath, path.join(readDir, f));
72
- }
73
- 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();
74
92
  }
75
- } catch {}
76
- return ctx ? `${ctx}---\n${rawPrompt}` : rawPrompt;
77
- }
78
-
79
- const enrichedPrompt = buildEnrichedPrompt(prompt);
80
- const sigPayload = JSON.stringify({ target, prompt: enrichedPrompt, ts });
81
- const sig = crypto.createHmac('sha256', secret).update(sigPayload).digest('hex');
82
93
 
83
- // Set callback: true when dispatched by another agent (not a user session)
84
- const callback = fromKey !== '_claude_session';
85
- const msg = { target, prompt: enrichedPrompt, from: fromKey, new_session: newSession, created_at: ts, ts, sig, ...(callback && { callback: true }) };
86
-
87
- // Ensure target's inbox exists โ€” lazy init, safe for new users and new agents
88
- 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
+ }
89
129
 
90
- 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');
91
158
  fs.mkdirSync(DISPATCH_DIR, { recursive: true });
92
- fs.appendFileSync(PENDING, JSON.stringify(msg) + '\n');
93
- 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)}`);
94
161
  }
95
162
 
96
- const sock = net.createConnection({ path: SOCK_PATH });
97
- let done = false;
98
-
99
- const timer = setTimeout(() => {
100
- if (done) return;
101
- done = true;
102
- sock.destroy();
103
- fallbackToFile();
104
- }, 2000);
105
-
106
- sock.on('connect', () => {
107
- sock.write(JSON.stringify(msg));
108
- sock.end();
109
- });
110
-
111
- sock.on('data', (data) => {
112
- if (done) return;
113
- done = true;
114
- clearTimeout(timer);
163
+ // โ”€โ”€ Team broadcast mode โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
164
+ if (teamMode) {
165
+ let config = null;
115
166
  try {
116
- const res = JSON.parse(data.toString().trim());
117
- if (res.ok) {
118
- console.log(`DISPATCH_OK(socket): ${target} โ†’ ${prompt.slice(0, 60)}${newSession ? ' [new session]' : ''}`);
119
- } else {
120
- fallbackToFile();
121
- }
122
- } catch {
123
- 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);
124
171
  }
125
- sock.destroy();
126
- });
127
-
128
- sock.on('error', () => {
129
- if (done) return;
130
- done = true;
131
- clearTimeout(timer);
132
- fallbackToFile();
133
- });
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
+ }