@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.
- package/README.md +1 -1
- package/dist/adf.mjs +59 -8
- package/dist/chunks/{APEv2Parser-2CNNHMXV.mjs → APEv2Parser-WIXRJLHB.mjs} +2 -2
- package/dist/chunks/{AiffParser-7BR4RL6G.mjs → AiffParser-ZC4QFOVJ.mjs} +5 -5
- package/dist/chunks/{App-7RIIOHJJ.mjs → App-A5GUFCFJ.mjs} +279 -293
- package/dist/chunks/{DsdiffParser-CMD4AX7A.mjs → DsdiffParser-UAPAJHFX.mjs} +5 -5
- package/dist/chunks/{DsfParser-UOGZDS2M.mjs → DsfParser-NNYZK2QX.mjs} +6 -6
- package/dist/chunks/{FlacParser-HB3T3CD4.mjs → FlacParser-MW2W4LO3.mjs} +6 -6
- package/dist/chunks/{MP4Parser-66HBRUSX.mjs → MP4Parser-Z57SYOOQ.mjs} +4 -4
- package/dist/chunks/{MpegParser-YW6LIUEW.mjs → MpegParser-XIIPAZVM.mjs} +7 -7
- package/dist/chunks/{MusepackParser-PAUSXP7D.mjs → MusepackParser-QWNDEYHJ.mjs} +6 -6
- package/dist/chunks/{OggParser-NUBV3GWC.mjs → OggParser-WRX53F77.mjs} +9 -9
- package/dist/chunks/{WavPackParser-WQAGT7TC.mjs → WavPackParser-MJ7WCSJ5.mjs} +2 -2
- package/dist/chunks/{WaveParser-EWLQVDX4.mjs → WaveParser-RAVLHZ5S.mjs} +7 -7
- package/dist/chunks/{adf-schema-US4NFHMR.mjs → adf-schema-YJSGLMA4.mjs} +2 -2
- package/dist/chunks/auth-manager-AJ32FZWS.mjs +19 -0
- package/dist/chunks/auth-manager-C5OBEQK2.mjs +18 -0
- package/dist/chunks/auth-manager-TBNKCJ45.mjs +17 -0
- package/dist/chunks/{chatgpt-subscription-E4MXTIAT.mjs → chatgpt-subscription-MMGNW5KJ.mjs} +5 -2
- package/dist/chunks/chunk-3UGZFWIF.mjs +26 -0
- package/dist/chunks/{chunk-5PO5PKRV.mjs → chunk-4OGZFUKA.mjs} +126 -67
- package/dist/chunks/{chunk-VN7UH7I5.mjs → chunk-6XQZGYMK.mjs} +246 -67
- package/dist/chunks/{chunk-XNPZBLYL.mjs → chunk-75DC5EWN.mjs} +13 -2
- package/dist/chunks/chunk-AZYOXXVL.mjs +865 -0
- package/dist/chunks/{chunk-6VQEXLE2.mjs → chunk-BHY7QDCM.mjs} +41 -3
- package/dist/chunks/chunk-C2S64Y6F.mjs +203 -0
- package/dist/chunks/chunk-C4GA5SI4.mjs +12799 -0
- package/dist/chunks/chunk-DDTTSKGG.mjs +2443 -0
- package/dist/chunks/{chunk-VAUOM3IN.mjs → chunk-DLWCKKIO.mjs} +1 -1
- package/dist/chunks/{chunk-QPLEAE6V.mjs → chunk-G6NBFYG5.mjs} +152 -824
- package/dist/chunks/{chunk-KXEF4F4E.mjs → chunk-GAZO6UMI.mjs} +1 -1
- package/dist/chunks/{chunk-XYTWGHFS.mjs → chunk-H3XLRWVE.mjs} +1 -1
- package/dist/chunks/{chunk-7T2YMTWL.mjs → chunk-H4C65MTP.mjs} +4 -4
- package/dist/chunks/chunk-HBOFZRUS.mjs +738 -0
- package/dist/chunks/chunk-JVPZIMXW.mjs +976 -0
- package/dist/chunks/{chunk-XYV3VCIX.mjs → chunk-JZFONJSL.mjs} +79 -20
- package/dist/chunks/chunk-KDDVQKCG.mjs +65 -0
- package/dist/chunks/chunk-KRPKWOMB.mjs +290 -0
- package/dist/chunks/{chunk-G6PH7RPW.mjs → chunk-LCMANRYY.mjs} +3 -3
- package/dist/chunks/{chunk-ZESQMP66.mjs → chunk-LGL6ML37.mjs} +3 -3
- package/dist/chunks/chunk-LKFHERRV.mjs +145 -0
- package/dist/chunks/chunk-LTX74NU6.mjs +29 -0
- package/dist/chunks/chunk-M2F7C2UJ.mjs +425 -0
- package/dist/chunks/{chunk-62NE4LG4.mjs → chunk-MQAMI5BF.mjs} +5 -5
- package/dist/chunks/chunk-MZRXFWPQ.mjs +278 -0
- package/dist/chunks/{chunk-PRMGZZM5.mjs → chunk-NZLEPJ5M.mjs} +2 -0
- package/dist/chunks/{chunk-CD3ATSXE.mjs → chunk-NZO5O4VN.mjs} +1 -1
- package/dist/chunks/{chunk-I3UYSITV.mjs → chunk-O7XKQ6CH.mjs} +1 -1
- package/dist/chunks/chunk-P3RG5LQA.mjs +244 -0
- package/dist/chunks/chunk-PAK5JCO2.mjs +48 -0
- package/dist/chunks/chunk-PC63YFB7.mjs +34 -0
- package/dist/chunks/{daemon-control-HKFVSQTQ.mjs → chunk-PRPTKMQW.mjs} +67 -26
- package/dist/chunks/{chunk-LFMB4PAA.mjs → chunk-TKXIH27X.mjs} +974 -113
- package/dist/chunks/{chunk-5W7KES6P.mjs → chunk-UZPDETEU.mjs} +1 -1
- package/dist/chunks/chunk-V2T6NZX2.mjs +517 -0
- package/dist/chunks/chunk-V3MM6PCH.mjs +366 -0
- package/dist/chunks/chunk-WJGPNHGX.mjs +67 -0
- package/dist/chunks/chunk-WVTPPOKK.mjs +290 -0
- package/dist/chunks/{chunk-J74STFKE.mjs → chunk-WX6QCUWI.mjs} +272 -51
- package/dist/chunks/{chunk-Y5ZLKFLE.mjs → chunk-XNNJQCZP.mjs} +196 -165
- package/dist/chunks/{chunk-HDBPBORT.mjs → chunk-XT2BJUFC.mjs} +15 -6
- package/dist/chunks/{chunk-HSBV6YFU.mjs → chunk-YRMKSHW3.mjs} +614 -92
- package/dist/chunks/{chunk-BNXUAOO4.mjs → chunk-ZGXUOAWZ.mjs} +3186 -959
- package/dist/chunks/chunk-ZPB3HYM7.mjs +45 -0
- package/dist/chunks/claude-subscription-33BMZB6I.mjs +29 -0
- package/dist/chunks/{cli-NMFIAR7Z.mjs → cli-I47IFDHJ.mjs} +797 -56
- package/dist/chunks/{commands-KPJWFRUE.mjs → commands-E4EQUHE5.mjs} +4 -3
- package/dist/chunks/{daemon-YDIK3AAM.mjs → daemon-FQ5BV3AP.mjs} +82058 -63958
- package/dist/chunks/daemon-control-L3CLG63P.mjs +59 -0
- package/dist/chunks/{daemon-token-WRSULP7G.mjs → daemon-token-L5QKTHU5.mjs} +3 -1
- package/dist/chunks/{discord-DSP4HUWI.mjs → discord-IR62565M.mjs} +3 -52
- package/dist/chunks/{email-BRG7KUO2.mjs → email-KHHTJBDW.mjs} +4 -2
- package/dist/chunks/{grok-subscription-4PYX7TBF.mjs → grok-subscription-E74FS5EG.mjs} +5 -2
- package/dist/chunks/{lib-N7FCZWKH.mjs → lib-L27L5PXM.mjs} +22 -22
- package/dist/chunks/{open-VAU7PXT6.mjs → open-TLGS2Z3Q.mjs} +4 -4
- package/dist/chunks/{parser-JVY2PWSM.mjs → parser-7GARNT5L.mjs} +1 -1
- package/dist/chunks/{pipeline-executor-O77NTGLP.mjs → pipeline-executor-FD4FBWQA.mjs} +6 -5
- package/dist/chunks/prompt-RRHZX4YT.mjs +13 -0
- package/dist/chunks/remote-access-cli-XXAPQWH6.mjs +268 -0
- package/dist/chunks/remote-hello-UFWUBDQI.mjs +37 -0
- package/dist/chunks/sdk-5FD2ZP7N.mjs +28422 -0
- package/dist/chunks/{slack-OCNNF4DX.mjs → slack-IYC5RAB5.mjs} +3 -4
- package/dist/chunks/{store-HUVDJPYN.mjs → store-ZRN2WCFB.mjs} +6 -3
- package/dist/chunks/tailscale-WLJATFMB.mjs +25 -0
- package/dist/chunks/{theme-SFE3JMA2.mjs → theme-HLBDTF6F.mjs} +1 -1
- package/dist/chunks/{themes-YLXLOIJF.mjs → themes-BQ72P446.mjs} +2 -2
- package/dist/chunks/{token-6G3HCMNX.mjs → token-6D22WECM.mjs} +20 -16
- package/dist/chunks/{token-util-UXYSDQIW.mjs → token-util-YF3ZWBZ4.mjs} +1 -2
- package/dist/chunks/{tui-UQJLK7XI.mjs → tui-SPSPDADM.mjs} +17 -14
- package/dist/chunks/{welcome-XHXFV7SH.mjs → welcome-J5JZZAM5.mjs} +1 -1
- package/dist/chunks/{whatsapp-AJMRQBT7.mjs → whatsapp-YJFFVN4Q.mjs} +9 -9
- package/package.json +1 -1
- package/dist/chunks/chunk-GKKCMXIZ.mjs +0 -94
- package/dist/chunks/chunk-IHDDCZBS.mjs +0 -112
- package/dist/chunks/chunk-KWFOS5WC.mjs +0 -41
- package/dist/chunks/chunk-UEZR2CNS.mjs +0 -65
- package/dist/chunks/chunk-VMHTYDH4.mjs +0 -172
- package/dist/chunks/chunk-Y2QA53AN.mjs +0 -259
- package/dist/chunks/{chunk-NBTUWMGP.mjs → chunk-2MTE22CR.mjs} +3 -3
- 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
|
-
|
|
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
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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-
|
|
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
|
|
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
|
|
92
|
-
|
|
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 =
|
|
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,
|
|
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
|
|
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(
|
|
266
|
-
if (!agent || !text) throw new Error("Usage: adf chat <agent> [--loop <name>] <message>");
|
|
267
|
-
let
|
|
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({
|
|
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
|
|
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
|
-
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
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 === "
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
` : ""}` :
|
|
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
|
-
])}`,
|
|
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>
|
|
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
|
-
|
|
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
|
|
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
|
|
1036
|
-
|
|
1037
|
-
|
|
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,
|