@agentdocumentformat/cli 0.7.8 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (100) hide show
  1. package/README.md +1 -1
  2. package/dist/adf.mjs +59 -8
  3. package/dist/chunks/{APEv2Parser-2CNNHMXV.mjs → APEv2Parser-WIXRJLHB.mjs} +2 -2
  4. package/dist/chunks/{AiffParser-7BR4RL6G.mjs → AiffParser-ZC4QFOVJ.mjs} +5 -5
  5. package/dist/chunks/{App-7RIIOHJJ.mjs → App-A5GUFCFJ.mjs} +279 -293
  6. package/dist/chunks/{DsdiffParser-CMD4AX7A.mjs → DsdiffParser-UAPAJHFX.mjs} +5 -5
  7. package/dist/chunks/{DsfParser-UOGZDS2M.mjs → DsfParser-NNYZK2QX.mjs} +6 -6
  8. package/dist/chunks/{FlacParser-HB3T3CD4.mjs → FlacParser-MW2W4LO3.mjs} +6 -6
  9. package/dist/chunks/{MP4Parser-66HBRUSX.mjs → MP4Parser-Z57SYOOQ.mjs} +4 -4
  10. package/dist/chunks/{MpegParser-YW6LIUEW.mjs → MpegParser-XIIPAZVM.mjs} +7 -7
  11. package/dist/chunks/{MusepackParser-PAUSXP7D.mjs → MusepackParser-QWNDEYHJ.mjs} +6 -6
  12. package/dist/chunks/{OggParser-NUBV3GWC.mjs → OggParser-WRX53F77.mjs} +9 -9
  13. package/dist/chunks/{WavPackParser-WQAGT7TC.mjs → WavPackParser-MJ7WCSJ5.mjs} +2 -2
  14. package/dist/chunks/{WaveParser-EWLQVDX4.mjs → WaveParser-RAVLHZ5S.mjs} +7 -7
  15. package/dist/chunks/{adf-schema-US4NFHMR.mjs → adf-schema-YJSGLMA4.mjs} +2 -2
  16. package/dist/chunks/auth-manager-AJ32FZWS.mjs +19 -0
  17. package/dist/chunks/auth-manager-C5OBEQK2.mjs +18 -0
  18. package/dist/chunks/auth-manager-TBNKCJ45.mjs +17 -0
  19. package/dist/chunks/{chatgpt-subscription-E4MXTIAT.mjs → chatgpt-subscription-MMGNW5KJ.mjs} +5 -2
  20. package/dist/chunks/chunk-3UGZFWIF.mjs +26 -0
  21. package/dist/chunks/{chunk-5PO5PKRV.mjs → chunk-4OGZFUKA.mjs} +126 -67
  22. package/dist/chunks/{chunk-VN7UH7I5.mjs → chunk-6XQZGYMK.mjs} +246 -67
  23. package/dist/chunks/{chunk-XNPZBLYL.mjs → chunk-75DC5EWN.mjs} +13 -2
  24. package/dist/chunks/chunk-AZYOXXVL.mjs +865 -0
  25. package/dist/chunks/{chunk-6VQEXLE2.mjs → chunk-BHY7QDCM.mjs} +41 -3
  26. package/dist/chunks/chunk-C2S64Y6F.mjs +203 -0
  27. package/dist/chunks/chunk-C4GA5SI4.mjs +12799 -0
  28. package/dist/chunks/chunk-DDTTSKGG.mjs +2443 -0
  29. package/dist/chunks/{chunk-VAUOM3IN.mjs → chunk-DLWCKKIO.mjs} +1 -1
  30. package/dist/chunks/{chunk-QPLEAE6V.mjs → chunk-G6NBFYG5.mjs} +152 -824
  31. package/dist/chunks/{chunk-KXEF4F4E.mjs → chunk-GAZO6UMI.mjs} +1 -1
  32. package/dist/chunks/{chunk-XYTWGHFS.mjs → chunk-H3XLRWVE.mjs} +1 -1
  33. package/dist/chunks/{chunk-7T2YMTWL.mjs → chunk-H4C65MTP.mjs} +4 -4
  34. package/dist/chunks/chunk-HBOFZRUS.mjs +738 -0
  35. package/dist/chunks/chunk-JVPZIMXW.mjs +976 -0
  36. package/dist/chunks/{chunk-XYV3VCIX.mjs → chunk-JZFONJSL.mjs} +79 -20
  37. package/dist/chunks/chunk-KDDVQKCG.mjs +65 -0
  38. package/dist/chunks/chunk-KRPKWOMB.mjs +290 -0
  39. package/dist/chunks/{chunk-G6PH7RPW.mjs → chunk-LCMANRYY.mjs} +3 -3
  40. package/dist/chunks/{chunk-ZESQMP66.mjs → chunk-LGL6ML37.mjs} +3 -3
  41. package/dist/chunks/chunk-LKFHERRV.mjs +145 -0
  42. package/dist/chunks/chunk-LTX74NU6.mjs +29 -0
  43. package/dist/chunks/chunk-M2F7C2UJ.mjs +425 -0
  44. package/dist/chunks/{chunk-62NE4LG4.mjs → chunk-MQAMI5BF.mjs} +5 -5
  45. package/dist/chunks/chunk-MZRXFWPQ.mjs +278 -0
  46. package/dist/chunks/{chunk-PRMGZZM5.mjs → chunk-NZLEPJ5M.mjs} +2 -0
  47. package/dist/chunks/{chunk-CD3ATSXE.mjs → chunk-NZO5O4VN.mjs} +1 -1
  48. package/dist/chunks/{chunk-I3UYSITV.mjs → chunk-O7XKQ6CH.mjs} +1 -1
  49. package/dist/chunks/chunk-P3RG5LQA.mjs +244 -0
  50. package/dist/chunks/chunk-PAK5JCO2.mjs +48 -0
  51. package/dist/chunks/chunk-PC63YFB7.mjs +34 -0
  52. package/dist/chunks/{daemon-control-HKFVSQTQ.mjs → chunk-PRPTKMQW.mjs} +67 -26
  53. package/dist/chunks/{chunk-LFMB4PAA.mjs → chunk-TKXIH27X.mjs} +974 -113
  54. package/dist/chunks/{chunk-5W7KES6P.mjs → chunk-UZPDETEU.mjs} +1 -1
  55. package/dist/chunks/chunk-V2T6NZX2.mjs +517 -0
  56. package/dist/chunks/chunk-V3MM6PCH.mjs +366 -0
  57. package/dist/chunks/chunk-WJGPNHGX.mjs +67 -0
  58. package/dist/chunks/chunk-WVTPPOKK.mjs +290 -0
  59. package/dist/chunks/{chunk-J74STFKE.mjs → chunk-WX6QCUWI.mjs} +272 -51
  60. package/dist/chunks/{chunk-Y5ZLKFLE.mjs → chunk-XNNJQCZP.mjs} +196 -165
  61. package/dist/chunks/{chunk-HDBPBORT.mjs → chunk-XT2BJUFC.mjs} +15 -6
  62. package/dist/chunks/{chunk-HSBV6YFU.mjs → chunk-YRMKSHW3.mjs} +614 -92
  63. package/dist/chunks/{chunk-BNXUAOO4.mjs → chunk-ZGXUOAWZ.mjs} +3186 -959
  64. package/dist/chunks/chunk-ZPB3HYM7.mjs +45 -0
  65. package/dist/chunks/claude-subscription-33BMZB6I.mjs +29 -0
  66. package/dist/chunks/{cli-NMFIAR7Z.mjs → cli-I47IFDHJ.mjs} +797 -56
  67. package/dist/chunks/{commands-KPJWFRUE.mjs → commands-E4EQUHE5.mjs} +4 -3
  68. package/dist/chunks/{daemon-YDIK3AAM.mjs → daemon-FQ5BV3AP.mjs} +82058 -63958
  69. package/dist/chunks/daemon-control-L3CLG63P.mjs +59 -0
  70. package/dist/chunks/{daemon-token-WRSULP7G.mjs → daemon-token-L5QKTHU5.mjs} +3 -1
  71. package/dist/chunks/{discord-DSP4HUWI.mjs → discord-IR62565M.mjs} +3 -52
  72. package/dist/chunks/{email-BRG7KUO2.mjs → email-KHHTJBDW.mjs} +4 -2
  73. package/dist/chunks/{grok-subscription-4PYX7TBF.mjs → grok-subscription-E74FS5EG.mjs} +5 -2
  74. package/dist/chunks/{lib-N7FCZWKH.mjs → lib-L27L5PXM.mjs} +22 -22
  75. package/dist/chunks/{open-VAU7PXT6.mjs → open-TLGS2Z3Q.mjs} +4 -4
  76. package/dist/chunks/{parser-JVY2PWSM.mjs → parser-7GARNT5L.mjs} +1 -1
  77. package/dist/chunks/{pipeline-executor-O77NTGLP.mjs → pipeline-executor-FD4FBWQA.mjs} +6 -5
  78. package/dist/chunks/prompt-RRHZX4YT.mjs +13 -0
  79. package/dist/chunks/remote-access-cli-XXAPQWH6.mjs +268 -0
  80. package/dist/chunks/remote-hello-UFWUBDQI.mjs +37 -0
  81. package/dist/chunks/sdk-5FD2ZP7N.mjs +28422 -0
  82. package/dist/chunks/{slack-OCNNF4DX.mjs → slack-IYC5RAB5.mjs} +3 -4
  83. package/dist/chunks/{store-HUVDJPYN.mjs → store-ZRN2WCFB.mjs} +6 -3
  84. package/dist/chunks/tailscale-WLJATFMB.mjs +25 -0
  85. package/dist/chunks/{theme-SFE3JMA2.mjs → theme-HLBDTF6F.mjs} +1 -1
  86. package/dist/chunks/{themes-YLXLOIJF.mjs → themes-BQ72P446.mjs} +2 -2
  87. package/dist/chunks/{token-6G3HCMNX.mjs → token-6D22WECM.mjs} +20 -16
  88. package/dist/chunks/{token-util-UXYSDQIW.mjs → token-util-YF3ZWBZ4.mjs} +1 -2
  89. package/dist/chunks/{tui-UQJLK7XI.mjs → tui-SPSPDADM.mjs} +17 -14
  90. package/dist/chunks/{welcome-XHXFV7SH.mjs → welcome-J5JZZAM5.mjs} +1 -1
  91. package/dist/chunks/{whatsapp-AJMRQBT7.mjs → whatsapp-YJFFVN4Q.mjs} +9 -9
  92. package/package.json +1 -1
  93. package/dist/chunks/chunk-GKKCMXIZ.mjs +0 -94
  94. package/dist/chunks/chunk-IHDDCZBS.mjs +0 -112
  95. package/dist/chunks/chunk-KWFOS5WC.mjs +0 -41
  96. package/dist/chunks/chunk-UEZR2CNS.mjs +0 -65
  97. package/dist/chunks/chunk-VMHTYDH4.mjs +0 -172
  98. package/dist/chunks/chunk-Y2QA53AN.mjs +0 -259
  99. package/dist/chunks/{chunk-NBTUWMGP.mjs → chunk-2MTE22CR.mjs} +3 -3
  100. package/dist/chunks/{telegram-2EFGKIVK.mjs → telegram-JYY2VDTI.mjs} +5 -5
