metame-cli 1.5.3 → 1.5.5

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 (51) hide show
  1. package/README.md +60 -18
  2. package/index.js +352 -79
  3. package/package.json +2 -2
  4. package/scripts/agent-layer.js +4 -2
  5. package/scripts/bin/dispatch_to +178 -90
  6. package/scripts/daemon-admin-commands.js +353 -105
  7. package/scripts/daemon-agent-commands.js +434 -66
  8. package/scripts/daemon-bridges.js +477 -68
  9. package/scripts/daemon-claude-engine.js +1267 -674
  10. package/scripts/daemon-command-router.js +205 -27
  11. package/scripts/daemon-command-session-route.js +118 -0
  12. package/scripts/daemon-default.yaml +7 -0
  13. package/scripts/daemon-engine-runtime.js +96 -20
  14. package/scripts/daemon-exec-commands.js +108 -49
  15. package/scripts/daemon-file-browser.js +64 -7
  16. package/scripts/daemon-notify.js +18 -4
  17. package/scripts/daemon-ops-commands.js +16 -2
  18. package/scripts/daemon-remote-dispatch.js +55 -1
  19. package/scripts/daemon-runtime-lifecycle.js +87 -0
  20. package/scripts/daemon-session-commands.js +102 -45
  21. package/scripts/daemon-session-store.js +497 -66
  22. package/scripts/daemon-siri-bridge.js +234 -0
  23. package/scripts/daemon-siri-imessage.js +209 -0
  24. package/scripts/daemon-task-scheduler.js +10 -2
  25. package/scripts/daemon.js +697 -179
  26. package/scripts/daemon.yaml +7 -0
  27. package/scripts/docs/agent-guide.md +36 -3
  28. package/scripts/docs/hook-config.md +134 -0
  29. package/scripts/docs/maintenance-manual.md +162 -5
  30. package/scripts/docs/pointer-map.md +60 -5
  31. package/scripts/feishu-adapter.js +7 -15
  32. package/scripts/hooks/doc-router.js +29 -0
  33. package/scripts/hooks/hook-utils.js +61 -0
  34. package/scripts/hooks/intent-doc-router.js +54 -0
  35. package/scripts/hooks/intent-engine.js +72 -0
  36. package/scripts/hooks/intent-file-transfer.js +51 -0
  37. package/scripts/hooks/intent-memory-recall.js +35 -0
  38. package/scripts/hooks/intent-ops-assist.js +54 -0
  39. package/scripts/hooks/intent-task-create.js +35 -0
  40. package/scripts/hooks/intent-team-dispatch.js +106 -0
  41. package/scripts/hooks/team-context.js +143 -0
  42. package/scripts/intent-registry.js +59 -0
  43. package/scripts/memory-extract.js +59 -0
  44. package/scripts/memory-nightly-reflect.js +109 -43
  45. package/scripts/memory.js +55 -17
  46. package/scripts/mentor-engine.js +6 -0
  47. package/scripts/schema.js +1 -0
  48. package/scripts/self-reflect.js +110 -12
  49. package/scripts/session-analytics.js +160 -0
  50. package/scripts/signal-capture.js +1 -1
  51. package/scripts/team-dispatch.js +315 -0
@@ -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,208 @@ 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 {
21
+ parseRemoteTargetRef,
22
+ normalizeRemoteDispatchConfig,
23
+ encodePacket,
24
+ } = require('../daemon-remote-dispatch');
13
25
 
26
+ const METAME_DIR = path.join(os.homedir(), '.metame');
27
+ const DISPATCH_DIR = path.join(METAME_DIR, 'dispatch');
28
+ const PENDING = path.join(DISPATCH_DIR, 'pending.jsonl');
29
+ const DISPATCH_SECRET_FILE = path.join(METAME_DIR, '.dispatch_secret');
30
+ const SOCK_PATH = socketPath(METAME_DIR);
31
+
32
+ // ── Parse flags ──────────────────────────────────────────────────────────────
14
33
  const args = process.argv.slice(2);
15
34
  const newSession = args[0] === '--new' ? (args.shift(), true) : false;
16
35
 
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
36
  let fromKey = process.env.METAME_PROJECT || '_claude_session';
37
+ const sourceSenderId = String(process.env.METAME_SENDER_ID || '').trim();
20
38
  const fromIdx = args.indexOf('--from');
21
39
  if (fromIdx !== -1 && args[fromIdx + 1]) {
22
40
  fromKey = args.splice(fromIdx, 2)[1];
23
41
  }
24
42
 
43
+ const teamMode = args[0] === '--team' ? (args.shift(), true) : false;
44
+
25
45
  const [target, ...rest] = args;
