agent-workbench 0.3.0 → 0.4.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/dist/server.js CHANGED
@@ -3,7 +3,7 @@
3
3
  // packages/server/src/index.ts
4
4
  import { existsSync as existsSync4 } from "node:fs";
5
5
  import { createServer } from "node:http";
6
- import path46 from "node:path";
6
+ import path49 from "node:path";
7
7
  import { fileURLToPath as fileURLToPath2 } from "node:url";
8
8
  import express from "express";
9
9
 
@@ -45,6 +45,15 @@ function asArrayOf(value, parseItem) {
45
45
  }
46
46
  return result;
47
47
  }
48
+ function asArrayFiltered(value, parseItem) {
49
+ if (!Array.isArray(value)) return null;
50
+ const result = [];
51
+ for (const item of value) {
52
+ const parsed = parseItem(item);
53
+ if (parsed !== null) result.push(parsed);
54
+ }
55
+ return result;
56
+ }
48
57
  function asLiteral(value, allowed) {
49
58
  return typeof value === "string" && allowed.includes(value) ? value : null;
50
59
  }
@@ -354,6 +363,41 @@ function modelFamilyOf(value, options = MODEL_OPTIONS) {
354
363
  return candidates.find((option) => value.includes(option.family))?.family ?? null;
355
364
  }
356
365
 
366
+ // packages/shared/src/composer-draft.ts
367
+ var MAX_COMPOSER_DRAFT_CHARS = 1e6;
368
+ var MAX_COMPOSER_DRAFT_PASTES = 100;
369
+ var MAX_PASTE_NUMBER = 999999;
370
+ function composerDraftChars(draft) {
371
+ return draft.pasted.reduce((total, paste) => total + paste.text.length, draft.text.length);
372
+ }
373
+ function isEmptyComposerDraft(draft) {
374
+ return draft.text.trim().length === 0 && draft.pasted.every((paste) => paste.text.trim().length === 0);
375
+ }
376
+ function sameComposerDraft(a, b) {
377
+ if (a.text !== b.text || a.pasted.length !== b.pasted.length) return false;
378
+ return a.pasted.every((paste, index) => {
379
+ const other = b.pasted[index];
380
+ return other !== void 0 && other.number === paste.number && other.text === paste.text;
381
+ });
382
+ }
383
+ function parseComposerDraftPaste(value) {
384
+ const record = asRecord(value);
385
+ if (record === null) return null;
386
+ const number = record["number"];
387
+ const text = asString(record["text"]);
388
+ if (typeof number !== "number" || !Number.isInteger(number) || number < 1 || number > MAX_PASTE_NUMBER) return null;
389
+ return text === null ? null : { number, text };
390
+ }
391
+ function parseComposerDraft(value) {
392
+ const record = asRecord(value);
393
+ if (record === null) return null;
394
+ const text = asString(record["text"]);
395
+ const pasted2 = asArrayOf(record["pasted"], parseComposerDraftPaste);
396
+ if (text === null || pasted2 === null || pasted2.length > MAX_COMPOSER_DRAFT_PASTES) return null;
397
+ if (new Set(pasted2.map((paste) => paste.number)).size !== pasted2.length) return null;
398
+ return { text, pasted: pasted2 };
399
+ }
400
+
357
401
  // packages/shared/src/memory.ts
358
402
  var MEMORY_DIR = ".agents/memory";
359
403
  var MEMORY_INDEX_FILE = "MEMORY.md";
@@ -620,6 +664,21 @@ function parseVaultBodyLine(value) {
620
664
  return null;
621
665
  }
622
666
 
667
+ // packages/shared/src/remote.ts
668
+ var REMOTE_DEFAULT_PORT = 24837;
669
+ var REMOTE_MIN_PORT = 1024;
670
+ var REMOTE_MAX_PORT = 65535;
671
+ var PAIR_QUERY_PARAM = "pair";
672
+ var PAIRING_TTL_MS = 5 * 6e4;
673
+ var REMOTE_DEVICE_LABEL_MAX_CHARS = 60;
674
+ var REMOTE_REVOKED_CLOSE_CODE = 4001;
675
+ function isRemotePort(value) {
676
+ return typeof value === "number" && Number.isInteger(value) && value >= REMOTE_MIN_PORT && value <= REMOTE_MAX_PORT;
677
+ }
678
+ function cleanDeviceLabel(label) {
679
+ return label.replace(/\s+/g, " ").trim().slice(0, REMOTE_DEVICE_LABEL_MAX_CHARS);
680
+ }
681
+
623
682
  // packages/shared/src/protocol.ts
624
683
  var PROTOCOL_VERSION = 8;
625
684
  var WS_PATH = "/ws";
@@ -971,6 +1030,43 @@ function parseClientMessage(raw) {
971
1030
  const searchId = asNonEmptyString(record["searchId"]);
972
1031
  return searchId === null ? null : { type: "search.cancel", searchId };
973
1032
  }
1033
+ case "remote.configure": {
1034
+ const rawEnabled = record["enabled"];
1035
+ const rawPort = record["port"];
1036
+ const enabled2 = rawEnabled === void 0 ? void 0 : asBoolean(rawEnabled);
1037
+ const port = rawPort === void 0 ? void 0 : asFiniteNumber(rawPort);
1038
+ if (enabled2 === null || port === null || enabled2 === void 0 && port === void 0) return null;
1039
+ return {
1040
+ type: "remote.configure",
1041
+ ...enabled2 !== void 0 ? { enabled: enabled2 } : {},
1042
+ ...port !== void 0 ? { port } : {}
1043
+ };
1044
+ }
1045
+ case "remote.pairing.start":
1046
+ return { type: "remote.pairing.start" };
1047
+ case "remote.pairing.cancel":
1048
+ return { type: "remote.pairing.cancel" };
1049
+ case "remote.device.rename": {
1050
+ const deviceId = asNonEmptyString(record["deviceId"]);
1051
+ const rawLabel = asString(record["label"]);
1052
+ const label = rawLabel === null ? "" : cleanDeviceLabel(rawLabel);
1053
+ return deviceId === null || label.length === 0 ? null : { type: "remote.device.rename", deviceId, label };
1054
+ }
1055
+ case "remote.device.revoke": {
1056
+ const deviceId = asNonEmptyString(record["deviceId"]);
1057
+ return deviceId === null ? null : { type: "remote.device.revoke", deviceId };
1058
+ }
1059
+ case "remote.phone.start":
1060
+ return { type: "remote.phone.start" };
1061
+ case "remote.phone.cancel":
1062
+ return { type: "remote.phone.cancel" };
1063
+ case "remote.device.forgetSelf":
1064
+ return { type: "remote.device.forgetSelf" };
1065
+ case "composer.draft": {
1066
+ const terminalId = asNonEmptyString(record["terminalId"]);
1067
+ const draft = parseComposerDraft(record["draft"]);
1068
+ return terminalId === null || draft === null ? null : { type: "composer.draft", terminalId, draft };
1069
+ }
974
1070
  default:
975
1071
  return null;
976
1072
  }
