@phnx-labs/agents-cli 1.20.63 → 1.20.64
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/CHANGELOG.md +17 -0
- package/README.md +9 -0
- package/dist/bin/agents +0 -0
- package/dist/commands/exec.js +55 -28
- package/dist/commands/feed.d.ts +4 -0
- package/dist/commands/feed.js +27 -8
- package/dist/commands/lease.d.ts +23 -0
- package/dist/commands/lease.js +201 -0
- package/dist/commands/mailboxes.d.ts +20 -0
- package/dist/commands/mailboxes.js +390 -0
- package/dist/commands/routines.js +20 -14
- package/dist/commands/sessions-export.d.ts +2 -0
- package/dist/commands/sessions-export.js +279 -0
- package/dist/commands/sessions-import.d.ts +2 -0
- package/dist/commands/sessions-import.js +230 -0
- package/dist/commands/sessions.js +4 -0
- package/dist/commands/ssh.js +98 -3
- package/dist/commands/usage.d.ts +2 -0
- package/dist/commands/usage.js +7 -2
- package/dist/commands/view.d.ts +1 -1
- package/dist/index.js +2 -1
- package/dist/lib/agents.d.ts +18 -0
- package/dist/lib/agents.js +27 -17
- package/dist/lib/browser/drivers/ssh.js +19 -2
- package/dist/lib/comms-render.d.ts +37 -0
- package/dist/lib/comms-render.js +89 -0
- package/dist/lib/crabbox/cli.d.ts +72 -0
- package/dist/lib/crabbox/cli.js +158 -9
- package/dist/lib/crabbox/runtimes.d.ts +13 -0
- package/dist/lib/crabbox/runtimes.js +24 -0
- package/dist/lib/daemon.js +6 -1
- package/dist/lib/devices/health.d.ts +77 -0
- package/dist/lib/devices/health.js +186 -0
- package/dist/lib/mailbox.d.ts +39 -0
- package/dist/lib/mailbox.js +112 -0
- package/dist/lib/paths.d.ts +13 -0
- package/dist/lib/paths.js +26 -4
- package/dist/lib/routines.d.ts +21 -2
- package/dist/lib/routines.js +35 -12
- package/dist/lib/runner.js +255 -13
- package/dist/lib/sandbox.d.ts +9 -1
- package/dist/lib/sandbox.js +11 -2
- package/dist/lib/session/bundle.d.ts +150 -0
- package/dist/lib/session/bundle.js +189 -0
- package/dist/lib/session/remote-bundle.d.ts +12 -0
- package/dist/lib/session/remote-bundle.js +61 -0
- package/dist/lib/session/sync/agents.d.ts +54 -6
- package/dist/lib/session/sync/agents.js +0 -0
- package/dist/lib/session/sync/manifest.d.ts +14 -3
- package/dist/lib/session/sync/manifest.js +4 -0
- package/dist/lib/session/sync/sync.d.ts +23 -2
- package/dist/lib/session/sync/sync.js +177 -74
- package/dist/lib/ssh-tunnel.js +13 -1
- package/dist/lib/staleness/detectors/subagents.d.ts +5 -0
- package/dist/lib/staleness/detectors/subagents.js +5 -192
- package/dist/lib/staleness/writers/subagents.d.ts +10 -0
- package/dist/lib/staleness/writers/subagents.js +11 -102
- package/dist/lib/startup/command-registry.d.ts +2 -0
- package/dist/lib/startup/command-registry.js +5 -0
- package/dist/lib/subagents-registry.d.ts +85 -0
- package/dist/lib/subagents-registry.js +393 -0
- package/dist/lib/subagents.d.ts +8 -8
- package/dist/lib/subagents.js +32 -663
- package/dist/lib/sync-umbrella.d.ts +1 -0
- package/dist/lib/sync-umbrella.js +14 -3
- package/dist/lib/types.d.ts +9 -0
- package/dist/lib/usage.d.ts +42 -3
- package/dist/lib/usage.js +162 -22
- package/package.json +1 -1
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
import * as os from 'os';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import { die, humanDuration, relTime, truncate, visibleWidth } from '../lib/format.js';
|
|
5
|
+
import { listBoxes, readBox, mailboxDir, isValidMailboxId, watchMessages, } from '../lib/mailbox.js';
|
|
6
|
+
import { GLYPH, masthead, sparkline, aggregate, hourlyCounts, graphEdges, } from '../lib/comms-render.js';
|
|
7
|
+
import { getMailboxRootDir } from '../lib/state.js';
|
|
8
|
+
import { getActiveSessions } from '../lib/session/active.js';
|
|
9
|
+
import { mailboxIdForActiveSession } from '../lib/mailbox-target.js';
|
|
10
|
+
/** Short, human label for a box id — the live session's topic when running, else the id stem. */
|
|
11
|
+
function labelForBox(id, byMailbox) {
|
|
12
|
+
const s = byMailbox.get(id);
|
|
13
|
+
if (s) {
|
|
14
|
+
const topic = s.name || s.topic || s.label;
|
|
15
|
+
return { label: topic ? truncate(topic, 40) : id.slice(0, 8), live: true };
|
|
16
|
+
}
|
|
17
|
+
return { label: id.slice(0, 8), live: false };
|
|
18
|
+
}
|
|
19
|
+
/** Build the per-box views, resolving live-session labels once. */
|
|
20
|
+
async function collectBoxes(root) {
|
|
21
|
+
let sessions = [];
|
|
22
|
+
try {
|
|
23
|
+
sessions = await getActiveSessions();
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
// Label enrichment is best-effort; a box with no live session still lists.
|
|
27
|
+
}
|
|
28
|
+
const byMailbox = new Map();
|
|
29
|
+
for (const s of sessions) {
|
|
30
|
+
const mid = mailboxIdForActiveSession(s);
|
|
31
|
+
if (mid)
|
|
32
|
+
byMailbox.set(mid, s);
|
|
33
|
+
}
|
|
34
|
+
return listBoxes(root).map((id) => {
|
|
35
|
+
const { label, live } = labelForBox(id, byMailbox);
|
|
36
|
+
return { id, label, live, messages: readBox(mailboxDir(id, root)) };
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
function pending(box) {
|
|
40
|
+
return box.messages.filter((m) => m.state !== 'consumed').length;
|
|
41
|
+
}
|
|
42
|
+
/** `from` label for a message — agents stamp `claude/<slug>`; operator sends may omit it. */
|
|
43
|
+
function senderOf(m) {
|
|
44
|
+
return m.from || 'operator';
|
|
45
|
+
}
|
|
46
|
+
const STATE_TAG = {
|
|
47
|
+
inbox: chalk.yellow('pending'),
|
|
48
|
+
processing: chalk.cyan('in-flight'),
|
|
49
|
+
consumed: chalk.dim('delivered'),
|
|
50
|
+
};
|
|
51
|
+
/**
|
|
52
|
+
* The watching agent's own box id. Spawn wiring (`buildExecEnv`,
|
|
53
|
+
* lib/exec.ts) hands every agent `AGENTS_MAILBOX_DIR` keyed by its box, so
|
|
54
|
+
* `basename` is the id that resolves to "you". Unset for a human operator at
|
|
55
|
+
* a plain terminal — nothing in the spool is addressed to them.
|
|
56
|
+
*/
|
|
57
|
+
function selfMailboxId() {
|
|
58
|
+
const dir = process.env.AGENTS_MAILBOX_DIR;
|
|
59
|
+
if (!dir)
|
|
60
|
+
return undefined;
|
|
61
|
+
const id = path.basename(dir.replace(/[/\\]+$/, ''));
|
|
62
|
+
return id || undefined;
|
|
63
|
+
}
|
|
64
|
+
/** Parse `--since`: relative offsets (30s/5m/2h/7d/4w) or an ISO/absolute date. Returns epoch ms. */
|
|
65
|
+
function parseSinceArg(s) {
|
|
66
|
+
const m = s.match(/^(\d+)([smhdw])$/);
|
|
67
|
+
if (m) {
|
|
68
|
+
const n = parseInt(m[1], 10);
|
|
69
|
+
const unitMs = {
|
|
70
|
+
s: 1000, m: 60_000, h: 3_600_000, d: 86_400_000, w: 604_800_000,
|
|
71
|
+
};
|
|
72
|
+
return Date.now() - n * unitMs[m[2]];
|
|
73
|
+
}
|
|
74
|
+
const ms = Date.parse(s);
|
|
75
|
+
if (Number.isNaN(ms))
|
|
76
|
+
die(`Invalid --since value: ${JSON.stringify(s)} (use e.g. 2h, 7d, or an ISO date).`);
|
|
77
|
+
return ms;
|
|
78
|
+
}
|
|
79
|
+
function buildFilters(opts) {
|
|
80
|
+
return {
|
|
81
|
+
from: opts.from,
|
|
82
|
+
to: opts.to,
|
|
83
|
+
sinceMs: opts.since ? parseSinceArg(opts.since) : undefined,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
function hasFilters(f) {
|
|
87
|
+
return Boolean(f.from || f.to || f.sinceMs != null);
|
|
88
|
+
}
|
|
89
|
+
/** Sender substring on `from`, recipient substring on box id/label, recency cutoff — all case-insensitive. */
|
|
90
|
+
function matchesFilters(msg, toLabel, boxId, f) {
|
|
91
|
+
if (f.sinceMs != null) {
|
|
92
|
+
const t = Date.parse(msg.ts);
|
|
93
|
+
if (Number.isNaN(t) || t < f.sinceMs)
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
if (f.from && !msg.from.toLowerCase().includes(f.from.toLowerCase()))
|
|
97
|
+
return false;
|
|
98
|
+
if (f.to && !toLabel.toLowerCase().includes(f.to.toLowerCase()) && !boxId.toLowerCase().includes(f.to.toLowerCase()))
|
|
99
|
+
return false;
|
|
100
|
+
return true;
|
|
101
|
+
}
|
|
102
|
+
/** HH:MM:SS local wall-clock for the --watch stream; unparseable stamps render as dashes. */
|
|
103
|
+
function clockTime(iso) {
|
|
104
|
+
const d = new Date(iso);
|
|
105
|
+
if (Number.isNaN(d.getTime()))
|
|
106
|
+
return '--:--:--';
|
|
107
|
+
const p = (n) => String(n).padStart(2, '0');
|
|
108
|
+
return `${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())}`;
|
|
109
|
+
}
|
|
110
|
+
function renderOverview(boxes, limit, filters) {
|
|
111
|
+
const nonEmpty = boxes.filter((b) => b.messages.length > 0);
|
|
112
|
+
if (nonEmpty.length === 0) {
|
|
113
|
+
console.log(chalk.dim('No mailboxes with messages. Boxes are created on the first `agents message`.'));
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
// 1. Masthead + 24h volume sparkline over the whole spool.
|
|
117
|
+
const msgs = aggregate(nonEmpty);
|
|
118
|
+
const live = boxes.filter((b) => b.live).length;
|
|
119
|
+
const awaiting = msgs.filter((m) => m.state !== 'consumed').length;
|
|
120
|
+
console.log(masthead({
|
|
121
|
+
title: 'fleet comms',
|
|
122
|
+
host: os.hostname(),
|
|
123
|
+
accent: 'cyan',
|
|
124
|
+
right: `${live} live · ${boxes.length} boxes`,
|
|
125
|
+
stats: [`${msgs.length} messages`, `${awaiting} awaiting delivery`, `last ${relTime(msgs[0].ts)}`],
|
|
126
|
+
}));
|
|
127
|
+
console.log(` ${chalk.dim('24h')} ${chalk.cyan(sparkline(hourlyCounts(msgs, 24)))}`);
|
|
128
|
+
console.log();
|
|
129
|
+
// 2. Box summary — one row per box that has ever held mail.
|
|
130
|
+
const rows = [...nonEmpty].sort((a, b) => lastTs(b) - lastTs(a));
|
|
131
|
+
for (const box of rows) {
|
|
132
|
+
const p = pending(box);
|
|
133
|
+
const dot = box.live ? chalk.green(GLYPH.live) : chalk.dim(GLYPH.idle);
|
|
134
|
+
const counts = [
|
|
135
|
+
p > 0 ? chalk.yellow(`${p} pending`) : null,
|
|
136
|
+
chalk.dim(`${box.messages.length} total`),
|
|
137
|
+
].filter(Boolean).join(chalk.dim(' · '));
|
|
138
|
+
const last = box.messages.length ? chalk.dim(relTime(newestMessage(box).ts)) : '';
|
|
139
|
+
console.log(` ${dot} ${chalk.bold(box.label.padEnd(40))} ${counts} ${last}`);
|
|
140
|
+
console.log(` ${chalk.dim(box.id)}`);
|
|
141
|
+
}
|
|
142
|
+
// 3. Recent cross-box message log — the agent-to-agent chatter, newest first.
|
|
143
|
+
const all = msgs
|
|
144
|
+
.filter((m) => matchesFilters(m, m.toLabel, m.box, filters))
|
|
145
|
+
.slice(0, limit);
|
|
146
|
+
console.log();
|
|
147
|
+
console.log(chalk.bold(`Recent messages`) + chalk.dim(` (${all.length})`));
|
|
148
|
+
if (all.length === 0) {
|
|
149
|
+
console.log(chalk.dim(' Nothing matches the current filters.'));
|
|
150
|
+
}
|
|
151
|
+
for (const m of all) {
|
|
152
|
+
const when = chalk.dim(relTime(m.ts).padStart(10));
|
|
153
|
+
const route = `${chalk.magenta(truncate(m.from, 24))} ${chalk.dim(GLYPH.route)} ${chalk.cyan(truncate(m.toLabel, 40))}`;
|
|
154
|
+
console.log(` ${when} ${STATE_TAG[m.state]} ${route}`);
|
|
155
|
+
console.log(` ${truncate(m.text.replace(/\s+/g, ' ').trim(), 100)}`);
|
|
156
|
+
}
|
|
157
|
+
console.log();
|
|
158
|
+
console.log(chalk.dim('Tip: `agents mailboxes <id>` one box · `--watch` live tail · `--between <a> <b>` thread · `--graph` routes · `--json` machine output'));
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* --watch: the money shot. Stream every new cross-box message as it lands,
|
|
162
|
+
* resolved to live labels; a message addressed to the watching agent's own
|
|
163
|
+
* box (AGENTS_MAILBOX_DIR) renders as `▲ you` so an orchestrator sees its
|
|
164
|
+
* replies light up. ⌃C aborts the poller cleanly via AbortController.
|
|
165
|
+
*/
|
|
166
|
+
async function runWatch(root, opts) {
|
|
167
|
+
const boxes = await collectBoxes(root);
|
|
168
|
+
const labels = new Map(boxes.map((b) => [b.id, b.label]));
|
|
169
|
+
const self = selfMailboxId();
|
|
170
|
+
const controller = new AbortController();
|
|
171
|
+
const onSigint = () => controller.abort();
|
|
172
|
+
process.on('SIGINT', onSigint);
|
|
173
|
+
try {
|
|
174
|
+
if (!opts.json) {
|
|
175
|
+
console.log(masthead({
|
|
176
|
+
title: 'fleet comms',
|
|
177
|
+
host: os.hostname(),
|
|
178
|
+
accent: 'cyan',
|
|
179
|
+
right: chalk.green(GLYPH.live) + chalk.dim(' live'),
|
|
180
|
+
stats: ['watching — ⌃C to stop'],
|
|
181
|
+
}));
|
|
182
|
+
}
|
|
183
|
+
// --since backfills: the watcher replays existing mail and the recency
|
|
184
|
+
// filter keeps only the requested window, then the tail continues live.
|
|
185
|
+
for await (const m of watchMessages(root, {
|
|
186
|
+
signal: controller.signal,
|
|
187
|
+
backfill: opts.filters.sinceMs != null,
|
|
188
|
+
})) {
|
|
189
|
+
const toLabel = labels.get(m.box) ?? m.toLabel;
|
|
190
|
+
if (!matchesFilters(m, toLabel, m.box, opts.filters))
|
|
191
|
+
continue;
|
|
192
|
+
const msg = { ...m, toLabel };
|
|
193
|
+
if (opts.json) {
|
|
194
|
+
console.log(JSON.stringify(msg));
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
const addressedToYou = self != null && m.to === self;
|
|
198
|
+
const to = addressedToYou
|
|
199
|
+
? chalk.yellow(`${GLYPH.ask} you`)
|
|
200
|
+
: chalk.cyan(truncate(toLabel, 24));
|
|
201
|
+
const text = truncate(m.text.replace(/\s+/g, ' ').trim(), 100);
|
|
202
|
+
console.log(`${chalk.dim(clockTime(m.ts))} ${chalk.magenta(truncate(m.from, 24))} ${chalk.dim(GLYPH.stream)} ${to} ${text}`);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
finally {
|
|
206
|
+
process.removeListener('SIGINT', onSigint);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
/** Sender stamp match: `from` is freeform, so match the counterpart's full id, an id prefix, or its resolved label. */
|
|
210
|
+
function senderIsBox(from, box) {
|
|
211
|
+
if (from === box.id)
|
|
212
|
+
return true;
|
|
213
|
+
if (from.length >= 4 && box.id.startsWith(from))
|
|
214
|
+
return true;
|
|
215
|
+
return from.toLowerCase() === box.label.toLowerCase();
|
|
216
|
+
}
|
|
217
|
+
function renderBetween(boxes, a, b, json) {
|
|
218
|
+
const resolveBox = (q) => {
|
|
219
|
+
const found = boxes.find((x) => x.id === q)
|
|
220
|
+
?? boxes.find((x) => x.id.startsWith(q))
|
|
221
|
+
?? boxes.find((x) => x.label.toLowerCase().includes(q.toLowerCase()));
|
|
222
|
+
if (!found)
|
|
223
|
+
die(`No mailbox matching ${JSON.stringify(q)}. Run \`agents mailboxes\` to list boxes.`);
|
|
224
|
+
return found;
|
|
225
|
+
};
|
|
226
|
+
const boxA = resolveBox(a);
|
|
227
|
+
const boxB = resolveBox(b);
|
|
228
|
+
if (boxA.id === boxB.id)
|
|
229
|
+
die('--between needs two different boxes.');
|
|
230
|
+
// Both directions, each stamped with its route so the thread reads chronologically.
|
|
231
|
+
const thread = [
|
|
232
|
+
...boxB.messages.filter((m) => senderIsBox(senderOf(m), boxA)).map((m) => ({
|
|
233
|
+
from: senderOf(m), to: boxB.id, toLabel: boxB.label, ts: m.ts, text: m.text, state: m.state, box: boxB.id,
|
|
234
|
+
})),
|
|
235
|
+
...boxA.messages.filter((m) => senderIsBox(senderOf(m), boxB)).map((m) => ({
|
|
236
|
+
from: senderOf(m), to: boxA.id, toLabel: boxA.label, ts: m.ts, text: m.text, state: m.state, box: boxA.id,
|
|
237
|
+
})),
|
|
238
|
+
].sort((x, y) => (x.ts < y.ts ? -1 : x.ts > y.ts ? 1 : 0));
|
|
239
|
+
if (json) {
|
|
240
|
+
console.log(JSON.stringify({
|
|
241
|
+
a: { id: boxA.id, label: boxA.label },
|
|
242
|
+
b: { id: boxB.id, label: boxB.label },
|
|
243
|
+
count: thread.length,
|
|
244
|
+
messages: thread,
|
|
245
|
+
}, null, 2));
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
const span = thread.length >= 2
|
|
249
|
+
? humanDuration(Math.max(0, Date.parse(thread[thread.length - 1].ts) - Date.parse(thread[0].ts)))
|
|
250
|
+
: '0s';
|
|
251
|
+
console.log(` ${chalk.magenta(boxA.label)} ${chalk.dim(GLYPH.thread)} ${chalk.cyan(boxB.label)}` +
|
|
252
|
+
chalk.dim(` ${thread.length} message${thread.length === 1 ? '' : 's'} · ${span}`));
|
|
253
|
+
if (thread.length === 0) {
|
|
254
|
+
console.log(chalk.dim(' No messages between these boxes yet. Sender stamps match on box id or label.'));
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
console.log();
|
|
258
|
+
for (const m of thread) {
|
|
259
|
+
const when = chalk.dim(relTime(m.ts).padStart(10));
|
|
260
|
+
const route = `${chalk.magenta(truncate(m.from, 24))} ${chalk.dim(GLYPH.route)} ${chalk.cyan(truncate(m.toLabel, 24))}`;
|
|
261
|
+
console.log(` ${when} ${STATE_TAG[m.state]} ${route}`);
|
|
262
|
+
console.log(` ${truncate(m.text.replace(/\s+/g, ' ').trim(), 100)}`);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
function renderGraph(boxes, filters, json) {
|
|
266
|
+
const nonEmpty = boxes.filter((b) => b.messages.length > 0);
|
|
267
|
+
const msgs = aggregate(nonEmpty).filter((m) => matchesFilters(m, m.toLabel, m.box, filters));
|
|
268
|
+
const edges = graphEdges(msgs);
|
|
269
|
+
if (json) {
|
|
270
|
+
console.log(JSON.stringify(edges, null, 2));
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
console.log(masthead({
|
|
274
|
+
title: 'fleet comms',
|
|
275
|
+
host: os.hostname(),
|
|
276
|
+
accent: 'cyan',
|
|
277
|
+
right: `${edges.length} route${edges.length === 1 ? '' : 's'}`,
|
|
278
|
+
stats: [`${msgs.length} messages`],
|
|
279
|
+
}));
|
|
280
|
+
console.log();
|
|
281
|
+
if (edges.length === 0) {
|
|
282
|
+
console.log(chalk.dim(' No cross-box routes yet.'));
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
for (const e of edges) {
|
|
286
|
+
const route = ` ${chalk.magenta(truncate(e.from, 28))} ${chalk.dim('└─▶')} ${chalk.cyan(truncate(e.to, 40))}`;
|
|
287
|
+
const dots = '.'.repeat(Math.max(2, 76 - visibleWidth(route)));
|
|
288
|
+
console.log(`${route} ${chalk.dim(dots)} ${e.count}`);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
function renderBox(box) {
|
|
292
|
+
const dot = box.live ? chalk.green(`${GLYPH.live} live`) : chalk.dim(`${GLYPH.idle} not running`);
|
|
293
|
+
console.log(chalk.bold(box.label) + ' ' + dot);
|
|
294
|
+
console.log(chalk.dim(box.id));
|
|
295
|
+
if (box.messages.length === 0) {
|
|
296
|
+
console.log(chalk.dim(' (empty)'));
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
console.log();
|
|
300
|
+
for (const m of box.messages) {
|
|
301
|
+
const when = chalk.dim(relTime(m.ts));
|
|
302
|
+
console.log(` ${STATE_TAG[m.state]} ${chalk.magenta(senderOf(m))} ${when}${m.blockId ? chalk.dim(` block ${m.blockId.slice(0, 12)}`) : ''}`);
|
|
303
|
+
console.log(` ${truncate(m.text.replace(/\s+/g, ' ').trim(), 200)}`);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
function lastTs(box) {
|
|
307
|
+
if (box.messages.length === 0)
|
|
308
|
+
return 0;
|
|
309
|
+
const t = Date.parse(newestMessage(box).ts);
|
|
310
|
+
return Number.isNaN(t) ? 0 : t;
|
|
311
|
+
}
|
|
312
|
+
function newestMessage(box) {
|
|
313
|
+
return box.messages.reduce((a, b) => (a.ts >= b.ts ? a : b));
|
|
314
|
+
}
|
|
315
|
+
export function registerMailboxesCommand(program) {
|
|
316
|
+
program
|
|
317
|
+
.command('mailboxes')
|
|
318
|
+
.alias('mailbox')
|
|
319
|
+
.argument('[id]', 'A mailbox id (session UUID / teams agentId) to inspect in full')
|
|
320
|
+
.description('Fleet comms — boxes, live cross-box traffic, threads, and routes across the agent mailbox spool')
|
|
321
|
+
.option('--json', 'Output as JSON (NDJSON stream with --watch)')
|
|
322
|
+
.option('-n, --limit <n>', 'Max recent messages in the overview log', '20')
|
|
323
|
+
.option('-f, --watch', 'Live tail: stream new cross-box messages until Ctrl-C')
|
|
324
|
+
.option('--between <boxes...>', 'Thread view: every message between two boxes (id or label), either direction')
|
|
325
|
+
.option('--from <agent>', 'Only messages whose sender contains <agent>')
|
|
326
|
+
.option('--to <agent>', 'Only messages to boxes whose id or label contains <agent>')
|
|
327
|
+
.option('--since <dur>', 'Only messages newer than <dur> (30s/5m/2h/7d or ISO date); with --watch, backfills the window')
|
|
328
|
+
.option('--graph', 'Who-talks-to-whom adjacency, busiest first')
|
|
329
|
+
.action(async (id, opts) => {
|
|
330
|
+
const root = getMailboxRootDir();
|
|
331
|
+
const limit = Math.max(1, Number.parseInt(opts.limit ?? '20', 10) || 20);
|
|
332
|
+
const filters = buildFilters(opts);
|
|
333
|
+
if (opts.between && opts.between.length !== 2) {
|
|
334
|
+
die(`--between takes exactly two boxes: \`agents mailboxes --between <a> <b>\`.`);
|
|
335
|
+
}
|
|
336
|
+
// The views are mutually exclusive — never silently drop a flag.
|
|
337
|
+
const viewCount = (id ? 1 : 0) + (opts.between ? 1 : 0) + (opts.graph ? 1 : 0);
|
|
338
|
+
if (opts.watch && viewCount > 0) {
|
|
339
|
+
die('--watch streams the whole fleet and combines with no other view. Drop <id>/--between/--graph, or use --from/--to/--since to filter the stream.');
|
|
340
|
+
}
|
|
341
|
+
if (!opts.watch && viewCount > 1) {
|
|
342
|
+
die('Pick one view: <id>, --between, or --graph.');
|
|
343
|
+
}
|
|
344
|
+
if (opts.watch) {
|
|
345
|
+
await runWatch(root, { json: opts.json, filters });
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
const boxes = await collectBoxes(root);
|
|
349
|
+
if (opts.between) {
|
|
350
|
+
renderBetween(boxes, opts.between[0], opts.between[1], opts.json);
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
if (opts.graph) {
|
|
354
|
+
renderGraph(boxes, filters, opts.json);
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
if (id) {
|
|
358
|
+
if (!isValidMailboxId(id))
|
|
359
|
+
die(`Invalid mailbox id ${JSON.stringify(id)}.`);
|
|
360
|
+
const box = boxes.find((b) => b.id === id) ?? boxes.find((b) => b.id.startsWith(id));
|
|
361
|
+
if (!box)
|
|
362
|
+
die(`No mailbox ${JSON.stringify(id)} under ${root}.`);
|
|
363
|
+
if (opts.json) {
|
|
364
|
+
console.log(JSON.stringify(box, null, 2));
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
renderBox(box);
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
if (opts.json) {
|
|
371
|
+
// Unfiltered output keeps the legacy shape byte-for-byte; with filters
|
|
372
|
+
// the JSON mirrors the filtered overview log — pending/total recount.
|
|
373
|
+
console.log(JSON.stringify(boxes.map((b) => {
|
|
374
|
+
const messages = hasFilters(filters)
|
|
375
|
+
? b.messages.filter((m) => matchesFilters({ from: senderOf(m), ts: m.ts }, b.label, b.id, filters))
|
|
376
|
+
: b.messages;
|
|
377
|
+
return {
|
|
378
|
+
id: b.id,
|
|
379
|
+
label: b.label,
|
|
380
|
+
live: b.live,
|
|
381
|
+
pending: messages.filter((m) => m.state !== 'consumed').length,
|
|
382
|
+
total: messages.length,
|
|
383
|
+
messages,
|
|
384
|
+
};
|
|
385
|
+
}), null, 2));
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
renderOverview(boxes, limit, filters);
|
|
389
|
+
});
|
|
390
|
+
}
|
|
@@ -149,7 +149,7 @@ async function pickJob(message, filter, alternatives = [], cwd) {
|
|
|
149
149
|
message,
|
|
150
150
|
choices: jobs.map((job) => ({
|
|
151
151
|
value: job.name,
|
|
152
|
-
name: `${job.name} ${chalk.gray(`(${job.workflow ? `wf:${job.workflow}` : job.agent}, ${job.schedule ?? fireConditionLabel(job)})`)}`,
|
|
152
|
+
name: `${job.name} ${chalk.gray(`(${job.command ? 'command' : job.workflow ? `wf:${job.workflow}` : job.agent}, ${job.schedule ?? fireConditionLabel(job)})`)}`,
|
|
153
153
|
})),
|
|
154
154
|
});
|
|
155
155
|
}
|
|
@@ -282,6 +282,7 @@ export function registerRoutinesCommands(program) {
|
|
|
282
282
|
name: job.name,
|
|
283
283
|
agent: job.agent ?? null,
|
|
284
284
|
workflow: job.workflow ?? null,
|
|
285
|
+
command: job.command ?? null,
|
|
285
286
|
repo: job.repo ?? null,
|
|
286
287
|
schedule: job.schedule ?? null,
|
|
287
288
|
scheduleHuman: fireConditionLabel(job),
|
|
@@ -356,9 +357,11 @@ export function registerRoutinesCommands(program) {
|
|
|
356
357
|
: lastStatus === 'timeout' ? chalk.yellow
|
|
357
358
|
: chalk.gray;
|
|
358
359
|
const overdueTag = overdueSet.has(job.name) ? chalk.yellow(' (overdue)') : '';
|
|
359
|
-
const agentLabelPadded = job.
|
|
360
|
-
? chalk.magenta(
|
|
361
|
-
:
|
|
360
|
+
const agentLabelPadded = job.command
|
|
361
|
+
? chalk.magenta('command'.padEnd(10))
|
|
362
|
+
: job.workflow
|
|
363
|
+
? chalk.magenta(`wf:${job.workflow}`.padEnd(10))
|
|
364
|
+
: (job.agent || '').padEnd(10);
|
|
362
365
|
console.log(` ${chalk.cyan(job.name.padEnd(NAME_W))} ${agentLabelPadded} ${repoCell}${' '.repeat(repoPadding)} ${deviceCell}${' '.repeat(devicePad)} ${schedStr.padEnd(SCHED_W)} ${enabledStr}${' '.repeat(enabledPad)} ${chalk.gray(nextStr.padEnd(NEXT_W))} ${statusColor(lastStatus)}${overdueTag}`);
|
|
363
366
|
}
|
|
364
367
|
if (overdueSet.size > 0) {
|
|
@@ -374,6 +377,7 @@ export function registerRoutinesCommands(program) {
|
|
|
374
377
|
.option('-s, --schedule <cron>', 'Cron schedule in standard format (5 fields: minute hour day month weekday)')
|
|
375
378
|
.option('-a, --agent <agent>', 'Which agent runs this routine: claude, codex, gemini, cursor, or opencode')
|
|
376
379
|
.option('--workflow <name>', 'Run an installed workflow (~/.agents/workflows/<name>) via `agents run`. Mutually exclusive with --agent.')
|
|
380
|
+
.option('--command <sh>', 'Run a plain shell command directly (no agent, no auth, no sandbox) — for deterministic housekeeping routines. Mutually exclusive with --agent and --workflow; --prompt is not used.')
|
|
377
381
|
.option('-p, --prompt <prompt>', 'Task instruction for the agent')
|
|
378
382
|
.option('-m, --mode <mode>', "Execution mode: plan (read-only), edit (can write files), auto (smart classifier, the default), or skip (bypass all permission prompts). 'full' accepted as alias for skip.", 'auto')
|
|
379
383
|
.option('-e, --effort <effort>', 'Reasoning effort: low | medium | high | xhigh | max | auto', 'auto')
|
|
@@ -392,7 +396,7 @@ export function registerRoutinesCommands(program) {
|
|
|
392
396
|
.option('--resume <sessionId>', 'At fire time, resume this existing session id (via `agents run <agent> --resume`) instead of starting fresh — the actual session reopens with full context and the prompt becomes its next turn. Powers self-scheduled wake-ups (e.g. /hibernate). Requires --agent claude or codex; runs un-sandboxed (the session store lives in the real home, not the job overlay).')
|
|
393
397
|
.action(async (nameOrPath, options) => {
|
|
394
398
|
// Check if inline mode (has flags) or file mode
|
|
395
|
-
const hasInlineFlags = options.schedule || options.agent || options.workflow || options.prompt || options.at || options.on;
|
|
399
|
+
const hasInlineFlags = options.schedule || options.agent || options.workflow || options.command || options.prompt || options.at || options.on;
|
|
396
400
|
if (hasInlineFlags) {
|
|
397
401
|
// Inline mode: create job from flags
|
|
398
402
|
if (!nameOrPath) {
|
|
@@ -400,9 +404,9 @@ export function registerRoutinesCommands(program) {
|
|
|
400
404
|
console.log(chalk.gray('Usage: agents routines add <name> --schedule "..." --agent <agent> --prompt "..."'));
|
|
401
405
|
process.exit(1);
|
|
402
406
|
}
|
|
403
|
-
// Validate mutually exclusive --agent / --workflow
|
|
404
|
-
if (options.agent
|
|
405
|
-
console.log(chalk.red('--agent and --
|
|
407
|
+
// Validate mutually exclusive --agent / --workflow / --command
|
|
408
|
+
if ([options.agent, options.workflow, options.command].filter(Boolean).length > 1) {
|
|
409
|
+
console.log(chalk.red('--agent, --workflow, and --command are mutually exclusive; specify exactly one'));
|
|
406
410
|
process.exit(1);
|
|
407
411
|
}
|
|
408
412
|
let schedule = options.schedule;
|
|
@@ -430,11 +434,12 @@ export function registerRoutinesCommands(program) {
|
|
|
430
434
|
console.log(chalk.red('Schedule or trigger is required (use --schedule, --at, or --on)'));
|
|
431
435
|
process.exit(1);
|
|
432
436
|
}
|
|
433
|
-
if (!options.agent && !options.workflow) {
|
|
434
|
-
console.log(chalk.red('An agent or
|
|
437
|
+
if (!options.agent && !options.workflow && !options.command) {
|
|
438
|
+
console.log(chalk.red('An agent, workflow, or command is required (use --agent, --workflow, or --command)'));
|
|
435
439
|
process.exit(1);
|
|
436
440
|
}
|
|
437
|
-
|
|
441
|
+
// Command routines run a plain shell and take no prompt; agent/workflow routines require one.
|
|
442
|
+
if (!options.command && !options.prompt) {
|
|
438
443
|
console.log(chalk.red('Prompt is required (use --prompt)'));
|
|
439
444
|
process.exit(1);
|
|
440
445
|
}
|
|
@@ -447,13 +452,14 @@ export function registerRoutinesCommands(program) {
|
|
|
447
452
|
name: nameOrPath,
|
|
448
453
|
...(schedule ? { schedule } : {}),
|
|
449
454
|
...(trigger ? { trigger } : {}),
|
|
450
|
-
agent: options.agent,
|
|
455
|
+
...(options.agent ? { agent: options.agent } : {}),
|
|
451
456
|
...(options.workflow ? { workflow: options.workflow } : {}),
|
|
457
|
+
...(options.command ? { command: options.command } : {}),
|
|
452
458
|
mode: options.mode,
|
|
453
459
|
effort: options.effort,
|
|
454
460
|
timeout: options.timeout,
|
|
455
461
|
enabled: !options.disabled,
|
|
456
|
-
prompt: options.prompt,
|
|
462
|
+
prompt: options.prompt ?? '',
|
|
457
463
|
timezone: options.timezone,
|
|
458
464
|
...(devices ? { devices } : {}),
|
|
459
465
|
...(runOnce ? { runOnce: true } : {}),
|
|
@@ -664,7 +670,7 @@ export function registerRoutinesCommands(program) {
|
|
|
664
670
|
console.log(chalk.gray(` ${eligibility.suggestion}`));
|
|
665
671
|
process.exit(1);
|
|
666
672
|
}
|
|
667
|
-
const runLabel = job.workflow ? `workflow: ${job.workflow}` : `agent: ${job.agent}`;
|
|
673
|
+
const runLabel = job.command ? 'command' : job.workflow ? `workflow: ${job.workflow}` : `agent: ${job.agent}`;
|
|
668
674
|
console.log(chalk.bold(`Running job '${name}' (${runLabel}, mode: ${job.mode})\n`));
|
|
669
675
|
const spinner = ora('Executing...').start();
|
|
670
676
|
try {
|