26
46
  const prompt = rest.join(' ').replace(/^["']|["']$/g, '');
47
+
27
48
  if (!target || !prompt) {
28
- console.error('Usage: dispatch_to [--new] [--from <project_key>] <project_key> "<prompt>"');
49
+ console.error(
50
+ 'Usage: dispatch_to [--new] [--from <key>] [--team] <project_key> "<prompt>"\n' +
51
+ ' --team: broadcast to all members of the named project team'
52
+ );
29
53
  process.exit(1);
30
54
  }
31
55
 
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
-
56
+ // ── Shared helpers ────────────────────────────────────────────────────────────
38
57
  function getDispatchSecret() {
39
58
  try {
40
59
  if (fs.existsSync(DISPATCH_SECRET_FILE)) {
41
60
  return fs.readFileSync(DISPATCH_SECRET_FILE, 'utf8').trim();
42
61
  }
43
- } catch { /* fall through to generate */ }
62
+ } catch { /* fall through */ }
44
63
  const secret = crypto.randomBytes(32).toString('hex');
45
- try {
46
- fs.writeFileSync(DISPATCH_SECRET_FILE, secret, { mode: 0o600 });
47
- } catch { /* ignore write errors */ }
64
+ try { fs.writeFileSync(DISPATCH_SECRET_FILE, secret, { mode: 0o600 }); } catch {}
48
65
  return secret;
49
66
  }
50
67
 
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';
68
+ function sendOne(memberTarget, memberPrompt, opts = {}) {
69
+ return new Promise((resolve) => {
70
+ const ts = new Date().toISOString();
71
+ const secret = getDispatchSecret();
72
+ const enriched = opts.skipEnrich
73
+ ? memberPrompt
74
+ : buildEnrichedPrompt(memberTarget, memberPrompt, METAME_DIR, { includeShared: !!opts.includeShared });
75
+ const sigPayload = JSON.stringify({ target: memberTarget, prompt: enriched, ts });
76
+ const sig = crypto.createHmac('sha256', secret).update(sigPayload).digest('hex');
77
+
78
+ const callback = fromKey !== '_claude_session';
79
+ const msg = {
80
+ target: memberTarget,
81
+ prompt: enriched,
82
+ from: fromKey,
83
+ source_sender_id: sourceSenderId,
84
+ new_session: newSession,
85
+ created_at: ts,
86
+ ts,
87
+ sig,
88
+ team_roster_injected: opts.team_roster_injected || false,
89
+ ...(callback && { callback: true }),
90
+ };
91
+
92
+ // Ensure target inbox dir
93
+ fs.mkdirSync(path.join(METAME_DIR, 'memory', 'inbox', memberTarget, 'read'), { recursive: true });
94
+
95
+ function fallback() {
96
+ fs.mkdirSync(DISPATCH_DIR, { recursive: true });
97
+ fs.appendFileSync(PENDING, JSON.stringify(msg) + '\n');
98
+ console.log(`DISPATCH_OK(file): ${memberTarget} → ${memberPrompt.slice(0, 60)}${newSession ? ' [new session]' : ''}`);
99
+ resolve();
75
100
  }
76
- } catch {}
77
- return ctx ? `${ctx}---\n${rawPrompt}` : rawPrompt;
101
+
102
+ const sock = net.createConnection({ path: SOCK_PATH });
103
+ let done = false;
104
+
105
+ const timer = setTimeout(() => {
106
+ if (done) return;
107
+ done = true;
108
+ sock.destroy();
109
+ fallback();
110
+ }, 2000);
111
+
112
+ sock.on('connect', () => { sock.write(JSON.stringify(msg)); sock.end(); });
113
+ sock.on('data', (data) => {
114
+ if (done) return;
115
+ done = true;
116
+ clearTimeout(timer);
117
+ try {
118
+ const res = JSON.parse(data.toString().trim());
119
+ if (res.ok) {
120
+ console.log(`DISPATCH_OK(socket): ${memberTarget} → ${memberPrompt.slice(0, 60)}${newSession ? ' [new session]' : ''}`);
121
+ } else {
122
+ fallback();
123
+ return;
124
+ }
125
+ } catch { fallback(); return; }
126
+ sock.destroy();
127
+ resolve();
128
+ });
129
+ sock.on('error', () => {
130
+ if (done) return;
131
+ done = true;
132
+ clearTimeout(timer);
133
+ fallback();
134
+ });
135
+ });
78
136
  }