@@ -7,24 +7,511 @@ const __dirname = __adfDirname(__filename);
7
7
  import {
8
8
  AUTH_PROVIDER_LABELS,
9
9
  loginChatGpt,
10
+ loginClaude,
10
11
  loginGrok,
11
12
  normalizeAuthProvider,
12
- openBrowser
13
- } from "./chunk-6VQEXLE2.mjs";
13
+ openBrowser,
14
+ submitClaudeCode
15
+ } from "./chunk-BHY7QDCM.mjs";
16
+ import {
17
+ MdnsService
18
+ } from "./chunk-DDTTSKGG.mjs";
19
+ import {
20
+ TailscaleClient,
21
+ defaultExec
22
+ } from "./chunk-V3MM6PCH.mjs";
23
+ import "./chunk-KDDVQKCG.mjs";
14
24
  import "./chunk-2NAP2ZF7.mjs";
15
25
  import {
26
+ UNSAFE_DISPLAY_CHARS,
27
+ cleanDisplayName,
28
+ fetchHello,
29
+ olderAdfSentence,
30
+ printable,
31
+ verifyHello,
32
+ verifyHelloWaiting
33
+ } from "./chunk-M2F7C2UJ.mjs";
34
+ import {
35
+ daemonPaths
36
+ } from "./chunk-PRPTKMQW.mjs";
37
+ import "./chunk-JVPZIMXW.mjs";
38
+ import {
39
+ DEFAULT_DAEMON_PORT,
16
40
  DEFAULT_DAEMON_URL,
41
+ KEYCHAIN_FILE_COPY,
42
+ RemoteMalformedError,
43
+ RemotesUnreadableError,
44
+ deleteRemoteToken,
45
+ forgetRemote,
17
46
  isLocalDaemonUrl,
47
+ localDaemonToken,
18
48
  localProofHeaders,
19
49
  loopbackPort,
50
+ malformedRemoteSentence,
51
+ pinnedRemoteRefusal,
52
+ readRemoteToken,
53
+ readRemotes,
54
+ remoteNameFor,
55
+ remotesFilePath,
20
56
  resolveDaemonToken,
21
57
  resolveDaemonUrl,
22
- tunnelTokenHint
23
- } from "./chunk-GKKCMXIZ.mjs";
24
- import "./chunk-UEZR2CNS.mjs";
25
- import "./chunk-IHDDCZBS.mjs";
58
+ storeRemoteToken,
59
+ tunnelTokenHint,
60
+ updateRemotes
61
+ } from "./chunk-V2T6NZX2.mjs";
62
+ import "./chunk-WVTPPOKK.mjs";
63
+ import "./chunk-WJGPNHGX.mjs";
64
+ import "./chunk-PAK5JCO2.mjs";
65
+ import "./chunk-ZPB3HYM7.mjs";
66
+ import "./chunk-LTX74NU6.mjs";
67
+ import {
68
+ stdinPrompt
69
+ } from "./chunk-PC63YFB7.mjs";
26
70
  import "./chunk-M2RX3S2T.mjs";
27
71
 
