@agentproto/cli 0.1.0-alpha.1 → 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 CHANGED
@@ -1,19 +1,460 @@
1
1
  #!/usr/bin/env node
2
2
  import { spawn } from 'child_process';
3
+ import { userInfo, hostname, homedir, platform, tmpdir } from 'os';
3
4
  import { parseArgs } from 'util';
4
- import { resolve } from 'path';
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';
6
- import { userInfo, hostname } from 'os';
7
12
  import { wrapWebSocket, createTunnelServer } from '@agentproto/acp/tunnel';
13
+ import { createGateway } from '@agentproto/runtime';
8
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';
9
18
 
10
19
  /**
11
20
  * @agentproto/cli v0.1.0-alpha
12
21
  * The `agentproto` binary — install / run / serve AIP-45 agent CLIs.
13
22
  */
14
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}'.
15
119
 
16
- // src/registry/resolve.ts
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}
200
+
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
+ }
17
458
  var slugToCamel = (slug) => slug.replace(/-([a-z0-9])/g, (_, c) => c.toUpperCase());
18
459
  async function resolveAdapter(slug) {
19
460
  if (!/^[a-z][a-z0-9-]*$/.test(slug)) {
@@ -41,6 +482,460 @@ async function resolveAdapter(slug) {
41
482
  }
42
483
  return { slug, handle: candidate, source: "npm", packageName };
43
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
+ }
44
939
 
45
940
  // src/commands/install.ts
46
941
  async function runInstall(args) {
@@ -50,7 +945,8 @@ async function runInstall(args) {
50
945
  strict: true,
51
946
  options: {
52
947
  force: { type: "boolean", short: "f" },
53
- "dry-run": { type: "boolean" }
948
+ "dry-run": { type: "boolean" },
949
+ "skip-setup": { type: "boolean" }
54
950
  }
55
951
  });
56
952
  const slug = positionals[0];
@@ -69,6 +965,7 @@ async function runInstall(args) {
69
965
  );
70
966
  return 0;
71
967
  }
968
+ let alreadyInstalled = false;
72
969
  if (!values.force) {
73
970
  const checked = await runVersionCheck(adapter.handle.version_check);
74
971
  if (checked.ok) {
@@ -76,26 +973,67 @@ async function runInstall(args) {
76
973
  `agentproto: '${slug}' already installed (${checked.message}). Pass --force to reinstall.
77
974
  `
78
975
  );
79
- return 0;
976
+ alreadyInstalled = true;
80
977
  }
81
978
  }