79
137
 
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');
138
+ // ── Remote dispatch helpers ───────────────────────────────────────────────────
139
+ function sendRemoteViaRelay(peer, project, memberPrompt) {
140
+ let config;
141
+ try {
142
+ config = yaml.load(fs.readFileSync(path.join(METAME_DIR, 'daemon.yaml'), 'utf8'));
143
+ } catch (e) {
144
+ console.error(`dispatch_to: failed to load daemon.yaml: ${e.message}`);
145
+ process.exit(1);
146
+ }
147
+ const rd = normalizeRemoteDispatchConfig(config);
148
+ if (!rd) {
149
+ console.error('dispatch_to: feishu.remote_dispatch not configured or disabled');
150
+ process.exit(1);
151
+ }
152
+ if (!rd.secret) {
153
+ console.error('dispatch_to: remote dispatch secret missing; run /dispatch code then /dispatch pair <code>');
154
+ process.exit(1);
155
+ }
156
+ const ts = new Date().toISOString();
157
+ const id = `${rd.selfPeer}_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
158
+ const body = encodePacket({
159
+ v: 1, id, ts,
160
+ type: 'task',
161
+ from_peer: rd.selfPeer,
162
+ to_peer: peer,
163
+ target_project: project,
164
+ prompt: memberPrompt,
165
+ source_sender_key: fromKey,
166
+ source_sender_id: sourceSenderId,
167
+ }, rd.secret);
168
+
169
+ // Write to dispatch/remote-pending.jsonl for daemon to pick up and send via bot
170
+ const remotePending = path.join(DISPATCH_DIR, 'remote-pending.jsonl');
171
+ fs.appendFileSync(remotePending, JSON.stringify({ relay_chat_id: rd.chatId, body }) + '\n');
172
+ console.log(`DISPATCH_OK(remote): ${peer}:${project} → ${memberPrompt.slice(0, 60)}`);
173
+ }
83
174
 
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 }) };
175
+ // ── Team broadcast mode ───────────────────────────────────────────────────────
176
+ if (teamMode) {
177
+ let config = null;
178
+ try {
179
+ config = yaml.load(fs.readFileSync(path.join(METAME_DIR, 'daemon.yaml'), 'utf8'));
180
+ } catch (e) {
181
+ console.error(`dispatch_to --team: failed to load daemon.yaml: ${e.message}`);
182
+ process.exit(1);
183
+ }
87
184
 
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 });
185
+ const project = config && config.projects && config.projects[target];
186
+ if (!project) {
187
+ console.error(`dispatch_to --team: project "${target}" not found in daemon.yaml`);
188
+ process.exit(1);
189
+ }
90
190
 
91
- function fallbackToFile() {
92
- 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]' : ''}`);
95
- }
191
+ const team = Array.isArray(project.team) ? project.team : [];
192
+ if (team.length === 0) {
193
+ console.error(`dispatch_to --team: project "${target}" has no team members`);
194
+ process.exit(1);
195
+ }
96
196
 
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);
116
- 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();
197
+ console.log(`📢 Team broadcast → ${target} (${team.length} members): ${prompt.slice(0, 60)}`);
198
+
199
+ // Await all dispatches before exiting so async socket/file ops complete
200
+ Promise.all(team.map((member) => {
201
+ const roster = buildTeamRosterHint(target, member.key, config.projects);
202
+ const enriched = buildEnrichedPrompt(member.key, prompt, METAME_DIR, { includeShared: true });
203
+ const memberPrompt = roster ? `${roster}\n\n---\n${enriched}` : enriched;
204
+ // Remote member → relay dispatch
205
+ if (member.peer) {
206
+ sendRemoteViaRelay(member.peer, member.key, memberPrompt);
207
+ return Promise.resolve();
122
208
  }
123
- } catch {
124
- fallbackToFile();
125
- }
126
- sock.destroy();
127
- });
128
-
129
- sock.on('error', () => {
130
- if (done) return;
131
- done = true;
132
- clearTimeout(timer);
133
- fallbackToFile();
134
- });
209
+ return sendOne(member.key, memberPrompt, { team_roster_injected: true, skipEnrich: true });
210
+ })).then(() => process.exit(0));
211
+ } else {
212
+
213
+ // ── Normal single-target dispatch ─────────────────────────────────────────────
214
+ const remoteTarget = parseRemoteTargetRef(target);
215
+ if (remoteTarget) {
216
+ const enriched = buildEnrichedPrompt(remoteTarget.project, prompt, METAME_DIR, { includeShared: false });
217
+ sendRemoteViaRelay(remoteTarget.peer, remoteTarget.project, enriched);
218
+ process.exit(0);
219
+ } else {
220
+ sendOne(target, prompt).then(() => process.exit(0));
221
+ }
222
+ }