72
+ // src/main/cli/index.ts
73
+ import { createInterface } from "readline";
74
+ import { readFileSync as readFileSync2 } from "fs";
75
+ import { basename } from "path";
76
+
77
+ // src/main/cli/pair.ts
78
+ import { hostname as hostname2 } from "node:os";
79
+
80
+ // src/main/cli/daemon-discovery.ts
81
+ import { randomBytes } from "node:crypto";
82
+ function hostPart(address) {
83
+ return address.includes(":") ? `[${address}]` : address;
84
+ }
85
+ function daemonsOf(peers) {
86
+ return peers.flatMap((p) => p.remote ? [{ name: p.name, fingerprint: p.remote.fingerprint, url: p.remote.url, kind: p.remote.kind, pairingOpen: p.remote.pairingOpen, clash: p.remote.clash ?? null }] : []);
87
+ }
88
+ async function browseMdns(ms, createMdns = () => new MdnsService(), tailnet = async () => []) {
89
+ let service = createMdns();
90
+ try {
91
+ await service.start({ announce: !1, browse: !0, port: 0, runtimeId: `cli-browse-${randomBytes(6).toString("hex")}` });
92
+ let [found] = await Promise.all([tailnet().catch(() => []), new Promise((resolve) => setTimeout(resolve, ms))]);
93
+ for (let d of found)
94
+ try {
95
+ let url = new URL(d.url);
96
+ service.upsertTailnetDaemon({ fingerprint: d.fingerprint, name: d.name, address: url.hostname.replace(/^\[|\]$/g, ""), port: Number(url.port) || DEFAULT_DAEMON_PORT, pairingOpen: d.pairingOpen });
97
+ } catch {
98
+ }
99
+ return daemonsOf(service.peers());
100
+ } catch {
101
+ return [];
102
+ } finally {
103
+ await service.stop().catch(() => {
104
+ });
105
+ }
106
+ }
107
+ async function probeTailnet(opts = {}) {
108
+ let exec = opts.exec ?? defaultExec, client = new TailscaleClient(exec);
109
+ if ((await client.self()).state !== "running") return [];
110
+ let peers = [];
111
+ try {
112
+ let out = await exec(client.command ?? "tailscale", ["status", "--json"]);
113
+ peers = Object.values(JSON.parse(out).Peer ?? {});
114
+ } catch {
115
+ return [];
116
+ }
117
+ let port = opts.port ?? DEFAULT_DAEMON_PORT, probes = peers.filter((p) => p.Online === !0 && Array.isArray(p.TailscaleIPs) && typeof p.TailscaleIPs[0] == "string").map(async (p) => {
118
+ let ip = p.TailscaleIPs.find((a) => !a.includes(":")) ?? p.TailscaleIPs[0], url = `http://${hostPart(ip)}:${port}`, hello = await fetchHello(url, opts.fetch);
119
+ return hello ? { name: hello.name, fingerprint: hello.fingerprint, url, kind: "tailnet", pairingOpen: hello.pairingOpen } : null;
120
+ });
121
+ return (await Promise.all(probes)).filter((d) => d !== null);
122
+ }
123
+ function mergeDiscovered(lists) {
124
+ let byFp = /* @__PURE__ */ new Map();
125
+ for (let d of lists.flat()) {
126
+ let urls = byFp.get(d.fingerprint) ?? /* @__PURE__ */ new Map(), existing = urls.get(d.url);
127
+ urls.set(d.url, existing ? { ...existing, pairingOpen: existing.pairingOpen || d.pairingOpen, clash: existing.clash ?? d.clash ?? null } : { ...d }), byFp.set(d.fingerprint, urls);
128
+ }
129
+ let out = [];
130
+ for (let urls of byFp.values()) {
131
+ let all = [...urls.values()];
132
+ if (all.length > 1 || all.some((d) => (d.clash ?? 0) > 1)) {
133
+ for (let d of all) out.push({ ...d, clash: Math.max(all.length, d.clash ?? 0) });
134
+ continue;
135
+ }
136
+ out.push({ ...all[0], clash: null });
137
+ }
138
+ return out.sort((a, b) => Number(b.pairingOpen) - Number(a.pairingOpen) || a.name.localeCompare(b.name) || a.url.localeCompare(b.url));
139
+ }
140
+ async function peersFromDaemon(daemon, ms, fetchImpl = fetch, env = process.env) {
141
+ try {
142
+ let wait = Math.min(Math.max(ms, 0), 5e3), res = await fetchImpl(`${daemon.url.replace(/\/+$/, "")}/network/peers?wait=${wait}`, {
143
+ headers: { Authorization: `Bearer ${daemon.token}`, Accept: "application/json", ...localProofHeaders(daemon.url, "/network/peers", env) },
144
+ signal: AbortSignal.timeout(wait + 5e3)
145
+ });
146
+ if (!res.ok) return null;
147
+ let body = await res.json();
148
+ return Array.isArray(body.peers) ? body.peers : null;
149
+ } catch {
150
+ return null;
151
+ }
152
+ }
153
+ async function discoverDaemons(ms = 3e3, opts = {}) {
154
+ if (opts.localDaemon) {
155
+ let peers = await peersFromDaemon(opts.localDaemon, ms, opts.fetch, opts.env);
156
+ if (peers) return mergeDiscovered([daemonsOf(peers)]);
157
+ }
158
+ return mergeDiscovered([await browseMdns(ms, void 0, () => probeTailnet({ ...opts.fetch ? { fetch: opts.fetch } : {} }))]);
159
+ }
160
+
161
+ // src/main/backend/local-runtime-name.ts
162
+ import { readFileSync, watch } from "node:fs";
163
+ import { hostname } from "node:os";
164
+
165
+ // src/shared/runtime-name.ts
166
+ var LOCAL_LABEL = "Local";
167
+ function localRuntimeName(name) {
168
+ let trimmed = typeof name == "string" ? name.trim() : "";
169
+ return trimmed ? `${trimmed} (${LOCAL_LABEL})` : LOCAL_LABEL;
170
+ }
171
+
172
+ // src/main/backend/local-runtime-name.ts
173
+ function aliasOf(text) {
174
+ let alias = JSON.parse(text)?.runtimeAlias;
175
+ return cleanDisplayName(alias, 64);
176
+ }
177
+ function machineNameFromFile(settingsFile, host = hostname) {
178
+ if (settingsFile)
179
+ try {
180
+ let name = aliasOf(readFileSync(settingsFile, "utf-8"));
181
+ if (name) return name;
182
+ } catch {
183
+ }
184
+ return host().trim() || null;
185
+ }
186
+
187
+ // src/main/cli/pair.ts
188
+ var PAIR_USAGE = `Usage: adf pair [<host>[:<port>] | <url>] [--name <this computer's name>] [--as <remote name>] [--yes] [--default | --no-default]
189
+
190
+ Pairs this computer with another computer's ADF daemon. On that computer,
191
+ open a pairing window first: ADF Studio \u2192 Settings \u2192 Connections \u2192 Add \u2192
192
+ Share, or adf daemon pair. Type the 6-digit code it shows; it
193
+ then asks whether to allow this computer.
194
+
195
+ Without a host, adf lists the ADF computers it finds on this network and on
196
+ your tailnet. The token adf receives is as powerful as that computer's owner
197
+ seed phrase; it is saved in the OS keychain (or a file only you can read).`, REMOTES_USAGE = `Usage: adf remotes List saved remote daemons (* = default)
198
+ adf remotes use <name|local> Talk to that daemon by default
199
+ adf remotes forget <name> Delete its saved address and token`;
200
+ function parsePairArgs(args) {
201
+ let out = { yes: !1, makeDefault: null };
202
+ for (let i = 0; i < args.length; i++) {
203
+ let arg = args[i], value = () => {
204
+ let v = args[++i];
205
+ if (v === void 0) throw new Error(`${arg} requires a value`);
206
+ return v;
207
+ };
208
+ if (arg === "--name") out.name = value();
209
+ else if (arg === "--as") out.as = value();
210
+ else if (arg === "--yes" || arg === "-y") out.yes = !0;
211
+ else if (arg === "--default") out.makeDefault = !0;
212
+ else if (arg === "--no-default") out.makeDefault = !1;
213
+ else {
214
+ if (arg === "--help" || arg === "-h") return PAIR_USAGE;
215
+ if (arg.startsWith("-")) return `Unknown option: ${arg}
216
+
217
+ ${PAIR_USAGE}`;
218
+ if (!out.host) out.host = arg;
219
+ else return `Too many arguments
220
+
221
+ ${PAIR_USAGE}`;
222
+ }
223
+ }
224
+ return out;
225
+ }
226
+ function hostToUrl(host) {
227
+ return /^https?:\/\//i.test(host) ? host.replace(/\/+$/, "") : /^\[.*\](:\d+)?$/.test(host) ? `http://${host.includes("]:") ? host : `${host}:${DEFAULT_DAEMON_PORT}`}` : host.includes(":") && host.split(":").length > 2 ? `http://[${host}]:${DEFAULT_DAEMON_PORT}` : `http://${/:\d+$/.test(host) ? host : `${host}:${DEFAULT_DAEMON_PORT}`}`;
228
+ }
229
+ function transportOf(url) {
230
+ let parsed;
231
+ try {
232
+ parsed = new URL(url);
233
+ } catch {
234
+ return "plain";
235
+ }
236
+ if (parsed.protocol === "https:") return "https";
237
+ let host = parsed.hostname.replace(/^\[|\]$/g, "").toLowerCase();
238
+ return host === "localhost" || host === "::1" || /^127\./.test(host) ? "loopback" : /^100\.(6[4-9]|[7-9]\d|1[01]\d|12[0-7])\./.test(host) || /^fd7a:115c:a1e0:/.test(host) ? "tailnet" : "plain";
239
+ }
240
+ async function postJson(io, url, body) {
241
+ let res = await io.fetch(url, { method: "POST", headers: { "Content-Type": "application/json", Accept: "application/json" }, body: JSON.stringify(body), redirect: "manual" }), text = await res.text(), parsed = text ? JSON.parse(text) : {}, retry = Number(res.headers.get("retry-after"));
242
+ return { status: res.status, body: parsed, retryAfter: Number.isFinite(retry) && retry > 0 ? retry : null };
243
+ }
244
+ async function getJson(io, url, headers = {}) {
245
+ let res = await io.fetch(url, { headers: { Accept: "application/json", ...headers }, redirect: "manual" }), text = await res.text();
246
+ return { status: res.status, body: text ? JSON.parse(text) : {} };
247
+ }
248
+ function localDaemonOf(env) {
249
+ let url = `http://127.0.0.1:${Number(env.ADF_DAEMON_PORT) || DEFAULT_DAEMON_PORT}`, token = localDaemonToken(url, env);
250
+ return token ? { url, token } : null;
251
+ }
252
+ async function chooseDaemon(io) {
253
+ io.stderr(`Looking for ADF computers on this network and your tailnet\u2026
254
+ `);
255
+ let found = await (io.discover ?? ((ms) => discoverDaemons(ms, { localDaemon: localDaemonOf(io.env ?? process.env), fetch: io.fetch, env: io.env ?? process.env })))(3e3);
256
+ if (found.length === 0)
257
+ return io.stderr(`No ADF computer found. On the other computer turn on remote access (Settings \u2192 Connections \u2192 Add \u2192 Share, or adf daemon remote on), then run adf pair <its address>.
258
+ `), null;
259
+ found.forEach((d, i) => io.stderr(` ${i + 1}. ${d.name} ${d.fingerprint} ${d.url} (${d.kind === "tailnet" ? "tailnet" : "this network"}${d.pairingOpen ? ", ready to pair" : ""})
260
+ `));
261
+ let answer = await io.prompt(found.length === 1 ? "Pair with 1? [Y/n] " : `Which one (1-${found.length})? `);
262
+ if (answer === null) return null;
263
+ let index = found.length === 1 && /^\s*(y|yes)?\s*$/i.test(answer) ? 0 : Number(answer.trim()) - 1, chosen = found[index];
264
+ if (!chosen)
265
+ return io.stderr(`No such entry.
266
+ `), null;
267
+ let hello = await fetchHello(chosen.url, io.fetch);
268
+ return hello ? { url: chosen.url, hello } : (io.stderr(`${chosen.url} does not answer as an ADF daemon with remote access on.
269
+ `), null);
270
+ }
271
+ async function bestUrl(io, used, urls, fingerprint, token, yes) {
272
+ if (transportOf(used) === "https") return used;
273
+ let view = null, { NO_TAILNET, TailscaleClient: TailscaleClient2, tailnetUrlAllowed } = await import("./tailscale-WLJATFMB.mjs");
274
+ for (let candidate of urls) {
275
+ if (candidate.url === used || candidate.kind !== "tailscale-https" && candidate.kind !== "tailnet" || (view ??= await (io.tailnetView ?? (() => new TailscaleClient2().tailnetView()))().catch(() => NO_TAILNET), !tailnetUrlAllowed(candidate.url, view))) continue;
276
+ let there = await verifyHello(candidate.url, token, { fetch: io.fetch });
277
+ if (there.kind !== "verified" || there.hello.fingerprint !== fingerprint) continue;
278
+ let hello = there.hello, answer = yes ? "y" : await io.prompt(`"${hello.name}" is also reachable at ${candidate.url}, encrypted by Tailscale. Save that address instead of ${used}? [Y/n] `);
279
+ return answer !== null && /^\s*(y|yes)?\s*$/i.test(answer) ? candidate.url : used;
280
+ }
281
+ return used;
282
+ }
283
+ function hostDeviceName(raw) {
284
+ let unsafe = new RegExp(UNSAFE_DISPLAY_CHARS.source, "gu"), name = [...raw.replace(unsafe, "").replace(/\s+/g, " ").trim()].slice(0, 64).join("").trim();
285
+ return cleanDisplayName(name, 64) ?? "ADF computer";
286
+ }
287
+ function terminalSafe(io) {
288
+ let prompt = io.prompt;
289
+ return {
290
+ ...io,
291
+ stdout: (text) => io.stdout(printable(text)),
292
+ stderr: (text) => io.stderr(printable(text)),
293
+ ...prompt ? { prompt: (question) => prompt(printable(question)) } : {}
294
+ };
295
+ }
296
+ async function runPair(args, rawIo) {
297
+ let io = terminalSafe(rawIo), env = io.env ?? process.env, sleep = io.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms))), parsed;
298
+ try {
299
+ parsed = parsePairArgs(args);
300
+ } catch (err) {
301
+ return io.stderr(`${err instanceof Error ? err.message : String(err)}
302
+ `), 2;
303
+ }
304
+ if (typeof parsed == "string")
305
+ return io.stdout(`${parsed}
306
+ `), parsed === PAIR_USAGE ? 0 : 2;
307
+ let deviceName = parsed.name !== void 0 ? cleanDisplayName(parsed.name, 64) : hostDeviceName(hostname2());
308
+ if (!deviceName)
309
+ return io.stderr(`--name must be 1-64 characters without control or bidi characters.
310
+ `), 2;
311
+ let remotes = readRemotes(env);
312
+ if (remotes.unreadable)
313
+ return io.stderr(`${new RemotesUnreadableError(remotesFilePath(env), remotes.unreadable).message}
314
+ `), 1;
315
+ if (parsed.as !== void 0 && remotes.malformed.includes(parsed.as))
316
+ return io.stderr(`${malformedRemoteSentence(remotesFilePath(env), parsed.as)}; or pair under another name (--as <name>).
317
+ `), 1;
318
+ let url, hello;
319
+ if (parsed.host) {
320
+ url = hostToUrl(parsed.host);
321
+ let h = await fetchHello(url, io.fetch);
322
+ if (!h)
323
+ return io.stderr(`${url} does not answer as an ADF daemon with remote access on. On that computer turn on remote access (Settings \u2192 Connections \u2192 Add \u2192 Share, or adf daemon remote on); an older ADF version cannot pair.
324
+ `), 1;
325
+ hello = h;
326
+ } else {
327
+ let chosen = await chooseDaemon(io);
328
+ if (!chosen) return 1;
329
+ url = chosen.url, hello = chosen.hello;
330
+ }
331
+ if (hello.helloProof !== 1)
332
+ return io.stderr(`${olderAdfSentence(`"${hello.name}"`)}
333
+ `), 1;
334
+ if (io.stderr(`Pairing with "${hello.name}" (fingerprint ${hello.fingerprint}) at ${url}. Check that it shows the same fingerprint.
335
+ `), transportOf(url) === "plain" && (io.stderr(`This address uses plain http on this network: the token is sent unencrypted, and anyone who can see the traffic can take it. On a tailnet, pair with the tailnet address instead.
336
+ `), !parsed.yes)) {
337
+ let ok = await io.prompt("I trust this network. Continue? [y/N] ");
338
+ if (!ok || !/^\s*y(es)?\s*$/i.test(ok))
339
+ return io.stderr(`Not paired.
340
+ `), 1;
341
+ }
342
+ hello.pairingOpen || io.stderr(`No pairing window is open on "${hello.name}" yet. Open one there (Pair a computer, or adf daemon pair).
343
+ `);
344
+ let accepted = null;
345
+ for (; !accepted; ) {
346
+ let answer = await io.prompt(`Code shown on "${hello.name}": `);
347
+ if (answer === null) return 1;
348
+ let code = answer.replace(/\s+/g, "");
349
+ if (!/^\d{6}$/.test(code)) {
350
+ io.stderr(`The code is 6 digits.
351
+ `);
352
+ continue;
353
+ }
354
+ let res = await postJson(io, `${url}/pair/claim`, { code, deviceName, clientKind: "cli" });
355
+ if (res.status === 202) {
356
+ accepted = res.body;
357
+ break;
358
+ }
359
+ let message = res.body.error ?? `HTTP ${res.status}`;
360
+ if (res.body.code === "wrong_code" && (res.body.attemptsLeft ?? 0) > 0) {
361
+ io.stderr(`${message} ${res.body.attemptsLeft} attempt(s) left.
362
+ `), await sleep(2100);
363
+ continue;
364
+ }
365
+ if (res.body.code === "rate_limited" && res.retryAfter) {
366
+ io.stderr(`${message} Waiting ${res.retryAfter} s.
367
+ `), await sleep(res.retryAfter * 1e3);
368
+ continue;
369
+ }
370
+ return io.stderr(`${message}
371
+ `), 1;
372
+ }
373
+ io.stderr(`Waiting for "${hello.name}" to allow this computer\u2026
374
+ `);
375
+ let result = null;
376
+ for (; !result || result.state === "pending"; ) {
377
+ let res = await getJson(io, `${url}/pair/claims/${encodeURIComponent(accepted.claimId)}?wait=25`, { "X-ADF-Pair-Secret": accepted.collectSecret });
378
+ if (res.status !== 200)
379
+ return io.stderr(`${res.body.error ?? `HTTP ${res.status}`}
380
+ `), 1;
381
+ result = res.body;
382
+ }
383
+ if (result.state === "denied")
384
+ return io.stderr(`"${hello.name}" did not allow this computer.
385
+ `), 1;
386
+ let revokeHint = `"${result.daemon.name}" lists this computer as "${result.device.label}": remove it there (adf daemon devices revoke ${result.device.id}).`;
387
+ if (result.daemon.fingerprint !== hello.fingerprint)
388
+ return io.stderr(`Refused: the approval came from a computer with fingerprint ${result.daemon.fingerprint}, not ${hello.fingerprint} as "${hello.name}" showed. Nothing was saved. ${revokeHint}
389
+ `), 1;
390
+ let read = readRemotes(env), file = read.file, sameHost = Object.entries(file.remotes).find(([, r]) => r.fingerprint === result.daemon.fingerprint), name = parsed.as ?? sameHost?.[0] ?? remoteNameFor(result.daemon.name, { ...Object.fromEntries(read.malformed.map((n) => [n, !0])), ...file.remotes }), previous = file.remotes[name];
391
+ if (previous && previous.fingerprint !== result.daemon.fingerprint)
392
+ return io.stderr(`Refused: the saved remote "${name}" is ${previous.hostName ?? previous.url} with fingerprint ${previous.fingerprint}, not this computer (${result.daemon.fingerprint}). Nothing was saved. Pair under another name (--as <name>), or forget it first (adf remotes forget ${name}). ${revokeHint}
393
+ `), 1;
394
+ let saveUrl = await bestUrl(io, url, result.daemon.urls, result.daemon.fingerprint, result.token, parsed.yes), proved = await verifyHelloWaiting(saveUrl, result.token, { fetch: io.fetch, sleep, onWait: (ms) => io.stderr(`"${result.daemon.name}" is rate-limiting connections; retrying in ${Math.ceil(ms / 1e3)} s.
395
+ `) });
396
+ if (proved.kind !== "verified" || proved.hello.fingerprint !== result.daemon.fingerprint) {
397
+ let why = proved.kind === "verified" ? `${saveUrl} answers with fingerprint ${proved.hello.fingerprint}, not ${result.daemon.fingerprint}` : proved.kind === "none" ? `${saveUrl} did not answer after "${result.daemon.name}" allowed this computer` : proved.kind === "rate_limited" ? `"${result.daemon.name}" kept rate-limiting connections` : proved.kind === "unsigned" ? `"${result.daemon.name}" runs an older ADF` : `${saveUrl} could not prove it is "${result.daemon.name}"`;
398
+ return io.stderr(`Refused: ${why}. Nothing was saved. ${revokeHint}
399
+ `), 1;
400
+ }
401
+ if (previous) {
402
+ let changes = [previous.url !== saveUrl ? `its address (${previous.url} \u2192 ${saveUrl})` : null, "its token"].filter(Boolean).join(" and "), answer = parsed.yes ? "y" : await io.prompt(`"${name}" (${previous.hostName ?? previous.url}) is already saved. Replace ${changes}? [y/N] `);
403
+ if (!answer || !/^\s*y(es)?\s*$/i.test(answer))
404
+ return io.stderr(`Kept the saved "${name}" as it was. ${revokeHint}
405
+ `), 1;
406
+ }
407
+ let previousToken = previous ? readRemoteToken(previous.fingerprint, env) : null, stored;
408
+ try {
409
+ stored = await storeRemoteToken(result.daemon.fingerprint, result.token, env, async (why) => {
410
+ if (parsed.yes) return !0;
411
+ io.stderr(`${KEYCHAIN_FILE_COPY.title}: ${KEYCHAIN_FILE_COPY.message} (${why})
412
+ `);
413
+ let answer = await io.prompt(KEYCHAIN_FILE_COPY.prompt);
414
+ return !!answer && /^\s*y(es)?\s*$/i.test(answer);
415
+ });
416
+ } catch (err) {
417
+ return io.stderr(`The token was not saved: ${err instanceof Error ? err.message : String(err)}
418
+ `), io.stderr(`"${result.daemon.name}" lists this computer as "${result.device.label}": remove it there (adf daemon devices revoke ${result.device.id}) and pair again once this is fixed.
419
+ `), 1;
420
+ }
421
+ let entry = {
422
+ url: saveUrl,
423
+ fingerprint: result.daemon.fingerprint,
424
+ deviceId: result.device.id,
425
+ label: result.device.label,
426
+ pairedAt: (/* @__PURE__ */ new Date()).toISOString(),
427
+ hostName: result.daemon.name
428
+ }, makeDefault = parsed.makeDefault;
429
+ if (makeDefault === null) {
430
+ let answer = await io.prompt("Use it by default for adf commands and the terminal app? [Y/n] ");
431
+ makeDefault = answer !== null && /^\s*(y|yes)?\s*$/i.test(answer);
432
+ }
433
+ try {
434
+ await updateRemotes(env, (latest, malformed) => {
435
+ let now = latest.remotes[name];
436
+ if (now && now.fingerprint !== entry.fingerprint) throw new Error(`the saved remote "${name}" changed to another computer (${now.fingerprint}) meanwhile`);
437
+ if (!now && name in malformed) throw new Error(`"${name}" became a malformed entry meanwhile; fix or forget it first`);
438
+ latest.remotes[name] = entry, makeDefault && (latest.default = name);
439
+ });
440
+ } catch (err) {
441
+ try {
442
+ previousToken ? await storeRemoteToken(result.daemon.fingerprint, previousToken, env, async () => !0) : deleteRemoteToken(result.daemon.fingerprint, env);
443
+ } catch {
444
+ }
445
+ return io.stderr(`The remote was not saved: ${err instanceof Error ? err.message : String(err)}
446
+ ${revokeHint}
447
+ `), 1;
448
+ }
449
+ return io.stdout(`Paired with "${result.daemon.name}" as "${result.device.label}". Saved as remote "${name}" (${saveUrl})${makeDefault ? ", the default" : `; use it with --remote ${name}`}.
450
+ `), io.stderr(stored.where === "keychain" ? `The token is in the OS keychain.
451
+ ` : `The token is in ${stored.path}, readable by you only (${stored.why}).
452
+ `), transportOf(saveUrl) === "plain" && io.stderr(`This address is plain http: the token travels unencrypted with every request. Prefer https through tailscale serve or the tailnet address.
453
+ `), 0;
454
+ }
455
+ function localRemoteName(env) {
456
+ return localRuntimeName(machineNameFromFile(daemonPaths(DEFAULT_DAEMON_PORT, env).settingsFile));
457
+ }
458
+ async function runRemotes(args, rawIo, json = !1) {
459
+ let io = terminalSafe(rawIo), env = io.env ?? process.env, [action, name] = args, read = readRemotes(env);
460
+ if (read.unreadable)
461
+ return io.stderr(`${new RemotesUnreadableError(remotesFilePath(env), read.unreadable).message}
462
+ `), 1;
463
+ let file = read.file;
464
+ if (!action || action === "list") {
465
+ if (json)
466
+ return io.stdout(`${JSON.stringify(file, null, 2)}
467
+ `), 0;
468
+ let rows = Object.entries(file.remotes);
469
+ io.stdout(`${file.default === null && !read.malformedDefault ? "*" : " "} local ${printable(localRemoteName(env))}
470
+ `);
471
+ for (let [n, r] of rows) io.stdout(`${file.default === n ? "*" : " "} ${n} ${r.url} ${r.hostName ?? ""} ${r.fingerprint} as "${r.label}"
472
+ `);
473
+ for (let n of read.malformed) io.stderr(`${n === read.malformedDefault ? "* " : ""}${malformedRemoteSentence(remotesFilePath(env), n)}
474
+ `);
475
+ return rows.length === 0 && read.malformed.length === 0 && io.stderr(`No saved remotes. Pair one with: adf pair
476
+ `), 0;
477
+ }
478
+ if (action === "use")
479
+ return name ? name !== "local" && !file.remotes[name] ? (io.stderr(read.malformed.includes(name) ? `${malformedRemoteSentence(remotesFilePath(env), name)}
480
+ ` : `No saved remote named "${name}". List them with: adf remotes
481
+ `), 1) : (await updateRemotes(env, (latest) => {
482
+ latest.default = name === "local" ? null : name;
483
+ }), io.stdout(name === "local" ? `adf now talks to this computer's daemon by default.
484
+ ` : `adf now talks to "${name}" (${file.remotes[name].url}) by default.
485
+ `), 0) : (io.stderr(`${REMOTES_USAGE}
486
+ `), 2);
487
+ if (action === "forget") {
488
+ if (!name || !file.remotes[name] && !read.malformed.includes(name))
489
+ return io.stderr(name ? `No saved remote named "${name}".
490
+ ` : `${REMOTES_USAGE}
491
+ `), name ? 1 : 2;
492
+ let forgot;
493
+ try {
494
+ forgot = await forgetRemote(env, name);
495
+ } catch (err) {
496
+ return io.stderr(`"${name}" was not forgotten: ${err instanceof Error ? err.message : String(err)}
497
+ `), 1;
498
+ }
499
+ if (!forgot)
500
+ return io.stderr(`No saved remote named "${name}".
501
+ `), 1;
502
+ let remote = forgot.remote;
503
+ if (!remote)
504
+ return io.stdout(`Forgot "${name}".
505
+ `), 0;
506
+ let what = forgot.sharedWith ? `"${name}"; its token stays for "${forgot.sharedWith}", the same computer` : `"${name}" and its token`;
507
+ return io.stdout(`Forgot ${what}. "${remote.hostName ?? remote.url}" still lists this computer: remove it there (adf daemon devices revoke ${remote.deviceId}).
508
+ `), forgot.tokenError ? (io.stderr(`The token could not be removed: ${forgot.tokenError}
509
+ `), 1) : 0;
510
+ }
511
+ return io.stderr(`${REMOTES_USAGE}
512
+ `), 2;
513
+ }
514
+
28
515
  // src/main/cli/index.ts