82
- for (const [i, step] of installSteps.entries()) {
83
- process.stdout.write(
84
- `agentproto install [${i + 1}/${installSteps.length}] ${describeStep(step)}
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)
85
986
  `
86
- );
87
- if (values["dry-run"]) continue;
88
- const code = await runStep(step);
89
- if (code !== 0) {
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) {
90
1017
  process.stderr.write(
91
- `agentproto install: step ${i + 1} failed with exit ${code}.
1018
+ `agentproto install: no usable install method found for '${slug}'.
92
1019
  `
93
1020
  );
94
- return code;
1021
+ return 1;
95
1022
  }
1023
+ process.stdout.write(`agentproto: '${slug}' installed.
1024
+ `);
96
1025
  }
97
- process.stdout.write(`agentproto: '${slug}' installed.
1026
+ if (!values["skip-setup"] && (adapter.handle.setup?.length ?? 0) > 0) {
1027
+ process.stdout.write(`agentproto: running setup for '${slug}'\u2026
98
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
+ }
99
1037
  return 0;
100
1038
  }
101
1039
  function describeStep(step) {
@@ -105,48 +1043,233 @@ function describeStep(step) {
105
1043
  case "brew":
106
1044
  return `brew install ${step.package ?? "(?)"}`;
107
1045
  case "pip":
108
- return `pip install ${step.package ?? "(?)"}`;
1046
+ return `pip install ${step.user ? "--user " : ""}${step.package ?? "(?)"}`;
109
1047
  case "cargo":
110
1048
  return `cargo install ${step.package ?? "(?)"}`;
111
1049
  case "go":
112
1050
  return `go install ${step.package ?? "(?)"}`;
113
1051
  case "curl":
1052
+ return `curl ${step.url ?? "(?)"} | bash`;
114
1053
  case "download":
115
- return `${step.method} ${step.url ?? "(?)"} \u2192 ${step.path ?? "(default)"}`;
1054
+ return `download ${step.url ?? "(?)"} \u2192 ${step.extract_bin ?? "(?)"}`;
1055
+ default:
1056
+ return `${step.method} ${step.package ?? step.url ?? step.path ?? "(?)"}`;
1057
+ }
1058
+ }
1059
+ async function runStep(step) {
1060
+ switch (step.method) {
1061
+ case "npm": {
1062
+ if (!step.package) {
1063
+ process.stderr.write("agentproto install: npm step missing package.\n");
1064
+ return 2;
1065
+ }
1066
+ const argv = ["install"];
1067
+ if (step.global) argv.push("-g");
1068
+ argv.push(step.package);
1069
+ return spawnInherit("npm", argv);
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
+ }
116
1124
  default:
117
- return `${step.method} ${step.package ?? step.url ?? "(?)"}`;
1125
+ process.stderr.write(
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
+ });
118
1179
  }
119
1180
  }
120
- async function runStep(step) {
121
- switch (step.method) {
122
- case "npm": {
123
- if (!step.package) {
124
- process.stderr.write("agentproto install: npm step missing package.\n");
125
- return 2;
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;
126
1205
  }
127
- const argv = ["install"];
128
- if (step.global) argv.push("-g");
129
- argv.push(step.package);
130
- return spawnInherit("npm", argv);
131
1206
  }
132
- default:
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 {
133
1226
  process.stderr.write(
134
- `agentproto install: method '${step.method}' not yet implemented in this CLI version. Run it manually for now (see the adapter's INSTALL.md).
1227
+ `agentproto install: unsupported archive format for ${filename}. Supported: .tar, .tar.gz, .tgz, .zip.
135
1228
  `
136
1229
  );
137
- return 1;
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)}
1252
+ `
1253
+ );
1254
+ return 1;
1255
+ } finally {
1256
+ await rm(dir, { recursive: true, force: true }).catch(() => {
1257
+ });
138
1258
  }
139
1259
  }
1260
+ function homedir3() {
1261
+ return process.env["HOME"] ?? process.env["USERPROFILE"] ?? "/";
1262
+ }
140
1263
  function spawnInherit(cmd, argv) {
141
- return new Promise((resolve, reject) => {
1264
+ return new Promise((resolve3, reject) => {
142
1265
  const child = spawn(cmd, argv, { stdio: "inherit" });
143
1266
  child.once("error", reject);
144
- child.once("exit", (code) => resolve(code ?? 0));
1267
+ child.once("exit", (code) => resolve3(code ?? 0));
145
1268
  });
146
1269
  }
147
1270
  async function runVersionCheck(check) {
148
1271
  if (!check) return { ok: false, message: "no version_check declared" };
149
- return new Promise((resolve) => {
1272
+ return new Promise((resolve3) => {
150
1273
  const child = spawn("bash", ["-lc", check.cmd], {
151
1274
  stdio: ["ignore", "pipe", "ignore"]
152
1275
  });
@@ -154,19 +1277,19 @@ async function runVersionCheck(check) {
154
1277
  child.stdout.on("data", (c) => {
155
1278
  buf += c.toString("utf8");
156
1279
  });
157
- child.once("error", () => resolve({ ok: false, message: "check failed" }));
1280
+ child.once("error", () => resolve3({ ok: false, message: "check failed" }));
158
1281
  child.once("exit", (code) => {
159
1282
  if (code !== 0) {
160
- resolve({ ok: false, message: `check exited ${code}` });
1283
+ resolve3({ ok: false, message: `check exited ${code}` });
161
1284
  return;
162
1285
  }
163
1286
  const re = new RegExp(check.parse);
164
1287
  const m = buf.match(re);
165
1288
  if (!m || !m[1]) {
166
- resolve({ ok: false, message: "could not parse version" });
1289
+ resolve3({ ok: false, message: "could not parse version" });
167
1290
  return;
168
1291
  }
169
- resolve({ ok: true, message: `version ${m[1]}` });
1292
+ resolve3({ ok: true, message: `version ${m[1]}` });
170
1293
  });
171
1294
  });
172
1295
  }
@@ -274,10 +1397,32 @@ function printPretty(ev) {
274
1397
  \x1B[2m[turn-end: ${ev.reason}]\x1B[0m
275
1398
  `);
276
1399
  break;
277
- case "error":
278
- process.stderr.write(`\x1B[31m[error] ${ev.error.message}\x1B[0m
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
279
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
+ }
280
1424
  break;
1425
+ }
281
1426
  }
282
1427
  }
283
1428
  async function runServe(args) {
@@ -288,39 +1433,143 @@ async function runServe(args) {
288
1433
  options: {
289
1434
  connect: { type: "string", short: "c" },
290
1435
  token: { type: "string", short: "t" },
291
- 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" }
292
1440
  }
293
1441
  });
294
- if (!values.connect) {
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 {
295
1453
  process.stderr.write(
296
- "agentproto serve: --connect <url> is required.\n example: agentproto serve --connect wss://guilde.work/api/v1/agentproto/tunnel --token $TOKEN\n"
1454
+ `agentproto serve: --workspace "${workspace}" does not exist.
1455
+ Create it first: mkdir -p "${workspace}"
1456
+ `
297
1457
  );
298
1458
  return 2;
299
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
+ }
300
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
+ }
301
1484
  const opts = {
302
- connect: values.connect,
303
- token: values.token ?? process.env.AGENTPROTO_TOKEN,
1485
+ workspace,
1486
+ port,
1487
+ bind: values.bind ?? "127.0.0.1",
1488
+ ...values.connect ? { connect: values.connect } : {},
1489
+ ...token ? { token } : {},
304
1490
  label
305
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
+ }
306
1532
  process.stderr.write(
307
- `agentproto serve: connecting to ${opts.connect} as '${opts.label}'\u2026
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
308
1538
  `
309
1539
  );
310
1540
  const aborter = new AbortController();
311
- const onSignal = (sig) => {
1541
+ let shuttingDown = false;
1542
+ const shutdown = async (signal) => {
1543
+ if (shuttingDown) return;
1544
+ shuttingDown = true;
312
1545
  process.stderr.write(`
313
- agentproto serve: received ${sig}, shutting down.
1546
+ agentproto serve: ${signal} \u2014 shutting down.
314
1547
  `);
315
1548
  aborter.abort();
1549
+ await gateway.stop().catch(() => void 0);
1550
+ process.exit(0);
316
1551
  };
317
- process.once("SIGINT", onSignal);
318
- process.once("SIGTERM", onSignal);
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
+ }
1564
+ process.stderr.write(
1565
+ `agentproto serve: tunnel \u2014 connecting to ${opts.connect} as '${opts.label}'\u2026
1566
+ `
1567
+ );
319
1568
  let backoffMs = opts.reconnectMinMs ?? 1e3;
320
1569
  const backoffMax = opts.reconnectMaxMs ?? 3e4;
321
1570
  while (!aborter.signal.aborted) {
322
1571
  try {
323
- await runOneTunnel(opts, aborter.signal);
1572
+ await runOneTunnel(opts, gateway, aborter.signal);
324
1573
  backoffMs = opts.reconnectMinMs ?? 1e3;
325
1574
  } catch (err) {
326
1575
  if (aborter.signal.aborted) break;
@@ -330,24 +1579,23 @@ agentproto serve: received ${sig}, shutting down.
330
1579
  reconnecting in ${backoffMs}ms\u2026
331
1580
  `
332
1581
  );
333
- await sleep(backoffMs, aborter.signal);
1582
+ await sleep2(backoffMs, aborter.signal);
334
1583
  backoffMs = Math.min(backoffMs * 2, backoffMax);
335
1584
  }
336
1585
  }
