@phnx-labs/agents-cli 1.22.43 → 1.22.44

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 (164) hide show
  1. package/CHANGELOG.md +77 -0
  2. package/README.md +14 -12
  3. package/dist/bootstrap.js +8 -6
  4. package/dist/cli/command-registry.d.ts +0 -5
  5. package/dist/cli/command-registry.js +1 -17
  6. package/dist/commands/apply.d.ts +7 -10
  7. package/dist/commands/apply.js +15 -35
  8. package/dist/commands/artifacts.js +4 -5
  9. package/dist/commands/audit.d.ts +6 -6
  10. package/dist/commands/audit.js +12 -12
  11. package/dist/commands/auth.js +7 -3
  12. package/dist/commands/beta.d.ts +7 -1
  13. package/dist/commands/beta.js +16 -9
  14. package/dist/commands/commands.js +1 -1
  15. package/dist/commands/doctor.js +1 -4
  16. package/dist/commands/events.d.ts +2 -2
  17. package/dist/commands/events.js +6 -5
  18. package/dist/commands/exec.d.ts +1 -1
  19. package/dist/commands/exec.js +8 -4
  20. package/dist/commands/factory.d.ts +1 -1
  21. package/dist/commands/fleet-capture.d.ts +1 -1
  22. package/dist/commands/fleet-capture.js +4 -5
  23. package/dist/commands/harness.js +1 -49
  24. package/dist/commands/hooks.js +1 -1
  25. package/dist/commands/insights.d.ts +1 -1
  26. package/dist/commands/insights.js +3 -2
  27. package/dist/commands/menubar.js +58 -1
  28. package/dist/commands/org.d.ts +5 -0
  29. package/dist/commands/org.js +82 -29
  30. package/dist/commands/output.js +2 -2
  31. package/dist/commands/prune.js +5 -4
  32. package/dist/commands/routines.test-fixture.d.ts +86 -0
  33. package/dist/commands/routines.test-fixture.js +345 -0
  34. package/dist/commands/sessions.test-fixture.d.ts +24 -0
  35. package/dist/commands/sessions.test-fixture.js +224 -0
  36. package/dist/commands/setup.js +5 -1
  37. package/dist/commands/share.d.ts +9 -10
  38. package/dist/commands/share.js +68 -50
  39. package/dist/commands/skills.js +1 -1
  40. package/dist/commands/ssh.js +15 -62
  41. package/dist/commands/subagents.js +1 -1
  42. package/dist/commands/view.js +5 -2
  43. package/dist/lib/analytics/mix-commands.d.ts +6 -28
  44. package/dist/lib/analytics/mix-commands.js +14 -40
  45. package/dist/lib/audit/log.d.ts +2 -2
  46. package/dist/lib/audit/log.js +2 -2
  47. package/dist/lib/beta.js +1 -1
  48. package/dist/lib/browser/domain-skills.d.ts +26 -6
  49. package/dist/lib/browser/domain-skills.js +81 -43
  50. package/dist/lib/daemon/daemon.js +11 -0
  51. package/dist/lib/daemon/daemon.test-fixture.d.ts +24 -0
  52. package/dist/lib/daemon/daemon.test-fixture.js +71 -0
  53. package/dist/lib/daemon-ticks.d.ts +38 -1
  54. package/dist/lib/daemon-ticks.js +52 -5
  55. package/dist/lib/device-config.js +1 -2
  56. package/dist/lib/devices/discovery-policy.d.ts +3 -3
  57. package/dist/lib/devices/discovery-policy.js +3 -3
  58. package/dist/lib/devices/fleet.d.ts +4 -8
  59. package/dist/lib/devices/fleet.js +3 -15
  60. package/dist/lib/devices/health-report.js +3 -3
  61. package/dist/lib/devices/pool.d.ts +0 -6
  62. package/dist/lib/devices/pool.js +0 -6
  63. package/dist/lib/devices/registry.d.ts +0 -15
  64. package/dist/lib/devices/registry.js +0 -9
  65. package/dist/lib/devices/stats-cache.d.ts +12 -0
  66. package/dist/lib/devices/stats-cache.js +24 -5
  67. package/dist/lib/fleet/capture.d.ts +1 -1
  68. package/dist/lib/fleet/manifest.js +1 -1
  69. package/dist/lib/fleet/remote-login.d.ts +2 -2
  70. package/dist/lib/fleet/remote-login.js +3 -4
  71. package/dist/lib/hosts/dispatch.d.ts +2 -0
  72. package/dist/lib/hosts/dispatch.js +8 -1
  73. package/dist/lib/hosts/passthrough.js +0 -1
  74. package/dist/lib/hosts/providers/devices.js +1 -13
  75. package/dist/lib/hosts/ready.d.ts +12 -3
  76. package/dist/lib/hosts/ready.js +27 -12
  77. package/dist/lib/hosts/registry.d.ts +4 -5
  78. package/dist/lib/hosts/registry.js +3 -7
  79. package/dist/lib/installations/versions.js +21 -2
  80. package/dist/lib/menubar/MenubarHelper.app/Contents/Info.plist +1 -5
  81. package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
  82. package/dist/lib/menubar/MenubarHelper.app/Contents/_CodeSignature/CodeResources +2 -15
  83. package/dist/lib/menubar/install-menubar.d.ts +109 -0
  84. package/dist/lib/menubar/install-menubar.js +199 -4
  85. package/dist/lib/prix-account.d.ts +2 -1
  86. package/dist/lib/prix-account.js +2 -1
  87. package/dist/lib/remote-agents-json.js +1 -7
  88. package/dist/lib/routines-placement.d.ts +1 -1
  89. package/dist/lib/routines-placement.js +1 -1
  90. package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
  91. package/dist/lib/secrets/Agents CLI.app/Contents/Info.plist +0 -2
  92. package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
  93. package/dist/lib/secrets/Agents CLI.app/Contents/_CodeSignature/CodeResources +1 -13
  94. package/dist/lib/session/active.d.ts +4 -16
  95. package/dist/lib/session/active.js +2 -6
  96. package/dist/lib/session/db.d.ts +13 -98
  97. package/dist/lib/session/db.js +4 -11
  98. package/dist/lib/session/discover.d.ts +5 -81
  99. package/dist/lib/session/discover.js +5 -5
  100. package/dist/lib/session/remote/remote-bundle.d.ts +12 -0
  101. package/dist/lib/session/remote/remote-bundle.js +61 -0
  102. package/dist/lib/session/remote/remote-list.d.ts +148 -0
  103. package/dist/lib/session/remote/remote-list.js +607 -0
  104. package/dist/lib/session/remote/remote.d.ts +132 -0
  105. package/dist/lib/session/remote/remote.js +314 -0
  106. package/dist/lib/session/remote/watch.d.ts +101 -0
  107. package/dist/lib/session/remote/watch.js +241 -0
  108. package/dist/lib/session/remote-bundle.d.ts +1 -12
  109. package/dist/lib/session/remote-bundle.js +3 -61
  110. package/dist/lib/session/remote-list.d.ts +1 -148
  111. package/dist/lib/session/remote-list.js +3 -607
  112. package/dist/lib/session/remote.d.ts +1 -132
  113. package/dist/lib/session/remote.js +3 -314
  114. package/dist/lib/session/watch.d.ts +1 -101
  115. package/dist/lib/session/watch.js +3 -242
  116. package/dist/lib/share/config.d.ts +12 -2
  117. package/dist/lib/share/config.js +47 -11
  118. package/dist/lib/share/delete.js +1 -1
  119. package/dist/lib/share/publish.js +1 -1
  120. package/dist/lib/share/worker-template.js +2 -2
  121. package/dist/lib/smart-launch.d.ts +4 -4
  122. package/dist/lib/smart-launch.js +8 -18
  123. package/dist/lib/staleness/index.d.ts +3 -1
  124. package/dist/lib/staleness/index.js +21 -1
  125. package/dist/lib/staleness/types.d.ts +11 -0
  126. package/dist/lib/staleness/writers/commands.js +13 -4
  127. package/dist/lib/staleness/writers/hooks.js +3 -1
  128. package/dist/lib/staleness/writers/mcp.d.ts +0 -7
  129. package/dist/lib/staleness/writers/mcp.js +16 -1
  130. package/dist/lib/staleness/writers/rules.js +1 -1
  131. package/dist/lib/staleness/writers/skills.js +3 -1
  132. package/dist/lib/staleness/writers/subagents.d.ts +0 -9
  133. package/dist/lib/staleness/writers/subagents.js +18 -1
  134. package/dist/lib/staleness/writers/types.d.ts +10 -0
  135. package/dist/lib/startup/command-registry.d.ts +7 -1
  136. package/dist/lib/startup/command-registry.js +17 -5
  137. package/dist/lib/startup/root-command.d.ts +18 -1
  138. package/dist/lib/startup/root-command.js +18 -1
  139. package/dist/lib/state.js +7 -0
  140. package/dist/lib/teams/placement-probe.js +27 -12
  141. package/dist/lib/teams/scheduler.d.ts +3 -1
  142. package/dist/lib/types.d.ts +1 -1
  143. package/dist/lib/view-types.d.ts +3 -0
  144. package/package.json +2 -3
  145. package/dist/bin/agents +0 -0
  146. package/dist/commands/serve.d.ts +0 -10
  147. package/dist/commands/serve.js +0 -68
  148. package/dist/commands/trends.d.ts +0 -10
  149. package/dist/commands/trends.js +0 -12
  150. package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
  151. package/dist/lib/menubar/MenubarHelper.app/Contents/Resources/AppIcon.icns +0 -0
  152. package/dist/lib/secrets/Agents CLI.app/Contents/Resources/AppIcon.icns +0 -0
  153. package/dist/lib/serve/control.d.ts +0 -95
  154. package/dist/lib/serve/control.js +0 -260
  155. package/dist/lib/serve/data.d.ts +0 -81
  156. package/dist/lib/serve/data.js +0 -91
  157. package/dist/lib/serve/page.d.ts +0 -7
  158. package/dist/lib/serve/page.js +0 -140
  159. package/dist/lib/serve/server.d.ts +0 -80
  160. package/dist/lib/serve/server.js +0 -145
  161. package/dist/lib/serve/stream.d.ts +0 -43
  162. package/dist/lib/serve/stream.js +0 -116
  163. package/dist/lib/serve/token.d.ts +0 -35
  164. package/dist/lib/serve/token.js +0 -85