@@ -1030,6 +1126,9 @@ function pastedBoundaryPattern() {
1030
1126
  return /\[(?:Start|End) of pasted text #\d{1,6}\]/g;
1031
1127
  }
1032
1128
 
1129
+ // packages/shared/src/support.ts
1130
+ var SUPPORT_URL = "https://buymeacoffee.com/cvelasquez";
1131
+
1033
1132
  // packages/server/src/archived-sessions.ts
1034
1133
  import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
1035
1134
  import path2 from "node:path";
@@ -1068,12 +1167,18 @@ function archivedSessionsPath() {
1068
1167
  function appSettingsPath() {
1069
1168
  return path.join(appConfigDir(), "settings.json");
1070
1169
  }
1170
+ function remoteDevicesPath() {
1171
+ return path.join(appConfigDir(), "remote-devices.json");
1172
+ }
1071
1173
  function notesStatePath() {
1072
1174
  return path.join(appConfigDir(), "notes.json");
1073
1175
  }
1074
1176
  function notesImagesDir() {
1075
1177
  return path.join(appConfigDir(), "notes-images");
1076
1178
  }
1179
+ function composerDraftsPath() {
1180
+ return path.join(appConfigDir(), "composer-drafts.json");
1181
+ }
1077
1182
  function catalogTempDir() {
1078
1183
  return path.join(tmpdir(), "agent-workbench", "agy-catalog");
1079
1184
  }
@@ -10617,195 +10722,1104 @@ function openBrowser(url) {
10617
10722
  }
10618
10723
  }
10619
10724
 
10620
- // packages/server/src/security.ts
10621
- import { randomBytes as randomBytes2, timingSafeEqual } from "node:crypto";
10622
- var LOOPBACK_HOST = "127.0.0.1";
10623
- var ALLOWED_HOSTNAMES = /* @__PURE__ */ new Set(["127.0.0.1", "localhost", "[::1]", "::1"]);
10624
- var TOKEN_HEADER = "x-agent-workbench-token";
10625
- var TOKEN_COOKIE = "agent_workbench_token";
10626
- function createSessionToken() {
10627
- return randomBytes2(32).toString("base64url");
10628
- }
10629
- function constantTimeEquals(a, b) {
10630
- const bufferA = Buffer.from(a, "utf8");
10631
- const bufferB = Buffer.from(b, "utf8");
10632
- if (bufferA.length !== bufferB.length) return false;
10633
- return timingSafeEqual(bufferA, bufferB);
10725
+ // packages/server/src/pty-helper.ts
10726
+ import { chmodSync, statSync } from "node:fs";
10727
+ import { createRequire } from "node:module";
10728
+ import path29 from "node:path";
10729
+ function helperCandidates(packageDir, platform, arch) {
10730
+ return [
10731
+ path29.join(packageDir, "build", "Release", "spawn-helper"),
10732
+ path29.join(packageDir, "build", "Debug", "spawn-helper"),
10733
+ path29.join(packageDir, "prebuilds", `${platform}-${arch}`, "spawn-helper")
10734
+ ];
10634
10735
  }
10635
- function requestUrl(request) {
10736
+ function locateNodePty() {
10636
10737
  try {
10637
- return new URL(request.url ?? "/", `http://${request.headers.host ?? LOOPBACK_HOST}`);
10738
+ const require2 = createRequire(import.meta.url);
10739
+ return path29.resolve(path29.dirname(require2.resolve("node-pty")), "..");
10638
10740
  } catch {
10639
10741
  return null;
10640
10742
  }
10641
10743
  }
10642
- function readCookie(request, name) {
10643
- const header = request.headers.cookie;
10644
- if (typeof header !== "string") return null;
10645
- for (const pair of header.split(";")) {
10646
- const separator = pair.indexOf("=");
10647
- if (separator === -1) continue;
10648
- if (pair.slice(0, separator).trim() !== name) continue;
10649
- const value = pair.slice(separator + 1).trim();
10650
- return value.length > 0 ? decodeURIComponent(value) : null;
10744
+ function ensurePtyHelperExecutable(options = {}) {
10745
+ const platform = options.platform ?? process.platform;
10746
+ if (platform === "win32") return [];
10747
+ const packageDir = options.packageDir === void 0 ? locateNodePty() : options.packageDir;
10748
+ if (packageDir === null) return [];
10749
+ const warn = options.warn ?? ((message) => console.warn(message));
10750
+ const fixed = [];
10751
+ for (const file of helperCandidates(packageDir, platform, options.arch ?? process.arch)) {
10752
+ let mode;
10753
+ try {
10754
+ const stats = statSync(file);
10755
+ if (!stats.isFile()) continue;
10756
+ mode = stats.mode;
10757
+ } catch {
10758
+ continue;
10759
+ }
10760
+ if ((mode & 73) === 73) continue;
10761
+ try {
10762
+ chmodSync(file, mode & 511 | 493);
10763
+ fixed.push(file);
10764
+ } catch {
10765
+ warn(
10766
+ `[pty] ${file} is not executable and couldn't be fixed: no tab will be able to start a CLI. Run: chmod +x "${file}"`
10767
+ );
10768
+ }
10651
10769
  }
10652
- return null;
10770
+ return fixed;
10653
10771
  }
10654
- function matchToken(request, expected) {
10655
- const url = requestUrl(request);
10656
- const fromQuery = url?.searchParams.get(TOKEN_QUERY_PARAM) ?? null;
10657
- if (fromQuery !== null && constantTimeEquals(fromQuery, expected)) {
10658
- return { source: "query" };
10772
+
10773
+ // packages/server/src/remote-access-service.ts
10774
+ import { execFile as execFile3 } from "node:child_process";
10775
+ import { randomBytes as cryptoRandomBytes, randomUUID as randomUUID2 } from "node:crypto";
10776
+ import os from "node:os";
10777
+
10778
+ // packages/server/src/phone-pairing.ts
10779
+ import { createPrivateKey, createPublicKey } from "node:crypto";
10780
+ var PHONE_PAYLOAD_PREFIX = "agentworkbench://pair?";
10781
+ var PHONE_PAYLOAD_VERSION = 1;
10782
+ var PHONE_SSH_PORT = 22;
10783
+ var PHONE_KEY_COMMENT = "agent-workbench-phone";
10784
+ var PHONE_SEED_BYTES = 32;
10785
+ var ED25519_PKCS8_PREFIX = Buffer.from("302e020100300506032b657004220420", "hex");
10786
+ function ed25519PublicKey(seed) {
10787
+ if (seed.length !== PHONE_SEED_BYTES) throw new Error(`an Ed25519 seed has ${PHONE_SEED_BYTES} bytes`);
10788
+ const privateKey = createPrivateKey({ key: Buffer.concat([ED25519_PKCS8_PREFIX, seed]), format: "der", type: "pkcs8" });
10789
+ const spki = createPublicKey(privateKey).export({ format: "der", type: "spki" });
10790
+ return Buffer.from(spki.subarray(spki.length - 32));
10791
+ }
10792
+ function sshString(bytes) {
10793
+ const length = Buffer.alloc(4);
10794
+ length.writeUInt32BE(bytes.length);
10795
+ return Buffer.concat([length, bytes]);
10796
+ }
10797
+ function sshEd25519PublicKey(publicKey) {
10798
+ const blob = Buffer.concat([sshString(Buffer.from("ssh-ed25519", "ascii")), sshString(publicKey)]);
10799
+ return `ssh-ed25519 ${blob.toString("base64")}`;
10800
+ }
10801
+ function phoneAuthorizedKeysEntry(publicKey, appPort) {
10802
+ return `restrict,port-forwarding,permitopen="127.0.0.1:${appPort}",command="exit" ${sshEd25519PublicKey(publicKey)} ${PHONE_KEY_COMMENT}`;
10803
+ }
10804
+ function phoneAuthorizeCommand(entry, shell) {
10805
+ switch (shell) {
10806
+ case "powershell-admin":
10807
+ return `$f = "$env:ProgramData\\ssh\\administrators_authorized_keys"; Add-Content -Path $f -Encoding ascii -Value @('', '${entry}'); icacls.exe $f /inheritance:r /grant '*S-1-5-32-544:F' /grant '*S-1-5-18:F'`;
10808
+ case "powershell":
10809
+ return `New-Item -ItemType Directory -Force "$HOME\\.ssh" | Out-Null; Add-Content -Path "$HOME\\.ssh\\authorized_keys" -Encoding ascii -Value @('', '${entry}')`;
10810
+ case "terminal":
10811
+ return `mkdir -p ~/.ssh && chmod 700 ~/.ssh && printf '\\n%s\\n' '${entry}' >> ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys`;
10812
+ }
10813
+ }
10814
+ function authorizeShellFromGroups(whoamiGroups) {
10815
+ return /S-1-5-32-544/.test(whoamiGroups) ? "powershell-admin" : "powershell";
10816
+ }
10817
+ function phonePairingPayload(fields) {
10818
+ const params = new URLSearchParams();
10819
+ params.set("v", String(PHONE_PAYLOAD_VERSION));
10820
+ params.set("n", fields.pcName);
10821
+ params.set("h", fields.hosts.join(","));
10822
+ params.set("p", String(fields.sshPort));
10823
+ params.set("u", fields.user);
10824
+ params.set("a", String(fields.appPort));
10825
+ params.set("c", fields.code);
10826
+ params.set("k", fields.seed.toString("base64url"));
10827
+ return PHONE_PAYLOAD_PREFIX + params.toString();
10828
+ }
10829
+
10830
+ // packages/server/src/remote-access.ts
10831
+ import { createHash, timingSafeEqual } from "node:crypto";
10832
+ var PAIRING_ALPHABET = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
10833
+ var PAIRING_CODE_CHARS = 8;
10834
+ var PAIRING_MAX_FAILURES = 5;
10835
+ function formatPairingCode(raw) {
10836
+ const half = PAIRING_CODE_CHARS / 2;
10837
+ return `${raw.slice(0, half)}-${raw.slice(half)}`;
10838
+ }
10839
+ function normalizePairingCode(input) {
10840
+ const raw = input.replace(/[\s-]/g, "").toUpperCase();
10841
+ if (raw.length !== PAIRING_CODE_CHARS) return null;
10842
+ for (const char of raw) {
10843
+ if (!PAIRING_ALPHABET.includes(char)) return null;
10844
+ }
10845
+ return raw;
10846
+ }
10847
+ function sameText(a, b) {
10848
+ const bufferA = Buffer.from(a, "utf8");
10849
+ const bufferB = Buffer.from(b, "utf8");
10850
+ if (bufferA.length !== bufferB.length) return false;
10851
+ return timingSafeEqual(bufferA, bufferB);
10852
+ }
10853
+ var PairingDesk = class {
10854
+ constructor(deps) {
10855
+ this.deps = deps;
10659
10856
  }
10660
- const fromHeader = request.headers[TOKEN_HEADER];
10661
- if (typeof fromHeader === "string" && constantTimeEquals(fromHeader, expected)) {
10662
- return { source: "header" };
10857
+ deps;
10858
+ current = null;
10859
+ /** Un código nuevo. Reemplaza al que hubiera: nunca hay dos vigentes. */
10860
+ start() {
10861
+ const bytes = this.deps.randomBytes(PAIRING_CODE_CHARS);
10862
+ let raw = "";
10863
+ for (let i = 0; i < PAIRING_CODE_CHARS; i++) raw += PAIRING_ALPHABET[(bytes[i] ?? 0) % PAIRING_ALPHABET.length];
10864
+ const expiresAt = this.deps.now() + PAIRING_TTL_MS;
10865
+ this.current = { raw, expiresAt, failures: 0 };
10866
+ return { code: formatPairingCode(raw), expiresAt };
10663
10867
  }
10664
- const fromCookie = readCookie(request, TOKEN_COOKIE);
10665
- if (fromCookie !== null && constantTimeEquals(fromCookie, expected)) {
10666
- return { source: "cookie" };
10868
+ cancel() {
10869
+ this.current = null;
10667
10870
  }
10668
- return null;
10669
- }
10670
- function hasValidToken(request, expected) {
10671
- return matchToken(request, expected) !== null;
10871
+ isActive() {
10872
+ if (this.current !== null && this.deps.now() >= this.current.expiresAt) this.current = null;
10873
+ return this.current !== null;
10874
+ }
10875
+ /** Cuándo vence el vigente, o null. */
10876
+ expiresAt() {
10877
+ return this.isActive() ? this.current?.expiresAt ?? null : null;
10878
+ }
10879
+ redeem(input) {
10880
+ if (!this.isActive() || this.current === null) return "none";
10881
+ const raw = normalizePairingCode(input);
10882
+ if (raw === null) return "none";
10883
+ if (sameText(raw, this.current.raw)) {
10884
+ this.current = null;
10885
+ return "ok";
10886
+ }
10887
+ this.current.failures += 1;
10888
+ if (this.current.failures >= PAIRING_MAX_FAILURES) this.current = null;
10889
+ return "wrong";
10890
+ }
10891
+ };
10892
+ var REMOTE_DEVICES_VERSION = 1;
10893
+ var REMOTE_MAX_DEVICES = 20;
10894
+ function hashCredential(credential) {
10895
+ return createHash("sha256").update(credential, "utf8").digest("hex");
10672
10896
  }
10673
- function buildTokenCookie(token) {
10674
- return `${TOKEN_COOKIE}=${encodeURIComponent(token)}; Path=/; SameSite=Strict; HttpOnly`;
10897
+ var DeviceBook = class {
10898
+ constructor(deps) {
10899
+ this.deps = deps;
10900
+ }
10901
+ deps;
10902
+ devices = [];
10903
+ load(devices) {
10904
+ this.devices = devices.slice(0, REMOTE_MAX_DEVICES).map((device) => ({ ...device }));
10905
+ }
10906
+ /** Para guardar: con el hash. */
10907
+ snapshot() {
10908
+ return this.devices.map((device) => ({ ...device }));
10909
+ }
10910
+ /** Para mostrar: sin el hash. */
10911
+ list() {
10912
+ return this.devices.map(({ id, label, createdAt, lastSeenAt }) => ({ id, label, createdAt, lastSeenAt }));
10913
+ }
10914
+ get size() {
10915
+ return this.devices.length;
10916
+ }
10917
+ /** Un equipo nuevo y su credencial, que no queda guardada en ningún lado; null si ya no entran más. */
10918
+ enroll(label) {
10919
+ if (this.devices.length >= REMOTE_MAX_DEVICES) return null;
10920
+ const credential = this.deps.randomBytes(32).toString("base64url");
10921
+ const device = {
10922
+ id: this.deps.randomId(),
10923
+ label: cleanDeviceLabel(label) || "Device",
10924
+ hash: hashCredential(credential),
10925
+ createdAt: this.deps.now(),
10926
+ lastSeenAt: null
10927
+ };
10928
+ this.devices.push(device);
10929
+ return { device: { ...device }, credential };
10930
+ }
10931
+ match(credential) {
10932
+ if (credential.length === 0) return null;
10933
+ const hash = hashCredential(credential);
10934
+ let found = null;
10935
+ for (const device of this.devices) {
10936
+ if (sameText(device.hash, hash)) found = device;
10937
+ }
10938
+ return found === null ? null : { ...found };
10939
+ }
10940
+ /** Anota cuándo se lo vio: ahora, o la marca que se le pase. */
10941
+ touch(id, at = this.deps.now()) {
10942
+ const device = this.devices.find((candidate) => candidate.id === id);
10943
+ if (device === void 0) return false;
10944
+ device.lastSeenAt = at;
10945
+ return true;
10946
+ }
10947
+ rename(id, label) {
10948
+ const device = this.devices.find((candidate) => candidate.id === id);
10949
+ const clean = cleanDeviceLabel(label);
10950
+ if (device === void 0 || clean.length === 0) return false;
10951
+ device.label = clean;
10952
+ return true;
10953
+ }
10954
+ revoke(id) {
10955
+ const before = this.devices.length;
10956
+ this.devices = this.devices.filter((device) => device.id !== id);
10957
+ return this.devices.length !== before;
10958
+ }
10959
+ };
10960
+ function parseRemoteDevice(value) {
10961
+ const record = asRecord(value);
10962
+ if (record === null) return null;
10963
+ const id = asNonEmptyString(record["id"]);
10964
+ const label = asString(record["label"]);
10965
+ const hash = asString(record["hash"]);
10966
+ const createdAt = asFiniteNumber(record["createdAt"]);
10967
+ if (id === null || label === null || hash === null || createdAt === null) return null;
10968
+ if (!/^[0-9a-f]{64}$/.test(hash)) return null;
10969
+ const lastSeenAt = record["lastSeenAt"] === null || record["lastSeenAt"] === void 0 ? null : asFiniteNumber(record["lastSeenAt"]);
10970
+ return { id, label: cleanDeviceLabel(label) || "Device", hash, createdAt, lastSeenAt };
10675
10971
  }
10676
- function hasValidHost(request, port) {
10677
- const host = request.headers.host;
10678
- if (typeof host !== "string") return false;
10679
- const separator = host.lastIndexOf(":");
10680
- const hostname = separator === -1 ? host : host.slice(0, separator);
10681
- const hostPort = separator === -1 ? "" : host.slice(separator + 1);
10682
- if (!ALLOWED_HOSTNAMES.has(hostname)) return false;
10683
- return hostPort === "" || hostPort === String(port);
10972
+ function parseRemoteDevicesFile(value) {
10973
+ const record = asRecord(value);
10974
+ if (record === null || record["version"] !== REMOTE_DEVICES_VERSION) return null;
10975
+ return asArrayFiltered(record["devices"], parseRemoteDevice) ?? [];
10684
10976
  }
10685
- function hasValidOrigin(request, port) {
10686
- const origin = request.headers.origin;
10687
- if (typeof origin !== "string" || origin.length === 0) return true;
10688
- let parsed;
10689
- try {
10690
- parsed = new URL(origin);
10691
- } catch {
10692
- return false;
10693
- }
10694
- if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return false;
10695
- if (!ALLOWED_HOSTNAMES.has(parsed.hostname)) return false;
10696
- return parsed.port === String(port);
10977
+ function serializeRemoteDevices(devices) {
10978
+ return `${JSON.stringify({ version: REMOTE_DEVICES_VERSION, devices }, null, 2)}
10979
+ `;
10697
10980
  }
10698
- function rejectRequest(request, port, token) {
10699
- if (!hasValidHost(request, port)) return "bad-host";
10700
- if (!hasValidOrigin(request, port)) return "bad-origin";
10701
- if (!hasValidToken(request, token)) return "bad-token";
10981
+ function remoteAccessState(remote, listening) {
10982
+ if (!remote.enabled) return "off";
10983
+ if (listening.fixedPortBusy) return "port-busy";
10984
+ if (!listening.fixed || listening.port !== remote.port) return "restart-needed";
10985
+ return "active";
10986
+ }
10987
+ function isPrivateIpv4(address) {
10988
+ const parts = address.split(".").map((part) => Number(part));
10989
+ if (parts.length !== 4 || parts.some((part) => !Number.isInteger(part) || part < 0 || part > 255)) return false;
10990
+ const [a, b] = parts;
10991
+ return a === 10 || a === 172 && b >= 16 && b <= 31 || a === 192 && b === 168;
10992
+ }
10993
+ function privateIpv4Addresses(interfaces) {
10994
+ const addresses = [];
10995
+ for (const list of Object.values(interfaces)) {
10996
+ for (const info of list ?? []) {
10997
+ if (info.internal || info.family !== "IPv4" || !isPrivateIpv4(info.address)) continue;
10998
+ if (!addresses.includes(info.address)) addresses.push(info.address);
10999
+ }
11000
+ }
11001
+ return addresses;
11002
+ }
11003
+ var PHONE_APP_USER_AGENT = /AgentWorkbenchAndroid\/\S+ \(Android [^;)]*; ([^)]{1,80})\)/;
11004
+ function deviceLabelFromUserAgent(userAgent) {
11005
+ if (userAgent === void 0) return "Device";
11006
+ const phone = PHONE_APP_USER_AGENT.exec(userAgent);
11007
+ if (phone !== null) {
11008
+ const name = cleanDeviceLabel(phone[1] ?? "");
11009
+ return name.length > 0 ? cleanDeviceLabel(`${name} \xB7 Android`) : "Android";
11010
+ }
11011
+ const browser = /Edg\//.test(userAgent) ? "Edge" : /OPR\//.test(userAgent) ? "Opera" : /Firefox\//.test(userAgent) ? "Firefox" : /Chrome\//.test(userAgent) ? "Chrome" : /Safari\//.test(userAgent) ? "Safari" : null;
11012
+ const system = /Windows/.test(userAgent) ? "Windows" : /Android/.test(userAgent) ? "Android" : /iPhone|iPad/.test(userAgent) ? "iOS" : /Mac OS X/.test(userAgent) ? "macOS" : /Linux/.test(userAgent) ? "Linux" : null;
11013
+ if (browser === null || system === null) return "Device";
11014
+ return `${browser} \xB7 ${system}`;
11015
+ }
11016
+ var HOST_ONLY = /* @__PURE__ */ new Set([
11017
+ "remote.configure",
11018
+ "remote.pairing.start",
11019
+ "remote.pairing.cancel",
11020
+ "remote.device.rename",
11021
+ "remote.device.revoke",
11022
+ "remote.phone.start",
11023
+ "remote.phone.cancel"
11024
+ ]);
11025
+ var OPENS_ON_HOST = /* @__PURE__ */ new Set([
11026
+ "files.reveal",
11027
+ "vault.reveal",
11028
+ "vault.openSession",
11029
+ "vault.exportProject"
11030
+ ]);
11031
+ function remoteRefusal(type, remoteClient) {
11032
+ if (!remoteClient) return null;
11033
+ if (HOST_ONLY.has(type)) return serverText("remoteHostOnly");
11034
+ if (OPENS_ON_HOST.has(type)) return serverText("remoteOpensOnHost");
10702
11035
  return null;
10703
11036
  }
10704
11037
 
10705
- // packages/server/src/repo-hub.ts
10706
- import { EventEmitter as EventEmitter2 } from "node:events";
10707
- import chokidar from "chokidar";
10708
-
10709
- // packages/server/src/git-repo.ts
10710
- import { execFile as execFile3 } from "node:child_process";
10711
- import { readFile as readFile9 } from "node:fs/promises";
10712
- import path29 from "node:path";
10713
- function gitOutputText(output, command) {
10714
- return output.length > 0 ? serverText("raw", { text: output }) : serverText("gitCommandFailed", { command });
10715
- }
10716
- var MAX_CHANGES = 2e3;
10717
- var MAX_DIFF_LINES = 4e3;
10718
- var MAX_DIFF_BYTES = 512 * 1024;
10719
- var GIT_TIMEOUT_MS = 15e3;
10720
- var GIT_MAX_BUFFER = 32 * 1024 * 1024;
10721
- function isMissingBinary(error) {
10722
- return typeof error === "object" && error !== null && error.code === "ENOENT";
10723
- }
10724
- var GitMissingError = class extends Error {
10725
- constructor() {
10726
- super("git wasn't found in the PATH.");
10727
- this.name = "GitMissingError";
10728
- }
10729
- };
10730
- function runGit(cwd, args) {
10731
- return new Promise((resolve, reject) => {
10732
- execFile3(
10733
- "git",
10734
- [
10735
- // Sin esto git escapa los no-ASCII en octal y un archivo con acento
10736
- // vuelve ilegible.
10737
- "-c",
10738
- "core.quotepath=false",
10739
- // Esta app corre `git status` sola, cada pocos segundos, mientras el
10740
- // usuario trabaja. Sin esta bandera `status` refresca y **escribe** el
10741
- // indice, y eso trae dos problemas: toma `index.lock`, con lo que puede
10742
- // hacer fallar un `git add` que el usuario esta escribiendo en la
10743
- // terminal, y modifica `.git/index`, que es justo lo que observa
10744
- // nuestro watcher — o sea que cada lectura se dispara a si misma.
10745
- "--no-optional-locks",
10746
- ...args
10747
- ],
10748
- {
10749
- cwd,
10750
- timeout: GIT_TIMEOUT_MS,
10751
- maxBuffer: GIT_MAX_BUFFER,
10752
- windowsHide: true,
10753
- encoding: "utf8"
10754
- },
10755
- (error, stdout, stderr) => {
10756
- if (error !== null && isMissingBinary(error)) {
10757
- reject(new GitMissingError());
10758
- return;
10759
- }
10760
- const code = error !== null && typeof error.code === "number" ? error.code : error !== null ? 1 : 0;
10761
- resolve({ stdout, stderr, code });
10762
- }
10763
- );
11038
+ // packages/server/src/remote-access-service.ts
11039
+ var TOUCH_SAVE_DELAY_MS = 5e3;
11040
+ var FILES_POLL_MS = 2e3;
11041
+ function detectAuthorizeShell() {
11042
+ if (process.platform !== "win32") return Promise.resolve("terminal");
11043
+ return new Promise((resolve) => {
11044
+ execFile3("whoami", ["/groups", "/fo", "csv", "/nh"], { windowsHide: true, timeout: 5e3 }, (error, stdout) => {
11045
+ resolve(error === null ? authorizeShellFromGroups(String(stdout)) : "powershell-admin");
11046
+ });
10764
11047
  });
10765
11048
  }
10766
- function toPosix(value) {
10767
- return value.split("\\").join("/");
11049
+ function detectHostInfo() {
11050
+ let user = "";
11051
+ try {
11052
+ user = os.userInfo().username;
11053
+ } catch {
11054
+ user = process.env["USERNAME"] ?? process.env["USER"] ?? "";
11055
+ }
11056
+ return { user, hostNames: [os.hostname(), ...privateIpv4Addresses(os.networkInterfaces())] };
10768
11057
  }
10769
- async function resolveRepo(cwd) {
10770
- const result = await runGit(cwd, ["rev-parse", "--show-toplevel"]);
10771
- if (result.code === 0) {
10772
- const root = result.stdout.trim();
10773
- if (root.length > 0) return { state: "ready", root: path29.resolve(root), message: null };
11058
+ var RemoteAccessService = class {
11059
+ settings;
11060
+ store;
11061
+ listening;
11062
+ host;
11063
+ now;
11064
+ log;
11065
+ book;
11066
+ desk;
11067
+ randomBytes;
11068
+ authorizeShell;
11069
+ authorizeShellCache = null;
11070
+ /**
11071
+ * La semilla de la llave del teléfono que se está emparejando (hito 38). Sólo
11072
+ * en memoria, y sólo mientras dure ese emparejamiento: se olvida al
11073
+ * cancelarlo, al canjearse el código o al cerrarse la ventana que lo pidió.
11074
+ */
11075
+ phoneSeed = null;
11076
+ changeListeners = /* @__PURE__ */ new Set();
11077
+ revokedListeners = /* @__PURE__ */ new Set();
11078
+ /** Cuántas ventanas tiene abiertas cada equipo. */
11079
+ connections = /* @__PURE__ */ new Map();
11080
+ /** Las "última vez visto" que todavía no se guardaron: releer el archivo no se las lleva. */
11081
+ pendingSeen = /* @__PURE__ */ new Map();
11082
+ pairingTimer = null;
11083
+ touchTimer = null;
11084
+ pollTimer = null;
11085
+ /** Cómo estaban los dos archivos la última vez que se leyeron o escribieron desde acá. */
11086
+ stamps = { devices: "", settings: "" };
11087
+ /** Las sincronizaciones, en fila: la del sondeo y la de un cambio no se cruzan. */
11088
+ syncing = Promise.resolve();
11089
+ constructor(options) {
11090
+ this.settings = options.settings;
11091
+ this.store = options.store;
11092
+ this.listening = options.listening;
11093
+ this.host = options.host ?? null;
11094
+ this.now = options.now ?? (() => Date.now());
11095
+ this.log = options.log ?? console;
11096
+ const randomBytes5 = options.randomBytes ?? ((size) => cryptoRandomBytes(size));
11097
+ this.randomBytes = randomBytes5;
11098
+ this.authorizeShell = options.authorizeShell ?? detectAuthorizeShell;
11099
+ this.book = new DeviceBook({ now: this.now, randomBytes: randomBytes5, randomId: () => randomUUID2() });
11100
+ this.desk = new PairingDesk({ now: this.now, randomBytes: randomBytes5 });
10774
11101
  }
10775
- const stderr = result.stderr.trim();
10776
- if (/not a git repository|no es un repositorio/i.test(stderr)) {
10777
- return { state: "not-a-repo", root: "", message: null };
11102
+ async load() {
11103
+ this.book.load(await this.store.load());
11104
+ this.stamps = { devices: await this.store.stamp(), settings: await this.settings.stamp() };
11105
+ this.updatePolling();
10778
11106
  }
10779
- if (stderr.length === 0 && result.code !== 0) {
11107
+ // ---- Qué corre ---------------------------------------------------------------
11108
+ remote() {
11109
+ return this.settings.get().remote;
11110
+ }
11111
+ /** Entran equipos: encendido, y el servidor escucha en el puerto fijo con el que arrancó. */
11112
+ accepting() {
11113
+ return this.remote().enabled && this.listening.fixed;
11114
+ }
11115
+ /** A quién pertenece una credencial, o null si ahora no entra nadie. */
11116
+ verifier() {
11117
+ return this.accepting() ? this.book : null;
11118
+ }
11119
+ get pairedDevices() {
11120
+ return this.book.size;
11121
+ }
11122
+ status() {
11123
+ const remote = this.remote();
11124
+ const host = this.host ?? detectHostInfo();
10780
11125
  return {
10781
- state: "error",
10782
- root: "",
10783
- message: serverText("gitRunFailed")
11126
+ enabled: remote.enabled,
11127
+ port: remote.port,
11128
+ state: remoteAccessState(remote, this.listening),
11129
+ listeningPort: this.listening.port,
11130
+ sshUser: host.user,
11131
+ hostNames: [...host.hostNames],
11132
+ pairingActive: this.desk.isActive(),
11133
+ devices: this.book.list().map((device) => ({
11134
+ ...device,
11135
+ connected: (this.connections.get(device.id) ?? 0) > 0
11136
+ }))
10784
11137
  };
10785
11138
  }
10786
- return {
10787
- state: "error",
10788
- root: "",
10789
- message: gitOutputText(stderr.slice(0, 400), "rev-parse")
10790
- };
10791
- }
10792
- async function findRepoRoot(cwd) {
10793
- const lookup = await resolveRepo(cwd);
10794
- return lookup.state === "ready" ? lookup.root : null;
10795
- }
10796
- async function findGitDir(cwd) {
10797
- const result = await runGit(cwd, ["rev-parse", "--absolute-git-dir"]);
10798
- if (result.code !== 0) return null;
10799
- const dir = result.stdout.trim();
10800
- return dir.length > 0 ? path29.resolve(dir) : null;
10801
- }
10802
- function letterToKind(letter) {
10803
- switch (letter) {
10804
- case "A":
10805
- return "added";
10806
- case "M":
10807
- return "modified";
10808
- case "D":
11139
+ // ---- Lo que cambió otra instancia ----------------------------------------------
11140
+ /**
11141
+ * Trae lo que haya cambiado en los dos archivos desde la última vez. Un equipo
11142
+ * que ya no está se avisa como revocado, y el acceso apagado desde otro lado
11143
+ * corta igual que apagado desde acá.
11144
+ */
11145
+ sync() {
11146
+ const result = this.syncing.then(() => this.syncNow());
11147
+ this.syncing = result.catch(() => void 0);
11148
+ return result;
11149
+ }
11150
+ async syncNow() {
11151
+ let changed = false;
11152
+ const settingsStamp = await this.settings.stamp();
11153
+ if (settingsStamp !== this.stamps.settings) {
11154
+ this.stamps.settings = settingsStamp;
11155
+ const wasAccepting = this.accepting();
11156
+ await this.settings.reloadRemote();
11157
+ if (wasAccepting && !this.accepting()) this.cutOff();
11158
+ changed = true;
11159
+ }
11160
+ const devicesStamp = await this.store.stamp();
11161
+ if (devicesStamp !== this.stamps.devices) {
11162
+ this.stamps.devices = devicesStamp;
11163
+ const before = this.book.list().map((device) => device.id);
11164
+ this.book.load(await this.store.load());
11165
+ for (const [deviceId, at] of this.pendingSeen) this.book.touch(deviceId, at);
11166
+ const still = new Set(this.book.list().map((device) => device.id));
11167
+ for (const deviceId of before) {
11168
+ if (still.has(deviceId)) continue;
11169
+ this.connections.delete(deviceId);
11170
+ for (const listener of this.revokedListeners) listener(deviceId);
11171
+ }
11172
+ changed = true;
11173
+ }
11174
+ if (!changed) return;
11175
+ this.updatePolling();
11176
+ this.emitChange();
11177
+ }
11178
+ /** Sondea sólo con el acceso encendido: apagado, esta instancia no hace nada nuevo. */
11179
+ updatePolling() {
11180
+ const wanted = this.remote().enabled;
11181
+ if (wanted && this.pollTimer === null) {
11182
+ this.pollTimer = setInterval(() => void this.sync().catch(() => void 0), FILES_POLL_MS);
11183
+ this.pollTimer.unref();
11184
+ } else if (!wanted && this.pollTimer !== null) {
11185
+ clearInterval(this.pollTimer);
11186
+ this.pollTimer = null;
11187
+ }
11188
+ }
11189
+ /** El acceso dejó de estar encendido: el código muere y se avisa cerrar las ventanas de todos. */
11190
+ cutOff() {
11191
+ this.stopPairing();
11192
+ this.log.log("[remote] remote access turned off: paired devices are disconnected");
11193
+ for (const listener of this.revokedListeners) listener(null);
11194
+ }
11195
+ // ---- Cambios desde el anfitrión ----------------------------------------------
11196
+ /** Lanza un `ServerTextError` si el puerto no sirve; los ajustes quedan como estaban. */
11197
+ async configure(patch) {
11198
+ if (patch.port !== void 0 && !isRemotePort(patch.port)) {
11199
+ throw new ServerTextError(serverText("remotePortInvalid", { max: REMOTE_MAX_PORT, min: REMOTE_MIN_PORT }));
11200
+ }
11201
+ await this.sync();
11202
+ const wasAccepting = this.accepting();
11203
+ await this.settings.update({ remote: patch });
11204
+ this.stamps.settings = await this.settings.stamp();
11205
+ if (wasAccepting && !this.accepting()) this.cutOff();
11206
+ this.updatePolling();
11207
+ this.emitChange();
11208
+ }
11209
+ startPairing() {
11210
+ if (!this.accepting()) throw new ServerTextError(serverText("remoteNotActive"));
11211
+ if (this.book.size >= REMOTE_MAX_DEVICES) {
11212
+ throw new ServerTextError(serverText("remoteTooManyDevices", { max: REMOTE_MAX_DEVICES }));
11213
+ }
11214
+ this.stopPairing();
11215
+ const pairing = this.desk.start();
11216
+ this.pairingTimer = setTimeout(() => {
11217
+ this.pairingTimer = null;
11218
+ this.emitChange();
11219
+ }, Math.max(0, pairing.expiresAt - this.now()) + 50);
11220
+ this.pairingTimer.unref();
11221
+ this.emitChange();
11222
+ return pairing;
11223
+ }
11224
+ cancelPairing() {
11225
+ if (!this.desk.isActive()) return;
11226
+ this.stopPairing();
11227
+ this.emitChange();
11228
+ }
11229
+ /**
11230
+ * Empareja un teléfono (hito 38, §15): un código de un solo uso y la llave SSH
11231
+ * que va con él en el QR. La llave es la misma mientras no se cancele: si el
11232
+ * código vence antes de que el teléfono lo use, el usuario pide otro y no
11233
+ * tiene que autorizar otra llave.
11234
+ *
11235
+ * La dirección lleva el puerto en el que escucha este arranque, que es el que
11236
+ * el teléfono va a reenviar.
11237
+ */
11238
+ async startPhonePairing() {
11239
+ const pairing = this.startPairing();
11240
+ this.phoneSeed ??= this.randomBytes(PHONE_SEED_BYTES);
11241
+ const seed = this.phoneSeed;
11242
+ this.authorizeShellCache ??= this.authorizeShell();
11243
+ const shell = await this.authorizeShellCache;
11244
+ const host = this.host ?? detectHostInfo();
11245
+ const [pcName = "", ...addresses] = host.hostNames;
11246
+ const appPort = this.listening.port;
11247
+ return {
11248
+ payload: phonePairingPayload({
11249
+ pcName,
11250
+ hosts: addresses.length > 0 ? addresses : [pcName],
11251
+ sshPort: PHONE_SSH_PORT,
11252
+ user: host.user,
11253
+ appPort,
11254
+ code: pairing.code.replace("-", ""),
11255
+ seed
11256
+ }),
11257
+ authorizeCommand: phoneAuthorizeCommand(phoneAuthorizedKeysEntry(ed25519PublicKey(seed), appPort), shell),
11258
+ shell,
11259
+ code: pairing.code,
11260
+ expiresAt: pairing.expiresAt
11261
+ };
11262
+ }
11263
+ /** Olvida la llave del teléfono y da de baja el código. */
11264
+ cancelPhonePairing() {
11265
+ this.phoneSeed = null;
11266
+ this.cancelPairing();
11267
+ }
11268
+ /** Para el chequeo: si todavía hay una llave de teléfono en memoria. */
11269
+ get phoneKeyHeld() {
11270
+ return this.phoneSeed !== null;
11271
+ }
11272
+ stopPairing() {
11273
+ this.desk.cancel();
11274
+ if (this.pairingTimer !== null) clearTimeout(this.pairingTimer);
11275
+ this.pairingTimer = null;
11276
+ }
11277
+ async renameDevice(deviceId, label) {
11278
+ await this.sync();
11279
+ if (!this.book.rename(deviceId, label)) return false;
11280
+ await this.persist();
11281
+ this.emitChange();
11282
+ return true;
11283
+ }
11284
+ async revokeDevice(deviceId) {
11285
+ await this.sync();
11286
+ if (!this.book.revoke(deviceId)) return false;
11287
+ this.connections.delete(deviceId);
11288
+ this.log.log("[remote] a paired device was revoked");
11289
+ for (const listener of this.revokedListeners) listener(deviceId);
11290
+ await this.persist();
11291
+ this.emitChange();
11292
+ return true;
11293
+ }
11294
+ // ---- Lo que llega de afuera --------------------------------------------------
11295
+ /**
11296
+ * Canjea un código por un equipo nuevo, o null. El nombre sale del navegador
11297
+ * que lo presentó; el usuario lo cambia después.
11298
+ */
11299
+ async redeemPairing(input, userAgent) {
11300
+ if (!this.accepting()) return null;
11301
+ const outcome = this.desk.redeem(input);
11302
+ if (outcome === "none") return null;
11303
+ if (outcome === "wrong") {
11304
+ this.log.warn("[remote] a wrong pairing code was presented");
11305
+ if (!this.desk.isActive()) {
11306
+ this.stopPairing();
11307
+ this.log.warn("[remote] too many wrong codes: the pairing code is no longer valid");
11308
+ this.emitChange();
11309
+ }
11310
+ return null;
11311
+ }
11312
+ this.stopPairing();
11313
+ this.phoneSeed = null;
11314
+ await this.sync();
11315
+ const enrolled = this.accepting() ? this.book.enroll(deviceLabelFromUserAgent(userAgent)) : null;
11316
+ if (enrolled === null) {
11317
+ this.emitChange();
11318
+ return null;
11319
+ }
11320
+ await this.persist();
11321
+ this.log.log(`[remote] paired a new device: ${enrolled.device.label}`);
11322
+ this.emitChange();
11323
+ return { deviceId: enrolled.device.id, credential: enrolled.credential };
11324
+ }
11325
+ noteConnected(deviceId) {
11326
+ this.connections.set(deviceId, (this.connections.get(deviceId) ?? 0) + 1);
11327
+ this.touch(deviceId);
11328
+ this.emitChange();
11329
+ }
11330
+ noteDisconnected(deviceId) {
11331
+ const left = (this.connections.get(deviceId) ?? 0) - 1;
11332
+ if (left > 0) this.connections.set(deviceId, left);
11333
+ else this.connections.delete(deviceId);
11334
+ this.touch(deviceId);
11335
+ this.emitChange();
11336
+ }
11337
+ /** La "última vez visto" no vale una escritura por conexión: se junta. */
11338
+ touch(deviceId) {
11339
+ const at = this.now();
11340
+ if (!this.book.touch(deviceId, at)) return;
11341
+ this.pendingSeen.set(deviceId, at);
11342
+ if (this.touchTimer !== null) return;
11343
+ this.touchTimer = setTimeout(() => {
11344
+ this.touchTimer = null;
11345
+ void this.persistTouches();
11346
+ }, TOUCH_SAVE_DELAY_MS);
11347
+ this.touchTimer.unref();
11348
+ }
11349
+ /**
11350
+ * Guarda las "última vez visto". Relee antes: una fecha no puede devolverle
11351
+ * la vida a un equipo que otra instancia revocó mientras tanto —al releer, el
11352
+ * que ya no está no recibe la suya—.
11353
+ */
11354
+ async persistTouches() {
11355
+ if (this.pendingSeen.size === 0) return;
11356
+ await this.sync();
11357
+ this.pendingSeen.clear();
11358
+ await this.persist();
11359
+ }
11360
+ // ---- Avisos y disco ----------------------------------------------------------
11361
+ onChange(listener) {
11362
+ this.changeListeners.add(listener);
11363
+ return () => this.changeListeners.delete(listener);
11364
+ }
11365
+ /** Hay que cerrar las ventanas de ese equipo; con null, las de todos. */
11366
+ onRevoked(listener) {
11367
+ this.revokedListeners.add(listener);
11368
+ return () => this.revokedListeners.delete(listener);
11369
+ }
11370
+ emitChange() {
11371
+ for (const listener of this.changeListeners) listener();
11372
+ }
11373
+ /** Escribe los equipos y anota cómo quedó el archivo, para no tomarlo por un cambio ajeno. Nunca rechaza. */
11374
+ async persist() {
11375
+ try {
11376
+ await this.store.save(this.book.snapshot());
11377
+ this.stamps.devices = await this.store.stamp();
11378
+ } catch (error) {
11379
+ this.log.warn("[remote] couldn't save the paired devices:", error instanceof Error ? error.message : String(error));
11380
+ }
11381
+ }
11382
+ /** Escribe lo pendiente y espera. Nunca rechaza. */
11383
+ async flush() {
11384
+ if (this.touchTimer !== null) clearTimeout(this.touchTimer);
11385
+ this.touchTimer = null;
11386
+ await this.persistTouches().catch(() => void 0);
11387
+ await this.store.flush();
11388
+ }
11389
+ /** Frena los relojes. Lo que quede por guardar lo escribe `flush()`. */
11390
+ dispose() {
11391
+ this.stopPairing();
11392
+ this.phoneSeed = null;
11393
+ if (this.pollTimer !== null) clearInterval(this.pollTimer);
11394
+ this.pollTimer = null;
11395
+ }
11396
+ };
11397
+
11398
+ // packages/server/src/remote-devices-store.ts
11399
+ import { chmod, mkdir as mkdir6, readFile as readFile9, stat as stat16 } from "node:fs/promises";
11400
+ import path31 from "node:path";
11401
+
11402
+ // packages/server/src/vault/write.ts
11403
+ import { randomBytes as randomBytes2 } from "node:crypto";
11404
+ import { mkdir as mkdir5, rename as rename3, rm as rm2, stat as stat15, writeFile as writeFile3 } from "node:fs/promises";
11405
+
11406
+ // packages/server/src/vault/paths.ts
11407
+ import { createHash as createHash2 } from "node:crypto";
11408
+ import path30 from "node:path";
11409
+ function defaultVaultDir() {
11410
+ return path30.join(appConfigDir(), "vault");
11411
+ }
11412
+ function vaultMarkerFile(dir) {
11413
+ return path30.join(dir, "vault.json");
11414
+ }
11415
+ function sessionsRoot2(dir) {
11416
+ return path30.join(dir, "sessions");
11417
+ }
11418
+ function isSafeSessionId(sessionId) {
11419
+ return isVaultSessionId(sessionId);
11420
+ }
11421
+ function isSessionAgentId(value) {
11422
+ return SESSION_AGENT_IDS.includes(value);
11423
+ }
11424
+ function assertSessionKey(agent, sessionId) {
11425
+ if (!isSessionAgentId(agent)) throw new Error(`Unknown source for the local copy: ${JSON.stringify(agent)}`);
11426
+ if (!isSafeSessionId(sessionId)) throw new Error(`Session id that isn't copied: ${JSON.stringify(sessionId)}`);
11427
+ }
11428
+ function agentSessionsDir(dir, agent) {
11429
+ if (!isSessionAgentId(agent)) throw new Error(`Unknown source for the local copy: ${JSON.stringify(agent)}`);
11430
+ return path30.join(sessionsRoot2(dir), agent);
11431
+ }
11432
+ function sessionFile(dir, agent, sessionId) {
11433
+ assertSessionKey(agent, sessionId);
11434
+ return path30.join(sessionsRoot2(dir), agent, `${sessionId}.jsonl`);
11435
+ }
11436
+ function assetsDir(dir, agent, sessionId) {
11437
+ assertSessionKey(agent, sessionId);
11438
+ return path30.join(sessionsRoot2(dir), agent, `${sessionId}.assets`);
11439
+ }
11440
+ function assetFile(dir, agent, sessionId, asset) {
11441
+ if (!VAULT_ASSET_NAME_PATTERN.test(asset)) throw new Error(`Invalid asset name: ${JSON.stringify(asset)}`);
11442
+ return path30.join(assetsDir(dir, agent, sessionId), asset);
11443
+ }
11444
+ function projectFolderName(cwd, platform) {
11445
+ const hash = createHash2("sha256").update(normalizeCwdKey(cwd, platform)).digest("hex").slice(0, 8);
11446
+ const base = (platform === "win32" ? path30.win32 : path30.posix).basename(cwd);
11447
+ const name = base.length === 0 ? "proyecto" : sanitizeFileName(base, 60);
11448
+ return `${name}-${hash}`;
11449
+ }
11450
+ function memoryDir(dir, cwd, platform) {
11451
+ return path30.join(dir, "memory", projectFolderName(cwd, platform));
11452
+ }
11453
+ function projectExportDir(dir, cwd, platform) {
11454
+ return path30.join(dir, "export", projectFolderName(cwd, platform));
11455
+ }
11456
+ function projectExportDirFor(dir, project, platform) {
11457
+ if (project.cwd.length > 0) return projectExportDir(dir, project.cwd, platform);
11458
+ const hash = createHash2("sha256").update(project.key).digest("hex").slice(0, 8);
11459
+ const name = project.fallbackName.length > 0 ? sanitizeFileName(project.fallbackName, 60) : "proyecto";
11460
+ return path30.join(dir, "export", `${name}-${hash}`);
11461
+ }
11462
+ function sessionExportFile(dir, agent, sessionId) {
11463
+ assertSessionKey(agent, sessionId);
11464
+ return path30.join(dir, "export", "sesiones", `${agent}-${sessionId}.md`);
11465
+ }
11466
+ var RESERVED_WINDOWS_NAME = /^(con|prn|aux|nul|com[1-9]|lpt[1-9])$/i;
11467
+ function sanitizeFileName(text, max = 80) {
11468
+ const limit = Math.max(1, Math.floor(max));
11469
+ const kept = text.normalize("NFC").replace(/[^\p{L}\p{M}\p{N} ._-]/gu, "_");
11470
+ const trimEnds = (value) => value.replace(/^ +/u, "").replace(/[. ]+$/u, "");
11471
+ let name = trimEnds(Array.from(trimEnds(kept)).slice(0, limit).join(""));
11472
+ if (name.length === 0) return "_";
11473
+ const stem = (name.split(".")[0] ?? "").replace(/ +$/u, "");
11474
+ if (RESERVED_WINDOWS_NAME.test(stem)) {
11475
+ name = `_${Array.from(name).slice(0, limit - 1).join("")}`;
11476
+ name = name.replace(/[. ]+$/u, "");
11477
+ }
11478
+ return name;
11479
+ }
11480
+
11481
+ // packages/server/src/vault/write.ts
11482
+ var RENAME_RETRIES = 3;
11483
+ var RENAME_RETRY_MS = 100;
11484
+ var DEFAULT_WRITE_DEPS = {
11485
+ rename: rename3,
11486
+ platform: process.platform,
11487
+ delay: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
11488
+ now: () => Date.now()
11489
+ };
11490
+ function temporaryPathFor(target) {
11491
+ return `${target}.${process.pid}.${randomBytes2(6).toString("hex")}.tmp`;
11492
+ }
11493
+ function isRetryableRename(error, platform) {
11494
+ if (platform !== "win32") return false;
11495
+ const code = error?.code;
11496
+ return code === "EPERM" || code === "EBUSY";
11497
+ }
11498
+ async function writeFileAtomic(target, data, deps = DEFAULT_WRITE_DEPS) {
11499
+ const temporary = temporaryPathFor(target);
11500
+ try {
11501
+ await writeFile3(temporary, data);
11502
+ for (let attempt = 0; ; attempt += 1) {
11503
+ try {
11504
+ await deps.rename(temporary, target);
11505
+ return;
11506
+ } catch (error) {
11507
+ if (attempt >= RENAME_RETRIES || !isRetryableRename(error, deps.platform)) throw error;
11508
+ await deps.delay(RENAME_RETRY_MS);
11509
+ }
11510
+ }
11511
+ } catch (error) {
11512
+ await rm2(temporary, { force: true }).catch(() => void 0);
11513
+ throw error;
11514
+ }
11515
+ }
11516
+ async function ensureVaultMarker(dir, deps = DEFAULT_WRITE_DEPS) {
11517
+ const marker = vaultMarkerFile(dir);
11518
+ try {
11519
+ await stat15(marker);
11520
+ return;
11521
+ } catch {
11522
+ }
11523
+ await mkdir5(dir, { recursive: true });
11524
+ await writeFileAtomic(marker, `${JSON.stringify({ format: VAULT_FORMAT, createdAt: deps.now() })}
11525
+ `, deps);
11526
+ }
11527
+ async function assetPresent(file, size) {
11528
+ try {
11529
+ const info = await stat15(file);
11530
+ return info.isFile() && info.size === size;
11531
+ } catch {
11532
+ return false;
11533
+ }
11534
+ }
11535
+ async function writeSessionFile(dir, serialized, deps = DEFAULT_WRITE_DEPS) {
11536
+ const { agent, sessionId } = serialized.header;
11537
+ const file = sessionFile(dir, agent, sessionId);
11538
+ await ensureVaultMarker(dir, deps);
11539
+ await mkdir5(agentSessionsDir(dir, agent), { recursive: true });
11540
+ let assetsWritten = 0;
11541
+ let assetBytesWritten = 0;
11542
+ if (serialized.assets.size > 0) {
11543
+ await mkdir5(assetsDir(dir, agent, sessionId), { recursive: true });
11544
+ for (const [name, bytes] of serialized.assets) {
11545
+ const target = assetFile(dir, agent, sessionId, name);
11546
+ if (await assetPresent(target, bytes.length)) continue;
11547
+ await writeFileAtomic(target, bytes, deps);
11548
+ assetsWritten += 1;
11549
+ assetBytesWritten += bytes.length;
11550
+ }
11551
+ }
11552
+ const text = Buffer.from(serialized.text, "utf8");
11553
+ await writeFileAtomic(file, text, deps);
11554
+ return { file, bytes: text.length, assetsWritten, assetBytesWritten };
11555
+ }
11556
+
11557
+ // packages/server/src/remote-devices-store.ts
11558
+ var RemoteDevicesStore = class {
11559
+ /** La ruta y el aviso son inyectables para las pruebas. */
11560
+ constructor(filePath = remoteDevicesPath(), log = console) {
11561
+ this.filePath = filePath;
11562
+ this.log = log;
11563
+ }
11564
+ filePath;
11565
+ log;
11566
+ /** Las escrituras, en fila: dos seguidas no se pisan. */
11567
+ queue = Promise.resolve();
11568
+ async load() {
11569
+ let raw;
11570
+ try {
11571
+ raw = await readFile9(this.filePath, "utf8");
11572
+ } catch {
11573
+ return [];
11574
+ }
11575
+ let parsed;
11576
+ try {
11577
+ parsed = JSON.parse(raw);
11578
+ } catch {
11579
+ this.log.warn("[remote] remote-devices.json isn't valid JSON: no paired devices are loaded");
11580
+ return [];
11581
+ }
11582
+ const devices = parseRemoteDevicesFile(parsed);
11583
+ if (devices === null) {
11584
+ this.log.warn("[remote] remote-devices.json is from another version: no paired devices are loaded");
11585
+ return [];
11586
+ }
11587
+ return devices;
11588
+ }
11589
+ save(devices) {
11590
+ const text = serializeRemoteDevices(devices);
11591
+ const run = async () => {
11592
+ await mkdir6(path31.dirname(this.filePath), { recursive: true });
11593
+ await writeFileAtomic(this.filePath, text);
11594
+ await chmod(this.filePath, 384).catch(() => void 0);
11595
+ };
11596
+ const result = this.queue.then(run);
11597
+ this.queue = result.catch(() => void 0);
11598
+ return result;
11599
+ }
11600
+ /** Espera lo que quede por escribir. Nunca rechaza. */
11601
+ flush() {
11602
+ return this.queue;
11603
+ }
11604
+ /**
11605
+ * Una marca que cambia cuando el archivo cambia (fecha y tamaño), o '' si no
11606
+ * está. Es lo que mira otra instancia de la app para enterarse de que acá se
11607
+ * revocó un equipo: comparten la carpeta de configuración.
11608
+ */
11609
+ async stamp() {
11610
+ try {
11611
+ const info = await stat16(this.filePath);
11612
+ return `${info.mtimeMs}:${info.size}`;
11613
+ } catch {
11614
+ return "";
11615
+ }
11616
+ }
11617
+ };
11618
+
11619
+ // packages/server/src/security.ts
11620
+ import { randomBytes as randomBytes3, timingSafeEqual as timingSafeEqual2 } from "node:crypto";
11621
+ var LOOPBACK_HOST = "127.0.0.1";
11622
+ var ALLOWED_HOSTNAMES = /* @__PURE__ */ new Set(["127.0.0.1", "localhost", "[::1]", "::1"]);
11623
+ var TOKEN_HEADER = "x-agent-workbench-token";
11624
+ var TOKEN_COOKIE = "agent_workbench_token";
11625
+ function createSessionToken() {
11626
+ return randomBytes3(32).toString("base64url");
11627
+ }
11628
+ function constantTimeEquals(a, b) {
11629
+ const bufferA = Buffer.from(a, "utf8");
11630
+ const bufferB = Buffer.from(b, "utf8");
11631
+ if (bufferA.length !== bufferB.length) return false;
11632
+ return timingSafeEqual2(bufferA, bufferB);
11633
+ }
11634
+ function requestUrl(request) {
11635
+ try {
11636
+ return new URL(request.url ?? "/", `http://${request.headers.host ?? LOOPBACK_HOST}`);
11637
+ } catch {
11638
+ return null;
11639
+ }
11640
+ }
11641
+ function readCookie(request, name) {
11642
+ const header = request.headers.cookie;
11643
+ if (typeof header !== "string") return null;
11644
+ for (const pair of header.split(";")) {
11645
+ const separator = pair.indexOf("=");
11646
+ if (separator === -1) continue;
11647
+ if (pair.slice(0, separator).trim() !== name) continue;
11648
+ const value = pair.slice(separator + 1).trim();
11649
+ return value.length > 0 ? decodeURIComponent(value) : null;
11650
+ }
11651
+ return null;
11652
+ }
11653
+ function matchToken(request, expected) {
11654
+ const url = requestUrl(request);
11655
+ const fromQuery = url?.searchParams.get(TOKEN_QUERY_PARAM) ?? null;
11656
+ if (fromQuery !== null && constantTimeEquals(fromQuery, expected)) {
11657
+ return { source: "query" };
11658
+ }
11659
+ const fromHeader = request.headers[TOKEN_HEADER];
11660
+ if (typeof fromHeader === "string" && constantTimeEquals(fromHeader, expected)) {
11661
+ return { source: "header" };
11662
+ }
11663
+ const fromCookie = readCookie(request, TOKEN_COOKIE);
11664
+ if (fromCookie !== null && constantTimeEquals(fromCookie, expected)) {
11665
+ return { source: "cookie" };
11666
+ }
11667
+ return null;
11668
+ }
11669
+ function buildTokenCookie(token) {
11670
+ return `${TOKEN_COOKIE}=${encodeURIComponent(token)}; Path=/; SameSite=Strict; HttpOnly`;
11671
+ }
11672
+ var DEVICE_COOKIE = "agent_workbench_device";
11673
+ var DEVICE_COOKIE_MAX_AGE_S = 400 * 24 * 60 * 60;
11674
+ function buildDeviceCookie(credential) {
11675
+ return `${DEVICE_COOKIE}=${encodeURIComponent(credential)}; Path=/; Max-Age=${DEVICE_COOKIE_MAX_AGE_S}; SameSite=Strict; HttpOnly`;
11676
+ }
11677
+ function matchCredential(request, token, devices) {
11678
+ const session = matchToken(request, token);
11679
+ if (session !== null) return { kind: "session", source: session.source };
11680
+ if (devices === null) return null;
11681
+ const credential = readCookie(request, DEVICE_COOKIE);
11682
+ if (credential === null) return null;
11683
+ const device = devices.match(credential);
11684
+ return device === null ? null : { kind: "device", deviceId: device.id, credential };
11685
+ }
11686
+ function readPairingCode(request) {
11687
+ const code = requestUrl(request)?.searchParams.get(PAIR_QUERY_PARAM) ?? null;
11688
+ return code !== null && code.length > 0 ? code : null;
11689
+ }
11690
+ function hasValidHost(request, port) {
11691
+ const host = request.headers.host;
11692
+ if (typeof host !== "string") return false;
11693
+ const separator = host.lastIndexOf(":");
11694
+ const hostname = separator === -1 ? host : host.slice(0, separator);
11695
+ const hostPort = separator === -1 ? "" : host.slice(separator + 1);
11696
+ if (!ALLOWED_HOSTNAMES.has(hostname)) return false;
11697
+ return hostPort === "" || hostPort === String(port);
11698
+ }
11699
+ function hasValidOrigin(request, port) {
11700
+ const origin = request.headers.origin;
11701
+ if (typeof origin !== "string" || origin.length === 0) return true;
11702
+ let parsed;
11703
+ try {
11704
+ parsed = new URL(origin);
11705
+ } catch {
11706
+ return false;
11707
+ }
11708
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return false;
11709
+ if (!ALLOWED_HOSTNAMES.has(parsed.hostname)) return false;
11710
+ return parsed.port === String(port);
11711
+ }
11712
+ function authorizeRequest(request, port, token, devices) {
11713
+ if (!hasValidHost(request, port)) return { ok: false, reason: "bad-host" };
11714
+ if (!hasValidOrigin(request, port)) return { ok: false, reason: "bad-origin" };
11715
+ const credential = matchCredential(request, token, devices);
11716
+ return credential === null ? { ok: false, reason: "bad-token" } : { ok: true, credential };
11717
+ }
11718
+
11719
+ // packages/server/src/repo-hub.ts
11720
+ import { EventEmitter as EventEmitter2 } from "node:events";
11721
+ import chokidar from "chokidar";
11722
+
11723
+ // packages/server/src/git-repo.ts
11724
+ import { execFile as execFile4 } from "node:child_process";
11725
+ import { readFile as readFile10 } from "node:fs/promises";
11726
+ import path32 from "node:path";
11727
+ function gitOutputText(output, command) {
11728
+ return output.length > 0 ? serverText("raw", { text: output }) : serverText("gitCommandFailed", { command });
11729
+ }
11730
+ var MAX_CHANGES = 2e3;
11731
+ var MAX_DIFF_LINES = 4e3;
11732
+ var MAX_DIFF_BYTES = 512 * 1024;
11733
+ var GIT_TIMEOUT_MS = 15e3;
11734
+ var GIT_MAX_BUFFER = 32 * 1024 * 1024;
11735
+ function isMissingBinary(error) {
11736
+ return typeof error === "object" && error !== null && error.code === "ENOENT";
11737
+ }
11738
+ var GitMissingError = class extends Error {
11739
+ constructor() {
11740
+ super("git wasn't found in the PATH.");
11741
+ this.name = "GitMissingError";
11742
+ }
11743
+ };
11744
+ function runGit(cwd, args) {
11745
+ return new Promise((resolve, reject) => {
11746
+ execFile4(
11747
+ "git",
11748
+ [
11749
+ // Sin esto git escapa los no-ASCII en octal y un archivo con acento
11750
+ // vuelve ilegible.
11751
+ "-c",
11752
+ "core.quotepath=false",
11753
+ // Esta app corre `git status` sola, cada pocos segundos, mientras el
11754
+ // usuario trabaja. Sin esta bandera `status` refresca y **escribe** el
11755
+ // indice, y eso trae dos problemas: toma `index.lock`, con lo que puede
11756
+ // hacer fallar un `git add` que el usuario esta escribiendo en la
11757
+ // terminal, y modifica `.git/index`, que es justo lo que observa
11758
+ // nuestro watcher — o sea que cada lectura se dispara a si misma.
11759
+ "--no-optional-locks",
11760
+ ...args
11761
+ ],
11762
+ {
11763
+ cwd,
11764
+ timeout: GIT_TIMEOUT_MS,
11765
+ maxBuffer: GIT_MAX_BUFFER,
11766
+ windowsHide: true,
11767
+ encoding: "utf8"
11768
+ },
11769
+ (error, stdout, stderr) => {
11770
+ if (error !== null && isMissingBinary(error)) {
11771
+ reject(new GitMissingError());
11772
+ return;
11773
+ }
11774
+ const code = error !== null && typeof error.code === "number" ? error.code : error !== null ? 1 : 0;
11775
+ resolve({ stdout, stderr, code });
11776
+ }
11777
+ );
11778
+ });
11779
+ }
11780
+ function toPosix(value) {
11781
+ return value.split("\\").join("/");
11782
+ }
11783
+ async function resolveRepo(cwd) {
11784
+ const result = await runGit(cwd, ["rev-parse", "--show-toplevel"]);
11785
+ if (result.code === 0) {
11786
+ const root = result.stdout.trim();
11787
+ if (root.length > 0) return { state: "ready", root: path32.resolve(root), message: null };
11788
+ }
11789
+ const stderr = result.stderr.trim();
11790
+ if (/not a git repository|no es un repositorio/i.test(stderr)) {
11791
+ return { state: "not-a-repo", root: "", message: null };
11792
+ }
11793
+ if (stderr.length === 0 && result.code !== 0) {
11794
+ return {
11795
+ state: "error",
11796
+ root: "",
11797
+ message: serverText("gitRunFailed")
11798
+ };
11799
+ }
11800
+ return {
11801
+ state: "error",
11802
+ root: "",
11803
+ message: gitOutputText(stderr.slice(0, 400), "rev-parse")
11804
+ };
11805
+ }
11806
+ async function findRepoRoot(cwd) {
11807
+ const lookup = await resolveRepo(cwd);
11808
+ return lookup.state === "ready" ? lookup.root : null;
11809
+ }
11810
+ async function findGitDir(cwd) {
11811
+ const result = await runGit(cwd, ["rev-parse", "--absolute-git-dir"]);
11812
+ if (result.code !== 0) return null;
11813
+ const dir = result.stdout.trim();
11814
+ return dir.length > 0 ? path32.resolve(dir) : null;
11815
+ }
11816
+ function letterToKind(letter) {
11817
+ switch (letter) {
11818
+ case "A":
11819
+ return "added";
11820
+ case "M":
11821
+ return "modified";
11822
+ case "D":
10809
11823
  return "deleted";
10810
11824
  case "R":
10811
11825
  return "renamed";
@@ -10900,7 +11914,7 @@ function parseWorktrees(raw, currentRoot) {
10900
11914
  worktrees.push({
10901
11915
  path: current.path,
10902
11916
  branch: current.branch,
10903
- isCurrent: path29.resolve(current.path) === path29.resolve(currentRoot)
11917
+ isCurrent: path32.resolve(current.path) === path32.resolve(currentRoot)
10904
11918
  });
10905
11919
  current = null;
10906
11920
  };
@@ -11043,7 +12057,7 @@ function looksBinary(buffer) {
11043
12057
  async function readUntrackedAsDiff(absolutePath, relativePath) {
11044
12058
  let content;
11045
12059
  try {
11046
- content = await readFile9(absolutePath);
12060
+ content = await readFile10(absolutePath);
11047
12061
  } catch {
11048
12062
  return {
11049
12063
  path: relativePath,
@@ -11091,7 +12105,7 @@ async function readUntrackedAsDiff(absolutePath, relativePath) {
11091
12105
  async function readDiff(options) {
11092
12106
  const { repoRoot: repoRoot2, relativePath, staged, untracked } = options;
11093
12107
  if (untracked && !staged) {
11094
- return readUntrackedAsDiff(path29.join(repoRoot2, relativePath), relativePath);
12108
+ return readUntrackedAsDiff(path32.join(repoRoot2, relativePath), relativePath);
11095
12109
  }
11096
12110
  const args = [
11097
12111
  "diff",
@@ -11130,7 +12144,7 @@ async function filterIgnored(repoRoot2, relativePaths) {
11130
12144
  const ignored = /* @__PURE__ */ new Set();
11131
12145
  if (relativePaths.length === 0) return ignored;
11132
12146
  const result = await new Promise((resolve) => {
11133
- const child = execFile3(
12147
+ const child = execFile4(
11134
12148
  "git",
11135
12149
  ["-c", "core.quotepath=false", "check-ignore", "--stdin", "-z"],
11136
12150
  { cwd: repoRoot2, timeout: GIT_TIMEOUT_MS, maxBuffer: GIT_MAX_BUFFER, windowsHide: true },
@@ -11311,24 +12325,24 @@ var RepoHub = class extends EventEmitter2 {
11311
12325
  // packages/server/src/memory-hub.ts
11312
12326
  import { EventEmitter as EventEmitter3 } from "node:events";
11313
12327
  import { homedir as homedir10 } from "node:os";
11314
- import path31 from "node:path";
12328
+ import path34 from "node:path";
11315
12329
  import chokidar2 from "chokidar";
11316
12330
 
11317
12331
  // packages/server/src/memory-bridge.ts
11318
- import { execFile as execFile4 } from "node:child_process";
11319
- import { randomBytes as randomBytes3 } from "node:crypto";
12332
+ import { execFile as execFile5 } from "node:child_process";
12333
+ import { randomBytes as randomBytes4 } from "node:crypto";
11320
12334
  import {
11321
- mkdir as mkdir5,
12335
+ mkdir as mkdir7,
11322
12336
  open as open6,
11323
12337
  readdir as readdir9,
11324
- readFile as readFile10,
12338
+ readFile as readFile11,
11325
12339
  realpath as realpath2,
11326
- rename as rename3,
11327
- stat as stat15,
12340
+ rename as rename4,
12341
+ stat as stat17,
11328
12342
  unlink as unlink3
11329
12343
  } from "node:fs/promises";
11330
12344
  import { homedir as homedir9 } from "node:os";
11331
- import path30 from "node:path";
12345
+ import path33 from "node:path";
11332
12346
  var MemoryBridgeError = class extends ServerTextError {
11333
12347
  };
11334
12348
  var MAX_LISTED_NOTES = 500;
@@ -11346,9 +12360,9 @@ var GITIGNORE_MEMORY_LINE = `${MEMORY_DIR}/`;
11346
12360
  var BOM2 = Buffer.from([239, 187, 191]);
11347
12361
  async function inspectMemory(cwd, home = homedir9()) {
11348
12362
  await assertMemoryDirInside(cwd);
11349
- const folder = path30.join(cwd, MEMORY_DIR);
12363
+ const folder = path33.join(cwd, MEMORY_DIR);
11350
12364
  const folderExists = await isDirectory2(folder);
11351
- const indexExists = folderExists && await isFile(path30.join(folder, MEMORY_INDEX_FILE));
12365
+ const indexExists = folderExists && await isFile(path33.join(folder, MEMORY_INDEX_FILE));
11352
12366
  const noteNames = folderExists ? await listNoteFiles(folder) : [];
11353
12367
  const notes = await describeNotes(folder, noteNames);
11354
12368
  const files = await Promise.all(
@@ -11380,7 +12394,7 @@ async function installMemory(cwd, options, home = homedir9()) {
11380
12394
  }
11381
12395
  async function importNativeMemory(cwd, home = homedir9()) {
11382
12396
  await assertMemoryDirInside(cwd);
11383
- if (!await isDirectory2(path30.join(cwd, MEMORY_DIR))) {
12397
+ if (!await isDirectory2(path33.join(cwd, MEMORY_DIR))) {
11384
12398
  throw new MemoryBridgeError(serverText("memoryNotInstalled"));
11385
12399
  }
11386
12400
  const plan = new PlanBuilder(cwd);
@@ -11398,9 +12412,9 @@ async function readMemoryNote(cwd, name) {
11398
12412
  }
11399
12413
  const absolute = await resolveInside(cwd, `${MEMORY_DIR}/${name}`);
11400
12414
  try {
11401
- const info = await stat15(absolute);
12415
+ const info = await stat17(absolute);
11402
12416
  if (!info.isFile()) return null;
11403
- const content = await readFile10(absolute);
12417
+ const content = await readFile11(absolute);
11404
12418
  return {
11405
12419
  name,
11406
12420
  text: content.subarray(0, MAX_NOTE_BYTES).toString("utf8"),
@@ -11415,7 +12429,7 @@ function isNoteName(name) {
11415
12429
  if (name.length === 0 || name.length > 255) return false;
11416
12430
  if (name.includes("\0")) return false;
11417
12431
  if (name === "." || name === "..") return false;
11418
- if (path30.basename(name) !== name) return false;
12432
+ if (path33.basename(name) !== name) return false;
11419
12433
  if (/[\\/:*?"<>|\u0000-\u001f]/.test(name)) return false;
11420
12434
  return name.toLowerCase().endsWith(".md");
11421
12435
  }
@@ -11452,7 +12466,7 @@ var PlanBuilder = class {
11452
12466
  /** Nombres de nota que habria en la carpeta, en minusculas. */
11453
12467
  async noteNames() {
11454
12468
  const names = /* @__PURE__ */ new Map();
11455
- for (const name of await listNoteFiles(path30.join(this.cwd, MEMORY_DIR))) {
12469
+ for (const name of await listNoteFiles(path33.join(this.cwd, MEMORY_DIR))) {
11456
12470
  names.set(name.toLowerCase(), name);
11457
12471
  }
11458
12472
  for (const rel of this.files.keys()) {
@@ -11495,7 +12509,7 @@ var PlanBuilder = class {
11495
12509
  continue;
11496
12510
  }
11497
12511
  const target = await realpath2(absolute);
11498
- const info = await stat15(target);
12512
+ const info = await stat17(target);
11499
12513
  if (info.nlink > 1) {
11500
12514
  throw new MemoryBridgeError(serverText("memoryHardLinks", { file: file.rel }));
11501
12515
  }
@@ -11514,7 +12528,7 @@ var PlanBuilder = class {
11514
12528
  );
11515
12529
  if (pending.length === 0) return;
11516
12530
  if (pending.some((file) => file.rel.startsWith(`${MEMORY_DIR}/`))) {
11517
- await mkdir5(path30.join(this.cwd, MEMORY_DIR), { recursive: true });
12531
+ await mkdir7(path33.join(this.cwd, MEMORY_DIR), { recursive: true });
11518
12532
  }
11519
12533
  for (const file of pending) {
11520
12534
  if (file.target === null) throw new Error(`Unvalidated plan: ${file.rel}`);
@@ -11625,7 +12639,7 @@ async function planNativeImport(plan, cwd, home) {
11625
12639
  const local = await plan.noteNames();
11626
12640
  for (const name of await listNoteFiles(folder)) {
11627
12641
  const key = name.toLowerCase();
11628
- const content = await readIfFile(path30.join(folder, name), name);
12642
+ const content = await readIfFile(path33.join(folder, name), name);
11629
12643
  if (content === null) continue;
11630
12644
  const already = chosen.get(key);
11631
12645
  if (already !== void 0) {
@@ -11639,7 +12653,7 @@ async function planNativeImport(plan, cwd, home) {
11639
12653
  if (existing !== null && !existing.equals(content)) skipped.push(name);
11640
12654
  continue;
11641
12655
  }
11642
- plan.put(`${MEMORY_DIR}/${name}`, null, content, "copy", "", path30.join(folder, name));
12656
+ plan.put(`${MEMORY_DIR}/${name}`, null, content, "copy", "", path33.join(folder, name));
11643
12657
  copied.push(name);
11644
12658
  }
11645
12659
  }
@@ -11649,7 +12663,7 @@ async function planNativeImport(plan, cwd, home) {
11649
12663
  const known = new Set(linkTargets(index.toString("utf8")));
11650
12664
  const additions = [];
11651
12665
  for (const folder of folders) {
11652
- const nativeIndex = await readIfFile(path30.join(folder, MEMORY_INDEX_FILE), MEMORY_INDEX_FILE);
12666
+ const nativeIndex = await readIfFile(path33.join(folder, MEMORY_INDEX_FILE), MEMORY_INDEX_FILE);
11653
12667
  if (nativeIndex === null) continue;
11654
12668
  for (const line of nativeIndex.toString("utf8").replace(/^\uFEFF/, "").split(/\r?\n/)) {
11655
12669
  const target = linkTargets(line)[0];
@@ -11874,7 +12888,7 @@ async function describeNotes(folder, names) {
11874
12888
  const stated = [];
11875
12889
  for (const name of names) {
11876
12890
  try {
11877
- const info = await stat15(path30.join(folder, name));
12891
+ const info = await stat17(path33.join(folder, name));
11878
12892
  if (info.isFile()) stated.push({ name, sizeBytes: info.size, modifiedAt: info.mtimeMs });
11879
12893
  } catch {
11880
12894
  }
@@ -11882,7 +12896,7 @@ async function describeNotes(folder, names) {
11882
12896
  stated.sort((a, b) => b.modifiedAt - a.modifiedAt || a.name.localeCompare(b.name));
11883
12897
  const notes = [];
11884
12898
  for (const item of stated.slice(0, MAX_LISTED_NOTES)) {
11885
- const head = await readHead(path30.join(folder, item.name));
12899
+ const head = await readHead(path33.join(folder, item.name));
11886
12900
  notes.push({ ...item, ...describeNoteText(head, item.name) });
11887
12901
  }
11888
12902
  return notes;
@@ -11940,28 +12954,28 @@ async function countNativeMemory(cwd, home, sharedNames) {
11940
12954
  return { available: names.size, pending };
11941
12955
  }
11942
12956
  async function nativeMemoryFolders(cwd, home) {
11943
- const root = path30.join(home, ".claude", "projects");
12957
+ const root = path33.join(home, ".claude", "projects");
11944
12958
  const slug = projectSlugFor(cwd).toLowerCase();
11945
12959
  try {
11946
12960
  const entries = await readdir9(root, { withFileTypes: true });
11947
- return entries.filter((entry) => entry.isDirectory() && entry.name.toLowerCase() === slug).map((entry) => entry.name).sort().map((name) => path30.join(root, name, "memory"));
12961
+ return entries.filter((entry) => entry.isDirectory() && entry.name.toLowerCase() === slug).map((entry) => entry.name).sort().map((name) => path33.join(root, name, "memory"));
11948
12962
  } catch {
11949
12963
  return [];
11950
12964
  }
11951
12965
  }
11952
12966
  function buildGlobalFragments(home) {
11953
12967
  const windows = process.platform === "win32";
11954
- const globalFile = path30.join(home, ...GLOBAL_MEMORY_RELATIVE.split("/"));
12968
+ const globalFile = path33.join(home, ...GLOBAL_MEMORY_RELATIVE.split("/"));
11955
12969
  const hardLink = (link) => windows ? `mklink /H "${link}" "${globalFile}"` : `ln "${globalFile}" "${link}"`;
11956
12970
  const hardLinkNote = windows ? serverText("memoryNoteHardLinkCmd", { path: GLOBAL_MEMORY_RELATIVE }) : serverText("memoryNoteHardLink", { path: GLOBAL_MEMORY_RELATIVE });
11957
- const codexTarget = path30.join(home, ".codex", "AGENTS.md");
11958
- const geminiTarget = path30.join(home, ".gemini", "GEMINI.md");
12971
+ const codexTarget = path33.join(home, ".codex", "AGENTS.md");
12972
+ const geminiTarget = path33.join(home, ".gemini", "GEMINI.md");
11959
12973
  const homeSlashes = home.replace(/\\/g, "/").replace(/\/+$/, "");
11960
12974
  return [
11961
12975
  {
11962
12976
  agent: "claude-code",
11963
12977
  label: MEMORY_AGENT_LABELS["claude-code"],
11964
- target: path30.join(home, ".claude", "CLAUDE.md"),
12978
+ target: path33.join(home, ".claude", "CLAUDE.md"),
11965
12979
  kind: "line",
11966
12980
  text: `@~/${GLOBAL_MEMORY_RELATIVE}`,
11967
12981
  note: serverText("memoryNoteClaude")
@@ -11985,7 +12999,7 @@ function buildGlobalFragments(home) {
11985
12999
  {
11986
13000
  agent: "opencode",
11987
13001
  label: MEMORY_AGENT_LABELS.opencode,
11988
- target: path30.join(home, ".config", "opencode", "opencode.json"),
13002
+ target: path33.join(home, ".config", "opencode", "opencode.json"),
11989
13003
  kind: "json",
11990
13004
  text: `"instructions": ["${homeSlashes}/${GLOBAL_MEMORY_RELATIVE}"]`,
11991
13005
  note: serverText("memoryNoteOpenCode")
@@ -11994,7 +13008,7 @@ function buildGlobalFragments(home) {
11994
13008
  }
11995
13009
  function runGit2(cwd, args) {
11996
13010
  return new Promise((resolve) => {
11997
- execFile4(
13011
+ execFile5(
11998
13012
  "git",
11999
13013
  ["-c", "core.quotepath=false", "--no-optional-locks", ...args],
12000
13014
  { cwd, timeout: GIT_TIMEOUT_MS2, windowsHide: true, encoding: "utf8", maxBuffer: 1024 * 1024 },
@@ -12033,17 +13047,17 @@ async function readGit(cwd) {
12033
13047
  if (!toplevel || !gitDir || !commonDir) return fail(serverText("gitUnexpected", { command: "rev-parse" }));
12034
13048
  const ignored = await checkIgnored(cwd, INDEX_REL);
12035
13049
  if (typeof ignored !== "boolean") return fail(ignored);
12036
- const absoluteGitDir = path30.resolve(cwd, gitDir);
12037
- const absoluteCommonDir = path30.resolve(cwd, commonDir);
13050
+ const absoluteGitDir = path33.resolve(cwd, gitDir);
13051
+ const absoluteCommonDir = path33.resolve(cwd, commonDir);
12038
13052
  let worktree = null;
12039
13053
  let main2 = null;
12040
13054
  if (!await samePath(absoluteGitDir, absoluteCommonDir)) {
12041
- if (path30.basename(absoluteCommonDir).toLowerCase() === ".git") {
12042
- const mainRoot = path30.dirname(absoluteCommonDir);
12043
- const inner = path30.relative(path30.resolve(toplevel), path30.resolve(cwd));
12044
- const sameFolder = inner.length === 0 || inner.startsWith("..") || path30.isAbsolute(inner);
12045
- const mainPath = sameFolder ? mainRoot : path30.join(mainRoot, inner);
12046
- const memoryRel = sameFolder ? MEMORY_DIR : [...inner.split(path30.sep), MEMORY_DIR].join("/");
13055
+ if (path33.basename(absoluteCommonDir).toLowerCase() === ".git") {
13056
+ const mainRoot = path33.dirname(absoluteCommonDir);
13057
+ const inner = path33.relative(path33.resolve(toplevel), path33.resolve(cwd));
13058
+ const sameFolder = inner.length === 0 || inner.startsWith("..") || path33.isAbsolute(inner);
13059
+ const mainPath = sameFolder ? mainRoot : path33.join(mainRoot, inner);
13060
+ const memoryRel = sameFolder ? MEMORY_DIR : [...inner.split(path33.sep), MEMORY_DIR].join("/");
12047
13061
  main2 = { root: mainRoot, memoryRel };
12048
13062
  worktree = {
12049
13063
  mainPath,
@@ -12068,7 +13082,7 @@ async function samePath(a, b) {
12068
13082
  try {
12069
13083
  return await realpath2(value);
12070
13084
  } catch {
12071
- return path30.resolve(value);
13085
+ return path33.resolve(value);
12072
13086
  }
12073
13087
  };
12074
13088
  const [left, right] = await Promise.all([resolve(a), resolve(b)]);
@@ -12110,9 +13124,9 @@ async function listNoteFiles(folder) {
12110
13124
  }
12111
13125
  async function readIfFile(absolute, label) {
12112
13126
  try {
12113
- const info = await stat15(absolute);
13127
+ const info = await stat17(absolute);
12114
13128
  if (!info.isFile()) throw new MemoryBridgeError(serverText("memoryNotAFile", { label }));
12115
- return await readFile10(absolute);
13129
+ return await readFile11(absolute);
12116
13130
  } catch (error) {
12117
13131
  if (isNotFound(error)) return null;
12118
13132
  throw error;
@@ -12134,14 +13148,14 @@ async function readHead(absolute) {
12134
13148
  }
12135
13149
  async function isFile(absolute) {
12136
13150
  try {
12137
- return (await stat15(absolute)).isFile();
13151
+ return (await stat17(absolute)).isFile();
12138
13152
  } catch {
12139
13153
  return false;
12140
13154
  }
12141
13155
  }
12142
13156
  async function isDirectory2(absolute) {
12143
13157
  try {
12144
- return (await stat15(absolute)).isDirectory();
13158
+ return (await stat17(absolute)).isDirectory();
12145
13159
  } catch {
12146
13160
  return false;
12147
13161
  }
@@ -12159,7 +13173,7 @@ async function writeAtomic(target, content, mustNotExist) {
12159
13173
  await handle.close();
12160
13174
  }
12161
13175
  if (mustNotExist && await exists2(target)) {
12162
- throw new MemoryBridgeError(serverText("memoryAppeared", { file: path30.basename(target) }));
13176
+ throw new MemoryBridgeError(serverText("memoryAppeared", { file: path33.basename(target) }));
12163
13177
  }
12164
13178
  await renameWithRetry(temporary, target);
12165
13179
  } catch (error) {
@@ -12169,10 +13183,10 @@ async function writeAtomic(target, content, mustNotExist) {
12169
13183
  }
12170
13184
  async function createTemporary(target) {
12171
13185
  for (let attempt = 1; ; attempt += 1) {
12172
- const suffix = randomBytes3(6).toString("hex");
12173
- const temporary = path30.join(
12174
- path30.dirname(target),
12175
- `.${path30.basename(target)}.${process.pid}.${suffix}.tmp`
13186
+ const suffix = randomBytes4(6).toString("hex");
13187
+ const temporary = path33.join(
13188
+ path33.dirname(target),
13189
+ `.${path33.basename(target)}.${process.pid}.${suffix}.tmp`
12176
13190
  );
12177
13191
  try {
12178
13192
  return { temporary, handle: await open6(temporary, "wx") };
@@ -12184,7 +13198,7 @@ async function createTemporary(target) {
12184
13198
  }
12185
13199
  async function exists2(absolute) {
12186
13200
  try {
12187
- await stat15(absolute);
13201
+ await stat17(absolute);
12188
13202
  return true;
12189
13203
  } catch {
12190
13204
  return false;
@@ -12193,7 +13207,7 @@ async function exists2(absolute) {
12193
13207
  async function renameWithRetry(from, to) {
12194
13208
  for (let attempt = 1; ; attempt += 1) {
12195
13209
  try {
12196
- await rename3(from, to);
13210
+ await rename4(from, to);
12197
13211
  return;
12198
13212
  } catch (error) {
12199
13213
  const code = error.code;
@@ -12410,13 +13424,13 @@ var MemoryHub = class extends EventEmitter3 {
12410
13424
  }
12411
13425
  };
12412
13426
  function keyFor(cwd) {
12413
- const resolved = path31.resolve(cwd);
13427
+ const resolved = path34.resolve(cwd);
12414
13428
  return process.platform === "win32" ? resolved.toLowerCase() : resolved;
12415
13429
  }
12416
13430
  function isRelevantPath(cwd, candidate) {
12417
- const relative = path31.relative(cwd, candidate);
13431
+ const relative = path34.relative(cwd, candidate);
12418
13432
  if (relative.length === 0) return true;
12419
- if (relative.startsWith("..") || path31.isAbsolute(relative)) return false;
13433
+ if (relative.startsWith("..") || path34.isAbsolute(relative)) return false;
12420
13434
  const same = (a, b) => process.platform === "win32" ? a.toLowerCase() === b.toLowerCase() : a === b;
12421
13435
  const parts = relative.split(/[\\/]/);
12422
13436
  const [first = "", second = "", third = ""] = parts;
@@ -12433,10 +13447,10 @@ function isRelevantPath(cwd, candidate) {
12433
13447
  }
12434
13448
 
12435
13449
  // packages/server/src/paste-store.ts
12436
- import { createHash, randomUUID as randomUUID2 } from "node:crypto";
12437
- import { mkdir as mkdir6, readdir as readdir10, rm as rm2, stat as stat16, writeFile as writeFile3 } from "node:fs/promises";
13450
+ import { createHash as createHash3, randomUUID as randomUUID3 } from "node:crypto";
13451
+ import { mkdir as mkdir8, readdir as readdir10, rm as rm3, stat as stat18, writeFile as writeFile4 } from "node:fs/promises";
12438
13452
  import { tmpdir as tmpdir4 } from "node:os";
12439
- import path32 from "node:path";
13453
+ import path35 from "node:path";
12440
13454
 
12441
13455
  // packages/server/src/vault/markdown.ts
12442
13456
  var pad2 = (value) => String(value).padStart(2, "0");
@@ -12951,8 +13965,8 @@ function buildContinuationMessage(input) {
12951
13965
  }
12952
13966
  return sanitizeForPaste(lines.join("\n"));
12953
13967
  }
12954
- function transcriptReferenceFor(path47, style) {
12955
- return style === "at-quoted" ? fileReference(path47, "at-quoted") : `"${path47}"`;
13968
+ function transcriptReferenceFor(path50, style) {
13969
+ return style === "at-quoted" ? fileReference(path50, "at-quoted") : `"${path50}"`;
12956
13970
  }
12957
13971
  function planTranscript(input) {
12958
13972
  const turns = splitTurns(input.events);
@@ -13127,7 +14141,7 @@ var PasteStore = class {
13127
14141
  root;
13128
14142
  counter = 0;
13129
14143
  constructor(root) {
13130
- this.root = root ?? path32.join(tmpdir4(), "agent-workbench", "pasted");
14144
+ this.root = root ?? path35.join(tmpdir4(), "agent-workbench", "pasted");
13131
14145
  }
13132
14146
  /**
13133
14147
  * Guarda una imagen y devuelve su ruta.
@@ -13154,12 +14168,12 @@ var PasteStore = class {
13154
14168
  if (signature === null) {
13155
14169
  throw new PasteImageError(serverText("imageUnknownFormat"));
13156
14170
  }
13157
- const directory = path32.join(this.root, safeSegment(terminalId));
13158
- await mkdir6(directory, { recursive: true, mode: DIRECTORY_MODE });
14171
+ const directory = path35.join(this.root, safeSegment(terminalId));
14172
+ await mkdir8(directory, { recursive: true, mode: DIRECTORY_MODE });
13159
14173
  this.counter += 1;
13160
- const name = `pegada-${this.counter}-${randomUUID2().slice(0, 8)}.${signature.ext}`;
13161
- const file = path32.join(directory, name);
13162
- await writeFile3(file, bytes, { mode: FILE_MODE });
14174
+ const name = `pegada-${this.counter}-${randomUUID3().slice(0, 8)}.${signature.ext}`;
14175
+ const file = path35.join(directory, name);
14176
+ await writeFile4(file, bytes, { mode: FILE_MODE });
13163
14177
  return { path: file, bytes: bytes.length };
13164
14178
  }
13165
14179
  /**
@@ -13176,12 +14190,12 @@ var PasteStore = class {
13176
14190
  if (bytes.length > MAX_TEXT_BYTES) {
13177
14191
  throw new PasteTextError(`The text is ${bytes.length} bytes; the maximum is ${MAX_TEXT_BYTES}.`);
13178
14192
  }
13179
- const directory = path32.join(this.root, safeSegment(terminalId));
13180
- await mkdir6(directory, { recursive: true, mode: DIRECTORY_MODE });
14193
+ const directory = path35.join(this.root, safeSegment(terminalId));
14194
+ await mkdir8(directory, { recursive: true, mode: DIRECTORY_MODE });
13181
14195
  this.counter += 1;
13182
- const name = `${baseName}-${this.counter}-${randomUUID2().slice(0, 8)}.md`;
13183
- const file = path32.join(directory, name);
13184
- await writeFile3(file, bytes, { mode: FILE_MODE });
14196
+ const name = `${baseName}-${this.counter}-${randomUUID3().slice(0, 8)}.md`;
14197
+ const file = path35.join(directory, name);
14198
+ await writeFile4(file, bytes, { mode: FILE_MODE });
13185
14199
  return { path: file, bytes: bytes.length };
13186
14200
  }
13187
14201
  /**
@@ -13210,17 +14224,17 @@ var PasteStore = class {
13210
14224
  })
13211
14225
  );
13212
14226
  }
13213
- const directory = path32.join(this.root, safeSegment(terminalId));
13214
- await mkdir6(directory, { recursive: true, mode: DIRECTORY_MODE });
14227
+ const directory = path35.join(this.root, safeSegment(terminalId));
14228
+ await mkdir8(directory, { recursive: true, mode: DIRECTORY_MODE });
13215
14229
  this.counter += 1;
13216
- const name = `adjunto-${this.counter}-${randomUUID2().slice(0, 8)}-${safe}`;
13217
- const file = path32.join(directory, name);
13218
- await writeFile3(file, bytes, { mode: FILE_MODE });
14230
+ const name = `adjunto-${this.counter}-${randomUUID3().slice(0, 8)}-${safe}`;
14231
+ const file = path35.join(directory, name);
14232
+ await writeFile4(file, bytes, { mode: FILE_MODE });
13219
14233
  return { path: file, bytes: bytes.length, name };
13220
14234
  }
13221
14235
  /** Borra lo de una pestana. Se llama al cerrarla. */
13222
14236
  async clearTerminal(terminalId) {
13223
- await rm2(path32.join(this.root, safeSegment(terminalId)), {
14237
+ await rm3(path35.join(this.root, safeSegment(terminalId)), {
13224
14238
  recursive: true,
13225
14239
  force: true
13226
14240
  }).catch(() => {
@@ -13242,11 +14256,11 @@ var PasteStore = class {
13242
14256
  }
13243
14257
  await Promise.all(
13244
14258
  entries.map(async (entry) => {
13245
- const full = path32.join(this.root, entry);
14259
+ const full = path35.join(this.root, entry);
13246
14260
  try {
13247
- const info = await stat16(full);
14261
+ const info = await stat18(full);
13248
14262
  if (now - info.mtimeMs > STALE_AFTER_MS) {
13249
- await rm2(full, { recursive: true, force: true });
14263
+ await rm3(full, { recursive: true, force: true });
13250
14264
  }
13251
14265
  } catch {
13252
14266
  }
@@ -13256,13 +14270,13 @@ var PasteStore = class {
13256
14270
  };
13257
14271
  function safeSegment(value) {
13258
14272
  const clean = value.replace(/[^a-zA-Z0-9_-]/g, "");
13259
- return clean.length > 0 ? clean : createHash("sha1").update(value).digest("hex").slice(0, 16);
14273
+ return clean.length > 0 ? clean : createHash3("sha1").update(value).digest("hex").slice(0, 16);
13260
14274
  }
13261
14275
 
13262
14276
  // packages/server/src/session-index.ts
13263
14277
  import { EventEmitter as EventEmitter4 } from "node:events";
13264
- import { mkdir as mkdir7, readFile as readFile11, stat as stat17, writeFile as writeFile4 } from "node:fs/promises";
13265
- import path33 from "node:path";
14278
+ import { mkdir as mkdir9, readFile as readFile12, stat as stat19, writeFile as writeFile5 } from "node:fs/promises";
14279
+ import path36 from "node:path";
13266
14280
  var CACHE_VERSION = 4;
13267
14281
  var vaultPairKey = (agent, sessionId) => `${agent}/${sessionId}`;
13268
14282
  function vaultOnlySessions(natives, vault, listState, ready) {
@@ -13311,7 +14325,7 @@ function cacheKey(agent, ref) {
13311
14325
  }
13312
14326
  async function pathExists3(candidate) {
13313
14327
  try {
13314
- await stat17(candidate);
14328
+ await stat19(candidate);
13315
14329
  return true;
13316
14330
  } catch {
13317
14331
  return false;
@@ -13361,7 +14375,7 @@ function migrateEntryV3(filePath, value) {
13361
14375
  return {
13362
14376
  agent: "claude-code",
13363
14377
  ref: filePath,
13364
- group: path33.basename(path33.dirname(filePath)),
14378
+ group: path36.basename(path36.dirname(filePath)),
13365
14379
  mtimeMs,
13366
14380
  sizeBytes,
13367
14381
  cwd,
@@ -13626,7 +14640,7 @@ var SessionIndex = class extends EventEmitter4 {
13626
14640
  }
13627
14641
  async loadCache() {
13628
14642
  try {
13629
- const raw = await readFile11(sessionIndexCachePath(), "utf8");
14643
+ const raw = await readFile12(sessionIndexCachePath(), "utf8");
13630
14644
  const parsed = JSON.parse(raw);
13631
14645
  const migrated = migrateIndexCache(parsed);
13632
14646
  if (migrated === null) return;
@@ -13639,8 +14653,8 @@ var SessionIndex = class extends EventEmitter4 {
13639
14653
  if (!this.cacheDirty) return;
13640
14654
  this.cacheDirty = false;
13641
14655
  try {
13642
- await mkdir7(appConfigDir(), { recursive: true });
13643
- await writeFile4(sessionIndexCachePath(), JSON.stringify(this.cache), "utf8");
14656
+ await mkdir9(appConfigDir(), { recursive: true });
14657
+ await writeFile5(sessionIndexCachePath(), JSON.stringify(this.cache), "utf8");
13644
14658
  } catch (error) {
13645
14659
  console.warn("[index] couldn't save the cache:", error);
13646
14660
  }
@@ -13816,200 +14830,43 @@ var SessionIndex = class extends EventEmitter4 {
13816
14830
  const history = this.agents.adapter(agent).history;
13817
14831
  let refs;
13818
14832
  try {
13819
- refs = await history.changedRefs(filePath);
13820
- } catch {
13821
- refs = null;
13822
- }
13823
- if (refs === null || refs.length === 0) return;
13824
- let changed = false;
13825
- for (const ref of refs) {
13826
- const key = cacheKey(agent, ref);
13827
- const cachedBefore = this.cache.entries[key];
13828
- if (cachedBefore !== void 0) {
13829
- delete this.cache.entries[key];
13830
- this.cacheDirty = true;
13831
- }
13832
- let indexed;
13833
- try {
13834
- const item = await history.item(ref);
13835
- indexed = item === null ? null : await this.readOrThrow(agent, history, item);
13836
- } catch {
13837
- if (cachedBefore !== void 0 && !(key in this.cache.entries)) this.cache.entries[key] = cachedBefore;
13838
- continue;
13839
- }
13840
- if (indexed === null) {
13841
- if (this.sessions.delete(key)) changed = true;
13842
- continue;
13843
- }
13844
- if (indexed.cwd !== null) this.cwdExists.set(indexed.cwd, await pathExists3(indexed.cwd));
13845
- this.sessions.set(key, indexed);
13846
- changed = true;
13847
- }
13848
- if (changed) this.emitProjects();
13849
- await this.saveCache();
13850
- }
13851
- };
13852
-
13853
- // packages/server/src/settings-store.ts
13854
- import { mkdir as mkdir9, readFile as readFile12 } from "node:fs/promises";
13855
- import path35 from "node:path";
13856
-
13857
- // packages/server/src/vault/write.ts
13858
- import { randomBytes as randomBytes4 } from "node:crypto";
13859
- import { mkdir as mkdir8, rename as rename4, rm as rm3, stat as stat18, writeFile as writeFile5 } from "node:fs/promises";
13860
-
13861
- // packages/server/src/vault/paths.ts
13862
- import { createHash as createHash2 } from "node:crypto";
13863
- import path34 from "node:path";
13864
- function defaultVaultDir() {
13865
- return path34.join(appConfigDir(), "vault");
13866
- }
13867
- function vaultMarkerFile(dir) {
13868
- return path34.join(dir, "vault.json");
13869
- }
13870
- function sessionsRoot2(dir) {
13871
- return path34.join(dir, "sessions");
13872
- }
13873
- function isSafeSessionId(sessionId) {
13874
- return isVaultSessionId(sessionId);
13875
- }
13876
- function isSessionAgentId(value) {
13877
- return SESSION_AGENT_IDS.includes(value);
13878
- }
13879
- function assertSessionKey(agent, sessionId) {
13880
- if (!isSessionAgentId(agent)) throw new Error(`Unknown source for the local copy: ${JSON.stringify(agent)}`);
13881
- if (!isSafeSessionId(sessionId)) throw new Error(`Session id that isn't copied: ${JSON.stringify(sessionId)}`);
13882
- }
13883
- function agentSessionsDir(dir, agent) {
13884
- if (!isSessionAgentId(agent)) throw new Error(`Unknown source for the local copy: ${JSON.stringify(agent)}`);
13885
- return path34.join(sessionsRoot2(dir), agent);
13886
- }
13887
- function sessionFile(dir, agent, sessionId) {
13888
- assertSessionKey(agent, sessionId);
13889
- return path34.join(sessionsRoot2(dir), agent, `${sessionId}.jsonl`);
13890
- }
13891
- function assetsDir(dir, agent, sessionId) {
13892
- assertSessionKey(agent, sessionId);
13893
- return path34.join(sessionsRoot2(dir), agent, `${sessionId}.assets`);
13894
- }
13895
- function assetFile(dir, agent, sessionId, asset) {
13896
- if (!VAULT_ASSET_NAME_PATTERN.test(asset)) throw new Error(`Invalid asset name: ${JSON.stringify(asset)}`);
13897
- return path34.join(assetsDir(dir, agent, sessionId), asset);
13898
- }
13899
- function projectFolderName(cwd, platform) {
13900
- const hash = createHash2("sha256").update(normalizeCwdKey(cwd, platform)).digest("hex").slice(0, 8);
13901
- const base = (platform === "win32" ? path34.win32 : path34.posix).basename(cwd);
13902
- const name = base.length === 0 ? "proyecto" : sanitizeFileName(base, 60);
13903
- return `${name}-${hash}`;
13904
- }
13905
- function memoryDir(dir, cwd, platform) {
13906
- return path34.join(dir, "memory", projectFolderName(cwd, platform));
13907
- }
13908
- function projectExportDir(dir, cwd, platform) {
13909
- return path34.join(dir, "export", projectFolderName(cwd, platform));
13910
- }
13911
- function projectExportDirFor(dir, project, platform) {
13912
- if (project.cwd.length > 0) return projectExportDir(dir, project.cwd, platform);
13913
- const hash = createHash2("sha256").update(project.key).digest("hex").slice(0, 8);
13914
- const name = project.fallbackName.length > 0 ? sanitizeFileName(project.fallbackName, 60) : "proyecto";
13915
- return path34.join(dir, "export", `${name}-${hash}`);
13916
- }
13917
- function sessionExportFile(dir, agent, sessionId) {
13918
- assertSessionKey(agent, sessionId);
13919
- return path34.join(dir, "export", "sesiones", `${agent}-${sessionId}.md`);
13920
- }
13921
- var RESERVED_WINDOWS_NAME = /^(con|prn|aux|nul|com[1-9]|lpt[1-9])$/i;
13922
- function sanitizeFileName(text, max = 80) {
13923
- const limit = Math.max(1, Math.floor(max));
13924
- const kept = text.normalize("NFC").replace(/[^\p{L}\p{M}\p{N} ._-]/gu, "_");
13925
- const trimEnds = (value) => value.replace(/^ +/u, "").replace(/[. ]+$/u, "");
13926
- let name = trimEnds(Array.from(trimEnds(kept)).slice(0, limit).join(""));
13927
- if (name.length === 0) return "_";
13928
- const stem = (name.split(".")[0] ?? "").replace(/ +$/u, "");
13929
- if (RESERVED_WINDOWS_NAME.test(stem)) {
13930
- name = `_${Array.from(name).slice(0, limit - 1).join("")}`;
13931
- name = name.replace(/[. ]+$/u, "");
13932
- }
13933
- return name;
13934
- }
13935
-
13936
- // packages/server/src/vault/write.ts
13937
- var RENAME_RETRIES = 3;
13938
- var RENAME_RETRY_MS = 100;
13939
- var DEFAULT_WRITE_DEPS = {
13940
- rename: rename4,
13941
- platform: process.platform,
13942
- delay: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
13943
- now: () => Date.now()
13944
- };
13945
- function temporaryPathFor(target) {
13946
- return `${target}.${process.pid}.${randomBytes4(6).toString("hex")}.tmp`;
13947
- }
13948
- function isRetryableRename(error, platform) {
13949
- if (platform !== "win32") return false;
13950
- const code = error?.code;
13951
- return code === "EPERM" || code === "EBUSY";
13952
- }
13953
- async function writeFileAtomic(target, data, deps = DEFAULT_WRITE_DEPS) {
13954
- const temporary = temporaryPathFor(target);
13955
- try {
13956
- await writeFile5(temporary, data);
13957
- for (let attempt = 0; ; attempt += 1) {
14833
+ refs = await history.changedRefs(filePath);
14834
+ } catch {
14835
+ refs = null;
14836
+ }
14837
+ if (refs === null || refs.length === 0) return;
14838
+ let changed = false;
14839
+ for (const ref of refs) {
14840
+ const key = cacheKey(agent, ref);
14841
+ const cachedBefore = this.cache.entries[key];
14842
+ if (cachedBefore !== void 0) {
14843
+ delete this.cache.entries[key];
14844
+ this.cacheDirty = true;
14845
+ }
14846
+ let indexed;
13958
14847
  try {
13959
- await deps.rename(temporary, target);
13960
- return;
13961
- } catch (error) {
13962
- if (attempt >= RENAME_RETRIES || !isRetryableRename(error, deps.platform)) throw error;
13963
- await deps.delay(RENAME_RETRY_MS);
14848
+ const item = await history.item(ref);
14849
+ indexed = item === null ? null : await this.readOrThrow(agent, history, item);
14850
+ } catch {
14851
+ if (cachedBefore !== void 0 && !(key in this.cache.entries)) this.cache.entries[key] = cachedBefore;
14852
+ continue;
13964
14853
  }
14854
+ if (indexed === null) {
14855
+ if (this.sessions.delete(key)) changed = true;
14856
+ continue;
14857
+ }
14858
+ if (indexed.cwd !== null) this.cwdExists.set(indexed.cwd, await pathExists3(indexed.cwd));
14859
+ this.sessions.set(key, indexed);
14860
+ changed = true;
13965
14861
  }
13966
- } catch (error) {
13967
- await rm3(temporary, { force: true }).catch(() => void 0);
13968
- throw error;
13969
- }
13970
- }
13971
- async function ensureVaultMarker(dir, deps = DEFAULT_WRITE_DEPS) {
13972
- const marker = vaultMarkerFile(dir);
13973
- try {
13974
- await stat18(marker);
13975
- return;
13976
- } catch {
13977
- }
13978
- await mkdir8(dir, { recursive: true });
13979
- await writeFileAtomic(marker, `${JSON.stringify({ format: VAULT_FORMAT, createdAt: deps.now() })}
13980
- `, deps);
13981
- }
13982
- async function assetPresent(file, size) {
13983
- try {
13984
- const info = await stat18(file);
13985
- return info.isFile() && info.size === size;
13986
- } catch {
13987
- return false;
13988
- }
13989
- }
13990
- async function writeSessionFile(dir, serialized, deps = DEFAULT_WRITE_DEPS) {
13991
- const { agent, sessionId } = serialized.header;
13992
- const file = sessionFile(dir, agent, sessionId);
13993
- await ensureVaultMarker(dir, deps);
13994
- await mkdir8(agentSessionsDir(dir, agent), { recursive: true });
13995
- let assetsWritten = 0;
13996
- let assetBytesWritten = 0;
13997
- if (serialized.assets.size > 0) {
13998
- await mkdir8(assetsDir(dir, agent, sessionId), { recursive: true });
13999
- for (const [name, bytes] of serialized.assets) {
14000
- const target = assetFile(dir, agent, sessionId, name);
14001
- if (await assetPresent(target, bytes.length)) continue;
14002
- await writeFileAtomic(target, bytes, deps);
14003
- assetsWritten += 1;
14004
- assetBytesWritten += bytes.length;
14005
- }
14862
+ if (changed) this.emitProjects();
14863
+ await this.saveCache();
14006
14864
  }
14007
- const text = Buffer.from(serialized.text, "utf8");
14008
- await writeFileAtomic(file, text, deps);
14009
- return { file, bytes: text.length, assetsWritten, assetBytesWritten };
14010
- }
14865
+ };
14011
14866
 
14012
14867
  // packages/server/src/settings-store.ts
14868
+ import { mkdir as mkdir10, readFile as readFile13, stat as stat20 } from "node:fs/promises";
14869
+ import path37 from "node:path";
14013
14870
  var SETTINGS_VERSION = 1;
14014
14871
  var VAULT_TOOL_RESULT_DEFAULT_CHARS = 64e3;
14015
14872
  var VAULT_TOOL_RESULT_MIN_CHARS = 4e3;
@@ -14017,13 +14874,14 @@ var VAULT_TOOL_RESULT_MAX_CHARS = 1e6;
14017
14874
  function defaultAppSettings() {
14018
14875
  return {
14019
14876
  version: SETTINGS_VERSION,
14020
- vault: { enabled: false, dir: null, toolResultMaxChars: VAULT_TOOL_RESULT_DEFAULT_CHARS }
14877
+ vault: { enabled: false, dir: null, toolResultMaxChars: VAULT_TOOL_RESULT_DEFAULT_CHARS },
14878
+ remote: { enabled: false, port: REMOTE_DEFAULT_PORT }
14021
14879
  };
14022
14880
  }
14023
14881
  function isAbsoluteDir(dir, platform) {
14024
14882
  if (dir.length === 0 || dir.includes("\0")) return false;
14025
14883
  if (platform === "win32") return /^[A-Za-z]:[\\/]/.test(dir) || /^\\\\[^\\/]+[\\/][^\\/]+/.test(dir);
14026
- return path35.posix.isAbsolute(dir);
14884
+ return path37.posix.isAbsolute(dir);
14027
14885
  }
14028
14886
  function clampToolResultMaxChars(value) {
14029
14887
  if (typeof value !== "number" || !Number.isFinite(value)) return VAULT_TOOL_RESULT_DEFAULT_CHARS;
@@ -14033,6 +14891,11 @@ function parseAppSettings(value, platform, warn = () => void 0) {
14033
14891
  const record = asRecord(value);
14034
14892
  if (record === null || record["version"] !== SETTINGS_VERSION) return null;
14035
14893
  const settings2 = defaultAppSettings();
14894
+ const remote = asRecord(record["remote"]);
14895
+ if (remote !== null) {
14896
+ settings2.remote.enabled = asBoolean(remote["enabled"]) ?? false;
14897
+ if (isRemotePort(remote["port"])) settings2.remote.port = remote["port"];
14898
+ }
14036
14899
  const vault = asRecord(record["vault"]);
14037
14900
  if (vault === null) return settings2;
14038
14901
  settings2.vault.enabled = asBoolean(vault["enabled"]) ?? false;
@@ -14046,7 +14909,11 @@ function parseAppSettings(value, platform, warn = () => void 0) {
14046
14909
  return settings2;
14047
14910
  }
14048
14911
  function frozen(settings2) {
14049
- return Object.freeze({ ...settings2, vault: Object.freeze({ ...settings2.vault }) });
14912
+ return Object.freeze({
14913
+ ...settings2,
14914
+ vault: Object.freeze({ ...settings2.vault }),
14915
+ remote: Object.freeze({ ...settings2.remote })
14916
+ });
14050
14917
  }
14051
14918
  var SettingsStore = class {
14052
14919
  /** La ruta es inyectable para las pruebas; por defecto, la de la carpeta de configuracion. */
@@ -14065,7 +14932,7 @@ var SettingsStore = class {
14065
14932
  async load() {
14066
14933
  let raw;
14067
14934
  try {
14068
- raw = await readFile12(this.filePath, "utf8");
14935
+ raw = await readFile13(this.filePath, "utf8");
14069
14936
  } catch {
14070
14937
  this.current = frozen(defaultAppSettings());
14071
14938
  return;
@@ -14084,17 +14951,49 @@ var SettingsStore = class {
14084
14951
  }
14085
14952
  this.current = frozen(settings2 ?? defaultAppSettings());
14086
14953
  }
14954
+ /**
14955
+ * Relee **solo** lo del acceso remoto (hito 37). Otra instancia de la app
14956
+ * comparte este archivo, y "apagar" desde una tiene que valer en la que tiene
14957
+ * el puerto; lo de la copia propia no se toca, que cada instancia lo lleva en
14958
+ * memoria como siempre. Sin archivo o ilegible, queda lo que habia.
14959
+ */
14960
+ async reloadRemote() {
14961
+ let parsed;
14962
+ try {
14963
+ parsed = JSON.parse(await readFile13(this.filePath, "utf8"));
14964
+ } catch {
14965
+ return;
14966
+ }
14967
+ const settings2 = parseAppSettings(parsed, this.platform);
14968
+ if (settings2 === null) return;
14969
+ this.current = frozen({ ...this.current, remote: settings2.remote });
14970
+ }
14971
+ /** Una marca que cambia cuando el archivo cambia (fecha y tamano), o '' si no esta. */
14972
+ async stamp() {
14973
+ try {
14974
+ const info = await stat20(this.filePath);
14975
+ return `${info.mtimeMs}:${info.size}`;
14976
+ } catch {
14977
+ return "";
14978
+ }
14979
+ }
14087
14980
  /** Lo ultimo que quedo escrito. Congelado: nadie de afuera lo cambia sin pasar por `update`. */
14088
14981
  get() {
14089
14982
  return this.current;
14090
14983
  }
14091
14984
  /**
14092
- * Aplica un cambio y lo escribe. Lanza si la carpeta no es absoluta o si no
14093
- * se pudo escribir; en los dos casos `get()` sigue dando lo anterior.
14985
+ * Aplica un cambio y lo escribe. Lanza si la carpeta no es absoluta, si el
14986
+ * puerto no sirve o si no se pudo escribir; en todos los casos `get()` sigue
14987
+ * dando lo anterior.
14094
14988
  */
14095
14989
  update(patch) {
14096
14990
  const run = async () => {
14097
- const next = { version: SETTINGS_VERSION, vault: { ...this.current.vault } };
14991
+ if (patch.remote === void 0) await this.reloadRemote();
14992
+ const next = {
14993
+ version: SETTINGS_VERSION,
14994
+ vault: { ...this.current.vault },
14995
+ remote: { ...this.current.remote }
14996
+ };
14098
14997
  const vault = patch.vault ?? {};
14099
14998
  if (vault.enabled !== void 0) next.vault.enabled = vault.enabled;
14100
14999
  if (vault.dir !== void 0) {
@@ -14106,7 +15005,13 @@ var SettingsStore = class {
14106
15005
  if (vault.toolResultMaxChars !== void 0) {
14107
15006
  next.vault.toolResultMaxChars = clampToolResultMaxChars(vault.toolResultMaxChars);
14108
15007
  }
14109
- await mkdir9(path35.dirname(this.filePath), { recursive: true });
15008
+ const remote = patch.remote ?? {};
15009
+ if (remote.enabled !== void 0) next.remote.enabled = remote.enabled;
15010
+ if (remote.port !== void 0) {
15011
+ if (!isRemotePort(remote.port)) throw new Error(`The remote access port isn't valid: ${JSON.stringify(remote.port)}`);
15012
+ next.remote.port = remote.port;
15013
+ }
15014
+ await mkdir10(path37.dirname(this.filePath), { recursive: true });
14110
15015
  await writeFileAtomic(this.filePath, `${JSON.stringify(next, null, 2)}
14111
15016
  `);
14112
15017
  this.current = frozen(next);
@@ -14133,6 +15038,19 @@ function statusLineStartupText(state) {
14133
15038
  return "couldn't read its settings.json";
14134
15039
  }
14135
15040
  }
15041
+ var REMOTE_ACCESS_LABEL = "Remote access";
15042
+ function remoteAccessStartupLine(state, port, pairedDevices) {
15043
+ switch (state) {
15044
+ case "off":
15045
+ return null;
15046
+ case "active":
15047
+ return ` ${REMOTE_ACCESS_LABEL} on, port ${port}, ${pairedDevices} paired device${pairedDevices === 1 ? "" : "s"}`;
15048
+ case "port-busy":
15049
+ return ` ${REMOTE_ACCESS_LABEL} NOT WORKING: port ${port} is busy (another Agent Workbench, or another program)`;
15050
+ case "restart-needed":
15051
+ return ` ${REMOTE_ACCESS_LABEL} on, takes effect after a restart`;
15052
+ }
15053
+ }
14136
15054
  var AVAILABLE_AGENTS_LABEL = "Available CLIs";
14137
15055
  var HISTORY_LABEL = "History";
14138
15056
  function missingText(text) {
@@ -14171,11 +15089,18 @@ function startupAgentLines(agents) {
14171
15089
  }
14172
15090
  return lines;
14173
15091
  }
15092
+ var SUPPORT_LABEL = "Support";
15093
+ function supportStartupLine() {
15094
+ return ` ${SUPPORT_LABEL} If Agent Workbench is useful to you, consider supporting it: ${SUPPORT_URL}`;
15095
+ }
15096
+ function supportStartupLineColored() {
15097
+ return ` \x1B[95m${SUPPORT_LABEL}\x1B[0m \x1B[33mIf Agent Workbench is useful to you, consider supporting it:\x1B[0m \x1B[4;94m${SUPPORT_URL}\x1B[0m`;
15098
+ }
14174
15099
 
14175
15100
  // packages/server/src/notes-store.ts
14176
- import { randomUUID as randomUUID3 } from "node:crypto";
14177
- import { mkdir as mkdir10, readFile as readFile13, rename as rename5, rm as rm4, writeFile as writeFile6 } from "node:fs/promises";
14178
- import path36 from "node:path";
15101
+ import { randomUUID as randomUUID4 } from "node:crypto";
15102
+ import { mkdir as mkdir11, readFile as readFile14, rename as rename5, rm as rm4, writeFile as writeFile6 } from "node:fs/promises";
15103
+ import path38 from "node:path";
14179
15104
  var STATE_VERSION2 = 1;
14180
15105
  var WRITE_DEBOUNCE_MS2 = 500;
14181
15106
  var NotesError = class extends ServerTextError {
@@ -14218,7 +15143,7 @@ var NotesStore = class {
14218
15143
  }
14219
15144
  async load() {
14220
15145
  try {
14221
- const raw = await readFile13(this.statePath, "utf8");
15146
+ const raw = await readFile14(this.statePath, "utf8");
14222
15147
  this.notes = parseState2(raw) ?? [];
14223
15148
  } catch {
14224
15149
  this.notes = [];
@@ -14302,11 +15227,11 @@ var NotesStore = class {
14302
15227
  throw new NotesError(serverText("imageUnknownFormat"));
14303
15228
  }
14304
15229
  const image = {
14305
- imageId: randomUUID3(),
15230
+ imageId: randomUUID4(),
14306
15231
  mediaType: format.mediaType,
14307
15232
  bytes: bytes.length
14308
15233
  };
14309
- await mkdir10(this.imagesDir, { recursive: true });
15234
+ await mkdir11(this.imagesDir, { recursive: true });
14310
15235
  await writeFile6(this.imagePath(image), bytes);
14311
15236
  note.images.push(image);
14312
15237
  note.updatedAt = now;
@@ -14335,7 +15260,7 @@ var NotesStore = class {
14335
15260
  const image = this.findImage(imageId);
14336
15261
  if (image === null) return null;
14337
15262
  try {
14338
- const bytes = await readFile13(this.imagePath(image));
15263
+ const bytes = await readFile14(this.imagePath(image));
14339
15264
  return { mediaType: image.mediaType, data: bytes.toString("base64") };
14340
15265
  } catch {
14341
15266
  return null;
@@ -14355,7 +15280,7 @@ var NotesStore = class {
14355
15280
  const images = [];
14356
15281
  for (const image of note.images) {
14357
15282
  try {
14358
- const bytes = await readFile13(this.imagePath(image));
15283
+ const bytes = await readFile14(this.imagePath(image));
14359
15284
  images.push({ mediaType: image.mediaType, data: bytes.toString("base64") });
14360
15285
  } catch {
14361
15286
  }
@@ -14379,7 +15304,7 @@ var NotesStore = class {
14379
15304
  }
14380
15305
  imagePath(image) {
14381
15306
  const safe = image.imageId.replace(/[^a-zA-Z0-9-]/g, "");
14382
- return path36.join(this.imagesDir, `${safe}.${extensionFor(image.mediaType)}`);
15307
+ return path38.join(this.imagesDir, `${safe}.${extensionFor(image.mediaType)}`);
14383
15308
  }
14384
15309
  async unlinkImage(image) {
14385
15310
  await rm4(this.imagePath(image), { force: true }).catch(() => {
@@ -14397,8 +15322,8 @@ var NotesStore = class {
14397
15322
  async writeNow() {
14398
15323
  this.dirty = false;
14399
15324
  try {
14400
- await mkdir10(path36.dirname(this.statePath), { recursive: true });
14401
- const temporary = path36.join(path36.dirname(this.statePath), `notes.${process.pid}.tmp`);
15325
+ await mkdir11(path38.dirname(this.statePath), { recursive: true });
15326
+ const temporary = path38.join(path38.dirname(this.statePath), `notes.${process.pid}.tmp`);
14402
15327
  await writeFile6(
14403
15328
  temporary,
14404
15329
  JSON.stringify({ version: STATE_VERSION2, notes: this.notes }, null, 2),
@@ -14411,10 +15336,226 @@ var NotesStore = class {
14411
15336
  }
14412
15337
  };
14413
15338
 
15339
+ // packages/server/src/draft-store.ts
15340
+ import { mkdir as mkdir12, readFile as readFile15, rename as rename6, writeFile as writeFile7 } from "node:fs/promises";
15341
+ import path39 from "node:path";
15342
+ var STATE_VERSION3 = 1;
15343
+ var WRITE_DEBOUNCE_MS3 = 500;
15344
+ var MAX_STORED_DRAFTS = 100;
15345
+ function draftOwnerOf(descriptor) {
15346
+ if (descriptor === null || descriptor.kind !== "agent" || descriptor.agent === null) return null;
15347
+ if (descriptor.sessionId.length === 0) return null;
15348
+ return { agent: descriptor.agent, sessionId: descriptor.sessionId };
15349
+ }
15350
+ function workspaceDraftOwners(state) {
15351
+ const owners = state.tabs.map(({ agent, sessionId }) => ({ agent, sessionId }));
15352
+ for (const { raw } of state.foreignTabs) {
15353
+ const agent = raw["agent"];
15354
+ const sessionId = raw["sessionId"];
15355
+ if (typeof agent === "string" && typeof sessionId === "string") owners.push({ agent, sessionId });
15356
+ }
15357
+ return owners;
15358
+ }
15359
+ var DraftTabs = class {
15360
+ seen = /* @__PURE__ */ new Set();
15361
+ owners = /* @__PURE__ */ new Map();
15362
+ update(descriptors) {
15363
+ const appeared = [];
15364
+ const moved = [];
15365
+ const present = /* @__PURE__ */ new Set();
15366
+ for (const descriptor of descriptors) {
15367
+ const { terminalId } = descriptor;
15368
+ present.add(terminalId);
15369
+ if (!this.seen.has(terminalId)) appeared.push(descriptor);
15370
+ const current = draftOwnerOf(descriptor);
15371
+ if (current === null) continue;
15372
+ const previous = this.owners.get(terminalId);
15373
+ if (previous !== void 0 && keyOf(previous) !== keyOf(current)) moved.push({ from: previous, to: current });
15374
+ this.owners.set(terminalId, current);
15375
+ }
15376
+ for (const terminalId of [...this.owners.keys()]) {
15377
+ if (!present.has(terminalId)) this.owners.delete(terminalId);
15378
+ }
15379
+ this.seen = present;
15380
+ return { appeared, moved };
15381
+ }
15382
+ };
15383
+ function keyOf(owner) {
15384
+ return JSON.stringify([owner.agent, owner.sessionId]);
15385
+ }
15386
+ function copyOf(draft) {
15387
+ return { text: draft.text, pasted: draft.pasted.map((paste) => ({ ...paste })) };
15388
+ }
15389
+ function parseEntry(value) {
15390
+ const record = asRecord(value);
15391
+ if (record === null) return null;
15392
+ const agent = asNonEmptyString(record["agent"]);
15393
+ const sessionId = asNonEmptyString(record["sessionId"]);
15394
+ const draft = parseComposerDraft(record["draft"]);
15395
+ const updatedAt = asFiniteNumber(record["updatedAt"]);
15396
+ if (agent === null || sessionId === null || draft === null || updatedAt === null) return null;
15397
+ if (isEmptyComposerDraft(draft) || composerDraftChars(draft) > MAX_COMPOSER_DRAFT_CHARS) return null;
15398
+ return { agent, sessionId, draft, updatedAt };
15399
+ }
15400
+ function parseState3(raw) {
15401
+ const drafts = /* @__PURE__ */ new Map();
15402
+ let parsed;
15403
+ try {
15404
+ parsed = JSON.parse(raw);
15405
+ } catch {
15406
+ return drafts;
15407
+ }
15408
+ const record = asRecord(parsed);
15409
+ if (record === null || record["version"] !== STATE_VERSION3) return drafts;
15410
+ for (const entry of asArrayFiltered(record["drafts"], parseEntry) ?? []) drafts.set(keyOf(entry), entry);
15411
+ return drafts;
15412
+ }
15413
+ function trimOldest(drafts) {
15414
+ if (drafts.size <= MAX_STORED_DRAFTS) return [];
15415
+ const oldest = [...drafts.entries()].sort(([, a], [, b]) => a.updatedAt - b.updatedAt).slice(0, drafts.size - MAX_STORED_DRAFTS).map(([key]) => key);
15416
+ for (const key of oldest) drafts.delete(key);
15417
+ return oldest;
15418
+ }
15419
+ var DraftStore = class {
15420
+ drafts = /* @__PURE__ */ new Map();
15421
+ /** Las claves que cambiaron aca desde la ultima escritura: lo unico que se escribe. */
15422
+ changed = /* @__PURE__ */ new Set();
15423
+ writeTimer = null;
15424
+ /** La escritura en curso: van de a una, porque dos se pisarian el temporal. */
15425
+ writing = Promise.resolve();
15426
+ statePath;
15427
+ now;
15428
+ /** Ruta y reloj inyectables para las pruebas; por defecto, la carpeta de configuracion. */
15429
+ constructor(options = {}) {
15430
+ this.statePath = options.statePath ?? composerDraftsPath();
15431
+ this.now = options.now ?? Date.now;
15432
+ }
15433
+ async load() {
15434
+ try {
15435
+ this.drafts = parseState3(await readFile15(this.statePath, "utf8"));
15436
+ } catch {
15437
+ this.drafts = /* @__PURE__ */ new Map();
15438
+ }
15439
+ }
15440
+ /** El borrador guardado de una conversacion, o null. Una copia. */
15441
+ get(owner) {
15442
+ const stored = this.drafts.get(keyOf(owner));
15443
+ return stored === void 0 ? null : copyOf(stored.draft);
15444
+ }
15445
+ /** Cuantos hay. Para las pruebas. */
15446
+ get size() {
15447
+ return this.drafts.size;
15448
+ }
15449
+ save(owner, draft) {
15450
+ if (composerDraftChars(draft) > MAX_COMPOSER_DRAFT_CHARS) {
15451
+ this.delete(owner);
15452
+ return "too-long";
15453
+ }
15454
+ if (isEmptyComposerDraft(draft)) return this.delete(owner) ? "deleted" : "unchanged";
15455
+ const key = keyOf(owner);
15456
+ const current = this.drafts.get(key);
15457
+ if (current !== void 0 && sameComposerDraft(current.draft, draft)) return "unchanged";
15458
+ this.drafts.set(key, { agent: owner.agent, sessionId: owner.sessionId, draft: copyOf(draft), updatedAt: this.now() });
15459
+ this.changed.add(key);
15460
+ for (const gone of trimOldest(this.drafts)) this.changed.add(gone);
15461
+ this.schedule();
15462
+ return "saved";
15463
+ }
15464
+ /** Borra el de esa conversacion: se mando, o se cerro su pestana. false si no habia. */
15465
+ delete(owner) {
15466
+ const key = keyOf(owner);
15467
+ if (!this.drafts.delete(key)) return false;
15468
+ this.changed.add(key);
15469
+ this.schedule();
15470
+ return true;
15471
+ }
15472
+ /**
15473
+ * La pestana cambio de conversacion —una CLI que la descubre, o que empieza
15474
+ * otra con `/clear`— y su borrador va con ella: es lo que se restaura con la
15475
+ * pestana, que desde ahora se guarda con el id nuevo.
15476
+ */
15477
+ move(from, to) {
15478
+ const fromKey = keyOf(from);
15479
+ const stored = this.drafts.get(fromKey);
15480
+ if (stored === void 0 || fromKey === keyOf(to)) return;
15481
+ this.delete(from);
15482
+ this.save(to, stored.draft);
15483
+ }
15484
+ /**
15485
+ * Deja solo los de estas conversaciones: las pestanas del archivo del
15486
+ * arranque. Lo demas es de una pestana que ya no esta —se cerro con una build
15487
+ * anterior, o su carpeta se borro— y nadie lo va a pedir.
15488
+ */
15489
+ retainOnly(owners) {
15490
+ const keep = new Set([...owners].map(keyOf));
15491
+ let removed = false;
15492
+ for (const key of [...this.drafts.keys()]) {
15493
+ if (keep.has(key)) continue;
15494
+ this.drafts.delete(key);
15495
+ this.changed.add(key);
15496
+ removed = true;
15497
+ }
15498
+ if (removed) this.schedule();
15499
+ }
15500
+ /** Escritura inmediata, para el apagado. */
15501
+ async flush() {
15502
+ if (this.writeTimer !== null) {
15503
+ clearTimeout(this.writeTimer);
15504
+ this.writeTimer = null;
15505
+ }
15506
+ await this.write();
15507
+ }
15508
+ schedule() {
15509
+ if (this.writeTimer !== null) return;
15510
+ this.writeTimer = setTimeout(() => {
15511
+ this.writeTimer = null;
15512
+ void this.write();
15513
+ }, WRITE_DEBOUNCE_MS3);
15514
+ this.writeTimer.unref();
15515
+ }
15516
+ write() {
15517
+ const run = async () => {
15518
+ if (this.changed.size > 0) await this.writeNow();
15519
+ };
15520
+ this.writing = this.writing.then(run, run);
15521
+ return this.writing;
15522
+ }
15523
+ async writeNow() {
15524
+ let onDisk = /* @__PURE__ */ new Map();
15525
+ try {
15526
+ onDisk = parseState3(await readFile15(this.statePath, "utf8"));
15527
+ } catch {
15528
+ }
15529
+ const changed = this.changed;
15530
+ this.changed = /* @__PURE__ */ new Set();
15531
+ const merged = onDisk;
15532
+ for (const key of changed) {
15533
+ const mine = this.drafts.get(key);
15534
+ if (mine === void 0) merged.delete(key);
15535
+ else merged.set(key, mine);
15536
+ }
15537
+ trimOldest(merged);
15538
+ this.drafts = new Map(merged);
15539
+ try {
15540
+ await mkdir12(path39.dirname(this.statePath), { recursive: true });
15541
+ const temporary = path39.join(path39.dirname(this.statePath), `composer-drafts.${process.pid}.tmp`);
15542
+ const payload = {
15543
+ version: STATE_VERSION3,
15544
+ drafts: [...merged.values()].map(({ agent, sessionId, updatedAt, draft }) => ({ agent, sessionId, updatedAt, draft }))
15545
+ };
15546
+ await writeFile7(temporary, JSON.stringify(payload, null, 2), { encoding: "utf8", mode: 384 });
15547
+ await rename6(temporary, this.statePath);
15548
+ } catch (error) {
15549
+ for (const key of changed) this.changed.add(key);
15550
+ console.warn("[drafts] couldn't save the message box drafts:", error);
15551
+ }
15552
+ }
15553
+ };
15554
+
14414
15555
  // packages/server/src/shell-locator.ts
14415
15556
  import { access as access2 } from "node:fs/promises";
14416
15557
  import { constants as fsConstants2 } from "node:fs";
14417
- import path37 from "node:path";
15558
+ import path40 from "node:path";
14418
15559
  async function isExecutable(candidate) {
14419
15560
  try {
14420
15561
  await access2(candidate, process.platform === "win32" ? fsConstants2.R_OK : fsConstants2.X_OK);
@@ -14426,7 +15567,7 @@ async function isExecutable(candidate) {
14426
15567
  function windowsPowerShellPath() {
14427
15568
  const systemRoot = process.env["SystemRoot"] ?? process.env["windir"];
14428
15569
  if (systemRoot === void 0 || systemRoot.length === 0) return null;
14429
- return path37.join(systemRoot, "System32", "WindowsPowerShell", "v1.0", "powershell.exe");
15570
+ return path40.join(systemRoot, "System32", "WindowsPowerShell", "v1.0", "powershell.exe");
14430
15571
  }
14431
15572
  async function locateWindowsShell() {
14432
15573
  const pwsh = await findInPath("pwsh");
@@ -14446,7 +15587,7 @@ async function locateWindowsShell() {
14446
15587
  async function locatePosixShell() {
14447
15588
  const preferred = process.env["SHELL"];
14448
15589
  if (preferred !== void 0 && preferred.length > 0 && await isExecutable(preferred)) {
14449
- return { file: preferred, args: [], name: path37.basename(preferred) };
15590
+ return { file: preferred, args: [], name: path40.basename(preferred) };
14450
15591
  }
14451
15592
  if (await isExecutable("/bin/bash")) return { file: "/bin/bash", args: [], name: "bash" };
14452
15593
  if (await isExecutable("/bin/sh")) return { file: "/bin/sh", args: [], name: "sh" };
@@ -14457,13 +15598,13 @@ async function locateShell() {
14457
15598
  }
14458
15599
 
14459
15600
  // packages/server/src/session-watcher.ts
14460
- import { stat as stat19 } from "node:fs/promises";
15601
+ import { stat as stat21 } from "node:fs/promises";
14461
15602
  import chokidar3 from "chokidar";
14462
15603
  var INDEX_DEBOUNCE_MS = 750;
14463
15604
  var ROOT_PROBE_MS = 5e3;
14464
15605
  async function isDirectory3(target) {
14465
15606
  try {
14466
- return (await stat19(target)).isDirectory();
15607
+ return (await stat21(target)).isDirectory();
14467
15608
  } catch {
14468
15609
  return false;
14469
15610
  }
@@ -14564,9 +15705,9 @@ function watchSessions(index, hub, agents, options = {}) {
14564
15705
  }
14565
15706
 
14566
15707
  // packages/server/src/terminal-registry.ts
14567
- import { randomUUID as randomUUID4 } from "node:crypto";
15708
+ import { randomUUID as randomUUID5 } from "node:crypto";
14568
15709
  import { EventEmitter as EventEmitter5 } from "node:events";
14569
- import { stat as stat20 } from "node:fs/promises";
15710
+ import { stat as stat22 } from "node:fs/promises";
14570
15711
 
14571
15712
  // packages/server/src/launch-hook-slot.ts
14572
15713
  var LaunchHookSlot = class {
@@ -14828,10 +15969,10 @@ async function resolveLaunchPlan(launch) {
14828
15969
  }
14829
15970
 
14830
15971
  // packages/server/src/workspace-store.ts
14831
- import { mkdir as mkdir11, readFile as readFile14, writeFile as writeFile7 } from "node:fs/promises";
14832
- import path38 from "node:path";
14833
- var STATE_VERSION3 = 1;
14834
- var WRITE_DEBOUNCE_MS3 = 400;
15972
+ import { mkdir as mkdir13, readFile as readFile16, writeFile as writeFile8 } from "node:fs/promises";
15973
+ import path41 from "node:path";
15974
+ var STATE_VERSION4 = 1;
15975
+ var WRITE_DEBOUNCE_MS4 = 400;
14835
15976
  var LEGACY_TAB_AGENT = "claude-code";
14836
15977
  function placeByPosition(base, placed) {
14837
15978
  const result = [...base];
@@ -14887,11 +16028,11 @@ function readTab(tab, missingAgent) {
14887
16028
  if (agent === null) return null;
14888
16029
  return { kind: "known", tab: { agent, cwd, sessionId, label: typeof label === "string" ? label : "" } };
14889
16030
  }
14890
- function parseState3(raw) {
16031
+ function parseState4(raw) {
14891
16032
  try {
14892
16033
  const parsed = JSON.parse(raw);
14893
16034
  if (!isRecord(parsed)) return null;
14894
- if (parsed["version"] !== STATE_VERSION3) return null;
16035
+ if (parsed["version"] !== STATE_VERSION4) return null;
14895
16036
  const rawTabs = parsed["tabs"];
14896
16037
  if (!Array.isArray(rawTabs)) return null;
14897
16038
  const base = [];
@@ -14929,7 +16070,7 @@ function serializeState(state) {
14929
16070
  otherTabs.push({ ...entry.tab, position });
14930
16071
  }
14931
16072
  }
14932
- return otherTabs.length === 0 ? { version: STATE_VERSION3, tabs } : { version: STATE_VERSION3, tabs, otherTabs };
16073
+ return otherTabs.length === 0 ? { version: STATE_VERSION4, tabs } : { version: STATE_VERSION4, tabs, otherTabs };
14933
16074
  }
14934
16075
  var EMPTY_STATE = () => ({ tabs: [], foreignTabs: [] });
14935
16076
  function persistableTabs(descriptors) {
@@ -14951,8 +16092,8 @@ var WorkspaceStore = class {
14951
16092
  pending = null;
14952
16093
  async load() {
14953
16094
  try {
14954
- const raw = await readFile14(workspaceStatePath(), "utf8");
14955
- return parseState3(raw) ?? EMPTY_STATE();
16095
+ const raw = await readFile16(workspaceStatePath(), "utf8");
16096
+ return parseState4(raw) ?? EMPTY_STATE();
14956
16097
  } catch {
14957
16098
  return EMPTY_STATE();
14958
16099
  }
@@ -14966,7 +16107,7 @@ var WorkspaceStore = class {
14966
16107
  const toWrite = this.pending;
14967
16108
  this.pending = null;
14968
16109
  if (toWrite !== null) void this.writeNow(toWrite);
14969
- }, WRITE_DEBOUNCE_MS3);
16110
+ }, WRITE_DEBOUNCE_MS4);
14970
16111
  this.writeTimer.unref();
14971
16112
  }
14972
16113
  /** Escritura inmediata, para el apagado. */
@@ -14981,12 +16122,12 @@ var WorkspaceStore = class {
14981
16122
  }
14982
16123
  async writeNow(state) {
14983
16124
  try {
14984
- await mkdir11(appConfigDir(), { recursive: true });
16125
+ await mkdir13(appConfigDir(), { recursive: true });
14985
16126
  const target = workspaceStatePath();
14986
- const temporary = path38.join(path38.dirname(target), `workspace.${process.pid}.tmp`);
14987
- await writeFile7(temporary, JSON.stringify(serializeState(state), null, 2), "utf8");
14988
- const { rename: rename6 } = await import("node:fs/promises");
14989
- await rename6(temporary, target);
16127
+ const temporary = path41.join(path41.dirname(target), `workspace.${process.pid}.tmp`);
16128
+ await writeFile8(temporary, JSON.stringify(serializeState(state), null, 2), "utf8");
16129
+ const { rename: rename7 } = await import("node:fs/promises");
16130
+ await rename7(temporary, target);
14990
16131
  } catch (error) {
14991
16132
  console.warn("[workspace] couldn't save the state:", error);
14992
16133
  }
@@ -15103,14 +16244,14 @@ var TerminalRegistry = class extends EventEmitter5 {
15103
16244
  }
15104
16245
  await this.assertDirectory(options.cwd);
15105
16246
  const resumed = kind === "agent" && options.resumeSessionId !== void 0;
15106
- const terminalId = randomUUID4();
16247
+ const terminalId = randomUUID5();
15107
16248
  const descriptor = {
15108
16249
  terminalId,
15109
16250
  kind,
15110
16251
  agent,
15111
16252
  cwd: options.cwd,
15112
16253
  // Provisional: `spawn` lo confirma con lo que diga el adaptador.
15113
- sessionId: kind === "agent" ? options.resumeSessionId ?? randomUUID4() : "",
16254
+ sessionId: kind === "agent" ? options.resumeSessionId ?? randomUUID5() : "",
15114
16255
  label: options.label ?? "",
15115
16256
  resumed,
15116
16257
  createdAt: Date.now(),
@@ -15214,7 +16355,7 @@ var TerminalRegistry = class extends EventEmitter5 {
15214
16355
  async assertDirectory(cwd) {
15215
16356
  let info;
15216
16357
  try {
15217
- info = await stat20(cwd);
16358
+ info = await stat22(cwd);
15218
16359
  } catch {
15219
16360
  throw new TerminalOpenError("invalid-cwd", serverText("cwdMissing", { cwd }));
15220
16361
  }
@@ -15242,7 +16383,7 @@ var TerminalRegistry = class extends EventEmitter5 {
15242
16383
  let launch;
15243
16384
  let adapter = null;
15244
16385
  let launchedSessionId = sessionId;
15245
- const launchToken = randomUUID4();
16386
+ const launchToken = randomUUID5();
15246
16387
  if (launcher.kind === "agent") {
15247
16388
  adapter = launcher.agent.adapter;
15248
16389
  const { location } = launcher.agent;
@@ -15478,7 +16619,7 @@ var TerminalRegistry = class extends EventEmitter5 {
15478
16619
  console.warn(`[workspace] the tab of ${tab.cwd} wasn't restored: ${reason2}`);
15479
16620
  continue;
15480
16621
  }
15481
- const terminalId = randomUUID4();
16622
+ const terminalId = randomUUID5();
15482
16623
  const descriptor = {
15483
16624
  terminalId,
15484
16625
  kind: "agent",
@@ -15640,20 +16781,20 @@ var TerminalRegistry = class extends EventEmitter5 {
15640
16781
  import { WebSocketServer } from "ws";
15641
16782
 
15642
16783
  // packages/server/src/directory-picker.ts
15643
- import { randomUUID as randomUUID5 } from "node:crypto";
15644
- import { mkdir as mkdir12, readdir as readdir11, stat as stat21 } from "node:fs/promises";
16784
+ import { randomUUID as randomUUID6 } from "node:crypto";
16785
+ import { mkdir as mkdir14, readdir as readdir11, stat as stat23 } from "node:fs/promises";
15645
16786
  import { homedir as homedir11 } from "node:os";
15646
- import path39 from "node:path";
16787
+ import path42 from "node:path";
15647
16788
  var MAX_ENTRIES = 500;
15648
16789
  var MAX_PICKERS = 8;
15649
16790
  var SKIPPED = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "bin", "obj", "$RECYCLE.BIN"]);
15650
16791
  var DirectoryPickerError = class extends ServerTextError {
15651
16792
  };
15652
16793
  function isInsideProtected(target, dirs) {
15653
- const resolved = path39.resolve(target);
16794
+ const resolved = path42.resolve(target);
15654
16795
  return dirs.some((dir) => {
15655
- const protectedDir = path39.resolve(dir);
15656
- return resolved === protectedDir || resolved.startsWith(protectedDir + path39.sep);
16796
+ const protectedDir = path42.resolve(dir);
16797
+ return resolved === protectedDir || resolved.startsWith(protectedDir + path42.sep);
15657
16798
  });
15658
16799
  }
15659
16800
  async function listRoots() {
@@ -15665,7 +16806,7 @@ async function listRoots() {
15665
16806
  letters.map(async (letter) => {
15666
16807
  const drive = `${letter}:\\`;
15667
16808
  try {
15668
- await stat21(drive);
16809
+ await stat23(drive);
15669
16810
  return drive;
15670
16811
  } catch {
15671
16812
  return null;
@@ -15692,7 +16833,7 @@ var DirectoryPickers = class {
15692
16833
  if (this.pickers.size >= MAX_PICKERS) {
15693
16834
  throw new DirectoryPickerError(serverText("pickerTooMany"));
15694
16835
  }
15695
- const picker = { id: randomUUID5(), current: homedir11(), entries: /* @__PURE__ */ new Set() };
16836
+ const picker = { id: randomUUID6(), current: homedir11(), entries: /* @__PURE__ */ new Set() };
15696
16837
  this.pickers.set(picker.id, picker);
15697
16838
  return this.listing(picker);
15698
16839
  }
@@ -15717,14 +16858,14 @@ var DirectoryPickers = class {
15717
16858
  async enter(pickerId, name) {
15718
16859
  const picker = this.require(pickerId);
15719
16860
  if (name === "..") {
15720
- const parent = path39.dirname(picker.current);
16861
+ const parent = path42.dirname(picker.current);
15721
16862
  if (parent !== picker.current) picker.current = parent;
15722
16863
  return this.listing(picker);
15723
16864
  }
15724
16865
  if (!picker.entries.has(name)) {
15725
16866
  throw new DirectoryPickerError(serverText("pickerNotListed"));
15726
16867
  }
15727
- const target = path39.join(picker.current, name);
16868
+ const target = path42.join(picker.current, name);
15728
16869
  if (isInsideProtected(target, this.protectedDirs)) {
15729
16870
  throw new DirectoryPickerError(serverText("pickerCliFolder"));
15730
16871
  }
@@ -15764,12 +16905,12 @@ var DirectoryPickers = class {
15764
16905
  throw new DirectoryPickerError(serverText("pickerNameChars"));
15765
16906
  }
15766
16907
  }
15767
- const target = path39.join(picker.current, clean);
16908
+ const target = path42.join(picker.current, clean);
15768
16909
  if (isInsideProtected(target, this.protectedDirs)) {
15769
16910
  throw new DirectoryPickerError(serverText("pickerNoProjectsHere"));
15770
16911
  }
15771
16912
  try {
15772
- await mkdir12(target);
16913
+ await mkdir14(target);
15773
16914
  } catch (error) {
15774
16915
  const code = error.code;
15775
16916
  throw new DirectoryPickerError(
@@ -15801,7 +16942,7 @@ var DirectoryPickers = class {
15801
16942
  let truncated = false;
15802
16943
  try {
15803
16944
  const found = await readdir11(picker.current, { withFileTypes: true });
15804
- const dirs = found.filter((entry) => entry.isDirectory()).map((entry) => entry.name).filter((name) => !SKIPPED.has(name)).filter((name) => !isInsideProtected(path39.join(picker.current, name), this.protectedDirs)).sort((a, b) => a.localeCompare(b));
16945
+ const dirs = found.filter((entry) => entry.isDirectory()).map((entry) => entry.name).filter((name) => !SKIPPED.has(name)).filter((name) => !isInsideProtected(path42.join(picker.current, name), this.protectedDirs)).sort((a, b) => a.localeCompare(b));
15805
16946
  truncated = dirs.length > MAX_ENTRIES;
15806
16947
  names = dirs.slice(0, MAX_ENTRIES);
15807
16948
  } catch {
@@ -15812,15 +16953,15 @@ var DirectoryPickers = class {
15812
16953
  path: picker.current,
15813
16954
  entries: names,
15814
16955
  roots,
15815
- canGoUp: path39.dirname(picker.current) !== picker.current,
16956
+ canGoUp: path42.dirname(picker.current) !== picker.current,
15816
16957
  truncated
15817
16958
  };
15818
16959
  }
15819
16960
  };
15820
16961
 
15821
16962
  // packages/server/src/file-browser.ts
15822
- import { readFile as readFile15, readdir as readdir12, stat as stat22 } from "node:fs/promises";
15823
- import path40 from "node:path";
16963
+ import { readFile as readFile17, readdir as readdir12, stat as stat24 } from "node:fs/promises";
16964
+ import path43 from "node:path";
15824
16965
  var MAX_ENTRIES2 = 1e3;
15825
16966
  var SEARCH_MAX_RESULTS = 200;
15826
16967
  var SEARCH_MAX_DIRECTORIES = 4e3;
@@ -15883,7 +17024,7 @@ function languageFor(fileName) {
15883
17024
  const lower = fileName.toLowerCase();
15884
17025
  const byName = LANGUAGE_BY_NAME[lower];
15885
17026
  if (byName !== void 0) return byName;
15886
- const extension = path40.extname(lower);
17027
+ const extension = path43.extname(lower);
15887
17028
  return LANGUAGE_BY_EXTENSION[extension] ?? null;
15888
17029
  }
15889
17030
  function looksBinary2(buffer) {
@@ -15915,7 +17056,7 @@ async function listDirectory(cwd, relativePath, options = {}) {
15915
17056
  let sizeBytes = 0;
15916
17057
  let modifiedAt = 0;
15917
17058
  try {
15918
- const info = await stat22(path40.join(absolute, candidate.name));
17059
+ const info = await stat24(path43.join(absolute, candidate.name));
15919
17060
  sizeBytes = candidate.isDirectory ? 0 : info.size;
15920
17061
  modifiedAt = info.mtimeMs;
15921
17062
  } catch {
@@ -15948,7 +17089,7 @@ async function readLevel(absolute) {
15948
17089
  const always = ALWAYS_SKIP.has(dirent.name.toLowerCase());
15949
17090
  if (dirent.isSymbolicLink()) {
15950
17091
  try {
15951
- const target = await stat22(path40.join(absolute, dirent.name));
17092
+ const target = await stat24(path43.join(absolute, dirent.name));
15952
17093
  candidates.push({
15953
17094
  name: dirent.name,
15954
17095
  isDirectory: target.isDirectory(),
@@ -15977,7 +17118,7 @@ async function markIgnored(repoRoot2, levels) {
15977
17118
  for (const level of levels) {
15978
17119
  for (const entry of level.entries) {
15979
17120
  if (entry.hidden !== null) continue;
15980
- keys.set(entry, toPosixPath(path40.relative(repoRoot2, path40.join(level.absolute, entry.name))));
17121
+ keys.set(entry, toPosixPath(path43.relative(repoRoot2, path43.join(level.absolute, entry.name))));
15981
17122
  }
15982
17123
  }
15983
17124
  if (keys.size === 0) return;
@@ -16038,7 +17179,7 @@ async function searchFiles(cwd, query, options = {}) {
16038
17179
  let sizeBytes = 0;
16039
17180
  let modifiedAt = 0;
16040
17181
  try {
16041
- const info = await stat22(path40.join(level.absolute, candidate.name));
17182
+ const info = await stat24(path43.join(level.absolute, candidate.name));
16042
17183
  sizeBytes = candidate.isDirectory ? 0 : info.size;
16043
17184
  modifiedAt = info.mtimeMs;
16044
17185
  } catch {
@@ -16072,12 +17213,12 @@ async function searchFiles(cwd, query, options = {}) {
16072
17213
  }
16073
17214
  async function readPreview(cwd, relativePath) {
16074
17215
  const absolute = await resolveInside(cwd, relativePath, { mustExist: true });
16075
- const info = await stat22(absolute);
17216
+ const info = await stat24(absolute);
16076
17217
  if (info.isDirectory()) {
16077
17218
  throw new Error("It's a directory, not a file.");
16078
17219
  }
16079
- const content = await readFile15(absolute);
16080
- const fileName = path40.basename(absolute);
17220
+ const content = await readFile17(absolute);
17221
+ const fileName = path43.basename(absolute);
16081
17222
  if (looksBinary2(content)) {
16082
17223
  return {
16083
17224
  path: relativePath,
@@ -16666,14 +17807,14 @@ var TerminalWriteQueue = class {
16666
17807
  };
16667
17808
 
16668
17809
  // packages/server/src/vault/service.ts
16669
- import { mkdir as mkdir15, stat as stat26 } from "node:fs/promises";
17810
+ import { mkdir as mkdir17, stat as stat28 } from "node:fs/promises";
16670
17811
  import { homedir as homedir12 } from "node:os";
16671
- import path44 from "node:path";
17812
+ import path47 from "node:path";
16672
17813
 
16673
17814
  // packages/server/src/vault/move.ts
16674
17815
  import { constants } from "node:fs";
16675
- import { copyFile as copyFile2, lstat, mkdir as mkdir13, readdir as readdir13, rm as rm5, stat as stat23, utimes, writeFile as writeFile8 } from "node:fs/promises";
16676
- import path41 from "node:path";
17816
+ import { copyFile as copyFile2, lstat, mkdir as mkdir15, readdir as readdir13, rm as rm5, stat as stat25, utimes, writeFile as writeFile9 } from "node:fs/promises";
17817
+ import path44 from "node:path";
16677
17818
  var MOVED_ENTRIES = ["vault.json", "sessions", "memory"];
16678
17819
  var OWN_TEMP_FILE = /\.\d+\.[0-9a-f]{12}\.tmp$/;
16679
17820
  async function copyTree(source, target, result) {
@@ -16684,14 +17825,14 @@ async function copyTree(source, target, result) {
16684
17825
  return;
16685
17826
  }
16686
17827
  if (info.isDirectory()) {
16687
- await mkdir13(target, { recursive: true });
17828
+ await mkdir15(target, { recursive: true });
16688
17829
  for (const name of await readdir13(source)) {
16689
- await copyTree(path41.join(source, name), path41.join(target, name), result);
17830
+ await copyTree(path44.join(source, name), path44.join(target, name), result);
16690
17831
  }
16691
17832
  return;
16692
17833
  }
16693
17834
  if (!info.isFile() || OWN_TEMP_FILE.test(source)) return;
16694
- await mkdir13(path41.dirname(target), { recursive: true });
17835
+ await mkdir15(path44.dirname(target), { recursive: true });
16695
17836
  try {
16696
17837
  await copyFile2(source, target, constants.COPYFILE_EXCL);
16697
17838
  } catch (error) {
@@ -16706,9 +17847,9 @@ async function copyTree(source, target, result) {
16706
17847
  }
16707
17848
  async function moveVault(from, to) {
16708
17849
  const result = { copied: 0, skipped: 0 };
16709
- await mkdir13(to, { recursive: true });
17850
+ await mkdir15(to, { recursive: true });
16710
17851
  for (const entry of MOVED_ENTRIES) {
16711
- await copyTree(path41.join(from, entry), path41.join(to, entry), result);
17852
+ await copyTree(path44.join(from, entry), path44.join(to, entry), result);
16712
17853
  }
16713
17854
  return result;
16714
17855
  }
@@ -16740,11 +17881,11 @@ function checkVaultTarget(current, target, options) {
16740
17881
  return { kind: "ok" };
16741
17882
  }
16742
17883
  async function canWriteInto(dir) {
16743
- const probe = temporaryPathFor(path41.join(dir, ".escritura"));
17884
+ const probe = temporaryPathFor(path44.join(dir, ".escritura"));
16744
17885
  try {
16745
- await mkdir13(dir, { recursive: true });
16746
- if (!(await stat23(dir)).isDirectory()) return false;
16747
- await writeFile8(probe, "");
17886
+ await mkdir15(dir, { recursive: true });
17887
+ if (!(await stat25(dir)).isDirectory()) return false;
17888
+ await writeFile9(probe, "");
16748
17889
  return true;
16749
17890
  } catch {
16750
17891
  return false;
@@ -16754,12 +17895,12 @@ async function canWriteInto(dir) {
16754
17895
  }
16755
17896
 
16756
17897
  // packages/server/src/vault/serialize.ts
16757
- import { createHash as createHash3 } from "node:crypto";
17898
+ import { createHash as createHash4 } from "node:crypto";
16758
17899
  function imageKey(eventId, source, index) {
16759
17900
  return `${eventId}|${source}|${index}`;
16760
17901
  }
16761
17902
  function assetName(bytes) {
16762
- const hash = createHash3("sha256").update(bytes).digest("hex").slice(0, 32);
17903
+ const hash = createHash4("sha256").update(bytes).digest("hex").slice(0, 32);
16763
17904
  return `${hash}.${detectImageFormat(bytes)?.ext ?? "bin"}`;
16764
17905
  }
16765
17906
  function buildHeader(input) {
@@ -16846,12 +17987,12 @@ function serializeSession(input) {
16846
17987
  }
16847
17988
 
16848
17989
  // packages/server/src/vault/writer.ts
16849
- import { readdir as readdir15, rm as rm6, stat as stat25 } from "node:fs/promises";
16850
- import path43 from "node:path";
17990
+ import { readdir as readdir15, rm as rm6, stat as stat27 } from "node:fs/promises";
17991
+ import path46 from "node:path";
16851
17992
 
16852
17993
  // packages/server/src/vault/memory-copy.ts
16853
- import { mkdir as mkdir14, readdir as readdir14, readFile as readFile16, stat as stat24, utimes as utimes2 } from "node:fs/promises";
16854
- import path42 from "node:path";
17994
+ import { mkdir as mkdir16, readdir as readdir14, readFile as readFile18, stat as stat26, utimes as utimes2 } from "node:fs/promises";
17995
+ import path45 from "node:path";
16855
17996
  var MEMORY_FILE_MAX_BYTES = 1024 * 1024;
16856
17997
  var MEMORY_MAX_FILES = 500;
16857
17998
  var MEMORY_SOURCE_FILE = "source.json";
@@ -16876,7 +18017,7 @@ async function listMemoryNotes(cwd) {
16876
18017
  let absolute;
16877
18018
  try {
16878
18019
  absolute = await resolveInside(cwd, `${MEMORY_DIR}/${name}`, { mustExist: true });
16879
- const info = await stat24(absolute);
18020
+ const info = await stat26(absolute);
16880
18021
  if (!info.isFile() || info.size > MEMORY_FILE_MAX_BYTES) continue;
16881
18022
  notes.push({ name, absolute, size: info.size, mtimeMs: info.mtimeMs });
16882
18023
  } catch {
@@ -16890,7 +18031,7 @@ async function measureProjectMemory(cwd) {
16890
18031
  }
16891
18032
  async function isUpToDate(target, note) {
16892
18033
  try {
16893
- const info = await stat24(target);
18034
+ const info = await stat26(target);
16894
18035
  return info.isFile() && info.size === note.size && Math.abs(info.mtimeMs - note.mtimeMs) < SAME_MTIME_MS;
16895
18036
  } catch {
16896
18037
  return false;
@@ -16898,7 +18039,7 @@ async function isUpToDate(target, note) {
16898
18039
  }
16899
18040
  async function exists3(file) {
16900
18041
  try {
16901
- await stat24(file);
18042
+ await stat26(file);
16902
18043
  return true;
16903
18044
  } catch {
16904
18045
  return false;
@@ -16914,14 +18055,14 @@ async function copyProjectMemory(dir, cwd, platform, deps = DEFAULT_WRITE_DEPS)
16914
18055
  if (prepared) return;
16915
18056
  prepared = true;
16916
18057
  await ensureVaultMarker(dir, deps);
16917
- await mkdir14(target, { recursive: true });
18058
+ await mkdir16(target, { recursive: true });
16918
18059
  };
16919
18060
  for (const note of notes) {
16920
- const destination = path42.join(target, note.name);
18061
+ const destination = path45.join(target, note.name);
16921
18062
  if (await isUpToDate(destination, note)) continue;
16922
18063
  let content;
16923
18064
  try {
16924
- content = await readFile16(note.absolute);
18065
+ content = await readFile18(note.absolute);
16925
18066
  } catch {
16926
18067
  continue;
16927
18068
  }
@@ -16933,7 +18074,7 @@ async function copyProjectMemory(dir, cwd, platform, deps = DEFAULT_WRITE_DEPS)
16933
18074
  result.files += 1;
16934
18075
  result.bytes += content.length;
16935
18076
  }
16936
- const source = path42.join(target, MEMORY_SOURCE_FILE);
18077
+ const source = path45.join(target, MEMORY_SOURCE_FILE);
16937
18078
  if (result.files > 0 || !await exists3(source)) {
16938
18079
  await prepare();
16939
18080
  await writeFileAtomic(source, `${JSON.stringify({ cwd, copiedAt: deps.now() })}
@@ -17427,7 +18568,7 @@ var VaultWriter = class {
17427
18568
  }
17428
18569
  async assetOnDisk(dir, agent, sessionId, asset, bytes) {
17429
18570
  try {
17430
- const info = await stat25(assetFile(dir, agent, sessionId, asset));
18571
+ const info = await stat27(assetFile(dir, agent, sessionId, asset));
17431
18572
  return info.isFile() && info.size === bytes;
17432
18573
  } catch {
17433
18574
  return false;
@@ -17527,17 +18668,17 @@ var VaultWriter = class {
17527
18668
  const folders = [dir];
17528
18669
  const subfolders = async (parent) => {
17529
18670
  try {
17530
- return (await readdir15(parent, { withFileTypes: true })).filter((entry) => entry.isDirectory()).map((entry) => path43.join(parent, entry.name));
18671
+ return (await readdir15(parent, { withFileTypes: true })).filter((entry) => entry.isDirectory()).map((entry) => path46.join(parent, entry.name));
17531
18672
  } catch {
17532
18673
  return [];
17533
18674
  }
17534
18675
  };
17535
18676
  for (const agentDir of await subfolders(sessionsRoot2(dir))) {
17536
- if (!SESSION_AGENT_IDS.includes(path43.basename(agentDir))) continue;
18677
+ if (!SESSION_AGENT_IDS.includes(path46.basename(agentDir))) continue;
17537
18678
  folders.push(agentDir);
17538
18679
  folders.push(...(await subfolders(agentDir)).filter((folder) => folder.endsWith(".assets")));
17539
18680
  }
17540
- folders.push(...await subfolders(path43.join(dir, "memory")));
18681
+ folders.push(...await subfolders(path46.join(dir, "memory")));
17541
18682
  for (const folder of folders) {
17542
18683
  let names;
17543
18684
  try {
@@ -17547,9 +18688,9 @@ var VaultWriter = class {
17547
18688
  }
17548
18689
  for (const name of names) {
17549
18690
  if (!OWN_TEMP_FILE2.test(name)) continue;
17550
- const file = path43.join(folder, name);
18691
+ const file = path46.join(folder, name);
17551
18692
  try {
17552
- const info = await stat25(file);
18693
+ const info = await stat27(file);
17553
18694
  if (info.isFile() && now - info.mtimeMs > TEMP_MAX_AGE_MS) await rm6(file, { force: true });
17554
18695
  } catch {
17555
18696
  }
@@ -17737,14 +18878,14 @@ var VaultService = class {
17737
18878
  async setDir(target) {
17738
18879
  const { platform, agents, settings: settings2, catalog } = this.options;
17739
18880
  const current = this.dir();
17740
- const protectedDirs = [...agents.protectedDirs(), path44.join(this.options.homeDir ?? homedir12(), ".gemini")];
18881
+ const protectedDirs = [...agents.protectedDirs(), path47.join(this.options.homeDir ?? homedir12(), ".gemini")];
17741
18882
  const check = checkVaultTarget(current, target, { protectedDirs, platform });
17742
18883
  if (check.kind === "refused") throw new VaultError(check.text);
17743
18884
  if (check.kind === "same") return;
17744
18885
  try {
17745
18886
  await this.writer.exclusive("moving", async () => {
17746
18887
  if (!await canWriteInto(target)) throw new VaultError(serverText("vaultCannotWrite"));
17747
- const hadCopy = await stat26(vaultMarkerFile(current)).then(
18888
+ const hadCopy = await stat28(vaultMarkerFile(current)).then(
17748
18889
  () => true,
17749
18890
  () => false
17750
18891
  );
@@ -17778,7 +18919,7 @@ var VaultService = class {
17778
18919
  /** Abre la carpeta de la copia con el explorador del sistema. */
17779
18920
  async reveal() {
17780
18921
  const dir = this.activeDir();
17781
- const isFolder = await stat26(dir).then(
18922
+ const isFolder = await stat28(dir).then(
17782
18923
  (info) => info.isDirectory(),
17783
18924
  () => false
17784
18925
  );
@@ -17802,7 +18943,7 @@ var VaultService = class {
17802
18943
  fromCopy: true
17803
18944
  });
17804
18945
  const file = sessionExportFile(dir, agent, sessionId);
17805
- await mkdir15(path44.dirname(file), { recursive: true });
18946
+ await mkdir17(path47.dirname(file), { recursive: true });
17806
18947
  await writeFileAtomic(file, text, this.writeDeps);
17807
18948
  this.openWithSystem(file);
17808
18949
  return file;
@@ -17889,7 +19030,7 @@ var VaultService = class {
17889
19030
  const project = this.options.index.getProjects().find((candidate) => candidate.key === projectKey);
17890
19031
  if (project === void 0) throw new VaultError(serverText("vaultProjectGone"));
17891
19032
  const folder = projectExportDirFor(this.activeDir(), project, this.options.platform);
17892
- await mkdir15(folder, { recursive: true });
19033
+ await mkdir17(folder, { recursive: true });
17893
19034
  const entries = [];
17894
19035
  const names = /* @__PURE__ */ new Set();
17895
19036
  let skipped = 0;
@@ -17906,7 +19047,7 @@ var VaultService = class {
17906
19047
  continue;
17907
19048
  }
17908
19049
  const fileName = this.exportFileName(summary, names);
17909
- await writeFileAtomic(path44.join(folder, fileName), text, this.writeDeps);
19050
+ await writeFileAtomic(path47.join(folder, fileName), text, this.writeDeps);
17910
19051
  entries.push({
17911
19052
  updatedAt: summary.updatedAt,
17912
19053
  agentLabel: this.labelOf(summary.agent),
@@ -17917,9 +19058,9 @@ var VaultService = class {
17917
19058
  });
17918
19059
  await nextTurn3();
17919
19060
  }
17920
- const name = project.cwd.length > 0 ? path44.basename(project.cwd) || project.cwd : project.fallbackName;
19061
+ const name = project.cwd.length > 0 ? path47.basename(project.cwd) || project.cwd : project.fallbackName;
17921
19062
  const index = renderProjectIndex({ name, cwd: project.cwd, exportedAt: this.now(), entries, skipped });
17922
- await writeFileAtomic(path44.join(folder, "index.md"), index, this.writeDeps);
19063
+ await writeFileAtomic(path47.join(folder, "index.md"), index, this.writeDeps);
17923
19064
  this.openWithSystem(folder);
17924
19065
  return { sessions: entries.length, skipped, folder };
17925
19066
  }
@@ -18075,12 +19216,14 @@ function attachTerminalSocket(options) {
18075
19216
  index,
18076
19217
  archived,
18077
19218
  notes,
19219
+ drafts,
18078
19220
  conversations,
18079
19221
  repos,
18080
19222
  memory,
18081
19223
  agents,
18082
19224
  defaultCwd,
18083
- vault
19225
+ vault,
19226
+ remote
18084
19227
  } = options;
18085
19228
  const pasteStore = options.pasteStore ?? new PasteStore();
18086
19229
  const writeQueue = new TerminalWriteQueue();
@@ -18095,6 +19238,14 @@ function attachTerminalSocket(options) {
18095
19238
  if (socket.readyState === socket.OPEN) socket.send(payload);
18096
19239
  }
18097
19240
  };
19241
+ const remoteSockets = /* @__PURE__ */ new Map();
19242
+ let phonePairingOwner = null;
19243
+ const broadcastToHost = (message) => {
19244
+ const payload = encodeServerMessage(message);
19245
+ for (const socket of clients) {
19246
+ if (!remoteSockets.has(socket) && socket.readyState === socket.OPEN) socket.send(payload);
19247
+ }
19248
+ };
18098
19249
  const broadcastExcept = (except, message) => {
18099
19250
  const payload = encodeServerMessage(message);
18100
19251
  for (const socket of clients) {
@@ -18145,7 +19296,25 @@ function attachTerminalSocket(options) {
18145
19296
  }
18146
19297
  return false;
18147
19298
  };
18148
- const onRegistryChanged = () => broadcast(terminalListMessage());
19299
+ const draftEntriesOf = (descriptors) => {
19300
+ const entries = [];
19301
+ for (const descriptor of descriptors) {
19302
+ const owner = draftOwnerOf(descriptor);
19303
+ const draft = owner === null ? null : drafts.get(owner);
19304
+ if (draft !== null) entries.push({ terminalId: descriptor.terminalId, draft });
19305
+ }
19306
+ return entries;
19307
+ };
19308
+ const draftTabs = new DraftTabs();
19309
+ draftTabs.update(registry.list());
19310
+ const onRegistryChanged = () => {
19311
+ const terminals = registry.list();
19312
+ broadcast({ type: "terminal.list", terminals, order: registry.getOrder() });
19313
+ const { appeared, moved } = draftTabs.update(terminals);
19314
+ for (const { from, to } of moved) drafts.move(from, to);
19315
+ const entries = draftEntriesOf(appeared);
19316
+ if (entries.length > 0) broadcast({ type: "composer.drafts", drafts: entries });
19317
+ };
18149
19318
  const onRegistryExit = (terminalId, exitCode, signal) => broadcast({ type: "terminal.exit", terminalId, exitCode, signal });
18150
19319
  const onRegistryActivity = (terminalId, activity, offlineReason) => broadcast(
18151
19320
  offlineReason === null ? { type: "terminal.activity", terminalId, activity } : { type: "terminal.activity", terminalId, activity, offlineReason }
@@ -18173,7 +19342,9 @@ function attachTerminalSocket(options) {
18173
19342
  };
18174
19343
  const pieceWriter = (socket, terminalId) => (piece) => writeToTerminal(socket, terminalId, piece);
18175
19344
  const dropTab = (terminalId) => {
19345
+ const draftOwner = draftOwnerOf(registry.get(terminalId));
18176
19346
  if (!registry.close(terminalId)) return false;
19347
+ if (draftOwner !== null) drafts.delete(draftOwner);
18177
19348
  conversations.drop(terminalId);
18178
19349
  repos.drop(terminalId);
18179
19350
  memory.drop(terminalId);
@@ -18245,6 +19416,23 @@ function attachTerminalSocket(options) {
18245
19416
  memory.on("status", onMemoryStatus);
18246
19417
  const stopAgentChanges = agents.subscribeChanges(() => broadcast(agentsMessage()));
18247
19418
  const stopVaultStatus = vault.onStatus((status) => broadcast({ type: "vault.status", status }));
19419
+ const stopRemoteStatus = remote.onChange(
19420
+ () => broadcastToHost({ type: "remote.status", status: remote.status() })
19421
+ );
19422
+ const stopRemoteRevoked = remote.onRevoked((deviceId) => {
19423
+ for (const [socket, id] of remoteSockets) {
19424
+ if (deviceId === null || id === deviceId) socket.close(REMOTE_REVOKED_CLOSE_CODE, "revoked");
19425
+ }
19426
+ });
19427
+ const sendRemoteError = (socket, error) => {
19428
+ if (error instanceof ServerTextError) {
19429
+ sendError(socket, "remote-failed", error.text);
19430
+ return;
19431
+ }
19432
+ const detail = error instanceof Error ? error.message : String(error);
19433
+ console.warn("[remote] a request failed:", detail);
19434
+ sendError(socket, "remote-failed", serverText("remoteFailed"), detail);
19435
+ };
18248
19436
  const sendVaultError = (socket, error) => {
18249
19437
  if (error instanceof VaultError) {
18250
19438
  sendError(socket, "vault-failed", error.text);
@@ -18262,20 +19450,25 @@ function attachTerminalSocket(options) {
18262
19450
  return;
18263
19451
  }
18264
19452
  if (pathname !== WS_PATH) return;
18265
- const rejection = rejectRequest(request, port, token);
18266
- if (rejection !== null) {
19453
+ const authorization = authorizeRequest(request, port, token, remote.verifier());
19454
+ if (!authorization.ok) {
18267
19455
  socket.write("HTTP/1.1 403 Forbidden\r\nConnection: close\r\n\r\n");
18268
19456
  socket.destroy();
18269
- console.warn(`[security] upgrade rejected on ${WS_PATH}: ${rejection}`);
19457
+ console.warn(`[security] upgrade rejected on ${WS_PATH}: ${authorization.reason}`);
18270
19458
  return;
18271
19459
  }
18272
19460
  wss.handleUpgrade(request, socket, head, (client) => {
18273
- wss.emit("connection", client, request);
19461
+ wss.emit("connection", client, request, authorization.credential);
18274
19462
  });
18275
19463
  };
18276
19464
  httpServer.on("upgrade", onUpgrade);
18277
- wss.on("connection", (socket) => {
19465
+ wss.on("connection", (socket, _request, credential) => {
18278
19466
  clients.add(socket);
19467
+ const deviceId = credential.kind === "device" ? credential.deviceId : null;
19468
+ if (deviceId !== null) {
19469
+ remoteSockets.set(socket, deviceId);
19470
+ remote.noteConnected(deviceId);
19471
+ }
18279
19472
  const listener = (terminalId, chunk) => {
18280
19473
  send(socket, { type: "terminal.output", terminalId, data: chunk });
18281
19474
  };
@@ -18338,7 +19531,8 @@ function attachTerminalSocket(options) {
18338
19531
  defaultAgent: agents.defaultAgent(),
18339
19532
  platform: process.platform,
18340
19533
  defaultCwd,
18341
- shellName: shell?.name ?? null
19534
+ shellName: shell?.name ?? null,
19535
+ remoteClient: deviceId !== null
18342
19536
  });
18343
19537
  send(socket, terminalListMessage());
18344
19538
  for (const entry of registry.activitySnapshot()) {
@@ -18347,7 +19541,9 @@ function attachTerminalSocket(options) {
18347
19541
  send(socket, { type: "index.status", status: index.getStatus() });
18348
19542
  send(socket, { type: "index.projects", projects: index.getProjects(), replace: true });
18349
19543
  send(socket, { type: "notes.list", notes: notes.list() });
19544
+ send(socket, { type: "composer.drafts", drafts: draftEntriesOf(registry.list()) });
18350
19545
  send(socket, { type: "vault.status", status: vault.status() });
19546
+ if (deviceId === null) send(socket, { type: "remote.status", status: remote.status() });
18351
19547
  socket.on("message", (raw) => {
18352
19548
  const text = Array.isArray(raw) ? Buffer.concat(raw).toString("utf8") : Buffer.from(raw).toString("utf8");
18353
19549
  const message = parseClientMessage(text);
@@ -18357,9 +19553,16 @@ function attachTerminalSocket(options) {
18357
19553
  return;
18358
19554
  }
18359
19555
  if (message.type !== "input") debugLog("socket", `received ${message.type}`);
19556
+ const refusal = remoteRefusal(message.type, deviceId !== null);
19557
+ if (refusal !== null) {
19558
+ sendError(socket, "remote-refused", refusal);
19559
+ return;
19560
+ }
18360
19561
  switch (message.type) {
18361
19562
  case "input":
18362
- writeToTerminal(socket, message.terminalId, message.data);
19563
+ if (!registry.write(message.terminalId, message.data) && registry.get(message.terminalId) === null) {
19564
+ sendError(socket, "unknown-terminal", serverText("terminalGone"));
19565
+ }
18363
19566
  break;
18364
19567
  /*
18365
19568
  Un mensaje del cuadro de escritura.
@@ -18375,6 +19578,8 @@ function attachTerminalSocket(options) {
18375
19578
  */
18376
19579
  case "agent.submit": {
18377
19580
  const { terminalId, text: text2, images } = message;
19581
+ const submittedOwner = draftOwnerOf(registry.get(terminalId));
19582
+ if (submittedOwner !== null) drafts.delete(submittedOwner);
18378
19583
  const files = message.files ?? [];
18379
19584
  if (files.length > MAX_FILES_PER_SUBMIT) {
18380
19585
  sendError(
@@ -18402,7 +19607,7 @@ function attachTerminalSocket(options) {
18402
19607
  if (submitting !== null) {
18403
19608
  const launchedAt = registry.launchedAtOf(terminalId);
18404
19609
  const blind = isBlind();
18405
- const refusal = submitRefusal({
19610
+ const refusal2 = submitRefusal({
18406
19611
  label: submitting.label,
18407
19612
  approvesPendingOnCycle: guardsPending,
18408
19613
  waitingFor: conversations.getWaitingFor(terminalId),
@@ -18411,8 +19616,8 @@ function attachTerminalSocket(options) {
18411
19616
  blind,
18412
19617
  openToolCall: blind && launchedAt !== null && conversations.hasOpenToolCall(terminalId, launchedAt)
18413
19618
  });
18414
- if (refusal !== null) {
18415
- sendError(socket, "submit-failed", refusal);
19619
+ if (refusal2 !== null) {
19620
+ sendError(socket, "submit-failed", refusal2);
18416
19621
  break;
18417
19622
  }
18418
19623
  }
@@ -19083,6 +20288,19 @@ function attachTerminalSocket(options) {
19083
20288
  }
19084
20289
  })();
19085
20290
  break;
20291
+ /*
20292
+ El borrador del cuadro de una pestana, despues de una pausa del
20293
+ teclado (§6.29). No se contesta. Una consola, o una pestana cuya CLI
20294
+ todavia no dijo su conversacion, no tiene donde guardarlo: lo escrito
20295
+ sigue en el cuadro, y se guarda con la pausa siguiente a que aparezca.
20296
+ */
20297
+ case "composer.draft": {
20298
+ const owner = draftOwnerOf(registry.get(message.terminalId));
20299
+ if (owner === null) break;
20300
+ const outcome = drafts.save(owner, message.draft);
20301
+ debugLog("drafts", `${message.terminalId.slice(0, 8)}: ${outcome}`);
20302
+ break;
20303
+ }
19086
20304
  /*
19087
20305
  Notas sueltas. No van dirigidas a ninguna pestana.
19088
20306
 
@@ -19367,9 +20585,70 @@ function attachTerminalSocket(options) {
19367
20585
  case "search.cancel":
19368
20586
  globalSearch.cancel(message.searchId);
19369
20587
  break;
20588
+ /*
20589
+ El acceso remoto (hito 37). Hasta aca solo llega una ventana del
20590
+ anfitrion: a un equipo remoto lo freno `remoteRefusal`. El estado
20591
+ nuevo sale a las ventanas del anfitrion por `remote.onChange`; aca
20592
+ solo se contesta lo que falla, y el codigo, que va **solo** a quien lo
20593
+ pidio.
20594
+ */
20595
+ case "remote.configure": {
20596
+ const patch = {};
20597
+ if (message.enabled !== void 0) patch.enabled = message.enabled;
20598
+ if (message.port !== void 0) patch.port = message.port;
20599
+ void remote.configure(patch).catch((error) => sendRemoteError(socket, error));
20600
+ break;
20601
+ }
20602
+ case "remote.pairing.start":
20603
+ try {
20604
+ send(socket, { type: "remote.pairing.code", pairing: remote.startPairing() });
20605
+ } catch (error) {
20606
+ sendRemoteError(socket, error);
20607
+ }
20608
+ break;
20609
+ case "remote.pairing.cancel":
20610
+ remote.cancelPairing();
20611
+ break;
20612
+ /*
20613
+ El telefono (hito 38): la respuesta lleva su llave privada dentro del
20614
+ texto del QR, asi que va solo a esta ventana, y si esta ventana se
20615
+ cierra sin cancelar, la llave se olvida igual (`teardown`).
20616
+ */
20617
+ case "remote.phone.start":
20618
+ phonePairingOwner = socket;
20619
+ void remote.startPhonePairing().then((pairing) => send(socket, { type: "remote.phone.pairing", pairing })).catch((error) => sendRemoteError(socket, error));
20620
+ break;
20621
+ case "remote.phone.cancel":
20622
+ if (phonePairingOwner === socket) phonePairingOwner = null;
20623
+ remote.cancelPhonePairing();
20624
+ break;
20625
+ case "remote.device.rename":
20626
+ void remote.renameDevice(message.deviceId, message.label).then((renamed) => {
20627
+ if (!renamed) sendError(socket, "remote-failed", serverText("remoteDeviceGone"));
20628
+ }).catch((error) => sendRemoteError(socket, error));
20629
+ break;
20630
+ case "remote.device.revoke":
20631
+ void remote.revokeDevice(message.deviceId).then((revoked) => {
20632
+ if (!revoked) sendError(socket, "remote-failed", serverText("remoteDeviceGone"));
20633
+ }).catch((error) => sendRemoteError(socket, error));
20634
+ break;
20635
+ /*
20636
+ "Olvidar este equipo" del teléfono: se revoca el equipo con el que
20637
+ entró esta ventana, y ningún otro. Revocar cierra sus ventanas con
20638
+ 4001, esta incluida: ése es el acuse. Una ventana del anfitrión no es
20639
+ un equipo, y no tiene nada que olvidar.
20640
+ */
20641
+ case "remote.device.forgetSelf":
20642
+ if (deviceId === null) break;
20643
+ void remote.revokeDevice(deviceId).catch((error) => sendRemoteError(socket, error));
20644
+ break;
19370
20645
  }
19371
20646
  });
19372
20647
  const teardown = () => {
20648
+ if (phonePairingOwner === socket) {
20649
+ phonePairingOwner = null;
20650
+ remote.cancelPhonePairing();
20651
+ }
19373
20652
  globalSearch.cancel();
19374
20653
  pickers.closeAll();
19375
20654
  registry.detachAll(listener);
@@ -19380,6 +20659,7 @@ function attachTerminalSocket(options) {
19380
20659
  for (const terminalId of memorySubscriptions) memory.unsubscribe(terminalId);
19381
20660
  memorySubscriptions.clear();
19382
20661
  clients.delete(socket);
20662
+ if (deviceId !== null && remoteSockets.delete(socket)) remote.noteDisconnected(deviceId);
19383
20663
  };
19384
20664
  socket.on("close", teardown);
19385
20665
  socket.on("error", teardown);
@@ -19404,18 +20684,20 @@ function attachTerminalSocket(options) {
19404
20684
  memory.off("status", onMemoryStatus);
19405
20685
  stopAgentChanges();
19406
20686
  stopVaultStatus();
20687
+ stopRemoteStatus();
20688
+ stopRemoteRevoked();
19407
20689
  for (const client of wss.clients) client.terminate();
19408
20690
  wss.close();
19409
20691
  };
19410
20692
  }
19411
20693
 
19412
20694
  // packages/server/src/vault/catalog.ts
19413
- import { open as open7, readdir as readdir16, stat as stat27 } from "node:fs/promises";
19414
- import path45 from "node:path";
20695
+ import { open as open7, readdir as readdir16, stat as stat29 } from "node:fs/promises";
20696
+ import path48 from "node:path";
19415
20697
  import { createInterface as createInterface2 } from "node:readline";
19416
20698
  var HEADER_MAX_BYTES = 64 * 1024;
19417
20699
  var LOAD_CONCURRENCY = 16;
19418
- var keyOf = (agent, sessionId) => `${agent}/${sessionId}`;
20700
+ var keyOf2 = (agent, sessionId) => `${agent}/${sessionId}`;
19419
20701
  function isSessionAgentId2(value) {
19420
20702
  return SESSION_AGENT_IDS.includes(value);
19421
20703
  }
@@ -19431,7 +20713,7 @@ async function assetBytesOf(dir, agent, sessionId) {
19431
20713
  for (const name of names) {
19432
20714
  if (!VAULT_ASSET_NAME_PATTERN.test(name)) continue;
19433
20715
  try {
19434
- const info = await stat27(path45.join(folder, name));
20716
+ const info = await stat29(path48.join(folder, name));
19435
20717
  if (info.isFile()) total += info.size;
19436
20718
  } catch {
19437
20719
  }
@@ -19442,7 +20724,7 @@ async function readEntry(dir, agent, sessionId) {
19442
20724
  const file = sessionFile(dir, agent, sessionId);
19443
20725
  let sizeBytes;
19444
20726
  try {
19445
- const info = await stat27(file);
20727
+ const info = await stat29(file);
19446
20728
  if (!info.isFile()) return { kind: "absent" };
19447
20729
  sizeBytes = info.size;
19448
20730
  } catch {
@@ -19523,8 +20805,8 @@ var VaultCatalog = class {
19523
20805
  const foreign = /* @__PURE__ */ new Set();
19524
20806
  keys.forEach((key, position) => {
19525
20807
  const read = reads[position];
19526
- if (read?.kind === "listed") entries.set(keyOf(key.agent, key.sessionId), read.entry);
19527
- else if (read?.kind === "foreign") foreign.add(keyOf(key.agent, key.sessionId));
20808
+ if (read?.kind === "listed") entries.set(keyOf2(key.agent, key.sessionId), read.entry);
20809
+ else if (read?.kind === "foreign") foreign.add(keyOf2(key.agent, key.sessionId));
19528
20810
  });
19529
20811
  this.entries = entries;
19530
20812
  this.foreign = foreign;
@@ -19538,7 +20820,7 @@ var VaultCatalog = class {
19538
20820
  const generation = this.generation;
19539
20821
  const read = await readEntry(dir, agent, sessionId);
19540
20822
  if (generation !== this.generation) return;
19541
- const key = keyOf(agent, sessionId);
20823
+ const key = keyOf2(agent, sessionId);
19542
20824
  const before = JSON.stringify(this.entries.get(key) ?? null);
19543
20825
  const wasForeign = this.foreign.has(key);
19544
20826
  this.entries.delete(key);
@@ -19568,11 +20850,11 @@ var VaultCatalog = class {
19568
20850
  return this.summaryCache;
19569
20851
  }
19570
20852
  header(agent, sessionId) {
19571
- return this.entries.get(keyOf(agent, sessionId))?.header ?? null;
20853
+ return this.entries.get(keyOf2(agent, sessionId))?.header ?? null;
19572
20854
  }
19573
20855
  /** true si en disco hay una copia de esa sesion con un formato que esta app no conoce. */
19574
20856
  hasForeignFormat(agent, sessionId) {
19575
- return this.foreign.has(keyOf(agent, sessionId));
20857
+ return this.foreign.has(keyOf2(agent, sessionId));
19576
20858
  }
19577
20859
  /**
19578
20860
  * El cuerpo de una sesion listada, linea por linea: eventos y documentos.
@@ -19595,7 +20877,7 @@ var VaultCatalog = class {
19595
20877
  */
19596
20878
  async *bodyLines(agent, sessionId) {
19597
20879
  const dir = this.dir;
19598
- if (dir === null || !this.entries.has(keyOf(agent, sessionId))) return;
20880
+ if (dir === null || !this.entries.has(keyOf2(agent, sessionId))) return;
19599
20881
  const handle = await open7(sessionFile(dir, agent, sessionId), "r").catch(() => null);
19600
20882
  if (handle === null) return;
19601
20883
  const input = handle.createReadStream({ encoding: "utf8" });
@@ -19651,38 +20933,77 @@ var VaultCatalog = class {
19651
20933
  };
19652
20934
 
19653
20935
  // packages/server/src/index.ts
19654
- var serverDir = path46.dirname(fileURLToPath2(import.meta.url));
20936
+ var serverDir = path49.dirname(fileURLToPath2(import.meta.url));
19655
20937
  var isPackaged = true;
19656
- var webRoot = isPackaged ? serverDir : path46.resolve(serverDir, "../../web");
19657
- var webDist = isPackaged ? path46.join(serverDir, "web") : path46.join(webRoot, "dist");
19658
- var repoRoot = path46.resolve(serverDir, "../../..");
20938
+ var webRoot = isPackaged ? serverDir : path49.resolve(serverDir, "../../web");
20939
+ var webDist = isPackaged ? path49.join(serverDir, "web") : path49.join(webRoot, "dist");
20940
+ var repoRoot = path49.resolve(serverDir, "../../..");
19659
20941
  var isProduction = isPackaged || process.argv.includes("--prod");
19660
20942
  function resolveDefaultCwd() {
19661
20943
  const override = process.env["AGENT_WORKBENCH_CWD"];
19662
- if (override !== void 0 && override.length > 0) return path46.resolve(override);
20944
+ if (override !== void 0 && override.length > 0) return path49.resolve(override);
19663
20945
  if (isPackaged) return process.cwd();
19664
20946
  return repoRoot;
19665
20947
  }
19666
20948
  var ORIGIN_REFUSED_TEXT = "Origin not allowed.\nOrigen no permitido.";
19667
20949
  var TOKEN_MISSING_TEXT = "The session token is missing. Open the URL the server printed at startup.\nFalta el token de sesi\xF3n. Abre la URL que imprimi\xF3 el servidor al arrancar.";
19668
20950
  var NOT_FOUND_TEXT = "Not found.\nNo encontrado.";
19669
- function createAuthMiddleware(port, token) {
20951
+ var PAIRING_HINT_TEXT = '\n\nFrom another computer: pair it first. On the computer that runs Agent Workbench, open "Remote access" and choose "Pair a device".\nDesde otro equipo: primero empar\xE9jalo. En el equipo donde corre Agent Workbench, abre "Acceso remoto" y elige "Emparejar un equipo".';
20952
+ var PAIRING_REFUSED_TEXT = `That pairing code isn't valid or has expired. Ask for a new one in "Remote access".
20953
+ Ese c\xF3digo para emparejar no es v\xE1lido o venci\xF3. Pide uno nuevo en "Acceso remoto".`;
20954
+ function createAuthMiddleware(port, token, remote) {
19670
20955
  return (request, response, next) => {
19671
- if (!hasValidHost(request, port) || !hasValidOrigin(request, port)) {
19672
- response.status(403).type("text/plain").send(ORIGIN_REFUSED_TEXT);
20956
+ const authorization = authorizeRequest(request, port, token, remote.verifier());
20957
+ if (authorization.ok) {
20958
+ const { credential } = authorization;
20959
+ if (credential.kind === "session" && credential.source === "query") {
20960
+ response.setHeader("Set-Cookie", buildTokenCookie(token));
20961
+ }
20962
+ if (credential.kind === "device" && request.method === "GET" && request.path === "/") {
20963
+ response.setHeader("Set-Cookie", buildDeviceCookie(credential.credential));
20964
+ }
20965
+ next();
19673
20966
  return;
19674
20967
  }
19675
- const match = matchToken(request, token);
19676
- if (match === null) {
19677
- response.status(401).type("text/plain").send(TOKEN_MISSING_TEXT);
20968
+ if (authorization.reason !== "bad-token") {
20969
+ response.status(403).type("text/plain").send(ORIGIN_REFUSED_TEXT);
19678
20970
  return;
19679
20971
  }
19680
- if (match.source === "query") {
19681
- response.setHeader("Set-Cookie", buildTokenCookie(token));
20972
+ const code = request.method === "GET" && request.path === "/" ? readPairingCode(request) : null;
20973
+ if (code !== null && remote.verifier() !== null) {
20974
+ response.setHeader("Cache-Control", "no-store");
20975
+ void remote.redeemPairing(code, request.headers["user-agent"]).catch(() => null).then((redeemed) => {
20976
+ if (redeemed === null) {
20977
+ response.status(401).type("text/plain").send(PAIRING_REFUSED_TEXT);
20978
+ return;
20979
+ }
20980
+ response.setHeader("Set-Cookie", buildDeviceCookie(redeemed.credential));
20981
+ response.redirect(303, "/");
20982
+ });
20983
+ return;
19682
20984
  }
19683
- next();
20985
+ response.status(401).type("text/plain").send(remote.verifier() === null ? TOKEN_MISSING_TEXT : TOKEN_MISSING_TEXT + PAIRING_HINT_TEXT);
19684
20986
  };
19685
20987
  }
20988
+ function isPortUnavailable(error) {
20989
+ const code = error?.code;
20990
+ return code === "EADDRINUSE" || code === "EACCES";
20991
+ }
20992
+ function listenOnLoopback(httpServer, port) {
20993
+ return new Promise((resolve, reject) => {
20994
+ const onError = (error) => {
20995
+ httpServer.off("listening", onListening);
20996
+ reject(error);
20997
+ };
20998
+ const onListening = () => {
20999
+ httpServer.off("error", onError);
21000
+ resolve();
21001
+ };
21002
+ httpServer.once("error", onError);
21003
+ httpServer.once("listening", onListening);
21004
+ httpServer.listen(port, LOOPBACK_HOST);
21005
+ });
21006
+ }
19686
21007
  async function mountDevUi(app, httpServer) {
19687
21008
  const { createServer: createViteServer } = await import("vite");
19688
21009
  const vite = await createViteServer({
@@ -19695,7 +21016,7 @@ async function mountDevUi(app, httpServer) {
19695
21016
  }
19696
21017
  var LOOKS_LIKE_FILE = /\.[a-zA-Z0-9]{1,8}$/;
19697
21018
  function mountBuiltUi(app) {
19698
- const indexHtml = path46.join(webDist, "index.html");
21019
+ const indexHtml = path49.join(webDist, "index.html");
19699
21020
  if (!existsSync4(indexHtml)) {
19700
21021
  throw new Error(
19701
21022
  `There's no compiled interface in ${webDist}.
@@ -19712,7 +21033,7 @@ Run "pnpm build" before "pnpm start", or use "pnpm dev".`
19712
21033
  });
19713
21034
  return () => Promise.resolve();
19714
21035
  }
19715
- function describeStartup(url, cwd, agents, shell) {
21036
+ function describeStartup(url, cwd, agents, shell, remoteLine) {
19716
21037
  const agentList = agents.list();
19717
21038
  const line = "-".repeat(64);
19718
21039
  console.log(`
@@ -19734,6 +21055,9 @@ ${line}`);
19734
21055
  }));
19735
21056
  for (const agentLine of startupAgentLines(startupAgents)) console.log(agentLine);
19736
21057
  console.log(` Console ${shell === null ? "not found" : shell.file}`);
21058
+ if (remoteLine !== null) console.log(remoteLine);
21059
+ const colored = process.stdout.isTTY === true && process.env["NO_COLOR"] === void 0;
21060
+ console.log(colored ? supportStartupLineColored() : supportStartupLine());
19737
21061
  if (agentList.some((info) => info.environmentNotice === "child-session-marker")) {
19738
21062
  console.log("");
19739
21063
  console.log(" Note: this process inherited CLAUDE_CODE_CHILD_SESSION, which turns off");
@@ -19751,6 +21075,7 @@ async function main() {
19751
21075
  const movedFrom = await migrateLegacyConfigDir();
19752
21076
  if (movedFrom !== null) console.log(`Configuration moved from ${movedFrom} to ${appConfigDir()}`);
19753
21077
  await agents.prepareAll();
21078
+ ensurePtyHelperExecutable();
19754
21079
  const store = new WorkspaceStore();
19755
21080
  const archived = new ArchivedSessions();
19756
21081
  await archived.load();
@@ -19769,6 +21094,8 @@ async function main() {
19769
21094
  );
19770
21095
  const notes = new NotesStore();
19771
21096
  await notes.load();
21097
+ const drafts = new DraftStore();
21098
+ await drafts.load();
19772
21099
  const conversations = new ConversationHub(registry, agents);
19773
21100
  const repos = new RepoHub(registry);
19774
21101
  const memory = new MemoryHub(registry);
@@ -19776,16 +21103,31 @@ async function main() {
19776
21103
  const app = express();
19777
21104
  app.disable("x-powered-by");
19778
21105
  const httpServer = createServer(app);
19779
- await new Promise((resolve, reject) => {
19780
- httpServer.once("error", reject);
19781
- httpServer.listen(0, LOOPBACK_HOST, resolve);
19782
- });
21106
+ const remoteSettings = settings2.get().remote;
21107
+ let listeningFixed = false;
21108
+ let fixedPortBusy = false;
21109
+ if (remoteSettings.enabled) {
21110
+ try {
21111
+ await listenOnLoopback(httpServer, remoteSettings.port);
21112
+ listeningFixed = true;
21113
+ } catch (error) {
21114
+ if (!isPortUnavailable(error)) throw error;
21115
+ fixedPortBusy = true;
21116
+ }
21117
+ }
21118
+ if (!listeningFixed) await listenOnLoopback(httpServer, 0);
19783
21119
  const address = httpServer.address();
19784
21120
  if (address === null || typeof address === "string") {
19785
21121
  throw new Error("Couldn't determine the assigned port.");
19786
21122
  }
19787
21123
  const { port } = address;
19788
- app.use(createAuthMiddleware(port, token));
21124
+ const remote = new RemoteAccessService({
21125
+ settings: settings2,
21126
+ store: new RemoteDevicesStore(),
21127
+ listening: { port, fixed: listeningFixed, fixedPortBusy }
21128
+ });
21129
+ await remote.load();
21130
+ app.use(createAuthMiddleware(port, token, remote));
19789
21131
  const closeUi = isProduction ? mountBuiltUi(app) : await mountDevUi(app, httpServer);
19790
21132
  const detachSocket = attachTerminalSocket({
19791
21133
  pasteStore,
@@ -19797,18 +21139,21 @@ async function main() {
19797
21139
  index,
19798
21140
  archived,
19799
21141
  notes,
21142
+ drafts,
19800
21143
  conversations,
19801
21144
  repos,
19802
21145
  memory,
19803
21146
  agents,
19804
21147
  defaultCwd,
19805
- vault
21148
+ vault,
21149
+ remote
19806
21150
  });
19807
21151
  void pasteStore.purgeStale();
19808
21152
  void index.start();
19809
21153
  const stopWatching = watchSessions(index, conversations, agents);
19810
21154
  if (process.env["AGENT_WORKBENCH_NO_RESTORE"] !== "1") {
19811
21155
  void store.load().then((state) => {
21156
+ drafts.retainOnly(workspaceDraftOwners(state));
19812
21157
  if (state.tabs.length === 0 && state.foreignTabs.length === 0) return;
19813
21158
  if (agents.anyAvailable()) {
19814
21159
  console.log(`Restoring ${state.tabs.length} tab(s) from the previous run...`);
@@ -19817,7 +21162,14 @@ async function main() {
19817
21162
  });
19818
21163
  }
19819
21164
  const url = `http://${LOOPBACK_HOST}:${port}/?${TOKEN_QUERY_PARAM}=${token}`;
19820
- describeStartup(url, defaultCwd, agents, shell);
21165
+ const remoteStatus = remote.status();
21166
+ describeStartup(
21167
+ url,
21168
+ defaultCwd,
21169
+ agents,
21170
+ shell,
21171
+ remoteAccessStartupLine(remoteStatus.state, remoteStatus.port, remote.pairedDevices)
21172
+ );
19821
21173
  if (process.env["AGENT_WORKBENCH_NO_OPEN"] !== "1") openBrowser(url);
19822
21174
  let shuttingDown = false;
19823
21175
  const shutdown = () => {
@@ -19832,7 +21184,8 @@ async function main() {
19832
21184
  repos.disposeAll();
19833
21185
  void memory.disposeAll();
19834
21186
  registry.disposeAll();
19835
- void Promise.all([store.flush(), archived.flush(), notes.flush(), agentsDisposed]).catch(() => void 0).then(() => closeUi()).catch(() => void 0).finally(() => {
21187
+ remote.dispose();
21188
+ void Promise.all([store.flush(), archived.flush(), notes.flush(), drafts.flush(), remote.flush(), agentsDisposed]).catch(() => void 0).then(() => closeUi()).catch(() => void 0).finally(() => {
19836
21189
  httpServer.close(() => process.exit(0));
19837
21190
  setTimeout(() => process.exit(0), 2e3).unref();
19838
21191
  });