337
- process.off("SIGINT", onSignal);
338
- process.off("SIGTERM", onSignal);
339
1586
  return 0;
340
1587
  }
341
- async function runOneTunnel(opts, signal) {
1588
+ async function runOneTunnel(opts, gateway, signal) {
1589
+ if (!opts.connect) throw new Error("runOneTunnel: --connect not set");
342
1590
  const headers = {
343
1591
  "user-agent": "agentproto/0.1.0-alpha"
344
1592
  };
345
1593
  if (opts.token) headers.authorization = `Bearer ${opts.token}`;
346
1594
  const ws = new WebSocket(opts.connect, { headers });
347
- await new Promise((resolve, reject) => {
1595
+ await new Promise((resolve3, reject) => {
348
1596
  const onOpen = () => {
349
1597
  ws.off("error", onError);
350
- resolve();
1598
+ resolve3();
351
1599
  };
352
1600
  const onError = (err) => {
353
1601
  ws.off("open", onOpen);
@@ -374,50 +1622,536 @@ async function runOneTunnel(opts, signal) {
374
1622
  // v0 authorize hook: trust the bearer-authenticated host completely.
375
1623
  // Token possession proves the host was provisioned for this daemon.
376
1624
  // Per-spawn policy filtering will land alongside the policy.toml.
377
- authorize: (req) => req
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
+ }
378
1642
  });
379
- await new Promise((resolve) => {
1643
+ await new Promise((resolve3) => {
380
1644
  const offClose = sink.onClose(() => {
381
1645
  offClose();
382
- resolve();
1646
+ resolve3();
383
1647
  });
384
1648
  if (signal.aborted) {
385
- server.close().finally(() => resolve());
1649
+ server.close().finally(() => resolve3());
386
1650
  }
387
1651
  signal.addEventListener("abort", () => {
388
- server.close().finally(() => resolve());
1652
+ server.close().finally(() => resolve3());
389
1653
  });
390
1654
  });
391
1655
  process.stderr.write(`agentproto serve: tunnel closed.
392
1656
  `);
393
1657
  }
394
- function sleep(ms, signal) {
395
- return new Promise((resolve) => {
396
- if (signal.aborted) return resolve();
397
- const timer = setTimeout(resolve, ms);
1658
+ function sleep2(ms, signal) {
1659
+ return new Promise((resolve3) => {
1660
+ if (signal.aborted) return resolve3();
1661
+ const timer = setTimeout(resolve3, ms);
398
1662
  signal.addEventListener("abort", () => {
399
1663
  clearTimeout(timer);
400
- resolve();
1664
+ resolve3();
1665
+ });
1666
+ });
1667
+ }
1668
+ var USAGE2 = `agentproto workspace \u2014 manage local workspaces
1669
+
1670
+ Usage:
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
1676
+
1677
+ Config file: ${DEFAULT_CONFIG_PATH()}
1678
+ `;
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) {
1707
+ const { values, positionals } = parseArgs({
1708
+ args: [...args],
1709
+ allowPositionals: true,
1710
+ strict: true,
1711
+ options: {
1712
+ slug: { type: "string" },
1713
+ label: { type: "string" }
1714
+ }
1715
+ });
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
+ `);
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;
1846
+ }
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);
1860
+ return 0;
1861
+ }
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");
1890
+ return 0;
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);
401
2096
  });
402
2097
  });
403
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
+ }
404
2121
 
405
2122
  // src/cli.ts
406
- var USAGE = `agentproto \u2014 AIP-45 agent CLI host
2123
+ var USAGE4 = `agentproto \u2014 AIP-45 agent CLI host
407
2124
 
408
2125
  Usage:
409
- agentproto install <slug> [--registry <path>]
410
- agentproto run <slug> [--cwd <dir>] [--prompt <text>] [--resume <session-id>]
411
- agentproto serve --connect <url> [--token <jwt>] [--label <name>]
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]
412
2134
  agentproto --help
413
2135
  agentproto --version
414
2136
 
415
2137
  Examples:
2138
+ agentproto auth login --host wss://guilde.work # device flow \u2192 ~/.agentproto/credentials.json
416
2139
  agentproto install claude-code
2140
+ agentproto setup openclaw # re-run setup (idempotent via skip_if + ledger)
417
2141
  agentproto run claude-code --cwd . --prompt "summarise this repo"
418
- agentproto serve --connect wss://guilde.work/api/v1/agentproto/tunnel --token $AGENTPROTO_TOKEN
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
419
2145
  `;
420
- var VERBS = /* @__PURE__ */ new Set(["install", "run", "serve"]);
2146
+ var VERBS = /* @__PURE__ */ new Set([
2147
+ "auth",
2148
+ "install",
2149
+ "setup",
2150
+ "run",
2151
+ "serve",
2152
+ "workspace",
2153
+ "sessions"
2154
+ ]);
421
2155
  async function main(argv) {
422
2156
  const verbIdx = argv.findIndex((a) => VERBS.has(a));
423
2157
  if (verbIdx === -1) {
@@ -426,29 +2160,37 @@ async function main(argv) {
426
2160
  return 0;
427
2161
  }
428
2162
  if (argv.includes("--help") || argv.includes("-h") || argv.length === 0) {
429
- process.stdout.write(USAGE);
2163
+ process.stdout.write(USAGE4);
430
2164
  return 0;
431
2165
  }
432
2166
  process.stderr.write(
433
2167
  `agentproto: unrecognised argument(s): ${argv.join(" ")}
434
2168
 
435
- ${USAGE}`
2169
+ ${USAGE4}`
436
2170
  );
437
2171
  return 2;
438
2172
  }
439
2173
  const verb = argv[verbIdx];
440
2174
  const rest = argv.slice(verbIdx + 1);
441
2175
  switch (verb) {
2176
+ case "auth":
2177
+ return runAuth(rest);
442
2178
  case "install":
443
2179
  return runInstall(rest);
2180
+ case "setup":
2181
+ return runSetupCommand(rest);
444
2182
  case "run":
445
2183
  return runRun(rest);
446
2184
  case "serve":
447
2185
  return runServe(rest);
2186
+ case "workspace":
2187
+ return runWorkspace(rest);
2188
+ case "sessions":
2189
+ return runSessions(rest);
448
2190
  default:
449
2191
  process.stderr.write(`agentproto: unknown verb '${verb}'
450
2192
 
451
- ${USAGE}`);
2193
+ ${USAGE4}`);
452
2194
  return 2;
453
2195
  }
454
2196
  }