@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.
Files changed (197) hide show
  1. package/CHANGELOG.md +56 -3
  2. package/README.md +46 -2
  3. package/dist/bin/agents +0 -0
  4. package/dist/commands/apply.d.ts +12 -0
  5. package/dist/commands/apply.js +274 -0
  6. package/dist/commands/browser.js +2 -2
  7. package/dist/commands/cloud.js +32 -2
  8. package/dist/commands/doctor.js +4 -1
  9. package/dist/commands/exec.js +149 -77
  10. package/dist/commands/feed.d.ts +4 -0
  11. package/dist/commands/feed.js +52 -19
  12. package/dist/commands/hosts.js +44 -6
  13. package/dist/commands/lease.d.ts +23 -0
  14. package/dist/commands/lease.js +201 -0
  15. package/dist/commands/mailboxes.d.ts +20 -0
  16. package/dist/commands/mailboxes.js +390 -0
  17. package/dist/commands/mcp.js +55 -5
  18. package/dist/commands/monitors.d.ts +12 -0
  19. package/dist/commands/monitors.js +740 -0
  20. package/dist/commands/output.js +2 -2
  21. package/dist/commands/routines.js +43 -16
  22. package/dist/commands/secrets.d.ts +16 -0
  23. package/dist/commands/secrets.js +215 -64
  24. package/dist/commands/serve.js +31 -0
  25. package/dist/commands/sessions-export.d.ts +2 -0
  26. package/dist/commands/sessions-export.js +284 -0
  27. package/dist/commands/sessions-import.d.ts +2 -0
  28. package/dist/commands/sessions-import.js +230 -0
  29. package/dist/commands/sessions.d.ts +16 -0
  30. package/dist/commands/sessions.js +40 -6
  31. package/dist/commands/ssh.js +143 -5
  32. package/dist/commands/usage.d.ts +2 -0
  33. package/dist/commands/usage.js +7 -2
  34. package/dist/commands/versions.js +7 -3
  35. package/dist/commands/view.d.ts +27 -1
  36. package/dist/commands/view.js +32 -9
  37. package/dist/commands/webhook.js +10 -2
  38. package/dist/index.js +35 -14
  39. package/dist/lib/agents.d.ts +36 -0
  40. package/dist/lib/agents.js +80 -20
  41. package/dist/lib/auto-dispatch-provider.js +7 -2
  42. package/dist/lib/auto-dispatch.d.ts +3 -0
  43. package/dist/lib/auto-dispatch.js +3 -0
  44. package/dist/lib/browser/chrome.js +2 -2
  45. package/dist/lib/browser/drivers/ssh.js +19 -2
  46. package/dist/lib/cloud/antigravity.js +2 -2
  47. package/dist/lib/cloud/host.d.ts +59 -0
  48. package/dist/lib/cloud/host.js +224 -0
  49. package/dist/lib/cloud/registry.js +4 -0
  50. package/dist/lib/cloud/types.d.ts +6 -4
  51. package/dist/lib/comms-render.d.ts +37 -0
  52. package/dist/lib/comms-render.js +89 -0
  53. package/dist/lib/computer-rpc.js +3 -1
  54. package/dist/lib/crabbox/cli.d.ts +72 -0
  55. package/dist/lib/crabbox/cli.js +162 -9
  56. package/dist/lib/crabbox/runtimes.d.ts +13 -0
  57. package/dist/lib/crabbox/runtimes.js +35 -2
  58. package/dist/lib/daemon.d.ts +20 -4
  59. package/dist/lib/daemon.js +68 -20
  60. package/dist/lib/devices/fleet.d.ts +3 -2
  61. package/dist/lib/devices/fleet.js +9 -0
  62. package/dist/lib/devices/health.d.ts +77 -0
  63. package/dist/lib/devices/health.js +186 -0
  64. package/dist/lib/devices/registry.d.ts +15 -0
  65. package/dist/lib/devices/registry.js +9 -0
  66. package/dist/lib/exec.d.ts +19 -2
  67. package/dist/lib/exec.js +41 -13
  68. package/dist/lib/fleet/apply.d.ts +63 -0
  69. package/dist/lib/fleet/apply.js +214 -0
  70. package/dist/lib/fleet/auth-sync.d.ts +67 -0
  71. package/dist/lib/fleet/auth-sync.js +142 -0
  72. package/dist/lib/fleet/manifest.d.ts +29 -0
  73. package/dist/lib/fleet/manifest.js +127 -0
  74. package/dist/lib/fleet/types.d.ts +129 -0
  75. package/dist/lib/fleet/types.js +13 -0
  76. package/dist/lib/git.d.ts +27 -0
  77. package/dist/lib/git.js +34 -2
  78. package/dist/lib/hosts/dispatch.d.ts +29 -8
  79. package/dist/lib/hosts/dispatch.js +46 -18
  80. package/dist/lib/hosts/passthrough.js +2 -0
  81. package/dist/lib/hosts/providers/devices.d.ts +27 -0
  82. package/dist/lib/hosts/providers/devices.js +98 -0
  83. package/dist/lib/hosts/registry.d.ts +10 -16
  84. package/dist/lib/hosts/registry.js +17 -50
  85. package/dist/lib/hosts/remote-cmd.d.ts +23 -0
  86. package/dist/lib/hosts/remote-cmd.js +71 -0
  87. package/dist/lib/hosts/run-target.d.ts +84 -0
  88. package/dist/lib/hosts/run-target.js +99 -0
  89. package/dist/lib/hosts/types.d.ts +23 -5
  90. package/dist/lib/hosts/types.js +22 -4
  91. package/dist/lib/linear-autoclose.d.ts +30 -0
  92. package/dist/lib/linear-autoclose.js +22 -0
  93. package/dist/lib/mailbox.d.ts +39 -0
  94. package/dist/lib/mailbox.js +112 -0
  95. package/dist/lib/mcp.d.ts +27 -1
  96. package/dist/lib/mcp.js +126 -12
  97. package/dist/lib/monitors/config.d.ts +161 -0
  98. package/dist/lib/monitors/config.js +372 -0
  99. package/dist/lib/monitors/dispatch.d.ts +28 -0
  100. package/dist/lib/monitors/dispatch.js +91 -0
  101. package/dist/lib/monitors/engine.d.ts +61 -0
  102. package/dist/lib/monitors/engine.js +201 -0
  103. package/dist/lib/monitors/sources/command.d.ts +11 -0
  104. package/dist/lib/monitors/sources/command.js +31 -0
  105. package/dist/lib/monitors/sources/device.d.ts +13 -0
  106. package/dist/lib/monitors/sources/device.js +35 -0
  107. package/dist/lib/monitors/sources/file.d.ts +14 -0
  108. package/dist/lib/monitors/sources/file.js +57 -0
  109. package/dist/lib/monitors/sources/http.d.ts +10 -0
  110. package/dist/lib/monitors/sources/http.js +34 -0
  111. package/dist/lib/monitors/sources/index.d.ts +14 -0
  112. package/dist/lib/monitors/sources/index.js +31 -0
  113. package/dist/lib/monitors/sources/poll.d.ts +9 -0
  114. package/dist/lib/monitors/sources/poll.js +9 -0
  115. package/dist/lib/monitors/sources/types.d.ts +18 -0
  116. package/dist/lib/monitors/sources/types.js +9 -0
  117. package/dist/lib/monitors/sources/webhook.d.ts +23 -0
  118. package/dist/lib/monitors/sources/webhook.js +47 -0
  119. package/dist/lib/monitors/sources/ws.d.ts +14 -0
  120. package/dist/lib/monitors/sources/ws.js +45 -0
  121. package/dist/lib/monitors/state.d.ts +69 -0
  122. package/dist/lib/monitors/state.js +144 -0
  123. package/dist/lib/paths.d.ts +13 -0
  124. package/dist/lib/paths.js +26 -4
  125. package/dist/lib/platform/exec.d.ts +16 -0
  126. package/dist/lib/platform/exec.js +17 -0
  127. package/dist/lib/plugins.js +101 -2
  128. package/dist/lib/redact.d.ts +14 -1
  129. package/dist/lib/redact.js +47 -1
  130. package/dist/lib/remote-agents-json.js +7 -1
  131. package/dist/lib/rotate.d.ts +6 -3
  132. package/dist/lib/rotate.js +0 -1
  133. package/dist/lib/routines.d.ts +37 -2
  134. package/dist/lib/routines.js +54 -12
  135. package/dist/lib/runner.d.ts +1 -0
  136. package/dist/lib/runner.js +348 -13
  137. package/dist/lib/sandbox.d.ts +9 -1
  138. package/dist/lib/sandbox.js +11 -2
  139. package/dist/lib/secrets/agent.d.ts +48 -10
  140. package/dist/lib/secrets/agent.js +123 -15
  141. package/dist/lib/secrets/bundles.d.ts +26 -0
  142. package/dist/lib/secrets/bundles.js +59 -8
  143. package/dist/lib/secrets/mcp.js +4 -2
  144. package/dist/lib/secrets/remote.d.ts +17 -0
  145. package/dist/lib/secrets/remote.js +40 -0
  146. package/dist/lib/self-update.d.ts +20 -0
  147. package/dist/lib/self-update.js +54 -1
  148. package/dist/lib/serve/control.d.ts +95 -0
  149. package/dist/lib/serve/control.js +260 -0
  150. package/dist/lib/serve/server.d.ts +35 -1
  151. package/dist/lib/serve/server.js +106 -76
  152. package/dist/lib/serve/stream.d.ts +43 -0
  153. package/dist/lib/serve/stream.js +116 -0
  154. package/dist/lib/serve/token.d.ts +35 -0
  155. package/dist/lib/serve/token.js +85 -0
  156. package/dist/lib/session/bundle.d.ts +164 -0
  157. package/dist/lib/session/bundle.js +200 -0
  158. package/dist/lib/session/remote-bundle.d.ts +12 -0
  159. package/dist/lib/session/remote-bundle.js +61 -0
  160. package/dist/lib/session/remote-list.js +5 -1
  161. package/dist/lib/session/state.d.ts +7 -25
  162. package/dist/lib/session/state.js +16 -6
  163. package/dist/lib/session/sync/agents.d.ts +54 -6
  164. package/dist/lib/session/sync/agents.js +0 -0
  165. package/dist/lib/session/sync/config.js +8 -2
  166. package/dist/lib/session/sync/manifest.d.ts +14 -3
  167. package/dist/lib/session/sync/manifest.js +4 -0
  168. package/dist/lib/session/sync/sync.d.ts +23 -2
  169. package/dist/lib/session/sync/sync.js +177 -74
  170. package/dist/lib/session/types.d.ts +30 -0
  171. package/dist/lib/ssh-tunnel.d.ts +19 -1
  172. package/dist/lib/ssh-tunnel.js +99 -8
  173. package/dist/lib/staleness/detectors/subagents.d.ts +5 -0
  174. package/dist/lib/staleness/detectors/subagents.js +5 -192
  175. package/dist/lib/staleness/writers/subagents.d.ts +10 -0
  176. package/dist/lib/staleness/writers/subagents.js +11 -102
  177. package/dist/lib/startup/command-registry.d.ts +4 -0
  178. package/dist/lib/startup/command-registry.js +9 -0
  179. package/dist/lib/state.d.ts +5 -0
  180. package/dist/lib/state.js +12 -0
  181. package/dist/lib/subagents-registry.d.ts +85 -0
  182. package/dist/lib/subagents-registry.js +393 -0
  183. package/dist/lib/subagents.d.ts +8 -8
  184. package/dist/lib/subagents.js +32 -663
  185. package/dist/lib/sync-umbrella.d.ts +1 -0
  186. package/dist/lib/sync-umbrella.js +14 -3
  187. package/dist/lib/tmux/session.d.ts +7 -0
  188. package/dist/lib/tmux/session.js +3 -1
  189. package/dist/lib/triggers/webhook.d.ts +18 -0
  190. package/dist/lib/triggers/webhook.js +105 -0
  191. package/dist/lib/types.d.ts +35 -1
  192. package/dist/lib/usage.d.ts +42 -3
  193. package/dist/lib/usage.js +163 -21
  194. package/dist/lib/versions.js +14 -11
  195. package/dist/lib/workflows.d.ts +20 -0
  196. package/dist/lib/workflows.js +24 -0
  197. 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
+ }
@@ -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 tag = source === 'central' ? chalk.green('declared in ~/.agents/mcp/') :
801
- source === 'manifest' ? chalk.gray('declared in agents.yaml') :
802
- chalk.yellow('unmanaged (not in central or manifest)');
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;