29
516
  var defaultIo = {
30
517
  fetch: globalThis.fetch.bind(globalThis),
@@ -44,15 +531,15 @@ function tuiInvocation(argv) {
44
531
  return argv.filter((a) => a !== "--json");
45
532
  }
46
533
  async function defaultLaunchTui(argv) {
47
- let { runTui } = await import("./tui-UQJLK7XI.mjs");
534
+ let { runTui } = await import("./tui-SPSPDADM.mjs");
48
535
  return await runTui(argv);
49
536
  }
50
- function urlFromArgv(argv) {
537
+ function urlFromArgv(argv, env) {
51
538
  for (let i = 0; i < argv.length; i++) {
52
- if (argv[i] === "--url" || argv[i] === "-u") return resolveDaemonUrl(argv[i + 1]);
53
- if (argv[i].startsWith("--url=")) return resolveDaemonUrl(argv[i].slice(6));
539
+ if (argv[i] === "--url" || argv[i] === "-u") return resolveDaemonUrl(argv[i + 1], env);
540
+ if (argv[i].startsWith("--url=")) return resolveDaemonUrl(argv[i].slice(6), env);
54
541
  }
55
- return resolveDaemonUrl(void 0);
542
+ return resolveDaemonUrl(void 0, env);
56
543
  }
57
544
  async function daemonReady(io, url) {
58
545
  if (!io.ensureDaemon) return { ok: !0, started: null };
@@ -65,10 +552,26 @@ ${advice ? `${advice}
65
552
  ` : ""}`), { ok: !1, started: null };
66
553
  }
67
554
  }
555
+ async function pinnedRemoteOk(io, url, explicitToken) {
556
+ let env = io.env ?? process.env;
557
+ if (explicitToken || env.ADF_DAEMON_TOKEN) return !0;
558
+ let refusal = await pinnedRemoteRefusal(url, env, io.fetch, { retries: 2, onWait: (sentence) => io.stderr(`${sentence}
559
+ `), ...io.sleep ? { sleep: io.sleep } : {} });
560
+ return refusal && io.stderr(`${refusal}
561
+ `), refusal === null;
562
+ }
68
563
  async function runCli(argv = process.argv.slice(2), io = defaultIo) {
69
564
  let tuiArgv = tuiInvocation(argv);
70
565
  if (tuiArgv) {
71
- let ready = tuiArgv.includes("--help") || tuiArgv.includes("-h") ? { ok: !0, started: null } : await daemonReady(io, urlFromArgv(tuiArgv));
566
+ let wantsHelp = tuiArgv.includes("--help") || tuiArgv.includes("-h"), tuiUrl = "";
567
+ try {
568
+ wantsHelp || (tuiUrl = urlFromArgv(tuiArgv, io.env ?? process.env));
569
+ } catch (err) {
570
+ return io.stderr(`${err instanceof Error ? err.message : String(err)}
571
+ `), 1;
572
+ }
573
+ if (!wantsHelp && !await pinnedRemoteOk(io, tuiUrl, tuiArgv.some((a) => a === "--token" || a.startsWith("--token=")))) return 1;
574
+ let ready = wantsHelp ? { ok: !0, started: null } : await daemonReady(io, tuiUrl);
72
575
  if (!ready.ok) return 1;
73
576
  try {
74
577
  let code = await (io.launchTui ?? defaultLaunchTui)(tuiArgv);
@@ -88,8 +591,18 @@ async function runCli(argv = process.argv.slice(2), io = defaultIo) {
88
591
  ${usage()}
89
592
  `), 2;
90
593
  }
91
- let { command, args, options } = parsed;
92
- if (!["help", "--help", "-h"].includes(command) && !(await daemonReady(io, options.daemonUrl)).ok) return 1;
594
+ let { command, args } = parsed, talksToDaemon = !["help", "--help", "-h", "pair", "remotes"].includes(command), options;
595
+ try {
596
+ let env = io.env ?? process.env;
597
+ options = { ...parsed.options, env, daemonUrl: talksToDaemon ? resolveDaemonUrl(parsed.explicitUrl ?? void 0, env) : "" };
598
+ } catch (err) {
599
+ return err instanceof RemotesUnreadableError || err instanceof RemoteMalformedError ? (io.stderr(`${err.message}
600
+ `), 1) : (io.stderr(`${err instanceof Error ? err.message : String(err)}
601
+
602
+ ${usage()}
603
+ `), 2);
604
+ }
605
+ if (talksToDaemon && !await pinnedRemoteOk(io, options.daemonUrl, !!options.token) || talksToDaemon && !(await daemonReady(io, options.daemonUrl)).ok) return 1;
93
606
  try {
94
607
  switch (command) {
95
608
  case "help":
@@ -170,6 +683,16 @@ ${usage()}
170
683
  return await printGet(io, options, "/templates", formatTemplates);
171
684
  case "new":
172
685
  return await newAgent(io, options, args);
686
+ case "pair": {
687
+ let prompt = io.prompt ? null : stdinPrompt();
688
+ try {
689
+ return await runPair(args, { fetch: io.fetch, stdout: io.stdout, stderr: io.stderr, prompt: io.prompt ? ((q) => io.prompt(q)) : ((q) => prompt.ask(q)), ...io.discover ? { discover: io.discover } : {}, ...io.sleep ? { sleep: io.sleep } : {}, ...io.env ? { env: io.env } : {} });
690
+ } finally {
691
+ prompt?.close();
692
+ }
693
+ }
694
+ case "remotes":
695
+ return await runRemotes(args, { stdout: io.stdout, stderr: io.stderr, ...io.env ? { env: io.env } : {} }, options.json);
173
696
  default:
174
697
  return io.stderr(`Unknown command: ${command}
175
698
 
@@ -182,7 +705,7 @@ ${usage()}
182
705
  }
183
706
  }
184
707
  function parseArgs(argv) {
185
- let args = [...argv], daemonUrl = process.env.ADF_DAEMON_URL ?? DEFAULT_DAEMON_URL, json = !1, token, positional = [];
708
+ let args = [...argv], daemonUrl = null, json = !1, token, positional = [];
186
709
  for (let i = 0; i < args.length; i++) {
187
710
  let arg = args[i];
188
711
  if (arg === "--json")
@@ -203,10 +726,10 @@ function parseArgs(argv) {
203
726
  command: positional[0] ?? "help",
204
727
  args: positional.slice(1),
205
728
  options: {
206
- daemonUrl: daemonUrl.replace(/\/+$/, ""),
207
729
  json,
208
730
  ...token ? { token } : {}
209
- }
731
+ },
732
+ explicitUrl: daemonUrl
210
733
  };
211
734
  }
212
735
  async function printGet(io, options, path, formatter) {
@@ -215,7 +738,7 @@ async function printGet(io, options, path, formatter) {
215
738
  ` : formatter(data)), 0;
216
739
  }
217
740
  function authHeader(options) {
218
- let token = resolveDaemonToken(options.token, process.env, options.daemonUrl);
741
+ let token = resolveDaemonToken(options.token, options.env, options.daemonUrl);
219
742
  return token ? { Authorization: `Bearer ${token}` } : {};
220
743
  }
221
744
  async function requestJson(io, options, path, init) {
@@ -227,12 +750,12 @@ async function requestJson(io, options, path, init) {
227
750
  ...init?.headers ?? {},
228
751
  Accept: "application/json",
229
752
  ...authHeader(options),
230
- ...localProofHeaders(options.daemonUrl, path)
753
+ ...localProofHeaders(options.daemonUrl, path, options.env)
231
754
  }
232
755
  });
233
756
  } catch (err) {
234
757
  if (err instanceof TypeError) {
235
- let forwarded = loopbackPort(options.daemonUrl) !== null && !isLocalDaemonUrl(options.daemonUrl);
758
+ let forwarded = loopbackPort(options.daemonUrl) !== null && !isLocalDaemonUrl(options.daemonUrl, options.env);
236
759
  throw new Error(`Cannot reach the ADF daemon at ${options.daemonUrl} (${err.message}). ${forwarded ? "That is not this machine's daemon port, so adf did not start one there: check the SSH tunnel, or start a daemon on it with: adf daemon start --port <n>" : "Start it with: adf daemon start"}`);
237
760
  }
238
761
  throw err;
@@ -245,12 +768,22 @@ async function requestJson(io, options, path, init) {
245
768
  body = text;
246
769
  }
247
770
  if (!response.ok) {
248
- let message = isRecord(body) && typeof body.error == "string" ? body.error : `HTTP ${response.status} ${response.statusText}`, hint = response.status === 401 ? tunnelTokenHint(options.daemonUrl, options.token) : null;
249
- throw new Error(hint ? `${message}
250
- ${hint}` : message);
771
+ let message = isRecord(body) && typeof body.error == "string" ? body.error : `HTTP ${response.status} ${response.statusText}`, hint = response.status === 401 ? tunnelTokenHint(options.daemonUrl, options.token, options.env) : null;
772
+ throw new DaemonRequestError(hint ? `${message}
773
+ ${hint}` : message, response.status, isRecord(body) && typeof body.code == "string" ? body.code : null);
251
774
  }
252
775
  return body;
253
776
  }