@@ -1,260 +0,0 @@
1
- import { createServer } from 'http';
2
- import fs from 'fs';
3
- import { spawn } from 'child_process';
4
- import { randomUUID, randomBytes } from 'crypto';
5
- import { getAgentsInvocation } from '../daemon/daemon.js';
6
- import { handleServeGet, resolveServeContext } from './server.js';
7
- import { verifyControlToken } from './token.js';
8
- import { readNewEvents, streamDir, streamLogPath } from './stream.js';
9
- const MAX_BODY_BYTES = 64 * 1024;
10
- const DEFAULT_STREAM_POLL_MS = 300;
11
- /** Pull the presented bearer token from either header form. */
12
- function presentedToken(req) {
13
- const auth = req.headers['authorization'];
14
- if (typeof auth === 'string' && auth.toLowerCase().startsWith('bearer ')) {
15
- return auth.slice(7).trim();
16
- }
17
- const x = req.headers['x-agents-token'];
18
- return typeof x === 'string' ? x.trim() : undefined;
19
- }
20
- /** Read a JSON request body with a hard size cap. Rejects on overflow/parse error. */
21
- function readJsonBody(req) {
22
- return new Promise((resolve, reject) => {
23
- let size = 0;
24
- const chunks = [];
25
- req.on('data', (c) => {
26
- size += c.length;
27
- if (size > MAX_BODY_BYTES) {
28
- reject(new Error('request body too large'));
29
- req.destroy();
30
- return;
31
- }
32
- chunks.push(c);
33
- });
34
- req.on('end', () => {
35
- const raw = Buffer.concat(chunks).toString('utf-8').trim();
36
- if (!raw)
37
- return resolve({});
38
- try {
39
- resolve(JSON.parse(raw));
40
- }
41
- catch {
42
- reject(new Error('invalid JSON body'));
43
- }
44
- });
45
- req.on('error', reject);
46
- });
47
- }
48
- function sendJson(res, code, obj) {
49
- res.writeHead(code, { 'content-type': 'application/json; charset=utf-8' });
50
- res.end(JSON.stringify(obj));
51
- }
52
- /**
53
- * SSE stream of a session's normalized NDJSON events, offset-tailed from its
54
- * capture file. Resumes from `?offset=<bytes>` or the `Last-Event-ID` header
55
- * (each event's `id:` is its exact byte offset, so resume neither loses nor
56
- * duplicates). Closes when a terminal (`result`/`error`) event is seen or the
57
- * client disconnects. A not-yet-created file simply yields nothing until the
58
- * run starts writing — the phone can open the stream the instant it dispatches.
59
- */
60
- export function startSessionStream(file, req, res, pollMs) {
61
- res.writeHead(200, {
62
- 'content-type': 'text/event-stream',
63
- 'cache-control': 'no-cache',
64
- connection: 'keep-alive',
65
- });
66
- const params = new URL(req.url ?? '/', 'http://anchor').searchParams;
67
- const qOffset = Number.parseInt(params.get('offset') ?? '', 10);
68
- const lastId = Number.parseInt(Array.isArray(req.headers['last-event-id'])
69
- ? req.headers['last-event-id'][0]
70
- : req.headers['last-event-id'] ?? '', 10);
71
- // Clamp at the source: a negative/garbage offset (or Last-Event-ID) is
72
- // meaningless and, worse, `Buffer.subarray(neg)` indexes from the END — so an
73
- // unclamped negative offset would read nothing and never self-correct. A bad
74
- // resume value replays from the start, the EventSource-friendly behavior.
75
- const wanted = Number.isFinite(qOffset) ? qOffset : Number.isFinite(lastId) ? lastId : 0;
76
- let offset = Math.max(0, wanted);
77
- let closed = false;
78
- let timer;
79
- const stop = () => {
80
- closed = true;
81
- if (timer)
82
- clearInterval(timer);
83
- };
84
- req.on('close', stop);
85
- const tick = () => {
86
- if (closed)
87
- return;
88
- const { events, newOffset, done } = readNewEvents(file, offset);
89
- for (const { event, offset: at } of events) {
90
- // `at` is the per-event resume point the CLIENT echoes as Last-Event-ID.
91
- res.write(`id: ${at}\nevent: ${event.type}\ndata: ${JSON.stringify(event.raw)}\n\n`);
92
- }
93
- // The SERVER cursor always adopts newOffset — past every complete line,
94
- // including trailing blank/non-JSON lines after the last event — so the
95
- // stream always makes progress (even on an empty read) and never re-scans.
96
- offset = newOffset;
97
- if (done && !closed) {
98
- res.write('event: end\ndata: {"ok":true}\n\n');
99
- stop();
100
- res.end();
101
- }
102
- };
103
- tick(); // immediate catch-up from the resume offset
104
- if (!closed)
105
- timer = setInterval(tick, pollMs);
106
- }
107
- /**
108
- * Spawn a detached child and settle once it has either successfully spawned or
109
- * failed to. Attaching an `'error'` listener is mandatory: a `ChildProcess` is
110
- * an `EventEmitter`, so an `'error'` event (e.g. ENOENT from a stale/missing
111
- * `agents` binary — a real condition `validateDaemonBinary` guards against)
112
- * with no listener throws and would take down the whole anchor process, not
113
- * just the one request. Mirrors the `once('spawn') / once('error')` pattern in
114
- * `teams/agents.ts`. The `'error'` listener registered by `once` survives after
115
- * a successful spawn, so a later error is absorbed rather than crashing.
116
- */
117
- export function spawnDetached(command, args, stdio = 'ignore') {
118
- return new Promise((resolve, reject) => {
119
- const child = spawn(command, args, { detached: true, stdio, env: process.env });
120
- child.once('spawn', () => {
121
- child.unref();
122
- resolve();
123
- });
124
- child.once('error', (err) => reject(err instanceof Error ? err : new Error(String(err))));
125
- });
126
- }
127
- /**
128
- * Default run dispatcher: re-invoke `agents run` with a server-minted id so the
129
- * run is addressable, detached so it outlives this request (and the anchor
130
- * process). Local runs detach here; `--device` runs detach on the executor via
131
- * the existing dispatch path.
132
- */
133
- export const defaultRunner = async (req) => {
134
- if (!req.agent?.trim())
135
- throw new Error('agent is required');
136
- if (!req.prompt?.trim())
137
- throw new Error('prompt is required');
138
- const name = `ios-${randomBytes(3).toString('hex')}`;
139
- // claude accepts an explicit session UUID; other agents resolve by --name.
140
- const sessionId = req.agent === 'claude' ? randomUUID() : name;
141
- const argv = ['run', req.agent, req.prompt, '--json', '--headless', '--quiet', '--name', name];
142
- if (req.agent === 'claude')
143
- argv.push('--session-id', sessionId);
144
- if (req.mode)
145
- argv.push('--mode', req.mode);
146
- if (req.host)
147
- argv.push('--device', req.host);
148
- if (req.cwd)
149
- argv.push('--cwd', req.cwd);
150
- const inv = getAgentsInvocation(argv);
151
- // Await spawn/error so a failed launch becomes a clean 400, never an
152
- // unhandled 'error' that crashes the anchor for every other session.
153
- //
154
- // For an anchor-local run, capture the `--json` NDJSON to a per-session file
155
- // so `GET /api/session/:id/stream` can offset-tail it. A `--device` run emits
156
- // its NDJSON on the remote box (streaming that reuses pullRemoteLogDelta — a
157
- // follow-up), so we don't capture the local dispatcher's output.
158
- if (req.host) {
159
- await spawnDetached(inv.command, inv.args);
160
- }
161
- else {
162
- fs.mkdirSync(streamDir(), { recursive: true });
163
- const fd = fs.openSync(streamLogPath(sessionId), 'a');
164
- try {
165
- await spawnDetached(inv.command, inv.args, ['ignore', fd, fd]);
166
- }
167
- finally {
168
- // The child inherited the fd; the parent's copy is no longer needed.
169
- fs.closeSync(fd);
170
- }
171
- }
172
- return { sessionId, name };
173
- };
174
- /** Default messenger: re-invoke `agents message <id> <text>` and await its exit. */
175
- export const defaultMessenger = async (id, text, from) => {
176
- const argv = ['message', id, text];
177
- if (from)
178
- argv.push('--from', from);
179
- const inv = getAgentsInvocation(argv);
180
- const code = await new Promise((resolve) => {
181
- const child = spawn(inv.command, inv.args, { stdio: 'ignore', env: process.env });
182
- child.on('exit', (c) => resolve(c ?? 1));
183
- child.on('error', () => resolve(1));
184
- });
185
- if (code !== 0)
186
- throw new Error(`agents message exited with code ${code}`);
187
- };
188
- /**
189
- * Create (but do not start) the authenticated control server. Caller invokes
190
- * `.listen(port, bind)`.
191
- */
192
- export function createControlServer(opts = {}) {
193
- const ctx = resolveServeContext(opts);
194
- const verify = opts.verifyToken ?? verifyControlToken;
195
- const runner = opts.runner ?? defaultRunner;
196
- const messenger = opts.messenger ?? defaultMessenger;
197
- const streamPollMs = opts.streamPollMs ?? DEFAULT_STREAM_POLL_MS;
198
- const streamLogPathFor = opts.streamLogPathFor ?? streamLogPath;
199
- return createServer(async (req, res) => {
200
- // Auth gates EVERY request — this server may be reachable off-box.
201
- if (!verify(presentedToken(req))) {
202
- res.writeHead(401, { 'content-type': 'application/json', 'www-authenticate': 'Bearer' });
203
- res.end(JSON.stringify({ error: 'unauthorized' }));
204
- return;
205
- }
206
- const method = req.method ?? 'GET';
207
- const url = (req.url || '/').split('?')[0];
208
- if (method === 'GET') {
209
- const streamMatch = /^\/api\/session\/([^/]+)\/stream$/.exec(url);
210
- if (streamMatch) {
211
- const id = decodeURIComponent(streamMatch[1]);
212
- startSessionStream(streamLogPathFor(id), req, res, streamPollMs);
213
- return;
214
- }
215
- const handled = await handleServeGet(url, req, res, ctx);
216
- if (!handled)
217
- sendJson(res, 404, { error: 'not found' });
218
- return;
219
- }
220
- if (method === 'POST' && url === '/api/run') {
221
- try {
222
- const body = (await readJsonBody(req));
223
- const result = await runner(body);
224
- sendJson(res, 200, result);
225
- }
226
- catch (err) {
227
- sendJson(res, 400, { error: String(err?.message ?? err) });
228
- }
229
- return;
230
- }
231
- const msgMatch = method === 'POST' && /^\/api\/session\/([^/]+)\/message$/.exec(url);
232
- if (msgMatch) {
233
- const id = decodeURIComponent(msgMatch[1]);
234
- try {
235
- const body = (await readJsonBody(req));
236
- if (!body.text?.trim())
237
- throw new Error('text is required');
238
- await messenger(id, body.text, body.from);
239
- sendJson(res, 200, { ok: true, id });
240
- }
241
- catch (err) {
242
- sendJson(res, 400, { error: String(err?.message ?? err) });
243
- }
244
- return;
245
- }
246
- sendJson(res, 404, { error: 'not found' });
247
- });
248
- }
249
- /** Start the control server on `port`, bound to `bind`. Resolves the bound port. */
250
- export function startControlServer(port, bind, opts = {}) {
251
- const server = createControlServer(opts);
252
- return new Promise((resolve, reject) => {
253
- server.once('error', reject);
254
- server.listen(port, bind, () => {
255
- const addr = server.address();
256
- const boundPort = typeof addr === 'object' && addr ? addr.port : port;
257
- resolve({ server, port: boundPort });
258
- });
259
- });
260
- }
@@ -1,81 +0,0 @@
1
- /**
2
- * Read-only state assembly for `agents serve`.
3
- *
4
- * Reuses the SAME structured data the CLI already emits — no recomputation:
5
- * - teams: {@link handleTasks} + {@link handleStatus} + {@link toTaskStatusSummary}
6
- * (the shape behind `agents teams status --json`), plus a per-worktree
7
- * `git diff` via {@link gitDiff}.
8
- * - routines: {@link listJobs} (the shape behind `agents routines list --json`).
9
- * - cloud: {@link listTasks} from the local SQLite store (behind `agents cloud list --json`).
10
- *
11
- * Every panel is assembled independently: if one subsystem throws (e.g. the
12
- * SQLite cloud store isn't provisioned on this box), that panel reports an
13
- * `error` string and the others still render. This is an aggregator dashboard,
14
- * not a fallback that hides a bug in a single code path.
15
- */
16
- import { AgentManager } from '../teams/agents.js';
17
- import type { AgentStatusSummary } from '../teams/api.js';
18
- import type { JobConfig } from '../scheduling/routines.js';
19
- import type { CloudTask } from '../cloud/types.js';
20
- /** One teammate's worktree and its current uncommitted diff. */
21
- export interface WorktreeDiff {
22
- agent_id: string;
23
- name: string | null;
24
- agent_type: string;
25
- status: string;
26
- worktree_name: string | null;
27
- worktree_path: string | null;
28
- pr_url: string | null;
29
- /** Uncommitted `git diff HEAD` for the worktree, capped. '' when clean/absent. */
30
- diff: string;
31
- }
32
- /** One team: its per-teammate status summary plus per-worktree diffs. */
33
- export interface TeamPanel {
34
- task_name: string;
35
- agent_count: number;
36
- running: number;
37
- completed: number;
38
- failed: number;
39
- agents: AgentStatusSummary[];
40
- worktrees: WorktreeDiff[];
41
- }
42
- /** A panel that either carries data or an error string (never both). */
43
- export type PanelResult<T> = {
44
- ok: true;
45
- data: T;
46
- } | {
47
- ok: false;
48
- error: string;
49
- };
50
- /** The full read-only snapshot streamed to the browser. */
51
- export interface ServeState {
52
- generated_at: string;
53
- teams: PanelResult<TeamPanel[]>;
54
- routines: PanelResult<JobConfig[]>;
55
- cloud: PanelResult<CloudTask[]>;
56
- }
57
- /** Shape of the teammate fields needed to build a worktree diff. */
58
- export interface WorktreeAgentLike {
59
- agentId: string;
60
- name: string | null;
61
- agentType: string;
62
- status: string;
63
- worktreeName: string | null;
64
- worktreePath: string | null;
65
- prUrl: string | null;
66
- }
67
- /**
68
- * Build the per-worktree diffs for a set of teammates. Only teammates with a
69
- * `worktreePath` produce a diff; each gets a real `git diff HEAD` via
70
- * {@link gitDiff}. Pure over its inputs (no manager, no globals) so it can be
71
- * unit-tested against a real temp git worktree.
72
- */
73
- export declare function buildWorktreeDiffs(agents: WorktreeAgentLike[]): Promise<WorktreeDiff[]>;
74
- /**
75
- * Assemble the full read-only snapshot. Each panel degrades independently to an
76
- * `error` string so one unavailable subsystem never blanks the whole dashboard.
77
- *
78
- * @param cwd - Project root used for project-scoped routine discovery.
79
- * @param manager - Injectable for tests; defaults to a fresh {@link AgentManager}.
80
- */
81
- export declare function assembleState(cwd?: string, manager?: AgentManager): Promise<ServeState>;
@@ -1,91 +0,0 @@
1
- /**
2
- * Read-only state assembly for `agents serve`.
3
- *
4
- * Reuses the SAME structured data the CLI already emits — no recomputation:
5
- * - teams: {@link handleTasks} + {@link handleStatus} + {@link toTaskStatusSummary}
6
- * (the shape behind `agents teams status --json`), plus a per-worktree
7
- * `git diff` via {@link gitDiff}.
8
- * - routines: {@link listJobs} (the shape behind `agents routines list --json`).
9
- * - cloud: {@link listTasks} from the local SQLite store (behind `agents cloud list --json`).
10
- *
11
- * Every panel is assembled independently: if one subsystem throws (e.g. the
12
- * SQLite cloud store isn't provisioned on this box), that panel reports an
13
- * `error` string and the others still render. This is an aggregator dashboard,
14
- * not a fallback that hides a bug in a single code path.
15
- */
16
- import { AgentManager } from '../teams/agents.js';
17
- import { handleTasks, handleStatus, toTaskStatusSummary } from '../teams/api.js';
18
- import { gitDiff } from '../teams/worktree.js';
19
- import { listJobs } from '../scheduling/routines.js';
20
- import { listTasks as listCloudTasks } from '../cloud/store.js';
21
- /**
22
- * Build the per-worktree diffs for a set of teammates. Only teammates with a
23
- * `worktreePath` produce a diff; each gets a real `git diff HEAD` via
24
- * {@link gitDiff}. Pure over its inputs (no manager, no globals) so it can be
25
- * unit-tested against a real temp git worktree.
26
- */
27
- export async function buildWorktreeDiffs(agents) {
28
- const withTree = agents.filter((a) => !!a.worktreePath);
29
- return Promise.all(withTree.map(async (a) => ({
30
- agent_id: a.agentId,
31
- name: a.name,
32
- agent_type: a.agentType,
33
- status: a.status,
34
- worktree_name: a.worktreeName,
35
- worktree_path: a.worktreePath,
36
- pr_url: a.prUrl,
37
- diff: await gitDiff(a.worktreePath),
38
- })));
39
- }
40
- /** Assemble the teams panel: overview counts, status summaries, worktree diffs. */
41
- async function assembleTeams(manager) {
42
- const { tasks } = await handleTasks(manager, 1000);
43
- const panels = [];
44
- for (const t of tasks) {
45
- const status = toTaskStatusSummary(await handleStatus(manager, t.task_name, 'all'));
46
- const teamAgents = await manager.listByTask(t.task_name);
47
- const worktrees = await buildWorktreeDiffs(teamAgents.map((a) => ({
48
- agentId: a.agentId,
49
- name: a.name,
50
- agentType: a.agentType,
51
- status: a.status,
52
- worktreeName: a.worktreeName,
53
- worktreePath: a.worktreePath,
54
- prUrl: a.prUrl,
55
- })));
56
- panels.push({
57
- task_name: t.task_name,
58
- agent_count: t.agent_count,
59
- running: t.running,
60
- completed: t.completed,
61
- failed: t.failed,
62
- agents: status.agents,
63
- worktrees,
64
- });
65
- }
66
- return panels;
67
- }
68
- /**
69
- * Assemble the full read-only snapshot. Each panel degrades independently to an
70
- * `error` string so one unavailable subsystem never blanks the whole dashboard.
71
- *
72
- * @param cwd - Project root used for project-scoped routine discovery.
73
- * @param manager - Injectable for tests; defaults to a fresh {@link AgentManager}.
74
- */
75
- export async function assembleState(cwd = process.cwd(), manager = new AgentManager()) {
76
- const [teams, routines, cloud] = await Promise.all([
77
- assembleTeams(manager).then((data) => ({ ok: true, data }), (err) => ({ ok: false, error: String(err?.message ?? err) })),
78
- Promise.resolve()
79
- .then(() => listJobs(cwd))
80
- .then((data) => ({ ok: true, data }), (err) => ({ ok: false, error: String(err?.message ?? err) })),
81
- Promise.resolve()
82
- .then(() => listCloudTasks({ limit: 50 }))
83
- .then((data) => ({ ok: true, data }), (err) => ({ ok: false, error: String(err?.message ?? err) })),
84
- ]);
85
- return {
86
- generated_at: new Date().toISOString(),
87
- teams,
88
- routines,
89
- cloud,
90
- };
91
- }
@@ -1,7 +0,0 @@
1
- /**
2
- * Self-contained HTML page for `agents serve`. No framework, no external assets
3
- * — a single inline <style> + <script> that subscribes to /events (SSE) and
4
- * re-renders the panels on each snapshot. Terminal-coded per the agents-cli
5
- * brand: #0a0a0a bg, #a3e635 lime accent, JetBrains Mono.
6
- */
7
- export declare function renderPage(): string;
@@ -1,140 +0,0 @@
1
- /**
2
- * Self-contained HTML page for `agents serve`. No framework, no external assets
3
- * — a single inline <style> + <script> that subscribes to /events (SSE) and
4
- * re-renders the panels on each snapshot. Terminal-coded per the agents-cli
5
- * brand: #0a0a0a bg, #a3e635 lime accent, JetBrains Mono.
6
- */
7
- export function renderPage() {
8
- return `<!DOCTYPE html>
9
- <html lang="en">
10
- <head>
11
- <meta charset="utf-8" />
12
- <meta name="viewport" content="width=device-width, initial-scale=1" />
13
- <title>agents serve</title>
14
- <style>
15
- :root {
16
- --bg: #0a0a0a; --panel: #121212; --border: #262626; --fg: #e5e5e5;
17
- --dim: #737373; --accent: #a3e635; --add: #4ade80; --del: #f87171; --hunk: #60a5fa;
18
- }
19
- * { box-sizing: border-box; }
20
- body {
21
- margin: 0; background: var(--bg); color: var(--fg);
22
- font-family: ui-monospace, "JetBrains Mono", "SF Mono", Menlo, monospace;
23
- font-size: 13px; line-height: 1.5;
24
- }
25
- header {
26
- position: sticky; top: 0; z-index: 10; background: var(--bg);
27
- border-bottom: 1px solid var(--border); padding: 12px 20px;
28
- display: flex; align-items: center; gap: 12px;
29
- }
30
- header .mark { color: var(--accent); font-weight: 700; letter-spacing: .5px; }
31
- header .status { color: var(--dim); }
32
- header .dot { display: inline-block; width: 8px; height: 8px; border-radius: 50%; background: var(--del); margin-right: 6px; vertical-align: middle; }
33
- header .dot.live { background: var(--accent); }
34
- main { padding: 20px; max-width: 1100px; margin: 0 auto; display: grid; gap: 24px; }
35
- h2 { font-size: 14px; color: var(--accent); border-bottom: 1px solid var(--border); padding-bottom: 6px; margin: 0 0 12px; text-transform: uppercase; letter-spacing: 1px; }
36
- .card { background: var(--panel); border: 1px solid var(--border); border-radius: 6px; padding: 14px; margin-bottom: 12px; }
37
- .row { display: flex; justify-content: space-between; gap: 12px; flex-wrap: wrap; }
38
- .muted { color: var(--dim); }
39
- .badge { font-size: 11px; padding: 1px 7px; border-radius: 10px; border: 1px solid var(--border); }
40
- .badge.running { color: var(--accent); border-color: var(--accent); }
41
- .badge.completed { color: var(--add); border-color: var(--add); }
42
- .badge.failed { color: var(--del); border-color: var(--del); }
43
- a { color: var(--hunk); text-decoration: none; }
44
- a:hover { text-decoration: underline; }
45
- details { margin-top: 8px; }
46
- summary { cursor: pointer; color: var(--dim); }
47
- pre.diff { overflow-x: auto; background: #0d0d0d; border: 1px solid var(--border); border-radius: 4px; padding: 10px; margin: 8px 0 0; white-space: pre; }
48
- pre.diff .a { color: var(--add); } pre.diff .d { color: var(--del); } pre.diff .h { color: var(--hunk); } pre.diff .m { color: var(--dim); }
49
- .err { color: var(--del); }
50
- .empty { color: var(--dim); font-style: italic; }
51
- table { width: 100%; border-collapse: collapse; }
52
- td, th { text-align: left; padding: 4px 8px; border-bottom: 1px solid var(--border); vertical-align: top; }
53
- th { color: var(--dim); font-weight: 500; }
54
- </style>
55
- </head>
56
- <body>
57
- <header>
58
- <span class="mark">agents serve</span>
59
- <span class="status"><span class="dot" id="dot"></span><span id="conn">connecting…</span></span>
60
- <span class="status" id="ts"></span>
61
- </header>
62
- <main>
63
- <section><h2>Teams &amp; Diffs</h2><div id="teams"></div></section>
64
- <section><h2>Routines</h2><div id="routines"></div></section>
65
- <section><h2>Cloud</h2><div id="cloud"></div></section>
66
- </main>
67
- <script>
68
- const $ = (id) => document.getElementById(id);
69
- const esc = (s) => String(s == null ? '' : s).replace(/[&<>"]/g, (c) => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c]));
70
- // href allowlist: only http(s) URLs are linkable. esc() blocks attribute
71
- // breakout, but a javascript:/data: URL would still execute on click — so a
72
- // non-http(s) pr_url renders as an inert '#' rather than a live link.
73
- const safeUrl = (u) => { const s = String(u == null ? '' : u); return /^https?:\\/\\//i.test(s) ? s : '#'; };
74
- function colorDiff(text) {
75
- return esc(text).split('\\n').map((line) => {
76
- if (line.startsWith('+')) return '<span class="a">' + line + '</span>';
77
- if (line.startsWith('-')) return '<span class="d">' + line + '</span>';
78
- if (line.startsWith('@@')) return '<span class="h">' + line + '</span>';
79
- if (line.startsWith('diff ') || line.startsWith('index ')) return '<span class="m">' + line + '</span>';
80
- return line;
81
- }).join('\\n');
82
- }
83
- function panelError(el, error) { el.innerHTML = '<div class="card err">error: ' + esc(error) + '</div>'; }
84
-
85
- function renderTeams(res) {
86
- const el = $('teams');
87
- if (!res.ok) return panelError(el, res.error);
88
- if (!res.data.length) { el.innerHTML = '<div class="empty">no teams</div>'; return; }
89
- el.innerHTML = res.data.map((t) => {
90
- const agents = t.agents.map((a) =>
91
- '<div class="row"><span>' + esc(a.name || a.agent_id.slice(0,8)) +
92
- ' <span class="muted">' + esc(a.agent_type) + '</span>' +
93
- (a.pr_url ? ' <a href="' + esc(safeUrl(a.pr_url)) + '" target="_blank">PR</a>' : '') +
94
- '</span><span class="badge ' + esc(a.status) + '">' + esc(a.status) + '</span></div>'
95
- ).join('');
96
- const worktrees = t.worktrees.map((w) => {
97
- const body = w.diff
98
- ? '<details><summary>diff · ' + esc(w.worktree_name || w.worktree_path) + '</summary><pre class="diff">' + colorDiff(w.diff) + '</pre></details>'
99
- : '<div class="muted">' + esc(w.worktree_name || w.worktree_path) + ' — no uncommitted changes</div>';
100
- return body;
101
- }).join('');
102
- return '<div class="card"><div class="row"><strong>' + esc(t.task_name) + '</strong>' +
103
- '<span class="muted">' + t.running + ' running · ' + t.completed + ' done · ' + t.failed + ' failed</span></div>' +
104
- agents + worktrees + '</div>';
105
- }).join('');
106
- }
107
- function renderRoutines(res) {
108
- const el = $('routines');
109
- if (!res.ok) return panelError(el, res.error);
110
- if (!res.data.length) { el.innerHTML = '<div class="empty">no routines</div>'; return; }
111
- el.innerHTML = '<div class="card"><table><tr><th>name</th><th>schedule</th><th>agent</th><th>enabled</th></tr>' +
112
- res.data.map((j) => '<tr><td>' + esc(j.name) + '</td><td class="muted">' + esc(j.schedule) + '</td><td>' +
113
- esc(j.workflow ? 'wf:' + j.workflow : j.agent) + '</td><td>' + (j.enabled ? 'yes' : '<span class="muted">no</span>') + '</td></tr>').join('') +
114
- '</table></div>';
115
- }
116
- function renderCloud(res) {
117
- const el = $('cloud');
118
- if (!res.ok) return panelError(el, res.error);
119
- if (!res.data.length) { el.innerHTML = '<div class="empty">no cloud tasks</div>'; return; }
120
- el.innerHTML = '<div class="card"><table><tr><th>id</th><th>provider</th><th>status</th><th>repo</th><th></th></tr>' +
121
- res.data.map((c) => '<tr><td>' + esc(c.id.slice(0,10)) + '</td><td>' + esc(c.provider) + '</td><td><span class="badge ' + esc(c.status) + '">' +
122
- esc(c.status) + '</span></td><td class="muted">' + esc(c.repo || '') + '</td><td>' +
123
- (c.prUrl ? '<a href="' + esc(safeUrl(c.prUrl)) + '" target="_blank">PR</a>' : '') + '</td></tr>').join('') +
124
- '</table></div>';
125
- }
126
- function render(state) {
127
- renderTeams(state.teams); renderRoutines(state.routines); renderCloud(state.cloud);
128
- $('ts').textContent = 'updated ' + new Date(state.generated_at).toLocaleTimeString();
129
- }
130
- function setConn(live) {
131
- $('dot').className = 'dot' + (live ? ' live' : '');
132
- $('conn').textContent = live ? 'live' : 'reconnecting…';
133
- }
134
- const src = new EventSource('/events');
135
- src.addEventListener('state', (e) => { setConn(true); render(JSON.parse(e.data)); });
136
- src.onerror = () => setConn(false);
137
- </script>
138
- </body>
139
- </html>`;
140
- }
@@ -1,80 +0,0 @@
1
- /**
2
- * Read-only localhost HTTP + Server-Sent-Events server for `agents serve`.
3
- *
4
- * There is deliberately NO framework and NO mutation surface:
5
- * - binds 127.0.0.1 only (loopback) — never reachable off the machine;
6
- * - answers GET only (every other method → 405);
7
- * - routes: `GET /` → HTML page, `GET /api/state` → JSON snapshot,
8
- * `GET /events` → SSE stream that re-pushes the snapshot on an interval.
9
- *
10
- * The whole thing is a viewer over data other commands already own
11
- * ({@link assembleState}); it writes nothing.
12
- *
13
- * The GET routing is exported as {@link handleServeGet} so the authenticated
14
- * `--control` variant (see control.ts) can reuse it verbatim rather than
15
- * duplicate the state/SSE surface.
16
- */
17
- import type http from 'http';
18
- import type { ServeState } from './data.js';
19
- /** Loopback address — the server binds here and nowhere else. */
20
- export declare const SERVE_HOST = "127.0.0.1";
21
- /**
22
- * DNS-rebinding guard. The socket binds loopback, but binding alone does not
23
- * stop a remote page: an attacker can point a hostname's DNS at 127.0.0.1 and
24
- * drive the victim's browser to `GET /api/state`, which exposes uncommitted
25
- * `git diff HEAD` of every worktree plus routine + cloud config. A browser
26
- * always sends a `Host` header naming the site it *thinks* it reached, so we
27
- * trust only a Host that is itself loopback. A missing Host (HTTP/1.0 or a raw
28
- * non-browser client like curl-without-Host) is not a rebind vector and is
29
- * allowed; any explicit non-loopback Host is rejected.
30
- */
31
- export declare function isAllowedServeHost(hostHeader: string | undefined): boolean;
32
- /** Default port for `agents serve`. */
33
- export declare const DEFAULT_SERVE_PORT = 4477;
34
- /** Default SSE push cadence. */
35
- export declare const DEFAULT_INTERVAL_MS = 3000;
36
- export interface ServeOptions {
37
- /** Project root for project-scoped routine discovery. Defaults to process.cwd(). */
38
- cwd?: string;
39
- /** SSE push cadence in ms. Defaults to {@link DEFAULT_INTERVAL_MS}. */
40
- intervalMs?: number;
41
- /**
42
- * Snapshot data source. Defaults to {@link assembleState}. Read-only DI seam
43
- * so tests can drive SSE lifecycle timing deterministically (e.g. gate the
44
- * first snapshot to reproduce a mid-snapshot disconnect); production never
45
- * sets it.
46
- */
47
- snapshot?: () => Promise<ServeState>;
48
- }
49
- /**
50
- * Resolved GET-route context: the snapshot source and SSE cadence, with all
51
- * defaults applied. Built once by {@link resolveServeContext}.
52
- */
53
- export interface ServeContext {
54
- snapshot: () => Promise<ServeState>;
55
- intervalMs: number;
56
- }
57
- /** Apply defaults to {@link ServeOptions}, producing a {@link ServeContext}. */
58
- export declare function resolveServeContext(opts?: ServeOptions): ServeContext;
59
- /**
60
- * Handle the shared read-only GET surface (`/`, `/api/state`, `/events`).
61
- * Returns `true` when the route was recognized and a response was written (or
62
- * begun, for SSE); `false` when the path is unknown so the caller can 404 or
63
- * try its own routes first. Extracted so the `--control` server reuses the
64
- * exact same state/SSE behavior — including the load-bearing SSE close/timer
65
- * ordering below — without duplicating it.
66
- */
67
- export declare function handleServeGet(url: string, req: http.IncomingMessage, res: http.ServerResponse, ctx: ServeContext): Promise<boolean>;
68
- /**
69
- * Create (but do not start) the read-only serve server. Caller invokes
70
- * `.listen(port, SERVE_HOST)`. Returned so tests can bind an ephemeral port.
71
- */
72
- export declare function createServeServer(opts?: ServeOptions): http.Server;
73
- /**
74
- * Start the serve server on `port`, bound to loopback. Resolves with the
75
- * actual bound port (useful when `port === 0` for tests).
76
- */
77
- export declare function startServeServer(port?: number, opts?: ServeOptions): Promise<{
78
- server: http.Server;
79
- port: number;
80
- }>;