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.
- package/README.md +90 -17
- package/index.js +76 -25
- package/package.json +1 -1
- package/scripts/bin/dispatch_to +167 -90
- package/scripts/daemon-admin-commands.js +225 -24
- package/scripts/daemon-agent-commands.js +263 -8
- package/scripts/daemon-bridges.js +395 -6
- package/scripts/daemon-claude-engine.js +749 -582
- package/scripts/daemon-command-router.js +104 -0
- package/scripts/daemon-default.yaml +9 -4
- package/scripts/daemon-engine-runtime.js +33 -2
- package/scripts/daemon-exec-commands.js +8 -5
- package/scripts/daemon-file-browser.js +1 -0
- package/scripts/daemon-remote-dispatch.js +82 -0
- package/scripts/daemon-runtime-lifecycle.js +87 -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 +363 -8
- package/scripts/daemon.yaml +356 -0
- package/scripts/distill.js +35 -16
- package/scripts/docs/agent-guide.md +36 -3
- package/scripts/docs/hook-config.md +131 -0
- package/scripts/docs/maintenance-manual.md +214 -3
- package/scripts/docs/pointer-map.md +60 -5
- package/scripts/feishu-adapter.js +127 -58
- package/scripts/hooks/hook-utils.js +61 -0
- package/scripts/hooks/intent-agent-manage.js +50 -0
- package/scripts/hooks/intent-engine.js +103 -0
- package/scripts/hooks/intent-file-transfer.js +51 -0
- package/scripts/hooks/intent-hook-config.js +28 -0
- package/scripts/hooks/intent-memory-recall.js +35 -0
- package/scripts/hooks/intent-ops-assist.js +54 -0
- package/scripts/hooks/intent-task-create.js +35 -0
- package/scripts/hooks/intent-team-dispatch.js +106 -0
- package/scripts/hooks/team-context.js +143 -0
- package/scripts/memory-extract.js +1 -1
- package/scripts/memory-nightly-reflect.js +109 -43
- package/scripts/memory-write.js +21 -4
- package/scripts/memory.js +55 -17
- package/scripts/publish-public.sh +24 -35
- package/scripts/qmd-client.js +1 -1
- package/scripts/signal-capture.js +14 -0
- 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.
|
|
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
|
|
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.
|
|
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
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
fs.
|
|
109
|
-
|
|
110
|
-
|
|
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
package/scripts/bin/dispatch_to
CHANGED
|
@@ -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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
-
|
|
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(
|
|
93
|
-
console.log(`DISPATCH_OK(
|
|
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
|
-
|
|
97
|
-
|
|
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
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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
|
+
}
|