cyberlegion 0.0.0
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/bin/cyberlegion.mjs +6 -0
- package/dist/cli.d.mts +1 -0
- package/dist/cli.mjs +720 -0
- package/dist/index.d.mts +656 -0
- package/dist/index.mjs +28 -0
- package/dist/watch-DA8I44dv.mjs +1403 -0
- package/package.json +61 -0
package/dist/cli.mjs
ADDED
|
@@ -0,0 +1,720 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { B as loadAgent, C as resolveAgentDef, E as spawn, F as selectSessionAdapter, H as realExec, I as probeMultiplexer, K as resolveSelfId, L as bumpLastSeen, R as detectHarness, S as listAgentDefs, U as register, V as prune, W as resolveAgent, _ as inbox, a as fail, b as send, c as toonObject, d as DispatchWaitingError, f as channel, g as deleteMessage, h as ack, i as emit, j as resolveRoot, l as install, m as awaitReply, n as FileStore, o as nextStep, p as prep, q as touch, r as injectInbox, s as toonList, t as watchMail, u as collect, v as peek, w as realizeLaunch, x as decommission, y as resolveBody, z as listAgents } from "./watch-DA8I44dv.mjs";
|
|
3
|
+
import { resolve } from "node:path";
|
|
4
|
+
import { Command, Option } from "commander";
|
|
5
|
+
//#region src/admin.ts
|
|
6
|
+
/**
|
|
7
|
+
* Merge one store's registry + mailboxes + briefs into another (skip agent ids already present at
|
|
8
|
+
* the destination). Used by `admin migrate` when moving state between an old project-local root
|
|
9
|
+
* and the new global hub. Best-effort: it re-files every message into the destination's unread
|
|
10
|
+
* set — the source's own read/unread split is not preserved across a migrate.
|
|
11
|
+
*/
|
|
12
|
+
function migrateStore(from, to) {
|
|
13
|
+
let agents = 0;
|
|
14
|
+
let messages = 0;
|
|
15
|
+
let briefs = 0;
|
|
16
|
+
to.ensureMarker();
|
|
17
|
+
for (const rec of from.listAgents()) {
|
|
18
|
+
if (!to.getAgent(rec.id)) {
|
|
19
|
+
to.putAgent(rec);
|
|
20
|
+
agents++;
|
|
21
|
+
}
|
|
22
|
+
const snap = from.listInbox(rec.id);
|
|
23
|
+
for (const msg of [...snap.unread, ...snap.read]) {
|
|
24
|
+
to.putMessage(rec.id, msg);
|
|
25
|
+
messages++;
|
|
26
|
+
}
|
|
27
|
+
const brief = from.readBrief(rec.id);
|
|
28
|
+
if (brief != null) {
|
|
29
|
+
to.writeBrief(rec.id, brief);
|
|
30
|
+
briefs++;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return {
|
|
34
|
+
agents,
|
|
35
|
+
messages,
|
|
36
|
+
briefs
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
//#endregion
|
|
40
|
+
//#region src/cli.ts
|
|
41
|
+
const VERSION = "0.0.0";
|
|
42
|
+
function ctxOf(opts) {
|
|
43
|
+
return {
|
|
44
|
+
store: new FileStore(resolveRoot({ space: opts.space })),
|
|
45
|
+
env: process.env
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
function formatOf(opts) {
|
|
49
|
+
return opts.format === "json" ? "json" : "toon";
|
|
50
|
+
}
|
|
51
|
+
function requireSelf(ctx) {
|
|
52
|
+
const id = resolveSelfId(ctx);
|
|
53
|
+
if (!id) fail("no identity in this session — run `cyberlegion identity register` first");
|
|
54
|
+
bumpLastSeen(ctx, id);
|
|
55
|
+
return id;
|
|
56
|
+
}
|
|
57
|
+
function resolveTarget(ctx, ref) {
|
|
58
|
+
const agent = resolveAgent(ctx.store, ref);
|
|
59
|
+
const pane = agent.tmux?.pane ?? ctx.store.findPaneByAgentId(agent.id);
|
|
60
|
+
if (!pane) fail(`unit "${ref}" has no known session pane`);
|
|
61
|
+
return { id: pane };
|
|
62
|
+
}
|
|
63
|
+
function withGlobals(cmd) {
|
|
64
|
+
return cmd.option("--space <path>", "isolate the hub root (overrides the global hub / $CYBERLEGION_ROOT)").addOption(new Option("--format <format>", "output format").choices(["toon", "json"]).default("toon"));
|
|
65
|
+
}
|
|
66
|
+
const program = new Command();
|
|
67
|
+
program.name("cyberlegion").description("Harness-agnostic agent session spawning, messaging, and dispatch over the filesystem").version(VERSION).enablePositionalOptions();
|
|
68
|
+
const identity = program.command("identity").description("self-identify and discover peers");
|
|
69
|
+
withGlobals(identity.command("register")).description("register or refresh this session identity").option("--handle <name>", "human handle for this agent").option("--harness <h>", "claude | cursor | codex (else auto-detected)").action((opts) => {
|
|
70
|
+
const rec = register(ctxOf(opts), {
|
|
71
|
+
handle: opts.handle,
|
|
72
|
+
harness: opts.harness
|
|
73
|
+
});
|
|
74
|
+
emit(formatOf(opts), {
|
|
75
|
+
toon: toonObject({
|
|
76
|
+
id: rec.id,
|
|
77
|
+
handle: rec.handle,
|
|
78
|
+
harness: rec.harness,
|
|
79
|
+
status: rec.status
|
|
80
|
+
}),
|
|
81
|
+
json: rec
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
withGlobals(identity.command("whoami")).description("print this session own identity").action((opts) => {
|
|
85
|
+
const ctx = ctxOf(opts);
|
|
86
|
+
const id = resolveSelfId(ctx);
|
|
87
|
+
if (!id) fail("no identity in this session — run `cyberlegion identity register` first");
|
|
88
|
+
const rec = loadAgent(ctx.store, id);
|
|
89
|
+
if (!rec) fail(`registered self id "${id}" has no agent record`);
|
|
90
|
+
emit(formatOf(opts), {
|
|
91
|
+
toon: toonObject({
|
|
92
|
+
id: rec.id,
|
|
93
|
+
handle: rec.handle,
|
|
94
|
+
harness: rec.harness,
|
|
95
|
+
status: rec.status
|
|
96
|
+
}),
|
|
97
|
+
json: rec
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
function runWho(opts) {
|
|
101
|
+
const ctx = ctxOf(opts);
|
|
102
|
+
touch(ctx);
|
|
103
|
+
const agents = listAgents(ctx.store).filter((a) => opts.all || a.status !== "exited");
|
|
104
|
+
emit(formatOf(opts), {
|
|
105
|
+
toon: toonList("agents", agents, [
|
|
106
|
+
{
|
|
107
|
+
key: "id",
|
|
108
|
+
get: (a) => a.id
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
key: "handle",
|
|
112
|
+
get: (a) => a.handle
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
key: "harness",
|
|
116
|
+
get: (a) => a.harness
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
key: "status",
|
|
120
|
+
get: (a) => a.status
|
|
121
|
+
}
|
|
122
|
+
], `${agents.length} agents`),
|
|
123
|
+
json: agents
|
|
124
|
+
});
|
|
125
|
+
if (agents.length === 0) nextStep("cyberlegion identity register to join");
|
|
126
|
+
}
|
|
127
|
+
withGlobals(identity.command("who")).description("list the addressable peers").option("--all", "include exited agents").action(runWho);
|
|
128
|
+
withGlobals(identity.command("prune")).description("mark dead agents exited and sweep").action((opts) => {
|
|
129
|
+
const ctx = ctxOf(opts);
|
|
130
|
+
touch(ctx);
|
|
131
|
+
const changed = prune(ctx);
|
|
132
|
+
emit(formatOf(opts), {
|
|
133
|
+
toon: toonList("pruned", changed, [{
|
|
134
|
+
key: "id",
|
|
135
|
+
get: (a) => a.id
|
|
136
|
+
}, {
|
|
137
|
+
key: "handle",
|
|
138
|
+
get: (a) => a.handle
|
|
139
|
+
}], `${changed.length} pruned`),
|
|
140
|
+
json: changed
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
const session = program.command("session").description("warm peer session lifecycle over a multiplexer");
|
|
144
|
+
function defineSpawn(cmd) {
|
|
145
|
+
return withGlobals(cmd).description("launch a new peer session in its own git worktree (tmux or herdr)").option("--harness <h>", "claude | cursor | codex (required unless --agent/--agent-file resolves one)").option("--agent <name>", "resolve an agent def (.agents/agents/<name>.md) for harness/model/instructions").option("--agent-file <path>", "read an exact agent def file instead of resolving by name").option("--task <text>", "brief text, or - for stdin").option("--brief-file <path>", "read the brief from a file").option("--handle <name>", "handle for the new peer").option("--branch <name>", "branch for the new worktree (default cyberlegion/unit-<id>)").option("--worktree-path <path>", "where to check out the new worktree").addOption(new Option("--at <placement>", "where to open the new session").choices([
|
|
146
|
+
"pane:right",
|
|
147
|
+
"pane:down",
|
|
148
|
+
"tab",
|
|
149
|
+
"window"
|
|
150
|
+
]).default("pane:right")).action((opts) => {
|
|
151
|
+
const ctx = ctxOf(opts);
|
|
152
|
+
touch(ctx);
|
|
153
|
+
let harness = opts.harness;
|
|
154
|
+
let command;
|
|
155
|
+
if (opts.agent || opts.agentFile) {
|
|
156
|
+
let def;
|
|
157
|
+
try {
|
|
158
|
+
def = resolveAgentDef({
|
|
159
|
+
name: opts.agent,
|
|
160
|
+
file: opts.agentFile
|
|
161
|
+
});
|
|
162
|
+
} catch (err) {
|
|
163
|
+
fail(err instanceof Error ? err.message : String(err));
|
|
164
|
+
}
|
|
165
|
+
const realized = realizeLaunch(def, { harness: opts.harness });
|
|
166
|
+
harness = realized.harness;
|
|
167
|
+
command = realized.command;
|
|
168
|
+
}
|
|
169
|
+
if (!harness) fail("session spawn needs --harness, or --agent/--agent-file resolving one");
|
|
170
|
+
const res = spawn(ctx, {
|
|
171
|
+
harness,
|
|
172
|
+
command,
|
|
173
|
+
task: opts.task,
|
|
174
|
+
briefFile: opts.briefFile,
|
|
175
|
+
handle: opts.handle,
|
|
176
|
+
branch: opts.branch,
|
|
177
|
+
worktreePath: opts.worktreePath,
|
|
178
|
+
at: opts.at
|
|
179
|
+
});
|
|
180
|
+
emit(formatOf(opts), {
|
|
181
|
+
toon: toonObject({
|
|
182
|
+
spawned: res.agent.id,
|
|
183
|
+
handle: res.agent.handle,
|
|
184
|
+
harness: res.agent.harness,
|
|
185
|
+
worktree: res.agent.worktree?.root,
|
|
186
|
+
pane: res.pane
|
|
187
|
+
}),
|
|
188
|
+
json: res
|
|
189
|
+
});
|
|
190
|
+
nextStep(`cyberlegion session read ${res.agent.id}`);
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
defineSpawn(session.command("spawn"));
|
|
194
|
+
withGlobals(session.command("close")).description("tear down a unit's worktree + session and reap its state (the inverse of spawn)").argument("<id>", "unit id, handle, or worktree branch/CR ref").option("--force", "discard uncommitted changes in the worktree (never overrides refusing the primary checkout)").action((ref, opts) => {
|
|
195
|
+
const ctx = ctxOf(opts);
|
|
196
|
+
touch(ctx);
|
|
197
|
+
const agent = resolveAgent(ctx.store, ref);
|
|
198
|
+
const res = decommission(ctx, {
|
|
199
|
+
id: agent.id,
|
|
200
|
+
force: opts.force
|
|
201
|
+
});
|
|
202
|
+
emit(formatOf(opts), {
|
|
203
|
+
toon: toonObject({
|
|
204
|
+
closed: agent.id,
|
|
205
|
+
worktree: res.worktreeRoot ?? "-",
|
|
206
|
+
pane: res.pane ?? "-"
|
|
207
|
+
}),
|
|
208
|
+
json: res
|
|
209
|
+
});
|
|
210
|
+
});
|
|
211
|
+
withGlobals(session.command("list")).description("list live peer sessions").action((opts) => {
|
|
212
|
+
const ctx = ctxOf(opts);
|
|
213
|
+
touch(ctx);
|
|
214
|
+
const agents = listAgents(ctx.store).filter((a) => a.status !== "exited");
|
|
215
|
+
emit(formatOf(opts), {
|
|
216
|
+
toon: toonList("sessions", agents, [
|
|
217
|
+
{
|
|
218
|
+
key: "id",
|
|
219
|
+
get: (a) => a.id
|
|
220
|
+
},
|
|
221
|
+
{
|
|
222
|
+
key: "handle",
|
|
223
|
+
get: (a) => a.handle
|
|
224
|
+
},
|
|
225
|
+
{
|
|
226
|
+
key: "status",
|
|
227
|
+
get: (a) => a.status
|
|
228
|
+
},
|
|
229
|
+
{
|
|
230
|
+
key: "pane",
|
|
231
|
+
get: (a) => a.tmux?.pane ?? "-"
|
|
232
|
+
}
|
|
233
|
+
], `${agents.length} sessions`),
|
|
234
|
+
json: agents
|
|
235
|
+
});
|
|
236
|
+
});
|
|
237
|
+
withGlobals(session.command("focus")).description("move input focus to a peer's session").argument("<ref>", "unit id, handle, or worktree branch/CR ref").action((ref, opts) => {
|
|
238
|
+
const ctx = ctxOf(opts);
|
|
239
|
+
touch(ctx);
|
|
240
|
+
const target = resolveTarget(ctx, ref);
|
|
241
|
+
selectSessionAdapter(ctx.env ?? process.env).focus(realExec, target);
|
|
242
|
+
emit(formatOf(opts), {
|
|
243
|
+
toon: toonObject({
|
|
244
|
+
focused: ref,
|
|
245
|
+
pane: target.id
|
|
246
|
+
}),
|
|
247
|
+
json: {
|
|
248
|
+
ref,
|
|
249
|
+
pane: target.id
|
|
250
|
+
}
|
|
251
|
+
});
|
|
252
|
+
});
|
|
253
|
+
withGlobals(session.command("nudge")).description("ring a peer's session (a dumb doorbell — the mail is the payload)").argument("<ref>", "unit id, handle, or worktree branch/CR ref").action((ref, opts) => {
|
|
254
|
+
const ctx = ctxOf(opts);
|
|
255
|
+
touch(ctx);
|
|
256
|
+
const target = resolveTarget(ctx, ref);
|
|
257
|
+
selectSessionAdapter(ctx.env ?? process.env).send(realExec, target, "");
|
|
258
|
+
emit(formatOf(opts), {
|
|
259
|
+
toon: toonObject({
|
|
260
|
+
nudged: ref,
|
|
261
|
+
pane: target.id
|
|
262
|
+
}),
|
|
263
|
+
json: {
|
|
264
|
+
ref,
|
|
265
|
+
pane: target.id
|
|
266
|
+
}
|
|
267
|
+
});
|
|
268
|
+
});
|
|
269
|
+
withGlobals(session.command("read")).description("scrape a peer's session screen").argument("<ref>", "unit id, handle, or worktree branch/CR ref").option("--lines <n>", "trailing lines to capture", (v) => Number.parseInt(v, 10)).action((ref, opts) => {
|
|
270
|
+
const ctx = ctxOf(opts);
|
|
271
|
+
touch(ctx);
|
|
272
|
+
const target = resolveTarget(ctx, ref);
|
|
273
|
+
const text = selectSessionAdapter(ctx.env ?? process.env).read(realExec, target, { lines: opts.lines });
|
|
274
|
+
if (formatOf(opts) === "json") console.log(JSON.stringify({
|
|
275
|
+
ref,
|
|
276
|
+
pane: target.id,
|
|
277
|
+
output: text
|
|
278
|
+
}, null, 2));
|
|
279
|
+
else console.log(text);
|
|
280
|
+
});
|
|
281
|
+
const mail = program.command("mail").description("durable inter-agent messaging");
|
|
282
|
+
function defineSend(cmd) {
|
|
283
|
+
return withGlobals(cmd).description("send a message to a peer (by handle or id)").requiredOption("--to <peer>", "recipient handle or id").option("--from <id>", "sender id (else this session own identity)").option("--subject <s>", "subject").option("--body <text>", "message body").option("--body-file <path>", "read body from a file, or - for stdin").option("--thread <id>", "thread id").option("--reply-to <msg>", "message id this replies to").action((opts) => {
|
|
284
|
+
const ctx = ctxOf(opts);
|
|
285
|
+
const fromId = opts.from ?? requireSelf(ctx);
|
|
286
|
+
const body = resolveBody(opts.body, opts.bodyFile);
|
|
287
|
+
const msg = send({ store: ctx.store }, {
|
|
288
|
+
fromId,
|
|
289
|
+
to: opts.to,
|
|
290
|
+
subject: opts.subject,
|
|
291
|
+
body,
|
|
292
|
+
thread: opts.thread,
|
|
293
|
+
replyTo: opts.replyTo
|
|
294
|
+
});
|
|
295
|
+
emit(formatOf(opts), {
|
|
296
|
+
toon: toonObject({
|
|
297
|
+
sent: msg.id,
|
|
298
|
+
to: opts.to,
|
|
299
|
+
subject: msg.subject
|
|
300
|
+
}),
|
|
301
|
+
json: msg
|
|
302
|
+
});
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
defineSend(mail.command("send"));
|
|
306
|
+
function runInbox(opts) {
|
|
307
|
+
const ctx = ctxOf(opts);
|
|
308
|
+
touch(ctx);
|
|
309
|
+
const meId = requireSelf(ctx);
|
|
310
|
+
const items = inbox({ store: ctx.store }, {
|
|
311
|
+
meId,
|
|
312
|
+
unread: opts.unread,
|
|
313
|
+
from: opts.from,
|
|
314
|
+
thread: opts.thread
|
|
315
|
+
});
|
|
316
|
+
const unreadCount = items.filter((i) => !i.read).length;
|
|
317
|
+
emit(formatOf(opts), {
|
|
318
|
+
toon: toonList("messages", items, [
|
|
319
|
+
{
|
|
320
|
+
key: "id",
|
|
321
|
+
get: (m) => m.id
|
|
322
|
+
},
|
|
323
|
+
{
|
|
324
|
+
key: "from",
|
|
325
|
+
get: (m) => m.fromHandle
|
|
326
|
+
},
|
|
327
|
+
{
|
|
328
|
+
key: "subject",
|
|
329
|
+
get: (m) => m.subject ?? ""
|
|
330
|
+
},
|
|
331
|
+
{
|
|
332
|
+
key: "read",
|
|
333
|
+
get: (m) => m.read
|
|
334
|
+
}
|
|
335
|
+
], `${items.length} messages (${unreadCount} unread)`),
|
|
336
|
+
json: items
|
|
337
|
+
});
|
|
338
|
+
const firstUnread = items.find((m) => !m.read);
|
|
339
|
+
if (firstUnread) nextStep(`cyberlegion mail read ${firstUnread.id}`);
|
|
340
|
+
}
|
|
341
|
+
withGlobals(mail.command("inbox")).description("list your mail").option("--unread", "only un-acked mail").option("--from <id>", "filter by sender").option("--thread <id>", "filter to messages carrying this thread id").action(runInbox);
|
|
342
|
+
withGlobals(mail.command("read")).description("peek at a message without acknowledging it").argument("<msg-id>", "message id").action((msgId, opts) => {
|
|
343
|
+
const ctx = ctxOf(opts);
|
|
344
|
+
const meId = requireSelf(ctx);
|
|
345
|
+
const msg = peek({ store: ctx.store }, meId, msgId);
|
|
346
|
+
if (!msg) fail(`"${msgId}" is not a message in this inbox`);
|
|
347
|
+
emit(formatOf(opts), {
|
|
348
|
+
toon: toonObject({
|
|
349
|
+
id: msg.id,
|
|
350
|
+
from: msg.fromHandle,
|
|
351
|
+
subject: msg.subject,
|
|
352
|
+
body: msg.body
|
|
353
|
+
}),
|
|
354
|
+
json: msg
|
|
355
|
+
});
|
|
356
|
+
nextStep(`cyberlegion mail ack ${msg.id}`);
|
|
357
|
+
});
|
|
358
|
+
withGlobals(mail.command("ack")).description("acknowledge a message (moves it out of the unread set)").argument("<msg-id>", "message id").action((msgId, opts) => {
|
|
359
|
+
const ctx = ctxOf(opts);
|
|
360
|
+
const meId = requireSelf(ctx);
|
|
361
|
+
const msg = ack({ store: ctx.store }, meId, msgId);
|
|
362
|
+
emit(formatOf(opts), {
|
|
363
|
+
toon: toonObject({
|
|
364
|
+
acked: msg.id,
|
|
365
|
+
from: msg.fromHandle,
|
|
366
|
+
subject: msg.subject
|
|
367
|
+
}),
|
|
368
|
+
json: msg
|
|
369
|
+
});
|
|
370
|
+
});
|
|
371
|
+
withGlobals(mail.command("delete")).description("permanently remove a message from your inbox (unread or already-acked)").argument("<msg-id>", "message id").action((msgId, opts) => {
|
|
372
|
+
const ctx = ctxOf(opts);
|
|
373
|
+
const meId = requireSelf(ctx);
|
|
374
|
+
deleteMessage({ store: ctx.store }, meId, msgId);
|
|
375
|
+
emit(formatOf(opts), {
|
|
376
|
+
toon: toonObject({ deleted: msgId }),
|
|
377
|
+
json: { deleted: msgId }
|
|
378
|
+
});
|
|
379
|
+
});
|
|
380
|
+
withGlobals(mail.command("await")).description("block until a thread-correlated reply arrives, print it, and ack it").requiredOption("--thread <id>", "thread id to wait on").option("--from <h>", "only match a reply from this sender").option("--timeout <ms>", "give up after this many ms with no match (0 = wait forever); exits non-zero on timeout", (v) => Number.parseInt(v, 10), 6e5).option("--max-wait <s>", "self-cap for one internal poll cycle, in seconds — returns the clean \"waiting\" sentinel at this cap so the caller can re-arm rather than blocking past a harness tool-timeout", (v) => Number.parseInt(v, 10), 240).addHelpText("after", "\nThree outcomes:\n matched — exit 0; the message is printed on stdout and acked (moved out of the unread set).\n waiting — exit 0; a stderr \"waiting\" line and nothing on stdout — the per-call --max-wait\n cap was hit with no match yet; re-run the same command to keep waiting.\n timed-out — exit 1; a clear stderr message and nothing on stdout — --timeout elapsed with no match.\n").action(async (opts) => {
|
|
381
|
+
const ctx = ctxOf(opts);
|
|
382
|
+
const meId = requireSelf(ctx);
|
|
383
|
+
const outcome = await awaitReply({ store: ctx.store }, {
|
|
384
|
+
meId,
|
|
385
|
+
thread: opts.thread,
|
|
386
|
+
from: opts.from,
|
|
387
|
+
timeoutMs: opts.timeout,
|
|
388
|
+
maxWaitS: opts.maxWait
|
|
389
|
+
});
|
|
390
|
+
if (outcome.kind === "matched") {
|
|
391
|
+
const msg = outcome.message;
|
|
392
|
+
emit(formatOf(opts), {
|
|
393
|
+
toon: toonObject({
|
|
394
|
+
id: msg.id,
|
|
395
|
+
from: msg.fromHandle,
|
|
396
|
+
subject: msg.subject,
|
|
397
|
+
body: msg.body
|
|
398
|
+
}),
|
|
399
|
+
json: msg
|
|
400
|
+
});
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
if (outcome.kind === "waiting") {
|
|
404
|
+
console.error(`waiting — no reply on thread "${opts.thread}" within --max-wait ${opts.maxWait}s; re-run to keep waiting`);
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
fail(`no reply on thread "${opts.thread}" within ${opts.timeout}ms`);
|
|
408
|
+
});
|
|
409
|
+
withGlobals(mail.command("watch")).description("stream new matching mail as it arrives — an observer only, it never acks; Ctrl-C to stop").option("--thread <id>", "filter to a thread").option("--from <h>", "filter by sender").action(async (opts) => {
|
|
410
|
+
const ctx = ctxOf(opts);
|
|
411
|
+
const meId = requireSelf(ctx);
|
|
412
|
+
await watchMail({ store: ctx.store }, {
|
|
413
|
+
meId,
|
|
414
|
+
thread: opts.thread,
|
|
415
|
+
from: opts.from
|
|
416
|
+
}, (msg) => {
|
|
417
|
+
emit(formatOf(opts), {
|
|
418
|
+
toon: toonObject({
|
|
419
|
+
id: msg.id,
|
|
420
|
+
from: msg.fromHandle,
|
|
421
|
+
subject: msg.subject,
|
|
422
|
+
body: msg.body
|
|
423
|
+
}),
|
|
424
|
+
json: msg
|
|
425
|
+
});
|
|
426
|
+
});
|
|
427
|
+
});
|
|
428
|
+
withGlobals(mail.command("hook")).description("emit the harness hook injection payload (raw JSON on stdout, not TOON)").option("--event <event>", "SessionStart | PostToolUse", "SessionStart").action((opts) => {
|
|
429
|
+
const ctx = ctxOf(opts);
|
|
430
|
+
touch(ctx);
|
|
431
|
+
const payload = injectInbox(ctx, opts.event);
|
|
432
|
+
if (payload) console.log(JSON.stringify(payload));
|
|
433
|
+
});
|
|
434
|
+
const dispatch = program.command("dispatch").description("delegate work and await a result (result-slot primitives)");
|
|
435
|
+
function definePrepOptions(cmd) {
|
|
436
|
+
return withGlobals(cmd).option("--agent <name>", "resolve an agent def to build the instruction/launch from").option("--agent-file <path>", "read an exact agent def file instead of resolving by name").option("--role <name>", "role label folded into the generic instruction when no agent def is given").option("--brief-text <text>", "brief body text").option("--brief-file <path>", "read the brief from a file, or - for stdin").option("--verdict-schema <path>", "JSON schema file (required keys + primitive types) the result must satisfy").option("--thread <id>", "thread id (defaults to the minted dispatch id)");
|
|
437
|
+
}
|
|
438
|
+
function dispatchResultFields(r) {
|
|
439
|
+
return {
|
|
440
|
+
id: r.id,
|
|
441
|
+
verdict: r.verdict != null ? JSON.stringify(r.verdict) : void 0,
|
|
442
|
+
body: r.body,
|
|
443
|
+
ts: r.ts
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
definePrepOptions(dispatch.command("prep")).description("allocate an id + brief + result slot and return the envelope — spawns nothing, never invokes Task").action((opts) => {
|
|
447
|
+
const ctx = ctxOf(opts);
|
|
448
|
+
let envelope;
|
|
449
|
+
try {
|
|
450
|
+
envelope = prep({ store: ctx.store }, {
|
|
451
|
+
agent: opts.agent,
|
|
452
|
+
agentFile: opts.agentFile,
|
|
453
|
+
role: opts.role,
|
|
454
|
+
briefText: opts.briefText,
|
|
455
|
+
briefFile: opts.briefFile,
|
|
456
|
+
thread: opts.thread
|
|
457
|
+
});
|
|
458
|
+
} catch (err) {
|
|
459
|
+
fail(err instanceof Error ? err.message : String(err));
|
|
460
|
+
}
|
|
461
|
+
emit(formatOf(opts), {
|
|
462
|
+
toon: toonObject({ ...envelope }),
|
|
463
|
+
json: envelope
|
|
464
|
+
});
|
|
465
|
+
nextStep(`spawn a subagent with the instruction, then \`cyberlegion dispatch collect ${envelope.id}\` — or on the channel path, \`cyberlegion mail await --thread ${envelope.thread}\``);
|
|
466
|
+
});
|
|
467
|
+
definePrepOptions(dispatch.command("channel")).description("prep + spawn a peer session + (--wait) await the mail-thread reply — the one CLI-driven convenience (needs --agent or --agent-file to realize the peer launch)").addOption(new Option("--at <placement>", "where to open the new session").choices([
|
|
468
|
+
"pane:right",
|
|
469
|
+
"pane:down",
|
|
470
|
+
"tab",
|
|
471
|
+
"window"
|
|
472
|
+
]).default("pane:right")).option("--wait", "block for the mail-thread reply and print the validated result").option("--timeout <ms>", "give up after this many ms with no reply (0 = wait forever)", (v) => Number.parseInt(v, 10), 6e5).option("--max-wait <s>", "self-cap for one internal poll cycle, in seconds (re-arm by re-running with --wait)", (v) => Number.parseInt(v, 10), 240).action(async (opts) => {
|
|
473
|
+
const ctx = ctxOf(opts);
|
|
474
|
+
touch(ctx);
|
|
475
|
+
let result;
|
|
476
|
+
try {
|
|
477
|
+
result = await channel(ctx, {
|
|
478
|
+
agent: opts.agent,
|
|
479
|
+
agentFile: opts.agentFile,
|
|
480
|
+
role: opts.role,
|
|
481
|
+
briefText: opts.briefText,
|
|
482
|
+
briefFile: opts.briefFile,
|
|
483
|
+
verdictSchema: opts.verdictSchema,
|
|
484
|
+
thread: opts.thread,
|
|
485
|
+
at: opts.at,
|
|
486
|
+
wait: opts.wait,
|
|
487
|
+
timeoutMs: opts.timeout,
|
|
488
|
+
maxWaitS: opts.maxWait
|
|
489
|
+
});
|
|
490
|
+
} catch (err) {
|
|
491
|
+
if (err instanceof DispatchWaitingError) {
|
|
492
|
+
console.error(err.message);
|
|
493
|
+
return;
|
|
494
|
+
}
|
|
495
|
+
fail(err instanceof Error ? err.message : String(err));
|
|
496
|
+
}
|
|
497
|
+
if ("ts" in result) {
|
|
498
|
+
emit(formatOf(opts), {
|
|
499
|
+
toon: toonObject(dispatchResultFields(result)),
|
|
500
|
+
json: result
|
|
501
|
+
});
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
504
|
+
emit(formatOf(opts), {
|
|
505
|
+
toon: toonObject({ ...result }),
|
|
506
|
+
json: result
|
|
507
|
+
});
|
|
508
|
+
nextStep(`cyberlegion mail await --thread ${result.thread} to collect the reply later`);
|
|
509
|
+
});
|
|
510
|
+
withGlobals(dispatch.command("collect")).description("read + validate a subagent's result file (the subagent path's counterpart to mail await)").argument("<id>", "dispatch id").option("--verdict-schema <path>", "JSON schema file the result must satisfy").action((id, opts) => {
|
|
511
|
+
const ctx = ctxOf(opts);
|
|
512
|
+
let result;
|
|
513
|
+
try {
|
|
514
|
+
result = collect({ store: ctx.store }, id, opts.verdictSchema);
|
|
515
|
+
} catch (err) {
|
|
516
|
+
fail(err instanceof Error ? err.message : String(err));
|
|
517
|
+
}
|
|
518
|
+
emit(formatOf(opts), {
|
|
519
|
+
toon: toonObject(dispatchResultFields(result)),
|
|
520
|
+
json: result
|
|
521
|
+
});
|
|
522
|
+
});
|
|
523
|
+
const agent = program.command("agent").description("resolve reusable agent definitions");
|
|
524
|
+
const INSTRUCTIONS_PREVIEW_LEN = 200;
|
|
525
|
+
function truncated(text, full) {
|
|
526
|
+
if (full || text.length <= INSTRUCTIONS_PREVIEW_LEN) return text;
|
|
527
|
+
return `${text.slice(0, INSTRUCTIONS_PREVIEW_LEN)}… (${text.length} chars total, pass --full)`;
|
|
528
|
+
}
|
|
529
|
+
function agentDefFields(d) {
|
|
530
|
+
return {
|
|
531
|
+
name: d.name,
|
|
532
|
+
description: d.description,
|
|
533
|
+
model: d.model ?? "(harness default)",
|
|
534
|
+
effort: d.effort,
|
|
535
|
+
harness: d.harness ?? "(harness default)",
|
|
536
|
+
warm: d.warm ?? false,
|
|
537
|
+
interactive: d.interactive ?? false,
|
|
538
|
+
path: d.path
|
|
539
|
+
};
|
|
540
|
+
}
|
|
541
|
+
withGlobals(agent.command("list")).description("list resolvable agent definitions under .agents/agents/").option("--dir <path>", "project dir to search from", process.cwd()).action((opts) => {
|
|
542
|
+
const defs = listAgentDefs({ cwd: opts.dir });
|
|
543
|
+
emit(formatOf(opts), {
|
|
544
|
+
toon: toonList("defs", defs, [
|
|
545
|
+
{
|
|
546
|
+
key: "name",
|
|
547
|
+
get: (d) => d.name
|
|
548
|
+
},
|
|
549
|
+
{
|
|
550
|
+
key: "model",
|
|
551
|
+
get: (d) => d.model ?? "-"
|
|
552
|
+
},
|
|
553
|
+
{
|
|
554
|
+
key: "harness",
|
|
555
|
+
get: (d) => d.harness ?? "-"
|
|
556
|
+
}
|
|
557
|
+
], `${defs.length} agent definitions`),
|
|
558
|
+
json: defs
|
|
559
|
+
});
|
|
560
|
+
if (defs.length === 0) nextStep("add a .md file under .agents/agents/ to define one");
|
|
561
|
+
});
|
|
562
|
+
withGlobals(agent.command("show")).description("show a resolved agent definition (model/effort/harness/warm/interactive + instructions)").argument("<name>", "agent def name (file stem under .agents/agents/)").option("--dir <path>", "project dir to search from", process.cwd()).option("--full", "show the full instructions body (default: truncated)").action((name, opts) => {
|
|
563
|
+
let def;
|
|
564
|
+
try {
|
|
565
|
+
def = resolveAgentDef({
|
|
566
|
+
name,
|
|
567
|
+
cwd: opts.dir
|
|
568
|
+
});
|
|
569
|
+
} catch (err) {
|
|
570
|
+
fail(err instanceof Error ? err.message : String(err));
|
|
571
|
+
}
|
|
572
|
+
emit(formatOf(opts), {
|
|
573
|
+
toon: toonObject({
|
|
574
|
+
...agentDefFields(def),
|
|
575
|
+
instructions: truncated(def.instructions, opts.full)
|
|
576
|
+
}),
|
|
577
|
+
json: {
|
|
578
|
+
...def,
|
|
579
|
+
instructions: opts.full ? def.instructions : truncated(def.instructions, opts.full)
|
|
580
|
+
}
|
|
581
|
+
});
|
|
582
|
+
});
|
|
583
|
+
withGlobals(agent.command("resolve")).description("emit the full machine payload for a def — for a routing caller to compose a launch/spawn from").argument("[name]", "agent def name (omit when passing --file)").option("--file <path>", "read an exact def file instead of resolving by name (plugin-scoped escape hatch)").option("--dir <path>", "project dir to search from", process.cwd()).action((name, opts) => {
|
|
584
|
+
let def;
|
|
585
|
+
try {
|
|
586
|
+
def = resolveAgentDef({
|
|
587
|
+
name,
|
|
588
|
+
file: opts.file,
|
|
589
|
+
cwd: opts.dir
|
|
590
|
+
});
|
|
591
|
+
} catch (err) {
|
|
592
|
+
fail(err instanceof Error ? err.message : String(err));
|
|
593
|
+
}
|
|
594
|
+
emit(formatOf(opts), {
|
|
595
|
+
toon: toonObject(agentDefFields(def)),
|
|
596
|
+
json: def
|
|
597
|
+
});
|
|
598
|
+
});
|
|
599
|
+
withGlobals(agent.command("path")).description("print the resolved def file path").argument("<name>", "agent def name (file stem under .agents/agents/)").option("--dir <path>", "project dir to search from", process.cwd()).action((name, opts) => {
|
|
600
|
+
let def;
|
|
601
|
+
try {
|
|
602
|
+
def = resolveAgentDef({
|
|
603
|
+
name,
|
|
604
|
+
cwd: opts.dir
|
|
605
|
+
});
|
|
606
|
+
} catch (err) {
|
|
607
|
+
fail(err instanceof Error ? err.message : String(err));
|
|
608
|
+
}
|
|
609
|
+
emit(formatOf(opts), {
|
|
610
|
+
toon: toonObject({ path: def.path }),
|
|
611
|
+
json: { path: def.path }
|
|
612
|
+
});
|
|
613
|
+
});
|
|
614
|
+
const admin = program.command("admin").description("setup and diagnostics");
|
|
615
|
+
withGlobals(admin.command("install")).description("wire the mail-surfacing hook into a harness config").requiredOption("--agent <harness>", "claude | cursor | codex").option("--dir <path>", "project dir to write config into", process.cwd()).action((opts) => {
|
|
616
|
+
const results = install(opts.agent, opts.dir);
|
|
617
|
+
emit(formatOf(opts), {
|
|
618
|
+
toon: toonList("hooks", results, [
|
|
619
|
+
{
|
|
620
|
+
key: "event",
|
|
621
|
+
get: (r) => r.event
|
|
622
|
+
},
|
|
623
|
+
{
|
|
624
|
+
key: "status",
|
|
625
|
+
get: (r) => r.status
|
|
626
|
+
},
|
|
627
|
+
{
|
|
628
|
+
key: "file",
|
|
629
|
+
get: (r) => r.file
|
|
630
|
+
}
|
|
631
|
+
], `${results.length} hooks`),
|
|
632
|
+
json: results
|
|
633
|
+
});
|
|
634
|
+
});
|
|
635
|
+
withGlobals(admin.command("doctor")).description("probe harness, multiplexer (ancestry-discovered), hub root, and self-id").action((opts) => {
|
|
636
|
+
const ctx = ctxOf(opts);
|
|
637
|
+
const harness = detectHarness(void 0, ctx) ?? "unknown";
|
|
638
|
+
const probe = probeMultiplexer(ctx.exec ?? realExec, ctx.env ?? process.env);
|
|
639
|
+
const selfId = resolveSelfId(ctx) ?? "-";
|
|
640
|
+
emit(formatOf(opts), {
|
|
641
|
+
toon: toonObject({
|
|
642
|
+
harness,
|
|
643
|
+
mux: probe.mux,
|
|
644
|
+
pane: probe.pane,
|
|
645
|
+
via: probe.via,
|
|
646
|
+
hubRoot: ctx.store.root,
|
|
647
|
+
selfId
|
|
648
|
+
}),
|
|
649
|
+
json: {
|
|
650
|
+
harness,
|
|
651
|
+
mux: probe.mux,
|
|
652
|
+
pane: probe.pane,
|
|
653
|
+
via: probe.via,
|
|
654
|
+
hubRoot: ctx.store.root,
|
|
655
|
+
selfId
|
|
656
|
+
}
|
|
657
|
+
});
|
|
658
|
+
if (probe.mux !== "none") nextStep(`export CYBERLEGION_MUX=${probe.mux}${probe.pane ? ` CYBERLEGION_MUX_PANE=${probe.pane}` : ""} — pin the fast-path, skip ancestry discovery on later calls`);
|
|
659
|
+
});
|
|
660
|
+
withGlobals(admin.command("mode")).description("report the detected session-backend mode").action((opts) => {
|
|
661
|
+
let mux = "none";
|
|
662
|
+
try {
|
|
663
|
+
mux = selectSessionAdapter(process.env).name;
|
|
664
|
+
} catch {}
|
|
665
|
+
emit(formatOf(opts), {
|
|
666
|
+
toon: toonObject({ mode: mux }),
|
|
667
|
+
json: { mode: mux }
|
|
668
|
+
});
|
|
669
|
+
});
|
|
670
|
+
withGlobals(admin.command("migrate")).description("merge one hub root state into another (e.g. an old project-local root into the global hub)").requiredOption("--from <path>", "source hub root").requiredOption("--to <path>", "destination hub root").action((opts) => {
|
|
671
|
+
const res = migrateStore(new FileStore(resolve(opts.from)), new FileStore(resolve(opts.to)));
|
|
672
|
+
emit(formatOf(opts), {
|
|
673
|
+
toon: toonObject({
|
|
674
|
+
agents: res.agents,
|
|
675
|
+
messages: res.messages,
|
|
676
|
+
briefs: res.briefs
|
|
677
|
+
}),
|
|
678
|
+
json: res
|
|
679
|
+
});
|
|
680
|
+
});
|
|
681
|
+
defineSpawn(program.command("spawn"));
|
|
682
|
+
defineSend(program.command("send"));
|
|
683
|
+
withGlobals(program.command("inbox")).description("list your mail (alias of `mail inbox`)").option("--unread", "only un-acked mail").option("--from <id>", "filter by sender").option("--thread <id>", "filter to messages carrying this thread id").action(runInbox);
|
|
684
|
+
withGlobals(program.command("who")).description("list the addressable peers (alias of `identity who`)").option("--all", "include exited agents").action(runWho);
|
|
685
|
+
withGlobals(program).action((opts) => {
|
|
686
|
+
const ctx = ctxOf(opts);
|
|
687
|
+
touch(ctx);
|
|
688
|
+
const id = resolveSelfId(ctx);
|
|
689
|
+
const rec = id ? loadAgent(ctx.store, id) : void 0;
|
|
690
|
+
const unread = id ? inbox({ store: ctx.store }, {
|
|
691
|
+
meId: id,
|
|
692
|
+
unread: true
|
|
693
|
+
}).length : 0;
|
|
694
|
+
const units = listAgents(ctx.store).filter((a) => a.status !== "exited").length;
|
|
695
|
+
emit(formatOf(opts), {
|
|
696
|
+
toon: toonObject({
|
|
697
|
+
self: rec?.handle ?? id ?? "-",
|
|
698
|
+
harness: rec?.harness ?? "-",
|
|
699
|
+
unread,
|
|
700
|
+
units
|
|
701
|
+
}),
|
|
702
|
+
json: {
|
|
703
|
+
self: rec ? {
|
|
704
|
+
id: rec.id,
|
|
705
|
+
handle: rec.handle,
|
|
706
|
+
harness: rec.harness
|
|
707
|
+
} : null,
|
|
708
|
+
unread,
|
|
709
|
+
units
|
|
710
|
+
}
|
|
711
|
+
});
|
|
712
|
+
if (!id) nextStep("cyberlegion identity register to join");
|
|
713
|
+
else if (unread > 0) nextStep("cyberlegion mail inbox --unread");
|
|
714
|
+
});
|
|
715
|
+
program.parseAsync(process.argv).catch((err) => {
|
|
716
|
+
console.error(JSON.stringify({ error: err instanceof Error ? err.message : String(err) }));
|
|
717
|
+
process.exit(1);
|
|
718
|
+
});
|
|
719
|
+
//#endregion
|
|
720
|
+
export {};
|