@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.
@@ -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(flags, state) {
3853
+ function planSetup(given, state) {
3744
3854
  if (state.phonePortOwner === "other") {
3745
3855
  return {
3746
- error: `127.0.0.1:${flags.phonePort} is taken by another process (another user's agent?): pick a free one with --phone-port, e.g. ${flags.phonePort + 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") ?? "none";
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/agent-key-file.ts
4941
- import { mkdir as mkdir4, readFile as readFile4, writeFile as writeFile3 } from "node:fs/promises";
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 mkdir4(dirname5(path), { recursive: true, mode: 448 });
5144
+ await mkdir5(dirname6(path), { recursive: true, mode: 448 });
4951
5145
  try {
4952
- await writeFile3(path, `${JSON.stringify(keys, null, 2)}
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 mkdir5, readFile as readFile5 } from "node:fs/promises";
5024
- import { dirname as dirname6 } from "node:path";
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 mkdir5(dirname6(this.filePath), { recursive: true, mode: 448 });
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 = 10 * 6e4;
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 readFile6 } from "node:fs/promises";
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 readFile6(this.filePath, "utf8")));
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 mkdir6, readFile as readFile7, writeFile as writeFile4 } from "node:fs/promises";
5142
- import { dirname as dirname7, resolve as resolve5 } from "node:path";
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 = dirname7(fileURLToPath2(import.meta.url));
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 dirname7(stdout.trim());
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 readFile7(path, "utf8");
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 chmod(backup, 384);
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 mkdir6(dirname7(path), { recursive: true, mode: 448 });
5196
- await writeFile4(path, text, { mode: 384 });
5197
- await chmod(path, 384);
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 dirname8 } from "node:path";
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 = { lastReply: "lastReply" };
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
- agent: z6.object({ protocol: z6.number().int(), version: z6.string().optional(), features: z6.array(z6.string()).optional().catch(void 0) }).optional().catch(void 0)
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(dirname8(this.filePath), { recursive: true });
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 readFile8, writeFile as writeFile5, mkdir as mkdir7 } from "node:fs/promises";
5729
- import { dirname as dirname9 } from "node:path";
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 readFile8(this.filePath, "utf8");
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 mkdir7(dirname9(this.filePath), { recursive: true });
5764
- await writeFile5(this.filePath, JSON.stringify(entries, null, 2), "utf8");
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 mkdir8, writeFile as writeFile6, access } from "node:fs/promises";
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 mkdir8(dir, { recursive: true });
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 writeFile6(path, bytes);
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 { homedir } from "node:os";
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("the Codex app server is not running");
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 ?? join7(homedir(), ".codex", "app-server-control", "app-server-control.sock");
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
- call(run) {
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://${this.socketPath}:/`, { perMessageDeflate: false });
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
  };