@agentproto/cli 0.1.0-alpha.0 → 0.1.0-alpha.2
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/dist/cli.mjs +1901 -63
- package/dist/cli.mjs.map +1 -1
- package/dist/index.d.ts +93 -18
- package/dist/index.mjs +976 -30
- package/dist/index.mjs.map +1 -1
- package/package.json +21 -16
package/dist/cli.mjs
CHANGED
|
@@ -1,16 +1,460 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { parseArgs } from 'util';
|
|
3
2
|
import { spawn } from 'child_process';
|
|
4
|
-
import {
|
|
3
|
+
import { userInfo, hostname, homedir, platform, tmpdir } from 'os';
|
|
4
|
+
import { parseArgs } from 'util';
|
|
5
|
+
import { readFile, mkdir, writeFile, chmod, mkdtemp, rm, unlink } from 'fs/promises';
|
|
6
|
+
import { resolve, basename, join, dirname } from 'path';
|
|
7
|
+
import { createHash } from 'crypto';
|
|
8
|
+
import { promises, existsSync } from 'fs';
|
|
9
|
+
import { createInterface } from 'readline/promises';
|
|
10
|
+
import { stdout, stdin } from 'process';
|
|
5
11
|
import { createAgentCliRuntime } from '@agentproto/driver-agent-cli';
|
|
12
|
+
import { wrapWebSocket, createTunnelServer } from '@agentproto/acp/tunnel';
|
|
13
|
+
import { createGateway } from '@agentproto/runtime';
|
|
14
|
+
import WebSocket from 'ws';
|
|
15
|
+
import { DEFAULT_CONFIG_PATH, loadWorkspacesConfig, getActiveWorkspace, setActiveWorkspace, saveWorkspacesConfig, sanitizeSlug, removeWorkspace, addWorkspace } from '@agentproto/runtime/workspaces-config';
|
|
16
|
+
import http from 'http';
|
|
17
|
+
import https from 'https';
|
|
6
18
|
|
|
7
19
|
/**
|
|
8
20
|
* @agentproto/cli v0.1.0-alpha
|
|
9
21
|
* The `agentproto` binary — install / run / serve AIP-45 agent CLIs.
|
|
10
22
|
*/
|
|
11
23
|
|
|
24
|
+
var FILE_VERSION = 1;
|
|
25
|
+
function credentialsPath() {
|
|
26
|
+
const base = process.env["AGENTPROTO_HOME"] ?? join(homedir(), ".agentproto");
|
|
27
|
+
return join(base, "credentials.json");
|
|
28
|
+
}
|
|
29
|
+
function normaliseHost(host) {
|
|
30
|
+
return host.replace(/\/+$/, "");
|
|
31
|
+
}
|
|
32
|
+
async function loadCredentials() {
|
|
33
|
+
const path = credentialsPath();
|
|
34
|
+
try {
|
|
35
|
+
const raw = await readFile(path, "utf8");
|
|
36
|
+
const parsed = JSON.parse(raw);
|
|
37
|
+
if (parsed.version !== FILE_VERSION) {
|
|
38
|
+
throw new Error(
|
|
39
|
+
`credentials.json: unknown version ${parsed.version}; expected ${FILE_VERSION}. Delete the file and re-run \`agentproto auth login\`.`
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
return parsed;
|
|
43
|
+
} catch (err) {
|
|
44
|
+
if (err.code === "ENOENT") {
|
|
45
|
+
return { version: FILE_VERSION, hosts: {} };
|
|
46
|
+
}
|
|
47
|
+
throw err;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
async function saveCredentials(file) {
|
|
51
|
+
const path = credentialsPath();
|
|
52
|
+
await mkdir(dirname(path), { recursive: true });
|
|
53
|
+
await writeFile(path, JSON.stringify(file, null, 2));
|
|
54
|
+
await chmod(path, 384).catch(() => {
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
async function readHost(host) {
|
|
58
|
+
const f = await loadCredentials();
|
|
59
|
+
return f.hosts[normaliseHost(host)] ?? null;
|
|
60
|
+
}
|
|
61
|
+
async function writeHost(host, cred) {
|
|
62
|
+
const f = await loadCredentials();
|
|
63
|
+
f.hosts[normaliseHost(host)] = cred;
|
|
64
|
+
await saveCredentials(f);
|
|
65
|
+
}
|
|
66
|
+
async function deleteHost(host) {
|
|
67
|
+
const f = await loadCredentials();
|
|
68
|
+
const key = normaliseHost(host);
|
|
69
|
+
const prev = f.hosts[key] ?? null;
|
|
70
|
+
if (prev) {
|
|
71
|
+
delete f.hosts[key];
|
|
72
|
+
if (Object.keys(f.hosts).length === 0) {
|
|
73
|
+
await unlink(credentialsPath()).catch(() => {
|
|
74
|
+
});
|
|
75
|
+
} else {
|
|
76
|
+
await saveCredentials(f);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return prev;
|
|
80
|
+
}
|
|
81
|
+
function isExpired(cred, gracePeriodMs = 3e4) {
|
|
82
|
+
const exp = Date.parse(cred.expiresAt);
|
|
83
|
+
if (!Number.isFinite(exp)) return false;
|
|
84
|
+
return exp - gracePeriodMs <= Date.now();
|
|
85
|
+
}
|
|
86
|
+
function formatExpiry(cred) {
|
|
87
|
+
const exp = new Date(cred.expiresAt);
|
|
88
|
+
if (Number.isNaN(exp.getTime())) return "unknown";
|
|
89
|
+
const ms = exp.getTime() - Date.now();
|
|
90
|
+
if (ms < 0) return `expired ${formatRelative(-ms)} ago`;
|
|
91
|
+
return `expires in ${formatRelative(ms)}`;
|
|
92
|
+
}
|
|
93
|
+
function formatRelative(ms) {
|
|
94
|
+
if (ms < 6e4) return `${Math.round(ms / 1e3)}s`;
|
|
95
|
+
if (ms < 36e5) return `${Math.round(ms / 6e4)}m`;
|
|
96
|
+
if (ms < 864e5) return `${Math.round(ms / 36e5)}h`;
|
|
97
|
+
return `${Math.round(ms / 864e5)}d`;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// src/commands/auth.ts
|
|
101
|
+
async function runAuth(args) {
|
|
102
|
+
const sub = args[0];
|
|
103
|
+
const rest = args.slice(1);
|
|
104
|
+
switch (sub) {
|
|
105
|
+
case "login":
|
|
106
|
+
return runAuthLogin(rest);
|
|
107
|
+
case "status":
|
|
108
|
+
return runAuthStatus(rest);
|
|
109
|
+
case "logout":
|
|
110
|
+
return runAuthLogout(rest);
|
|
111
|
+
case void 0:
|
|
112
|
+
case "--help":
|
|
113
|
+
case "-h":
|
|
114
|
+
process.stdout.write(USAGE);
|
|
115
|
+
return 0;
|
|
116
|
+
default:
|
|
117
|
+
process.stderr.write(
|
|
118
|
+
`agentproto auth: unknown subcommand '${sub}'.
|
|
119
|
+
|
|
120
|
+
${USAGE}`
|
|
121
|
+
);
|
|
122
|
+
return 2;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
var USAGE = `agentproto auth \u2014 host-binding tokens (RFC 8628 device flow)
|
|
126
|
+
|
|
127
|
+
Usage:
|
|
128
|
+
agentproto auth login [--host <url>] [--label <name>] [--no-browser]
|
|
129
|
+
agentproto auth status [--host <url>] [--json]
|
|
130
|
+
agentproto auth logout [--host <url>]
|
|
131
|
+
|
|
132
|
+
The default host is the one most recently logged into; on first use,
|
|
133
|
+
\`--host\` is required. Examples:
|
|
134
|
+
|
|
135
|
+
agentproto auth login --host wss://guilde.work
|
|
136
|
+
agentproto auth status
|
|
137
|
+
agentproto auth logout --host wss://guilde.work
|
|
138
|
+
`;
|
|
139
|
+
async function runAuthLogin(args) {
|
|
140
|
+
const { values } = parseArgs({
|
|
141
|
+
args: [...args],
|
|
142
|
+
strict: true,
|
|
143
|
+
options: {
|
|
144
|
+
host: { type: "string" },
|
|
145
|
+
label: { type: "string" },
|
|
146
|
+
"no-browser": { type: "boolean" },
|
|
147
|
+
scope: { type: "string" }
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
const host = values.host ?? await pickDefaultHost();
|
|
151
|
+
if (!host) {
|
|
152
|
+
process.stderr.write(
|
|
153
|
+
"agentproto auth login: pass --host <url> on first login (e.g. --host wss://guilde.work).\n"
|
|
154
|
+
);
|
|
155
|
+
return 2;
|
|
156
|
+
}
|
|
157
|
+
const label = values.label ?? `${userInfo().username}@${hostname()}`;
|
|
158
|
+
const requestedScope = values.scope ?? "tunnel:connect agent-cli:dispatch";
|
|
159
|
+
const httpHost = toHttp(host);
|
|
160
|
+
const discoveryUrl = `${httpHost}/.well-known/agentproto-host.json`;
|
|
161
|
+
process.stdout.write(`agentproto auth: discovering ${discoveryUrl}
|
|
162
|
+
`);
|
|
163
|
+
let discovery;
|
|
164
|
+
try {
|
|
165
|
+
discovery = await fetchJson(discoveryUrl);
|
|
166
|
+
} catch (err) {
|
|
167
|
+
process.stderr.write(
|
|
168
|
+
`agentproto auth: failed to fetch host metadata: ${err instanceof Error ? err.message : String(err)}
|
|
169
|
+
Hosts SHOULD expose /.well-known/agentproto-host.json with device_authorization_endpoint + token_endpoint.
|
|
170
|
+
`
|
|
171
|
+
);
|
|
172
|
+
return 1;
|
|
173
|
+
}
|
|
174
|
+
let deviceRes;
|
|
175
|
+
try {
|
|
176
|
+
deviceRes = await postForm(
|
|
177
|
+
discovery.device_authorization_endpoint,
|
|
178
|
+
{
|
|
179
|
+
client_id: discovery.client_id,
|
|
180
|
+
scope: requestedScope,
|
|
181
|
+
// Custom field; hosts that ignore it stay compliant. The
|
|
182
|
+
// approval UI uses it to render "approve <label>" so the user
|
|
183
|
+
// recognises the device they're authorising.
|
|
184
|
+
device_label: label
|
|
185
|
+
}
|
|
186
|
+
);
|
|
187
|
+
} catch (err) {
|
|
188
|
+
process.stderr.write(
|
|
189
|
+
`agentproto auth: device authorization request failed: ${err instanceof Error ? err.message : String(err)}
|
|
190
|
+
`
|
|
191
|
+
);
|
|
192
|
+
return 1;
|
|
193
|
+
}
|
|
194
|
+
const verifyUrl = deviceRes.verification_uri_complete ?? deviceRes.verification_uri;
|
|
195
|
+
process.stdout.write(
|
|
196
|
+
`
|
|
197
|
+
agentproto auth: open
|
|
198
|
+
${verifyUrl}
|
|
199
|
+
and enter code ${deviceRes.user_code}
|
|
12
200
|
|
|
13
|
-
|
|
201
|
+
`
|
|
202
|
+
);
|
|
203
|
+
if (!values["no-browser"]) {
|
|
204
|
+
void openBrowser(verifyUrl);
|
|
205
|
+
}
|
|
206
|
+
process.stdout.write(
|
|
207
|
+
`agentproto auth: waiting for approval (expires in ${formatDuration(deviceRes.expires_in * 1e3)})\u2026
|
|
208
|
+
`
|
|
209
|
+
);
|
|
210
|
+
const interval = Math.max(1, deviceRes.interval ?? 5);
|
|
211
|
+
const deadline = Date.now() + deviceRes.expires_in * 1e3;
|
|
212
|
+
let pollIntervalMs = interval * 1e3;
|
|
213
|
+
let token = null;
|
|
214
|
+
while (Date.now() < deadline) {
|
|
215
|
+
await sleep(pollIntervalMs);
|
|
216
|
+
let res;
|
|
217
|
+
try {
|
|
218
|
+
res = await postForm(
|
|
219
|
+
discovery.token_endpoint,
|
|
220
|
+
{
|
|
221
|
+
grant_type: "urn:ietf:params:oauth:grant-type:device_code",
|
|
222
|
+
device_code: deviceRes.device_code,
|
|
223
|
+
client_id: discovery.client_id
|
|
224
|
+
}
|
|
225
|
+
);
|
|
226
|
+
} catch (err) {
|
|
227
|
+
process.stderr.write(
|
|
228
|
+
`agentproto auth: token poll failed: ${err instanceof Error ? err.message : String(err)}
|
|
229
|
+
`
|
|
230
|
+
);
|
|
231
|
+
return 1;
|
|
232
|
+
}
|
|
233
|
+
if ("access_token" in res) {
|
|
234
|
+
token = res;
|
|
235
|
+
break;
|
|
236
|
+
}
|
|
237
|
+
if (res.error === "authorization_pending") continue;
|
|
238
|
+
if (res.error === "slow_down") {
|
|
239
|
+
pollIntervalMs += 5e3;
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
if (res.error === "access_denied") {
|
|
243
|
+
process.stderr.write(`agentproto auth: approval denied. Aborting.
|
|
244
|
+
`);
|
|
245
|
+
return 1;
|
|
246
|
+
}
|
|
247
|
+
if (res.error === "expired_token") {
|
|
248
|
+
process.stderr.write(
|
|
249
|
+
`agentproto auth: device code expired before approval. Re-run \`agentproto auth login\`.
|
|
250
|
+
`
|
|
251
|
+
);
|
|
252
|
+
return 1;
|
|
253
|
+
}
|
|
254
|
+
process.stderr.write(
|
|
255
|
+
`agentproto auth: token endpoint returned '${res.error}'${res.error_description ? `: ${res.error_description}` : ""}
|
|
256
|
+
`
|
|
257
|
+
);
|
|
258
|
+
return 1;
|
|
259
|
+
}
|
|
260
|
+
if (!token) {
|
|
261
|
+
process.stderr.write(
|
|
262
|
+
`agentproto auth: timed out waiting for approval. Re-run \`agentproto auth login\`.
|
|
263
|
+
`
|
|
264
|
+
);
|
|
265
|
+
return 1;
|
|
266
|
+
}
|
|
267
|
+
const cred = {
|
|
268
|
+
token: token.access_token,
|
|
269
|
+
tokenType: "Bearer",
|
|
270
|
+
expiresAt: new Date(Date.now() + token.expires_in * 1e3).toISOString(),
|
|
271
|
+
obtainedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
272
|
+
...token.refresh_token ? { refreshToken: token.refresh_token } : {},
|
|
273
|
+
...token.scope ? { scope: token.scope } : {},
|
|
274
|
+
...token.subject ? { subject: token.subject } : {},
|
|
275
|
+
...token.revocation_id ? { revocationId: token.revocation_id } : {},
|
|
276
|
+
deviceLabel: label
|
|
277
|
+
};
|
|
278
|
+
await writeHost(host, cred);
|
|
279
|
+
process.stdout.write(
|
|
280
|
+
`agentproto auth: \u2713 logged in to ${normaliseHost(host)}
|
|
281
|
+
saved to ${credentialsPath()} (mode 0600)
|
|
282
|
+
${formatExpiry(cred)}${cred.subject ? `, subject ${cred.subject}` : ""}
|
|
283
|
+
`
|
|
284
|
+
);
|
|
285
|
+
return 0;
|
|
286
|
+
}
|
|
287
|
+
async function runAuthStatus(args) {
|
|
288
|
+
const { values } = parseArgs({
|
|
289
|
+
args: [...args],
|
|
290
|
+
strict: true,
|
|
291
|
+
options: {
|
|
292
|
+
host: { type: "string" },
|
|
293
|
+
json: { type: "boolean" }
|
|
294
|
+
}
|
|
295
|
+
});
|
|
296
|
+
const file = await loadCredentials();
|
|
297
|
+
const entries = Object.entries(file.hosts);
|
|
298
|
+
if (entries.length === 0) {
|
|
299
|
+
if (values.json) {
|
|
300
|
+
process.stdout.write(`{"hosts": []}
|
|
301
|
+
`);
|
|
302
|
+
} else {
|
|
303
|
+
process.stdout.write(
|
|
304
|
+
`agentproto auth: no credentials. Try: agentproto auth login --host wss://guilde.work
|
|
305
|
+
`
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
return 0;
|
|
309
|
+
}
|
|
310
|
+
const filtered = values.host ? entries.filter(([h]) => h === normaliseHost(values.host)) : entries;
|
|
311
|
+
if (values.json) {
|
|
312
|
+
process.stdout.write(
|
|
313
|
+
JSON.stringify(
|
|
314
|
+
{
|
|
315
|
+
hosts: filtered.map(([host, c]) => ({
|
|
316
|
+
host,
|
|
317
|
+
subject: c.subject ?? null,
|
|
318
|
+
scope: c.scope ?? null,
|
|
319
|
+
obtainedAt: c.obtainedAt,
|
|
320
|
+
expiresAt: c.expiresAt,
|
|
321
|
+
expired: isExpired(c),
|
|
322
|
+
deviceLabel: c.deviceLabel ?? null
|
|
323
|
+
}))
|
|
324
|
+
},
|
|
325
|
+
null,
|
|
326
|
+
2
|
|
327
|
+
) + "\n"
|
|
328
|
+
);
|
|
329
|
+
return 0;
|
|
330
|
+
}
|
|
331
|
+
for (const [host, c] of filtered) {
|
|
332
|
+
const status = isExpired(c) ? "\u2717 EXPIRED" : "\u2713 active";
|
|
333
|
+
process.stdout.write(
|
|
334
|
+
`${status} ${host}
|
|
335
|
+
subject: ${c.subject ?? "(none)"}
|
|
336
|
+
scope: ${c.scope ?? "(none)"}
|
|
337
|
+
label: ${c.deviceLabel ?? "(none)"}
|
|
338
|
+
${formatExpiry(c)} (obtained ${c.obtainedAt})
|
|
339
|
+
`
|
|
340
|
+
);
|
|
341
|
+
}
|
|
342
|
+
return 0;
|
|
343
|
+
}
|
|
344
|
+
async function runAuthLogout(args) {
|
|
345
|
+
const { values } = parseArgs({
|
|
346
|
+
args: [...args],
|
|
347
|
+
strict: true,
|
|
348
|
+
options: { host: { type: "string" } }
|
|
349
|
+
});
|
|
350
|
+
const host = values.host ?? await pickDefaultHost();
|
|
351
|
+
if (!host) {
|
|
352
|
+
process.stderr.write(
|
|
353
|
+
`agentproto auth logout: no credentials to revoke. Pass --host <url> if you stored one.
|
|
354
|
+
`
|
|
355
|
+
);
|
|
356
|
+
return 0;
|
|
357
|
+
}
|
|
358
|
+
const prev = await readHost(host);
|
|
359
|
+
if (!prev) {
|
|
360
|
+
process.stderr.write(
|
|
361
|
+
`agentproto auth logout: no credential found for ${host}.
|
|
362
|
+
`
|
|
363
|
+
);
|
|
364
|
+
return 0;
|
|
365
|
+
}
|
|
366
|
+
let serverRevoked = "skipped";
|
|
367
|
+
try {
|
|
368
|
+
const httpHost = toHttp(host);
|
|
369
|
+
const discovery = await fetchJson(
|
|
370
|
+
`${httpHost}/.well-known/agentproto-host.json`
|
|
371
|
+
);
|
|
372
|
+
if (discovery.revocation_endpoint) {
|
|
373
|
+
const params = {
|
|
374
|
+
client_id: discovery.client_id,
|
|
375
|
+
token: prev.token
|
|
376
|
+
};
|
|
377
|
+
if (prev.revocationId) params["revocation_id"] = prev.revocationId;
|
|
378
|
+
await postForm(discovery.revocation_endpoint, params);
|
|
379
|
+
serverRevoked = "ok";
|
|
380
|
+
}
|
|
381
|
+
} catch {
|
|
382
|
+
serverRevoked = "failed";
|
|
383
|
+
}
|
|
384
|
+
await deleteHost(host);
|
|
385
|
+
const note = serverRevoked === "ok" ? " (server revoked)" : serverRevoked === "failed" ? " (server revocation failed; local copy still removed)" : " (no revocation endpoint advertised; local copy removed)";
|
|
386
|
+
process.stdout.write(`agentproto auth: \u2713 logged out of ${host}${note}
|
|
387
|
+
`);
|
|
388
|
+
return 0;
|
|
389
|
+
}
|
|
390
|
+
async function pickDefaultHost() {
|
|
391
|
+
const f = await loadCredentials();
|
|
392
|
+
const keys = Object.keys(f.hosts);
|
|
393
|
+
if (keys.length === 0) return null;
|
|
394
|
+
let best = null;
|
|
395
|
+
for (const [host, cred] of Object.entries(f.hosts)) {
|
|
396
|
+
const t = Date.parse(cred.obtainedAt);
|
|
397
|
+
if (!Number.isFinite(t)) continue;
|
|
398
|
+
if (!best || t > best.obtainedAt) best = { host, obtainedAt: t };
|
|
399
|
+
}
|
|
400
|
+
return best?.host ?? keys[0] ?? null;
|
|
401
|
+
}
|
|
402
|
+
function toHttp(host) {
|
|
403
|
+
const trimmed = host.replace(/\/+$/, "");
|
|
404
|
+
if (trimmed.startsWith("wss://")) return "https://" + trimmed.slice(6);
|
|
405
|
+
if (trimmed.startsWith("ws://")) return "http://" + trimmed.slice(5);
|
|
406
|
+
return trimmed;
|
|
407
|
+
}
|
|
408
|
+
async function fetchJson(url) {
|
|
409
|
+
const res = await fetch(url, { headers: { Accept: "application/json" } });
|
|
410
|
+
if (!res.ok) {
|
|
411
|
+
throw new Error(`GET ${url} \u2192 ${res.status} ${res.statusText}`);
|
|
412
|
+
}
|
|
413
|
+
return await res.json();
|
|
414
|
+
}
|
|
415
|
+
async function postForm(url, body) {
|
|
416
|
+
const params = new URLSearchParams(body);
|
|
417
|
+
const res = await fetch(url, {
|
|
418
|
+
method: "POST",
|
|
419
|
+
headers: {
|
|
420
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
421
|
+
Accept: "application/json"
|
|
422
|
+
},
|
|
423
|
+
body: params.toString()
|
|
424
|
+
});
|
|
425
|
+
if (!res.ok && res.status !== 400) {
|
|
426
|
+
const text = await res.text().catch(() => "");
|
|
427
|
+
throw new Error(
|
|
428
|
+
`POST ${url} \u2192 ${res.status} ${res.statusText}${text ? ": " + text.slice(0, 200) : ""}`
|
|
429
|
+
);
|
|
430
|
+
}
|
|
431
|
+
return await res.json();
|
|
432
|
+
}
|
|
433
|
+
function sleep(ms) {
|
|
434
|
+
return new Promise((resolve3) => setTimeout(resolve3, ms));
|
|
435
|
+
}
|
|
436
|
+
function formatDuration(ms) {
|
|
437
|
+
if (ms < 6e4) return `${Math.round(ms / 1e3)}s`;
|
|
438
|
+
if (ms < 36e5) return `${Math.round(ms / 6e4)}m`;
|
|
439
|
+
return `${Math.round(ms / 36e5)}h`;
|
|
440
|
+
}
|
|
441
|
+
function openBrowser(url) {
|
|
442
|
+
const p = platform();
|
|
443
|
+
const cmd = p === "darwin" ? "open" : p === "win32" ? "cmd" : "xdg-open";
|
|
444
|
+
const args = p === "win32" ? ["/c", "start", url] : [url];
|
|
445
|
+
return new Promise((resolve3) => {
|
|
446
|
+
try {
|
|
447
|
+
const child = spawn(cmd, args, { stdio: "ignore", detached: true });
|
|
448
|
+
child.once("error", () => resolve3());
|
|
449
|
+
child.once("spawn", () => {
|
|
450
|
+
child.unref();
|
|
451
|
+
resolve3();
|
|
452
|
+
});
|
|
453
|
+
} catch {
|
|
454
|
+
resolve3();
|
|
455
|
+
}
|
|
456
|
+
});
|
|
457
|
+
}
|
|
14
458
|
var slugToCamel = (slug) => slug.replace(/-([a-z0-9])/g, (_, c) => c.toUpperCase());
|
|
15
459
|
async function resolveAdapter(slug) {
|
|
16
460
|
if (!/^[a-z][a-z0-9-]*$/.test(slug)) {
|
|
@@ -38,6 +482,460 @@ async function resolveAdapter(slug) {
|
|
|
38
482
|
}
|
|
39
483
|
return { slug, handle: candidate, source: "npm", packageName };
|
|
40
484
|
}
|
|
485
|
+
async function listInstalledAdapters(opts) {
|
|
486
|
+
const roots = await collectAgentprotoNamespaceRoots(opts?.searchRoot);
|
|
487
|
+
const seen = /* @__PURE__ */ new Set();
|
|
488
|
+
const out = [];
|
|
489
|
+
for (const root of roots) {
|
|
490
|
+
let entries = [];
|
|
491
|
+
try {
|
|
492
|
+
entries = await promises.readdir(root);
|
|
493
|
+
} catch {
|
|
494
|
+
continue;
|
|
495
|
+
}
|
|
496
|
+
for (const entry of entries) {
|
|
497
|
+
if (!entry.startsWith("adapter-")) continue;
|
|
498
|
+
const slug = entry.slice("adapter-".length);
|
|
499
|
+
if (seen.has(slug)) continue;
|
|
500
|
+
seen.add(slug);
|
|
501
|
+
try {
|
|
502
|
+
const resolved = await resolveAdapter(slug);
|
|
503
|
+
const handle = resolved.handle;
|
|
504
|
+
const info = {
|
|
505
|
+
slug,
|
|
506
|
+
name: typeof handle.name === "string" ? handle.name : slug,
|
|
507
|
+
version: typeof handle.version === "string" ? handle.version : "?",
|
|
508
|
+
description: typeof handle.description === "string" ? handle.description : "",
|
|
509
|
+
protocol: typeof handle.protocol === "string" ? handle.protocol : "unknown",
|
|
510
|
+
streaming: !!handle.capabilities?.streaming,
|
|
511
|
+
packageName: resolved.packageName ?? `@agentproto/adapter-${slug}`
|
|
512
|
+
};
|
|
513
|
+
out.push(info);
|
|
514
|
+
} catch (err) {
|
|
515
|
+
console.warn(
|
|
516
|
+
`[agentproto/cli] listInstalledAdapters: skipping broken adapter '${slug}': ${err instanceof Error ? err.message : String(err)}`
|
|
517
|
+
);
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
return out.sort((a, b) => a.slug.localeCompare(b.slug));
|
|
522
|
+
}
|
|
523
|
+
async function collectAgentprotoNamespaceRoots(start) {
|
|
524
|
+
const roots = [];
|
|
525
|
+
let cur = start ?? process.cwd();
|
|
526
|
+
const candidatesAt = (dir) => [
|
|
527
|
+
resolve(dir, "node_modules", "@agentproto"),
|
|
528
|
+
resolve(dir, "node_modules", ".pnpm", "node_modules", "@agentproto")
|
|
529
|
+
];
|
|
530
|
+
for (let depth = 0; depth < 16; depth++) {
|
|
531
|
+
for (const candidate of candidatesAt(cur)) {
|
|
532
|
+
try {
|
|
533
|
+
const stat = await promises.stat(candidate);
|
|
534
|
+
if (stat.isDirectory()) roots.push(candidate);
|
|
535
|
+
} catch {
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
const parent = resolve(cur, "..");
|
|
539
|
+
if (parent === cur) break;
|
|
540
|
+
cur = parent;
|
|
541
|
+
}
|
|
542
|
+
return roots;
|
|
543
|
+
}
|
|
544
|
+
async function runSetup(opts) {
|
|
545
|
+
const steps = opts.handle.setup ?? [];
|
|
546
|
+
if (steps.length === 0) {
|
|
547
|
+
process.stdout.write(`agentproto setup: no setup steps for '${opts.slug}'.
|
|
548
|
+
`);
|
|
549
|
+
return 0;
|
|
550
|
+
}
|
|
551
|
+
const ledgerPath = ledgerPathFor(opts.slug);
|
|
552
|
+
const ledger = await loadLedger(ledgerPath, opts.slug);
|
|
553
|
+
const onlySet = opts.only && opts.only.length > 0 ? new Set(opts.only) : null;
|
|
554
|
+
for (const [i, step] of steps.entries()) {
|
|
555
|
+
if (onlySet && !onlySet.has(step.id)) continue;
|
|
556
|
+
const idx = `[${i + 1}/${steps.length}]`;
|
|
557
|
+
const head = `${idx} ${step.kind}/${step.id}`;
|
|
558
|
+
const prev = ledger.steps[step.id];
|
|
559
|
+
if (!opts.force && prev) {
|
|
560
|
+
process.stdout.write(
|
|
561
|
+
`${head} \u2713 already completed (${prev.completedAt}${prev.skippedViaSkipIf ? ", via skip_if" : ""}). Pass --force to re-run.
|
|
562
|
+
`
|
|
563
|
+
);
|
|
564
|
+
continue;
|
|
565
|
+
}
|
|
566
|
+
if (step.skip_if && !opts.force) {
|
|
567
|
+
const ok = await checkSkipIf(step.skip_if);
|
|
568
|
+
if (ok) {
|
|
569
|
+
process.stdout.write(`${head} \u2713 skip_if matched \u2014 skipping.
|
|
570
|
+
`);
|
|
571
|
+
ledger.steps[step.id] = {
|
|
572
|
+
stepId: step.id,
|
|
573
|
+
kind: step.kind,
|
|
574
|
+
completedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
575
|
+
skippedViaSkipIf: true
|
|
576
|
+
};
|
|
577
|
+
continue;
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
if (opts.dryRun) {
|
|
581
|
+
process.stdout.write(`${head} (dry-run) \u2014 would execute.
|
|
582
|
+
`);
|
|
583
|
+
continue;
|
|
584
|
+
}
|
|
585
|
+
let entry = null;
|
|
586
|
+
try {
|
|
587
|
+
switch (step.kind) {
|
|
588
|
+
case "cmd":
|
|
589
|
+
entry = await runCmdStep(step, ledger, head);
|
|
590
|
+
break;
|
|
591
|
+
case "prompt":
|
|
592
|
+
entry = await runPromptStep(step, ledger, head);
|
|
593
|
+
break;
|
|
594
|
+
case "external":
|
|
595
|
+
entry = await runExternalStep(step, ledger, head);
|
|
596
|
+
break;
|
|
597
|
+
case "oauth":
|
|
598
|
+
process.stderr.write(
|
|
599
|
+
`${head} \u2717 kind=oauth is not yet implemented in the local CLI host. Run the OAuth flow on the host that ships a SECRETS.md driver, then re-invoke.
|
|
600
|
+
`
|
|
601
|
+
);
|
|
602
|
+
return 1;
|
|
603
|
+
}
|
|
604
|
+
} catch (err) {
|
|
605
|
+
process.stderr.write(
|
|
606
|
+
`${head} \u2717 failed: ${err instanceof Error ? err.message : String(err)}
|
|
607
|
+
`
|
|
608
|
+
);
|
|
609
|
+
await saveLedger(ledgerPath, ledger);
|
|
610
|
+
return 1;
|
|
611
|
+
}
|
|
612
|
+
if (entry) {
|
|
613
|
+
ledger.steps[step.id] = entry;
|
|
614
|
+
await saveLedger(ledgerPath, ledger);
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
process.stdout.write(`agentproto: setup for '${opts.slug}' complete.
|
|
618
|
+
`);
|
|
619
|
+
return 0;
|
|
620
|
+
}
|
|
621
|
+
async function runSetupCommand(args) {
|
|
622
|
+
const { values, positionals } = parseArgs({
|
|
623
|
+
args: [...args],
|
|
624
|
+
allowPositionals: true,
|
|
625
|
+
strict: true,
|
|
626
|
+
options: {
|
|
627
|
+
force: { type: "boolean", short: "f" },
|
|
628
|
+
"dry-run": { type: "boolean" },
|
|
629
|
+
only: { type: "string", multiple: true }
|
|
630
|
+
}
|
|
631
|
+
});
|
|
632
|
+
const slug = positionals[0];
|
|
633
|
+
if (!slug) {
|
|
634
|
+
process.stderr.write(
|
|
635
|
+
"agentproto setup: missing adapter slug. Try: agentproto setup openclaw\n"
|
|
636
|
+
);
|
|
637
|
+
return 2;
|
|
638
|
+
}
|
|
639
|
+
const adapter = await resolveAdapter(slug);
|
|
640
|
+
return runSetup({
|
|
641
|
+
slug,
|
|
642
|
+
handle: adapter.handle,
|
|
643
|
+
force: values.force ?? false,
|
|
644
|
+
dryRun: values["dry-run"] ?? false,
|
|
645
|
+
...values.only ? { only: values.only } : {}
|
|
646
|
+
});
|
|
647
|
+
}
|
|
648
|
+
async function runCmdStep(step, ledger, head) {
|
|
649
|
+
if (step.description) process.stdout.write(`${head} ${step.description}
|
|
650
|
+
`);
|
|
651
|
+
process.stdout.write(`${head} $ ${step.cmd}
|
|
652
|
+
`);
|
|
653
|
+
const captured = await runShellCapturing(step.cmd, {
|
|
654
|
+
timeoutMs: step.timeout_ms ?? 6e4,
|
|
655
|
+
interactive: step.interactive ?? false
|
|
656
|
+
});
|
|
657
|
+
if (captured.exitCode !== 0) {
|
|
658
|
+
throw new Error(
|
|
659
|
+
`cmd exited ${captured.exitCode}${captured.stderr ? ": " + truncate(captured.stderr, 400) : ""}`
|
|
660
|
+
);
|
|
661
|
+
}
|
|
662
|
+
let persistedTo;
|
|
663
|
+
if (step.persist) {
|
|
664
|
+
const value = captured.stdout.trim();
|
|
665
|
+
persistedTo = await applyPersist(step.persist, value, ledger);
|
|
666
|
+
}
|
|
667
|
+
return {
|
|
668
|
+
stepId: step.id,
|
|
669
|
+
kind: step.kind,
|
|
670
|
+
completedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
671
|
+
...persistedTo ? { persistedTo } : {}
|
|
672
|
+
};
|
|
673
|
+
}
|
|
674
|
+
async function runPromptStep(step, ledger, head) {
|
|
675
|
+
if (step.description) process.stdout.write(`${head} ${step.description}
|
|
676
|
+
`);
|
|
677
|
+
const type = step.type ?? "text";
|
|
678
|
+
let value;
|
|
679
|
+
switch (type) {
|
|
680
|
+
case "text":
|
|
681
|
+
case "secret":
|
|
682
|
+
value = await promptString(step.prompt, {
|
|
683
|
+
defaultValue: step.default,
|
|
684
|
+
masked: type === "secret"
|
|
685
|
+
});
|
|
686
|
+
break;
|
|
687
|
+
case "boolean":
|
|
688
|
+
value = await promptBoolean(step.prompt, step.default === "true") ? "true" : "false";
|
|
689
|
+
break;
|
|
690
|
+
case "select": {
|
|
691
|
+
const options = await resolveSelectOptions(step.options);
|
|
692
|
+
if (options.length === 0) {
|
|
693
|
+
throw new Error("select prompt has no options to choose from");
|
|
694
|
+
}
|
|
695
|
+
value = await promptSelect(step.prompt, options, step.default);
|
|
696
|
+
break;
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
let persistedTo;
|
|
700
|
+
if (step.persist) {
|
|
701
|
+
persistedTo = await applyPersist(step.persist, value, ledger);
|
|
702
|
+
}
|
|
703
|
+
return {
|
|
704
|
+
stepId: step.id,
|
|
705
|
+
kind: step.kind,
|
|
706
|
+
completedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
707
|
+
...persistedTo ? { persistedTo } : {}
|
|
708
|
+
};
|
|
709
|
+
}
|
|
710
|
+
async function runExternalStep(step, ledger, head) {
|
|
711
|
+
if (step.description) process.stdout.write(`${head} ${step.description}
|
|
712
|
+
`);
|
|
713
|
+
process.stdout.write(`${head} opening ${step.url}
|
|
714
|
+
`);
|
|
715
|
+
const opener = platform() === "darwin" ? "open" : platform() === "win32" ? "cmd" : "xdg-open";
|
|
716
|
+
const args = platform() === "win32" ? ["/c", "start", step.url] : [step.url];
|
|
717
|
+
await new Promise((resolve3) => {
|
|
718
|
+
const child = spawn(opener, args, { stdio: "ignore", detached: true });
|
|
719
|
+
child.once("error", () => resolve3());
|
|
720
|
+
child.once("spawn", () => resolve3());
|
|
721
|
+
});
|
|
722
|
+
let value = "";
|
|
723
|
+
if (step.callback?.param) {
|
|
724
|
+
value = await promptString(
|
|
725
|
+
`Paste the value of '${step.callback.param}' from the redirect`,
|
|
726
|
+
{ masked: false }
|
|
727
|
+
);
|
|
728
|
+
} else {
|
|
729
|
+
process.stdout.write(`${head} done \u2014 press Enter when finished.
|
|
730
|
+
`);
|
|
731
|
+
await promptString("(press Enter to continue)", { masked: false });
|
|
732
|
+
}
|
|
733
|
+
let persistedTo;
|
|
734
|
+
if (step.persist && value) {
|
|
735
|
+
persistedTo = await applyPersist(step.persist, value, ledger);
|
|
736
|
+
}
|
|
737
|
+
return {
|
|
738
|
+
stepId: step.id,
|
|
739
|
+
kind: step.kind,
|
|
740
|
+
completedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
741
|
+
...persistedTo ? { persistedTo } : {}
|
|
742
|
+
};
|
|
743
|
+
}
|
|
744
|
+
async function checkSkipIf(skip) {
|
|
745
|
+
const expectedCode = skip.exit_code ?? 0;
|
|
746
|
+
const captured = await runShellCapturing(skip.cmd, {
|
|
747
|
+
timeoutMs: skip.timeout_ms ?? 5e3,
|
|
748
|
+
interactive: false
|
|
749
|
+
});
|
|
750
|
+
return captured.exitCode === expectedCode;
|
|
751
|
+
}
|
|
752
|
+
async function applyPersist(persist, value, ledger) {
|
|
753
|
+
if ("env" in persist && persist.env) {
|
|
754
|
+
if (!ledger.envValues) ledger.envValues = {};
|
|
755
|
+
ledger.envValues[persist.env] = value;
|
|
756
|
+
return `env:${persist.env}`;
|
|
757
|
+
}
|
|
758
|
+
if ("secret_slug" in persist && persist.secret_slug) {
|
|
759
|
+
process.stdout.write(
|
|
760
|
+
`agentproto setup: store the prompted value in your secrets backend under slug '${persist.secret_slug}' (value not echoed; ledger records the slot only).
|
|
761
|
+
`
|
|
762
|
+
);
|
|
763
|
+
return `secret:${persist.secret_slug}`;
|
|
764
|
+
}
|
|
765
|
+
if ("cmd" in persist && persist.cmd) {
|
|
766
|
+
const cmd = persist.cmd.replace(/\$\{value\}/g, shellEscape(value));
|
|
767
|
+
const captured = await runShellCapturing(cmd, {
|
|
768
|
+
timeoutMs: 6e4,
|
|
769
|
+
interactive: false
|
|
770
|
+
});
|
|
771
|
+
if (captured.exitCode !== 0) {
|
|
772
|
+
throw new Error(`persist cmd exited ${captured.exitCode}`);
|
|
773
|
+
}
|
|
774
|
+
return "cmd";
|
|
775
|
+
}
|
|
776
|
+
throw new Error("persist block must declare exactly one of env / secret_slug / cmd");
|
|
777
|
+
}
|
|
778
|
+
async function promptString(question, opts) {
|
|
779
|
+
const isTty = process.stdin.isTTY ?? false;
|
|
780
|
+
const suffix = opts.defaultValue ? ` [${opts.defaultValue}]` : "";
|
|
781
|
+
const prompt = `${question}${suffix}: `;
|
|
782
|
+
if (opts.masked && !isTty) {
|
|
783
|
+
process.stdout.write(
|
|
784
|
+
`agentproto: stdin is not a TTY \u2014 masked input falls back to plain. Avoid piping secrets in non-interactive mode.
|
|
785
|
+
`
|
|
786
|
+
);
|
|
787
|
+
}
|
|
788
|
+
const rl = createInterface({ input: stdin, output: stdout, terminal: isTty });
|
|
789
|
+
if (opts.masked && isTty) {
|
|
790
|
+
process.stdin.setRawMode?.(true);
|
|
791
|
+
let buf = "";
|
|
792
|
+
process.stdout.write(prompt);
|
|
793
|
+
return new Promise((resolve3) => {
|
|
794
|
+
const onData = (chunk) => {
|
|
795
|
+
for (const code of chunk) {
|
|
796
|
+
if (code === 13 || code === 10) {
|
|
797
|
+
process.stdin.off("data", onData);
|
|
798
|
+
process.stdin.setRawMode?.(false);
|
|
799
|
+
process.stdout.write("\n");
|
|
800
|
+
rl.close();
|
|
801
|
+
resolve3(buf || opts.defaultValue || "");
|
|
802
|
+
return;
|
|
803
|
+
}
|
|
804
|
+
if (code === 3) {
|
|
805
|
+
process.stdin.off("data", onData);
|
|
806
|
+
process.stdin.setRawMode?.(false);
|
|
807
|
+
rl.close();
|
|
808
|
+
process.exit(130);
|
|
809
|
+
}
|
|
810
|
+
if (code === 127 || code === 8) {
|
|
811
|
+
buf = buf.slice(0, -1);
|
|
812
|
+
continue;
|
|
813
|
+
}
|
|
814
|
+
buf += String.fromCharCode(code);
|
|
815
|
+
process.stdout.write("\u2022");
|
|
816
|
+
}
|
|
817
|
+
};
|
|
818
|
+
process.stdin.on("data", onData);
|
|
819
|
+
});
|
|
820
|
+
}
|
|
821
|
+
try {
|
|
822
|
+
const ans = (await rl.question(prompt)).trim();
|
|
823
|
+
return ans || opts.defaultValue || "";
|
|
824
|
+
} finally {
|
|
825
|
+
rl.close();
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
async function promptBoolean(question, defaultYes) {
|
|
829
|
+
const def = defaultYes ? "Y/n" : "y/N";
|
|
830
|
+
const ans = await promptString(`${question} [${def}]`, { masked: false });
|
|
831
|
+
if (!ans) return defaultYes;
|
|
832
|
+
return /^y(es)?$/i.test(ans);
|
|
833
|
+
}
|
|
834
|
+
async function promptSelect(question, options, defaultValue) {
|
|
835
|
+
process.stdout.write(`${question}
|
|
836
|
+
`);
|
|
837
|
+
for (const [i, o] of options.entries()) {
|
|
838
|
+
const star = defaultValue && o.value === defaultValue ? "*" : i === 0 ? " " : " ";
|
|
839
|
+
process.stdout.write(
|
|
840
|
+
` ${star} ${i + 1}) ${o.label ?? o.value}${o.label ? ` (${o.value})` : ""}
|
|
841
|
+
`
|
|
842
|
+
);
|
|
843
|
+
}
|
|
844
|
+
const ans = await promptString(
|
|
845
|
+
`Choose 1-${options.length}`,
|
|
846
|
+
defaultValue ? { defaultValue } : { masked: false }
|
|
847
|
+
);
|
|
848
|
+
const asNum = Number.parseInt(ans, 10);
|
|
849
|
+
if (Number.isFinite(asNum) && asNum >= 1 && asNum <= options.length) {
|
|
850
|
+
return options[asNum - 1].value;
|
|
851
|
+
}
|
|
852
|
+
const exact = options.find((o) => o.value === ans);
|
|
853
|
+
if (exact) return exact.value;
|
|
854
|
+
if (defaultValue && options.some((o) => o.value === defaultValue)) {
|
|
855
|
+
return defaultValue;
|
|
856
|
+
}
|
|
857
|
+
throw new Error(
|
|
858
|
+
`invalid selection '${ans}'; expected one of ${options.map((o) => o.value).join(", ")}`
|
|
859
|
+
);
|
|
860
|
+
}
|
|
861
|
+
async function resolveSelectOptions(options) {
|
|
862
|
+
if (!options) return [];
|
|
863
|
+
if (Array.isArray(options)) {
|
|
864
|
+
return options.map((v) => ({ value: v }));
|
|
865
|
+
}
|
|
866
|
+
const captured = await runShellCapturing(options.cmd, {
|
|
867
|
+
timeoutMs: options.timeout_ms ?? 3e4,
|
|
868
|
+
interactive: false
|
|
869
|
+
});
|
|
870
|
+
if (captured.exitCode !== 0) {
|
|
871
|
+
throw new Error(
|
|
872
|
+
`select options cmd exited ${captured.exitCode}: ${truncate(captured.stderr, 400)}`
|
|
873
|
+
);
|
|
874
|
+
}
|
|
875
|
+
return captured.stdout.split("\n").map((l) => l.trim()).filter(Boolean).map((line) => {
|
|
876
|
+
const [value, label] = line.split(" ");
|
|
877
|
+
const out = { value };
|
|
878
|
+
if (label) out.label = label;
|
|
879
|
+
return out;
|
|
880
|
+
});
|
|
881
|
+
}
|
|
882
|
+
async function runShellCapturing(cmd, opts) {
|
|
883
|
+
return new Promise((resolve3) => {
|
|
884
|
+
const child = spawn("bash", ["-lc", cmd], {
|
|
885
|
+
stdio: opts.interactive ? "inherit" : ["ignore", "pipe", "pipe"]
|
|
886
|
+
});
|
|
887
|
+
let stdout = "";
|
|
888
|
+
let stderr = "";
|
|
889
|
+
child.stdout?.on("data", (c) => {
|
|
890
|
+
stdout += c.toString("utf8");
|
|
891
|
+
});
|
|
892
|
+
child.stderr?.on("data", (c) => {
|
|
893
|
+
stderr += c.toString("utf8");
|
|
894
|
+
});
|
|
895
|
+
const timer = setTimeout(() => {
|
|
896
|
+
child.kill("SIGTERM");
|
|
897
|
+
setTimeout(() => child.kill("SIGKILL"), 1e3);
|
|
898
|
+
}, opts.timeoutMs);
|
|
899
|
+
child.once("error", () => {
|
|
900
|
+
clearTimeout(timer);
|
|
901
|
+
resolve3({ exitCode: 127, stdout, stderr: stderr || "spawn error" });
|
|
902
|
+
});
|
|
903
|
+
child.once("exit", (code) => {
|
|
904
|
+
clearTimeout(timer);
|
|
905
|
+
resolve3({ exitCode: code ?? 0, stdout, stderr });
|
|
906
|
+
});
|
|
907
|
+
});
|
|
908
|
+
}
|
|
909
|
+
function shellEscape(s) {
|
|
910
|
+
return `'${s.replace(/'/g, `'"'"'`)}'`;
|
|
911
|
+
}
|
|
912
|
+
function truncate(s, max) {
|
|
913
|
+
return s.length <= max ? s : s.slice(0, max - 3) + "\u2026";
|
|
914
|
+
}
|
|
915
|
+
function ledgerPathFor(slug) {
|
|
916
|
+
const base = process.env["AGENTPROTO_HOME"] ?? join(homedir(), ".agentproto");
|
|
917
|
+
return join(base, "setup", `${slug}.json`);
|
|
918
|
+
}
|
|
919
|
+
async function loadLedger(path, slug) {
|
|
920
|
+
try {
|
|
921
|
+
const raw = await readFile(path, "utf8");
|
|
922
|
+
const parsed = JSON.parse(raw);
|
|
923
|
+
if (parsed.slug === slug && typeof parsed.steps === "object") return parsed;
|
|
924
|
+
} catch {
|
|
925
|
+
}
|
|
926
|
+
return {
|
|
927
|
+
slug,
|
|
928
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
929
|
+
steps: {}
|
|
930
|
+
};
|
|
931
|
+
}
|
|
932
|
+
async function saveLedger(path, ledger) {
|
|
933
|
+
ledger.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
934
|
+
await mkdir(dirname(path), { recursive: true });
|
|
935
|
+
await writeFile(path, JSON.stringify(ledger, null, 2));
|
|
936
|
+
await chmod(path, 384).catch(() => {
|
|
937
|
+
});
|
|
938
|
+
}
|
|
41
939
|
|
|
42
940
|
// src/commands/install.ts
|
|
43
941
|
async function runInstall(args) {
|
|
@@ -47,7 +945,8 @@ async function runInstall(args) {
|
|
|
47
945
|
strict: true,
|
|
48
946
|
options: {
|
|
49
947
|
force: { type: "boolean", short: "f" },
|
|
50
|
-
"dry-run": { type: "boolean" }
|
|
948
|
+
"dry-run": { type: "boolean" },
|
|
949
|
+
"skip-setup": { type: "boolean" }
|
|
51
950
|
}
|
|
52
951
|
});
|
|
53
952
|
const slug = positionals[0];
|
|
@@ -66,6 +965,7 @@ async function runInstall(args) {
|
|
|
66
965
|
);
|
|
67
966
|
return 0;
|
|
68
967
|
}
|
|
968
|
+
let alreadyInstalled = false;
|
|
69
969
|
if (!values.force) {
|
|
70
970
|
const checked = await runVersionCheck(adapter.handle.version_check);
|
|
71
971
|
if (checked.ok) {
|
|
@@ -73,26 +973,67 @@ async function runInstall(args) {
|
|
|
73
973
|
`agentproto: '${slug}' already installed (${checked.message}). Pass --force to reinstall.
|
|
74
974
|
`
|
|
75
975
|
);
|
|
76
|
-
|
|
976
|
+
alreadyInstalled = true;
|
|
77
977
|
}
|
|
78
978
|
}
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
979
|
+
if (!alreadyInstalled) {
|
|
980
|
+
let lastFailure = null;
|
|
981
|
+
let succeeded = false;
|
|
982
|
+
for (const [i, step] of installSteps.entries()) {
|
|
983
|
+
if (step.experimental) {
|
|
984
|
+
process.stdout.write(
|
|
985
|
+
`agentproto install [${i + 1}/${installSteps.length}] ${describeStep(step)} (experimental \u2014 skipping)
|
|
82
986
|
`
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
987
|
+
);
|
|
988
|
+
continue;
|
|
989
|
+
}
|
|
990
|
+
process.stdout.write(
|
|
991
|
+
`agentproto install [${i + 1}/${installSteps.length}] ${describeStep(step)}
|
|
992
|
+
`
|
|
993
|
+
);
|
|
994
|
+
if (values["dry-run"]) {
|
|
995
|
+
succeeded = true;
|
|
996
|
+
continue;
|
|
997
|
+
}
|
|
998
|
+
const code = await runStep(step);
|
|
999
|
+
if (code === 0) {
|
|
1000
|
+
succeeded = true;
|
|
1001
|
+
break;
|
|
1002
|
+
}
|
|
1003
|
+
lastFailure = { step, code };
|
|
1004
|
+
process.stdout.write(
|
|
1005
|
+
`agentproto install: method '${step.method}' returned ${code}; trying next.
|
|
1006
|
+
`
|
|
1007
|
+
);
|
|
1008
|
+
}
|
|
1009
|
+
if (!succeeded && lastFailure) {
|
|
1010
|
+
process.stderr.write(
|
|
1011
|
+
`agentproto install: all ${installSteps.length} install methods failed. Last failure: ${lastFailure.step.method} \u2192 exit ${lastFailure.code}.
|
|
1012
|
+
`
|
|
1013
|
+
);
|
|
1014
|
+
return lastFailure.code;
|
|
1015
|
+
}
|
|
1016
|
+
if (!succeeded && !lastFailure) {
|
|
87
1017
|
process.stderr.write(
|
|
88
|
-
`agentproto install:
|
|
1018
|
+
`agentproto install: no usable install method found for '${slug}'.
|
|
89
1019
|
`
|
|
90
1020
|
);
|
|
91
|
-
return
|
|
1021
|
+
return 1;
|
|
92
1022
|
}
|
|
1023
|
+
process.stdout.write(`agentproto: '${slug}' installed.
|
|
1024
|
+
`);
|
|
93
1025
|
}
|
|
94
|
-
|
|
1026
|
+
if (!values["skip-setup"] && (adapter.handle.setup?.length ?? 0) > 0) {
|
|
1027
|
+
process.stdout.write(`agentproto: running setup for '${slug}'\u2026
|
|
95
1028
|
`);
|
|
1029
|
+
const setupCode = await runSetup({
|
|
1030
|
+
slug,
|
|
1031
|
+
handle: adapter.handle,
|
|
1032
|
+
force: values.force ?? false,
|
|
1033
|
+
dryRun: values["dry-run"] ?? false
|
|
1034
|
+
});
|
|
1035
|
+
if (setupCode !== 0) return setupCode;
|
|
1036
|
+
}
|
|
96
1037
|
return 0;
|
|
97
1038
|
}
|
|
98
1039
|
function describeStep(step) {
|
|
@@ -102,16 +1043,17 @@ function describeStep(step) {
|
|
|
102
1043
|
case "brew":
|
|
103
1044
|
return `brew install ${step.package ?? "(?)"}`;
|
|
104
1045
|
case "pip":
|
|
105
|
-
return `pip install ${step.package ?? "(?)"}`;
|
|
1046
|
+
return `pip install ${step.user ? "--user " : ""}${step.package ?? "(?)"}`;
|
|
106
1047
|
case "cargo":
|
|
107
1048
|
return `cargo install ${step.package ?? "(?)"}`;
|
|
108
1049
|
case "go":
|
|
109
1050
|
return `go install ${step.package ?? "(?)"}`;
|
|
110
1051
|
case "curl":
|
|
1052
|
+
return `curl ${step.url ?? "(?)"} | bash`;
|
|
111
1053
|
case "download":
|
|
112
|
-
return
|
|
1054
|
+
return `download ${step.url ?? "(?)"} \u2192 ${step.extract_bin ?? "(?)"}`;
|
|
113
1055
|
default:
|
|
114
|
-
return `${step.method} ${step.package ?? step.url ?? "(?)"}`;
|
|
1056
|
+
return `${step.method} ${step.package ?? step.url ?? step.path ?? "(?)"}`;
|
|
115
1057
|
}
|
|
116
1058
|
}
|
|
117
1059
|
async function runStep(step) {
|
|
@@ -126,24 +1068,208 @@ async function runStep(step) {
|
|
|
126
1068
|
argv.push(step.package);
|
|
127
1069
|
return spawnInherit("npm", argv);
|
|
128
1070
|
}
|
|
1071
|
+
case "brew": {
|
|
1072
|
+
if (!step.package) {
|
|
1073
|
+
process.stderr.write("agentproto install: brew step missing package.\n");
|
|
1074
|
+
return 2;
|
|
1075
|
+
}
|
|
1076
|
+
return spawnInherit("brew", ["install", step.package]);
|
|
1077
|
+
}
|
|
1078
|
+
case "pip": {
|
|
1079
|
+
if (!step.package) {
|
|
1080
|
+
process.stderr.write("agentproto install: pip step missing package.\n");
|
|
1081
|
+
return 2;
|
|
1082
|
+
}
|
|
1083
|
+
const argv = ["install"];
|
|
1084
|
+
if (step.user) argv.push("--user");
|
|
1085
|
+
argv.push(step.package);
|
|
1086
|
+
return spawnInherit("pip", argv);
|
|
1087
|
+
}
|
|
1088
|
+
case "cargo": {
|
|
1089
|
+
if (!step.package) {
|
|
1090
|
+
process.stderr.write(
|
|
1091
|
+
"agentproto install: cargo step missing package.\n"
|
|
1092
|
+
);
|
|
1093
|
+
return 2;
|
|
1094
|
+
}
|
|
1095
|
+
return spawnInherit("cargo", ["install", step.package]);
|
|
1096
|
+
}
|
|
1097
|
+
case "go": {
|
|
1098
|
+
if (!step.package) {
|
|
1099
|
+
process.stderr.write("agentproto install: go step missing package.\n");
|
|
1100
|
+
return 2;
|
|
1101
|
+
}
|
|
1102
|
+
return spawnInherit("go", ["install", step.package]);
|
|
1103
|
+
}
|
|
1104
|
+
case "curl": {
|
|
1105
|
+
if (!step.url) {
|
|
1106
|
+
process.stderr.write("agentproto install: curl step missing url.\n");
|
|
1107
|
+
return 2;
|
|
1108
|
+
}
|
|
1109
|
+
return runCurlInstaller(step.url, step.verify_sha256);
|
|
1110
|
+
}
|
|
1111
|
+
case "download": {
|
|
1112
|
+
if (!step.url || !step.extract_bin) {
|
|
1113
|
+
process.stderr.write(
|
|
1114
|
+
"agentproto install: download step missing url or extract_bin.\n"
|
|
1115
|
+
);
|
|
1116
|
+
return 2;
|
|
1117
|
+
}
|
|
1118
|
+
return runDownloadInstaller({
|
|
1119
|
+
url: step.url,
|
|
1120
|
+
extractBin: step.extract_bin,
|
|
1121
|
+
verifySha256: step.verify_sha256
|
|
1122
|
+
});
|
|
1123
|
+
}
|
|
129
1124
|
default:
|
|
130
1125
|
process.stderr.write(
|
|
131
|
-
`agentproto install: method '${step.method}' not yet implemented in this CLI version. Run it manually for now (see the adapter's
|
|
1126
|
+
`agentproto install: method '${step.method}' not yet implemented in this CLI version. Run it manually for now (see the adapter's docs).
|
|
1127
|
+
`
|
|
1128
|
+
);
|
|
1129
|
+
return 1;
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
async function runCurlInstaller(url, verifySha256) {
|
|
1133
|
+
if (!verifySha256) {
|
|
1134
|
+
process.stdout.write(
|
|
1135
|
+
`agentproto install: curl ${url} (\u26A0 no verify_sha256 declared \u2014 installer integrity not verified)
|
|
1136
|
+
`
|
|
1137
|
+
);
|
|
1138
|
+
}
|
|
1139
|
+
const dir = await mkdtemp(join(tmpdir(), "agentproto-curl-"));
|
|
1140
|
+
const scriptPath = join(dir, "install.sh");
|
|
1141
|
+
try {
|
|
1142
|
+
const res = await fetch(url, { redirect: "follow" });
|
|
1143
|
+
if (!res.ok) {
|
|
1144
|
+
process.stderr.write(
|
|
1145
|
+
`agentproto install: curl fetch failed: ${res.status} ${res.statusText}
|
|
1146
|
+
`
|
|
1147
|
+
);
|
|
1148
|
+
return res.status;
|
|
1149
|
+
}
|
|
1150
|
+
const buf = Buffer.from(await res.arrayBuffer());
|
|
1151
|
+
if (verifySha256) {
|
|
1152
|
+
const got = createHash("sha256").update(buf).digest("hex");
|
|
1153
|
+
if (got !== verifySha256.toLowerCase()) {
|
|
1154
|
+
process.stderr.write(
|
|
1155
|
+
`agentproto install: SHA-256 mismatch for ${url}
|
|
1156
|
+
expected: ${verifySha256}
|
|
1157
|
+
got: ${got}
|
|
1158
|
+
`
|
|
1159
|
+
);
|
|
1160
|
+
return 4;
|
|
1161
|
+
}
|
|
1162
|
+
process.stdout.write(
|
|
1163
|
+
`agentproto install: SHA-256 verified (${got.slice(0, 16)}\u2026)
|
|
1164
|
+
`
|
|
1165
|
+
);
|
|
1166
|
+
}
|
|
1167
|
+
await writeFile(scriptPath, buf);
|
|
1168
|
+
await chmod(scriptPath, 493);
|
|
1169
|
+
return spawnInherit("bash", [scriptPath]);
|
|
1170
|
+
} catch (err) {
|
|
1171
|
+
process.stderr.write(
|
|
1172
|
+
`agentproto install: curl install failed: ${err instanceof Error ? err.message : String(err)}
|
|
1173
|
+
`
|
|
1174
|
+
);
|
|
1175
|
+
return 1;
|
|
1176
|
+
} finally {
|
|
1177
|
+
await rm(dir, { recursive: true, force: true }).catch(() => {
|
|
1178
|
+
});
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
async function runDownloadInstaller(opts) {
|
|
1182
|
+
const dir = await mkdtemp(join(tmpdir(), "agentproto-dl-"));
|
|
1183
|
+
try {
|
|
1184
|
+
const filename = opts.url.split("/").pop() || "archive.bin";
|
|
1185
|
+
const archivePath = join(dir, filename);
|
|
1186
|
+
const res = await fetch(opts.url, { redirect: "follow" });
|
|
1187
|
+
if (!res.ok) {
|
|
1188
|
+
process.stderr.write(
|
|
1189
|
+
`agentproto install: download fetch failed: ${res.status} ${res.statusText}
|
|
1190
|
+
`
|
|
1191
|
+
);
|
|
1192
|
+
return res.status;
|
|
1193
|
+
}
|
|
1194
|
+
const buf = Buffer.from(await res.arrayBuffer());
|
|
1195
|
+
if (opts.verifySha256) {
|
|
1196
|
+
const got = createHash("sha256").update(buf).digest("hex");
|
|
1197
|
+
if (got !== opts.verifySha256.toLowerCase()) {
|
|
1198
|
+
process.stderr.write(
|
|
1199
|
+
`agentproto install: SHA-256 mismatch
|
|
1200
|
+
expected: ${opts.verifySha256}
|
|
1201
|
+
got: ${got}
|
|
1202
|
+
`
|
|
1203
|
+
);
|
|
1204
|
+
return 4;
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
await writeFile(archivePath, buf);
|
|
1208
|
+
const extractDir = join(dir, "extract");
|
|
1209
|
+
await mkdir(extractDir, { recursive: true });
|
|
1210
|
+
let extractCode;
|
|
1211
|
+
if (filename.endsWith(".tar.gz") || filename.endsWith(".tgz") || filename.endsWith(".tar")) {
|
|
1212
|
+
extractCode = await spawnInherit("tar", [
|
|
1213
|
+
"-xf",
|
|
1214
|
+
archivePath,
|
|
1215
|
+
"-C",
|
|
1216
|
+
extractDir
|
|
1217
|
+
]);
|
|
1218
|
+
} else if (filename.endsWith(".zip")) {
|
|
1219
|
+
extractCode = await spawnInherit("unzip", [
|
|
1220
|
+
"-q",
|
|
1221
|
+
archivePath,
|
|
1222
|
+
"-d",
|
|
1223
|
+
extractDir
|
|
1224
|
+
]);
|
|
1225
|
+
} else {
|
|
1226
|
+
process.stderr.write(
|
|
1227
|
+
`agentproto install: unsupported archive format for ${filename}. Supported: .tar, .tar.gz, .tgz, .zip.
|
|
1228
|
+
`
|
|
1229
|
+
);
|
|
1230
|
+
return 5;
|
|
1231
|
+
}
|
|
1232
|
+
if (extractCode !== 0) return extractCode;
|
|
1233
|
+
const srcBin = join(extractDir, opts.extractBin);
|
|
1234
|
+
const binDir = process.env["AGENTPROTO_BIN_DIR"] ?? join(homedir3(), ".local", "bin");
|
|
1235
|
+
await mkdir(binDir, { recursive: true });
|
|
1236
|
+
const destBin = join(binDir, opts.extractBin.split("/").pop());
|
|
1237
|
+
const cpCode = await spawnInherit("cp", ["-p", srcBin, destBin]);
|
|
1238
|
+
if (cpCode !== 0) return cpCode;
|
|
1239
|
+
await chmod(destBin, 493);
|
|
1240
|
+
process.stdout.write(`agentproto install: ${destBin}
|
|
1241
|
+
`);
|
|
1242
|
+
if (!process.env["PATH"]?.includes(binDir)) {
|
|
1243
|
+
process.stdout.write(
|
|
1244
|
+
`agentproto install: \u26A0 ${binDir} is not on PATH \u2014 add it to your shell profile so the binary is reachable.
|
|
1245
|
+
`
|
|
1246
|
+
);
|
|
1247
|
+
}
|
|
1248
|
+
return 0;
|
|
1249
|
+
} catch (err) {
|
|
1250
|
+
process.stderr.write(
|
|
1251
|
+
`agentproto install: download failed: ${err instanceof Error ? err.message : String(err)}
|
|
132
1252
|
`
|
|
133
|
-
|
|
134
|
-
|
|
1253
|
+
);
|
|
1254
|
+
return 1;
|
|
1255
|
+
} finally {
|
|
1256
|
+
await rm(dir, { recursive: true, force: true }).catch(() => {
|
|
1257
|
+
});
|
|
135
1258
|
}
|
|
136
1259
|
}
|
|
1260
|
+
function homedir3() {
|
|
1261
|
+
return process.env["HOME"] ?? process.env["USERPROFILE"] ?? "/";
|
|
1262
|
+
}
|
|
137
1263
|
function spawnInherit(cmd, argv) {
|
|
138
|
-
return new Promise((
|
|
1264
|
+
return new Promise((resolve3, reject) => {
|
|
139
1265
|
const child = spawn(cmd, argv, { stdio: "inherit" });
|
|
140
1266
|
child.once("error", reject);
|
|
141
|
-
child.once("exit", (code) =>
|
|
1267
|
+
child.once("exit", (code) => resolve3(code ?? 0));
|
|
142
1268
|
});
|
|
143
1269
|
}
|
|
144
1270
|
async function runVersionCheck(check) {
|
|
145
1271
|
if (!check) return { ok: false, message: "no version_check declared" };
|
|
146
|
-
return new Promise((
|
|
1272
|
+
return new Promise((resolve3) => {
|
|
147
1273
|
const child = spawn("bash", ["-lc", check.cmd], {
|
|
148
1274
|
stdio: ["ignore", "pipe", "ignore"]
|
|
149
1275
|
});
|
|
@@ -151,19 +1277,19 @@ async function runVersionCheck(check) {
|
|
|
151
1277
|
child.stdout.on("data", (c) => {
|
|
152
1278
|
buf += c.toString("utf8");
|
|
153
1279
|
});
|
|
154
|
-
child.once("error", () =>
|
|
1280
|
+
child.once("error", () => resolve3({ ok: false, message: "check failed" }));
|
|
155
1281
|
child.once("exit", (code) => {
|
|
156
1282
|
if (code !== 0) {
|
|
157
|
-
|
|
1283
|
+
resolve3({ ok: false, message: `check exited ${code}` });
|
|
158
1284
|
return;
|
|
159
1285
|
}
|
|
160
1286
|
const re = new RegExp(check.parse);
|
|
161
1287
|
const m = buf.match(re);
|
|
162
1288
|
if (!m || !m[1]) {
|
|
163
|
-
|
|
1289
|
+
resolve3({ ok: false, message: "could not parse version" });
|
|
164
1290
|
return;
|
|
165
1291
|
}
|
|
166
|
-
|
|
1292
|
+
resolve3({ ok: true, message: `version ${m[1]}` });
|
|
167
1293
|
});
|
|
168
1294
|
});
|
|
169
1295
|
}
|
|
@@ -271,10 +1397,32 @@ function printPretty(ev) {
|
|
|
271
1397
|
\x1B[2m[turn-end: ${ev.reason}]\x1B[0m
|
|
272
1398
|
`);
|
|
273
1399
|
break;
|
|
274
|
-
case "error":
|
|
275
|
-
|
|
1400
|
+
case "error": {
|
|
1401
|
+
const code = typeof ev.error.code === "number" ? ` (code ${ev.error.code})` : "";
|
|
1402
|
+
process.stderr.write(
|
|
1403
|
+
`\x1B[31m[error]${code} ${ev.error.message}\x1B[0m
|
|
1404
|
+
`
|
|
1405
|
+
);
|
|
1406
|
+
const data = ev.error.data;
|
|
1407
|
+
if (data && typeof data === "object") {
|
|
1408
|
+
const stderr = data.stderr;
|
|
1409
|
+
if (typeof stderr === "string" && stderr.trim()) {
|
|
1410
|
+
process.stderr.write(`\x1B[2m\u2500\u2500 child stderr \u2500\u2500
|
|
1411
|
+
${stderr}\x1B[0m
|
|
276
1412
|
`);
|
|
1413
|
+
}
|
|
1414
|
+
const rest = { ...data };
|
|
1415
|
+
delete rest.stderr;
|
|
1416
|
+
if (Object.keys(rest).length > 0) {
|
|
1417
|
+
process.stderr.write(
|
|
1418
|
+
`\x1B[2m\u2500\u2500 error.data \u2500\u2500
|
|
1419
|
+
${JSON.stringify(rest, null, 2)}\x1B[0m
|
|
1420
|
+
`
|
|
1421
|
+
);
|
|
1422
|
+
}
|
|
1423
|
+
}
|
|
277
1424
|
break;
|
|
1425
|
+
}
|
|
278
1426
|
}
|
|
279
1427
|
}
|
|
280
1428
|
async function runServe(args) {
|
|
@@ -285,74 +1433,764 @@ async function runServe(args) {
|
|
|
285
1433
|
options: {
|
|
286
1434
|
connect: { type: "string", short: "c" },
|
|
287
1435
|
token: { type: "string", short: "t" },
|
|
288
|
-
label: { type: "string", short: "l" }
|
|
1436
|
+
label: { type: "string", short: "l" },
|
|
1437
|
+
workspace: { type: "string", short: "w" },
|
|
1438
|
+
port: { type: "string", short: "p" },
|
|
1439
|
+
bind: { type: "string", short: "b" }
|
|
289
1440
|
}
|
|
290
1441
|
});
|
|
291
|
-
|
|
1442
|
+
const workspace = resolve(values.workspace ?? process.cwd());
|
|
1443
|
+
try {
|
|
1444
|
+
const stat = await promises.stat(workspace);
|
|
1445
|
+
if (!stat.isDirectory()) {
|
|
1446
|
+
process.stderr.write(
|
|
1447
|
+
`agentproto serve: --workspace "${workspace}" is not a directory.
|
|
1448
|
+
`
|
|
1449
|
+
);
|
|
1450
|
+
return 2;
|
|
1451
|
+
}
|
|
1452
|
+
} catch {
|
|
292
1453
|
process.stderr.write(
|
|
293
|
-
|
|
1454
|
+
`agentproto serve: --workspace "${workspace}" does not exist.
|
|
1455
|
+
Create it first: mkdir -p "${workspace}"
|
|
1456
|
+
`
|
|
294
1457
|
);
|
|
295
1458
|
return 2;
|
|
296
1459
|
}
|
|
1460
|
+
const port = values.port ? Number.parseInt(values.port, 10) : 18790;
|
|
1461
|
+
if (!Number.isFinite(port) || port <= 0 || port > 65535) {
|
|
1462
|
+
process.stderr.write(`agentproto serve: invalid --port "${values.port}".
|
|
1463
|
+
`);
|
|
1464
|
+
return 2;
|
|
1465
|
+
}
|
|
1466
|
+
const label = values.label ?? `${userInfo().username}@${hostname()}`;
|
|
1467
|
+
let token = values.token ?? process.env.AGENTPROTO_TOKEN;
|
|
1468
|
+
if (!token && values.connect) {
|
|
1469
|
+
const cred = await readHost(values.connect);
|
|
1470
|
+
if (cred) {
|
|
1471
|
+
if (isExpired(cred)) {
|
|
1472
|
+
process.stderr.write(
|
|
1473
|
+
`agentproto serve: \u26A0 credentials for ${values.connect} are expired (${formatExpiry(cred)}). Re-run \`agentproto auth login --host ${values.connect}\`.
|
|
1474
|
+
`
|
|
1475
|
+
);
|
|
1476
|
+
}
|
|
1477
|
+
token = cred.token;
|
|
1478
|
+
process.stdout.write(
|
|
1479
|
+
`agentproto serve: using token from credentials.json (${formatExpiry(cred)})
|
|
1480
|
+
`
|
|
1481
|
+
);
|
|
1482
|
+
}
|
|
1483
|
+
}
|
|
1484
|
+
const opts = {
|
|
1485
|
+
workspace,
|
|
1486
|
+
port,
|
|
1487
|
+
bind: values.bind ?? "127.0.0.1",
|
|
1488
|
+
...values.connect ? { connect: values.connect } : {},
|
|
1489
|
+
...token ? { token } : {},
|
|
1490
|
+
label
|
|
1491
|
+
};
|
|
1492
|
+
const resolveAgentAdapter = async (slug) => {
|
|
1493
|
+
try {
|
|
1494
|
+
const adapter = await resolveAdapter(slug);
|
|
1495
|
+
const runtime = createAgentCliRuntime(adapter.handle);
|
|
1496
|
+
return {
|
|
1497
|
+
async startSession({ cwd }) {
|
|
1498
|
+
return runtime.start({ cwd });
|
|
1499
|
+
},
|
|
1500
|
+
commandPreview: `${adapter.handle.bin} ${(adapter.handle.bin_args ?? []).join(" ")}`.trim()
|
|
1501
|
+
};
|
|
1502
|
+
} catch (err) {
|
|
1503
|
+
console.warn(
|
|
1504
|
+
`[agentproto serve] resolveAgentAdapter('${slug}') failed: ${err instanceof Error ? err.message : String(err)}`
|
|
1505
|
+
);
|
|
1506
|
+
return null;
|
|
1507
|
+
}
|
|
1508
|
+
};
|
|
1509
|
+
let gateway;
|
|
1510
|
+
try {
|
|
1511
|
+
gateway = await createGateway({
|
|
1512
|
+
workspace: opts.workspace,
|
|
1513
|
+
port: opts.port,
|
|
1514
|
+
bind: opts.bind,
|
|
1515
|
+
specs: [],
|
|
1516
|
+
name: "agentproto-serve",
|
|
1517
|
+
// BOOT.md is silly for a tunnel daemon — skip it.
|
|
1518
|
+
boot: false,
|
|
1519
|
+
resolveAgentAdapter,
|
|
1520
|
+
// Discovery for UIs / operators — `GET /adapters` + `list_adapters`
|
|
1521
|
+
// MCP tool. Walks node_modules @agentproto/adapter-* on each call;
|
|
1522
|
+
// cheap enough that we don't bother caching here.
|
|
1523
|
+
listAgentAdapters: listInstalledAdapters
|
|
1524
|
+
});
|
|
1525
|
+
} catch (err) {
|
|
1526
|
+
process.stderr.write(
|
|
1527
|
+
`agentproto serve: gateway boot failed \u2014 ${err instanceof Error ? err.message : String(err)}
|
|
1528
|
+
`
|
|
1529
|
+
);
|
|
1530
|
+
return 1;
|
|
1531
|
+
}
|
|
1532
|
+
process.stderr.write(
|
|
1533
|
+
`agentproto serve: gateway up on ${gateway.url}
|
|
1534
|
+
workspace: ${gateway.workspace}
|
|
1535
|
+
mcp: ${gateway.url}/mcp
|
|
1536
|
+
sessions: ${gateway.url}/sessions
|
|
1537
|
+
events: ${gateway.url}/events
|
|
1538
|
+
`
|
|
1539
|
+
);
|
|
1540
|
+
const aborter = new AbortController();
|
|
1541
|
+
let shuttingDown = false;
|
|
1542
|
+
const shutdown = async (signal) => {
|
|
1543
|
+
if (shuttingDown) return;
|
|
1544
|
+
shuttingDown = true;
|
|
1545
|
+
process.stderr.write(`
|
|
1546
|
+
agentproto serve: ${signal} \u2014 shutting down.
|
|
1547
|
+
`);
|
|
1548
|
+
aborter.abort();
|
|
1549
|
+
await gateway.stop().catch(() => void 0);
|
|
1550
|
+
process.exit(0);
|
|
1551
|
+
};
|
|
1552
|
+
process.once("SIGINT", () => void shutdown("SIGINT"));
|
|
1553
|
+
process.once("SIGTERM", () => void shutdown("SIGTERM"));
|
|
1554
|
+
if (!opts.connect) {
|
|
1555
|
+
process.stderr.write(
|
|
1556
|
+
`agentproto serve: running local-only (no --connect). Press Ctrl-C to stop.
|
|
1557
|
+
`
|
|
1558
|
+
);
|
|
1559
|
+
await new Promise((resolve3) => {
|
|
1560
|
+
aborter.signal.addEventListener("abort", () => resolve3());
|
|
1561
|
+
});
|
|
1562
|
+
return 0;
|
|
1563
|
+
}
|
|
297
1564
|
process.stderr.write(
|
|
298
|
-
`agentproto serve: tunnel
|
|
299
|
-
Tracking: tunnel frame protocol is landing in @agentproto/acp next.
|
|
300
|
-
Wanted endpoint: ${values.connect}
|
|
301
|
-
Label: ${values.label ?? "(none)"}
|
|
1565
|
+
`agentproto serve: tunnel \u2014 connecting to ${opts.connect} as '${opts.label}'\u2026
|
|
302
1566
|
`
|
|
303
1567
|
);
|
|
304
|
-
|
|
1568
|
+
let backoffMs = opts.reconnectMinMs ?? 1e3;
|
|
1569
|
+
const backoffMax = opts.reconnectMaxMs ?? 3e4;
|
|
1570
|
+
while (!aborter.signal.aborted) {
|
|
1571
|
+
try {
|
|
1572
|
+
await runOneTunnel(opts, gateway, aborter.signal);
|
|
1573
|
+
backoffMs = opts.reconnectMinMs ?? 1e3;
|
|
1574
|
+
} catch (err) {
|
|
1575
|
+
if (aborter.signal.aborted) break;
|
|
1576
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1577
|
+
process.stderr.write(
|
|
1578
|
+
`agentproto serve: tunnel error: ${msg}
|
|
1579
|
+
reconnecting in ${backoffMs}ms\u2026
|
|
1580
|
+
`
|
|
1581
|
+
);
|
|
1582
|
+
await sleep2(backoffMs, aborter.signal);
|
|
1583
|
+
backoffMs = Math.min(backoffMs * 2, backoffMax);
|
|
1584
|
+
}
|
|
1585
|
+
}
|
|
1586
|
+
return 0;
|
|
305
1587
|
}
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
1588
|
+
async function runOneTunnel(opts, gateway, signal) {
|
|
1589
|
+
if (!opts.connect) throw new Error("runOneTunnel: --connect not set");
|
|
1590
|
+
const headers = {
|
|
1591
|
+
"user-agent": "agentproto/0.1.0-alpha"
|
|
1592
|
+
};
|
|
1593
|
+
if (opts.token) headers.authorization = `Bearer ${opts.token}`;
|
|
1594
|
+
const ws = new WebSocket(opts.connect, { headers });
|
|
1595
|
+
await new Promise((resolve3, reject) => {
|
|
1596
|
+
const onOpen = () => {
|
|
1597
|
+
ws.off("error", onError);
|
|
1598
|
+
resolve3();
|
|
1599
|
+
};
|
|
1600
|
+
const onError = (err) => {
|
|
1601
|
+
ws.off("open", onOpen);
|
|
1602
|
+
reject(err);
|
|
1603
|
+
};
|
|
1604
|
+
ws.once("open", onOpen);
|
|
1605
|
+
ws.once("error", onError);
|
|
1606
|
+
if (signal.aborted) {
|
|
1607
|
+
ws.close();
|
|
1608
|
+
reject(new Error("Aborted before WS opened."));
|
|
1609
|
+
}
|
|
1610
|
+
signal.addEventListener("abort", () => {
|
|
1611
|
+
ws.close();
|
|
1612
|
+
reject(new Error("Aborted while WS connecting."));
|
|
1613
|
+
});
|
|
1614
|
+
});
|
|
1615
|
+
process.stderr.write(`agentproto serve: tunnel up.
|
|
1616
|
+
`);
|
|
1617
|
+
const sink = wrapWebSocket(ws);
|
|
1618
|
+
const server = createTunnelServer({
|
|
1619
|
+
sink,
|
|
1620
|
+
label: opts.label,
|
|
1621
|
+
pty: false,
|
|
1622
|
+
// v0 authorize hook: trust the bearer-authenticated host completely.
|
|
1623
|
+
// Token possession proves the host was provisioned for this daemon.
|
|
1624
|
+
// Per-spawn policy filtering will land alongside the policy.toml.
|
|
1625
|
+
authorize: (req) => req,
|
|
1626
|
+
// ── AIP-46 bridge: tunnel spawns land in the gateway registry ──
|
|
1627
|
+
// Every cloud-driven spawn shows up in `gateway.url/sessions`
|
|
1628
|
+
// and the LocalDaemonSessionsCard, alongside spawns originated
|
|
1629
|
+
// through MCP tools or POST /sessions/agent. The execId is the
|
|
1630
|
+
// host's stable id — keep it so cloud cli_sessions rows can
|
|
1631
|
+
// cross-ref the daemon-side descriptor.
|
|
1632
|
+
onChildSpawned: ({ execId, child, request }) => {
|
|
1633
|
+
gateway.sessions.register({
|
|
1634
|
+
id: execId,
|
|
1635
|
+
child,
|
|
1636
|
+
workspaceSlug: opts.label,
|
|
1637
|
+
command: [request.command, ...request.args].join(" "),
|
|
1638
|
+
kind: "agent-cli",
|
|
1639
|
+
label: `tunnel: ${request.command.split("/").pop() ?? request.command}`
|
|
1640
|
+
});
|
|
1641
|
+
}
|
|
1642
|
+
});
|
|
1643
|
+
await new Promise((resolve3) => {
|
|
1644
|
+
const offClose = sink.onClose(() => {
|
|
1645
|
+
offClose();
|
|
1646
|
+
resolve3();
|
|
1647
|
+
});
|
|
1648
|
+
if (signal.aborted) {
|
|
1649
|
+
server.close().finally(() => resolve3());
|
|
1650
|
+
}
|
|
1651
|
+
signal.addEventListener("abort", () => {
|
|
1652
|
+
server.close().finally(() => resolve3());
|
|
1653
|
+
});
|
|
1654
|
+
});
|
|
1655
|
+
process.stderr.write(`agentproto serve: tunnel closed.
|
|
1656
|
+
`);
|
|
1657
|
+
}
|
|
1658
|
+
function sleep2(ms, signal) {
|
|
1659
|
+
return new Promise((resolve3) => {
|
|
1660
|
+
if (signal.aborted) return resolve3();
|
|
1661
|
+
const timer = setTimeout(resolve3, ms);
|
|
1662
|
+
signal.addEventListener("abort", () => {
|
|
1663
|
+
clearTimeout(timer);
|
|
1664
|
+
resolve3();
|
|
1665
|
+
});
|
|
1666
|
+
});
|
|
1667
|
+
}
|
|
1668
|
+
var USAGE2 = `agentproto workspace \u2014 manage local workspaces
|
|
309
1669
|
|
|
310
1670
|
Usage:
|
|
311
|
-
agentproto
|
|
312
|
-
agentproto
|
|
313
|
-
agentproto
|
|
314
|
-
agentproto
|
|
315
|
-
agentproto --
|
|
1671
|
+
agentproto workspace add <path> [--slug <slug>] [--label <text>]
|
|
1672
|
+
agentproto workspace list [--json]
|
|
1673
|
+
agentproto workspace remove <slug>
|
|
1674
|
+
agentproto workspace use <slug>
|
|
1675
|
+
agentproto workspace --help
|
|
316
1676
|
|
|
317
|
-
|
|
318
|
-
agentproto install claude-code
|
|
319
|
-
agentproto run claude-code --cwd . --prompt "summarise this repo"
|
|
320
|
-
agentproto serve --connect wss://guilde.work/api/v1/agentproto/tunnel --token $AGENTPROTO_TOKEN
|
|
1677
|
+
Config file: ${DEFAULT_CONFIG_PATH()}
|
|
321
1678
|
`;
|
|
322
|
-
async function
|
|
1679
|
+
async function runWorkspace(args) {
|
|
1680
|
+
if (args.length === 0 || args.includes("--help") || args.includes("-h")) {
|
|
1681
|
+
process.stdout.write(USAGE2);
|
|
1682
|
+
return 0;
|
|
1683
|
+
}
|
|
1684
|
+
const sub = args[0];
|
|
1685
|
+
const rest = args.slice(1);
|
|
1686
|
+
switch (sub) {
|
|
1687
|
+
case "add":
|
|
1688
|
+
return runAdd(rest);
|
|
1689
|
+
case "list":
|
|
1690
|
+
case "ls":
|
|
1691
|
+
return runList(rest);
|
|
1692
|
+
case "remove":
|
|
1693
|
+
case "rm":
|
|
1694
|
+
return runRemove(rest);
|
|
1695
|
+
case "use":
|
|
1696
|
+
return runUse(rest);
|
|
1697
|
+
default:
|
|
1698
|
+
process.stderr.write(
|
|
1699
|
+
`agentproto workspace: unknown subcommand '${sub}'
|
|
1700
|
+
|
|
1701
|
+
${USAGE2}`
|
|
1702
|
+
);
|
|
1703
|
+
return 2;
|
|
1704
|
+
}
|
|
1705
|
+
}
|
|
1706
|
+
async function runAdd(args) {
|
|
323
1707
|
const { values, positionals } = parseArgs({
|
|
324
|
-
args: [...
|
|
1708
|
+
args: [...args],
|
|
325
1709
|
allowPositionals: true,
|
|
326
|
-
strict:
|
|
1710
|
+
strict: true,
|
|
327
1711
|
options: {
|
|
328
|
-
|
|
329
|
-
|
|
1712
|
+
slug: { type: "string" },
|
|
1713
|
+
label: { type: "string" }
|
|
330
1714
|
}
|
|
331
1715
|
});
|
|
332
|
-
|
|
333
|
-
|
|
1716
|
+
const rawPath = positionals[0];
|
|
1717
|
+
if (!rawPath) {
|
|
1718
|
+
process.stderr.write(
|
|
1719
|
+
"agentproto workspace add: missing <path>. Try: agentproto workspace add ~/code/my-project\n"
|
|
1720
|
+
);
|
|
1721
|
+
return 2;
|
|
1722
|
+
}
|
|
1723
|
+
const absPath = resolve(rawPath.replace(/^~/, process.env.HOME ?? "~"));
|
|
1724
|
+
if (!existsSync(absPath)) {
|
|
1725
|
+
process.stderr.write(
|
|
1726
|
+
`agentproto workspace add: "${absPath}" doesn't exist. Create the directory first or pass an existing one.
|
|
1727
|
+
`
|
|
1728
|
+
);
|
|
1729
|
+
return 2;
|
|
1730
|
+
}
|
|
1731
|
+
const slug = sanitizeSlug(values.slug ?? basename(absPath));
|
|
1732
|
+
const config = await loadWorkspacesConfig();
|
|
1733
|
+
const next = addWorkspace(config, {
|
|
1734
|
+
slug,
|
|
1735
|
+
path: absPath,
|
|
1736
|
+
...values.label ? { label: values.label } : {}
|
|
1737
|
+
});
|
|
1738
|
+
await saveWorkspacesConfig(next);
|
|
1739
|
+
const wasActive = config.active === slug;
|
|
1740
|
+
const becameActive = next.active === slug && !wasActive;
|
|
1741
|
+
process.stdout.write(
|
|
1742
|
+
`\u2713 ${slug} \u2192 ${absPath}` + (becameActive ? " (set as active \u2014 first registered workspace)" : "") + "\n"
|
|
1743
|
+
);
|
|
1744
|
+
return 0;
|
|
1745
|
+
}
|
|
1746
|
+
async function runList(args) {
|
|
1747
|
+
const { values } = parseArgs({
|
|
1748
|
+
args: [...args],
|
|
1749
|
+
allowPositionals: false,
|
|
1750
|
+
strict: true,
|
|
1751
|
+
options: { json: { type: "boolean" } }
|
|
1752
|
+
});
|
|
1753
|
+
const config = await loadWorkspacesConfig();
|
|
1754
|
+
if (values.json) {
|
|
1755
|
+
process.stdout.write(JSON.stringify(config, null, 2) + "\n");
|
|
1756
|
+
return 0;
|
|
1757
|
+
}
|
|
1758
|
+
if (config.workspaces.length === 0) {
|
|
1759
|
+
process.stdout.write(
|
|
1760
|
+
"No workspaces registered.\nAdd one with: agentproto workspace add <path>\n"
|
|
1761
|
+
);
|
|
1762
|
+
return 0;
|
|
1763
|
+
}
|
|
1764
|
+
const sorted = [...config.workspaces].sort(
|
|
1765
|
+
(a, b) => b.updatedAt.localeCompare(a.updatedAt)
|
|
1766
|
+
);
|
|
1767
|
+
const slugCol = Math.max(...sorted.map((w) => w.slug.length), 6);
|
|
1768
|
+
for (const w of sorted) {
|
|
1769
|
+
const marker = w.slug === config.active ? "\u25CF" : " ";
|
|
1770
|
+
const slugCell = w.slug.padEnd(slugCol);
|
|
1771
|
+
const labelSuffix = w.label ? ` (${w.label})` : "";
|
|
1772
|
+
process.stdout.write(
|
|
1773
|
+
`${marker} ${slugCell} ${w.path}${labelSuffix}
|
|
1774
|
+
`
|
|
1775
|
+
);
|
|
1776
|
+
}
|
|
1777
|
+
process.stdout.write(
|
|
1778
|
+
`
|
|
1779
|
+
${sorted.length} workspace${sorted.length === 1 ? "" : "s"}` + (config.active ? `, active: ${config.active}` : ", no active") + "\n"
|
|
1780
|
+
);
|
|
1781
|
+
return 0;
|
|
1782
|
+
}
|
|
1783
|
+
async function runRemove(args) {
|
|
1784
|
+
const { positionals } = parseArgs({
|
|
1785
|
+
args: [...args],
|
|
1786
|
+
allowPositionals: true,
|
|
1787
|
+
strict: true,
|
|
1788
|
+
options: {}
|
|
1789
|
+
});
|
|
1790
|
+
const slug = positionals[0];
|
|
1791
|
+
if (!slug) {
|
|
1792
|
+
process.stderr.write(
|
|
1793
|
+
"agentproto workspace remove: missing <slug>.\n"
|
|
1794
|
+
);
|
|
1795
|
+
return 2;
|
|
1796
|
+
}
|
|
1797
|
+
const config = await loadWorkspacesConfig();
|
|
1798
|
+
const sanitised = sanitizeSlug(slug);
|
|
1799
|
+
const existed = config.workspaces.some((w) => w.slug === sanitised);
|
|
1800
|
+
if (!existed) {
|
|
1801
|
+
process.stderr.write(
|
|
1802
|
+
`agentproto workspace remove: no workspace named "${sanitised}".
|
|
1803
|
+
`
|
|
1804
|
+
);
|
|
1805
|
+
return 2;
|
|
1806
|
+
}
|
|
1807
|
+
const next = removeWorkspace(config, sanitised);
|
|
1808
|
+
await saveWorkspacesConfig(next);
|
|
1809
|
+
process.stdout.write(`\u2717 removed ${sanitised}
|
|
1810
|
+
`);
|
|
1811
|
+
if (next.active && next.active !== config.active) {
|
|
1812
|
+
process.stdout.write(` active reassigned to: ${next.active}
|
|
1813
|
+
`);
|
|
1814
|
+
} else if (!next.active && config.active) {
|
|
1815
|
+
process.stdout.write(` no workspaces left \u2014 active cleared
|
|
1816
|
+
`);
|
|
1817
|
+
}
|
|
1818
|
+
return 0;
|
|
1819
|
+
}
|
|
1820
|
+
async function runUse(args) {
|
|
1821
|
+
const { positionals } = parseArgs({
|
|
1822
|
+
args: [...args],
|
|
1823
|
+
allowPositionals: true,
|
|
1824
|
+
strict: true,
|
|
1825
|
+
options: {}
|
|
1826
|
+
});
|
|
1827
|
+
const slug = positionals[0];
|
|
1828
|
+
if (!slug) {
|
|
1829
|
+
process.stderr.write("agentproto workspace use: missing <slug>.\n");
|
|
1830
|
+
return 2;
|
|
1831
|
+
}
|
|
1832
|
+
const config = await loadWorkspacesConfig();
|
|
1833
|
+
try {
|
|
1834
|
+
const next = setActiveWorkspace(config, slug);
|
|
1835
|
+
await saveWorkspacesConfig(next);
|
|
1836
|
+
const w = next.workspaces.find((x) => x.slug === next.active);
|
|
1837
|
+
process.stdout.write(`\u25CF active: ${next.active} (${w?.path ?? "?"})
|
|
1838
|
+
`);
|
|
334
1839
|
return 0;
|
|
1840
|
+
} catch (err) {
|
|
1841
|
+
process.stderr.write(
|
|
1842
|
+
`agentproto workspace use: ${err instanceof Error ? err.message : String(err)}
|
|
1843
|
+
`
|
|
1844
|
+
);
|
|
1845
|
+
return 2;
|
|
335
1846
|
}
|
|
336
|
-
|
|
337
|
-
|
|
1847
|
+
}
|
|
1848
|
+
var USAGE3 = `agentproto sessions \u2014 browse local daemon sessions
|
|
1849
|
+
|
|
1850
|
+
Usage:
|
|
1851
|
+
agentproto sessions [--watch] [--json]
|
|
1852
|
+
agentproto sessions --attach <id> [--no-color]
|
|
1853
|
+
|
|
1854
|
+
Discovers the daemon via ~/.agentproto/runtime.json. Set
|
|
1855
|
+
AGENTPROTO_DAEMON_URL to override.
|
|
1856
|
+
`;
|
|
1857
|
+
async function runSessions(args) {
|
|
1858
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
1859
|
+
process.stdout.write(USAGE3);
|
|
338
1860
|
return 0;
|
|
339
1861
|
}
|
|
340
|
-
const
|
|
341
|
-
|
|
342
|
-
|
|
1862
|
+
const { values } = parseArgs({
|
|
1863
|
+
args: [...args],
|
|
1864
|
+
allowPositionals: false,
|
|
1865
|
+
strict: true,
|
|
1866
|
+
options: {
|
|
1867
|
+
watch: { type: "boolean" },
|
|
1868
|
+
json: { type: "boolean" },
|
|
1869
|
+
attach: { type: "string" },
|
|
1870
|
+
"no-color": { type: "boolean" }
|
|
1871
|
+
}
|
|
1872
|
+
});
|
|
1873
|
+
const baseUrl = await resolveDaemonUrl();
|
|
1874
|
+
if (!baseUrl) {
|
|
1875
|
+
process.stderr.write(
|
|
1876
|
+
"agentproto sessions: no daemon found.\n Start one with `agentproto serve` or set AGENTPROTO_DAEMON_URL.\n"
|
|
1877
|
+
);
|
|
1878
|
+
return 2;
|
|
1879
|
+
}
|
|
1880
|
+
if (values.attach) {
|
|
1881
|
+
return runAttach({
|
|
1882
|
+
baseUrl,
|
|
1883
|
+
id: values.attach,
|
|
1884
|
+
colour: !values["no-color"]
|
|
1885
|
+
});
|
|
1886
|
+
}
|
|
1887
|
+
if (values.json) {
|
|
1888
|
+
const list2 = await fetchSessions(baseUrl);
|
|
1889
|
+
process.stdout.write(JSON.stringify(list2, null, 2) + "\n");
|
|
343
1890
|
return 0;
|
|
344
1891
|
}
|
|
1892
|
+
if (values.watch) {
|
|
1893
|
+
return runWatch(baseUrl);
|
|
1894
|
+
}
|
|
1895
|
+
const list = await fetchSessions(baseUrl);
|
|
1896
|
+
printTable(list);
|
|
1897
|
+
return 0;
|
|
1898
|
+
}
|
|
1899
|
+
async function resolveDaemonUrl() {
|
|
1900
|
+
if (process.env.AGENTPROTO_DAEMON_URL) {
|
|
1901
|
+
return process.env.AGENTPROTO_DAEMON_URL.replace(/\/+$/, "");
|
|
1902
|
+
}
|
|
1903
|
+
const config = await loadWorkspacesConfig().catch(() => null);
|
|
1904
|
+
if (!config) return null;
|
|
1905
|
+
const candidates = [
|
|
1906
|
+
getActiveWorkspace(config),
|
|
1907
|
+
...config.workspaces
|
|
1908
|
+
].filter(
|
|
1909
|
+
(w, i, arr) => !!w && arr.findIndex((x) => x?.slug === w.slug) === i
|
|
1910
|
+
);
|
|
1911
|
+
for (const w of candidates) {
|
|
1912
|
+
const url = await readRuntimeJson(w.path);
|
|
1913
|
+
if (url) return url;
|
|
1914
|
+
}
|
|
1915
|
+
return null;
|
|
1916
|
+
}
|
|
1917
|
+
async function readRuntimeJson(workspacePath) {
|
|
1918
|
+
const path = resolve(workspacePath, ".agentproto", "runtime.json");
|
|
1919
|
+
try {
|
|
1920
|
+
const raw = await promises.readFile(path, "utf8");
|
|
1921
|
+
const parsed = JSON.parse(raw);
|
|
1922
|
+
if (typeof parsed.port !== "number") return null;
|
|
1923
|
+
return `http://${parsed.bind ?? "127.0.0.1"}:${parsed.port}`;
|
|
1924
|
+
} catch {
|
|
1925
|
+
return null;
|
|
1926
|
+
}
|
|
1927
|
+
}
|
|
1928
|
+
async function fetchSessions(baseUrl) {
|
|
1929
|
+
const body = await httpGetJson(`${baseUrl}/sessions`);
|
|
1930
|
+
if (!body || !Array.isArray(body.sessions)) {
|
|
1931
|
+
return [];
|
|
1932
|
+
}
|
|
1933
|
+
return body.sessions;
|
|
1934
|
+
}
|
|
1935
|
+
function printTable(rows) {
|
|
1936
|
+
if (rows.length === 0) {
|
|
1937
|
+
process.stdout.write("No sessions.\n");
|
|
1938
|
+
return;
|
|
1939
|
+
}
|
|
1940
|
+
const widths = {
|
|
1941
|
+
id: Math.max(...rows.map((r) => r.id.length), 4),
|
|
1942
|
+
kind: Math.max(...rows.map((r) => r.kind.length), 4),
|
|
1943
|
+
workspace: Math.max(...rows.map((r) => r.workspaceSlug.length), 9),
|
|
1944
|
+
status: Math.max(...rows.map((r) => r.status.length), 8),
|
|
1945
|
+
age: 8
|
|
1946
|
+
};
|
|
1947
|
+
const header = pad("ID", widths.id) + " " + pad("KIND", widths.kind) + " " + pad("WORKSPACE", widths.workspace) + " " + pad("STATUS", widths.status) + " " + pad("AGE", widths.age) + " COMMAND";
|
|
1948
|
+
process.stdout.write(`\x1B[2m${header}\x1B[0m
|
|
1949
|
+
`);
|
|
1950
|
+
const now = Date.now();
|
|
1951
|
+
for (const r of rows) {
|
|
1952
|
+
const age = humaniseDelta(now - new Date(r.startedAt).getTime());
|
|
1953
|
+
const tone = statusColour(r.status);
|
|
1954
|
+
process.stdout.write(
|
|
1955
|
+
pad(r.id, widths.id) + " " + pad(r.kind, widths.kind) + " " + pad(r.workspaceSlug, widths.workspace) + ` ${tone}${pad(r.status, widths.status)}\x1B[0m ` + pad(age, widths.age) + " " + truncate2(r.command, 60) + "\n"
|
|
1956
|
+
);
|
|
1957
|
+
}
|
|
1958
|
+
}
|
|
1959
|
+
function statusColour(status) {
|
|
1960
|
+
switch (status) {
|
|
1961
|
+
case "running":
|
|
1962
|
+
return "\x1B[32m";
|
|
1963
|
+
// green
|
|
1964
|
+
case "starting":
|
|
1965
|
+
case "mounting":
|
|
1966
|
+
return "\x1B[33m";
|
|
1967
|
+
// yellow
|
|
1968
|
+
case "exited":
|
|
1969
|
+
return "\x1B[2m";
|
|
1970
|
+
// dim
|
|
1971
|
+
case "killed":
|
|
1972
|
+
case "error":
|
|
1973
|
+
return "\x1B[31m";
|
|
1974
|
+
// red
|
|
1975
|
+
default:
|
|
1976
|
+
return "";
|
|
1977
|
+
}
|
|
1978
|
+
}
|
|
1979
|
+
function pad(s, n) {
|
|
1980
|
+
if (s.length >= n) return s;
|
|
1981
|
+
return s + " ".repeat(n - s.length);
|
|
1982
|
+
}
|
|
1983
|
+
function truncate2(s, n) {
|
|
1984
|
+
if (s.length <= n) return s;
|
|
1985
|
+
return s.slice(0, n - 1) + "\u2026";
|
|
1986
|
+
}
|
|
1987
|
+
function humaniseDelta(ms) {
|
|
1988
|
+
if (ms < 6e4) return `${Math.floor(ms / 1e3)}s`;
|
|
1989
|
+
if (ms < 36e5) return `${Math.floor(ms / 6e4)}m`;
|
|
1990
|
+
if (ms < 864e5) return `${Math.floor(ms / 36e5)}h`;
|
|
1991
|
+
return `${Math.floor(ms / 864e5)}d`;
|
|
1992
|
+
}
|
|
1993
|
+
async function runWatch(baseUrl) {
|
|
1994
|
+
const tty = process.stdin.isTTY === true;
|
|
1995
|
+
if (tty) {
|
|
1996
|
+
process.stdin.setRawMode(true);
|
|
1997
|
+
process.stdin.resume();
|
|
1998
|
+
process.stdin.setEncoding("utf8");
|
|
1999
|
+
}
|
|
2000
|
+
let stop = false;
|
|
2001
|
+
const onKey = (key) => {
|
|
2002
|
+
if (key === "q" || key === "") stop = true;
|
|
2003
|
+
};
|
|
2004
|
+
if (tty) process.stdin.on("data", onKey);
|
|
2005
|
+
process.on("SIGINT", () => {
|
|
2006
|
+
stop = true;
|
|
2007
|
+
});
|
|
2008
|
+
try {
|
|
2009
|
+
while (!stop) {
|
|
2010
|
+
const list = await fetchSessions(baseUrl).catch((err) => {
|
|
2011
|
+
process.stdout.write(`\x1Bc[fetch error] ${String(err)}
|
|
2012
|
+
`);
|
|
2013
|
+
return null;
|
|
2014
|
+
});
|
|
2015
|
+
process.stdout.write("\x1Bc");
|
|
2016
|
+
process.stdout.write(
|
|
2017
|
+
`\x1B[2magentproto sessions \u2022 ${baseUrl} \u2022 ${tty ? "press q to quit" : "Ctrl-C to quit"}\x1B[0m
|
|
2018
|
+
|
|
2019
|
+
`
|
|
2020
|
+
);
|
|
2021
|
+
if (list) printTable(list);
|
|
2022
|
+
await new Promise((res) => setTimeout(res, 2e3));
|
|
2023
|
+
}
|
|
2024
|
+
} finally {
|
|
2025
|
+
if (tty) {
|
|
2026
|
+
process.stdin.off("data", onKey);
|
|
2027
|
+
process.stdin.setRawMode(false);
|
|
2028
|
+
process.stdin.pause();
|
|
2029
|
+
}
|
|
2030
|
+
process.stdout.write("\n");
|
|
2031
|
+
}
|
|
2032
|
+
return 0;
|
|
2033
|
+
}
|
|
2034
|
+
async function runAttach({ baseUrl, id, colour }) {
|
|
2035
|
+
return new Promise((resolve3) => {
|
|
2036
|
+
const url = new URL(`${baseUrl}/sessions/${id}/stream`);
|
|
2037
|
+
const lib = url.protocol === "https:" ? https : http;
|
|
2038
|
+
const req = lib.get(
|
|
2039
|
+
url,
|
|
2040
|
+
{ headers: { accept: "text/event-stream" } },
|
|
2041
|
+
(res) => {
|
|
2042
|
+
if (res.statusCode === 404) {
|
|
2043
|
+
process.stderr.write(
|
|
2044
|
+
`agentproto sessions --attach: no session "${id}".
|
|
2045
|
+
`
|
|
2046
|
+
);
|
|
2047
|
+
resolve3(2);
|
|
2048
|
+
return;
|
|
2049
|
+
}
|
|
2050
|
+
if (res.statusCode !== 200) {
|
|
2051
|
+
process.stderr.write(
|
|
2052
|
+
`agentproto sessions --attach: HTTP ${res.statusCode}
|
|
2053
|
+
`
|
|
2054
|
+
);
|
|
2055
|
+
resolve3(1);
|
|
2056
|
+
return;
|
|
2057
|
+
}
|
|
2058
|
+
let buf = "";
|
|
2059
|
+
res.setEncoding("utf8");
|
|
2060
|
+
res.on("data", (chunk) => {
|
|
2061
|
+
buf += chunk;
|
|
2062
|
+
let idx = buf.indexOf("\n\n");
|
|
2063
|
+
while (idx !== -1) {
|
|
2064
|
+
const event = buf.slice(0, idx);
|
|
2065
|
+
buf = buf.slice(idx + 2);
|
|
2066
|
+
for (const line of event.split("\n")) {
|
|
2067
|
+
if (line.startsWith("data:")) {
|
|
2068
|
+
const payload = line.slice(5).trim();
|
|
2069
|
+
try {
|
|
2070
|
+
const json = JSON.parse(payload);
|
|
2071
|
+
if (typeof json.line !== "string") continue;
|
|
2072
|
+
if (colour && json.stream === "stderr") {
|
|
2073
|
+
process.stdout.write(`\x1B[31m${json.line}\x1B[0m
|
|
2074
|
+
`);
|
|
2075
|
+
} else {
|
|
2076
|
+
process.stdout.write(json.line + "\n");
|
|
2077
|
+
}
|
|
2078
|
+
} catch {
|
|
2079
|
+
}
|
|
2080
|
+
}
|
|
2081
|
+
}
|
|
2082
|
+
idx = buf.indexOf("\n\n");
|
|
2083
|
+
}
|
|
2084
|
+
});
|
|
2085
|
+
res.on("end", () => resolve3(0));
|
|
2086
|
+
}
|
|
2087
|
+
);
|
|
2088
|
+
req.on("error", (err) => {
|
|
2089
|
+
process.stderr.write(`agentproto sessions --attach: ${err.message}
|
|
2090
|
+
`);
|
|
2091
|
+
resolve3(1);
|
|
2092
|
+
});
|
|
2093
|
+
process.on("SIGINT", () => {
|
|
2094
|
+
req.destroy();
|
|
2095
|
+
resolve3(0);
|
|
2096
|
+
});
|
|
2097
|
+
});
|
|
2098
|
+
}
|
|
2099
|
+
function httpGetJson(url) {
|
|
2100
|
+
return new Promise((resolve3, reject) => {
|
|
2101
|
+
const u = new URL(url);
|
|
2102
|
+
const lib = u.protocol === "https:" ? https : http;
|
|
2103
|
+
lib.get(u, (res) => {
|
|
2104
|
+
let body = "";
|
|
2105
|
+
res.setEncoding("utf8");
|
|
2106
|
+
res.on("data", (c) => body += c);
|
|
2107
|
+
res.on("end", () => {
|
|
2108
|
+
if ((res.statusCode ?? 0) < 200 || (res.statusCode ?? 0) >= 300) {
|
|
2109
|
+
reject(new Error(`HTTP ${res.statusCode}: ${body.slice(0, 200)}`));
|
|
2110
|
+
return;
|
|
2111
|
+
}
|
|
2112
|
+
try {
|
|
2113
|
+
resolve3(JSON.parse(body));
|
|
2114
|
+
} catch (err) {
|
|
2115
|
+
reject(err instanceof Error ? err : new Error(String(err)));
|
|
2116
|
+
}
|
|
2117
|
+
});
|
|
2118
|
+
}).on("error", reject);
|
|
2119
|
+
});
|
|
2120
|
+
}
|
|
2121
|
+
|
|
2122
|
+
// src/cli.ts
|
|
2123
|
+
var USAGE4 = `agentproto \u2014 AIP-45 agent CLI host
|
|
2124
|
+
|
|
2125
|
+
Usage:
|
|
2126
|
+
agentproto auth <login|status|logout> [--host <url>] [--label <name>]
|
|
2127
|
+
agentproto install <slug> [--force] [--dry-run] [--skip-setup]
|
|
2128
|
+
agentproto setup <slug> [--force] [--dry-run] [--only <stepId>...]
|
|
2129
|
+
agentproto run <slug> [--cwd <dir>] [--prompt <text>] [--resume <session-id>]
|
|
2130
|
+
agentproto serve [--workspace <dir>] [--port <n>] [--bind <ip>]
|
|
2131
|
+
[--connect <url> [--token <jwt>] [--label <name>]]
|
|
2132
|
+
agentproto workspace <add|list|remove|use> [args]
|
|
2133
|
+
agentproto sessions [--watch] [--attach <id>] [--json]
|
|
2134
|
+
agentproto --help
|
|
2135
|
+
agentproto --version
|
|
2136
|
+
|
|
2137
|
+
Examples:
|
|
2138
|
+
agentproto auth login --host wss://guilde.work # device flow \u2192 ~/.agentproto/credentials.json
|
|
2139
|
+
agentproto install claude-code
|
|
2140
|
+
agentproto setup openclaw # re-run setup (idempotent via skip_if + ledger)
|
|
2141
|
+
agentproto run claude-code --cwd . --prompt "summarise this repo"
|
|
2142
|
+
agentproto workspace add ~/code/my-project --slug my-project
|
|
2143
|
+
agentproto workspace list
|
|
2144
|
+
agentproto serve --connect wss://guilde.work/api/v1/agentproto/tunnel
|
|
2145
|
+
`;
|
|
2146
|
+
var VERBS = /* @__PURE__ */ new Set([
|
|
2147
|
+
"auth",
|
|
2148
|
+
"install",
|
|
2149
|
+
"setup",
|
|
2150
|
+
"run",
|
|
2151
|
+
"serve",
|
|
2152
|
+
"workspace",
|
|
2153
|
+
"sessions"
|
|
2154
|
+
]);
|
|
2155
|
+
async function main(argv) {
|
|
2156
|
+
const verbIdx = argv.findIndex((a) => VERBS.has(a));
|
|
2157
|
+
if (verbIdx === -1) {
|
|
2158
|
+
if (argv.includes("--version") || argv.includes("-v")) {
|
|
2159
|
+
process.stdout.write("agentproto 0.1.0-alpha\n");
|
|
2160
|
+
return 0;
|
|
2161
|
+
}
|
|
2162
|
+
if (argv.includes("--help") || argv.includes("-h") || argv.length === 0) {
|
|
2163
|
+
process.stdout.write(USAGE4);
|
|
2164
|
+
return 0;
|
|
2165
|
+
}
|
|
2166
|
+
process.stderr.write(
|
|
2167
|
+
`agentproto: unrecognised argument(s): ${argv.join(" ")}
|
|
2168
|
+
|
|
2169
|
+
${USAGE4}`
|
|
2170
|
+
);
|
|
2171
|
+
return 2;
|
|
2172
|
+
}
|
|
2173
|
+
const verb = argv[verbIdx];
|
|
2174
|
+
const rest = argv.slice(verbIdx + 1);
|
|
345
2175
|
switch (verb) {
|
|
2176
|
+
case "auth":
|
|
2177
|
+
return runAuth(rest);
|
|
346
2178
|
case "install":
|
|
347
2179
|
return runInstall(rest);
|
|
2180
|
+
case "setup":
|
|
2181
|
+
return runSetupCommand(rest);
|
|
348
2182
|
case "run":
|
|
349
2183
|
return runRun(rest);
|
|
350
2184
|
case "serve":
|
|
351
2185
|
return runServe(rest);
|
|
2186
|
+
case "workspace":
|
|
2187
|
+
return runWorkspace(rest);
|
|
2188
|
+
case "sessions":
|
|
2189
|
+
return runSessions(rest);
|
|
352
2190
|
default:
|
|
353
2191
|
process.stderr.write(`agentproto: unknown verb '${verb}'
|
|
354
2192
|
|
|
355
|
-
${
|
|
2193
|
+
${USAGE4}`);
|
|
356
2194
|
return 2;
|
|
357
2195
|
}
|
|
358
2196
|
}
|