@tarfa/cli 0.1.4 → 0.1.6
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 +3 -2
- package/dist/{chunk-OC5VX7K3.js → chunk-UPCCR4ZA.js} +474 -67
- package/dist/cli.js +145 -105
- package/dist/serve.js +43 -12
- package/package.json +1 -1
|
@@ -2463,8 +2463,6 @@ function deviceFingerprint(x25519PublicKey) {
|
|
|
2463
2463
|
|
|
2464
2464
|
// ../secure-channel/src/pairing-link.ts
|
|
2465
2465
|
var PREFIX = "#pair?";
|
|
2466
|
-
var KEY = /^[A-Za-z0-9_-]{43}$/;
|
|
2467
|
-
var MAX_NAME = 64;
|
|
2468
2466
|
var MAX_ADDRESSES = 4;
|
|
2469
2467
|
var MAX_PATH = 128;
|
|
2470
2468
|
function isPairingPath(path) {
|
|
@@ -2485,19 +2483,6 @@ function pairingLink(pwaOrigin, pairing) {
|
|
|
2485
2483
|
const query = fields.map(([name, value]) => `${name}=${encode(value)}`).join("&");
|
|
2486
2484
|
return `${new URL(pwaOrigin).origin}/${PREFIX}${query}`;
|
|
2487
2485
|
}
|
|
2488
|
-
function parsePairingLink(text) {
|
|
2489
|
-
const trimmed = text.trim();
|
|
2490
|
-
const hash = trimmed.startsWith("#") ? trimmed : trimmed.slice(Math.max(0, trimmed.indexOf("#")));
|
|
2491
|
-
if (!trimmed.includes("#") || !hash.startsWith(PREFIX)) return null;
|
|
2492
|
-
const query = new URLSearchParams(hash.slice(PREFIX.length));
|
|
2493
|
-
const urls = query.getAll("u");
|
|
2494
|
-
const [agentKey, secret] = ["k", "s"].map((name2) => query.get(name2) ?? "");
|
|
2495
|
-
const name = (query.get("n") ?? "").trim() || "machine";
|
|
2496
|
-
if (!KEY.test(agentKey) || !KEY.test(secret) || name.length > MAX_NAME) return null;
|
|
2497
|
-
if (urls.length === 0 || urls.length > MAX_ADDRESSES || !urls.every(isMachineUrl)) return null;
|
|
2498
|
-
const defaultPath = query.get("p") ?? "";
|
|
2499
|
-
return { urls, agentKey, secret, name, ...isPairingPath(defaultPath) ? { defaultPath } : {} };
|
|
2500
|
-
}
|
|
2501
2486
|
var LOOPBACK = /* @__PURE__ */ new Set(["127.0.0.1", "localhost", "[::1]"]);
|
|
2502
2487
|
function isMachineUrl(text) {
|
|
2503
2488
|
try {
|
|
@@ -2509,6 +2494,131 @@ function isMachineUrl(text) {
|
|
|
2509
2494
|
}
|
|
2510
2495
|
}
|
|
2511
2496
|
|
|
2497
|
+
// ../secure-channel/src/pairing-code.ts
|
|
2498
|
+
var PREFIX2 = "TARFA";
|
|
2499
|
+
var VERSION = 1;
|
|
2500
|
+
var MAX_PLAUSIBLE_VERSION = 15;
|
|
2501
|
+
var ALPHABET = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
|
|
2502
|
+
var KEY_BYTES = 32;
|
|
2503
|
+
var CHECKSUM_BYTES = 4;
|
|
2504
|
+
var MAX_NAME = 64;
|
|
2505
|
+
var SCHEMES = ["wss://", "ws://"];
|
|
2506
|
+
function pairingCode(pairing) {
|
|
2507
|
+
const out = [VERSION, ...rawKey(pairing.agentKey), ...rawKey(pairing.secret)];
|
|
2508
|
+
const text = (value) => {
|
|
2509
|
+
const bytes = new TextEncoder().encode(value);
|
|
2510
|
+
if (bytes.length > 65535) throw new Error("pairing code field too long");
|
|
2511
|
+
out.push(bytes.length >> 8, bytes.length & 255, ...bytes);
|
|
2512
|
+
};
|
|
2513
|
+
text(pairing.name);
|
|
2514
|
+
text(pairing.defaultPath ?? "");
|
|
2515
|
+
out.push(pairing.urls.length);
|
|
2516
|
+
for (const url of pairing.urls) {
|
|
2517
|
+
const scheme = SCHEMES.findIndex((s) => url.toLowerCase().startsWith(s));
|
|
2518
|
+
if (scheme < 0) throw new Error(`not a machine address: ${url}`);
|
|
2519
|
+
out.push(scheme);
|
|
2520
|
+
text(url.slice(SCHEMES[scheme].length));
|
|
2521
|
+
}
|
|
2522
|
+
const body = Uint8Array.from(out);
|
|
2523
|
+
const code = PREFIX2 + encode32(Uint8Array.from([...body, ...sha256(body).slice(0, CHECKSUM_BYTES)]));
|
|
2524
|
+
if (!parsePairingCode(code).ok) throw new Error("these pairing details do not make a valid pairing code");
|
|
2525
|
+
return code;
|
|
2526
|
+
}
|
|
2527
|
+
function rawKey(key2) {
|
|
2528
|
+
const bytes = /^[A-Za-z0-9_-]{43}$/.test(key2) ? b64url.decode(key2) : new Uint8Array();
|
|
2529
|
+
if (bytes.length !== KEY_BYTES || b64url.encode(bytes) !== key2) throw new Error("a pairing key must be 32 bytes, canonically encoded");
|
|
2530
|
+
return bytes;
|
|
2531
|
+
}
|
|
2532
|
+
function parsePairingCode(text) {
|
|
2533
|
+
const invalid = { ok: false, reason: "invalid" };
|
|
2534
|
+
const compact = text.replace(/\s+/g, "").toUpperCase();
|
|
2535
|
+
if (!compact.startsWith(PREFIX2)) return invalid;
|
|
2536
|
+
const bytes = decode32(compact.slice(PREFIX2.length));
|
|
2537
|
+
if (!bytes || bytes.length < 1 + CHECKSUM_BYTES) return invalid;
|
|
2538
|
+
if (bytes[0] > VERSION && bytes[0] <= MAX_PLAUSIBLE_VERSION) return { ok: false, reason: "newer" };
|
|
2539
|
+
const body = bytes.slice(0, -CHECKSUM_BYTES);
|
|
2540
|
+
const sum = sha256(body);
|
|
2541
|
+
if (!bytes.slice(-CHECKSUM_BYTES).every((b, i) => b === sum[i])) return invalid;
|
|
2542
|
+
if (body[0] !== VERSION) return invalid;
|
|
2543
|
+
const pairing = readBody(body);
|
|
2544
|
+
return pairing ? { ok: true, pairing } : invalid;
|
|
2545
|
+
}
|
|
2546
|
+
function readBody(body) {
|
|
2547
|
+
let at = 1;
|
|
2548
|
+
const take = (n) => {
|
|
2549
|
+
if (at + n > body.length) return null;
|
|
2550
|
+
at += n;
|
|
2551
|
+
return body.slice(at - n, at);
|
|
2552
|
+
};
|
|
2553
|
+
const decoder = new TextDecoder("utf-8", { fatal: true });
|
|
2554
|
+
const text = () => {
|
|
2555
|
+
const length = take(2);
|
|
2556
|
+
const bytes = length && take(length[0] << 8 | length[1]);
|
|
2557
|
+
if (!bytes) return null;
|
|
2558
|
+
try {
|
|
2559
|
+
return decoder.decode(bytes);
|
|
2560
|
+
} catch {
|
|
2561
|
+
return null;
|
|
2562
|
+
}
|
|
2563
|
+
};
|
|
2564
|
+
const agentKey = take(KEY_BYTES);
|
|
2565
|
+
const secret = take(KEY_BYTES);
|
|
2566
|
+
const rawName = text();
|
|
2567
|
+
const path = text();
|
|
2568
|
+
const count = take(1)?.[0];
|
|
2569
|
+
if (!agentKey || !secret || rawName === null || path === null || count === void 0) return null;
|
|
2570
|
+
if (count === 0 || count > MAX_ADDRESSES) return null;
|
|
2571
|
+
const urls = [];
|
|
2572
|
+
for (let i = 0; i < count; i++) {
|
|
2573
|
+
const scheme = SCHEMES[take(1)?.[0] ?? -1];
|
|
2574
|
+
const rest = text();
|
|
2575
|
+
if (!scheme || rest === null || !isMachineUrl(scheme + rest)) return null;
|
|
2576
|
+
urls.push(scheme + rest);
|
|
2577
|
+
}
|
|
2578
|
+
if (at !== body.length) return null;
|
|
2579
|
+
const name = rawName.trim() || "machine";
|
|
2580
|
+
if (name.length > MAX_NAME) return null;
|
|
2581
|
+
return {
|
|
2582
|
+
urls,
|
|
2583
|
+
agentKey: b64url.encode(agentKey),
|
|
2584
|
+
secret: b64url.encode(secret),
|
|
2585
|
+
name,
|
|
2586
|
+
// A path the phone cannot use costs the path, not the pairing.
|
|
2587
|
+
...isPairingPath(path) ? { defaultPath: path } : {}
|
|
2588
|
+
};
|
|
2589
|
+
}
|
|
2590
|
+
function encode32(bytes) {
|
|
2591
|
+
let out = "";
|
|
2592
|
+
let value = 0;
|
|
2593
|
+
let bits = 0;
|
|
2594
|
+
for (const byte of bytes) {
|
|
2595
|
+
value = (value << 8 | byte) & 4095;
|
|
2596
|
+
bits += 8;
|
|
2597
|
+
while (bits >= 5) {
|
|
2598
|
+
out += ALPHABET[value >>> bits - 5 & 31];
|
|
2599
|
+
bits -= 5;
|
|
2600
|
+
}
|
|
2601
|
+
}
|
|
2602
|
+
return bits > 0 ? out + ALPHABET[value << 5 - bits & 31] : out;
|
|
2603
|
+
}
|
|
2604
|
+
function decode32(text) {
|
|
2605
|
+
const out = [];
|
|
2606
|
+
let value = 0;
|
|
2607
|
+
let bits = 0;
|
|
2608
|
+
for (const char of text) {
|
|
2609
|
+
const digit = ALPHABET.indexOf(char);
|
|
2610
|
+
if (digit < 0) return null;
|
|
2611
|
+
value = (value << 5 | digit) & 4095;
|
|
2612
|
+
bits += 5;
|
|
2613
|
+
if (bits >= 8) {
|
|
2614
|
+
out.push(value >>> bits - 8 & 255);
|
|
2615
|
+
bits -= 8;
|
|
2616
|
+
}
|
|
2617
|
+
}
|
|
2618
|
+
if (bits >= 5 || (value & (1 << bits) - 1) !== 0) return null;
|
|
2619
|
+
return Uint8Array.from(out);
|
|
2620
|
+
}
|
|
2621
|
+
|
|
2512
2622
|
// ../../node_modules/.pnpm/@noble+ciphers@2.4.0/node_modules/@noble/ciphers/utils.js
|
|
2513
2623
|
function isBytes3(a) {
|
|
2514
2624
|
return a instanceof Uint8Array || ArrayBuffer.isView(a) && a.constructor.name === "Uint8Array" && "BYTES_PER_ELEMENT" in a && a.BYTES_PER_ELEMENT === 1;
|
|
@@ -3740,10 +3850,10 @@ var SHELL_SAFE = /^[\w./@+-]+$/;
|
|
|
3740
3850
|
var ADDRESS = /^wss:\/\/[A-Za-z0-9.-]+(:\d{1,5})?(\/[\w-]{0,64})?$/;
|
|
3741
3851
|
var PWA_ORIGIN = /^https?:\/\/[A-Za-z0-9.-]+(:\d{1,5})?\/?$/;
|
|
3742
3852
|
var FUNNEL_PORTS_CAP = "https://tailscale.com/cap/funnel-ports?ports=";
|
|
3743
|
-
function planSetup(
|
|
3853
|
+
function planSetup(given, state) {
|
|
3744
3854
|
if (state.phonePortOwner === "other") {
|
|
3745
3855
|
return {
|
|
3746
|
-
error: `127.0.0.1:${
|
|
3856
|
+
error: `127.0.0.1:${given.phonePort} is taken by another process (another user's agent?): pick a free one with --phone-port, e.g. ${given.phonePort + 1}`
|
|
3747
3857
|
};
|
|
3748
3858
|
}
|
|
3749
3859
|
if (state.hooksPortOwner === "other") {
|
|
@@ -3757,21 +3867,27 @@ function planSetup(flags, state) {
|
|
|
3757
3867
|
if (state.adoption?.agentLive.kind === "other") {
|
|
3758
3868
|
return { error: `${state.adoption.agentLive.path} is not a link, and setup only ever removes a link there: move it away, then run tarfa setup again` };
|
|
3759
3869
|
}
|
|
3870
|
+
const reach = resolveReachability(given, state);
|
|
3871
|
+
if ("error" in reach) return reach;
|
|
3872
|
+
const flags = reach.flags;
|
|
3873
|
+
const relayHint = (error) => reach.relayDefault ? { error: `${error.error} (this machine was reached through a public relay, so setup defaults to Funnel; pass --tailscale serve for tailnet only)` } : error;
|
|
3760
3874
|
const target = `http://127.0.0.1:${flags.phonePort}`;
|
|
3761
3875
|
const path = flags.slug ? `/${flags.slug}` : "/";
|
|
3762
3876
|
const steps = [];
|
|
3763
|
-
const checks = [];
|
|
3877
|
+
const checks = [...reach.notes];
|
|
3764
3878
|
let serveStep;
|
|
3765
3879
|
let operatorStep;
|
|
3766
3880
|
let derived;
|
|
3881
|
+
let publicDerived = false;
|
|
3767
3882
|
if (flags.tailscale !== "none") {
|
|
3768
3883
|
const port = flags.tailscalePort ?? (flags.tailscale === "funnel" ? 1e4 : 443);
|
|
3769
3884
|
const checked = checkTailscale(flags.tailscale, port, state.tailscale);
|
|
3770
|
-
if ("error" in checked) return checked;
|
|
3885
|
+
if ("error" in checked) return relayHint(checked);
|
|
3771
3886
|
checks.push(...checked.checks);
|
|
3772
3887
|
if (state.serve === null) return { error: "could not read `tailscale serve status --json`: setup will not add a handler it cannot check against the existing ones" };
|
|
3773
3888
|
const served = serveNeeds(state.serve, `${checked.host}:${port}`, port, path, target, flags.tailscale === "funnel");
|
|
3774
|
-
if ("error" in served) return served;
|
|
3889
|
+
if ("error" in served) return relayHint(served);
|
|
3890
|
+
publicDerived = flags.tailscale === "funnel" || served.public;
|
|
3775
3891
|
if (served.public) checks.push(`tailscale: port ${port} is on Funnel, so this address is reachable from the internet`);
|
|
3776
3892
|
if (served.needed) serveStep = { kind: "tailscale-serve", port, path, target, funnel: flags.tailscale === "funnel" || served.public };
|
|
3777
3893
|
derived = `wss://${checked.host}:${port}${path === "/" ? "" : path}`;
|
|
@@ -3782,6 +3898,7 @@ function planSetup(flags, state) {
|
|
|
3782
3898
|
}
|
|
3783
3899
|
const addresses = [...derived ? [derived] : [], ...flags.addresses];
|
|
3784
3900
|
if (addresses.length > MAX_ADDRESSES) return { error: `a machine takes at most ${MAX_ADDRESSES} addresses (got ${addresses.length})` };
|
|
3901
|
+
checks.push(describeReachability(addresses.length > 0 ? addresses : reach.kept, state, derived !== void 0 ? { address: derived, public: publicDerived } : null));
|
|
3785
3902
|
if (state.legacyService) steps.push({ kind: "stop-legacy-service" });
|
|
3786
3903
|
if (state.stateDir === "legacy") steps.push({ kind: "move-state" });
|
|
3787
3904
|
const adoption = state.adoption;
|
|
@@ -3820,6 +3937,65 @@ function planSetup(flags, state) {
|
|
|
3820
3937
|
if (flags.pair) steps.push({ kind: "pair" });
|
|
3821
3938
|
return { steps, checks };
|
|
3822
3939
|
}
|
|
3940
|
+
var CHOOSE = "say whether phones must reach this machine without Tailscale: --tailscale funnel (with or without Tailscale, through Funnel) or --tailscale serve (only with Tailscale on); --tailscale none --address <wss-url> if you publish the phone port yourself";
|
|
3941
|
+
function resolveReachability(flags, state) {
|
|
3942
|
+
const env = withLegacyEnv(parseEnvFile(state.envText ?? ""));
|
|
3943
|
+
const relay = Boolean(env.TARFA_RELAY_URLS?.trim() || env.TARFA_RELAY_URL?.trim()) || state.legacyService;
|
|
3944
|
+
const kept = (env.TARFA_PHONE_ADDRESSES ?? "").split(",").map((a) => a.trim()).filter(Boolean);
|
|
3945
|
+
const settled = (tailscale, over = {}, notes = [], relayDefault = false) => ({
|
|
3946
|
+
flags: { ...flags, ...over, tailscale },
|
|
3947
|
+
kept,
|
|
3948
|
+
notes,
|
|
3949
|
+
relayDefault
|
|
3950
|
+
});
|
|
3951
|
+
if (flags.tailscale !== void 0) {
|
|
3952
|
+
const lost = relay && flags.tailscale === "serve" ? ["this machine was reached through a public relay, so phones reached it without Tailscale; with --tailscale serve they reach it only with Tailscale on (--tailscale funnel keeps the old reach)"] : [];
|
|
3953
|
+
return settled(flags.tailscale, {}, lost);
|
|
3954
|
+
}
|
|
3955
|
+
if (flags.addresses.length > 0 || kept.length > 0) return settled("none");
|
|
3956
|
+
const handler = phonePortHandler(state.serve, `http://127.0.0.1:${flags.phonePort}`, flags.slug, flags.tailscalePort);
|
|
3957
|
+
if (handler && relay && !handler.funnel) {
|
|
3958
|
+
return {
|
|
3959
|
+
error: `this machine was reached through a public relay, but its phone port is served on the tailnet only (port ${handler.port}): pass --tailscale funnel to keep it reachable without Tailscale, or --tailscale serve to make it tailnet-only`
|
|
3960
|
+
};
|
|
3961
|
+
}
|
|
3962
|
+
if (handler) {
|
|
3963
|
+
return settled(handler.funnel ? "funnel" : "serve", { tailscalePort: handler.port, ...handler.slug ? { slug: handler.slug } : {} });
|
|
3964
|
+
}
|
|
3965
|
+
if (relay) {
|
|
3966
|
+
return settled("funnel", {}, ["this machine was reached through a public relay, so setup keeps it reachable without Tailscale: --tailscale funnel (pass --tailscale serve for tailnet only)"], true);
|
|
3967
|
+
}
|
|
3968
|
+
return { error: CHOOSE };
|
|
3969
|
+
}
|
|
3970
|
+
function phonePortHandler(serve, target, slug, port) {
|
|
3971
|
+
const found = [];
|
|
3972
|
+
for (const [hostPort, web] of Object.entries(serve?.Web ?? {})) {
|
|
3973
|
+
for (const [path, handler] of Object.entries(web.Handlers ?? {})) {
|
|
3974
|
+
if (handler.Proxy?.replace(/\/$/, "") !== target) continue;
|
|
3975
|
+
if (slug !== void 0 && path !== `/${slug}`) continue;
|
|
3976
|
+
const at = Number(hostPort.slice(hostPort.lastIndexOf(":") + 1));
|
|
3977
|
+
if (port !== void 0 && at !== port) continue;
|
|
3978
|
+
found.push({ port: at, ...path === "/" ? {} : { slug: path.slice(1) }, funnel: serve?.AllowFunnel?.[hostPort] === true });
|
|
3979
|
+
}
|
|
3980
|
+
}
|
|
3981
|
+
return found.find((h) => h.funnel) ?? found[0] ?? null;
|
|
3982
|
+
}
|
|
3983
|
+
function describeReachability(addresses, state, derived) {
|
|
3984
|
+
if (addresses.length === 0) {
|
|
3985
|
+
return "reachability: phones cannot reach this machine yet: it has no phone address (--tailscale funnel or serve, or --address)";
|
|
3986
|
+
}
|
|
3987
|
+
const self = (state.tailscale?.Self?.DNSName ?? "").replace(/\.$/, "");
|
|
3988
|
+
const kinds = addresses.map((address) => {
|
|
3989
|
+
if (derived?.address === address) return derived.public ? "public" : "tailnet";
|
|
3990
|
+
const url = new URL(address);
|
|
3991
|
+
if (state.serve === null || !self || url.hostname !== self) return "unknown";
|
|
3992
|
+
return state.serve.AllowFunnel?.[`${url.hostname}:${url.port || 443}`] === true ? "public" : "tailnet";
|
|
3993
|
+
});
|
|
3994
|
+
const list = addresses.join(", ");
|
|
3995
|
+
if (kinds.includes("public")) return `reachability: phones reach this machine with or without Tailscale (${list})`;
|
|
3996
|
+
if (kinds.every((k) => k === "tailnet")) return `reachability: phones reach this machine only with Tailscale on (${list})`;
|
|
3997
|
+
return `reachability: phones reach this machine at ${list}; setup cannot tell whether that needs Tailscale`;
|
|
3998
|
+
}
|
|
3823
3999
|
function planEnv(flags, state, addresses) {
|
|
3824
4000
|
const legacy = renameLegacyEnvLines(state.envText ?? "");
|
|
3825
4001
|
const before = legacy.text;
|
|
@@ -3971,8 +4147,8 @@ function parseSetupFlags(argv) {
|
|
|
3971
4147
|
if (phonePort === null) return { error: "--phone-port must be a port number (1-65535)" };
|
|
3972
4148
|
const slug = single.get("--slug");
|
|
3973
4149
|
if (slug !== void 0 && !SLUG.test(slug)) return { error: "--slug takes letters, digits, _ and - (at most 64)" };
|
|
3974
|
-
const tailscale = single.get("--tailscale")
|
|
3975
|
-
if (tailscale !== "serve" && tailscale !== "funnel" && tailscale !== "none") return { error: "--tailscale takes serve, funnel or none" };
|
|
4150
|
+
const tailscale = single.get("--tailscale");
|
|
4151
|
+
if (tailscale !== void 0 && tailscale !== "serve" && tailscale !== "funnel" && tailscale !== "none") return { error: "--tailscale takes serve, funnel or none" };
|
|
3976
4152
|
const tailscalePort = single.has("--tailscale-port") ? portNumber(single.get("--tailscale-port")) : void 0;
|
|
3977
4153
|
if (tailscalePort === null) return { error: "--tailscale-port must be a port number" };
|
|
3978
4154
|
const bad = addresses.find((a) => !isMachineUrl(a) || !ADDRESS.test(a));
|
|
@@ -3993,7 +4169,7 @@ function parseSetupFlags(argv) {
|
|
|
3993
4169
|
...defaultPath !== void 0 ? { defaultPath } : {},
|
|
3994
4170
|
phonePort,
|
|
3995
4171
|
...slug !== void 0 ? { slug } : {},
|
|
3996
|
-
tailscale,
|
|
4172
|
+
...tailscale !== void 0 ? { tailscale } : {},
|
|
3997
4173
|
...tailscalePort !== void 0 ? { tailscalePort } : {},
|
|
3998
4174
|
addresses,
|
|
3999
4175
|
...pwa !== void 0 ? { pwa } : {},
|
|
@@ -4207,6 +4383,7 @@ function loadConfig(rawEnv, home) {
|
|
|
4207
4383
|
// Usually a symlink to a vocabulary kept with the user's own dotfiles, so one list can
|
|
4208
4384
|
// serve every machine; TARFA_KEYTERMS_PATH points at it directly instead.
|
|
4209
4385
|
keytermsPath: env.TARFA_KEYTERMS_PATH ?? join3(stateDir, "keyterms.txt"),
|
|
4386
|
+
dictationKeysPath: join3(stateDir, "dictation-keys.json"),
|
|
4210
4387
|
clientLogPath: join3(stateDir, "client-log.jsonl"),
|
|
4211
4388
|
hookEventLogPath: join3(stateDir, "hook-events.jsonl"),
|
|
4212
4389
|
messageLogPath: join3(stateDir, "messages.jsonl"),
|
|
@@ -4432,8 +4609,9 @@ var TmuxAdapter = class {
|
|
|
4432
4609
|
return set;
|
|
4433
4610
|
}
|
|
4434
4611
|
async createSession(input) {
|
|
4612
|
+
const env = Object.entries(input.env ?? {}).flatMap(([k, v]) => ["-e", `${k}=${v}`]);
|
|
4435
4613
|
try {
|
|
4436
|
-
await this.tmux(["new-session", "-d", "-s", input.name, "-c", input.cwd, input.command]);
|
|
4614
|
+
await this.tmux(["new-session", "-d", "-s", input.name, "-c", input.cwd, ...env, input.command]);
|
|
4437
4615
|
} catch (err) {
|
|
4438
4616
|
if ((err.stderr ?? "").includes("duplicate session")) throw new DuplicateSessionError(input.name);
|
|
4439
4617
|
throw err;
|
|
@@ -4937,9 +5115,25 @@ async function browseDir(deps, path, allowedRoots) {
|
|
|
4937
5115
|
};
|
|
4938
5116
|
}
|
|
4939
5117
|
|
|
4940
|
-
// src/infrastructure/
|
|
4941
|
-
import { mkdir as mkdir4,
|
|
5118
|
+
// src/infrastructure/atomic-write.ts
|
|
5119
|
+
import { chmod, mkdir as mkdir4, rename as rename3, rm, writeFile as writeFile3 } from "node:fs/promises";
|
|
4942
5120
|
import { dirname as dirname5 } from "node:path";
|
|
5121
|
+
async function atomicWrite(path, text, mode) {
|
|
5122
|
+
await mkdir4(dirname5(path), { recursive: true });
|
|
5123
|
+
const temp = `${path}.tmp-${process.pid}`;
|
|
5124
|
+
try {
|
|
5125
|
+
await writeFile3(temp, text, { mode });
|
|
5126
|
+
await chmod(temp, mode);
|
|
5127
|
+
await rename3(temp, path);
|
|
5128
|
+
} catch (err) {
|
|
5129
|
+
await rm(temp, { force: true });
|
|
5130
|
+
throw err;
|
|
5131
|
+
}
|
|
5132
|
+
}
|
|
5133
|
+
|
|
5134
|
+
// src/infrastructure/agent-key-file.ts
|
|
5135
|
+
import { mkdir as mkdir5, readFile as readFile4, writeFile as writeFile4 } from "node:fs/promises";
|
|
5136
|
+
import { dirname as dirname6 } from "node:path";
|
|
4943
5137
|
async function loadOrCreateAgentKeys(path) {
|
|
4944
5138
|
try {
|
|
4945
5139
|
return parseKeys(await readFile4(path, "utf8"), path);
|
|
@@ -4947,9 +5141,9 @@ async function loadOrCreateAgentKeys(path) {
|
|
|
4947
5141
|
if (err.code !== "ENOENT") throw err;
|
|
4948
5142
|
}
|
|
4949
5143
|
const keys = generateAgentKeys();
|
|
4950
|
-
await
|
|
5144
|
+
await mkdir5(dirname6(path), { recursive: true, mode: 448 });
|
|
4951
5145
|
try {
|
|
4952
|
-
await
|
|
5146
|
+
await writeFile4(path, `${JSON.stringify(keys, null, 2)}
|
|
4953
5147
|
`, { mode: 384, flag: "wx" });
|
|
4954
5148
|
return keys;
|
|
4955
5149
|
} catch (err) {
|
|
@@ -5020,8 +5214,8 @@ function forgetDevice(roster, id2) {
|
|
|
5020
5214
|
}
|
|
5021
5215
|
|
|
5022
5216
|
// src/infrastructure/file-device-store.ts
|
|
5023
|
-
import { mkdir as
|
|
5024
|
-
import { dirname as
|
|
5217
|
+
import { mkdir as mkdir6, readFile as readFile5 } from "node:fs/promises";
|
|
5218
|
+
import { dirname as dirname7 } from "node:path";
|
|
5025
5219
|
import { z as z2 } from "zod";
|
|
5026
5220
|
var key = z2.string().regex(/^[A-Za-z0-9_-]{43}$/);
|
|
5027
5221
|
var device = z2.object({ id: z2.string(), x25519: key, ed25519: key, name: z2.string(), enrolledAt: z2.string() });
|
|
@@ -5068,7 +5262,7 @@ var FileDeviceStore = class {
|
|
|
5068
5262
|
}
|
|
5069
5263
|
if (parsed?.tokenEnrolment === false) return false;
|
|
5070
5264
|
}
|
|
5071
|
-
await
|
|
5265
|
+
await mkdir6(dirname7(this.filePath), { recursive: true, mode: 448 });
|
|
5072
5266
|
await this.update((roster) => roster);
|
|
5073
5267
|
return true;
|
|
5074
5268
|
}
|
|
@@ -5082,8 +5276,112 @@ var FileDeviceStore = class {
|
|
|
5082
5276
|
}
|
|
5083
5277
|
};
|
|
5084
5278
|
|
|
5279
|
+
// src/application/dictation-keys.ts
|
|
5280
|
+
var DICTATION_PROVIDERS = ["elevenlabs", "openrouter"];
|
|
5281
|
+
var MAX_KEY_LENGTH = 512;
|
|
5282
|
+
function providerOf(name) {
|
|
5283
|
+
return DICTATION_PROVIDERS.includes(name) ? name : null;
|
|
5284
|
+
}
|
|
5285
|
+
function checkKey(raw) {
|
|
5286
|
+
const key2 = raw.trim();
|
|
5287
|
+
if (key2 === "") return { ok: false, error: "no key given" };
|
|
5288
|
+
if (/\s/.test(key2)) return { ok: false, error: "a key holds no whitespace" };
|
|
5289
|
+
if (key2.length > MAX_KEY_LENGTH) return { ok: false, error: `a key is at most ${MAX_KEY_LENGTH} characters` };
|
|
5290
|
+
return { ok: true, key: key2 };
|
|
5291
|
+
}
|
|
5292
|
+
function parseDictationKeys(raw) {
|
|
5293
|
+
let value;
|
|
5294
|
+
try {
|
|
5295
|
+
value = JSON.parse(raw);
|
|
5296
|
+
} catch {
|
|
5297
|
+
return "malformed";
|
|
5298
|
+
}
|
|
5299
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return "malformed";
|
|
5300
|
+
const keys = {};
|
|
5301
|
+
for (const provider of DICTATION_PROVIDERS) {
|
|
5302
|
+
const candidate = value[provider];
|
|
5303
|
+
if (typeof candidate !== "string") continue;
|
|
5304
|
+
const checked = checkKey(candidate);
|
|
5305
|
+
if (checked.ok) keys[provider] = checked.key;
|
|
5306
|
+
}
|
|
5307
|
+
return keys;
|
|
5308
|
+
}
|
|
5309
|
+
function withKey(keys, provider, key2) {
|
|
5310
|
+
return { ...keys, [provider]: key2 };
|
|
5311
|
+
}
|
|
5312
|
+
function withoutKey(keys, provider) {
|
|
5313
|
+
const { [provider]: _removed, ...rest } = keys;
|
|
5314
|
+
return rest;
|
|
5315
|
+
}
|
|
5316
|
+
async function changeDictationKey(store, provider, raw) {
|
|
5317
|
+
let key2 = null;
|
|
5318
|
+
if (raw !== null) {
|
|
5319
|
+
const checked = checkKey(raw);
|
|
5320
|
+
if (!checked.ok) return checked;
|
|
5321
|
+
key2 = checked.key;
|
|
5322
|
+
}
|
|
5323
|
+
const result = await store.update((keys) => key2 === null ? withoutKey(keys, provider) : withKey(keys, provider, key2));
|
|
5324
|
+
return "problem" in result ? { ok: false, error: result.problem } : { ok: true };
|
|
5325
|
+
}
|
|
5326
|
+
|
|
5327
|
+
// src/infrastructure/file-dictation-keys-store.ts
|
|
5328
|
+
import { readFile as readFile6, stat as stat3 } from "node:fs/promises";
|
|
5329
|
+
|
|
5330
|
+
// src/application/private-files.ts
|
|
5331
|
+
function exposedFiles(files) {
|
|
5332
|
+
return files.filter(({ mode }) => (mode & 63) !== 0).map(({ path, mode }) => `${path} is readable or writable by other users (mode ${(mode & 511).toString(8)}): run \`chmod 600 ${path}\``);
|
|
5333
|
+
}
|
|
5334
|
+
|
|
5335
|
+
// src/infrastructure/file-dictation-keys-store.ts
|
|
5336
|
+
var STALE_LOCK_MS = 1e4;
|
|
5337
|
+
var FileDictationKeysStore = class {
|
|
5338
|
+
constructor(path, report = () => {
|
|
5339
|
+
}) {
|
|
5340
|
+
this.path = path;
|
|
5341
|
+
this.report = report;
|
|
5342
|
+
}
|
|
5343
|
+
path;
|
|
5344
|
+
report;
|
|
5345
|
+
async load() {
|
|
5346
|
+
let mode;
|
|
5347
|
+
try {
|
|
5348
|
+
mode = (await stat3(this.path)).mode;
|
|
5349
|
+
} catch (err) {
|
|
5350
|
+
if (err.code === "ENOENT") return { keys: {} };
|
|
5351
|
+
return { problem: `${this.path} cannot be read (${err.code ?? "error"})` };
|
|
5352
|
+
}
|
|
5353
|
+
const exposed = exposedFiles([{ path: this.path, mode }]);
|
|
5354
|
+
if (exposed.length > 0) return { problem: exposed[0] };
|
|
5355
|
+
let raw;
|
|
5356
|
+
try {
|
|
5357
|
+
raw = await readFile6(this.path, "utf8");
|
|
5358
|
+
} catch (err) {
|
|
5359
|
+
return { problem: `${this.path} cannot be read (${err.code ?? "error"})` };
|
|
5360
|
+
}
|
|
5361
|
+
const keys = parseDictationKeys(raw);
|
|
5362
|
+
return keys === "malformed" ? { problem: `${this.path} is malformed` } : { keys };
|
|
5363
|
+
}
|
|
5364
|
+
async read() {
|
|
5365
|
+
const loaded = await this.load();
|
|
5366
|
+
if ("problem" in loaded) {
|
|
5367
|
+
this.report(loaded.problem);
|
|
5368
|
+
return {};
|
|
5369
|
+
}
|
|
5370
|
+
return loaded.keys;
|
|
5371
|
+
}
|
|
5372
|
+
async update(change) {
|
|
5373
|
+
return withFileLock(this.path, STALE_LOCK_MS, async () => {
|
|
5374
|
+
const loaded = await this.load();
|
|
5375
|
+
if ("problem" in loaded) return { problem: loaded.problem };
|
|
5376
|
+
await atomicWrite(this.path, `${JSON.stringify(change(loaded.keys), null, 2)}
|
|
5377
|
+
`, 384);
|
|
5378
|
+
return { ok: true };
|
|
5379
|
+
});
|
|
5380
|
+
}
|
|
5381
|
+
};
|
|
5382
|
+
|
|
5085
5383
|
// src/application/pairing.ts
|
|
5086
|
-
var PAIRING_TTL_MS =
|
|
5384
|
+
var PAIRING_TTL_MS = 20 * 6e4;
|
|
5087
5385
|
var PAIRING_RETAIN_MS = 24 * 60 * 6e4;
|
|
5088
5386
|
var retained = (list, now) => list.filter((p) => Date.parse(p.expiresAt) + PAIRING_RETAIN_MS > now.getTime());
|
|
5089
5387
|
function stateOf(p, now) {
|
|
@@ -5105,8 +5403,32 @@ function redeemPairing(list, secret, now, device2) {
|
|
|
5105
5403
|
return { list: kept.map((p) => p === match ? spent : p), outcome: "redeemed" };
|
|
5106
5404
|
}
|
|
5107
5405
|
|
|
5406
|
+
// src/application/pairing-link.ts
|
|
5407
|
+
var GENERIC_MACHINE_IDS = /* @__PURE__ */ new Set(["", "local", "localhost"]);
|
|
5408
|
+
function pairingName(machineId, hostname) {
|
|
5409
|
+
return GENERIC_MACHINE_IDS.has(machineId) ? hostname.split(".")[0] : machineId;
|
|
5410
|
+
}
|
|
5411
|
+
function announcedAddresses(phoneAddresses2) {
|
|
5412
|
+
const valid = phoneAddresses2.length > 0 && phoneAddresses2.length <= MAX_ADDRESSES && phoneAddresses2.every(isMachineUrl);
|
|
5413
|
+
return valid ? phoneAddresses2 : void 0;
|
|
5414
|
+
}
|
|
5415
|
+
async function mintPairing(deps, input) {
|
|
5416
|
+
if (deps.defaultPath !== void 0 && !isPairingPath(deps.defaultPath)) {
|
|
5417
|
+
throw new Error(`TARFA_DEFAULT_PATH must be an absolute path of at most 128 characters (got ${deps.defaultPath})`);
|
|
5418
|
+
}
|
|
5419
|
+
const secret = deps.newSecret();
|
|
5420
|
+
await deps.pairings.update((list) => addPairing(list, secret, deps.now(), deps.ttlMs));
|
|
5421
|
+
return {
|
|
5422
|
+
urls: input.phoneUrls,
|
|
5423
|
+
agentKey: deps.keys.x25519.publicKey,
|
|
5424
|
+
secret,
|
|
5425
|
+
name: deps.machineId,
|
|
5426
|
+
...deps.defaultPath !== void 0 ? { defaultPath: deps.defaultPath } : {}
|
|
5427
|
+
};
|
|
5428
|
+
}
|
|
5429
|
+
|
|
5108
5430
|
// src/infrastructure/file-pairing-store.ts
|
|
5109
|
-
import { readFile as
|
|
5431
|
+
import { readFile as readFile7 } from "node:fs/promises";
|
|
5110
5432
|
import { z as z3 } from "zod";
|
|
5111
5433
|
var schema = z3.array(z3.object({ secret: z3.string().regex(/^[A-Za-z0-9_-]{43}$/), expiresAt: z3.string(), usedAt: z3.string().optional(), usedBy: z3.string().optional() }));
|
|
5112
5434
|
var FilePairingStore = class {
|
|
@@ -5118,7 +5440,7 @@ var FilePairingStore = class {
|
|
|
5118
5440
|
staleLockMs;
|
|
5119
5441
|
async read() {
|
|
5120
5442
|
try {
|
|
5121
|
-
const parsed = schema.safeParse(JSON.parse(await
|
|
5443
|
+
const parsed = schema.safeParse(JSON.parse(await readFile7(this.filePath, "utf8")));
|
|
5122
5444
|
return parsed.success ? parsed.data : [];
|
|
5123
5445
|
} catch {
|
|
5124
5446
|
return [];
|
|
@@ -5138,8 +5460,8 @@ var FilePairingStore = class {
|
|
|
5138
5460
|
// src/infrastructure/agent-env-file.ts
|
|
5139
5461
|
import { execFile as execFile3 } from "node:child_process";
|
|
5140
5462
|
import { existsSync as existsSync3 } from "node:fs";
|
|
5141
|
-
import { chmod, copyFile, mkdir as
|
|
5142
|
-
import { dirname as
|
|
5463
|
+
import { chmod as chmod2, copyFile, mkdir as mkdir7, readFile as readFile8, writeFile as writeFile5 } from "node:fs/promises";
|
|
5464
|
+
import { dirname as dirname8, resolve as resolve5 } from "node:path";
|
|
5143
5465
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
5144
5466
|
import { promisify as promisify3 } from "node:util";
|
|
5145
5467
|
|
|
@@ -5159,10 +5481,10 @@ function withoutGitVars(env) {
|
|
|
5159
5481
|
}
|
|
5160
5482
|
async function mainCheckout(kind2 = installKind()) {
|
|
5161
5483
|
if (kind2 === "package") return null;
|
|
5162
|
-
const here =
|
|
5484
|
+
const here = dirname8(fileURLToPath2(import.meta.url));
|
|
5163
5485
|
try {
|
|
5164
5486
|
const { stdout } = await exec3("git", ["-C", here, "rev-parse", "--path-format=absolute", "--git-common-dir"], { env: withoutGitVars(process.env) });
|
|
5165
|
-
return
|
|
5487
|
+
return dirname8(stdout.trim());
|
|
5166
5488
|
} catch {
|
|
5167
5489
|
return resolve5(here, "..", "..", "..", "..");
|
|
5168
5490
|
}
|
|
@@ -5175,7 +5497,7 @@ function agentEnvFile(path, now = () => /* @__PURE__ */ new Date()) {
|
|
|
5175
5497
|
path,
|
|
5176
5498
|
read: async () => {
|
|
5177
5499
|
try {
|
|
5178
|
-
return await
|
|
5500
|
+
return await readFile8(path, "utf8");
|
|
5179
5501
|
} catch (err) {
|
|
5180
5502
|
if (err.code === "ENOENT") {
|
|
5181
5503
|
throw new Error(`no agent config at ${path}: set the agent up first (docs/DEPLOY.md), or set TARFA_AGENT_ENV`);
|
|
@@ -5187,14 +5509,14 @@ function agentEnvFile(path, now = () => /* @__PURE__ */ new Date()) {
|
|
|
5187
5509
|
let backup = `${path}.bak-${now().toISOString().replace(/[:.]/g, "-")}`;
|
|
5188
5510
|
try {
|
|
5189
5511
|
await copyFile(path, backup);
|
|
5190
|
-
await
|
|
5512
|
+
await chmod2(backup, 384);
|
|
5191
5513
|
} catch (err) {
|
|
5192
5514
|
if (err.code !== "ENOENT") throw err;
|
|
5193
5515
|
backup = null;
|
|
5194
5516
|
}
|
|
5195
|
-
await
|
|
5196
|
-
await
|
|
5197
|
-
await
|
|
5517
|
+
await mkdir7(dirname8(path), { recursive: true, mode: 448 });
|
|
5518
|
+
await writeFile5(path, text, { mode: 384 });
|
|
5519
|
+
await chmod2(path, 384);
|
|
5198
5520
|
return backup;
|
|
5199
5521
|
}
|
|
5200
5522
|
};
|
|
@@ -5202,7 +5524,7 @@ function agentEnvFile(path, now = () => /* @__PURE__ */ new Date()) {
|
|
|
5202
5524
|
|
|
5203
5525
|
// src/infrastructure/file-status-store.ts
|
|
5204
5526
|
import { readFileSync as readFileSync2, writeFileSync, mkdirSync } from "node:fs";
|
|
5205
|
-
import { dirname as
|
|
5527
|
+
import { dirname as dirname9 } from "node:path";
|
|
5206
5528
|
|
|
5207
5529
|
// ../contracts/src/domain/workspace-status.ts
|
|
5208
5530
|
var WORKSPACE_STATUSES = [
|
|
@@ -5387,6 +5709,7 @@ var machineStatsSchema = z6.object({
|
|
|
5387
5709
|
oomKills: z6.number().optional()
|
|
5388
5710
|
}).optional()
|
|
5389
5711
|
});
|
|
5712
|
+
var dictationProviderSchema = z6.enum(["elevenlabs", "openrouter"]);
|
|
5390
5713
|
var clientMessageSchema = z6.discriminatedUnion("type", [
|
|
5391
5714
|
// `token` carries a `tarfa pair` code for a new phone, or nothing (an enrolled key needs none).
|
|
5392
5715
|
z6.object({ type: z6.literal("auth"), token: z6.string() }),
|
|
@@ -5403,6 +5726,13 @@ var clientMessageSchema = z6.discriminatedUnion("type", [
|
|
|
5403
5726
|
z6.object({ type: z6.literal("listLaunchers") }),
|
|
5404
5727
|
z6.object({ type: z6.literal("forgetLauncher"), command: z6.string() }),
|
|
5405
5728
|
z6.object({ type: z6.literal("listKeyterms") }),
|
|
5729
|
+
// Asked only of an agent whose authResult lists AGENT_FEATURES.dictationKeys.
|
|
5730
|
+
z6.object({ type: z6.literal("getDictationKeys") }),
|
|
5731
|
+
// Store (`key`) or remove (`null`) one of the machine's dictation keys, as `tarfa dictation-key`
|
|
5732
|
+
// does. Sent only to an agent whose authResult lists AGENT_FEATURES.setDictationKey; the key is
|
|
5733
|
+
// checked by the agent, which answers `dictationKeySet` with the same `id`, so a late answer to
|
|
5734
|
+
// an earlier save is never taken for this one's.
|
|
5735
|
+
z6.object({ type: z6.literal("setDictationKey"), id: z6.string(), provider: dictationProviderSchema, key: z6.string().nullable() }),
|
|
5406
5736
|
z6.object({ type: z6.literal("readFile"), path: z6.string() }),
|
|
5407
5737
|
z6.object({ type: z6.literal("attach"), workspaceId: z6.string(), cols: dim, rows: dim }),
|
|
5408
5738
|
z6.object({ type: z6.literal("detach"), workspaceId: z6.string() }),
|
|
@@ -5439,7 +5769,12 @@ var clientMessageSchema = z6.discriminatedUnion("type", [
|
|
|
5439
5769
|
]);
|
|
5440
5770
|
var detachReasonSchema = z6.enum(["opened-elsewhere", "closed", "error"]);
|
|
5441
5771
|
var PROTOCOL_VERSION = 1;
|
|
5442
|
-
var AGENT_FEATURES = {
|
|
5772
|
+
var AGENT_FEATURES = {
|
|
5773
|
+
lastReply: "lastReply",
|
|
5774
|
+
dictationKeys: "dictationKeys",
|
|
5775
|
+
setDictationKey: "setDictationKey",
|
|
5776
|
+
addresses: "addresses"
|
|
5777
|
+
};
|
|
5443
5778
|
var agentMessageSchema = z6.discriminatedUnion("type", [
|
|
5444
5779
|
// `credential` is what authenticated the session (`token` only from an agent from before token
|
|
5445
5780
|
// enrolment was removed); `enrolled` says the device key is enrolled (written), so the phone
|
|
@@ -5454,7 +5789,14 @@ var agentMessageSchema = z6.discriminatedUnion("type", [
|
|
|
5454
5789
|
pushed: z6.literal(true).optional().catch(void 0),
|
|
5455
5790
|
// `features` names what this agent answers beyond the protocol, so the PWA offers a control
|
|
5456
5791
|
// only to an agent that supports it (a worktree agent shares a release's version number).
|
|
5457
|
-
|
|
5792
|
+
// `addresses`: where phones reach this agent now (its TARFA_PHONE_ADDRESSES), which a phone
|
|
5793
|
+
// adopts only once it has connected through each one.
|
|
5794
|
+
agent: z6.object({
|
|
5795
|
+
protocol: z6.number().int(),
|
|
5796
|
+
version: z6.string().optional(),
|
|
5797
|
+
features: z6.array(z6.string()).optional().catch(void 0),
|
|
5798
|
+
addresses: z6.array(z6.string()).optional().catch(void 0)
|
|
5799
|
+
}).optional().catch(void 0)
|
|
5458
5800
|
}),
|
|
5459
5801
|
// `claudeVoice`: whether this machine can play a Claude voice dictation (see voiceStart).
|
|
5460
5802
|
// An older agent omits it, and the PWA then treats Claude voice as unavailable there.
|
|
@@ -5473,6 +5815,16 @@ var agentMessageSchema = z6.discriminatedUnion("type", [
|
|
|
5473
5815
|
// keyterms so jargon transcribes correctly. Additive like `roots`: older agents omit it,
|
|
5474
5816
|
// so it defaults to [] instead of letting the strict client parser drop the whole message.
|
|
5475
5817
|
z6.object({ type: z6.literal("keyterms"), keyterms: z6.array(z6.string()).default([]) }),
|
|
5818
|
+
// The dictation provider keys the agent's operator stored (~/.tarfa/dictation-keys.json), sent
|
|
5819
|
+
// only in reply to getDictationKeys. A key the agent does not hold is absent.
|
|
5820
|
+
z6.object({
|
|
5821
|
+
type: z6.literal("dictationKeys"),
|
|
5822
|
+
elevenlabs: z6.string().optional().catch(void 0),
|
|
5823
|
+
openrouter: z6.string().optional().catch(void 0)
|
|
5824
|
+
}),
|
|
5825
|
+
// The answer to setDictationKey. `error` is the agent's fixed wording of a refusal and never
|
|
5826
|
+
// holds the key.
|
|
5827
|
+
z6.object({ type: z6.literal("dictationKeySet"), id: z6.string(), provider: dictationProviderSchema, ok: z6.boolean(), error: z6.string().optional() }),
|
|
5476
5828
|
// `files` is additive: older agents that predate it omit the field, so it defaults to []
|
|
5477
5829
|
// (a strictly-required field would make the strict client parser silently drop their
|
|
5478
5830
|
// dirEntries during a version-skewed rollout, hanging the browser on "Loading…").
|
|
@@ -5696,7 +6048,7 @@ var FileStatusStore = class {
|
|
|
5696
6048
|
}
|
|
5697
6049
|
}
|
|
5698
6050
|
persist() {
|
|
5699
|
-
mkdirSync(
|
|
6051
|
+
mkdirSync(dirname9(this.filePath), { recursive: true });
|
|
5700
6052
|
writeFileSync(this.filePath, JSON.stringify(Object.fromEntries(this.statuses), null, 2), "utf8");
|
|
5701
6053
|
}
|
|
5702
6054
|
};
|
|
@@ -5725,8 +6077,8 @@ function parseEntry(value) {
|
|
|
5725
6077
|
}
|
|
5726
6078
|
|
|
5727
6079
|
// src/infrastructure/file-parked-store.ts
|
|
5728
|
-
import { readFile as
|
|
5729
|
-
import { dirname as
|
|
6080
|
+
import { readFile as readFile9, writeFile as writeFile6, mkdir as mkdir8 } from "node:fs/promises";
|
|
6081
|
+
import { dirname as dirname10 } from "node:path";
|
|
5730
6082
|
var FileParkedStore = class {
|
|
5731
6083
|
constructor(filePath) {
|
|
5732
6084
|
this.filePath = filePath;
|
|
@@ -5735,7 +6087,7 @@ var FileParkedStore = class {
|
|
|
5735
6087
|
async list() {
|
|
5736
6088
|
let raw;
|
|
5737
6089
|
try {
|
|
5738
|
-
raw = await
|
|
6090
|
+
raw = await readFile9(this.filePath, "utf8");
|
|
5739
6091
|
} catch {
|
|
5740
6092
|
return [];
|
|
5741
6093
|
}
|
|
@@ -5760,8 +6112,8 @@ var FileParkedStore = class {
|
|
|
5760
6112
|
await this.persist(rest);
|
|
5761
6113
|
}
|
|
5762
6114
|
async persist(entries) {
|
|
5763
|
-
await
|
|
5764
|
-
await
|
|
6115
|
+
await mkdir8(dirname10(this.filePath), { recursive: true });
|
|
6116
|
+
await writeFile6(this.filePath, JSON.stringify(entries, null, 2), "utf8");
|
|
5765
6117
|
}
|
|
5766
6118
|
};
|
|
5767
6119
|
function parseEntry2(value) {
|
|
@@ -5795,10 +6147,10 @@ function parseEntry2(value) {
|
|
|
5795
6147
|
}
|
|
5796
6148
|
|
|
5797
6149
|
// src/infrastructure/fs-file-writer.ts
|
|
5798
|
-
import { mkdir as
|
|
6150
|
+
import { mkdir as mkdir9, writeFile as writeFile7, access } from "node:fs/promises";
|
|
5799
6151
|
var FsFileWriter = class {
|
|
5800
6152
|
async ensureDir(dir) {
|
|
5801
|
-
await
|
|
6153
|
+
await mkdir9(dir, { recursive: true });
|
|
5802
6154
|
}
|
|
5803
6155
|
async exists(path) {
|
|
5804
6156
|
try {
|
|
@@ -5809,7 +6161,7 @@ var FsFileWriter = class {
|
|
|
5809
6161
|
}
|
|
5810
6162
|
}
|
|
5811
6163
|
async writeFile(path, bytes) {
|
|
5812
|
-
await
|
|
6164
|
+
await writeFile7(path, bytes);
|
|
5813
6165
|
}
|
|
5814
6166
|
};
|
|
5815
6167
|
|
|
@@ -5878,12 +6230,12 @@ function loginShellResolver(shell = process.env.SHELL || "/bin/sh") {
|
|
|
5878
6230
|
}
|
|
5879
6231
|
|
|
5880
6232
|
// src/infrastructure/codex-app-server.ts
|
|
5881
|
-
import {
|
|
6233
|
+
import { realpath as realpath3 } from "node:fs/promises";
|
|
5882
6234
|
import { join as join7 } from "node:path";
|
|
5883
6235
|
import { WebSocket } from "ws";
|
|
5884
6236
|
var CodexAppServerDown = class extends Error {
|
|
5885
|
-
constructor() {
|
|
5886
|
-
super(
|
|
6237
|
+
constructor(socketPath) {
|
|
6238
|
+
super(`the Codex app server is not running (no socket at ${socketPath})`);
|
|
5887
6239
|
}
|
|
5888
6240
|
};
|
|
5889
6241
|
var CodexRpcError = class extends Error {
|
|
@@ -5954,8 +6306,8 @@ var Connection = class {
|
|
|
5954
6306
|
var CodexAppServer = class {
|
|
5955
6307
|
socketPath;
|
|
5956
6308
|
timeoutMs;
|
|
5957
|
-
constructor(options
|
|
5958
|
-
this.socketPath = options.socketPath
|
|
6309
|
+
constructor(options) {
|
|
6310
|
+
this.socketPath = "socketPath" in options ? options.socketPath : join7(options.codexHome, "app-server-control", "app-server-control.sock");
|
|
5959
6311
|
this.timeoutMs = options.timeoutMs ?? 5e3;
|
|
5960
6312
|
}
|
|
5961
6313
|
loadedThreads() {
|
|
@@ -6009,9 +6361,15 @@ var CodexAppServer = class {
|
|
|
6009
6361
|
return thread.id;
|
|
6010
6362
|
});
|
|
6011
6363
|
}
|
|
6012
|
-
|
|
6364
|
+
// Codex 0.167 binds the socket under /tmp/codex-daemon-<uid>/ and publishes this path as a link
|
|
6365
|
+
// to it. Connecting to the link's target keeps the path within the 107 bytes a unix socket
|
|
6366
|
+
// address holds, however deep CODEX_HOME is; a link to nothing means no daemon.
|
|
6367
|
+
async call(run) {
|
|
6368
|
+
const target = await realpath3(this.socketPath).catch((err) => {
|
|
6369
|
+
throw err.code === "ENOENT" ? new CodexAppServerDown(this.socketPath) : err;
|
|
6370
|
+
});
|
|
6013
6371
|
return new Promise((resolve6, reject) => {
|
|
6014
|
-
const ws = new WebSocket(`ws+unix://${
|
|
6372
|
+
const ws = new WebSocket(`ws+unix://${target}:/`, { perMessageDeflate: false });
|
|
6015
6373
|
const c = new Connection(ws);
|
|
6016
6374
|
let settled = false;
|
|
6017
6375
|
const settle = (fn) => {
|
|
@@ -6030,7 +6388,7 @@ var CodexAppServer = class {
|
|
|
6030
6388
|
}, this.timeoutMs);
|
|
6031
6389
|
ws.once("error", (err) => {
|
|
6032
6390
|
const down = err.code === "ENOENT" || err.code === "ECONNREFUSED";
|
|
6033
|
-
settle(() => reject(down ? new CodexAppServerDown() : err));
|
|
6391
|
+
settle(() => reject(down ? new CodexAppServerDown(this.socketPath) : err));
|
|
6034
6392
|
});
|
|
6035
6393
|
ws.once("close", () => settle(() => reject(new Error("the Codex app server closed the connection"))));
|
|
6036
6394
|
ws.once("open", () => {
|
|
@@ -6046,6 +6404,45 @@ var CodexAppServer = class {
|
|
|
6046
6404
|
}
|
|
6047
6405
|
};
|
|
6048
6406
|
|
|
6407
|
+
// src/infrastructure/codex-versions.ts
|
|
6408
|
+
import { execFile as execFile5 } from "node:child_process";
|
|
6409
|
+
function loginShell(shell = process.env.SHELL || "/bin/sh", extraEnv = {}) {
|
|
6410
|
+
return (script) => new Promise((resolve6, reject) => {
|
|
6411
|
+
execFile5(shell, ["-lc", script], { env: { ...childEnv(), ...extraEnv }, timeout: 15e3 }, (err, stdout) => err ? reject(err) : resolve6(stdout));
|
|
6412
|
+
});
|
|
6413
|
+
}
|
|
6414
|
+
function codexVersions(run = loginShell()) {
|
|
6415
|
+
return async () => {
|
|
6416
|
+
const [cliOut, daemonOut] = await Promise.all([
|
|
6417
|
+
run("codex --version").catch((err) => {
|
|
6418
|
+
throw new Error(`cannot run codex --version: ${err.message}`);
|
|
6419
|
+
}),
|
|
6420
|
+
run("codex app-server daemon version").catch((err) => {
|
|
6421
|
+
throw new Error(`cannot read the Codex app server's version: ${err.message}`);
|
|
6422
|
+
})
|
|
6423
|
+
]);
|
|
6424
|
+
const cli = cliOut.trim().split(/\s+/).pop() ?? "";
|
|
6425
|
+
let appServer;
|
|
6426
|
+
try {
|
|
6427
|
+
appServer = JSON.parse(daemonOut).appServerVersion;
|
|
6428
|
+
} catch {
|
|
6429
|
+
appServer = void 0;
|
|
6430
|
+
}
|
|
6431
|
+
if (typeof appServer !== "string" || appServer === "") throw new Error("the Codex app server is not running");
|
|
6432
|
+
return { cli, appServer };
|
|
6433
|
+
};
|
|
6434
|
+
}
|
|
6435
|
+
|
|
6436
|
+
// src/infrastructure/codex-home.ts
|
|
6437
|
+
import { join as join8 } from "node:path";
|
|
6438
|
+
var MARK = "codex-home=";
|
|
6439
|
+
async function resolveCodexHome(env, home, run = loginShell()) {
|
|
6440
|
+
if (env.CODEX_HOME) return env.CODEX_HOME;
|
|
6441
|
+
const out = await run(`printf '\\n${MARK}%s\\n' "\${CODEX_HOME-}"`).catch(() => "");
|
|
6442
|
+
const value = out.split("\n").reverse().find((line) => line.startsWith(MARK))?.slice(MARK.length);
|
|
6443
|
+
return value || join8(home, ".codex");
|
|
6444
|
+
}
|
|
6445
|
+
|
|
6049
6446
|
export {
|
|
6050
6447
|
packageRoot,
|
|
6051
6448
|
packageVersion,
|
|
@@ -6065,11 +6462,10 @@ export {
|
|
|
6065
6462
|
parseDeviceHello,
|
|
6066
6463
|
deviceFingerprint,
|
|
6067
6464
|
MAX_ADDRESSES,
|
|
6068
|
-
isPairingPath,
|
|
6069
6465
|
generatePairingSecret,
|
|
6070
6466
|
pairingLink,
|
|
6071
|
-
parsePairingLink,
|
|
6072
6467
|
isMachineUrl,
|
|
6468
|
+
pairingCode,
|
|
6073
6469
|
REFUSAL,
|
|
6074
6470
|
IN_BAND_REFUSAL,
|
|
6075
6471
|
DEVICE_REFUSED,
|
|
@@ -6112,6 +6508,7 @@ export {
|
|
|
6112
6508
|
getRecentPaths,
|
|
6113
6509
|
pathWithinRoots,
|
|
6114
6510
|
browseDir,
|
|
6511
|
+
atomicWrite,
|
|
6115
6512
|
loadOrCreateAgentKeys,
|
|
6116
6513
|
enrolmentMode,
|
|
6117
6514
|
withEnrolmentMode,
|
|
@@ -6120,9 +6517,16 @@ export {
|
|
|
6120
6517
|
revokeDevice,
|
|
6121
6518
|
forgetDevice,
|
|
6122
6519
|
FileDeviceStore,
|
|
6520
|
+
DICTATION_PROVIDERS,
|
|
6521
|
+
providerOf,
|
|
6522
|
+
changeDictationKey,
|
|
6523
|
+
exposedFiles,
|
|
6524
|
+
FileDictationKeysStore,
|
|
6123
6525
|
PAIRING_TTL_MS,
|
|
6124
|
-
addPairing,
|
|
6125
6526
|
redeemPairing,
|
|
6527
|
+
pairingName,
|
|
6528
|
+
announcedAddresses,
|
|
6529
|
+
mintPairing,
|
|
6126
6530
|
FilePairingStore,
|
|
6127
6531
|
mainCheckout,
|
|
6128
6532
|
locateAgentEnv,
|
|
@@ -6149,5 +6553,8 @@ export {
|
|
|
6149
6553
|
detectInstalled,
|
|
6150
6554
|
loginShellResolver,
|
|
6151
6555
|
CodexAppServerDown,
|
|
6152
|
-
CodexAppServer
|
|
6556
|
+
CodexAppServer,
|
|
6557
|
+
loginShell,
|
|
6558
|
+
codexVersions,
|
|
6559
|
+
resolveCodexHome
|
|
6153
6560
|
};
|