777
+ var DaemonRequestError = class extends Error {
778
+ constructor(message, status, apiCode) {
779
+ super(message);
780
+ this.status = status;
781
+ this.apiCode = apiCode;
782
+ this.name = "DaemonRequestError";
783
+ }
784
+ status;
785
+ apiCode;
786
+ };
254
787
  function takeLoop(args) {
255
788
  let rest = [], loop;
256
789
  for (let i = 0; i < args.length; i++) {
@@ -261,13 +794,43 @@ function takeLoop(args) {
261
794
  }
262
795
  return { loop: loop && loop !== "main" ? loop : void 0, rest };
263
796
  }
797
+ function takeAttachments(args) {
798
+ let rest = [], files = [];
799
+ for (let i = 0; i < args.length; i++) {
800
+ let arg = args[i];
801
+ if (arg === "--attach") {
802
+ let file = args[++i];
803
+ if (!file) throw new Error("--attach requires a file path");
804
+ files.push(file);
805
+ } else arg.startsWith("--attach=") ? files.push(arg.slice(9)) : rest.push(arg);
806
+ }
807
+ return { files, rest };
808
+ }
809
+ function uploadName(file) {
810
+ return `loop-upload/${basename(file).replace(/[^A-Za-z0-9._-]+/g, "-").replace(/^[-.]+/, "") || "attachment"}`;
811
+ }
264
812
  async function sendChat(io, options, args) {
265
- let { loop, rest } = takeLoop(args), agent = rest[0], text = rest.slice(1).join(" ");
266
- if (!agent || !text) throw new Error("Usage: adf chat <agent> [--loop <name>] <message>");
267
- let data = await requestJson(io, options, `/agents/${enc(agent)}/chat`, {
813
+ let attached = takeAttachments(args), { loop, rest } = takeLoop(attached.rest), agent = rest[0], text = rest.slice(1).join(" ");
814
+ if (!agent || !text && attached.files.length === 0) throw new Error("Usage: adf chat <agent> [--loop <name>] [--attach <file>]... <message>");
815
+ let refs = [];
816
+ for (let file of attached.files) {
817
+ let bytes;
818
+ try {
819
+ bytes = readFileSync2(file);
820
+ } catch (err) {
821
+ throw new Error(`Cannot read ${file}: ${err instanceof Error ? err.message : String(err)}`);
822
+ }
823
+ let saved = await requestJson(io, options, `/agents/${enc(agent)}/files/raw?path=${enc(uploadName(file))}&unique=true`, {
824
+ method: "PUT",
825
+ headers: { "Content-Type": "application/octet-stream" },
826
+ body: new Uint8Array(bytes)
827
+ }), path = isRecord(saved) && typeof saved.path == "string" ? saved.path : uploadName(file);
828
+ refs.push({ type: "file_ref", file_ref: { path, mime_type: "", size: bytes.length } });
829
+ }
830
+ let body = refs.length > 0 ? { content: [...text ? [{ type: "text", text }] : [], ...refs] } : { text }, data = await requestJson(io, options, `/agents/${enc(agent)}/chat`, {
268
831
  method: "POST",
269
832
  headers: { "Content-Type": "application/json" },
270
- body: JSON.stringify({ text, ...loop ? { loop } : {} })
833
+ body: JSON.stringify({ ...body, ...loop ? { loop } : {} })
271
834
  });
272
835
  return io.stdout(options.json ? `${JSON.stringify(data, null, 2)}
273
836
  ` : formatChatAck(data)), 0;
@@ -337,21 +900,23 @@ function authDeps(io, options) {
337
900
  }
338
901
  function requireAuthProvider(value) {
339
902
  let provider = normalizeAuthProvider(value);
340
- if (!provider) throw new Error("Specify a provider: chatgpt or grok");
903
+ if (!provider) throw new Error("Specify a provider: chatgpt, grok or claude");
341
904
  return provider;
342
905
  }
343
906
  async function authCommand(io, options, args) {
344
907
  let flags = new Set(args.filter((arg) => arg.startsWith("--"))), positional = args.filter((arg) => !arg.startsWith("--")), action = positional[0];
345
908
  if (action === "status") return await printGet(io, options, "/runtime/auth", formatAuth);
346
- if (action === "login")
347
- return requireAuthProvider(positional[1]) === "chatgpt" ? await loginChatGptCli(io, options, flags) : await loginGrokCli(io, options);
909
+ if (action === "login") {
910
+ let provider = requireAuthProvider(positional[1]);
911
+ return provider === "claude" ? await loginClaudeCli(io, options) : provider === "chatgpt" ? await loginChatGptCli(io, options, flags) : await loginGrokCli(io, options);
912
+ }
348
913
  if (action === "logout") {
349
914
  let provider = requireAuthProvider(positional[1]), data = await requestJson(io, options, `/auth/${provider}/logout`, { method: "POST" });
350
915
  return io.stdout(options.json ? `${JSON.stringify(data, null, 2)}
351
916
  ` : `Signed out of ${AUTH_PROVIDER_LABELS[provider]}.
352
917
  `), 0;
353
918
  }
354
- throw new Error("Usage: adf auth [status | login <chatgpt|grok> | logout <chatgpt|grok>]");
919
+ throw new Error("Usage: adf auth [status | login <chatgpt|grok|claude> | logout <chatgpt|grok|claude>]");
355
920
  }
356
921
  function reportAuth(io, provider, outcome) {
357
922
  return outcome.ok ? (io.stdout(`Signed in to ${AUTH_PROVIDER_LABELS[provider]}${outcome.email ? ` as ${outcome.email}` : ""}.
@@ -405,6 +970,63 @@ Waiting for approval...
405
970
  });
406
971
  return options.json ? 0 : reportAuth(io, "grok", outcome);
407
972
  }
973
+ async function loginClaudeCli(io, options) {
974
+ let stop = new AbortController(), deps = { ...authDeps(io, options), signal: stop.signal }, stopLines = () => {
975
+ }, stopInterrupt = (io.onInterrupt ?? defaultOnInterrupt)(() => stop.abort());
976
+ try {
977
+ let outcome = await loginClaude(deps, {
978
+ // --json hands out the start answer and stops waiting; the daemon's
979
+ // sign-in keeps running (only Ctrl-C cancels it), like ChatGPT/Grok.
980
+ wait: !options.json,
981
+ onStart: (info) => {
982
+ if (options.json) {
983
+ io.stdout(`${JSON.stringify(info.raw, null, 2)}
984
+ `);
985
+ return;
986
+ }
987
+ io.stdout(`Opening Claude sign-in in your browser\u2026
988
+ ${info.url ? `If it does not open, visit:
989
+
990
+ ${info.url}
991
+
992
+ ` : `
993
+ `}If the page shows a code, paste it here (or wait): `), stopLines = (io.readLines ?? defaultReadLines)((line) => {
994
+ let code = line.trim();
995
+ code && submitClaudeCode(deps, code).then(() => io.stdout(`Code sent. Waiting for Claude Code to finish...
996
+ `)).catch((err) => io.stderr(`Could not send the code: ${err instanceof Error ? err.message : String(err)}
997
+ `));
998
+ });
999
+ }
1000
+ });
1001
+ if (!outcome.ok && outcome.notInstalled)
1002
+ return io.stderr(`${outcome.error}
1003
+ `), 1;
1004
+ if (options.json) return outcome.ok || outcome.detached ? 0 : outcome.cancelled ? 130 : reportAuth(io, "claude", outcome);
1005
+ if (io.stdout(`
1006
+ `), outcome.ok) {
1007
+ let plan = isRecord(outcome.raw) && typeof outcome.raw.plan == "string" ? outcome.raw.plan : "";
1008
+ return io.stdout(`Signed in to Claude${outcome.email ? ` as ${outcome.email}` : ""}${plan ? ` (${plan} plan)` : ""}.
1009
+ `), 0;
1010
+ }
1011
+ return outcome.cancelled ? (io.stderr(`Claude sign-in cancelled.
1012
+ `), 130) : reportAuth(io, "claude", outcome);
1013
+ } finally {
1014
+ stopLines(), stopInterrupt();
1015
+ }
1016
+ }
1017
+ function defaultReadLines(onLine) {
1018
+ if (!process.stdin.isTTY) return () => {
1019
+ };
1020
+ let rl = createInterface({ input: process.stdin, terminal: !1, historySize: 0 });
1021
+ return rl.on("line", onLine), () => {
1022
+ rl.close(), process.stdin.pause();
1023
+ };
1024
+ }
1025
+ function defaultOnInterrupt(fn) {
1026
+ return process.once("SIGINT", fn), () => {
1027
+ process.removeListener("SIGINT", fn);
1028
+ };
1029
+ }
408
1030
  async function networkAdmin(io, options, args) {
409
1031
  let area = args[0], action = args[1];
410
1032
  if (area === "mesh") {
@@ -432,7 +1054,7 @@ async function streamEvents(io, options, args) {
432
1054
  headers: { Accept: "text/event-stream", ...authHeader(options) }
433
1055
  });
434
1056
  if (!response.ok || !response.body) {
435
- let detail = await response.json().catch(() => null), hint = response.status === 401 ? tunnelTokenHint(options.daemonUrl, options.token) : null;
1057
+ let detail = await response.json().catch(() => null), hint = response.status === 401 ? tunnelTokenHint(options.daemonUrl, options.token, options.env) : null;
436
1058
  throw new Error(`Event stream failed: ${typeof detail?.error == "string" ? detail.error : `HTTP ${response.status} ${response.statusText}`}${hint ? `
437
1059
  ${hint}` : ""}`);
438
1060
  }
@@ -485,7 +1107,7 @@ async function withAgentAndValue(args, label, io, run) {
485
1107
  }
486
1108
  var pipedLines = null, pipedLineIndex = 0;
487
1109
  async function defaultPrompt(question, opts = {}) {
488
- let { createInterface } = await import("readline");
1110
+ let { createInterface: createInterface2 } = await import("readline");
489
1111
  if (!process.stdin.isTTY) {
490
1112
  if (pipedLines === null) {
491
1113
  let chunks = [];
@@ -494,7 +1116,7 @@ async function defaultPrompt(question, opts = {}) {
494
1116
  }
495
1117
  return pipedLines[pipedLineIndex++] ?? "";
496
1118
  }
497
- let rl = createInterface({ input: process.stdin, output: process.stdout, terminal: !0, historySize: 0 }), internals = rl, muted = !1;
1119
+ let rl = createInterface2({ input: process.stdin, output: process.stdout, terminal: !0, historySize: 0 }), internals = rl, muted = !1;
498
1120
  if (opts.hidden) {
499
1121
  let write = internals._writeToOutput.bind(rl);
500
1122
  internals._writeToOutput = (text) => {
@@ -513,7 +1135,7 @@ async function defaultPrompt(question, opts = {}) {
513
1135
  function ask(io, question, hidden = !1) {
514
1136
  return (io.prompt ?? defaultPrompt)(question, { hidden });
515
1137
  }
516
- function postJson(io, options, path, body = {}) {
1138
+ function postJson2(io, options, path, body = {}) {
517
1139
  return requestJson(io, options, path, {
518
1140
  method: "POST",
519
1141
  headers: { "Content-Type": "application/json" },
@@ -537,7 +1159,7 @@ async function identityCommand(io, options, args) {
537
1159
  if (isRecord(current) && current.status !== "none")
538
1160
  return io.stdout(formatIdentity(current)), io.stderr(`An owner identity already exists here; nothing was created.
539
1161
  `), 1;
540
- let passphrase = await passphraseIfNeeded(io, current, !0), created = await postJson(io, options, "/identity/create", passphrase ? { passphrase } : {});
1162
+ let passphrase = await passphraseIfNeeded(io, current, !0), created = await postJson2(io, options, "/identity/create", passphrase ? { passphrase } : {});
541
1163
  if (options.json)
542
1164
  return io.stdout(`${JSON.stringify(created, null, 2)}
543
1165
  `), 0;
@@ -557,12 +1179,13 @@ on another machine to be the same owner there.
557
1179
  `
558
1180
  );
559
1181
  let answer = (await ask(io, 'Type "yes" once you have written the words down: ')).trim().toLowerCase();
560
- return answer === "yes" || answer === "y" ? (await postJson(io, options, "/identity/confirm-backup"), io.stdout(`Backup confirmed.
1182
+ return answer === "yes" || answer === "y" ? (await postJson2(io, options, "/identity/confirm-backup"), io.stdout(`Backup confirmed.
561
1183
  `)) : io.stdout(`Backup not confirmed. The phrase cannot be shown again by the daemon; if you did not save it,
562
1184
  you can reveal it in ADF Studio on this machine (Settings \u2192 Back Up Seed Phrase).
563
1185
  `), 0;
564
1186
  }
565
- if (action === "restore" || action === "import") {
1187
+ if (action === "import") return await importIdentity(io, options, args.slice(1));
1188
+ if (action === "restore") {
566
1189
  let current = await requestJson(io, options, "/identity");
567
1190
  if (isRecord(current) && current.status === "ready")
568
1191
  return io.stdout(formatIdentity(current)), 0;
@@ -570,22 +1193,97 @@ you can reveal it in ADF Studio on this machine (Settings \u2192 Back Up Seed Ph
570
1193
  `);
571
1194
  let mnemonic = (await ask(io, "Seed phrase (12 words, input hidden): ", !0)).trim();
572
1195
  if (!mnemonic) throw new Error("No seed phrase entered.");
573
- let passphrase = await passphraseIfNeeded(io, current, isRecord(current) && current.status !== "locked"), restored = await postJson(io, options, "/identity/restore", { mnemonic, ...passphrase ? { passphrase } : {} }), identity = isRecord(restored) && isRecord(restored.identity) ? restored.identity : restored;
1196
+ let passphrase = await passphraseIfNeeded(io, current, isRecord(current) && current.status !== "locked"), restored = await postJson2(io, options, "/identity/restore", { mnemonic, ...passphrase ? { passphrase } : {} }), identity = isRecord(restored) && isRecord(restored.identity) ? restored.identity : restored;
574
1197
  return io.stdout(options.json ? `${JSON.stringify(restored, null, 2)}
575
1198
  ` : `Owner identity restored.
576
1199
  ${formatIdentity(identity)}`), 0;
577
1200
  }
578
1201
  if (action === "unlock") {
579
- let passphrase = await ask(io, "Passphrase: ", !0), unlocked = await postJson(io, options, "/identity/unlock", { passphrase }), identity = isRecord(unlocked) && isRecord(unlocked.identity) ? unlocked.identity : unlocked;
1202
+ let passphrase = await ask(io, "Passphrase: ", !0), unlocked = await postJson2(io, options, "/identity/unlock", { passphrase }), identity = isRecord(unlocked) && isRecord(unlocked.identity) ? unlocked.identity : unlocked;
580
1203
  return io.stdout(options.json ? `${JSON.stringify(unlocked, null, 2)}
581
1204
  ` : formatIdentity(identity)), 0;
582
1205
  }
583
1206
  if (action === "lock") {
584
- let locked = await postJson(io, options, "/identity/lock"), identity = isRecord(locked) && isRecord(locked.identity) ? locked.identity : locked;
1207
+ let locked = await postJson2(io, options, "/identity/lock"), identity = isRecord(locked) && isRecord(locked.identity) ? locked.identity : locked;
585
1208
  return io.stdout(options.json ? `${JSON.stringify(locked, null, 2)}
586
1209
  ` : formatIdentity(identity)), 0;
587
1210
  }
588
- throw new Error("Usage: adf identity [status | new | restore | unlock | lock]");
1211
+ if (action === "trust-daemon") return await trustDaemon(io, options, args.slice(1));
1212
+ if (action === "untrust-daemon") {
1213
+ let answer = await postJson2(io, options, "/identity/untrust-daemon");
1214
+ if (options.json)
1215
+ return io.stdout(`${JSON.stringify(answer, null, 2)}
1216
+ `), 0;
1217
+ let result = isRecord(answer) ? answer : {}, trust = isRecord(result.trust) ? result.trust : {};
1218
+ return io.stdout(
1219
+ `Revoked key ${String(result.retiredLabel ?? "")}: the daemon now uses a new key, and the old one no longer opens anything here.
1220
+ Slots removed from ${String(result.agentsUpdated ?? 0)} loaded agent(s); the tracked folders are swept in the background, and
1221
+ ADF Studio removes the slot from each agent it opens. A copy of an agent outside the tracked folders keeps the old slot
1222
+ until it is opened, and anyone holding a copy of the old key file can open that slot until then.
1223
+ ${String(trust.message ?? "")}
1224
+ `
1225
+ ), 0;
1226
+ }
1227
+ throw new Error("Usage: adf identity [status | new | restore | import | unlock | lock | trust-daemon | untrust-daemon]");
1228
+ }
1229
+ async function trustDaemon(io, options, args) {
1230
+ for (let arg of args)
1231
+ if (arg !== "--yes") throw new Error(`Unknown option for adf identity trust-daemon: ${arg}`);
1232
+ let current = await requestJson(io, options, "/identity"), trust = isRecord(current) && isRecord(current.trust) ? current.trust : null;
1233
+ if (trust?.state === "trusted")
1234
+ return io.stdout(`${String(trust.message ?? "")}
1235
+ `), 0;
1236
+ if (!args.includes("--yes") && (io.stdout(
1237
+ `Trusting this daemon adds its key (the runtime-enc-key file next to the daemon settings) to
1238
+ trustedDaemonEncKeys. ADF Studio then wraps every agent's credentials to that key, so anyone who
1239
+ can read that file, or write the settings file, can unlock them without the owner passphrase.
1240
+ The grant is an entry in the settings file, not a signed statement.
1241
+ Revoke it with adf identity untrust-daemon.
1242
+ `
1243
+ ), (await ask(io, 'Type "trust" to go ahead: ')).trim().toLowerCase() !== "trust"))
1244
+ return io.stdout(`Nothing was changed.
1245
+ `), 1;
1246
+ let answer = await postJson2(io, options, "/identity/trust-daemon", { confirm: !0 });
1247
+ if (options.json)
1248
+ return io.stdout(`${JSON.stringify(answer, null, 2)}
1249
+ `), 0;
1250
+ let result = isRecord(answer) ? answer : {}, granted = isRecord(result.trust) ? result.trust : {};
1251
+ return io.stdout(`${String(granted.message ?? "")}
1252
+ Slots added to ${String(result.agentsUpdated ?? 0)} loaded agent(s).
1253
+ `), 0;
1254
+ }
1255
+ async function importIdentity(io, options, args = []) {
1256
+ for (let arg of args)
1257
+ if (arg !== "--i-have-a-backup") throw new Error(`Unknown option for adf identity import: ${arg}`);
1258
+ let haveBackup = args.includes("--i-have-a-backup"), current = await requestJson(io, options, "/identity");
1259
+ isRecord(current) && typeof current.ownerDid == "string" && current.ownerDid && io.stdout(`This machine's owner is ${current.ownerDid}. A phrase of another owner replaces it.
1260
+ `);
1261
+ let mnemonic = (await ask(io, "Seed phrase (12 words, input hidden): ", !0)).trim();
1262
+ if (!mnemonic) throw new Error("No seed phrase entered.");
1263
+ let passphrase = await passphraseIfNeeded(io, current, isRecord(current) && current.status !== "locked"), body = { mnemonic, ...passphrase ? { passphrase } : {}, ...haveBackup ? { previousOwnerBackupConfirmed: !0 } : {} }, imported;
1264
+ try {
1265
+ imported = await postJson2(io, options, "/identity/import", body);
1266
+ } catch (err) {
1267
+ if (!(err instanceof DaemonRequestError) || err.apiCode !== "switch_unconfirmed") throw err;
1268
+ if (io.stdout(`${err.message}
1269
+ `), (await ask(io, 'Type "switch" to replace the owner: ')).trim().toLowerCase() !== "switch")
1270
+ return io.stdout(`Nothing was changed.
1271
+ `), 1;
1272
+ imported = await postJson2(io, options, "/identity/import", { ...body, switchOwner: !0 });
1273
+ }
1274
+ if (options.json)
1275
+ return io.stdout(`${JSON.stringify(imported, null, 2)}
1276
+ `), 0;
1277
+ let result = isRecord(imported) ? imported : {}, identity = isRecord(result.identity) ? result.identity : {}, restamp = isRecord(result.restamp) ? result.restamp : {}, failures = Array.isArray(restamp.failures) ? restamp.failures.map(String) : [], skipped = Array.isArray(restamp.skipped) ? restamp.skipped.map(String) : [], reloadFailures = Array.isArray(result.reloadFailures) ? result.reloadFailures.filter(isRecord) : [];
1278
+ return io.stdout(
1279
+ `${result.switched === !0 ? `Owner switched from ${String(result.previousOwnerDid ?? "")}.` : "Owner identity imported."}
1280
+ ` + formatIdentity(identity) + `Restamped ${String(restamp.restamped ?? 0)} agent file(s), ${failures.length} failure(s).
1281
+ ` + failures.map((f) => ` ${f}
1282
+ `).join("") + (skipped.length ? `Not restamped now, another runtime runs them (restamped when next opened):
1283
+ ${skipped.map((f) => ` ${f}
1284
+ `).join("")}` : "") + reloadFailures.map((f) => `Could not reload ${String(f.name ?? "")} (${String(f.filePath ?? "")}): ${String(f.error ?? "")}
1285
+ `).join("")
1286
+ ), failures.length > 0 || reloadFailures.length > 0 ? 1 : 0;
589
1287
  }
590
1288
  async function newAgent(io, options, args) {
591
1289
  let body = {}, valueFlags = {
@@ -613,7 +1311,7 @@ async function newAgent(io, options, args) {
613
1311
  positional.push(arg);
614
1312
  }
615
1313
  positional.length > 0 && (body.name = positional.join(" "));
616
- let created = await postJson(io, options, "/agents/create", body);
1314
+ let created = await postJson2(io, options, "/agents/create", body);
617
1315
  if (options.json)
618
1316
  return io.stdout(`${JSON.stringify(created, null, 2)}
619
1317
  `), 0;
@@ -637,10 +1335,14 @@ function formatIdentity(value) {
637
1335
  ["status", String(value.status ?? "")],
638
1336
  ["ownerDid", String(value.ownerDid ?? "")],
639
1337
  ["runtimeDid", String(value.runtimeDid ?? "")],
1338
+ ...isRecord(value.runtime) ? [["runtime", `${String(value.runtime.source ?? "")} (${String(value.runtime.state ?? "")})`]] : [],
1339
+ ...isRecord(value.trust) && value.trust.state !== "not-applicable" ? [["daemonKey", String(value.trust.state ?? "")]] : [],
640
1340
  ["storage", String(value.storage ?? "")],
641
1341
  ["backupConfirmed", String(value.backupConfirmed ?? !1)]
642
1342
  ])}${typeof value.message == "string" ? `${value.message}
643
- ` : ""}` : formatJsonPretty(value);
1343
+ ` : ""}` + // Only the actionable case: a Studio runtime entry exists but was not used.
1344
+ (isRecord(value.runtime) && value.runtime.state === "rejected" && typeof value.runtime.message == "string" ? `${value.runtime.message}
1345
+ ` : "") : formatJsonPretty(value);
644
1346
  }
645
1347
  function formatTemplates(value) {
646
1348
  let templates = isRecord(value) && Array.isArray(value.templates) ? value.templates.filter(isRecord) : [];
@@ -709,7 +1411,7 @@ ${usageTable}`;
709
1411
  }
710
1412
  function formatAuth(value) {
711
1413
  if (!isRecord(value)) return formatJsonPretty(value);
712
- let chatgpt = isRecord(value.chatgpt) ? value.chatgpt : {}, grok = isRecord(value.grok) ? value.grok : {}, providers = Array.isArray(value.providers) ? value.providers.filter(isRecord) : [], chatgptTable = `ChatGPT:
1414
+ let chatgpt = isRecord(value.chatgpt) ? value.chatgpt : {}, grok = isRecord(value.grok) ? value.grok : {}, claude = isRecord(value.claude) ? value.claude : {}, providers = Array.isArray(value.providers) ? value.providers.filter(isRecord) : [], chatgptTable = `ChatGPT:
713
1415
  ${table(["field", "value"], [
714
1416
  ["authenticated", String(chatgpt.authenticated ?? !1)],
715
1417
  ["email", String(chatgpt.email ?? "")]
@@ -717,7 +1419,14 @@ ${table(["field", "value"], [
717
1419
  ${table(["field", "value"], [
718
1420
  ["authenticated", String(grok.authenticated ?? !1)],
719
1421
  ["email", String(grok.email ?? "")]
720
- ])}`, providersTable = providers.length === 0 ? `Provider credentials: none
1422
+ ])}`, claudeRows = claude.installed === !1 ? [["authenticated", "false"], ["claude code", `not installed (${String(claude.installUrl ?? "")})`]] : [
1423
+ ["authenticated", String(claude.authenticated ?? !1)],
1424
+ ["email", String(claude.email ?? "")],
1425
+ ["plan", String(claude.plan ?? "")],
1426
+ ["claude code", claude.updateRequired ? `${String(claude.version ?? "")} (update required: ADF needs ${String(claude.minVersion ?? "")} or newer)` : String(claude.version ?? "")],
1427
+ ...typeof claude.flowError == "string" && claude.flowError ? [["error", claude.flowError]] : []
1428
+ ], claudeTable = `Claude:
1429
+ ${table(["field", "value"], claudeRows)}`, providersTable = providers.length === 0 ? `Provider credentials: none
721
1430
  ` : `Provider credentials:
722
1431
  ${table(["id", "type", "storage", "apiKey"], providers.map((provider) => [
723
1432
  String(provider.id ?? ""),
@@ -727,6 +1436,7 @@ ${table(["id", "type", "storage", "apiKey"], providers.map((provider) => [
727
1436
  ]))}`;
728
1437
  return `${chatgptTable}
729
1438
  ${grokTable}
1439
+ ${claudeTable}
730
1440
  ${providersTable}`;
731
1441
  }
732
1442
  function formatNetwork(value) {
@@ -972,11 +1682,16 @@ Commands:
972
1682
  runtime [agent] Show daemon or agent runtime diagnostics
973
1683
  providers Show provider configuration and agent resolution
974
1684
  auth Show auth and credential presence
975
- auth login <chatgpt|grok> Sign in to a subscription provider
1685
+ auth login <chatgpt|grok|claude>
1686
+ Sign in to a subscription provider
976
1687
  [--relay|--loopback] chooses where the
977
1688
  ChatGPT OAuth callback is served; defaults to
978
- relay when --url points at a remote daemon
979
- auth logout <chatgpt|grok> Clear a subscription provider's tokens
1689
+ relay when --url points at a remote daemon.
1690
+ claude signs in through Claude Code on the
1691
+ daemon host (ADF stores no Claude credentials)
1692
+ auth logout <chatgpt|grok|claude>
1693
+ Clear a subscription provider's tokens
1694
+ (claude: signs Claude Code out on that host)
980
1695
  settings Show sanitized daemon runtime settings
981
1696
  network Show mesh and WebSocket diagnostics
982
1697
  network mesh [enable|disable] Control daemon mesh registration
@@ -1001,21 +1716,40 @@ Commands:
1001
1716
  mcp [agent] Show daemon MCP registrations or agent MCP state
1002
1717
  adapters [agent] Show daemon adapter registrations or agent adapter state
1003
1718
  events [agent] Follow daemon SSE events
1004
- chat <agent> [--loop <name>] <message>
1719
+ chat <agent> [--loop <name>] [--attach <file>]... <message>
1005
1720
  Send chat (to main, or an inner loop) and
1006
- print the accepted turn id
1721
+ print the accepted turn id. --attach uploads
1722
+ a file to the agent's loop-upload/ folder
1723
+ and attaches it
1007
1724
  identity Show the owner identity status
1008
1725
  identity new Create an owner identity; shows the 12-word
1009
1726
  seed phrase once \u2014 write it down
1010
1727
  identity restore Restore the owner identity from its seed
1011
1728
  phrase (typed at a hidden prompt; the same
1012
1729
  phrase as ADF Studio = the same owner)
1013
- identity unlock|lock Unlock/lock a passphrase-protected identity
1730
+ identity import [--i-have-a-backup]
1731
+ Import an owner phrase like Studio's Import
1732
+ identity: another owner's phrase replaces
1733
+ this machine's owner (asks first; needs the
1734
+ old phrase backed up) and restamps local
1735
+ agents
1736
+ identity unlock|lock Unlock/lock a passphrase-protected identity
1014
1737
  (machines without an OS keychain)
1738
+ identity trust-daemon [--yes] No OS keychain: let ADF Studio wrap agent
1739
+ credentials to this daemon's key file, so
1740
+ they unlock without the passphrase (asks)
1741
+ identity untrust-daemon Revoke that trust
1015
1742
  templates List agent templates
1016
1743
  new [name] [--template <id>] [--provider <id>] [--model <id>]
1017
1744
  [--dir <path>] [--start] Create an agent from a template, sealed
1018
1745
  with the owner identity (like Studio)
1746
+ pair [<host>[:<port>]]
1747
+ [--name <n>] [--yes] Pair with another computer's daemon (its
1748
+ 6-digit code); without a host, lists the
1749
+ ADF computers found on this network and
1750
+ your tailnet. Saves the device token
1751
+ remotes [use <name|local> | forget <name>]
1752
+ Saved remote daemons; use picks the default
1019
1753
 
1020
1754
  Daemon:
1021
1755
  daemon [--port <n>] [--host <h>] [--settings <file>]
@@ -1032,12 +1766,18 @@ Daemon:
1032
1766
  containers stopped)
1033
1767
  daemon restart Stop, then start in the background
1034
1768
  daemon logs [-f] [-n <lines>] Show the background daemon's log (-f follows)
1035
- daemon token Print this machine's daemon access token (for
1036
- clients on other machines: --token or
1037
- ADF_DAEMON_TOKEN)
1769
+ daemon token Print this machine's daemon access token
1770
+ (this machine only; other computers get a
1771
+ device token: daemon devices add / pair)
1772
+ daemon remote on|off|status Remote access (live, no restart); also
1773
+ trust|untrust, serve on|off
1774
+ daemon devices [list|add <label>|revoke <id|label>]
1775
+ Computers with a device token
1776
+ daemon pair Open a pairing window and show its code
1038
1777
 
1039
1778
  Options:
1040
1779
  --url, -u <url> Daemon URL
1780
+ --remote <name> A daemon saved by adf pair (adf remotes)
1041
1781
  --token <token> Daemon access token (default: ADF_DAEMON_TOKEN,
1042
1782
  else the local daemon's token file)
1043
1783
  --json JSON output
@@ -1048,7 +1788,8 @@ Environment:
1048
1788
  ADF_DAEMON_URL Defaults to ${DEFAULT_DAEMON_URL}
1049
1789
  ADF_DAEMON_TOKEN Daemon access token (overrides the local
1050
1790
  <data dir>/daemon-token file)
1051
- ADF_NO_AUTOSTART=1 Same as --no-daemon`;
1791
+ ADF_NO_AUTOSTART=1 Same as --no-daemon
1792
+ ADF_REMOTE Same as --remote (local = this computer)`;
1052
1793
  }
1053
1794
  export {
1054
1795
  runCli,