@phnx-labs/agents-cli 1.20.63 → 1.20.65
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 +56 -3
- package/README.md +46 -2
- package/dist/bin/agents +0 -0
- package/dist/commands/apply.d.ts +12 -0
- package/dist/commands/apply.js +274 -0
- package/dist/commands/browser.js +2 -2
- package/dist/commands/cloud.js +32 -2
- package/dist/commands/doctor.js +4 -1
- package/dist/commands/exec.js +149 -77
- package/dist/commands/feed.d.ts +4 -0
- package/dist/commands/feed.js +52 -19
- package/dist/commands/hosts.js +44 -6
- 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/mcp.js +55 -5
- package/dist/commands/monitors.d.ts +12 -0
- package/dist/commands/monitors.js +740 -0
- package/dist/commands/output.js +2 -2
- package/dist/commands/routines.js +43 -16
- package/dist/commands/secrets.d.ts +16 -0
- package/dist/commands/secrets.js +215 -64
- package/dist/commands/serve.js +31 -0
- package/dist/commands/sessions-export.d.ts +2 -0
- package/dist/commands/sessions-export.js +284 -0
- package/dist/commands/sessions-import.d.ts +2 -0
- package/dist/commands/sessions-import.js +230 -0
- package/dist/commands/sessions.d.ts +16 -0
- package/dist/commands/sessions.js +40 -6
- package/dist/commands/ssh.js +143 -5
- package/dist/commands/usage.d.ts +2 -0
- package/dist/commands/usage.js +7 -2
- package/dist/commands/versions.js +7 -3
- package/dist/commands/view.d.ts +27 -1
- package/dist/commands/view.js +32 -9
- package/dist/commands/webhook.js +10 -2
- package/dist/index.js +35 -14
- package/dist/lib/agents.d.ts +36 -0
- package/dist/lib/agents.js +80 -20
- package/dist/lib/auto-dispatch-provider.js +7 -2
- package/dist/lib/auto-dispatch.d.ts +3 -0
- package/dist/lib/auto-dispatch.js +3 -0
- package/dist/lib/browser/chrome.js +2 -2
- package/dist/lib/browser/drivers/ssh.js +19 -2
- package/dist/lib/cloud/antigravity.js +2 -2
- package/dist/lib/cloud/host.d.ts +59 -0
- package/dist/lib/cloud/host.js +224 -0
- package/dist/lib/cloud/registry.js +4 -0
- package/dist/lib/cloud/types.d.ts +6 -4
- package/dist/lib/comms-render.d.ts +37 -0
- package/dist/lib/comms-render.js +89 -0
- package/dist/lib/computer-rpc.js +3 -1
- package/dist/lib/crabbox/cli.d.ts +72 -0
- package/dist/lib/crabbox/cli.js +162 -9
- package/dist/lib/crabbox/runtimes.d.ts +13 -0
- package/dist/lib/crabbox/runtimes.js +35 -2
- package/dist/lib/daemon.d.ts +20 -4
- package/dist/lib/daemon.js +68 -20
- package/dist/lib/devices/fleet.d.ts +3 -2
- package/dist/lib/devices/fleet.js +9 -0
- package/dist/lib/devices/health.d.ts +77 -0
- package/dist/lib/devices/health.js +186 -0
- package/dist/lib/devices/registry.d.ts +15 -0
- package/dist/lib/devices/registry.js +9 -0
- package/dist/lib/exec.d.ts +19 -2
- package/dist/lib/exec.js +41 -13
- package/dist/lib/fleet/apply.d.ts +63 -0
- package/dist/lib/fleet/apply.js +214 -0
- package/dist/lib/fleet/auth-sync.d.ts +67 -0
- package/dist/lib/fleet/auth-sync.js +142 -0
- package/dist/lib/fleet/manifest.d.ts +29 -0
- package/dist/lib/fleet/manifest.js +127 -0
- package/dist/lib/fleet/types.d.ts +129 -0
- package/dist/lib/fleet/types.js +13 -0
- package/dist/lib/git.d.ts +27 -0
- package/dist/lib/git.js +34 -2
- package/dist/lib/hosts/dispatch.d.ts +29 -8
- package/dist/lib/hosts/dispatch.js +46 -18
- package/dist/lib/hosts/passthrough.js +2 -0
- package/dist/lib/hosts/providers/devices.d.ts +27 -0
- package/dist/lib/hosts/providers/devices.js +98 -0
- package/dist/lib/hosts/registry.d.ts +10 -16
- package/dist/lib/hosts/registry.js +17 -50
- package/dist/lib/hosts/remote-cmd.d.ts +23 -0
- package/dist/lib/hosts/remote-cmd.js +71 -0
- package/dist/lib/hosts/run-target.d.ts +84 -0
- package/dist/lib/hosts/run-target.js +99 -0
- package/dist/lib/hosts/types.d.ts +23 -5
- package/dist/lib/hosts/types.js +22 -4
- package/dist/lib/linear-autoclose.d.ts +30 -0
- package/dist/lib/linear-autoclose.js +22 -0
- package/dist/lib/mailbox.d.ts +39 -0
- package/dist/lib/mailbox.js +112 -0
- package/dist/lib/mcp.d.ts +27 -1
- package/dist/lib/mcp.js +126 -12
- package/dist/lib/monitors/config.d.ts +161 -0
- package/dist/lib/monitors/config.js +372 -0
- package/dist/lib/monitors/dispatch.d.ts +28 -0
- package/dist/lib/monitors/dispatch.js +91 -0
- package/dist/lib/monitors/engine.d.ts +61 -0
- package/dist/lib/monitors/engine.js +201 -0
- package/dist/lib/monitors/sources/command.d.ts +11 -0
- package/dist/lib/monitors/sources/command.js +31 -0
- package/dist/lib/monitors/sources/device.d.ts +13 -0
- package/dist/lib/monitors/sources/device.js +35 -0
- package/dist/lib/monitors/sources/file.d.ts +14 -0
- package/dist/lib/monitors/sources/file.js +57 -0
- package/dist/lib/monitors/sources/http.d.ts +10 -0
- package/dist/lib/monitors/sources/http.js +34 -0
- package/dist/lib/monitors/sources/index.d.ts +14 -0
- package/dist/lib/monitors/sources/index.js +31 -0
- package/dist/lib/monitors/sources/poll.d.ts +9 -0
- package/dist/lib/monitors/sources/poll.js +9 -0
- package/dist/lib/monitors/sources/types.d.ts +18 -0
- package/dist/lib/monitors/sources/types.js +9 -0
- package/dist/lib/monitors/sources/webhook.d.ts +23 -0
- package/dist/lib/monitors/sources/webhook.js +47 -0
- package/dist/lib/monitors/sources/ws.d.ts +14 -0
- package/dist/lib/monitors/sources/ws.js +45 -0
- package/dist/lib/monitors/state.d.ts +69 -0
- package/dist/lib/monitors/state.js +144 -0
- package/dist/lib/paths.d.ts +13 -0
- package/dist/lib/paths.js +26 -4
- package/dist/lib/platform/exec.d.ts +16 -0
- package/dist/lib/platform/exec.js +17 -0
- package/dist/lib/plugins.js +101 -2
- package/dist/lib/redact.d.ts +14 -1
- package/dist/lib/redact.js +47 -1
- package/dist/lib/remote-agents-json.js +7 -1
- package/dist/lib/rotate.d.ts +6 -3
- package/dist/lib/rotate.js +0 -1
- package/dist/lib/routines.d.ts +37 -2
- package/dist/lib/routines.js +54 -12
- package/dist/lib/runner.d.ts +1 -0
- package/dist/lib/runner.js +348 -13
- package/dist/lib/sandbox.d.ts +9 -1
- package/dist/lib/sandbox.js +11 -2
- package/dist/lib/secrets/agent.d.ts +48 -10
- package/dist/lib/secrets/agent.js +123 -15
- package/dist/lib/secrets/bundles.d.ts +26 -0
- package/dist/lib/secrets/bundles.js +59 -8
- package/dist/lib/secrets/mcp.js +4 -2
- package/dist/lib/secrets/remote.d.ts +17 -0
- package/dist/lib/secrets/remote.js +40 -0
- package/dist/lib/self-update.d.ts +20 -0
- package/dist/lib/self-update.js +54 -1
- package/dist/lib/serve/control.d.ts +95 -0
- package/dist/lib/serve/control.js +260 -0
- package/dist/lib/serve/server.d.ts +35 -1
- package/dist/lib/serve/server.js +106 -76
- package/dist/lib/serve/stream.d.ts +43 -0
- package/dist/lib/serve/stream.js +116 -0
- package/dist/lib/serve/token.d.ts +35 -0
- package/dist/lib/serve/token.js +85 -0
- package/dist/lib/session/bundle.d.ts +164 -0
- package/dist/lib/session/bundle.js +200 -0
- package/dist/lib/session/remote-bundle.d.ts +12 -0
- package/dist/lib/session/remote-bundle.js +61 -0
- package/dist/lib/session/remote-list.js +5 -1
- package/dist/lib/session/state.d.ts +7 -25
- package/dist/lib/session/state.js +16 -6
- package/dist/lib/session/sync/agents.d.ts +54 -6
- package/dist/lib/session/sync/agents.js +0 -0
- package/dist/lib/session/sync/config.js +8 -2
- 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/session/types.d.ts +30 -0
- package/dist/lib/ssh-tunnel.d.ts +19 -1
- package/dist/lib/ssh-tunnel.js +99 -8
- 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 +4 -0
- package/dist/lib/startup/command-registry.js +9 -0
- package/dist/lib/state.d.ts +5 -0
- package/dist/lib/state.js +12 -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/tmux/session.d.ts +7 -0
- package/dist/lib/tmux/session.js +3 -1
- package/dist/lib/triggers/webhook.d.ts +18 -0
- package/dist/lib/triggers/webhook.js +105 -0
- package/dist/lib/types.d.ts +35 -1
- package/dist/lib/usage.d.ts +42 -3
- package/dist/lib/usage.js +163 -21
- package/dist/lib/versions.js +14 -11
- package/dist/lib/workflows.d.ts +20 -0
- package/dist/lib/workflows.js +24 -0
- package/package.json +2 -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
|
+
}
|
package/dist/commands/mcp.js
CHANGED
|
@@ -6,9 +6,9 @@ import { capableAgents, isCapable } from '../lib/capabilities.js';
|
|
|
6
6
|
import { emit } from '../lib/events.js';
|
|
7
7
|
import { AGENTS, getAllCliStates, resolveAgentName, formatAgentError, registerMcpToTargets, unregisterMcpFromTargets, listInstalledMcpsWithScope, parseMcpConfig, getMcpConfigPathForHome, agentLabel, } from '../lib/agents.js';
|
|
8
8
|
import { readManifest, writeManifest, createDefaultManifest } from '../lib/manifest.js';
|
|
9
|
-
import { listMcpServerConfigs, discoverMcpConfigsFromRepo, installMcpConfigCentrally, } from '../lib/mcp.js';
|
|
9
|
+
import { listMcpServerConfigs, discoverMcpConfigsFromRepo, installMcpConfigCentrally, isProjectMcpTrusted, trustProjectMcp, untrustProjectMcp, getMcpTrustStorePath, } from '../lib/mcp.js';
|
|
10
10
|
import { cloneRepo } from '../lib/git.js';
|
|
11
|
-
import { getMcpDir } from '../lib/state.js';
|
|
11
|
+
import { getMcpDir, getProjectAgentsDir } from '../lib/state.js';
|
|
12
12
|
import { getEffectiveHome, getGlobalDefault, listInstalledVersions, getVersionHomePath, resolveInstalledAgentTargets, resolveConfiguredAgentTargets, syncResourcesToVersion, } from '../lib/versions.js';
|
|
13
13
|
import { getUserAgentsDir } from '../lib/state.js';
|
|
14
14
|
import { isPromptCancelled, isInteractiveTerminal, requireInteractiveSelection, promptRemovalTargets, parseCommaSeparatedList, ensureAgentVersionsInstalled, resolveAgentTargetsAutoInstalling, resolveInstalledAgentTargetsAutoInstalling, VersionNotInstalledError, resolveListFilterOrExit, } from './utils.js';
|
|
@@ -178,6 +178,43 @@ When to use:
|
|
|
178
178
|
filterVersion,
|
|
179
179
|
});
|
|
180
180
|
});
|
|
181
|
+
mcpCmd
|
|
182
|
+
.command('trust')
|
|
183
|
+
.description('Trust this project so its .agents/mcp/ servers may be registered and spawned')
|
|
184
|
+
.action(() => {
|
|
185
|
+
const projectAgentsDir = getProjectAgentsDir();
|
|
186
|
+
if (!projectAgentsDir) {
|
|
187
|
+
console.log(chalk.red('Not inside a project (no .agents/ directory found) — nothing to trust.'));
|
|
188
|
+
process.exit(1);
|
|
189
|
+
}
|
|
190
|
+
const servers = listMcpServerConfigs().filter((s) => s.scope === 'project');
|
|
191
|
+
if (servers.length > 0) {
|
|
192
|
+
console.log(chalk.bold('Project-scoped MCP servers that will be trusted to run:'));
|
|
193
|
+
for (const s of servers) {
|
|
194
|
+
console.log(' ' + chalk.cyan(s.name) + ' ' + chalk.white(formatCentralCommand(s)));
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
const trusted = trustProjectMcp();
|
|
198
|
+
console.log(chalk.green(`Trusted project MCP servers for: ${trusted}`));
|
|
199
|
+
console.log(chalk.gray(`Recorded in ${getMcpTrustStorePath()} — revoke with: agents mcp untrust`));
|
|
200
|
+
});
|
|
201
|
+
mcpCmd
|
|
202
|
+
.command('untrust')
|
|
203
|
+
.description('Revoke MCP trust for this project (its .agents/mcp/ servers stop auto-applying)')
|
|
204
|
+
.action(() => {
|
|
205
|
+
const projectAgentsDir = getProjectAgentsDir();
|
|
206
|
+
if (!projectAgentsDir) {
|
|
207
|
+
console.log(chalk.red('Not inside a project (no .agents/ directory found).'));
|
|
208
|
+
process.exit(1);
|
|
209
|
+
}
|
|
210
|
+
const removed = untrustProjectMcp();
|
|
211
|
+
if (removed) {
|
|
212
|
+
console.log(chalk.green('Revoked MCP trust for this project.'));
|
|
213
|
+
}
|
|
214
|
+
else {
|
|
215
|
+
console.log(chalk.gray('This project was not trusted — nothing to revoke.'));
|
|
216
|
+
}
|
|
217
|
+
});
|
|
181
218
|
mcpCmd
|
|
182
219
|
.command('add <name> [command_or_url...]')
|
|
183
220
|
.description('Add an MCP server to the manifest (run "agents mcp register" afterward to apply)')
|
|
@@ -797,9 +834,15 @@ function formatCentralCommand(server) {
|
|
|
797
834
|
function formatMcpDetail(name, source, centralConfig, manifestConfig, command, targets) {
|
|
798
835
|
const lines = [];
|
|
799
836
|
lines.push(chalk.bold.cyan(name));
|
|
800
|
-
const
|
|
801
|
-
|
|
802
|
-
|
|
837
|
+
const isUntrustedProject = centralConfig?.scope === 'project'
|
|
838
|
+
&& !isProjectMcpTrusted(getProjectAgentsDir() ?? '');
|
|
839
|
+
const tag = centralConfig?.scope === 'project'
|
|
840
|
+
? (isUntrustedProject
|
|
841
|
+
? chalk.yellow('declared in <repo>/.agents/mcp/ (untrusted project — will NOT auto-apply)')
|
|
842
|
+
: chalk.green('declared in <repo>/.agents/mcp/ (trusted project)')) :
|
|
843
|
+
source === 'central' ? chalk.green('declared in ~/.agents/mcp/') :
|
|
844
|
+
source === 'manifest' ? chalk.gray('declared in agents.yaml') :
|
|
845
|
+
chalk.yellow('unmanaged (not in central or manifest)');
|
|
803
846
|
lines.push(' ' + tag);
|
|
804
847
|
lines.push('');
|
|
805
848
|
if (centralConfig) {
|
|
@@ -814,6 +857,13 @@ function formatMcpDetail(name, source, centralConfig, manifestConfig, command, t
|
|
|
814
857
|
if (command) {
|
|
815
858
|
lines.push(` ${chalk.gray('command:')} ${chalk.white(command)}`);
|
|
816
859
|
}
|
|
860
|
+
// A project-scoped server is an arbitrary command from a (possibly cloned)
|
|
861
|
+
// repo. Show exactly what would run and how to opt in (RUSH-1776).
|
|
862
|
+
if (isUntrustedProject) {
|
|
863
|
+
lines.push('');
|
|
864
|
+
lines.push(' ' + chalk.yellow('This project is untrusted — its MCP servers are skipped on sync and workflow runs.'));
|
|
865
|
+
lines.push(' ' + chalk.gray('Review the command above, then opt in with: ') + chalk.white('agents mcp trust'));
|
|
866
|
+
}
|
|
817
867
|
lines.push('');
|
|
818
868
|
lines.push(chalk.bold(' Synced to'));
|
|
819
869
|
lines.push(buildTargetsSection(targets));
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Monitors — durable event-triggered watchers.
|
|
3
|
+
*
|
|
4
|
+
* Registers the `agents monitors` command tree: create, list, view, dry-run test,
|
|
5
|
+
* edit, logs, fire history, pause/resume, (re)pin the owner device, and remove.
|
|
6
|
+
* Mirrors `agents routines` (commands/routines.ts) and shares the same daemon +
|
|
7
|
+
* dispatch engine underneath — a monitor is a routine whose trigger is a watched
|
|
8
|
+
* source instead of a clock.
|
|
9
|
+
*/
|
|
10
|
+
import type { Command } from 'commander';
|
|
11
|
+
/** Register the `agents monitors` command tree. */
|
|
12
|
+
export declare function registerMonitorsCommands(program: Command): void;
|