@phnx-labs/agents-cli 1.22.43 → 1.22.45
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 +85 -0
- package/README.md +14 -12
- package/dist/bootstrap.js +3 -6
- package/dist/cli/command-registry.d.ts +0 -7
- package/dist/cli/command-registry.js +1 -21
- package/dist/commands/accounts.d.ts +1 -9
- package/dist/commands/accounts.js +12 -90
- package/dist/commands/apply.d.ts +7 -10
- package/dist/commands/apply.js +15 -35
- package/dist/commands/artifacts.js +4 -5
- package/dist/commands/audit.d.ts +6 -6
- package/dist/commands/audit.js +12 -12
- package/dist/commands/beta.d.ts +7 -1
- package/dist/commands/beta.js +16 -9
- package/dist/commands/commands.js +1 -1
- package/dist/commands/doctor.js +1 -4
- package/dist/commands/events.d.ts +2 -2
- package/dist/commands/events.js +6 -5
- package/dist/commands/exec.d.ts +1 -1
- package/dist/commands/exec.js +8 -4
- package/dist/commands/factory.d.ts +1 -1
- package/dist/commands/fleet-capture.d.ts +1 -1
- package/dist/commands/fleet-capture.js +4 -5
- package/dist/commands/harness.js +1 -49
- package/dist/commands/hooks.js +1 -1
- package/dist/commands/insights.d.ts +1 -1
- package/dist/commands/insights.js +85 -156
- package/dist/commands/menubar.js +58 -1
- package/dist/commands/output.js +2 -2
- package/dist/commands/prune.js +5 -4
- package/dist/commands/routines.test-fixture.d.ts +86 -0
- package/dist/commands/routines.test-fixture.js +345 -0
- package/dist/commands/sessions.test-fixture.d.ts +24 -0
- package/dist/commands/sessions.test-fixture.js +224 -0
- package/dist/commands/setup.js +5 -1
- package/dist/commands/share.d.ts +9 -10
- package/dist/commands/share.js +68 -50
- package/dist/commands/skills.js +1 -1
- package/dist/commands/ssh.js +15 -62
- package/dist/commands/subagents.js +1 -1
- package/dist/commands/view.js +5 -2
- package/dist/lib/analytics/mix-commands.d.ts +6 -28
- package/dist/lib/analytics/mix-commands.js +14 -40
- package/dist/lib/audit/log.d.ts +2 -2
- package/dist/lib/audit/log.js +2 -2
- package/dist/lib/beta.js +1 -1
- package/dist/lib/browser/domain-skills.d.ts +26 -6
- package/dist/lib/browser/domain-skills.js +81 -43
- package/dist/lib/daemon/daemon.js +11 -0
- package/dist/lib/daemon/daemon.test-fixture.d.ts +24 -0
- package/dist/lib/daemon/daemon.test-fixture.js +71 -0
- package/dist/lib/daemon-ticks.d.ts +38 -1
- package/dist/lib/daemon-ticks.js +52 -5
- package/dist/lib/device-config.js +1 -2
- package/dist/lib/devices/discovery-policy.d.ts +3 -3
- package/dist/lib/devices/discovery-policy.js +3 -3
- package/dist/lib/devices/fleet.d.ts +4 -8
- package/dist/lib/devices/fleet.js +3 -15
- package/dist/lib/devices/health-report.js +3 -3
- package/dist/lib/devices/pool.d.ts +0 -6
- package/dist/lib/devices/pool.js +0 -6
- package/dist/lib/devices/registry.d.ts +0 -15
- package/dist/lib/devices/registry.js +0 -9
- package/dist/lib/devices/stats-cache.d.ts +12 -0
- package/dist/lib/devices/stats-cache.js +24 -5
- package/dist/lib/fleet/capture.d.ts +1 -1
- package/dist/lib/fleet/manifest.js +1 -1
- package/dist/lib/fleet/remote-login.d.ts +2 -2
- package/dist/lib/fleet/remote-login.js +3 -4
- package/dist/lib/hosts/dispatch.d.ts +2 -0
- package/dist/lib/hosts/dispatch.js +8 -1
- package/dist/lib/hosts/passthrough.js +0 -1
- package/dist/lib/hosts/providers/devices.js +1 -13
- package/dist/lib/hosts/ready.d.ts +12 -3
- package/dist/lib/hosts/ready.js +27 -12
- package/dist/lib/hosts/registry.d.ts +4 -5
- package/dist/lib/hosts/registry.js +3 -7
- package/dist/lib/installations/versions.js +21 -2
- package/dist/lib/menubar/install-menubar.d.ts +109 -0
- package/dist/lib/menubar/install-menubar.js +199 -4
- package/dist/lib/remote-agents-json.js +1 -7
- package/dist/lib/routines-placement.d.ts +1 -1
- package/dist/lib/routines-placement.js +1 -1
- package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/Info.plist +0 -2
- package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/_CodeSignature/CodeResources +1 -13
- package/dist/lib/session/active.d.ts +4 -16
- package/dist/lib/session/active.js +2 -6
- package/dist/lib/session/db.d.ts +13 -98
- package/dist/lib/session/db.js +4 -11
- package/dist/lib/session/discover.d.ts +5 -81
- package/dist/lib/session/discover.js +5 -5
- package/dist/lib/session/remote/remote-bundle.d.ts +12 -0
- package/dist/lib/session/remote/remote-bundle.js +61 -0
- package/dist/lib/session/remote/remote-list.d.ts +148 -0
- package/dist/lib/session/remote/remote-list.js +607 -0
- package/dist/lib/session/remote/remote.d.ts +132 -0
- package/dist/lib/session/remote/remote.js +314 -0
- package/dist/lib/session/remote/watch.d.ts +101 -0
- package/dist/lib/session/remote/watch.js +241 -0
- package/dist/lib/session/remote-bundle.d.ts +1 -12
- package/dist/lib/session/remote-bundle.js +3 -61
- package/dist/lib/session/remote-list.d.ts +1 -148
- package/dist/lib/session/remote-list.js +3 -607
- package/dist/lib/session/remote.d.ts +1 -132
- package/dist/lib/session/remote.js +3 -314
- package/dist/lib/session/watch.d.ts +1 -101
- package/dist/lib/session/watch.js +3 -242
- package/dist/lib/share/config.d.ts +12 -2
- package/dist/lib/share/config.js +47 -11
- package/dist/lib/share/delete.js +1 -1
- package/dist/lib/share/publish.js +1 -1
- package/dist/lib/share/worker-template.js +2 -2
- package/dist/lib/smart-launch.d.ts +4 -4
- package/dist/lib/smart-launch.js +8 -18
- package/dist/lib/staleness/index.d.ts +3 -1
- package/dist/lib/staleness/index.js +21 -1
- package/dist/lib/staleness/types.d.ts +11 -0
- package/dist/lib/staleness/writers/commands.js +13 -4
- package/dist/lib/staleness/writers/hooks.js +3 -1
- package/dist/lib/staleness/writers/mcp.d.ts +0 -7
- package/dist/lib/staleness/writers/mcp.js +16 -1
- package/dist/lib/staleness/writers/rules.js +1 -1
- package/dist/lib/staleness/writers/skills.js +3 -1
- package/dist/lib/staleness/writers/subagents.d.ts +0 -9
- package/dist/lib/staleness/writers/subagents.js +18 -1
- package/dist/lib/staleness/writers/types.d.ts +10 -0
- package/dist/lib/startup/command-registry.d.ts +8 -1
- package/dist/lib/startup/command-registry.js +21 -6
- package/dist/lib/startup/root-command.d.ts +18 -1
- package/dist/lib/startup/root-command.js +18 -1
- package/dist/lib/state.js +7 -0
- package/dist/lib/teams/placement-probe.js +27 -12
- package/dist/lib/teams/scheduler.d.ts +3 -1
- package/dist/lib/types.d.ts +1 -1
- package/dist/lib/view-types.d.ts +3 -0
- package/package.json +2 -3
- package/dist/bin/agents +0 -0
- package/dist/commands/auth.d.ts +0 -9
- package/dist/commands/auth.js +0 -108
- package/dist/commands/org.d.ts +0 -6
- package/dist/commands/org.js +0 -175
- package/dist/commands/serve.d.ts +0 -10
- package/dist/commands/serve.js +0 -68
- package/dist/commands/trends.d.ts +0 -10
- package/dist/commands/trends.js +0 -12
- package/dist/lib/entitlement.d.ts +0 -31
- package/dist/lib/entitlement.js +0 -137
- package/dist/lib/prix-account.d.ts +0 -158
- package/dist/lib/prix-account.js +0 -214
- package/dist/lib/secrets/Agents CLI.app/Contents/Resources/AppIcon.icns +0 -0
- package/dist/lib/serve/control.d.ts +0 -95
- package/dist/lib/serve/control.js +0 -260
- package/dist/lib/serve/data.d.ts +0 -81
- package/dist/lib/serve/data.js +0 -91
- package/dist/lib/serve/page.d.ts +0 -7
- package/dist/lib/serve/page.js +0 -140
- package/dist/lib/serve/server.d.ts +0 -80
- package/dist/lib/serve/server.js +0 -145
- package/dist/lib/serve/stream.d.ts +0 -43
- package/dist/lib/serve/stream.js +0 -116
- package/dist/lib/serve/token.d.ts +0 -35
- package/dist/lib/serve/token.js +0 -85
package/dist/lib/prix-account.js
DELETED
|
@@ -1,214 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Client for the Rush account layer (`api.prix.dev`) — backs `agents auth` and
|
|
3
|
-
* `agents org`. `agents org` maps to `/api/v1/spaces`, not `/api/v1/orgs`: spaces
|
|
4
|
-
* already carry the free-tier caps (1 owned space, 3 members) and can exist
|
|
5
|
-
* standalone with no parent organization, matching a "just want a team" CLI flow
|
|
6
|
-
* better than the heavier enterprise-tenancy `orgs` routes (domain, SSO). See
|
|
7
|
-
* `.agents/artifacts/2026-08-20/agi-cli-client-integration-spec.md` §4.2 in the
|
|
8
|
-
* agi-cli-web repo for the full reasoning and the live-confirmed route/shape audit.
|
|
9
|
-
*
|
|
10
|
-
* Token custody: `agents auth login` writes its own session file
|
|
11
|
-
* (`getPrixSessionFile()`), separate from `~/.rush/user.yaml`, so `agents auth
|
|
12
|
-
* logout` never signs the user out of `rush` (the two CLIs share a backend
|
|
13
|
-
* account, not a credential store). Reads fall back to `~/.rush/user.yaml`
|
|
14
|
-
* (the pattern `readRushToken` in `lib/secrets/drivers/rush.ts` already uses)
|
|
15
|
-
* so a user who is only signed in via `rush login` still gets a working
|
|
16
|
-
* `agents auth whoami` / `agents org` with zero extra login step.
|
|
17
|
-
*/
|
|
18
|
-
import * as fs from 'fs';
|
|
19
|
-
import * as os from 'os';
|
|
20
|
-
import * as path from 'path';
|
|
21
|
-
import * as yaml from 'yaml';
|
|
22
|
-
import { getRuntimeStateDir } from './state.js';
|
|
23
|
-
export const PRIX_API_BASE = 'https://api.prix.dev';
|
|
24
|
-
const RUSH_USER_YAML = path.join(os.homedir(), '.rush', 'user.yaml');
|
|
25
|
-
/** Computed per-call (not cached at module load) so `AGENTS_STATE_DIR` overrides in tests take effect. */
|
|
26
|
-
function prixSessionFile() {
|
|
27
|
-
return path.join(getRuntimeStateDir(), 'prix-account.json');
|
|
28
|
-
}
|
|
29
|
-
/** Read the token `rush login` wrote, with no expiry check — the same shape `readRushToken` reads. */
|
|
30
|
-
function readRushSessionToken() {
|
|
31
|
-
if (!fs.existsSync(RUSH_USER_YAML))
|
|
32
|
-
return null;
|
|
33
|
-
try {
|
|
34
|
-
const data = yaml.parse(fs.readFileSync(RUSH_USER_YAML, 'utf-8'));
|
|
35
|
-
return data?.session?.access_token ?? null;
|
|
36
|
-
}
|
|
37
|
-
catch {
|
|
38
|
-
return null;
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
export function getPrixSessionFile() {
|
|
42
|
-
return prixSessionFile();
|
|
43
|
-
}
|
|
44
|
-
export function readPrixSession() {
|
|
45
|
-
const file = prixSessionFile();
|
|
46
|
-
if (!fs.existsSync(file))
|
|
47
|
-
return null;
|
|
48
|
-
try {
|
|
49
|
-
const parsed = JSON.parse(fs.readFileSync(file, 'utf-8'));
|
|
50
|
-
if (!parsed || typeof parsed.access_token !== 'string')
|
|
51
|
-
return null;
|
|
52
|
-
return parsed;
|
|
53
|
-
}
|
|
54
|
-
catch {
|
|
55
|
-
return null;
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
export function writePrixSession(session) {
|
|
59
|
-
const file = prixSessionFile();
|
|
60
|
-
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
61
|
-
const tmp = `${file}.${process.pid}.tmp`;
|
|
62
|
-
fs.writeFileSync(tmp, JSON.stringify(session, null, 2), { mode: 0o600 });
|
|
63
|
-
fs.renameSync(tmp, file);
|
|
64
|
-
}
|
|
65
|
-
export function clearPrixSession() {
|
|
66
|
-
const file = prixSessionFile();
|
|
67
|
-
if (!fs.existsSync(file))
|
|
68
|
-
return false;
|
|
69
|
-
fs.rmSync(file);
|
|
70
|
-
return true;
|
|
71
|
-
}
|
|
72
|
-
export function resolvePrixToken() {
|
|
73
|
-
const own = readPrixSession();
|
|
74
|
-
if (own?.access_token)
|
|
75
|
-
return { token: own.access_token, source: 'agents' };
|
|
76
|
-
const rushToken = readRushSessionToken();
|
|
77
|
-
if (rushToken)
|
|
78
|
-
return { token: rushToken, source: 'rush' };
|
|
79
|
-
return null;
|
|
80
|
-
}
|
|
81
|
-
export class PrixApiError extends Error {
|
|
82
|
-
status;
|
|
83
|
-
constructor(status, message) {
|
|
84
|
-
super(message);
|
|
85
|
-
this.status = status;
|
|
86
|
-
this.name = 'PrixApiError';
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
async function prixRequest(method, endpoint, opts = {}) {
|
|
90
|
-
const auth = opts.auth !== false;
|
|
91
|
-
let token = opts.token;
|
|
92
|
-
if (auth && !token) {
|
|
93
|
-
const resolved = resolvePrixToken();
|
|
94
|
-
if (!resolved)
|
|
95
|
-
throw new Error("Not signed in. Run 'agents auth login' first.");
|
|
96
|
-
token = resolved.token;
|
|
97
|
-
}
|
|
98
|
-
const headers = { 'Content-Type': 'application/json' };
|
|
99
|
-
if (token)
|
|
100
|
-
headers.Authorization = `Bearer ${token}`;
|
|
101
|
-
const res = await fetch(`${PRIX_API_BASE}${endpoint}`, {
|
|
102
|
-
method,
|
|
103
|
-
headers,
|
|
104
|
-
body: opts.body === undefined ? undefined : JSON.stringify(opts.body),
|
|
105
|
-
});
|
|
106
|
-
const text = await res.text();
|
|
107
|
-
const data = text ? JSON.parse(text) : undefined;
|
|
108
|
-
if (!res.ok) {
|
|
109
|
-
const message = (data && typeof data === 'object' && 'error' in data) ? String(data.error) : `${res.status} ${res.statusText}`;
|
|
110
|
-
throw new PrixApiError(res.status, message);
|
|
111
|
-
}
|
|
112
|
-
return data;
|
|
113
|
-
}
|
|
114
|
-
/** `GET /api/v1/auth/me` — live-confirmed shape: `{email, userId, valid}`. */
|
|
115
|
-
export async function fetchWhoAmI(token) {
|
|
116
|
-
return prixRequest('GET', '/api/v1/auth/me', { token });
|
|
117
|
-
}
|
|
118
|
-
/** `POST /api/v1/auth/device/authorization` — public, no token. */
|
|
119
|
-
export async function startDeviceAuthorization() {
|
|
120
|
-
return prixRequest('POST', '/api/v1/auth/device/authorization', { auth: false, body: {} });
|
|
121
|
-
}
|
|
122
|
-
/** `POST /api/v1/auth/device/token` — one poll attempt. Callers own the interval loop. */
|
|
123
|
-
export async function pollDeviceToken(deviceCode) {
|
|
124
|
-
try {
|
|
125
|
-
const data = await prixRequest('POST', '/api/v1/auth/device/token', { auth: false, body: { grant_type: 'urn:ietf:params:oauth:grant-type:device_code', device_code: deviceCode } });
|
|
126
|
-
return { status: 'authorized', ...data };
|
|
127
|
-
}
|
|
128
|
-
catch (err) {
|
|
129
|
-
if (err instanceof PrixApiError) {
|
|
130
|
-
if (err.message.includes('authorization_pending'))
|
|
131
|
-
return { status: 'pending' };
|
|
132
|
-
if (err.message.includes('slow_down'))
|
|
133
|
-
return { status: 'slow_down' };
|
|
134
|
-
if (err.message.includes('expired_token'))
|
|
135
|
-
return { status: 'expired' };
|
|
136
|
-
if (err.message.includes('access_denied'))
|
|
137
|
-
return { status: 'denied' };
|
|
138
|
-
}
|
|
139
|
-
throw err;
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
/** `GET /api/v1/spaces` — live-confirmed: array of `SpaceSummary`. */
|
|
143
|
-
export async function listSpaces() {
|
|
144
|
-
return prixRequest('GET', '/api/v1/spaces');
|
|
145
|
-
}
|
|
146
|
-
/** `POST /api/v1/spaces` — 403 if the caller already owns a space (free tier: 1). */
|
|
147
|
-
export async function createSpace(input) {
|
|
148
|
-
return prixRequest('POST', '/api/v1/spaces', { body: input });
|
|
149
|
-
}
|
|
150
|
-
/** `GET /api/v1/spaces/:id` — requires membership. */
|
|
151
|
-
export async function getSpace(spaceId) {
|
|
152
|
-
return prixRequest('GET', `/api/v1/spaces/${encodeURIComponent(spaceId)}`);
|
|
153
|
-
}
|
|
154
|
-
/** `GET /api/v1/spaces/:id/members`. */
|
|
155
|
-
export async function listSpaceMembers(spaceId) {
|
|
156
|
-
return prixRequest('GET', `/api/v1/spaces/${encodeURIComponent(spaceId)}/members`);
|
|
157
|
-
}
|
|
158
|
-
/** `POST /api/v1/spaces/:id/invites` — sends a real email for the pending-invite path. */
|
|
159
|
-
export async function createSpaceInvite(spaceId, email, role = 'member') {
|
|
160
|
-
return prixRequest('POST', `/api/v1/spaces/${encodeURIComponent(spaceId)}/invites`, { body: { email, role } });
|
|
161
|
-
}
|
|
162
|
-
/** `GET /api/v1/spaces/:id/invites`. */
|
|
163
|
-
export async function listSpaceInvites(spaceId) {
|
|
164
|
-
return prixRequest('GET', `/api/v1/spaces/${encodeURIComponent(spaceId)}/invites`);
|
|
165
|
-
}
|
|
166
|
-
/** `DELETE /api/v1/spaces/:id/invites/:inviteId`. */
|
|
167
|
-
export async function revokeSpaceInvite(spaceId, inviteId) {
|
|
168
|
-
return prixRequest('DELETE', `/api/v1/spaces/${encodeURIComponent(spaceId)}/invites/${encodeURIComponent(inviteId)}`);
|
|
169
|
-
}
|
|
170
|
-
/** `PATCH /api/v1/spaces/:id/members/:userId` — owner-only for admin changes. Route takes userId, not email. */
|
|
171
|
-
export async function updateSpaceMemberRole(spaceId, userId, role) {
|
|
172
|
-
return prixRequest('PATCH', `/api/v1/spaces/${encodeURIComponent(spaceId)}/members/${encodeURIComponent(userId)}`, { body: { role } });
|
|
173
|
-
}
|
|
174
|
-
/** `DELETE /api/v1/spaces/:id/members/:userId` — owner, admin, or the member themself (leave). */
|
|
175
|
-
export async function removeSpaceMember(spaceId, userId) {
|
|
176
|
-
await prixRequest('DELETE', `/api/v1/spaces/${encodeURIComponent(spaceId)}/members/${encodeURIComponent(userId)}`);
|
|
177
|
-
}
|
|
178
|
-
/** `DELETE /api/v1/spaces/:id` — soft delete, 30-day restore window. */
|
|
179
|
-
export async function deleteSpace(spaceId) {
|
|
180
|
-
await prixRequest('DELETE', `/api/v1/spaces/${encodeURIComponent(spaceId)}`);
|
|
181
|
-
}
|
|
182
|
-
/** `agents-cli-space-name` -> `agi-cli-space-name`; lowercase, hyphenated, matches the backend's `^[a-z0-9-]+$` slug rule. */
|
|
183
|
-
export function slugify(name) {
|
|
184
|
-
const slug = name.toLowerCase().trim().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
|
|
185
|
-
return slug || 'space';
|
|
186
|
-
}
|
|
187
|
-
/**
|
|
188
|
-
* Resolve the `--space` a command should act on: an explicit id/slug match
|
|
189
|
-
* against the caller's own space list, or — with nothing passed — the
|
|
190
|
-
* caller's sole space (free tier caps ownership at one, so this is almost
|
|
191
|
-
* always unambiguous). Pure over an already-fetched list so it's cheaply
|
|
192
|
-
* unit-testable with a fixture.
|
|
193
|
-
*/
|
|
194
|
-
export function resolveSpaceFromList(spaces, explicit) {
|
|
195
|
-
if (explicit) {
|
|
196
|
-
const match = spaces.find(s => s.id === explicit || s.slug === explicit);
|
|
197
|
-
if (!match)
|
|
198
|
-
throw new Error(`No space matching '${explicit}'. Run 'agents org list' to see your spaces.`);
|
|
199
|
-
return match;
|
|
200
|
-
}
|
|
201
|
-
if (spaces.length === 0)
|
|
202
|
-
throw new Error("You have no spaces. Create one with 'agents org create <name>'.");
|
|
203
|
-
if (spaces.length > 1) {
|
|
204
|
-
throw new Error(`You belong to ${spaces.length} spaces — pass --space <id-or-slug>: ${spaces.map(s => s.slug).join(', ')}`);
|
|
205
|
-
}
|
|
206
|
-
return spaces[0];
|
|
207
|
-
}
|
|
208
|
-
/** Resolve a member's email to their `user_id` from an already-fetched member list. */
|
|
209
|
-
export function resolveMemberFromList(members, email) {
|
|
210
|
-
const match = members.find(m => m.email.toLowerCase() === email.toLowerCase());
|
|
211
|
-
if (!match)
|
|
212
|
-
throw new Error(`'${email}' is not a member of this space. Run 'agents org members' to see who is.`);
|
|
213
|
-
return match;
|
|
214
|
-
}
|
|
Binary file
|
|
@@ -1,95 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Authenticated `agents serve --control` server — the "anchor" the iOS/iPadOS
|
|
3
|
-
* cockpit talks to (RUSH-1731).
|
|
4
|
-
*
|
|
5
|
-
* It is the read-only {@link handleServeGet} surface plus a small mutation API,
|
|
6
|
-
* gated by a bearer token ({@link verifyControlToken}) on EVERY request because,
|
|
7
|
-
* unlike plain `agents serve`, this variant may bind beyond loopback (a tailnet
|
|
8
|
-
* address) so the phone can reach it. It adds no new execution machinery: a run
|
|
9
|
-
* request re-invokes the same `agents run` the CLI uses (inheriting host
|
|
10
|
-
* offload, secrets, detached dispatch), and a message re-invokes `agents
|
|
11
|
-
* message`. The server generates the run's session id so the run is immediately
|
|
12
|
-
* addressable for streaming (Phase 2) and steering.
|
|
13
|
-
*
|
|
14
|
-
* Both side-effecting operations are behind DI seams ({@link ControlOptions})
|
|
15
|
-
* so tests drive them without spawning real agents — mirroring the `snapshot`
|
|
16
|
-
* seam on the read-only server.
|
|
17
|
-
*/
|
|
18
|
-
import type http from 'http';
|
|
19
|
-
import type { StdioOptions } from 'child_process';
|
|
20
|
-
import { type ServeOptions } from './server.js';
|
|
21
|
-
/** A request to start an agent run on this anchor (local or offloaded). */
|
|
22
|
-
export interface RunRequest {
|
|
23
|
-
/** Agent to run: 'claude' | 'codex' | 'gemini' | … */
|
|
24
|
-
agent: string;
|
|
25
|
-
/** Headless prompt. */
|
|
26
|
-
prompt: string;
|
|
27
|
-
/** Permission mode: plan | edit | auto | skip. Defaults to the CLI default. */
|
|
28
|
-
mode?: string;
|
|
29
|
-
/** Offload onto a registered device / host (the executor). Omit → run on the anchor. */
|
|
30
|
-
host?: string;
|
|
31
|
-
/** Working directory (on the host when `host` is set). */
|
|
32
|
-
cwd?: string;
|
|
33
|
-
}
|
|
34
|
-
/** Identifiers by which the started run can later be streamed / messaged. */
|
|
35
|
-
export interface RunResult {
|
|
36
|
-
/** Addressable session id (a UUID for claude; otherwise equals `name`). */
|
|
37
|
-
sessionId: string;
|
|
38
|
-
/** Durable run name seeded into the session label. */
|
|
39
|
-
name: string;
|
|
40
|
-
}
|
|
41
|
-
/** Starts a run and returns its addressable ids. */
|
|
42
|
-
export type RunDispatcher = (req: RunRequest) => Promise<RunResult>;
|
|
43
|
-
/** Delivers a message to a running/parked agent by id. */
|
|
44
|
-
export type Messenger = (id: string, text: string, from?: string) => Promise<void>;
|
|
45
|
-
export interface ControlOptions extends ServeOptions {
|
|
46
|
-
/** Bearer verifier. Defaults to {@link verifyControlToken}. */
|
|
47
|
-
verifyToken?: (presented: string | undefined) => boolean;
|
|
48
|
-
/** Run starter. Defaults to spawning `agents run …` detached. */
|
|
49
|
-
runner?: RunDispatcher;
|
|
50
|
-
/** Message sender. Defaults to spawning `agents message …`. */
|
|
51
|
-
messenger?: Messenger;
|
|
52
|
-
/** Poll cadence (ms) for the session event stream. Defaults to 300. */
|
|
53
|
-
streamPollMs?: number;
|
|
54
|
-
/** Resolve a session id to its NDJSON capture file. Defaults to {@link streamLogPath}. */
|
|
55
|
-
streamLogPathFor?: (sessionId: string) => string;
|
|
56
|
-
}
|
|
57
|
-
/**
|
|
58
|
-
* SSE stream of a session's normalized NDJSON events, offset-tailed from its
|
|
59
|
-
* capture file. Resumes from `?offset=<bytes>` or the `Last-Event-ID` header
|
|
60
|
-
* (each event's `id:` is its exact byte offset, so resume neither loses nor
|
|
61
|
-
* duplicates). Closes when a terminal (`result`/`error`) event is seen or the
|
|
62
|
-
* client disconnects. A not-yet-created file simply yields nothing until the
|
|
63
|
-
* run starts writing — the phone can open the stream the instant it dispatches.
|
|
64
|
-
*/
|
|
65
|
-
export declare function startSessionStream(file: string, req: http.IncomingMessage, res: http.ServerResponse, pollMs: number): void;
|
|
66
|
-
/**
|
|
67
|
-
* Spawn a detached child and settle once it has either successfully spawned or
|
|
68
|
-
* failed to. Attaching an `'error'` listener is mandatory: a `ChildProcess` is
|
|
69
|
-
* an `EventEmitter`, so an `'error'` event (e.g. ENOENT from a stale/missing
|
|
70
|
-
* `agents` binary — a real condition `validateDaemonBinary` guards against)
|
|
71
|
-
* with no listener throws and would take down the whole anchor process, not
|
|
72
|
-
* just the one request. Mirrors the `once('spawn') / once('error')` pattern in
|
|
73
|
-
* `teams/agents.ts`. The `'error'` listener registered by `once` survives after
|
|
74
|
-
* a successful spawn, so a later error is absorbed rather than crashing.
|
|
75
|
-
*/
|
|
76
|
-
export declare function spawnDetached(command: string, args: string[], stdio?: StdioOptions): Promise<void>;
|
|
77
|
-
/**
|
|
78
|
-
* Default run dispatcher: re-invoke `agents run` with a server-minted id so the
|
|
79
|
-
* run is addressable, detached so it outlives this request (and the anchor
|
|
80
|
-
* process). Local runs detach here; `--device` runs detach on the executor via
|
|
81
|
-
* the existing dispatch path.
|
|
82
|
-
*/
|
|
83
|
-
export declare const defaultRunner: RunDispatcher;
|
|
84
|
-
/** Default messenger: re-invoke `agents message <id> <text>` and await its exit. */
|
|
85
|
-
export declare const defaultMessenger: Messenger;
|
|
86
|
-
/**
|
|
87
|
-
* Create (but do not start) the authenticated control server. Caller invokes
|
|
88
|
-
* `.listen(port, bind)`.
|
|
89
|
-
*/
|
|
90
|
-
export declare function createControlServer(opts?: ControlOptions): http.Server;
|
|
91
|
-
/** Start the control server on `port`, bound to `bind`. Resolves the bound port. */
|
|
92
|
-
export declare function startControlServer(port: number, bind: string, opts?: ControlOptions): Promise<{
|
|
93
|
-
server: http.Server;
|
|
94
|
-
port: number;
|
|
95
|
-
}>;
|
|
@@ -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
|
-
}
|
package/dist/lib/serve/data.d.ts
DELETED
|
@@ -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>;
|