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.
- package/README.md +60 -18
- package/index.js +352 -79
- package/package.json +2 -2
- package/scripts/agent-layer.js +4 -2
- package/scripts/bin/dispatch_to +178 -90
- package/scripts/daemon-admin-commands.js +353 -105
- package/scripts/daemon-agent-commands.js +434 -66
- package/scripts/daemon-bridges.js +477 -68
- package/scripts/daemon-claude-engine.js +1267 -674
- package/scripts/daemon-command-router.js +205 -27
- package/scripts/daemon-command-session-route.js +118 -0
- package/scripts/daemon-default.yaml +7 -0
- package/scripts/daemon-engine-runtime.js +96 -20
- package/scripts/daemon-exec-commands.js +108 -49
- package/scripts/daemon-file-browser.js +64 -7
- package/scripts/daemon-notify.js +18 -4
- package/scripts/daemon-ops-commands.js +16 -2
- package/scripts/daemon-remote-dispatch.js +55 -1
- package/scripts/daemon-runtime-lifecycle.js +87 -0
- package/scripts/daemon-session-commands.js +102 -45
- package/scripts/daemon-session-store.js +497 -66
- package/scripts/daemon-siri-bridge.js +234 -0
- package/scripts/daemon-siri-imessage.js +209 -0
- package/scripts/daemon-task-scheduler.js +10 -2
- package/scripts/daemon.js +697 -179
- package/scripts/daemon.yaml +7 -0
- package/scripts/docs/agent-guide.md +36 -3
- package/scripts/docs/hook-config.md +134 -0
- package/scripts/docs/maintenance-manual.md +162 -5
- package/scripts/docs/pointer-map.md +60 -5
- package/scripts/feishu-adapter.js +7 -15
- package/scripts/hooks/doc-router.js +29 -0
- package/scripts/hooks/hook-utils.js +61 -0
- package/scripts/hooks/intent-doc-router.js +54 -0
- package/scripts/hooks/intent-engine.js +72 -0
- package/scripts/hooks/intent-file-transfer.js +51 -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/intent-registry.js +59 -0
- package/scripts/memory-extract.js +59 -0
- package/scripts/memory-nightly-reflect.js +109 -43
- package/scripts/memory.js +55 -17
- package/scripts/mentor-engine.js +6 -0
- package/scripts/schema.js +1 -0
- package/scripts/self-reflect.js +110 -12
- package/scripts/session-analytics.js +160 -0
- package/scripts/signal-capture.js +1 -1
- package/scripts/team-dispatch.js +315 -0
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,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(
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
-
|
|
77
|
-
|
|
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
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
-
//
|
|
85
|
-
|
|
86
|
-
|
|
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
|
-
|
|
89
|
-
|
|
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
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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
|
+
}
|