@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/index.mjs
CHANGED
|
@@ -1,15 +1,22 @@
|
|
|
1
1
|
import { spawn } from 'child_process';
|
|
2
|
+
import { createHash } from 'crypto';
|
|
3
|
+
import { mkdtemp, writeFile, chmod, rm, mkdir, readFile } from 'fs/promises';
|
|
4
|
+
import { tmpdir, userInfo, hostname, homedir, platform } from 'os';
|
|
5
|
+
import { resolve, join, dirname } from 'path';
|
|
2
6
|
import { parseArgs } from 'util';
|
|
3
|
-
import {
|
|
7
|
+
import { promises } from 'fs';
|
|
8
|
+
import { createInterface } from 'readline/promises';
|
|
9
|
+
import { stdout, stdin } from 'process';
|
|
4
10
|
import { createAgentCliRuntime } from '@agentproto/driver-agent-cli';
|
|
11
|
+
import { wrapWebSocket, createTunnelServer } from '@agentproto/acp/tunnel';
|
|
12
|
+
import { createGateway } from '@agentproto/runtime';
|
|
13
|
+
import WebSocket from 'ws';
|
|
5
14
|
|
|
6
15
|
/**
|
|
7
16
|
* @agentproto/cli v0.1.0-alpha
|
|
8
17
|
* The `agentproto` binary — install / run / serve AIP-45 agent CLIs.
|
|
9
18
|
*/
|
|
10
19
|
|
|
11
|
-
|
|
12
|
-
// src/registry/resolve.ts
|
|
13
20
|
var slugToCamel = (slug) => slug.replace(/-([a-z0-9])/g, (_, c) => c.toUpperCase());
|
|
14
21
|
async function resolveAdapter(slug) {
|
|
15
22
|
if (!/^[a-z][a-z0-9-]*$/.test(slug)) {
|
|
@@ -37,6 +44,433 @@ async function resolveAdapter(slug) {
|
|
|
37
44
|
}
|
|
38
45
|
return { slug, handle: candidate, source: "npm", packageName };
|
|
39
46
|
}
|
|
47
|
+
async function listInstalledAdapters(opts) {
|
|
48
|
+
const roots = await collectAgentprotoNamespaceRoots(opts?.searchRoot);
|
|
49
|
+
const seen = /* @__PURE__ */ new Set();
|
|
50
|
+
const out = [];
|
|
51
|
+
for (const root of roots) {
|
|
52
|
+
let entries = [];
|
|
53
|
+
try {
|
|
54
|
+
entries = await promises.readdir(root);
|
|
55
|
+
} catch {
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
for (const entry of entries) {
|
|
59
|
+
if (!entry.startsWith("adapter-")) continue;
|
|
60
|
+
const slug = entry.slice("adapter-".length);
|
|
61
|
+
if (seen.has(slug)) continue;
|
|
62
|
+
seen.add(slug);
|
|
63
|
+
try {
|
|
64
|
+
const resolved = await resolveAdapter(slug);
|
|
65
|
+
const handle = resolved.handle;
|
|
66
|
+
const info = {
|
|
67
|
+
slug,
|
|
68
|
+
name: typeof handle.name === "string" ? handle.name : slug,
|
|
69
|
+
version: typeof handle.version === "string" ? handle.version : "?",
|
|
70
|
+
description: typeof handle.description === "string" ? handle.description : "",
|
|
71
|
+
protocol: typeof handle.protocol === "string" ? handle.protocol : "unknown",
|
|
72
|
+
streaming: !!handle.capabilities?.streaming,
|
|
73
|
+
packageName: resolved.packageName ?? `@agentproto/adapter-${slug}`
|
|
74
|
+
};
|
|
75
|
+
out.push(info);
|
|
76
|
+
} catch (err) {
|
|
77
|
+
console.warn(
|
|
78
|
+
`[agentproto/cli] listInstalledAdapters: skipping broken adapter '${slug}': ${err instanceof Error ? err.message : String(err)}`
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return out.sort((a, b) => a.slug.localeCompare(b.slug));
|
|
84
|
+
}
|
|
85
|
+
async function collectAgentprotoNamespaceRoots(start) {
|
|
86
|
+
const roots = [];
|
|
87
|
+
let cur = start ?? process.cwd();
|
|
88
|
+
const candidatesAt = (dir) => [
|
|
89
|
+
resolve(dir, "node_modules", "@agentproto"),
|
|
90
|
+
resolve(dir, "node_modules", ".pnpm", "node_modules", "@agentproto")
|
|
91
|
+
];
|
|
92
|
+
for (let depth = 0; depth < 16; depth++) {
|
|
93
|
+
for (const candidate of candidatesAt(cur)) {
|
|
94
|
+
try {
|
|
95
|
+
const stat = await promises.stat(candidate);
|
|
96
|
+
if (stat.isDirectory()) roots.push(candidate);
|
|
97
|
+
} catch {
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
const parent = resolve(cur, "..");
|
|
101
|
+
if (parent === cur) break;
|
|
102
|
+
cur = parent;
|
|
103
|
+
}
|
|
104
|
+
return roots;
|
|
105
|
+
}
|
|
106
|
+
async function runSetup(opts) {
|
|
107
|
+
const steps = opts.handle.setup ?? [];
|
|
108
|
+
if (steps.length === 0) {
|
|
109
|
+
process.stdout.write(`agentproto setup: no setup steps for '${opts.slug}'.
|
|
110
|
+
`);
|
|
111
|
+
return 0;
|
|
112
|
+
}
|
|
113
|
+
const ledgerPath = ledgerPathFor(opts.slug);
|
|
114
|
+
const ledger = await loadLedger(ledgerPath, opts.slug);
|
|
115
|
+
const onlySet = opts.only && opts.only.length > 0 ? new Set(opts.only) : null;
|
|
116
|
+
for (const [i, step] of steps.entries()) {
|
|
117
|
+
if (onlySet && !onlySet.has(step.id)) continue;
|
|
118
|
+
const idx = `[${i + 1}/${steps.length}]`;
|
|
119
|
+
const head = `${idx} ${step.kind}/${step.id}`;
|
|
120
|
+
const prev = ledger.steps[step.id];
|
|
121
|
+
if (!opts.force && prev) {
|
|
122
|
+
process.stdout.write(
|
|
123
|
+
`${head} \u2713 already completed (${prev.completedAt}${prev.skippedViaSkipIf ? ", via skip_if" : ""}). Pass --force to re-run.
|
|
124
|
+
`
|
|
125
|
+
);
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
if (step.skip_if && !opts.force) {
|
|
129
|
+
const ok = await checkSkipIf(step.skip_if);
|
|
130
|
+
if (ok) {
|
|
131
|
+
process.stdout.write(`${head} \u2713 skip_if matched \u2014 skipping.
|
|
132
|
+
`);
|
|
133
|
+
ledger.steps[step.id] = {
|
|
134
|
+
stepId: step.id,
|
|
135
|
+
kind: step.kind,
|
|
136
|
+
completedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
137
|
+
skippedViaSkipIf: true
|
|
138
|
+
};
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
if (opts.dryRun) {
|
|
143
|
+
process.stdout.write(`${head} (dry-run) \u2014 would execute.
|
|
144
|
+
`);
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
let entry = null;
|
|
148
|
+
try {
|
|
149
|
+
switch (step.kind) {
|
|
150
|
+
case "cmd":
|
|
151
|
+
entry = await runCmdStep(step, ledger, head);
|
|
152
|
+
break;
|
|
153
|
+
case "prompt":
|
|
154
|
+
entry = await runPromptStep(step, ledger, head);
|
|
155
|
+
break;
|
|
156
|
+
case "external":
|
|
157
|
+
entry = await runExternalStep(step, ledger, head);
|
|
158
|
+
break;
|
|
159
|
+
case "oauth":
|
|
160
|
+
process.stderr.write(
|
|
161
|
+
`${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.
|
|
162
|
+
`
|
|
163
|
+
);
|
|
164
|
+
return 1;
|
|
165
|
+
}
|
|
166
|
+
} catch (err) {
|
|
167
|
+
process.stderr.write(
|
|
168
|
+
`${head} \u2717 failed: ${err instanceof Error ? err.message : String(err)}
|
|
169
|
+
`
|
|
170
|
+
);
|
|
171
|
+
await saveLedger(ledgerPath, ledger);
|
|
172
|
+
return 1;
|
|
173
|
+
}
|
|
174
|
+
if (entry) {
|
|
175
|
+
ledger.steps[step.id] = entry;
|
|
176
|
+
await saveLedger(ledgerPath, ledger);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
process.stdout.write(`agentproto: setup for '${opts.slug}' complete.
|
|
180
|
+
`);
|
|
181
|
+
return 0;
|
|
182
|
+
}
|
|
183
|
+
async function runCmdStep(step, ledger, head) {
|
|
184
|
+
if (step.description) process.stdout.write(`${head} ${step.description}
|
|
185
|
+
`);
|
|
186
|
+
process.stdout.write(`${head} $ ${step.cmd}
|
|
187
|
+
`);
|
|
188
|
+
const captured = await runShellCapturing(step.cmd, {
|
|
189
|
+
timeoutMs: step.timeout_ms ?? 6e4,
|
|
190
|
+
interactive: step.interactive ?? false
|
|
191
|
+
});
|
|
192
|
+
if (captured.exitCode !== 0) {
|
|
193
|
+
throw new Error(
|
|
194
|
+
`cmd exited ${captured.exitCode}${captured.stderr ? ": " + truncate(captured.stderr, 400) : ""}`
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
let persistedTo;
|
|
198
|
+
if (step.persist) {
|
|
199
|
+
const value = captured.stdout.trim();
|
|
200
|
+
persistedTo = await applyPersist(step.persist, value, ledger);
|
|
201
|
+
}
|
|
202
|
+
return {
|
|
203
|
+
stepId: step.id,
|
|
204
|
+
kind: step.kind,
|
|
205
|
+
completedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
206
|
+
...persistedTo ? { persistedTo } : {}
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
async function runPromptStep(step, ledger, head) {
|
|
210
|
+
if (step.description) process.stdout.write(`${head} ${step.description}
|
|
211
|
+
`);
|
|
212
|
+
const type = step.type ?? "text";
|
|
213
|
+
let value;
|
|
214
|
+
switch (type) {
|
|
215
|
+
case "text":
|
|
216
|
+
case "secret":
|
|
217
|
+
value = await promptString(step.prompt, {
|
|
218
|
+
defaultValue: step.default,
|
|
219
|
+
masked: type === "secret"
|
|
220
|
+
});
|
|
221
|
+
break;
|
|
222
|
+
case "boolean":
|
|
223
|
+
value = await promptBoolean(step.prompt, step.default === "true") ? "true" : "false";
|
|
224
|
+
break;
|
|
225
|
+
case "select": {
|
|
226
|
+
const options = await resolveSelectOptions(step.options);
|
|
227
|
+
if (options.length === 0) {
|
|
228
|
+
throw new Error("select prompt has no options to choose from");
|
|
229
|
+
}
|
|
230
|
+
value = await promptSelect(step.prompt, options, step.default);
|
|
231
|
+
break;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
let persistedTo;
|
|
235
|
+
if (step.persist) {
|
|
236
|
+
persistedTo = await applyPersist(step.persist, value, ledger);
|
|
237
|
+
}
|
|
238
|
+
return {
|
|
239
|
+
stepId: step.id,
|
|
240
|
+
kind: step.kind,
|
|
241
|
+
completedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
242
|
+
...persistedTo ? { persistedTo } : {}
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
async function runExternalStep(step, ledger, head) {
|
|
246
|
+
if (step.description) process.stdout.write(`${head} ${step.description}
|
|
247
|
+
`);
|
|
248
|
+
process.stdout.write(`${head} opening ${step.url}
|
|
249
|
+
`);
|
|
250
|
+
const opener = platform() === "darwin" ? "open" : platform() === "win32" ? "cmd" : "xdg-open";
|
|
251
|
+
const args = platform() === "win32" ? ["/c", "start", step.url] : [step.url];
|
|
252
|
+
await new Promise((resolve) => {
|
|
253
|
+
const child = spawn(opener, args, { stdio: "ignore", detached: true });
|
|
254
|
+
child.once("error", () => resolve());
|
|
255
|
+
child.once("spawn", () => resolve());
|
|
256
|
+
});
|
|
257
|
+
let value = "";
|
|
258
|
+
if (step.callback?.param) {
|
|
259
|
+
value = await promptString(
|
|
260
|
+
`Paste the value of '${step.callback.param}' from the redirect`,
|
|
261
|
+
{ masked: false }
|
|
262
|
+
);
|
|
263
|
+
} else {
|
|
264
|
+
process.stdout.write(`${head} done \u2014 press Enter when finished.
|
|
265
|
+
`);
|
|
266
|
+
await promptString("(press Enter to continue)", { masked: false });
|
|
267
|
+
}
|
|
268
|
+
let persistedTo;
|
|
269
|
+
if (step.persist && value) {
|
|
270
|
+
persistedTo = await applyPersist(step.persist, value, ledger);
|
|
271
|
+
}
|
|
272
|
+
return {
|
|
273
|
+
stepId: step.id,
|
|
274
|
+
kind: step.kind,
|
|
275
|
+
completedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
276
|
+
...persistedTo ? { persistedTo } : {}
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
async function checkSkipIf(skip) {
|
|
280
|
+
const expectedCode = skip.exit_code ?? 0;
|
|
281
|
+
const captured = await runShellCapturing(skip.cmd, {
|
|
282
|
+
timeoutMs: skip.timeout_ms ?? 5e3,
|
|
283
|
+
interactive: false
|
|
284
|
+
});
|
|
285
|
+
return captured.exitCode === expectedCode;
|
|
286
|
+
}
|
|
287
|
+
async function applyPersist(persist, value, ledger) {
|
|
288
|
+
if ("env" in persist && persist.env) {
|
|
289
|
+
if (!ledger.envValues) ledger.envValues = {};
|
|
290
|
+
ledger.envValues[persist.env] = value;
|
|
291
|
+
return `env:${persist.env}`;
|
|
292
|
+
}
|
|
293
|
+
if ("secret_slug" in persist && persist.secret_slug) {
|
|
294
|
+
process.stdout.write(
|
|
295
|
+
`agentproto setup: store the prompted value in your secrets backend under slug '${persist.secret_slug}' (value not echoed; ledger records the slot only).
|
|
296
|
+
`
|
|
297
|
+
);
|
|
298
|
+
return `secret:${persist.secret_slug}`;
|
|
299
|
+
}
|
|
300
|
+
if ("cmd" in persist && persist.cmd) {
|
|
301
|
+
const cmd = persist.cmd.replace(/\$\{value\}/g, shellEscape(value));
|
|
302
|
+
const captured = await runShellCapturing(cmd, {
|
|
303
|
+
timeoutMs: 6e4,
|
|
304
|
+
interactive: false
|
|
305
|
+
});
|
|
306
|
+
if (captured.exitCode !== 0) {
|
|
307
|
+
throw new Error(`persist cmd exited ${captured.exitCode}`);
|
|
308
|
+
}
|
|
309
|
+
return "cmd";
|
|
310
|
+
}
|
|
311
|
+
throw new Error("persist block must declare exactly one of env / secret_slug / cmd");
|
|
312
|
+
}
|
|
313
|
+
async function promptString(question, opts) {
|
|
314
|
+
const isTty = process.stdin.isTTY ?? false;
|
|
315
|
+
const suffix = opts.defaultValue ? ` [${opts.defaultValue}]` : "";
|
|
316
|
+
const prompt = `${question}${suffix}: `;
|
|
317
|
+
if (opts.masked && !isTty) {
|
|
318
|
+
process.stdout.write(
|
|
319
|
+
`agentproto: stdin is not a TTY \u2014 masked input falls back to plain. Avoid piping secrets in non-interactive mode.
|
|
320
|
+
`
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
const rl = createInterface({ input: stdin, output: stdout, terminal: isTty });
|
|
324
|
+
if (opts.masked && isTty) {
|
|
325
|
+
process.stdin.setRawMode?.(true);
|
|
326
|
+
let buf = "";
|
|
327
|
+
process.stdout.write(prompt);
|
|
328
|
+
return new Promise((resolve) => {
|
|
329
|
+
const onData = (chunk) => {
|
|
330
|
+
for (const code of chunk) {
|
|
331
|
+
if (code === 13 || code === 10) {
|
|
332
|
+
process.stdin.off("data", onData);
|
|
333
|
+
process.stdin.setRawMode?.(false);
|
|
334
|
+
process.stdout.write("\n");
|
|
335
|
+
rl.close();
|
|
336
|
+
resolve(buf || opts.defaultValue || "");
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
if (code === 3) {
|
|
340
|
+
process.stdin.off("data", onData);
|
|
341
|
+
process.stdin.setRawMode?.(false);
|
|
342
|
+
rl.close();
|
|
343
|
+
process.exit(130);
|
|
344
|
+
}
|
|
345
|
+
if (code === 127 || code === 8) {
|
|
346
|
+
buf = buf.slice(0, -1);
|
|
347
|
+
continue;
|
|
348
|
+
}
|
|
349
|
+
buf += String.fromCharCode(code);
|
|
350
|
+
process.stdout.write("\u2022");
|
|
351
|
+
}
|
|
352
|
+
};
|
|
353
|
+
process.stdin.on("data", onData);
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
try {
|
|
357
|
+
const ans = (await rl.question(prompt)).trim();
|
|
358
|
+
return ans || opts.defaultValue || "";
|
|
359
|
+
} finally {
|
|
360
|
+
rl.close();
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
async function promptBoolean(question, defaultYes) {
|
|
364
|
+
const def = defaultYes ? "Y/n" : "y/N";
|
|
365
|
+
const ans = await promptString(`${question} [${def}]`, { masked: false });
|
|
366
|
+
if (!ans) return defaultYes;
|
|
367
|
+
return /^y(es)?$/i.test(ans);
|
|
368
|
+
}
|
|
369
|
+
async function promptSelect(question, options, defaultValue) {
|
|
370
|
+
process.stdout.write(`${question}
|
|
371
|
+
`);
|
|
372
|
+
for (const [i, o] of options.entries()) {
|
|
373
|
+
const star = defaultValue && o.value === defaultValue ? "*" : i === 0 ? " " : " ";
|
|
374
|
+
process.stdout.write(
|
|
375
|
+
` ${star} ${i + 1}) ${o.label ?? o.value}${o.label ? ` (${o.value})` : ""}
|
|
376
|
+
`
|
|
377
|
+
);
|
|
378
|
+
}
|
|
379
|
+
const ans = await promptString(
|
|
380
|
+
`Choose 1-${options.length}`,
|
|
381
|
+
defaultValue ? { defaultValue } : { masked: false }
|
|
382
|
+
);
|
|
383
|
+
const asNum = Number.parseInt(ans, 10);
|
|
384
|
+
if (Number.isFinite(asNum) && asNum >= 1 && asNum <= options.length) {
|
|
385
|
+
return options[asNum - 1].value;
|
|
386
|
+
}
|
|
387
|
+
const exact = options.find((o) => o.value === ans);
|
|
388
|
+
if (exact) return exact.value;
|
|
389
|
+
if (defaultValue && options.some((o) => o.value === defaultValue)) {
|
|
390
|
+
return defaultValue;
|
|
391
|
+
}
|
|
392
|
+
throw new Error(
|
|
393
|
+
`invalid selection '${ans}'; expected one of ${options.map((o) => o.value).join(", ")}`
|
|
394
|
+
);
|
|
395
|
+
}
|
|
396
|
+
async function resolveSelectOptions(options) {
|
|
397
|
+
if (!options) return [];
|
|
398
|
+
if (Array.isArray(options)) {
|
|
399
|
+
return options.map((v) => ({ value: v }));
|
|
400
|
+
}
|
|
401
|
+
const captured = await runShellCapturing(options.cmd, {
|
|
402
|
+
timeoutMs: options.timeout_ms ?? 3e4,
|
|
403
|
+
interactive: false
|
|
404
|
+
});
|
|
405
|
+
if (captured.exitCode !== 0) {
|
|
406
|
+
throw new Error(
|
|
407
|
+
`select options cmd exited ${captured.exitCode}: ${truncate(captured.stderr, 400)}`
|
|
408
|
+
);
|
|
409
|
+
}
|
|
410
|
+
return captured.stdout.split("\n").map((l) => l.trim()).filter(Boolean).map((line) => {
|
|
411
|
+
const [value, label] = line.split(" ");
|
|
412
|
+
const out = { value };
|
|
413
|
+
if (label) out.label = label;
|
|
414
|
+
return out;
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
async function runShellCapturing(cmd, opts) {
|
|
418
|
+
return new Promise((resolve) => {
|
|
419
|
+
const child = spawn("bash", ["-lc", cmd], {
|
|
420
|
+
stdio: opts.interactive ? "inherit" : ["ignore", "pipe", "pipe"]
|
|
421
|
+
});
|
|
422
|
+
let stdout = "";
|
|
423
|
+
let stderr = "";
|
|
424
|
+
child.stdout?.on("data", (c) => {
|
|
425
|
+
stdout += c.toString("utf8");
|
|
426
|
+
});
|
|
427
|
+
child.stderr?.on("data", (c) => {
|
|
428
|
+
stderr += c.toString("utf8");
|
|
429
|
+
});
|
|
430
|
+
const timer = setTimeout(() => {
|
|
431
|
+
child.kill("SIGTERM");
|
|
432
|
+
setTimeout(() => child.kill("SIGKILL"), 1e3);
|
|
433
|
+
}, opts.timeoutMs);
|
|
434
|
+
child.once("error", () => {
|
|
435
|
+
clearTimeout(timer);
|
|
436
|
+
resolve({ exitCode: 127, stdout, stderr: stderr || "spawn error" });
|
|
437
|
+
});
|
|
438
|
+
child.once("exit", (code) => {
|
|
439
|
+
clearTimeout(timer);
|
|
440
|
+
resolve({ exitCode: code ?? 0, stdout, stderr });
|
|
441
|
+
});
|
|
442
|
+
});
|
|
443
|
+
}
|
|
444
|
+
function shellEscape(s) {
|
|
445
|
+
return `'${s.replace(/'/g, `'"'"'`)}'`;
|
|
446
|
+
}
|
|
447
|
+
function truncate(s, max) {
|
|
448
|
+
return s.length <= max ? s : s.slice(0, max - 3) + "\u2026";
|
|
449
|
+
}
|
|
450
|
+
function ledgerPathFor(slug) {
|
|
451
|
+
const base = process.env["AGENTPROTO_HOME"] ?? join(homedir(), ".agentproto");
|
|
452
|
+
return join(base, "setup", `${slug}.json`);
|
|
453
|
+
}
|
|
454
|
+
async function loadLedger(path, slug) {
|
|
455
|
+
try {
|
|
456
|
+
const raw = await readFile(path, "utf8");
|
|
457
|
+
const parsed = JSON.parse(raw);
|
|
458
|
+
if (parsed.slug === slug && typeof parsed.steps === "object") return parsed;
|
|
459
|
+
} catch {
|
|
460
|
+
}
|
|
461
|
+
return {
|
|
462
|
+
slug,
|
|
463
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
464
|
+
steps: {}
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
async function saveLedger(path, ledger) {
|
|
468
|
+
ledger.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
469
|
+
await mkdir(dirname(path), { recursive: true });
|
|
470
|
+
await writeFile(path, JSON.stringify(ledger, null, 2));
|
|
471
|
+
await chmod(path, 384).catch(() => {
|
|
472
|
+
});
|
|
473
|
+
}
|
|
40
474
|
|
|
41
475
|
// src/commands/install.ts
|
|
42
476
|
async function runInstall(args) {
|
|
@@ -46,7 +480,8 @@ async function runInstall(args) {
|
|
|
46
480
|
strict: true,
|
|
47
481
|
options: {
|
|
48
482
|
force: { type: "boolean", short: "f" },
|
|
49
|
-
"dry-run": { type: "boolean" }
|
|
483
|
+
"dry-run": { type: "boolean" },
|
|
484
|
+
"skip-setup": { type: "boolean" }
|
|
50
485
|
}
|
|
51
486
|
});
|
|
52
487
|
const slug = positionals[0];
|
|
@@ -65,6 +500,7 @@ async function runInstall(args) {
|
|
|
65
500
|
);
|
|
66
501
|
return 0;
|
|
67
502
|
}
|
|
503
|
+
let alreadyInstalled = false;
|
|
68
504
|
if (!values.force) {
|
|
69
505
|
const checked = await runVersionCheck(adapter.handle.version_check);
|
|
70
506
|
if (checked.ok) {
|
|
@@ -72,26 +508,67 @@ async function runInstall(args) {
|
|
|
72
508
|
`agentproto: '${slug}' already installed (${checked.message}). Pass --force to reinstall.
|
|
73
509
|
`
|
|
74
510
|
);
|
|
75
|
-
|
|
511
|
+
alreadyInstalled = true;
|
|
76
512
|
}
|
|
77
513
|
}
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
514
|
+
if (!alreadyInstalled) {
|
|
515
|
+
let lastFailure = null;
|
|
516
|
+
let succeeded = false;
|
|
517
|
+
for (const [i, step] of installSteps.entries()) {
|
|
518
|
+
if (step.experimental) {
|
|
519
|
+
process.stdout.write(
|
|
520
|
+
`agentproto install [${i + 1}/${installSteps.length}] ${describeStep(step)} (experimental \u2014 skipping)
|
|
81
521
|
`
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
522
|
+
);
|
|
523
|
+
continue;
|
|
524
|
+
}
|
|
525
|
+
process.stdout.write(
|
|
526
|
+
`agentproto install [${i + 1}/${installSteps.length}] ${describeStep(step)}
|
|
527
|
+
`
|
|
528
|
+
);
|
|
529
|
+
if (values["dry-run"]) {
|
|
530
|
+
succeeded = true;
|
|
531
|
+
continue;
|
|
532
|
+
}
|
|
533
|
+
const code = await runStep(step);
|
|
534
|
+
if (code === 0) {
|
|
535
|
+
succeeded = true;
|
|
536
|
+
break;
|
|
537
|
+
}
|
|
538
|
+
lastFailure = { step, code };
|
|
539
|
+
process.stdout.write(
|
|
540
|
+
`agentproto install: method '${step.method}' returned ${code}; trying next.
|
|
541
|
+
`
|
|
542
|
+
);
|
|
543
|
+
}
|
|
544
|
+
if (!succeeded && lastFailure) {
|
|
86
545
|
process.stderr.write(
|
|
87
|
-
`agentproto install:
|
|
546
|
+
`agentproto install: all ${installSteps.length} install methods failed. Last failure: ${lastFailure.step.method} \u2192 exit ${lastFailure.code}.
|
|
88
547
|
`
|
|
89
548
|
);
|
|
90
|
-
return code;
|
|
549
|
+
return lastFailure.code;
|
|
91
550
|
}
|
|
551
|
+
if (!succeeded && !lastFailure) {
|
|
552
|
+
process.stderr.write(
|
|
553
|
+
`agentproto install: no usable install method found for '${slug}'.
|
|
554
|
+
`
|
|
555
|
+
);
|
|
556
|
+
return 1;
|
|
557
|
+
}
|
|
558
|
+
process.stdout.write(`agentproto: '${slug}' installed.
|
|
559
|
+
`);
|
|
92
560
|
}
|
|
93
|
-
|
|
561
|
+
if (!values["skip-setup"] && (adapter.handle.setup?.length ?? 0) > 0) {
|
|
562
|
+
process.stdout.write(`agentproto: running setup for '${slug}'\u2026
|
|
94
563
|
`);
|
|
564
|
+
const setupCode = await runSetup({
|
|
565
|
+
slug,
|
|
566
|
+
handle: adapter.handle,
|
|
567
|
+
force: values.force ?? false,
|
|
568
|
+
dryRun: values["dry-run"] ?? false
|
|
569
|
+
});
|
|
570
|
+
if (setupCode !== 0) return setupCode;
|
|
571
|
+
}
|
|
95
572
|
return 0;
|
|
96
573
|
}
|
|
97
574
|
function describeStep(step) {
|
|
@@ -101,16 +578,17 @@ function describeStep(step) {
|
|
|
101
578
|
case "brew":
|
|
102
579
|
return `brew install ${step.package ?? "(?)"}`;
|
|
103
580
|
case "pip":
|
|
104
|
-
return `pip install ${step.package ?? "(?)"}`;
|
|
581
|
+
return `pip install ${step.user ? "--user " : ""}${step.package ?? "(?)"}`;
|
|
105
582
|
case "cargo":
|
|
106
583
|
return `cargo install ${step.package ?? "(?)"}`;
|
|
107
584
|
case "go":
|
|
108
585
|
return `go install ${step.package ?? "(?)"}`;
|
|
109
586
|
case "curl":
|
|
587
|
+
return `curl ${step.url ?? "(?)"} | bash`;
|
|
110
588
|
case "download":
|
|
111
|
-
return
|
|
589
|
+
return `download ${step.url ?? "(?)"} \u2192 ${step.extract_bin ?? "(?)"}`;
|
|
112
590
|
default:
|
|
113
|
-
return `${step.method} ${step.package ?? step.url ?? "(?)"}`;
|
|
591
|
+
return `${step.method} ${step.package ?? step.url ?? step.path ?? "(?)"}`;
|
|
114
592
|
}
|
|
115
593
|
}
|
|
116
594
|
async function runStep(step) {
|
|
@@ -125,14 +603,198 @@ async function runStep(step) {
|
|
|
125
603
|
argv.push(step.package);
|
|
126
604
|
return spawnInherit("npm", argv);
|
|
127
605
|
}
|
|
606
|
+
case "brew": {
|
|
607
|
+
if (!step.package) {
|
|
608
|
+
process.stderr.write("agentproto install: brew step missing package.\n");
|
|
609
|
+
return 2;
|
|
610
|
+
}
|
|
611
|
+
return spawnInherit("brew", ["install", step.package]);
|
|
612
|
+
}
|
|
613
|
+
case "pip": {
|
|
614
|
+
if (!step.package) {
|
|
615
|
+
process.stderr.write("agentproto install: pip step missing package.\n");
|
|
616
|
+
return 2;
|
|
617
|
+
}
|
|
618
|
+
const argv = ["install"];
|
|
619
|
+
if (step.user) argv.push("--user");
|
|
620
|
+
argv.push(step.package);
|
|
621
|
+
return spawnInherit("pip", argv);
|
|
622
|
+
}
|
|
623
|
+
case "cargo": {
|
|
624
|
+
if (!step.package) {
|
|
625
|
+
process.stderr.write(
|
|
626
|
+
"agentproto install: cargo step missing package.\n"
|
|
627
|
+
);
|
|
628
|
+
return 2;
|
|
629
|
+
}
|
|
630
|
+
return spawnInherit("cargo", ["install", step.package]);
|
|
631
|
+
}
|
|
632
|
+
case "go": {
|
|
633
|
+
if (!step.package) {
|
|
634
|
+
process.stderr.write("agentproto install: go step missing package.\n");
|
|
635
|
+
return 2;
|
|
636
|
+
}
|
|
637
|
+
return spawnInherit("go", ["install", step.package]);
|
|
638
|
+
}
|
|
639
|
+
case "curl": {
|
|
640
|
+
if (!step.url) {
|
|
641
|
+
process.stderr.write("agentproto install: curl step missing url.\n");
|
|
642
|
+
return 2;
|
|
643
|
+
}
|
|
644
|
+
return runCurlInstaller(step.url, step.verify_sha256);
|
|
645
|
+
}
|
|
646
|
+
case "download": {
|
|
647
|
+
if (!step.url || !step.extract_bin) {
|
|
648
|
+
process.stderr.write(
|
|
649
|
+
"agentproto install: download step missing url or extract_bin.\n"
|
|
650
|
+
);
|
|
651
|
+
return 2;
|
|
652
|
+
}
|
|
653
|
+
return runDownloadInstaller({
|
|
654
|
+
url: step.url,
|
|
655
|
+
extractBin: step.extract_bin,
|
|
656
|
+
verifySha256: step.verify_sha256
|
|
657
|
+
});
|
|
658
|
+
}
|
|
128
659
|
default:
|
|
129
660
|
process.stderr.write(
|
|
130
|
-
`agentproto install: method '${step.method}' not yet implemented in this CLI version. Run it manually for now (see the adapter's
|
|
661
|
+
`agentproto install: method '${step.method}' not yet implemented in this CLI version. Run it manually for now (see the adapter's docs).
|
|
131
662
|
`
|
|
132
663
|
);
|
|
133
664
|
return 1;
|
|
134
665
|
}
|
|
135
666
|
}
|
|
667
|
+
async function runCurlInstaller(url, verifySha256) {
|
|
668
|
+
if (!verifySha256) {
|
|
669
|
+
process.stdout.write(
|
|
670
|
+
`agentproto install: curl ${url} (\u26A0 no verify_sha256 declared \u2014 installer integrity not verified)
|
|
671
|
+
`
|
|
672
|
+
);
|
|
673
|
+
}
|
|
674
|
+
const dir = await mkdtemp(join(tmpdir(), "agentproto-curl-"));
|
|
675
|
+
const scriptPath = join(dir, "install.sh");
|
|
676
|
+
try {
|
|
677
|
+
const res = await fetch(url, { redirect: "follow" });
|
|
678
|
+
if (!res.ok) {
|
|
679
|
+
process.stderr.write(
|
|
680
|
+
`agentproto install: curl fetch failed: ${res.status} ${res.statusText}
|
|
681
|
+
`
|
|
682
|
+
);
|
|
683
|
+
return res.status;
|
|
684
|
+
}
|
|
685
|
+
const buf = Buffer.from(await res.arrayBuffer());
|
|
686
|
+
if (verifySha256) {
|
|
687
|
+
const got = createHash("sha256").update(buf).digest("hex");
|
|
688
|
+
if (got !== verifySha256.toLowerCase()) {
|
|
689
|
+
process.stderr.write(
|
|
690
|
+
`agentproto install: SHA-256 mismatch for ${url}
|
|
691
|
+
expected: ${verifySha256}
|
|
692
|
+
got: ${got}
|
|
693
|
+
`
|
|
694
|
+
);
|
|
695
|
+
return 4;
|
|
696
|
+
}
|
|
697
|
+
process.stdout.write(
|
|
698
|
+
`agentproto install: SHA-256 verified (${got.slice(0, 16)}\u2026)
|
|
699
|
+
`
|
|
700
|
+
);
|
|
701
|
+
}
|
|
702
|
+
await writeFile(scriptPath, buf);
|
|
703
|
+
await chmod(scriptPath, 493);
|
|
704
|
+
return spawnInherit("bash", [scriptPath]);
|
|
705
|
+
} catch (err) {
|
|
706
|
+
process.stderr.write(
|
|
707
|
+
`agentproto install: curl install failed: ${err instanceof Error ? err.message : String(err)}
|
|
708
|
+
`
|
|
709
|
+
);
|
|
710
|
+
return 1;
|
|
711
|
+
} finally {
|
|
712
|
+
await rm(dir, { recursive: true, force: true }).catch(() => {
|
|
713
|
+
});
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
async function runDownloadInstaller(opts) {
|
|
717
|
+
const dir = await mkdtemp(join(tmpdir(), "agentproto-dl-"));
|
|
718
|
+
try {
|
|
719
|
+
const filename = opts.url.split("/").pop() || "archive.bin";
|
|
720
|
+
const archivePath = join(dir, filename);
|
|
721
|
+
const res = await fetch(opts.url, { redirect: "follow" });
|
|
722
|
+
if (!res.ok) {
|
|
723
|
+
process.stderr.write(
|
|
724
|
+
`agentproto install: download fetch failed: ${res.status} ${res.statusText}
|
|
725
|
+
`
|
|
726
|
+
);
|
|
727
|
+
return res.status;
|
|
728
|
+
}
|
|
729
|
+
const buf = Buffer.from(await res.arrayBuffer());
|
|
730
|
+
if (opts.verifySha256) {
|
|
731
|
+
const got = createHash("sha256").update(buf).digest("hex");
|
|
732
|
+
if (got !== opts.verifySha256.toLowerCase()) {
|
|
733
|
+
process.stderr.write(
|
|
734
|
+
`agentproto install: SHA-256 mismatch
|
|
735
|
+
expected: ${opts.verifySha256}
|
|
736
|
+
got: ${got}
|
|
737
|
+
`
|
|
738
|
+
);
|
|
739
|
+
return 4;
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
await writeFile(archivePath, buf);
|
|
743
|
+
const extractDir = join(dir, "extract");
|
|
744
|
+
await mkdir(extractDir, { recursive: true });
|
|
745
|
+
let extractCode;
|
|
746
|
+
if (filename.endsWith(".tar.gz") || filename.endsWith(".tgz") || filename.endsWith(".tar")) {
|
|
747
|
+
extractCode = await spawnInherit("tar", [
|
|
748
|
+
"-xf",
|
|
749
|
+
archivePath,
|
|
750
|
+
"-C",
|
|
751
|
+
extractDir
|
|
752
|
+
]);
|
|
753
|
+
} else if (filename.endsWith(".zip")) {
|
|
754
|
+
extractCode = await spawnInherit("unzip", [
|
|
755
|
+
"-q",
|
|
756
|
+
archivePath,
|
|
757
|
+
"-d",
|
|
758
|
+
extractDir
|
|
759
|
+
]);
|
|
760
|
+
} else {
|
|
761
|
+
process.stderr.write(
|
|
762
|
+
`agentproto install: unsupported archive format for ${filename}. Supported: .tar, .tar.gz, .tgz, .zip.
|
|
763
|
+
`
|
|
764
|
+
);
|
|
765
|
+
return 5;
|
|
766
|
+
}
|
|
767
|
+
if (extractCode !== 0) return extractCode;
|
|
768
|
+
const srcBin = join(extractDir, opts.extractBin);
|
|
769
|
+
const binDir = process.env["AGENTPROTO_BIN_DIR"] ?? join(homedir2(), ".local", "bin");
|
|
770
|
+
await mkdir(binDir, { recursive: true });
|
|
771
|
+
const destBin = join(binDir, opts.extractBin.split("/").pop());
|
|
772
|
+
const cpCode = await spawnInherit("cp", ["-p", srcBin, destBin]);
|
|
773
|
+
if (cpCode !== 0) return cpCode;
|
|
774
|
+
await chmod(destBin, 493);
|
|
775
|
+
process.stdout.write(`agentproto install: ${destBin}
|
|
776
|
+
`);
|
|
777
|
+
if (!process.env["PATH"]?.includes(binDir)) {
|
|
778
|
+
process.stdout.write(
|
|
779
|
+
`agentproto install: \u26A0 ${binDir} is not on PATH \u2014 add it to your shell profile so the binary is reachable.
|
|
780
|
+
`
|
|
781
|
+
);
|
|
782
|
+
}
|
|
783
|
+
return 0;
|
|
784
|
+
} catch (err) {
|
|
785
|
+
process.stderr.write(
|
|
786
|
+
`agentproto install: download failed: ${err instanceof Error ? err.message : String(err)}
|
|
787
|
+
`
|
|
788
|
+
);
|
|
789
|
+
return 1;
|
|
790
|
+
} finally {
|
|
791
|
+
await rm(dir, { recursive: true, force: true }).catch(() => {
|
|
792
|
+
});
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
function homedir2() {
|
|
796
|
+
return process.env["HOME"] ?? process.env["USERPROFILE"] ?? "/";
|
|
797
|
+
}
|
|
136
798
|
function spawnInherit(cmd, argv) {
|
|
137
799
|
return new Promise((resolve, reject) => {
|
|
138
800
|
const child = spawn(cmd, argv, { stdio: "inherit" });
|
|
@@ -270,12 +932,82 @@ function printPretty(ev) {
|
|
|
270
932
|
\x1B[2m[turn-end: ${ev.reason}]\x1B[0m
|
|
271
933
|
`);
|
|
272
934
|
break;
|
|
273
|
-
case "error":
|
|
274
|
-
|
|
935
|
+
case "error": {
|
|
936
|
+
const code = typeof ev.error.code === "number" ? ` (code ${ev.error.code})` : "";
|
|
937
|
+
process.stderr.write(
|
|
938
|
+
`\x1B[31m[error]${code} ${ev.error.message}\x1B[0m
|
|
939
|
+
`
|
|
940
|
+
);
|
|
941
|
+
const data = ev.error.data;
|
|
942
|
+
if (data && typeof data === "object") {
|
|
943
|
+
const stderr = data.stderr;
|
|
944
|
+
if (typeof stderr === "string" && stderr.trim()) {
|
|
945
|
+
process.stderr.write(`\x1B[2m\u2500\u2500 child stderr \u2500\u2500
|
|
946
|
+
${stderr}\x1B[0m
|
|
275
947
|
`);
|
|
948
|
+
}
|
|
949
|
+
const rest = { ...data };
|
|
950
|
+
delete rest.stderr;
|
|
951
|
+
if (Object.keys(rest).length > 0) {
|
|
952
|
+
process.stderr.write(
|
|
953
|
+
`\x1B[2m\u2500\u2500 error.data \u2500\u2500
|
|
954
|
+
${JSON.stringify(rest, null, 2)}\x1B[0m
|
|
955
|
+
`
|
|
956
|
+
);
|
|
957
|
+
}
|
|
958
|
+
}
|
|
276
959
|
break;
|
|
960
|
+
}
|
|
277
961
|
}
|
|
278
962
|
}
|
|
963
|
+
var FILE_VERSION = 1;
|
|
964
|
+
function credentialsPath() {
|
|
965
|
+
const base = process.env["AGENTPROTO_HOME"] ?? join(homedir(), ".agentproto");
|
|
966
|
+
return join(base, "credentials.json");
|
|
967
|
+
}
|
|
968
|
+
function normaliseHost(host) {
|
|
969
|
+
return host.replace(/\/+$/, "");
|
|
970
|
+
}
|
|
971
|
+
async function loadCredentials() {
|
|
972
|
+
const path = credentialsPath();
|
|
973
|
+
try {
|
|
974
|
+
const raw = await readFile(path, "utf8");
|
|
975
|
+
const parsed = JSON.parse(raw);
|
|
976
|
+
if (parsed.version !== FILE_VERSION) {
|
|
977
|
+
throw new Error(
|
|
978
|
+
`credentials.json: unknown version ${parsed.version}; expected ${FILE_VERSION}. Delete the file and re-run \`agentproto auth login\`.`
|
|
979
|
+
);
|
|
980
|
+
}
|
|
981
|
+
return parsed;
|
|
982
|
+
} catch (err) {
|
|
983
|
+
if (err.code === "ENOENT") {
|
|
984
|
+
return { version: FILE_VERSION, hosts: {} };
|
|
985
|
+
}
|
|
986
|
+
throw err;
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
async function readHost(host) {
|
|
990
|
+
const f = await loadCredentials();
|
|
991
|
+
return f.hosts[normaliseHost(host)] ?? null;
|
|
992
|
+
}
|
|
993
|
+
function isExpired(cred, gracePeriodMs = 3e4) {
|
|
994
|
+
const exp = Date.parse(cred.expiresAt);
|
|
995
|
+
if (!Number.isFinite(exp)) return false;
|
|
996
|
+
return exp - gracePeriodMs <= Date.now();
|
|
997
|
+
}
|
|
998
|
+
function formatExpiry(cred) {
|
|
999
|
+
const exp = new Date(cred.expiresAt);
|
|
1000
|
+
if (Number.isNaN(exp.getTime())) return "unknown";
|
|
1001
|
+
const ms = exp.getTime() - Date.now();
|
|
1002
|
+
if (ms < 0) return `expired ${formatRelative(-ms)} ago`;
|
|
1003
|
+
return `expires in ${formatRelative(ms)}`;
|
|
1004
|
+
}
|
|
1005
|
+
function formatRelative(ms) {
|
|
1006
|
+
if (ms < 6e4) return `${Math.round(ms / 1e3)}s`;
|
|
1007
|
+
if (ms < 36e5) return `${Math.round(ms / 6e4)}m`;
|
|
1008
|
+
if (ms < 864e5) return `${Math.round(ms / 36e5)}h`;
|
|
1009
|
+
return `${Math.round(ms / 864e5)}d`;
|
|
1010
|
+
}
|
|
279
1011
|
async function runServe(args) {
|
|
280
1012
|
const { values } = parseArgs({
|
|
281
1013
|
args: [...args],
|
|
@@ -284,25 +1016,239 @@ async function runServe(args) {
|
|
|
284
1016
|
options: {
|
|
285
1017
|
connect: { type: "string", short: "c" },
|
|
286
1018
|
token: { type: "string", short: "t" },
|
|
287
|
-
label: { type: "string", short: "l" }
|
|
1019
|
+
label: { type: "string", short: "l" },
|
|
1020
|
+
workspace: { type: "string", short: "w" },
|
|
1021
|
+
port: { type: "string", short: "p" },
|
|
1022
|
+
bind: { type: "string", short: "b" }
|
|
288
1023
|
}
|
|
289
1024
|
});
|
|
290
|
-
|
|
1025
|
+
const workspace = resolve(values.workspace ?? process.cwd());
|
|
1026
|
+
try {
|
|
1027
|
+
const stat = await promises.stat(workspace);
|
|
1028
|
+
if (!stat.isDirectory()) {
|
|
1029
|
+
process.stderr.write(
|
|
1030
|
+
`agentproto serve: --workspace "${workspace}" is not a directory.
|
|
1031
|
+
`
|
|
1032
|
+
);
|
|
1033
|
+
return 2;
|
|
1034
|
+
}
|
|
1035
|
+
} catch {
|
|
291
1036
|
process.stderr.write(
|
|
292
|
-
|
|
1037
|
+
`agentproto serve: --workspace "${workspace}" does not exist.
|
|
1038
|
+
Create it first: mkdir -p "${workspace}"
|
|
1039
|
+
`
|
|
293
1040
|
);
|
|
294
1041
|
return 2;
|
|
295
1042
|
}
|
|
1043
|
+
const port = values.port ? Number.parseInt(values.port, 10) : 18790;
|
|
1044
|
+
if (!Number.isFinite(port) || port <= 0 || port > 65535) {
|
|
1045
|
+
process.stderr.write(`agentproto serve: invalid --port "${values.port}".
|
|
1046
|
+
`);
|
|
1047
|
+
return 2;
|
|
1048
|
+
}
|
|
1049
|
+
const label = values.label ?? `${userInfo().username}@${hostname()}`;
|
|
1050
|
+
let token = values.token ?? process.env.AGENTPROTO_TOKEN;
|
|
1051
|
+
if (!token && values.connect) {
|
|
1052
|
+
const cred = await readHost(values.connect);
|
|
1053
|
+
if (cred) {
|
|
1054
|
+
if (isExpired(cred)) {
|
|
1055
|
+
process.stderr.write(
|
|
1056
|
+
`agentproto serve: \u26A0 credentials for ${values.connect} are expired (${formatExpiry(cred)}). Re-run \`agentproto auth login --host ${values.connect}\`.
|
|
1057
|
+
`
|
|
1058
|
+
);
|
|
1059
|
+
}
|
|
1060
|
+
token = cred.token;
|
|
1061
|
+
process.stdout.write(
|
|
1062
|
+
`agentproto serve: using token from credentials.json (${formatExpiry(cred)})
|
|
1063
|
+
`
|
|
1064
|
+
);
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
const opts = {
|
|
1068
|
+
workspace,
|
|
1069
|
+
port,
|
|
1070
|
+
bind: values.bind ?? "127.0.0.1",
|
|
1071
|
+
...values.connect ? { connect: values.connect } : {},
|
|
1072
|
+
...token ? { token } : {},
|
|
1073
|
+
label
|
|
1074
|
+
};
|
|
1075
|
+
const resolveAgentAdapter = async (slug) => {
|
|
1076
|
+
try {
|
|
1077
|
+
const adapter = await resolveAdapter(slug);
|
|
1078
|
+
const runtime = createAgentCliRuntime(adapter.handle);
|
|
1079
|
+
return {
|
|
1080
|
+
async startSession({ cwd }) {
|
|
1081
|
+
return runtime.start({ cwd });
|
|
1082
|
+
},
|
|
1083
|
+
commandPreview: `${adapter.handle.bin} ${(adapter.handle.bin_args ?? []).join(" ")}`.trim()
|
|
1084
|
+
};
|
|
1085
|
+
} catch (err) {
|
|
1086
|
+
console.warn(
|
|
1087
|
+
`[agentproto serve] resolveAgentAdapter('${slug}') failed: ${err instanceof Error ? err.message : String(err)}`
|
|
1088
|
+
);
|
|
1089
|
+
return null;
|
|
1090
|
+
}
|
|
1091
|
+
};
|
|
1092
|
+
let gateway;
|
|
1093
|
+
try {
|
|
1094
|
+
gateway = await createGateway({
|
|
1095
|
+
workspace: opts.workspace,
|
|
1096
|
+
port: opts.port,
|
|
1097
|
+
bind: opts.bind,
|
|
1098
|
+
specs: [],
|
|
1099
|
+
name: "agentproto-serve",
|
|
1100
|
+
// BOOT.md is silly for a tunnel daemon — skip it.
|
|
1101
|
+
boot: false,
|
|
1102
|
+
resolveAgentAdapter,
|
|
1103
|
+
// Discovery for UIs / operators — `GET /adapters` + `list_adapters`
|
|
1104
|
+
// MCP tool. Walks node_modules @agentproto/adapter-* on each call;
|
|
1105
|
+
// cheap enough that we don't bother caching here.
|
|
1106
|
+
listAgentAdapters: listInstalledAdapters
|
|
1107
|
+
});
|
|
1108
|
+
} catch (err) {
|
|
1109
|
+
process.stderr.write(
|
|
1110
|
+
`agentproto serve: gateway boot failed \u2014 ${err instanceof Error ? err.message : String(err)}
|
|
1111
|
+
`
|
|
1112
|
+
);
|
|
1113
|
+
return 1;
|
|
1114
|
+
}
|
|
1115
|
+
process.stderr.write(
|
|
1116
|
+
`agentproto serve: gateway up on ${gateway.url}
|
|
1117
|
+
workspace: ${gateway.workspace}
|
|
1118
|
+
mcp: ${gateway.url}/mcp
|
|
1119
|
+
sessions: ${gateway.url}/sessions
|
|
1120
|
+
events: ${gateway.url}/events
|
|
1121
|
+
`
|
|
1122
|
+
);
|
|
1123
|
+
const aborter = new AbortController();
|
|
1124
|
+
let shuttingDown = false;
|
|
1125
|
+
const shutdown = async (signal) => {
|
|
1126
|
+
if (shuttingDown) return;
|
|
1127
|
+
shuttingDown = true;
|
|
1128
|
+
process.stderr.write(`
|
|
1129
|
+
agentproto serve: ${signal} \u2014 shutting down.
|
|
1130
|
+
`);
|
|
1131
|
+
aborter.abort();
|
|
1132
|
+
await gateway.stop().catch(() => void 0);
|
|
1133
|
+
process.exit(0);
|
|
1134
|
+
};
|
|
1135
|
+
process.once("SIGINT", () => void shutdown("SIGINT"));
|
|
1136
|
+
process.once("SIGTERM", () => void shutdown("SIGTERM"));
|
|
1137
|
+
if (!opts.connect) {
|
|
1138
|
+
process.stderr.write(
|
|
1139
|
+
`agentproto serve: running local-only (no --connect). Press Ctrl-C to stop.
|
|
1140
|
+
`
|
|
1141
|
+
);
|
|
1142
|
+
await new Promise((resolve) => {
|
|
1143
|
+
aborter.signal.addEventListener("abort", () => resolve());
|
|
1144
|
+
});
|
|
1145
|
+
return 0;
|
|
1146
|
+
}
|
|
296
1147
|
process.stderr.write(
|
|
297
|
-
`agentproto serve: tunnel
|
|
298
|
-
Tracking: tunnel frame protocol is landing in @agentproto/acp next.
|
|
299
|
-
Wanted endpoint: ${values.connect}
|
|
300
|
-
Label: ${values.label ?? "(none)"}
|
|
1148
|
+
`agentproto serve: tunnel \u2014 connecting to ${opts.connect} as '${opts.label}'\u2026
|
|
301
1149
|
`
|
|
302
1150
|
);
|
|
303
|
-
|
|
1151
|
+
let backoffMs = opts.reconnectMinMs ?? 1e3;
|
|
1152
|
+
const backoffMax = opts.reconnectMaxMs ?? 3e4;
|
|
1153
|
+
while (!aborter.signal.aborted) {
|
|
1154
|
+
try {
|
|
1155
|
+
await runOneTunnel(opts, gateway, aborter.signal);
|
|
1156
|
+
backoffMs = opts.reconnectMinMs ?? 1e3;
|
|
1157
|
+
} catch (err) {
|
|
1158
|
+
if (aborter.signal.aborted) break;
|
|
1159
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1160
|
+
process.stderr.write(
|
|
1161
|
+
`agentproto serve: tunnel error: ${msg}
|
|
1162
|
+
reconnecting in ${backoffMs}ms\u2026
|
|
1163
|
+
`
|
|
1164
|
+
);
|
|
1165
|
+
await sleep(backoffMs, aborter.signal);
|
|
1166
|
+
backoffMs = Math.min(backoffMs * 2, backoffMax);
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
return 0;
|
|
1170
|
+
}
|
|
1171
|
+
async function runOneTunnel(opts, gateway, signal) {
|
|
1172
|
+
if (!opts.connect) throw new Error("runOneTunnel: --connect not set");
|
|
1173
|
+
const headers = {
|
|
1174
|
+
"user-agent": "agentproto/0.1.0-alpha"
|
|
1175
|
+
};
|
|
1176
|
+
if (opts.token) headers.authorization = `Bearer ${opts.token}`;
|
|
1177
|
+
const ws = new WebSocket(opts.connect, { headers });
|
|
1178
|
+
await new Promise((resolve, reject) => {
|
|
1179
|
+
const onOpen = () => {
|
|
1180
|
+
ws.off("error", onError);
|
|
1181
|
+
resolve();
|
|
1182
|
+
};
|
|
1183
|
+
const onError = (err) => {
|
|
1184
|
+
ws.off("open", onOpen);
|
|
1185
|
+
reject(err);
|
|
1186
|
+
};
|
|
1187
|
+
ws.once("open", onOpen);
|
|
1188
|
+
ws.once("error", onError);
|
|
1189
|
+
if (signal.aborted) {
|
|
1190
|
+
ws.close();
|
|
1191
|
+
reject(new Error("Aborted before WS opened."));
|
|
1192
|
+
}
|
|
1193
|
+
signal.addEventListener("abort", () => {
|
|
1194
|
+
ws.close();
|
|
1195
|
+
reject(new Error("Aborted while WS connecting."));
|
|
1196
|
+
});
|
|
1197
|
+
});
|
|
1198
|
+
process.stderr.write(`agentproto serve: tunnel up.
|
|
1199
|
+
`);
|
|
1200
|
+
const sink = wrapWebSocket(ws);
|
|
1201
|
+
const server = createTunnelServer({
|
|
1202
|
+
sink,
|
|
1203
|
+
label: opts.label,
|
|
1204
|
+
pty: false,
|
|
1205
|
+
// v0 authorize hook: trust the bearer-authenticated host completely.
|
|
1206
|
+
// Token possession proves the host was provisioned for this daemon.
|
|
1207
|
+
// Per-spawn policy filtering will land alongside the policy.toml.
|
|
1208
|
+
authorize: (req) => req,
|
|
1209
|
+
// ── AIP-46 bridge: tunnel spawns land in the gateway registry ──
|
|
1210
|
+
// Every cloud-driven spawn shows up in `gateway.url/sessions`
|
|
1211
|
+
// and the LocalDaemonSessionsCard, alongside spawns originated
|
|
1212
|
+
// through MCP tools or POST /sessions/agent. The execId is the
|
|
1213
|
+
// host's stable id — keep it so cloud cli_sessions rows can
|
|
1214
|
+
// cross-ref the daemon-side descriptor.
|
|
1215
|
+
onChildSpawned: ({ execId, child, request }) => {
|
|
1216
|
+
gateway.sessions.register({
|
|
1217
|
+
id: execId,
|
|
1218
|
+
child,
|
|
1219
|
+
workspaceSlug: opts.label,
|
|
1220
|
+
command: [request.command, ...request.args].join(" "),
|
|
1221
|
+
kind: "agent-cli",
|
|
1222
|
+
label: `tunnel: ${request.command.split("/").pop() ?? request.command}`
|
|
1223
|
+
});
|
|
1224
|
+
}
|
|
1225
|
+
});
|
|
1226
|
+
await new Promise((resolve) => {
|
|
1227
|
+
const offClose = sink.onClose(() => {
|
|
1228
|
+
offClose();
|
|
1229
|
+
resolve();
|
|
1230
|
+
});
|
|
1231
|
+
if (signal.aborted) {
|
|
1232
|
+
server.close().finally(() => resolve());
|
|
1233
|
+
}
|
|
1234
|
+
signal.addEventListener("abort", () => {
|
|
1235
|
+
server.close().finally(() => resolve());
|
|
1236
|
+
});
|
|
1237
|
+
});
|
|
1238
|
+
process.stderr.write(`agentproto serve: tunnel closed.
|
|
1239
|
+
`);
|
|
1240
|
+
}
|
|
1241
|
+
function sleep(ms, signal) {
|
|
1242
|
+
return new Promise((resolve) => {
|
|
1243
|
+
if (signal.aborted) return resolve();
|
|
1244
|
+
const timer = setTimeout(resolve, ms);
|
|
1245
|
+
signal.addEventListener("abort", () => {
|
|
1246
|
+
clearTimeout(timer);
|
|
1247
|
+
resolve();
|
|
1248
|
+
});
|
|
1249
|
+
});
|
|
304
1250
|
}
|
|
305
1251
|
|
|
306
|
-
export { resolveAdapter, runInstall, runRun, runServe };
|
|
1252
|
+
export { listInstalledAdapters, resolveAdapter, runInstall, runRun, runServe };
|
|
307
1253
|
//# sourceMappingURL=index.mjs.map
|
|
308
1254
|
//# sourceMappingURL=index.mjs.map
|