agent-workbench 0.3.1 → 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 path47 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
  }
@@ -10665,214 +10770,1075 @@ function ensurePtyHelperExecutable(options = {}) {
10665
10770
  return fixed;
10666
10771
  }
10667
10772
 
10668
- // packages/server/src/security.ts
10669
- import { randomBytes as randomBytes2, timingSafeEqual } from "node:crypto";
10670
- var LOOPBACK_HOST = "127.0.0.1";
10671
- var ALLOWED_HOSTNAMES = /* @__PURE__ */ new Set(["127.0.0.1", "localhost", "[::1]", "::1"]);
10672
- var TOKEN_HEADER = "x-agent-workbench-token";
10673
- var TOKEN_COOKIE = "agent_workbench_token";
10674
- function createSessionToken() {
10675
- return randomBytes2(32).toString("base64url");
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();
10676
10828
  }
10677
- function constantTimeEquals(a, b) {
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) {
10678
10848
  const bufferA = Buffer.from(a, "utf8");
10679
10849
  const bufferB = Buffer.from(b, "utf8");
10680
10850
  if (bufferA.length !== bufferB.length) return false;
10681
10851
  return timingSafeEqual(bufferA, bufferB);
10682
10852
  }
10683
- function requestUrl(request) {
10684
- try {
10685
- return new URL(request.url ?? "/", `http://${request.headers.host ?? LOOPBACK_HOST}`);
10686
- } catch {
10687
- return null;
10853
+ var PairingDesk = class {
10854
+ constructor(deps) {
10855
+ this.deps = deps;
10688
10856
  }
10689
- }
10690
- function readCookie(request, name) {
10691
- const header = request.headers.cookie;
10692
- if (typeof header !== "string") return null;
10693
- for (const pair of header.split(";")) {
10694
- const separator = pair.indexOf("=");
10695
- if (separator === -1) continue;
10696
- if (pair.slice(0, separator).trim() !== name) continue;
10697
- const value = pair.slice(separator + 1).trim();
10698
- return value.length > 0 ? decodeURIComponent(value) : null;
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 };
10699
10867
  }
10700
- return null;
10701
- }
10702
- function matchToken(request, expected) {
10703
- const url = requestUrl(request);
10704
- const fromQuery = url?.searchParams.get(TOKEN_QUERY_PARAM) ?? null;
10705
- if (fromQuery !== null && constantTimeEquals(fromQuery, expected)) {
10706
- return { source: "query" };
10868
+ cancel() {
10869
+ this.current = null;
10707
10870
  }
10708
- const fromHeader = request.headers[TOKEN_HEADER];
10709
- if (typeof fromHeader === "string" && constantTimeEquals(fromHeader, expected)) {
10710
- return { source: "header" };
10871
+ isActive() {
10872
+ if (this.current !== null && this.deps.now() >= this.current.expiresAt) this.current = null;
10873
+ return this.current !== null;
10711
10874
  }
10712
- const fromCookie = readCookie(request, TOKEN_COOKIE);
10713
- if (fromCookie !== null && constantTimeEquals(fromCookie, expected)) {
10714
- return { source: "cookie" };
10875
+ /** Cuándo vence el vigente, o null. */
10876
+ expiresAt() {
10877
+ return this.isActive() ? this.current?.expiresAt ?? null : null;
10715
10878
  }
10716
- return null;
10717
- }
10718
- function hasValidToken(request, expected) {
10719
- return matchToken(request, expected) !== null;
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");
10720
10896
  }
10721
- function buildTokenCookie(token) {
10722
- 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 };
10723
10971
  }
10724
- function hasValidHost(request, port) {
10725
- const host = request.headers.host;
10726
- if (typeof host !== "string") return false;
10727
- const separator = host.lastIndexOf(":");
10728
- const hostname = separator === -1 ? host : host.slice(0, separator);
10729
- const hostPort = separator === -1 ? "" : host.slice(separator + 1);
10730
- if (!ALLOWED_HOSTNAMES.has(hostname)) return false;
10731
- 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) ?? [];
10732
10976
  }
10733
- function hasValidOrigin(request, port) {
10734
- const origin = request.headers.origin;
10735
- if (typeof origin !== "string" || origin.length === 0) return true;
10736
- let parsed;
10737
- try {
10738
- parsed = new URL(origin);
10739
- } catch {
10740
- return false;
10741
- }
10742
- if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return false;
10743
- if (!ALLOWED_HOSTNAMES.has(parsed.hostname)) return false;
10744
- return parsed.port === String(port);
10977
+ function serializeRemoteDevices(devices) {
10978
+ return `${JSON.stringify({ version: REMOTE_DEVICES_VERSION, devices }, null, 2)}
10979
+ `;
10745
10980
  }
10746
- function rejectRequest(request, port, token) {
10747
- if (!hasValidHost(request, port)) return "bad-host";
10748
- if (!hasValidOrigin(request, port)) return "bad-origin";
10749
- 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");
10750
11035
  return null;
10751
11036
  }
10752
11037
 
10753
- // packages/server/src/repo-hub.ts
10754
- import { EventEmitter as EventEmitter2 } from "node:events";
10755
- import chokidar from "chokidar";
10756
-
10757
- // packages/server/src/git-repo.ts
10758
- import { execFile as execFile3 } from "node:child_process";
10759
- import { readFile as readFile9 } from "node:fs/promises";
10760
- import path30 from "node:path";
10761
- function gitOutputText(output, command) {
10762
- return output.length > 0 ? serverText("raw", { text: output }) : serverText("gitCommandFailed", { command });
10763
- }
10764
- var MAX_CHANGES = 2e3;
10765
- var MAX_DIFF_LINES = 4e3;
10766
- var MAX_DIFF_BYTES = 512 * 1024;
10767
- var GIT_TIMEOUT_MS = 15e3;
10768
- var GIT_MAX_BUFFER = 32 * 1024 * 1024;
10769
- function isMissingBinary(error) {
10770
- return typeof error === "object" && error !== null && error.code === "ENOENT";
10771
- }
10772
- var GitMissingError = class extends Error {
10773
- constructor() {
10774
- super("git wasn't found in the PATH.");
10775
- this.name = "GitMissingError";
10776
- }
10777
- };
10778
- function runGit(cwd, args) {
10779
- return new Promise((resolve, reject) => {
10780
- execFile3(
10781
- "git",
10782
- [
10783
- // Sin esto git escapa los no-ASCII en octal y un archivo con acento
10784
- // vuelve ilegible.
10785
- "-c",
10786
- "core.quotepath=false",
10787
- // Esta app corre `git status` sola, cada pocos segundos, mientras el
10788
- // usuario trabaja. Sin esta bandera `status` refresca y **escribe** el
10789
- // indice, y eso trae dos problemas: toma `index.lock`, con lo que puede
10790
- // hacer fallar un `git add` que el usuario esta escribiendo en la
10791
- // terminal, y modifica `.git/index`, que es justo lo que observa
10792
- // nuestro watcher — o sea que cada lectura se dispara a si misma.
10793
- "--no-optional-locks",
10794
- ...args
10795
- ],
10796
- {
10797
- cwd,
10798
- timeout: GIT_TIMEOUT_MS,
10799
- maxBuffer: GIT_MAX_BUFFER,
10800
- windowsHide: true,
10801
- encoding: "utf8"
10802
- },
10803
- (error, stdout, stderr) => {
10804
- if (error !== null && isMissingBinary(error)) {
10805
- reject(new GitMissingError());
10806
- return;
10807
- }
10808
- const code = error !== null && typeof error.code === "number" ? error.code : error !== null ? 1 : 0;
10809
- resolve({ stdout, stderr, code });
10810
- }
10811
- );
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
+ });
10812
11047
  });
10813
11048
  }
10814
- function toPosix(value) {
10815
- 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())] };
10816
11057
  }
10817
- async function resolveRepo(cwd) {
10818
- const result = await runGit(cwd, ["rev-parse", "--show-toplevel"]);
10819
- if (result.code === 0) {
10820
- const root = result.stdout.trim();
10821
- if (root.length > 0) return { state: "ready", root: path30.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 });
10822
11101
  }
10823
- const stderr = result.stderr.trim();
10824
- if (/not a git repository|no es un repositorio/i.test(stderr)) {
10825
- 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();
10826
11106
  }
10827
- 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();
10828
11125
  return {
10829
- state: "error",
10830
- root: "",
10831
- 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
+ }))
10832
11137
  };
10833
11138
  }
10834
- return {
10835
- state: "error",
10836
- root: "",
10837
- message: gitOutputText(stderr.slice(0, 400), "rev-parse")
10838
- };
10839
- }
10840
- async function findRepoRoot(cwd) {
10841
- const lookup = await resolveRepo(cwd);
10842
- return lookup.state === "ready" ? lookup.root : null;
10843
- }
10844
- async function findGitDir(cwd) {
10845
- const result = await runGit(cwd, ["rev-parse", "--absolute-git-dir"]);
10846
- if (result.code !== 0) return null;
10847
- const dir = result.stdout.trim();
10848
- return dir.length > 0 ? path30.resolve(dir) : null;
10849
- }
10850
- function letterToKind(letter) {
10851
- switch (letter) {
10852
- case "A":
10853
- return "added";
10854
- case "M":
10855
- return "modified";
10856
- case "D":
10857
- return "deleted";
10858
- case "R":
10859
- return "renamed";
10860
- case "C":
10861
- return "copied";
10862
- case "T":
10863
- return "type-changed";
10864
- default:
10865
- return "modified";
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;
10866
11149
  }
10867
- }
10868
- function parsePorcelainV2(raw) {
10869
- const fields = raw.split("\0");
10870
- const parsed = {
10871
- branch: null,
10872
- head: null,
10873
- upstream: null,
10874
- ahead: 0,
10875
- behind: 0,
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":
11823
+ return "deleted";
11824
+ case "R":
11825
+ return "renamed";
11826
+ case "C":
11827
+ return "copied";
11828
+ case "T":
11829
+ return "type-changed";
11830
+ default:
11831
+ return "modified";
11832
+ }
11833
+ }
11834
+ function parsePorcelainV2(raw) {
11835
+ const fields = raw.split("\0");
11836
+ const parsed = {
11837
+ branch: null,
11838
+ head: null,
11839
+ upstream: null,
11840
+ ahead: 0,
11841
+ behind: 0,
10876
11842
  changes: [],
10877
11843
  truncated: false
10878
11844
  };
@@ -10948,7 +11914,7 @@ function parseWorktrees(raw, currentRoot) {
10948
11914
  worktrees.push({
10949
11915
  path: current.path,
10950
11916
  branch: current.branch,
10951
- isCurrent: path30.resolve(current.path) === path30.resolve(currentRoot)
11917
+ isCurrent: path32.resolve(current.path) === path32.resolve(currentRoot)
10952
11918
  });
10953
11919
  current = null;
10954
11920
  };
@@ -11091,7 +12057,7 @@ function looksBinary(buffer) {
11091
12057
  async function readUntrackedAsDiff(absolutePath, relativePath) {
11092
12058
  let content;
11093
12059
  try {
11094
- content = await readFile9(absolutePath);
12060
+ content = await readFile10(absolutePath);
11095
12061
  } catch {
11096
12062
  return {
11097
12063
  path: relativePath,
@@ -11139,7 +12105,7 @@ async function readUntrackedAsDiff(absolutePath, relativePath) {
11139
12105
  async function readDiff(options) {
11140
12106
  const { repoRoot: repoRoot2, relativePath, staged, untracked } = options;
11141
12107
  if (untracked && !staged) {
11142
- return readUntrackedAsDiff(path30.join(repoRoot2, relativePath), relativePath);
12108
+ return readUntrackedAsDiff(path32.join(repoRoot2, relativePath), relativePath);
11143
12109
  }
11144
12110
  const args = [
11145
12111
  "diff",
@@ -11178,7 +12144,7 @@ async function filterIgnored(repoRoot2, relativePaths) {
11178
12144
  const ignored = /* @__PURE__ */ new Set();
11179
12145
  if (relativePaths.length === 0) return ignored;
11180
12146
  const result = await new Promise((resolve) => {
11181
- const child = execFile3(
12147
+ const child = execFile4(
11182
12148
  "git",
11183
12149
  ["-c", "core.quotepath=false", "check-ignore", "--stdin", "-z"],
11184
12150
  { cwd: repoRoot2, timeout: GIT_TIMEOUT_MS, maxBuffer: GIT_MAX_BUFFER, windowsHide: true },
@@ -11359,24 +12325,24 @@ var RepoHub = class extends EventEmitter2 {
11359
12325
  // packages/server/src/memory-hub.ts
11360
12326
  import { EventEmitter as EventEmitter3 } from "node:events";
11361
12327
  import { homedir as homedir10 } from "node:os";
11362
- import path32 from "node:path";
12328
+ import path34 from "node:path";
11363
12329
  import chokidar2 from "chokidar";
11364
12330
 
11365
12331
  // packages/server/src/memory-bridge.ts
11366
- import { execFile as execFile4 } from "node:child_process";
11367
- import { randomBytes as randomBytes3 } from "node:crypto";
12332
+ import { execFile as execFile5 } from "node:child_process";
12333
+ import { randomBytes as randomBytes4 } from "node:crypto";
11368
12334
  import {
11369
- mkdir as mkdir5,
12335
+ mkdir as mkdir7,
11370
12336
  open as open6,
11371
12337
  readdir as readdir9,
11372
- readFile as readFile10,
12338
+ readFile as readFile11,
11373
12339
  realpath as realpath2,
11374
- rename as rename3,
11375
- stat as stat15,
12340
+ rename as rename4,
12341
+ stat as stat17,
11376
12342
  unlink as unlink3
11377
12343
  } from "node:fs/promises";
11378
12344
  import { homedir as homedir9 } from "node:os";
11379
- import path31 from "node:path";
12345
+ import path33 from "node:path";
11380
12346
  var MemoryBridgeError = class extends ServerTextError {
11381
12347
  };
11382
12348
  var MAX_LISTED_NOTES = 500;
@@ -11394,9 +12360,9 @@ var GITIGNORE_MEMORY_LINE = `${MEMORY_DIR}/`;
11394
12360
  var BOM2 = Buffer.from([239, 187, 191]);
11395
12361
  async function inspectMemory(cwd, home = homedir9()) {
11396
12362
  await assertMemoryDirInside(cwd);
11397
- const folder = path31.join(cwd, MEMORY_DIR);
12363
+ const folder = path33.join(cwd, MEMORY_DIR);
11398
12364
  const folderExists = await isDirectory2(folder);
11399
- const indexExists = folderExists && await isFile(path31.join(folder, MEMORY_INDEX_FILE));
12365
+ const indexExists = folderExists && await isFile(path33.join(folder, MEMORY_INDEX_FILE));
11400
12366
  const noteNames = folderExists ? await listNoteFiles(folder) : [];
11401
12367
  const notes = await describeNotes(folder, noteNames);
11402
12368
  const files = await Promise.all(
@@ -11428,7 +12394,7 @@ async function installMemory(cwd, options, home = homedir9()) {
11428
12394
  }
11429
12395
  async function importNativeMemory(cwd, home = homedir9()) {
11430
12396
  await assertMemoryDirInside(cwd);
11431
- if (!await isDirectory2(path31.join(cwd, MEMORY_DIR))) {
12397
+ if (!await isDirectory2(path33.join(cwd, MEMORY_DIR))) {
11432
12398
  throw new MemoryBridgeError(serverText("memoryNotInstalled"));
11433
12399
  }
11434
12400
  const plan = new PlanBuilder(cwd);
@@ -11446,9 +12412,9 @@ async function readMemoryNote(cwd, name) {
11446
12412
  }
11447
12413
  const absolute = await resolveInside(cwd, `${MEMORY_DIR}/${name}`);
11448
12414
  try {
11449
- const info = await stat15(absolute);
12415
+ const info = await stat17(absolute);
11450
12416
  if (!info.isFile()) return null;
11451
- const content = await readFile10(absolute);
12417
+ const content = await readFile11(absolute);
11452
12418
  return {
11453
12419
  name,
11454
12420
  text: content.subarray(0, MAX_NOTE_BYTES).toString("utf8"),
@@ -11463,7 +12429,7 @@ function isNoteName(name) {
11463
12429
  if (name.length === 0 || name.length > 255) return false;
11464
12430
  if (name.includes("\0")) return false;
11465
12431
  if (name === "." || name === "..") return false;
11466
- if (path31.basename(name) !== name) return false;
12432
+ if (path33.basename(name) !== name) return false;
11467
12433
  if (/[\\/:*?"<>|\u0000-\u001f]/.test(name)) return false;
11468
12434
  return name.toLowerCase().endsWith(".md");
11469
12435
  }
@@ -11500,7 +12466,7 @@ var PlanBuilder = class {
11500
12466
  /** Nombres de nota que habria en la carpeta, en minusculas. */
11501
12467
  async noteNames() {
11502
12468
  const names = /* @__PURE__ */ new Map();
11503
- for (const name of await listNoteFiles(path31.join(this.cwd, MEMORY_DIR))) {
12469
+ for (const name of await listNoteFiles(path33.join(this.cwd, MEMORY_DIR))) {
11504
12470
  names.set(name.toLowerCase(), name);
11505
12471
  }
11506
12472
  for (const rel of this.files.keys()) {
@@ -11543,7 +12509,7 @@ var PlanBuilder = class {
11543
12509
  continue;
11544
12510
  }
11545
12511
  const target = await realpath2(absolute);
11546
- const info = await stat15(target);
12512
+ const info = await stat17(target);
11547
12513
  if (info.nlink > 1) {
11548
12514
  throw new MemoryBridgeError(serverText("memoryHardLinks", { file: file.rel }));
11549
12515
  }
@@ -11562,7 +12528,7 @@ var PlanBuilder = class {
11562
12528
  );
11563
12529
  if (pending.length === 0) return;
11564
12530
  if (pending.some((file) => file.rel.startsWith(`${MEMORY_DIR}/`))) {
11565
- await mkdir5(path31.join(this.cwd, MEMORY_DIR), { recursive: true });
12531
+ await mkdir7(path33.join(this.cwd, MEMORY_DIR), { recursive: true });
11566
12532
  }
11567
12533
  for (const file of pending) {
11568
12534
  if (file.target === null) throw new Error(`Unvalidated plan: ${file.rel}`);
@@ -11673,7 +12639,7 @@ async function planNativeImport(plan, cwd, home) {
11673
12639
  const local = await plan.noteNames();
11674
12640
  for (const name of await listNoteFiles(folder)) {
11675
12641
  const key = name.toLowerCase();
11676
- const content = await readIfFile(path31.join(folder, name), name);
12642
+ const content = await readIfFile(path33.join(folder, name), name);
11677
12643
  if (content === null) continue;
11678
12644
  const already = chosen.get(key);
11679
12645
  if (already !== void 0) {
@@ -11687,7 +12653,7 @@ async function planNativeImport(plan, cwd, home) {
11687
12653
  if (existing !== null && !existing.equals(content)) skipped.push(name);
11688
12654
  continue;
11689
12655
  }
11690
- plan.put(`${MEMORY_DIR}/${name}`, null, content, "copy", "", path31.join(folder, name));
12656
+ plan.put(`${MEMORY_DIR}/${name}`, null, content, "copy", "", path33.join(folder, name));
11691
12657
  copied.push(name);
11692
12658
  }
11693
12659
  }
@@ -11697,7 +12663,7 @@ async function planNativeImport(plan, cwd, home) {
11697
12663
  const known = new Set(linkTargets(index.toString("utf8")));
11698
12664
  const additions = [];
11699
12665
  for (const folder of folders) {
11700
- const nativeIndex = await readIfFile(path31.join(folder, MEMORY_INDEX_FILE), MEMORY_INDEX_FILE);
12666
+ const nativeIndex = await readIfFile(path33.join(folder, MEMORY_INDEX_FILE), MEMORY_INDEX_FILE);
11701
12667
  if (nativeIndex === null) continue;
11702
12668
  for (const line of nativeIndex.toString("utf8").replace(/^\uFEFF/, "").split(/\r?\n/)) {
11703
12669
  const target = linkTargets(line)[0];
@@ -11922,7 +12888,7 @@ async function describeNotes(folder, names) {
11922
12888
  const stated = [];
11923
12889
  for (const name of names) {
11924
12890
  try {
11925
- const info = await stat15(path31.join(folder, name));
12891
+ const info = await stat17(path33.join(folder, name));
11926
12892
  if (info.isFile()) stated.push({ name, sizeBytes: info.size, modifiedAt: info.mtimeMs });
11927
12893
  } catch {
11928
12894
  }
@@ -11930,7 +12896,7 @@ async function describeNotes(folder, names) {
11930
12896
  stated.sort((a, b) => b.modifiedAt - a.modifiedAt || a.name.localeCompare(b.name));
11931
12897
  const notes = [];
11932
12898
  for (const item of stated.slice(0, MAX_LISTED_NOTES)) {
11933
- const head = await readHead(path31.join(folder, item.name));
12899
+ const head = await readHead(path33.join(folder, item.name));
11934
12900
  notes.push({ ...item, ...describeNoteText(head, item.name) });
11935
12901
  }
11936
12902
  return notes;
@@ -11988,28 +12954,28 @@ async function countNativeMemory(cwd, home, sharedNames) {
11988
12954
  return { available: names.size, pending };
11989
12955
  }
11990
12956
  async function nativeMemoryFolders(cwd, home) {
11991
- const root = path31.join(home, ".claude", "projects");
12957
+ const root = path33.join(home, ".claude", "projects");
11992
12958
  const slug = projectSlugFor(cwd).toLowerCase();
11993
12959
  try {
11994
12960
  const entries = await readdir9(root, { withFileTypes: true });
11995
- return entries.filter((entry) => entry.isDirectory() && entry.name.toLowerCase() === slug).map((entry) => entry.name).sort().map((name) => path31.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"));
11996
12962
  } catch {
11997
12963
  return [];
11998
12964
  }
11999
12965
  }
12000
12966
  function buildGlobalFragments(home) {
12001
12967
  const windows = process.platform === "win32";
12002
- const globalFile = path31.join(home, ...GLOBAL_MEMORY_RELATIVE.split("/"));
12968
+ const globalFile = path33.join(home, ...GLOBAL_MEMORY_RELATIVE.split("/"));
12003
12969
  const hardLink = (link) => windows ? `mklink /H "${link}" "${globalFile}"` : `ln "${globalFile}" "${link}"`;
12004
12970
  const hardLinkNote = windows ? serverText("memoryNoteHardLinkCmd", { path: GLOBAL_MEMORY_RELATIVE }) : serverText("memoryNoteHardLink", { path: GLOBAL_MEMORY_RELATIVE });
12005
- const codexTarget = path31.join(home, ".codex", "AGENTS.md");
12006
- const geminiTarget = path31.join(home, ".gemini", "GEMINI.md");
12971
+ const codexTarget = path33.join(home, ".codex", "AGENTS.md");
12972
+ const geminiTarget = path33.join(home, ".gemini", "GEMINI.md");
12007
12973
  const homeSlashes = home.replace(/\\/g, "/").replace(/\/+$/, "");
12008
12974
  return [
12009
12975
  {
12010
12976
  agent: "claude-code",
12011
12977
  label: MEMORY_AGENT_LABELS["claude-code"],
12012
- target: path31.join(home, ".claude", "CLAUDE.md"),
12978
+ target: path33.join(home, ".claude", "CLAUDE.md"),
12013
12979
  kind: "line",
12014
12980
  text: `@~/${GLOBAL_MEMORY_RELATIVE}`,
12015
12981
  note: serverText("memoryNoteClaude")
@@ -12033,7 +12999,7 @@ function buildGlobalFragments(home) {
12033
12999
  {
12034
13000
  agent: "opencode",
12035
13001
  label: MEMORY_AGENT_LABELS.opencode,
12036
- target: path31.join(home, ".config", "opencode", "opencode.json"),
13002
+ target: path33.join(home, ".config", "opencode", "opencode.json"),
12037
13003
  kind: "json",
12038
13004
  text: `"instructions": ["${homeSlashes}/${GLOBAL_MEMORY_RELATIVE}"]`,
12039
13005
  note: serverText("memoryNoteOpenCode")
@@ -12042,7 +13008,7 @@ function buildGlobalFragments(home) {
12042
13008
  }
12043
13009
  function runGit2(cwd, args) {
12044
13010
  return new Promise((resolve) => {
12045
- execFile4(
13011
+ execFile5(
12046
13012
  "git",
12047
13013
  ["-c", "core.quotepath=false", "--no-optional-locks", ...args],
12048
13014
  { cwd, timeout: GIT_TIMEOUT_MS2, windowsHide: true, encoding: "utf8", maxBuffer: 1024 * 1024 },
@@ -12081,17 +13047,17 @@ async function readGit(cwd) {
12081
13047
  if (!toplevel || !gitDir || !commonDir) return fail(serverText("gitUnexpected", { command: "rev-parse" }));
12082
13048
  const ignored = await checkIgnored(cwd, INDEX_REL);
12083
13049
  if (typeof ignored !== "boolean") return fail(ignored);
12084
- const absoluteGitDir = path31.resolve(cwd, gitDir);
12085
- const absoluteCommonDir = path31.resolve(cwd, commonDir);
13050
+ const absoluteGitDir = path33.resolve(cwd, gitDir);
13051
+ const absoluteCommonDir = path33.resolve(cwd, commonDir);
12086
13052
  let worktree = null;
12087
13053
  let main2 = null;
12088
13054
  if (!await samePath(absoluteGitDir, absoluteCommonDir)) {
12089
- if (path31.basename(absoluteCommonDir).toLowerCase() === ".git") {
12090
- const mainRoot = path31.dirname(absoluteCommonDir);
12091
- const inner = path31.relative(path31.resolve(toplevel), path31.resolve(cwd));
12092
- const sameFolder = inner.length === 0 || inner.startsWith("..") || path31.isAbsolute(inner);
12093
- const mainPath = sameFolder ? mainRoot : path31.join(mainRoot, inner);
12094
- const memoryRel = sameFolder ? MEMORY_DIR : [...inner.split(path31.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("/");
12095
13061
  main2 = { root: mainRoot, memoryRel };
12096
13062
  worktree = {
12097
13063
  mainPath,
@@ -12116,7 +13082,7 @@ async function samePath(a, b) {
12116
13082
  try {
12117
13083
  return await realpath2(value);
12118
13084
  } catch {
12119
- return path31.resolve(value);
13085
+ return path33.resolve(value);
12120
13086
  }
12121
13087
  };
12122
13088
  const [left, right] = await Promise.all([resolve(a), resolve(b)]);
@@ -12158,9 +13124,9 @@ async function listNoteFiles(folder) {
12158
13124
  }
12159
13125
  async function readIfFile(absolute, label) {
12160
13126
  try {
12161
- const info = await stat15(absolute);
13127
+ const info = await stat17(absolute);
12162
13128
  if (!info.isFile()) throw new MemoryBridgeError(serverText("memoryNotAFile", { label }));
12163
- return await readFile10(absolute);
13129
+ return await readFile11(absolute);
12164
13130
  } catch (error) {
12165
13131
  if (isNotFound(error)) return null;
12166
13132
  throw error;
@@ -12182,14 +13148,14 @@ async function readHead(absolute) {
12182
13148
  }
12183
13149
  async function isFile(absolute) {
12184
13150
  try {
12185
- return (await stat15(absolute)).isFile();
13151
+ return (await stat17(absolute)).isFile();
12186
13152
  } catch {
12187
13153
  return false;
12188
13154
  }
12189
13155
  }
12190
13156
  async function isDirectory2(absolute) {
12191
13157
  try {
12192
- return (await stat15(absolute)).isDirectory();
13158
+ return (await stat17(absolute)).isDirectory();
12193
13159
  } catch {
12194
13160
  return false;
12195
13161
  }
@@ -12207,7 +13173,7 @@ async function writeAtomic(target, content, mustNotExist) {
12207
13173
  await handle.close();
12208
13174
  }
12209
13175
  if (mustNotExist && await exists2(target)) {
12210
- throw new MemoryBridgeError(serverText("memoryAppeared", { file: path31.basename(target) }));
13176
+ throw new MemoryBridgeError(serverText("memoryAppeared", { file: path33.basename(target) }));
12211
13177
  }
12212
13178
  await renameWithRetry(temporary, target);
12213
13179
  } catch (error) {
@@ -12217,10 +13183,10 @@ async function writeAtomic(target, content, mustNotExist) {
12217
13183
  }
12218
13184
  async function createTemporary(target) {
12219
13185
  for (let attempt = 1; ; attempt += 1) {
12220
- const suffix = randomBytes3(6).toString("hex");
12221
- const temporary = path31.join(
12222
- path31.dirname(target),
12223
- `.${path31.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`
12224
13190
  );
12225
13191
  try {
12226
13192
  return { temporary, handle: await open6(temporary, "wx") };
@@ -12232,7 +13198,7 @@ async function createTemporary(target) {
12232
13198
  }
12233
13199
  async function exists2(absolute) {
12234
13200
  try {
12235
- await stat15(absolute);
13201
+ await stat17(absolute);
12236
13202
  return true;
12237
13203
  } catch {
12238
13204
  return false;
@@ -12241,7 +13207,7 @@ async function exists2(absolute) {
12241
13207
  async function renameWithRetry(from, to) {
12242
13208
  for (let attempt = 1; ; attempt += 1) {
12243
13209
  try {
12244
- await rename3(from, to);
13210
+ await rename4(from, to);
12245
13211
  return;
12246
13212
  } catch (error) {
12247
13213
  const code = error.code;
@@ -12458,13 +13424,13 @@ var MemoryHub = class extends EventEmitter3 {
12458
13424
  }
12459
13425
  };
12460
13426
  function keyFor(cwd) {
12461
- const resolved = path32.resolve(cwd);
13427
+ const resolved = path34.resolve(cwd);
12462
13428
  return process.platform === "win32" ? resolved.toLowerCase() : resolved;
12463
13429
  }
12464
13430
  function isRelevantPath(cwd, candidate) {
12465
- const relative = path32.relative(cwd, candidate);
13431
+ const relative = path34.relative(cwd, candidate);
12466
13432
  if (relative.length === 0) return true;
12467
- if (relative.startsWith("..") || path32.isAbsolute(relative)) return false;
13433
+ if (relative.startsWith("..") || path34.isAbsolute(relative)) return false;
12468
13434
  const same = (a, b) => process.platform === "win32" ? a.toLowerCase() === b.toLowerCase() : a === b;
12469
13435
  const parts = relative.split(/[\\/]/);
12470
13436
  const [first = "", second = "", third = ""] = parts;
@@ -12481,10 +13447,10 @@ function isRelevantPath(cwd, candidate) {
12481
13447
  }
12482
13448
 
12483
13449
  // packages/server/src/paste-store.ts
12484
- import { createHash, randomUUID as randomUUID2 } from "node:crypto";
12485
- 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";
12486
13452
  import { tmpdir as tmpdir4 } from "node:os";
12487
- import path33 from "node:path";
13453
+ import path35 from "node:path";
12488
13454
 
12489
13455
  // packages/server/src/vault/markdown.ts
12490
13456
  var pad2 = (value) => String(value).padStart(2, "0");
@@ -12999,8 +13965,8 @@ function buildContinuationMessage(input) {
12999
13965
  }
13000
13966
  return sanitizeForPaste(lines.join("\n"));
13001
13967
  }
13002
- function transcriptReferenceFor(path48, style) {
13003
- return style === "at-quoted" ? fileReference(path48, "at-quoted") : `"${path48}"`;
13968
+ function transcriptReferenceFor(path50, style) {
13969
+ return style === "at-quoted" ? fileReference(path50, "at-quoted") : `"${path50}"`;
13004
13970
  }
13005
13971
  function planTranscript(input) {
13006
13972
  const turns = splitTurns(input.events);
@@ -13175,7 +14141,7 @@ var PasteStore = class {
13175
14141
  root;
13176
14142
  counter = 0;
13177
14143
  constructor(root) {
13178
- this.root = root ?? path33.join(tmpdir4(), "agent-workbench", "pasted");
14144
+ this.root = root ?? path35.join(tmpdir4(), "agent-workbench", "pasted");
13179
14145
  }
13180
14146
  /**
13181
14147
  * Guarda una imagen y devuelve su ruta.
@@ -13202,12 +14168,12 @@ var PasteStore = class {
13202
14168
  if (signature === null) {
13203
14169
  throw new PasteImageError(serverText("imageUnknownFormat"));
13204
14170
  }
13205
- const directory = path33.join(this.root, safeSegment(terminalId));
13206
- 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 });
13207
14173
  this.counter += 1;
13208
- const name = `pegada-${this.counter}-${randomUUID2().slice(0, 8)}.${signature.ext}`;
13209
- const file = path33.join(directory, name);
13210
- 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 });
13211
14177
  return { path: file, bytes: bytes.length };
13212
14178
  }
13213
14179
  /**
@@ -13224,12 +14190,12 @@ var PasteStore = class {
13224
14190
  if (bytes.length > MAX_TEXT_BYTES) {
13225
14191
  throw new PasteTextError(`The text is ${bytes.length} bytes; the maximum is ${MAX_TEXT_BYTES}.`);
13226
14192
  }
13227
- const directory = path33.join(this.root, safeSegment(terminalId));
13228
- 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 });
13229
14195
  this.counter += 1;
13230
- const name = `${baseName}-${this.counter}-${randomUUID2().slice(0, 8)}.md`;
13231
- const file = path33.join(directory, name);
13232
- 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 });
13233
14199
  return { path: file, bytes: bytes.length };
13234
14200
  }
13235
14201
  /**
@@ -13258,17 +14224,17 @@ var PasteStore = class {
13258
14224
  })
13259
14225
  );
13260
14226
  }
13261
- const directory = path33.join(this.root, safeSegment(terminalId));
13262
- 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 });
13263
14229
  this.counter += 1;
13264
- const name = `adjunto-${this.counter}-${randomUUID2().slice(0, 8)}-${safe}`;
13265
- const file = path33.join(directory, name);
13266
- 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 });
13267
14233
  return { path: file, bytes: bytes.length, name };
13268
14234
  }
13269
14235
  /** Borra lo de una pestana. Se llama al cerrarla. */
13270
14236
  async clearTerminal(terminalId) {
13271
- await rm2(path33.join(this.root, safeSegment(terminalId)), {
14237
+ await rm3(path35.join(this.root, safeSegment(terminalId)), {
13272
14238
  recursive: true,
13273
14239
  force: true
13274
14240
  }).catch(() => {
@@ -13290,11 +14256,11 @@ var PasteStore = class {
13290
14256
  }
13291
14257
  await Promise.all(
13292
14258
  entries.map(async (entry) => {
13293
- const full = path33.join(this.root, entry);
14259
+ const full = path35.join(this.root, entry);
13294
14260
  try {
13295
- const info = await stat16(full);
14261
+ const info = await stat18(full);
13296
14262
  if (now - info.mtimeMs > STALE_AFTER_MS) {
13297
- await rm2(full, { recursive: true, force: true });
14263
+ await rm3(full, { recursive: true, force: true });
13298
14264
  }
13299
14265
  } catch {
13300
14266
  }
@@ -13304,13 +14270,13 @@ var PasteStore = class {
13304
14270
  };
13305
14271
  function safeSegment(value) {
13306
14272
  const clean = value.replace(/[^a-zA-Z0-9_-]/g, "");
13307
- 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);
13308
14274
  }
13309
14275
 
13310
14276
  // packages/server/src/session-index.ts
13311
14277
  import { EventEmitter as EventEmitter4 } from "node:events";
13312
- import { mkdir as mkdir7, readFile as readFile11, stat as stat17, writeFile as writeFile4 } from "node:fs/promises";
13313
- import path34 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";
13314
14280
  var CACHE_VERSION = 4;
13315
14281
  var vaultPairKey = (agent, sessionId) => `${agent}/${sessionId}`;
13316
14282
  function vaultOnlySessions(natives, vault, listState, ready) {
@@ -13359,7 +14325,7 @@ function cacheKey(agent, ref) {
13359
14325
  }
13360
14326
  async function pathExists3(candidate) {
13361
14327
  try {
13362
- await stat17(candidate);
14328
+ await stat19(candidate);
13363
14329
  return true;
13364
14330
  } catch {
13365
14331
  return false;
@@ -13409,7 +14375,7 @@ function migrateEntryV3(filePath, value) {
13409
14375
  return {
13410
14376
  agent: "claude-code",
13411
14377
  ref: filePath,
13412
- group: path34.basename(path34.dirname(filePath)),
14378
+ group: path36.basename(path36.dirname(filePath)),
13413
14379
  mtimeMs,
13414
14380
  sizeBytes,
13415
14381
  cwd,
@@ -13674,7 +14640,7 @@ var SessionIndex = class extends EventEmitter4 {
13674
14640
  }
13675
14641
  async loadCache() {
13676
14642
  try {
13677
- const raw = await readFile11(sessionIndexCachePath(), "utf8");
14643
+ const raw = await readFile12(sessionIndexCachePath(), "utf8");
13678
14644
  const parsed = JSON.parse(raw);
13679
14645
  const migrated = migrateIndexCache(parsed);
13680
14646
  if (migrated === null) return;
@@ -13687,8 +14653,8 @@ var SessionIndex = class extends EventEmitter4 {
13687
14653
  if (!this.cacheDirty) return;
13688
14654
  this.cacheDirty = false;
13689
14655
  try {
13690
- await mkdir7(appConfigDir(), { recursive: true });
13691
- await writeFile4(sessionIndexCachePath(), JSON.stringify(this.cache), "utf8");
14656
+ await mkdir9(appConfigDir(), { recursive: true });
14657
+ await writeFile5(sessionIndexCachePath(), JSON.stringify(this.cache), "utf8");
13692
14658
  } catch (error) {
13693
14659
  console.warn("[index] couldn't save the cache:", error);
13694
14660
  }
@@ -13865,199 +14831,42 @@ var SessionIndex = class extends EventEmitter4 {
13865
14831
  let refs;
13866
14832
  try {
13867
14833
  refs = await history.changedRefs(filePath);
13868
- } catch {
13869
- refs = null;
13870
- }
13871
- if (refs === null || refs.length === 0) return;
13872
- let changed = false;
13873
- for (const ref of refs) {
13874
- const key = cacheKey(agent, ref);
13875
- const cachedBefore = this.cache.entries[key];
13876
- if (cachedBefore !== void 0) {
13877
- delete this.cache.entries[key];
13878
- this.cacheDirty = true;
13879
- }
13880
- let indexed;
13881
- try {
13882
- const item = await history.item(ref);
13883
- indexed = item === null ? null : await this.readOrThrow(agent, history, item);
13884
- } catch {
13885
- if (cachedBefore !== void 0 && !(key in this.cache.entries)) this.cache.entries[key] = cachedBefore;
13886
- continue;
13887
- }
13888
- if (indexed === null) {
13889
- if (this.sessions.delete(key)) changed = true;
13890
- continue;
13891
- }
13892
- if (indexed.cwd !== null) this.cwdExists.set(indexed.cwd, await pathExists3(indexed.cwd));
13893
- this.sessions.set(key, indexed);
13894
- changed = true;
13895
- }
13896
- if (changed) this.emitProjects();
13897
- await this.saveCache();
13898
- }
13899
- };
13900
-
13901
- // packages/server/src/settings-store.ts
13902
- import { mkdir as mkdir9, readFile as readFile12 } from "node:fs/promises";
13903
- import path36 from "node:path";
13904
-
13905
- // packages/server/src/vault/write.ts
13906
- import { randomBytes as randomBytes4 } from "node:crypto";
13907
- import { mkdir as mkdir8, rename as rename4, rm as rm3, stat as stat18, writeFile as writeFile5 } from "node:fs/promises";
13908
-
13909
- // packages/server/src/vault/paths.ts
13910
- import { createHash as createHash2 } from "node:crypto";
13911
- import path35 from "node:path";
13912
- function defaultVaultDir() {
13913
- return path35.join(appConfigDir(), "vault");
13914
- }
13915
- function vaultMarkerFile(dir) {
13916
- return path35.join(dir, "vault.json");
13917
- }
13918
- function sessionsRoot2(dir) {
13919
- return path35.join(dir, "sessions");
13920
- }
13921
- function isSafeSessionId(sessionId) {
13922
- return isVaultSessionId(sessionId);
13923
- }
13924
- function isSessionAgentId(value) {
13925
- return SESSION_AGENT_IDS.includes(value);
13926
- }
13927
- function assertSessionKey(agent, sessionId) {
13928
- if (!isSessionAgentId(agent)) throw new Error(`Unknown source for the local copy: ${JSON.stringify(agent)}`);
13929
- if (!isSafeSessionId(sessionId)) throw new Error(`Session id that isn't copied: ${JSON.stringify(sessionId)}`);
13930
- }
13931
- function agentSessionsDir(dir, agent) {
13932
- if (!isSessionAgentId(agent)) throw new Error(`Unknown source for the local copy: ${JSON.stringify(agent)}`);
13933
- return path35.join(sessionsRoot2(dir), agent);
13934
- }
13935
- function sessionFile(dir, agent, sessionId) {
13936
- assertSessionKey(agent, sessionId);
13937
- return path35.join(sessionsRoot2(dir), agent, `${sessionId}.jsonl`);
13938
- }
13939
- function assetsDir(dir, agent, sessionId) {
13940
- assertSessionKey(agent, sessionId);
13941
- return path35.join(sessionsRoot2(dir), agent, `${sessionId}.assets`);
13942
- }
13943
- function assetFile(dir, agent, sessionId, asset) {
13944
- if (!VAULT_ASSET_NAME_PATTERN.test(asset)) throw new Error(`Invalid asset name: ${JSON.stringify(asset)}`);
13945
- return path35.join(assetsDir(dir, agent, sessionId), asset);
13946
- }
13947
- function projectFolderName(cwd, platform) {
13948
- const hash = createHash2("sha256").update(normalizeCwdKey(cwd, platform)).digest("hex").slice(0, 8);
13949
- const base = (platform === "win32" ? path35.win32 : path35.posix).basename(cwd);
13950
- const name = base.length === 0 ? "proyecto" : sanitizeFileName(base, 60);
13951
- return `${name}-${hash}`;
13952
- }
13953
- function memoryDir(dir, cwd, platform) {
13954
- return path35.join(dir, "memory", projectFolderName(cwd, platform));
13955
- }
13956
- function projectExportDir(dir, cwd, platform) {
13957
- return path35.join(dir, "export", projectFolderName(cwd, platform));
13958
- }
13959
- function projectExportDirFor(dir, project, platform) {
13960
- if (project.cwd.length > 0) return projectExportDir(dir, project.cwd, platform);
13961
- const hash = createHash2("sha256").update(project.key).digest("hex").slice(0, 8);
13962
- const name = project.fallbackName.length > 0 ? sanitizeFileName(project.fallbackName, 60) : "proyecto";
13963
- return path35.join(dir, "export", `${name}-${hash}`);
13964
- }
13965
- function sessionExportFile(dir, agent, sessionId) {
13966
- assertSessionKey(agent, sessionId);
13967
- return path35.join(dir, "export", "sesiones", `${agent}-${sessionId}.md`);
13968
- }
13969
- var RESERVED_WINDOWS_NAME = /^(con|prn|aux|nul|com[1-9]|lpt[1-9])$/i;
13970
- function sanitizeFileName(text, max = 80) {
13971
- const limit = Math.max(1, Math.floor(max));
13972
- const kept = text.normalize("NFC").replace(/[^\p{L}\p{M}\p{N} ._-]/gu, "_");
13973
- const trimEnds = (value) => value.replace(/^ +/u, "").replace(/[. ]+$/u, "");
13974
- let name = trimEnds(Array.from(trimEnds(kept)).slice(0, limit).join(""));
13975
- if (name.length === 0) return "_";
13976
- const stem = (name.split(".")[0] ?? "").replace(/ +$/u, "");
13977
- if (RESERVED_WINDOWS_NAME.test(stem)) {
13978
- name = `_${Array.from(name).slice(0, limit - 1).join("")}`;
13979
- name = name.replace(/[. ]+$/u, "");
13980
- }
13981
- return name;
13982
- }
13983
-
13984
- // packages/server/src/vault/write.ts
13985
- var RENAME_RETRIES = 3;
13986
- var RENAME_RETRY_MS = 100;
13987
- var DEFAULT_WRITE_DEPS = {
13988
- rename: rename4,
13989
- platform: process.platform,
13990
- delay: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
13991
- now: () => Date.now()
13992
- };
13993
- function temporaryPathFor(target) {
13994
- return `${target}.${process.pid}.${randomBytes4(6).toString("hex")}.tmp`;
13995
- }
13996
- function isRetryableRename(error, platform) {
13997
- if (platform !== "win32") return false;
13998
- const code = error?.code;
13999
- return code === "EPERM" || code === "EBUSY";
14000
- }
14001
- async function writeFileAtomic(target, data, deps = DEFAULT_WRITE_DEPS) {
14002
- const temporary = temporaryPathFor(target);
14003
- try {
14004
- await writeFile5(temporary, data);
14005
- for (let attempt = 0; ; attempt += 1) {
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;
14006
14847
  try {
14007
- await deps.rename(temporary, target);
14008
- return;
14009
- } catch (error) {
14010
- if (attempt >= RENAME_RETRIES || !isRetryableRename(error, deps.platform)) throw error;
14011
- 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;
14012
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;
14013
14861
  }
14014
- } catch (error) {
14015
- await rm3(temporary, { force: true }).catch(() => void 0);
14016
- throw error;
14017
- }
14018
- }
14019
- async function ensureVaultMarker(dir, deps = DEFAULT_WRITE_DEPS) {
14020
- const marker = vaultMarkerFile(dir);
14021
- try {
14022
- await stat18(marker);
14023
- return;
14024
- } catch {
14025
- }
14026
- await mkdir8(dir, { recursive: true });
14027
- await writeFileAtomic(marker, `${JSON.stringify({ format: VAULT_FORMAT, createdAt: deps.now() })}
14028
- `, deps);
14029
- }
14030
- async function assetPresent(file, size) {
14031
- try {
14032
- const info = await stat18(file);
14033
- return info.isFile() && info.size === size;
14034
- } catch {
14035
- return false;
14036
- }
14037
- }
14038
- async function writeSessionFile(dir, serialized, deps = DEFAULT_WRITE_DEPS) {
14039
- const { agent, sessionId } = serialized.header;
14040
- const file = sessionFile(dir, agent, sessionId);
14041
- await ensureVaultMarker(dir, deps);
14042
- await mkdir8(agentSessionsDir(dir, agent), { recursive: true });
14043
- let assetsWritten = 0;
14044
- let assetBytesWritten = 0;
14045
- if (serialized.assets.size > 0) {
14046
- await mkdir8(assetsDir(dir, agent, sessionId), { recursive: true });
14047
- for (const [name, bytes] of serialized.assets) {
14048
- const target = assetFile(dir, agent, sessionId, name);
14049
- if (await assetPresent(target, bytes.length)) continue;
14050
- await writeFileAtomic(target, bytes, deps);
14051
- assetsWritten += 1;
14052
- assetBytesWritten += bytes.length;
14053
- }
14862
+ if (changed) this.emitProjects();
14863
+ await this.saveCache();
14054
14864
  }
14055
- const text = Buffer.from(serialized.text, "utf8");
14056
- await writeFileAtomic(file, text, deps);
14057
- return { file, bytes: text.length, assetsWritten, assetBytesWritten };
14058
- }
14865
+ };
14059
14866
 
14060
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";
14061
14870
  var SETTINGS_VERSION = 1;
14062
14871
  var VAULT_TOOL_RESULT_DEFAULT_CHARS = 64e3;
14063
14872
  var VAULT_TOOL_RESULT_MIN_CHARS = 4e3;
@@ -14065,13 +14874,14 @@ var VAULT_TOOL_RESULT_MAX_CHARS = 1e6;
14065
14874
  function defaultAppSettings() {
14066
14875
  return {
14067
14876
  version: SETTINGS_VERSION,
14068
- 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 }
14069
14879
  };
14070
14880
  }
14071
14881
  function isAbsoluteDir(dir, platform) {
14072
14882
  if (dir.length === 0 || dir.includes("\0")) return false;
14073
14883
  if (platform === "win32") return /^[A-Za-z]:[\\/]/.test(dir) || /^\\\\[^\\/]+[\\/][^\\/]+/.test(dir);
14074
- return path36.posix.isAbsolute(dir);
14884
+ return path37.posix.isAbsolute(dir);
14075
14885
  }
14076
14886
  function clampToolResultMaxChars(value) {
14077
14887
  if (typeof value !== "number" || !Number.isFinite(value)) return VAULT_TOOL_RESULT_DEFAULT_CHARS;
@@ -14081,6 +14891,11 @@ function parseAppSettings(value, platform, warn = () => void 0) {
14081
14891
  const record = asRecord(value);
14082
14892
  if (record === null || record["version"] !== SETTINGS_VERSION) return null;
14083
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
+ }
14084
14899
  const vault = asRecord(record["vault"]);
14085
14900
  if (vault === null) return settings2;
14086
14901
  settings2.vault.enabled = asBoolean(vault["enabled"]) ?? false;
@@ -14094,7 +14909,11 @@ function parseAppSettings(value, platform, warn = () => void 0) {
14094
14909
  return settings2;
14095
14910
  }
14096
14911
  function frozen(settings2) {
14097
- 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
+ });
14098
14917
  }
14099
14918
  var SettingsStore = class {
14100
14919
  /** La ruta es inyectable para las pruebas; por defecto, la de la carpeta de configuracion. */
@@ -14113,7 +14932,7 @@ var SettingsStore = class {
14113
14932
  async load() {
14114
14933
  let raw;
14115
14934
  try {
14116
- raw = await readFile12(this.filePath, "utf8");
14935
+ raw = await readFile13(this.filePath, "utf8");
14117
14936
  } catch {
14118
14937
  this.current = frozen(defaultAppSettings());
14119
14938
  return;
@@ -14132,17 +14951,49 @@ var SettingsStore = class {
14132
14951
  }
14133
14952
  this.current = frozen(settings2 ?? defaultAppSettings());
14134
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
+ }
14135
14980
  /** Lo ultimo que quedo escrito. Congelado: nadie de afuera lo cambia sin pasar por `update`. */
14136
14981
  get() {
14137
14982
  return this.current;
14138
14983
  }
14139
14984
  /**
14140
- * Aplica un cambio y lo escribe. Lanza si la carpeta no es absoluta o si no
14141
- * 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.
14142
14988
  */
14143
14989
  update(patch) {
14144
14990
  const run = async () => {
14145
- 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
+ };
14146
14997
  const vault = patch.vault ?? {};
14147
14998
  if (vault.enabled !== void 0) next.vault.enabled = vault.enabled;
14148
14999
  if (vault.dir !== void 0) {
@@ -14154,7 +15005,13 @@ var SettingsStore = class {
14154
15005
  if (vault.toolResultMaxChars !== void 0) {
14155
15006
  next.vault.toolResultMaxChars = clampToolResultMaxChars(vault.toolResultMaxChars);
14156
15007
  }
14157
- await mkdir9(path36.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 });
14158
15015
  await writeFileAtomic(this.filePath, `${JSON.stringify(next, null, 2)}
14159
15016
  `);
14160
15017
  this.current = frozen(next);
@@ -14181,6 +15038,19 @@ function statusLineStartupText(state) {
14181
15038
  return "couldn't read its settings.json";
14182
15039
  }
14183
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
+ }
14184
15054
  var AVAILABLE_AGENTS_LABEL = "Available CLIs";
14185
15055
  var HISTORY_LABEL = "History";
14186
15056
  function missingText(text) {
@@ -14219,11 +15089,18 @@ function startupAgentLines(agents) {
14219
15089
  }
14220
15090
  return lines;
14221
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
+ }
14222
15099
 
14223
15100
  // packages/server/src/notes-store.ts
14224
- import { randomUUID as randomUUID3 } from "node:crypto";
14225
- import { mkdir as mkdir10, readFile as readFile13, rename as rename5, rm as rm4, writeFile as writeFile6 } from "node:fs/promises";
14226
- import path37 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";
14227
15104
  var STATE_VERSION2 = 1;
14228
15105
  var WRITE_DEBOUNCE_MS2 = 500;
14229
15106
  var NotesError = class extends ServerTextError {
@@ -14266,7 +15143,7 @@ var NotesStore = class {
14266
15143
  }
14267
15144
  async load() {
14268
15145
  try {
14269
- const raw = await readFile13(this.statePath, "utf8");
15146
+ const raw = await readFile14(this.statePath, "utf8");
14270
15147
  this.notes = parseState2(raw) ?? [];
14271
15148
  } catch {
14272
15149
  this.notes = [];
@@ -14350,11 +15227,11 @@ var NotesStore = class {
14350
15227
  throw new NotesError(serverText("imageUnknownFormat"));
14351
15228
  }
14352
15229
  const image = {
14353
- imageId: randomUUID3(),
15230
+ imageId: randomUUID4(),
14354
15231
  mediaType: format.mediaType,
14355
15232
  bytes: bytes.length
14356
15233
  };
14357
- await mkdir10(this.imagesDir, { recursive: true });
15234
+ await mkdir11(this.imagesDir, { recursive: true });
14358
15235
  await writeFile6(this.imagePath(image), bytes);
14359
15236
  note.images.push(image);
14360
15237
  note.updatedAt = now;
@@ -14383,7 +15260,7 @@ var NotesStore = class {
14383
15260
  const image = this.findImage(imageId);
14384
15261
  if (image === null) return null;
14385
15262
  try {
14386
- const bytes = await readFile13(this.imagePath(image));
15263
+ const bytes = await readFile14(this.imagePath(image));
14387
15264
  return { mediaType: image.mediaType, data: bytes.toString("base64") };
14388
15265
  } catch {
14389
15266
  return null;
@@ -14403,7 +15280,7 @@ var NotesStore = class {
14403
15280
  const images = [];
14404
15281
  for (const image of note.images) {
14405
15282
  try {
14406
- const bytes = await readFile13(this.imagePath(image));
15283
+ const bytes = await readFile14(this.imagePath(image));
14407
15284
  images.push({ mediaType: image.mediaType, data: bytes.toString("base64") });
14408
15285
  } catch {
14409
15286
  }
@@ -14427,7 +15304,7 @@ var NotesStore = class {
14427
15304
  }
14428
15305
  imagePath(image) {
14429
15306
  const safe = image.imageId.replace(/[^a-zA-Z0-9-]/g, "");
14430
- return path37.join(this.imagesDir, `${safe}.${extensionFor(image.mediaType)}`);
15307
+ return path38.join(this.imagesDir, `${safe}.${extensionFor(image.mediaType)}`);
14431
15308
  }
14432
15309
  async unlinkImage(image) {
14433
15310
  await rm4(this.imagePath(image), { force: true }).catch(() => {
@@ -14445,8 +15322,8 @@ var NotesStore = class {
14445
15322
  async writeNow() {
14446
15323
  this.dirty = false;
14447
15324
  try {
14448
- await mkdir10(path37.dirname(this.statePath), { recursive: true });
14449
- const temporary = path37.join(path37.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`);
14450
15327
  await writeFile6(
14451
15328
  temporary,
14452
15329
  JSON.stringify({ version: STATE_VERSION2, notes: this.notes }, null, 2),
@@ -14459,10 +15336,226 @@ var NotesStore = class {
14459
15336
  }
14460
15337
  };
14461
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
+
14462
15555
  // packages/server/src/shell-locator.ts
14463
15556
  import { access as access2 } from "node:fs/promises";
14464
15557
  import { constants as fsConstants2 } from "node:fs";
14465
- import path38 from "node:path";
15558
+ import path40 from "node:path";
14466
15559
  async function isExecutable(candidate) {
14467
15560
  try {
14468
15561
  await access2(candidate, process.platform === "win32" ? fsConstants2.R_OK : fsConstants2.X_OK);
@@ -14474,7 +15567,7 @@ async function isExecutable(candidate) {
14474
15567
  function windowsPowerShellPath() {
14475
15568
  const systemRoot = process.env["SystemRoot"] ?? process.env["windir"];
14476
15569
  if (systemRoot === void 0 || systemRoot.length === 0) return null;
14477
- return path38.join(systemRoot, "System32", "WindowsPowerShell", "v1.0", "powershell.exe");
15570
+ return path40.join(systemRoot, "System32", "WindowsPowerShell", "v1.0", "powershell.exe");
14478
15571
  }
14479
15572
  async function locateWindowsShell() {
14480
15573
  const pwsh = await findInPath("pwsh");
@@ -14494,7 +15587,7 @@ async function locateWindowsShell() {
14494
15587
  async function locatePosixShell() {
14495
15588
  const preferred = process.env["SHELL"];
14496
15589
  if (preferred !== void 0 && preferred.length > 0 && await isExecutable(preferred)) {
14497
- return { file: preferred, args: [], name: path38.basename(preferred) };
15590
+ return { file: preferred, args: [], name: path40.basename(preferred) };
14498
15591
  }
14499
15592
  if (await isExecutable("/bin/bash")) return { file: "/bin/bash", args: [], name: "bash" };
14500
15593
  if (await isExecutable("/bin/sh")) return { file: "/bin/sh", args: [], name: "sh" };
@@ -14505,13 +15598,13 @@ async function locateShell() {
14505
15598
  }
14506
15599
 
14507
15600
  // packages/server/src/session-watcher.ts
14508
- import { stat as stat19 } from "node:fs/promises";
15601
+ import { stat as stat21 } from "node:fs/promises";
14509
15602
  import chokidar3 from "chokidar";
14510
15603
  var INDEX_DEBOUNCE_MS = 750;
14511
15604
  var ROOT_PROBE_MS = 5e3;
14512
15605
  async function isDirectory3(target) {
14513
15606
  try {
14514
- return (await stat19(target)).isDirectory();
15607
+ return (await stat21(target)).isDirectory();
14515
15608
  } catch {
14516
15609
  return false;
14517
15610
  }
@@ -14612,9 +15705,9 @@ function watchSessions(index, hub, agents, options = {}) {
14612
15705
  }
14613
15706
 
14614
15707
  // packages/server/src/terminal-registry.ts
14615
- import { randomUUID as randomUUID4 } from "node:crypto";
15708
+ import { randomUUID as randomUUID5 } from "node:crypto";
14616
15709
  import { EventEmitter as EventEmitter5 } from "node:events";
14617
- import { stat as stat20 } from "node:fs/promises";
15710
+ import { stat as stat22 } from "node:fs/promises";
14618
15711
 
14619
15712
  // packages/server/src/launch-hook-slot.ts
14620
15713
  var LaunchHookSlot = class {
@@ -14876,10 +15969,10 @@ async function resolveLaunchPlan(launch) {
14876
15969
  }
14877
15970
 
14878
15971
  // packages/server/src/workspace-store.ts
14879
- import { mkdir as mkdir11, readFile as readFile14, writeFile as writeFile7 } from "node:fs/promises";
14880
- import path39 from "node:path";
14881
- var STATE_VERSION3 = 1;
14882
- 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;
14883
15976
  var LEGACY_TAB_AGENT = "claude-code";
14884
15977
  function placeByPosition(base, placed) {
14885
15978
  const result = [...base];
@@ -14935,11 +16028,11 @@ function readTab(tab, missingAgent) {
14935
16028
  if (agent === null) return null;
14936
16029
  return { kind: "known", tab: { agent, cwd, sessionId, label: typeof label === "string" ? label : "" } };
14937
16030
  }
14938
- function parseState3(raw) {
16031
+ function parseState4(raw) {
14939
16032
  try {
14940
16033
  const parsed = JSON.parse(raw);
14941
16034
  if (!isRecord(parsed)) return null;
14942
- if (parsed["version"] !== STATE_VERSION3) return null;
16035
+ if (parsed["version"] !== STATE_VERSION4) return null;
14943
16036
  const rawTabs = parsed["tabs"];
14944
16037
  if (!Array.isArray(rawTabs)) return null;
14945
16038
  const base = [];
@@ -14977,7 +16070,7 @@ function serializeState(state) {
14977
16070
  otherTabs.push({ ...entry.tab, position });
14978
16071
  }
14979
16072
  }
14980
- 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 };
14981
16074
  }
14982
16075
  var EMPTY_STATE = () => ({ tabs: [], foreignTabs: [] });
14983
16076
  function persistableTabs(descriptors) {
@@ -14999,8 +16092,8 @@ var WorkspaceStore = class {
14999
16092
  pending = null;
15000
16093
  async load() {
15001
16094
  try {
15002
- const raw = await readFile14(workspaceStatePath(), "utf8");
15003
- return parseState3(raw) ?? EMPTY_STATE();
16095
+ const raw = await readFile16(workspaceStatePath(), "utf8");
16096
+ return parseState4(raw) ?? EMPTY_STATE();
15004
16097
  } catch {
15005
16098
  return EMPTY_STATE();
15006
16099
  }
@@ -15014,7 +16107,7 @@ var WorkspaceStore = class {
15014
16107
  const toWrite = this.pending;
15015
16108
  this.pending = null;
15016
16109
  if (toWrite !== null) void this.writeNow(toWrite);
15017
- }, WRITE_DEBOUNCE_MS3);
16110
+ }, WRITE_DEBOUNCE_MS4);
15018
16111
  this.writeTimer.unref();
15019
16112
  }
15020
16113
  /** Escritura inmediata, para el apagado. */
@@ -15029,12 +16122,12 @@ var WorkspaceStore = class {
15029
16122
  }
15030
16123
  async writeNow(state) {
15031
16124
  try {
15032
- await mkdir11(appConfigDir(), { recursive: true });
16125
+ await mkdir13(appConfigDir(), { recursive: true });
15033
16126
  const target = workspaceStatePath();
15034
- const temporary = path39.join(path39.dirname(target), `workspace.${process.pid}.tmp`);
15035
- await writeFile7(temporary, JSON.stringify(serializeState(state), null, 2), "utf8");
15036
- const { rename: rename6 } = await import("node:fs/promises");
15037
- 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);
15038
16131
  } catch (error) {
15039
16132
  console.warn("[workspace] couldn't save the state:", error);
15040
16133
  }
@@ -15151,14 +16244,14 @@ var TerminalRegistry = class extends EventEmitter5 {
15151
16244
  }
15152
16245
  await this.assertDirectory(options.cwd);
15153
16246
  const resumed = kind === "agent" && options.resumeSessionId !== void 0;
15154
- const terminalId = randomUUID4();
16247
+ const terminalId = randomUUID5();
15155
16248
  const descriptor = {
15156
16249
  terminalId,
15157
16250
  kind,
15158
16251
  agent,
15159
16252
  cwd: options.cwd,
15160
16253
  // Provisional: `spawn` lo confirma con lo que diga el adaptador.
15161
- sessionId: kind === "agent" ? options.resumeSessionId ?? randomUUID4() : "",
16254
+ sessionId: kind === "agent" ? options.resumeSessionId ?? randomUUID5() : "",
15162
16255
  label: options.label ?? "",
15163
16256
  resumed,
15164
16257
  createdAt: Date.now(),
@@ -15262,7 +16355,7 @@ var TerminalRegistry = class extends EventEmitter5 {
15262
16355
  async assertDirectory(cwd) {
15263
16356
  let info;
15264
16357
  try {
15265
- info = await stat20(cwd);
16358
+ info = await stat22(cwd);
15266
16359
  } catch {
15267
16360
  throw new TerminalOpenError("invalid-cwd", serverText("cwdMissing", { cwd }));
15268
16361
  }
@@ -15290,7 +16383,7 @@ var TerminalRegistry = class extends EventEmitter5 {
15290
16383
  let launch;
15291
16384
  let adapter = null;
15292
16385
  let launchedSessionId = sessionId;
15293
- const launchToken = randomUUID4();
16386
+ const launchToken = randomUUID5();
15294
16387
  if (launcher.kind === "agent") {
15295
16388
  adapter = launcher.agent.adapter;
15296
16389
  const { location } = launcher.agent;
@@ -15526,7 +16619,7 @@ var TerminalRegistry = class extends EventEmitter5 {
15526
16619
  console.warn(`[workspace] the tab of ${tab.cwd} wasn't restored: ${reason2}`);
15527
16620
  continue;
15528
16621
  }
15529
- const terminalId = randomUUID4();
16622
+ const terminalId = randomUUID5();
15530
16623
  const descriptor = {
15531
16624
  terminalId,
15532
16625
  kind: "agent",
@@ -15688,20 +16781,20 @@ var TerminalRegistry = class extends EventEmitter5 {
15688
16781
  import { WebSocketServer } from "ws";
15689
16782
 
15690
16783
  // packages/server/src/directory-picker.ts
15691
- import { randomUUID as randomUUID5 } from "node:crypto";
15692
- 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";
15693
16786
  import { homedir as homedir11 } from "node:os";
15694
- import path40 from "node:path";
16787
+ import path42 from "node:path";
15695
16788
  var MAX_ENTRIES = 500;
15696
16789
  var MAX_PICKERS = 8;
15697
16790
  var SKIPPED = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "bin", "obj", "$RECYCLE.BIN"]);
15698
16791
  var DirectoryPickerError = class extends ServerTextError {
15699
16792
  };
15700
16793
  function isInsideProtected(target, dirs) {
15701
- const resolved = path40.resolve(target);
16794
+ const resolved = path42.resolve(target);
15702
16795
  return dirs.some((dir) => {
15703
- const protectedDir = path40.resolve(dir);
15704
- return resolved === protectedDir || resolved.startsWith(protectedDir + path40.sep);
16796
+ const protectedDir = path42.resolve(dir);
16797
+ return resolved === protectedDir || resolved.startsWith(protectedDir + path42.sep);
15705
16798
  });
15706
16799
  }
15707
16800
  async function listRoots() {
@@ -15713,7 +16806,7 @@ async function listRoots() {
15713
16806
  letters.map(async (letter) => {
15714
16807
  const drive = `${letter}:\\`;
15715
16808
  try {
15716
- await stat21(drive);
16809
+ await stat23(drive);
15717
16810
  return drive;
15718
16811
  } catch {
15719
16812
  return null;
@@ -15740,7 +16833,7 @@ var DirectoryPickers = class {
15740
16833
  if (this.pickers.size >= MAX_PICKERS) {
15741
16834
  throw new DirectoryPickerError(serverText("pickerTooMany"));
15742
16835
  }
15743
- const picker = { id: randomUUID5(), current: homedir11(), entries: /* @__PURE__ */ new Set() };
16836
+ const picker = { id: randomUUID6(), current: homedir11(), entries: /* @__PURE__ */ new Set() };
15744
16837
  this.pickers.set(picker.id, picker);
15745
16838
  return this.listing(picker);
15746
16839
  }
@@ -15765,14 +16858,14 @@ var DirectoryPickers = class {
15765
16858
  async enter(pickerId, name) {
15766
16859
  const picker = this.require(pickerId);
15767
16860
  if (name === "..") {
15768
- const parent = path40.dirname(picker.current);
16861
+ const parent = path42.dirname(picker.current);
15769
16862
  if (parent !== picker.current) picker.current = parent;
15770
16863
  return this.listing(picker);
15771
16864
  }
15772
16865
  if (!picker.entries.has(name)) {
15773
16866
  throw new DirectoryPickerError(serverText("pickerNotListed"));
15774
16867
  }
15775
- const target = path40.join(picker.current, name);
16868
+ const target = path42.join(picker.current, name);
15776
16869
  if (isInsideProtected(target, this.protectedDirs)) {
15777
16870
  throw new DirectoryPickerError(serverText("pickerCliFolder"));
15778
16871
  }
@@ -15812,12 +16905,12 @@ var DirectoryPickers = class {
15812
16905
  throw new DirectoryPickerError(serverText("pickerNameChars"));
15813
16906
  }
15814
16907
  }
15815
- const target = path40.join(picker.current, clean);
16908
+ const target = path42.join(picker.current, clean);
15816
16909
  if (isInsideProtected(target, this.protectedDirs)) {
15817
16910
  throw new DirectoryPickerError(serverText("pickerNoProjectsHere"));
15818
16911
  }
15819
16912
  try {
15820
- await mkdir12(target);
16913
+ await mkdir14(target);
15821
16914
  } catch (error) {
15822
16915
  const code = error.code;
15823
16916
  throw new DirectoryPickerError(
@@ -15849,7 +16942,7 @@ var DirectoryPickers = class {
15849
16942
  let truncated = false;
15850
16943
  try {
15851
16944
  const found = await readdir11(picker.current, { withFileTypes: true });
15852
- const dirs = found.filter((entry) => entry.isDirectory()).map((entry) => entry.name).filter((name) => !SKIPPED.has(name)).filter((name) => !isInsideProtected(path40.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));
15853
16946
  truncated = dirs.length > MAX_ENTRIES;
15854
16947
  names = dirs.slice(0, MAX_ENTRIES);
15855
16948
  } catch {
@@ -15860,15 +16953,15 @@ var DirectoryPickers = class {
15860
16953
  path: picker.current,
15861
16954
  entries: names,
15862
16955
  roots,
15863
- canGoUp: path40.dirname(picker.current) !== picker.current,
16956
+ canGoUp: path42.dirname(picker.current) !== picker.current,
15864
16957
  truncated
15865
16958
  };
15866
16959
  }
15867
16960
  };
15868
16961
 
15869
16962
  // packages/server/src/file-browser.ts
15870
- import { readFile as readFile15, readdir as readdir12, stat as stat22 } from "node:fs/promises";
15871
- import path41 from "node:path";
16963
+ import { readFile as readFile17, readdir as readdir12, stat as stat24 } from "node:fs/promises";
16964
+ import path43 from "node:path";
15872
16965
  var MAX_ENTRIES2 = 1e3;
15873
16966
  var SEARCH_MAX_RESULTS = 200;
15874
16967
  var SEARCH_MAX_DIRECTORIES = 4e3;
@@ -15931,7 +17024,7 @@ function languageFor(fileName) {
15931
17024
  const lower = fileName.toLowerCase();
15932
17025
  const byName = LANGUAGE_BY_NAME[lower];
15933
17026
  if (byName !== void 0) return byName;
15934
- const extension = path41.extname(lower);
17027
+ const extension = path43.extname(lower);
15935
17028
  return LANGUAGE_BY_EXTENSION[extension] ?? null;
15936
17029
  }
15937
17030
  function looksBinary2(buffer) {
@@ -15963,7 +17056,7 @@ async function listDirectory(cwd, relativePath, options = {}) {
15963
17056
  let sizeBytes = 0;
15964
17057
  let modifiedAt = 0;
15965
17058
  try {
15966
- const info = await stat22(path41.join(absolute, candidate.name));
17059
+ const info = await stat24(path43.join(absolute, candidate.name));
15967
17060
  sizeBytes = candidate.isDirectory ? 0 : info.size;
15968
17061
  modifiedAt = info.mtimeMs;
15969
17062
  } catch {
@@ -15996,7 +17089,7 @@ async function readLevel(absolute) {
15996
17089
  const always = ALWAYS_SKIP.has(dirent.name.toLowerCase());
15997
17090
  if (dirent.isSymbolicLink()) {
15998
17091
  try {
15999
- const target = await stat22(path41.join(absolute, dirent.name));
17092
+ const target = await stat24(path43.join(absolute, dirent.name));
16000
17093
  candidates.push({
16001
17094
  name: dirent.name,
16002
17095
  isDirectory: target.isDirectory(),
@@ -16025,7 +17118,7 @@ async function markIgnored(repoRoot2, levels) {
16025
17118
  for (const level of levels) {
16026
17119
  for (const entry of level.entries) {
16027
17120
  if (entry.hidden !== null) continue;
16028
- keys.set(entry, toPosixPath(path41.relative(repoRoot2, path41.join(level.absolute, entry.name))));
17121
+ keys.set(entry, toPosixPath(path43.relative(repoRoot2, path43.join(level.absolute, entry.name))));
16029
17122
  }
16030
17123
  }
16031
17124
  if (keys.size === 0) return;
@@ -16086,7 +17179,7 @@ async function searchFiles(cwd, query, options = {}) {
16086
17179
  let sizeBytes = 0;
16087
17180
  let modifiedAt = 0;
16088
17181
  try {
16089
- const info = await stat22(path41.join(level.absolute, candidate.name));
17182
+ const info = await stat24(path43.join(level.absolute, candidate.name));
16090
17183
  sizeBytes = candidate.isDirectory ? 0 : info.size;
16091
17184
  modifiedAt = info.mtimeMs;
16092
17185
  } catch {
@@ -16120,12 +17213,12 @@ async function searchFiles(cwd, query, options = {}) {
16120
17213
  }
16121
17214
  async function readPreview(cwd, relativePath) {
16122
17215
  const absolute = await resolveInside(cwd, relativePath, { mustExist: true });
16123
- const info = await stat22(absolute);
17216
+ const info = await stat24(absolute);
16124
17217
  if (info.isDirectory()) {
16125
17218
  throw new Error("It's a directory, not a file.");
16126
17219
  }
16127
- const content = await readFile15(absolute);
16128
- const fileName = path41.basename(absolute);
17220
+ const content = await readFile17(absolute);
17221
+ const fileName = path43.basename(absolute);
16129
17222
  if (looksBinary2(content)) {
16130
17223
  return {
16131
17224
  path: relativePath,
@@ -16714,14 +17807,14 @@ var TerminalWriteQueue = class {
16714
17807
  };
16715
17808
 
16716
17809
  // packages/server/src/vault/service.ts
16717
- import { mkdir as mkdir15, stat as stat26 } from "node:fs/promises";
17810
+ import { mkdir as mkdir17, stat as stat28 } from "node:fs/promises";
16718
17811
  import { homedir as homedir12 } from "node:os";
16719
- import path45 from "node:path";
17812
+ import path47 from "node:path";
16720
17813
 
16721
17814
  // packages/server/src/vault/move.ts
16722
17815
  import { constants } from "node:fs";
16723
- 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";
16724
- import path42 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";
16725
17818
  var MOVED_ENTRIES = ["vault.json", "sessions", "memory"];
16726
17819
  var OWN_TEMP_FILE = /\.\d+\.[0-9a-f]{12}\.tmp$/;
16727
17820
  async function copyTree(source, target, result) {
@@ -16732,14 +17825,14 @@ async function copyTree(source, target, result) {
16732
17825
  return;
16733
17826
  }
16734
17827
  if (info.isDirectory()) {
16735
- await mkdir13(target, { recursive: true });
17828
+ await mkdir15(target, { recursive: true });
16736
17829
  for (const name of await readdir13(source)) {
16737
- await copyTree(path42.join(source, name), path42.join(target, name), result);
17830
+ await copyTree(path44.join(source, name), path44.join(target, name), result);
16738
17831
  }
16739
17832
  return;
16740
17833
  }
16741
17834
  if (!info.isFile() || OWN_TEMP_FILE.test(source)) return;
16742
- await mkdir13(path42.dirname(target), { recursive: true });
17835
+ await mkdir15(path44.dirname(target), { recursive: true });
16743
17836
  try {
16744
17837
  await copyFile2(source, target, constants.COPYFILE_EXCL);
16745
17838
  } catch (error) {
@@ -16754,9 +17847,9 @@ async function copyTree(source, target, result) {
16754
17847
  }
16755
17848
  async function moveVault(from, to) {
16756
17849
  const result = { copied: 0, skipped: 0 };
16757
- await mkdir13(to, { recursive: true });
17850
+ await mkdir15(to, { recursive: true });
16758
17851
  for (const entry of MOVED_ENTRIES) {
16759
- await copyTree(path42.join(from, entry), path42.join(to, entry), result);
17852
+ await copyTree(path44.join(from, entry), path44.join(to, entry), result);
16760
17853
  }
16761
17854
  return result;
16762
17855
  }
@@ -16788,11 +17881,11 @@ function checkVaultTarget(current, target, options) {
16788
17881
  return { kind: "ok" };
16789
17882
  }
16790
17883
  async function canWriteInto(dir) {
16791
- const probe = temporaryPathFor(path42.join(dir, ".escritura"));
17884
+ const probe = temporaryPathFor(path44.join(dir, ".escritura"));
16792
17885
  try {
16793
- await mkdir13(dir, { recursive: true });
16794
- if (!(await stat23(dir)).isDirectory()) return false;
16795
- await writeFile8(probe, "");
17886
+ await mkdir15(dir, { recursive: true });
17887
+ if (!(await stat25(dir)).isDirectory()) return false;
17888
+ await writeFile9(probe, "");
16796
17889
  return true;
16797
17890
  } catch {
16798
17891
  return false;
@@ -16802,12 +17895,12 @@ async function canWriteInto(dir) {
16802
17895
  }
16803
17896
 
16804
17897
  // packages/server/src/vault/serialize.ts
16805
- import { createHash as createHash3 } from "node:crypto";
17898
+ import { createHash as createHash4 } from "node:crypto";
16806
17899
  function imageKey(eventId, source, index) {
16807
17900
  return `${eventId}|${source}|${index}`;
16808
17901
  }
16809
17902
  function assetName(bytes) {
16810
- const hash = createHash3("sha256").update(bytes).digest("hex").slice(0, 32);
17903
+ const hash = createHash4("sha256").update(bytes).digest("hex").slice(0, 32);
16811
17904
  return `${hash}.${detectImageFormat(bytes)?.ext ?? "bin"}`;
16812
17905
  }
16813
17906
  function buildHeader(input) {
@@ -16894,12 +17987,12 @@ function serializeSession(input) {
16894
17987
  }
16895
17988
 
16896
17989
  // packages/server/src/vault/writer.ts
16897
- import { readdir as readdir15, rm as rm6, stat as stat25 } from "node:fs/promises";
16898
- import path44 from "node:path";
17990
+ import { readdir as readdir15, rm as rm6, stat as stat27 } from "node:fs/promises";
17991
+ import path46 from "node:path";
16899
17992
 
16900
17993
  // packages/server/src/vault/memory-copy.ts
16901
- import { mkdir as mkdir14, readdir as readdir14, readFile as readFile16, stat as stat24, utimes as utimes2 } from "node:fs/promises";
16902
- import path43 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";
16903
17996
  var MEMORY_FILE_MAX_BYTES = 1024 * 1024;
16904
17997
  var MEMORY_MAX_FILES = 500;
16905
17998
  var MEMORY_SOURCE_FILE = "source.json";
@@ -16924,7 +18017,7 @@ async function listMemoryNotes(cwd) {
16924
18017
  let absolute;
16925
18018
  try {
16926
18019
  absolute = await resolveInside(cwd, `${MEMORY_DIR}/${name}`, { mustExist: true });
16927
- const info = await stat24(absolute);
18020
+ const info = await stat26(absolute);
16928
18021
  if (!info.isFile() || info.size > MEMORY_FILE_MAX_BYTES) continue;
16929
18022
  notes.push({ name, absolute, size: info.size, mtimeMs: info.mtimeMs });
16930
18023
  } catch {
@@ -16938,7 +18031,7 @@ async function measureProjectMemory(cwd) {
16938
18031
  }
16939
18032
  async function isUpToDate(target, note) {
16940
18033
  try {
16941
- const info = await stat24(target);
18034
+ const info = await stat26(target);
16942
18035
  return info.isFile() && info.size === note.size && Math.abs(info.mtimeMs - note.mtimeMs) < SAME_MTIME_MS;
16943
18036
  } catch {
16944
18037
  return false;
@@ -16946,7 +18039,7 @@ async function isUpToDate(target, note) {
16946
18039
  }
16947
18040
  async function exists3(file) {
16948
18041
  try {
16949
- await stat24(file);
18042
+ await stat26(file);
16950
18043
  return true;
16951
18044
  } catch {
16952
18045
  return false;
@@ -16962,14 +18055,14 @@ async function copyProjectMemory(dir, cwd, platform, deps = DEFAULT_WRITE_DEPS)
16962
18055
  if (prepared) return;
16963
18056
  prepared = true;
16964
18057
  await ensureVaultMarker(dir, deps);
16965
- await mkdir14(target, { recursive: true });
18058
+ await mkdir16(target, { recursive: true });
16966
18059
  };
16967
18060
  for (const note of notes) {
16968
- const destination = path43.join(target, note.name);
18061
+ const destination = path45.join(target, note.name);
16969
18062
  if (await isUpToDate(destination, note)) continue;
16970
18063
  let content;
16971
18064
  try {
16972
- content = await readFile16(note.absolute);
18065
+ content = await readFile18(note.absolute);
16973
18066
  } catch {
16974
18067
  continue;
16975
18068
  }
@@ -16981,7 +18074,7 @@ async function copyProjectMemory(dir, cwd, platform, deps = DEFAULT_WRITE_DEPS)
16981
18074
  result.files += 1;
16982
18075
  result.bytes += content.length;
16983
18076
  }
16984
- const source = path43.join(target, MEMORY_SOURCE_FILE);
18077
+ const source = path45.join(target, MEMORY_SOURCE_FILE);
16985
18078
  if (result.files > 0 || !await exists3(source)) {
16986
18079
  await prepare();
16987
18080
  await writeFileAtomic(source, `${JSON.stringify({ cwd, copiedAt: deps.now() })}
@@ -17475,7 +18568,7 @@ var VaultWriter = class {
17475
18568
  }
17476
18569
  async assetOnDisk(dir, agent, sessionId, asset, bytes) {
17477
18570
  try {
17478
- const info = await stat25(assetFile(dir, agent, sessionId, asset));
18571
+ const info = await stat27(assetFile(dir, agent, sessionId, asset));
17479
18572
  return info.isFile() && info.size === bytes;
17480
18573
  } catch {
17481
18574
  return false;
@@ -17575,17 +18668,17 @@ var VaultWriter = class {
17575
18668
  const folders = [dir];
17576
18669
  const subfolders = async (parent) => {
17577
18670
  try {
17578
- return (await readdir15(parent, { withFileTypes: true })).filter((entry) => entry.isDirectory()).map((entry) => path44.join(parent, entry.name));
18671
+ return (await readdir15(parent, { withFileTypes: true })).filter((entry) => entry.isDirectory()).map((entry) => path46.join(parent, entry.name));
17579
18672
  } catch {
17580
18673
  return [];
17581
18674
  }
17582
18675
  };
17583
18676
  for (const agentDir of await subfolders(sessionsRoot2(dir))) {
17584
- if (!SESSION_AGENT_IDS.includes(path44.basename(agentDir))) continue;
18677
+ if (!SESSION_AGENT_IDS.includes(path46.basename(agentDir))) continue;
17585
18678
  folders.push(agentDir);
17586
18679
  folders.push(...(await subfolders(agentDir)).filter((folder) => folder.endsWith(".assets")));
17587
18680
  }
17588
- folders.push(...await subfolders(path44.join(dir, "memory")));
18681
+ folders.push(...await subfolders(path46.join(dir, "memory")));
17589
18682
  for (const folder of folders) {
17590
18683
  let names;
17591
18684
  try {
@@ -17595,9 +18688,9 @@ var VaultWriter = class {
17595
18688
  }
17596
18689
  for (const name of names) {
17597
18690
  if (!OWN_TEMP_FILE2.test(name)) continue;
17598
- const file = path44.join(folder, name);
18691
+ const file = path46.join(folder, name);
17599
18692
  try {
17600
- const info = await stat25(file);
18693
+ const info = await stat27(file);
17601
18694
  if (info.isFile() && now - info.mtimeMs > TEMP_MAX_AGE_MS) await rm6(file, { force: true });
17602
18695
  } catch {
17603
18696
  }
@@ -17785,14 +18878,14 @@ var VaultService = class {
17785
18878
  async setDir(target) {
17786
18879
  const { platform, agents, settings: settings2, catalog } = this.options;
17787
18880
  const current = this.dir();
17788
- const protectedDirs = [...agents.protectedDirs(), path45.join(this.options.homeDir ?? homedir12(), ".gemini")];
18881
+ const protectedDirs = [...agents.protectedDirs(), path47.join(this.options.homeDir ?? homedir12(), ".gemini")];
17789
18882
  const check = checkVaultTarget(current, target, { protectedDirs, platform });
17790
18883
  if (check.kind === "refused") throw new VaultError(check.text);
17791
18884
  if (check.kind === "same") return;
17792
18885
  try {
17793
18886
  await this.writer.exclusive("moving", async () => {
17794
18887
  if (!await canWriteInto(target)) throw new VaultError(serverText("vaultCannotWrite"));
17795
- const hadCopy = await stat26(vaultMarkerFile(current)).then(
18888
+ const hadCopy = await stat28(vaultMarkerFile(current)).then(
17796
18889
  () => true,
17797
18890
  () => false
17798
18891
  );
@@ -17826,7 +18919,7 @@ var VaultService = class {
17826
18919
  /** Abre la carpeta de la copia con el explorador del sistema. */
17827
18920
  async reveal() {
17828
18921
  const dir = this.activeDir();
17829
- const isFolder = await stat26(dir).then(
18922
+ const isFolder = await stat28(dir).then(
17830
18923
  (info) => info.isDirectory(),
17831
18924
  () => false
17832
18925
  );
@@ -17850,7 +18943,7 @@ var VaultService = class {
17850
18943
  fromCopy: true
17851
18944
  });
17852
18945
  const file = sessionExportFile(dir, agent, sessionId);
17853
- await mkdir15(path45.dirname(file), { recursive: true });
18946
+ await mkdir17(path47.dirname(file), { recursive: true });
17854
18947
  await writeFileAtomic(file, text, this.writeDeps);
17855
18948
  this.openWithSystem(file);
17856
18949
  return file;
@@ -17937,7 +19030,7 @@ var VaultService = class {
17937
19030
  const project = this.options.index.getProjects().find((candidate) => candidate.key === projectKey);
17938
19031
  if (project === void 0) throw new VaultError(serverText("vaultProjectGone"));
17939
19032
  const folder = projectExportDirFor(this.activeDir(), project, this.options.platform);
17940
- await mkdir15(folder, { recursive: true });
19033
+ await mkdir17(folder, { recursive: true });
17941
19034
  const entries = [];
17942
19035
  const names = /* @__PURE__ */ new Set();
17943
19036
  let skipped = 0;
@@ -17954,7 +19047,7 @@ var VaultService = class {
17954
19047
  continue;
17955
19048
  }
17956
19049
  const fileName = this.exportFileName(summary, names);
17957
- await writeFileAtomic(path45.join(folder, fileName), text, this.writeDeps);
19050
+ await writeFileAtomic(path47.join(folder, fileName), text, this.writeDeps);
17958
19051
  entries.push({
17959
19052
  updatedAt: summary.updatedAt,
17960
19053
  agentLabel: this.labelOf(summary.agent),
@@ -17965,9 +19058,9 @@ var VaultService = class {
17965
19058
  });
17966
19059
  await nextTurn3();
17967
19060
  }
17968
- const name = project.cwd.length > 0 ? path45.basename(project.cwd) || project.cwd : project.fallbackName;
19061
+ const name = project.cwd.length > 0 ? path47.basename(project.cwd) || project.cwd : project.fallbackName;
17969
19062
  const index = renderProjectIndex({ name, cwd: project.cwd, exportedAt: this.now(), entries, skipped });
17970
- await writeFileAtomic(path45.join(folder, "index.md"), index, this.writeDeps);
19063
+ await writeFileAtomic(path47.join(folder, "index.md"), index, this.writeDeps);
17971
19064
  this.openWithSystem(folder);
17972
19065
  return { sessions: entries.length, skipped, folder };
17973
19066
  }
@@ -18123,12 +19216,14 @@ function attachTerminalSocket(options) {
18123
19216
  index,
18124
19217
  archived,
18125
19218
  notes,
19219
+ drafts,
18126
19220
  conversations,
18127
19221
  repos,
18128
19222
  memory,
18129
19223
  agents,
18130
19224
  defaultCwd,
18131
- vault
19225
+ vault,
19226
+ remote
18132
19227
  } = options;
18133
19228
  const pasteStore = options.pasteStore ?? new PasteStore();
18134
19229
  const writeQueue = new TerminalWriteQueue();
@@ -18143,6 +19238,14 @@ function attachTerminalSocket(options) {
18143
19238
  if (socket.readyState === socket.OPEN) socket.send(payload);
18144
19239
  }
18145
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
+ };
18146
19249
  const broadcastExcept = (except, message) => {
18147
19250
  const payload = encodeServerMessage(message);
18148
19251
  for (const socket of clients) {
@@ -18193,7 +19296,25 @@ function attachTerminalSocket(options) {
18193
19296
  }
18194
19297
  return false;
18195
19298
  };
18196
- 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
+ };
18197
19318
  const onRegistryExit = (terminalId, exitCode, signal) => broadcast({ type: "terminal.exit", terminalId, exitCode, signal });
18198
19319
  const onRegistryActivity = (terminalId, activity, offlineReason) => broadcast(
18199
19320
  offlineReason === null ? { type: "terminal.activity", terminalId, activity } : { type: "terminal.activity", terminalId, activity, offlineReason }
@@ -18221,7 +19342,9 @@ function attachTerminalSocket(options) {
18221
19342
  };
18222
19343
  const pieceWriter = (socket, terminalId) => (piece) => writeToTerminal(socket, terminalId, piece);
18223
19344
  const dropTab = (terminalId) => {
19345
+ const draftOwner = draftOwnerOf(registry.get(terminalId));
18224
19346
  if (!registry.close(terminalId)) return false;
19347
+ if (draftOwner !== null) drafts.delete(draftOwner);
18225
19348
  conversations.drop(terminalId);
18226
19349
  repos.drop(terminalId);
18227
19350
  memory.drop(terminalId);
@@ -18293,6 +19416,23 @@ function attachTerminalSocket(options) {
18293
19416
  memory.on("status", onMemoryStatus);
18294
19417
  const stopAgentChanges = agents.subscribeChanges(() => broadcast(agentsMessage()));
18295
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
+ };
18296
19436
  const sendVaultError = (socket, error) => {
18297
19437
  if (error instanceof VaultError) {
18298
19438
  sendError(socket, "vault-failed", error.text);
@@ -18310,20 +19450,25 @@ function attachTerminalSocket(options) {
18310
19450
  return;
18311
19451
  }
18312
19452
  if (pathname !== WS_PATH) return;
18313
- const rejection = rejectRequest(request, port, token);
18314
- if (rejection !== null) {
19453
+ const authorization = authorizeRequest(request, port, token, remote.verifier());
19454
+ if (!authorization.ok) {
18315
19455
  socket.write("HTTP/1.1 403 Forbidden\r\nConnection: close\r\n\r\n");
18316
19456
  socket.destroy();
18317
- console.warn(`[security] upgrade rejected on ${WS_PATH}: ${rejection}`);
19457
+ console.warn(`[security] upgrade rejected on ${WS_PATH}: ${authorization.reason}`);
18318
19458
  return;
18319
19459
  }
18320
19460
  wss.handleUpgrade(request, socket, head, (client) => {
18321
- wss.emit("connection", client, request);
19461
+ wss.emit("connection", client, request, authorization.credential);
18322
19462
  });
18323
19463
  };
18324
19464
  httpServer.on("upgrade", onUpgrade);
18325
- wss.on("connection", (socket) => {
19465
+ wss.on("connection", (socket, _request, credential) => {
18326
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
+ }
18327
19472
  const listener = (terminalId, chunk) => {
18328
19473
  send(socket, { type: "terminal.output", terminalId, data: chunk });
18329
19474
  };
@@ -18386,7 +19531,8 @@ function attachTerminalSocket(options) {
18386
19531
  defaultAgent: agents.defaultAgent(),
18387
19532
  platform: process.platform,
18388
19533
  defaultCwd,
18389
- shellName: shell?.name ?? null
19534
+ shellName: shell?.name ?? null,
19535
+ remoteClient: deviceId !== null
18390
19536
  });
18391
19537
  send(socket, terminalListMessage());
18392
19538
  for (const entry of registry.activitySnapshot()) {
@@ -18395,7 +19541,9 @@ function attachTerminalSocket(options) {
18395
19541
  send(socket, { type: "index.status", status: index.getStatus() });
18396
19542
  send(socket, { type: "index.projects", projects: index.getProjects(), replace: true });
18397
19543
  send(socket, { type: "notes.list", notes: notes.list() });
19544
+ send(socket, { type: "composer.drafts", drafts: draftEntriesOf(registry.list()) });
18398
19545
  send(socket, { type: "vault.status", status: vault.status() });
19546
+ if (deviceId === null) send(socket, { type: "remote.status", status: remote.status() });
18399
19547
  socket.on("message", (raw) => {
18400
19548
  const text = Array.isArray(raw) ? Buffer.concat(raw).toString("utf8") : Buffer.from(raw).toString("utf8");
18401
19549
  const message = parseClientMessage(text);
@@ -18405,9 +19553,16 @@ function attachTerminalSocket(options) {
18405
19553
  return;
18406
19554
  }
18407
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
+ }
18408
19561
  switch (message.type) {
18409
19562
  case "input":
18410
- 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
+ }
18411
19566
  break;
18412
19567
  /*
18413
19568
  Un mensaje del cuadro de escritura.
@@ -18423,6 +19578,8 @@ function attachTerminalSocket(options) {
18423
19578
  */
18424
19579
  case "agent.submit": {
18425
19580
  const { terminalId, text: text2, images } = message;
19581
+ const submittedOwner = draftOwnerOf(registry.get(terminalId));
19582
+ if (submittedOwner !== null) drafts.delete(submittedOwner);
18426
19583
  const files = message.files ?? [];
18427
19584
  if (files.length > MAX_FILES_PER_SUBMIT) {
18428
19585
  sendError(
@@ -18450,7 +19607,7 @@ function attachTerminalSocket(options) {
18450
19607
  if (submitting !== null) {
18451
19608
  const launchedAt = registry.launchedAtOf(terminalId);
18452
19609
  const blind = isBlind();
18453
- const refusal = submitRefusal({
19610
+ const refusal2 = submitRefusal({
18454
19611
  label: submitting.label,
18455
19612
  approvesPendingOnCycle: guardsPending,
18456
19613
  waitingFor: conversations.getWaitingFor(terminalId),
@@ -18459,8 +19616,8 @@ function attachTerminalSocket(options) {
18459
19616
  blind,
18460
19617
  openToolCall: blind && launchedAt !== null && conversations.hasOpenToolCall(terminalId, launchedAt)
18461
19618
  });
18462
- if (refusal !== null) {
18463
- sendError(socket, "submit-failed", refusal);
19619
+ if (refusal2 !== null) {
19620
+ sendError(socket, "submit-failed", refusal2);
18464
19621
  break;
18465
19622
  }
18466
19623
  }
@@ -19131,6 +20288,19 @@ function attachTerminalSocket(options) {
19131
20288
  }
19132
20289
  })();
19133
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
+ }
19134
20304
  /*
19135
20305
  Notas sueltas. No van dirigidas a ninguna pestana.
19136
20306
 
@@ -19415,9 +20585,70 @@ function attachTerminalSocket(options) {
19415
20585
  case "search.cancel":
19416
20586
  globalSearch.cancel(message.searchId);
19417
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;
19418
20645
  }
19419
20646
  });
19420
20647
  const teardown = () => {
20648
+ if (phonePairingOwner === socket) {
20649
+ phonePairingOwner = null;
20650
+ remote.cancelPhonePairing();
20651
+ }
19421
20652
  globalSearch.cancel();
19422
20653
  pickers.closeAll();
19423
20654
  registry.detachAll(listener);
@@ -19428,6 +20659,7 @@ function attachTerminalSocket(options) {
19428
20659
  for (const terminalId of memorySubscriptions) memory.unsubscribe(terminalId);
19429
20660
  memorySubscriptions.clear();
19430
20661
  clients.delete(socket);
20662
+ if (deviceId !== null && remoteSockets.delete(socket)) remote.noteDisconnected(deviceId);
19431
20663
  };
19432
20664
  socket.on("close", teardown);
19433
20665
  socket.on("error", teardown);
@@ -19452,18 +20684,20 @@ function attachTerminalSocket(options) {
19452
20684
  memory.off("status", onMemoryStatus);
19453
20685
  stopAgentChanges();
19454
20686
  stopVaultStatus();
20687
+ stopRemoteStatus();
20688
+ stopRemoteRevoked();
19455
20689
  for (const client of wss.clients) client.terminate();
19456
20690
  wss.close();
19457
20691
  };
19458
20692
  }
19459
20693
 
19460
20694
  // packages/server/src/vault/catalog.ts
19461
- import { open as open7, readdir as readdir16, stat as stat27 } from "node:fs/promises";
19462
- import path46 from "node:path";
20695
+ import { open as open7, readdir as readdir16, stat as stat29 } from "node:fs/promises";
20696
+ import path48 from "node:path";
19463
20697
  import { createInterface as createInterface2 } from "node:readline";
19464
20698
  var HEADER_MAX_BYTES = 64 * 1024;
19465
20699
  var LOAD_CONCURRENCY = 16;
19466
- var keyOf = (agent, sessionId) => `${agent}/${sessionId}`;
20700
+ var keyOf2 = (agent, sessionId) => `${agent}/${sessionId}`;
19467
20701
  function isSessionAgentId2(value) {
19468
20702
  return SESSION_AGENT_IDS.includes(value);
19469
20703
  }
@@ -19479,7 +20713,7 @@ async function assetBytesOf(dir, agent, sessionId) {
19479
20713
  for (const name of names) {
19480
20714
  if (!VAULT_ASSET_NAME_PATTERN.test(name)) continue;
19481
20715
  try {
19482
- const info = await stat27(path46.join(folder, name));
20716
+ const info = await stat29(path48.join(folder, name));
19483
20717
  if (info.isFile()) total += info.size;
19484
20718
  } catch {
19485
20719
  }
@@ -19490,7 +20724,7 @@ async function readEntry(dir, agent, sessionId) {
19490
20724
  const file = sessionFile(dir, agent, sessionId);
19491
20725
  let sizeBytes;
19492
20726
  try {
19493
- const info = await stat27(file);
20727
+ const info = await stat29(file);
19494
20728
  if (!info.isFile()) return { kind: "absent" };
19495
20729
  sizeBytes = info.size;
19496
20730
  } catch {
@@ -19571,8 +20805,8 @@ var VaultCatalog = class {
19571
20805
  const foreign = /* @__PURE__ */ new Set();
19572
20806
  keys.forEach((key, position) => {
19573
20807
  const read = reads[position];
19574
- if (read?.kind === "listed") entries.set(keyOf(key.agent, key.sessionId), read.entry);
19575
- 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));
19576
20810
  });
19577
20811
  this.entries = entries;
19578
20812
  this.foreign = foreign;
@@ -19586,7 +20820,7 @@ var VaultCatalog = class {
19586
20820
  const generation = this.generation;
19587
20821
  const read = await readEntry(dir, agent, sessionId);
19588
20822
  if (generation !== this.generation) return;
19589
- const key = keyOf(agent, sessionId);
20823
+ const key = keyOf2(agent, sessionId);
19590
20824
  const before = JSON.stringify(this.entries.get(key) ?? null);
19591
20825
  const wasForeign = this.foreign.has(key);
19592
20826
  this.entries.delete(key);
@@ -19616,11 +20850,11 @@ var VaultCatalog = class {
19616
20850
  return this.summaryCache;
19617
20851
  }
19618
20852
  header(agent, sessionId) {
19619
- return this.entries.get(keyOf(agent, sessionId))?.header ?? null;
20853
+ return this.entries.get(keyOf2(agent, sessionId))?.header ?? null;
19620
20854
  }
19621
20855
  /** true si en disco hay una copia de esa sesion con un formato que esta app no conoce. */
19622
20856
  hasForeignFormat(agent, sessionId) {
19623
- return this.foreign.has(keyOf(agent, sessionId));
20857
+ return this.foreign.has(keyOf2(agent, sessionId));
19624
20858
  }
19625
20859
  /**
19626
20860
  * El cuerpo de una sesion listada, linea por linea: eventos y documentos.
@@ -19643,7 +20877,7 @@ var VaultCatalog = class {
19643
20877
  */
19644
20878
  async *bodyLines(agent, sessionId) {
19645
20879
  const dir = this.dir;
19646
- if (dir === null || !this.entries.has(keyOf(agent, sessionId))) return;
20880
+ if (dir === null || !this.entries.has(keyOf2(agent, sessionId))) return;
19647
20881
  const handle = await open7(sessionFile(dir, agent, sessionId), "r").catch(() => null);
19648
20882
  if (handle === null) return;
19649
20883
  const input = handle.createReadStream({ encoding: "utf8" });
@@ -19699,38 +20933,77 @@ var VaultCatalog = class {
19699
20933
  };
19700
20934
 
19701
20935
  // packages/server/src/index.ts
19702
- var serverDir = path47.dirname(fileURLToPath2(import.meta.url));
20936
+ var serverDir = path49.dirname(fileURLToPath2(import.meta.url));
19703
20937
  var isPackaged = true;
19704
- var webRoot = isPackaged ? serverDir : path47.resolve(serverDir, "../../web");
19705
- var webDist = isPackaged ? path47.join(serverDir, "web") : path47.join(webRoot, "dist");
19706
- var repoRoot = path47.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, "../../..");
19707
20941
  var isProduction = isPackaged || process.argv.includes("--prod");
19708
20942
  function resolveDefaultCwd() {
19709
20943
  const override = process.env["AGENT_WORKBENCH_CWD"];
19710
- if (override !== void 0 && override.length > 0) return path47.resolve(override);
20944
+ if (override !== void 0 && override.length > 0) return path49.resolve(override);
19711
20945
  if (isPackaged) return process.cwd();
19712
20946
  return repoRoot;
19713
20947
  }
19714
20948
  var ORIGIN_REFUSED_TEXT = "Origin not allowed.\nOrigen no permitido.";
19715
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.";
19716
20950
  var NOT_FOUND_TEXT = "Not found.\nNo encontrado.";
19717
- 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) {
19718
20955
  return (request, response, next) => {
19719
- if (!hasValidHost(request, port) || !hasValidOrigin(request, port)) {
19720
- 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();
19721
20966
  return;
19722
20967
  }
19723
- const match = matchToken(request, token);
19724
- if (match === null) {
19725
- 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);
19726
20970
  return;
19727
20971
  }
19728
- if (match.source === "query") {
19729
- 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;
19730
20984
  }
19731
- next();
20985
+ response.status(401).type("text/plain").send(remote.verifier() === null ? TOKEN_MISSING_TEXT : TOKEN_MISSING_TEXT + PAIRING_HINT_TEXT);
19732
20986
  };
19733
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
+ }
19734
21007
  async function mountDevUi(app, httpServer) {
19735
21008
  const { createServer: createViteServer } = await import("vite");
19736
21009
  const vite = await createViteServer({
@@ -19743,7 +21016,7 @@ async function mountDevUi(app, httpServer) {
19743
21016
  }
19744
21017
  var LOOKS_LIKE_FILE = /\.[a-zA-Z0-9]{1,8}$/;
19745
21018
  function mountBuiltUi(app) {
19746
- const indexHtml = path47.join(webDist, "index.html");
21019
+ const indexHtml = path49.join(webDist, "index.html");
19747
21020
  if (!existsSync4(indexHtml)) {
19748
21021
  throw new Error(
19749
21022
  `There's no compiled interface in ${webDist}.
@@ -19760,7 +21033,7 @@ Run "pnpm build" before "pnpm start", or use "pnpm dev".`
19760
21033
  });
19761
21034
  return () => Promise.resolve();
19762
21035
  }
19763
- function describeStartup(url, cwd, agents, shell) {
21036
+ function describeStartup(url, cwd, agents, shell, remoteLine) {
19764
21037
  const agentList = agents.list();
19765
21038
  const line = "-".repeat(64);
19766
21039
  console.log(`
@@ -19782,6 +21055,9 @@ ${line}`);
19782
21055
  }));
19783
21056
  for (const agentLine of startupAgentLines(startupAgents)) console.log(agentLine);
19784
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());
19785
21061
  if (agentList.some((info) => info.environmentNotice === "child-session-marker")) {
19786
21062
  console.log("");
19787
21063
  console.log(" Note: this process inherited CLAUDE_CODE_CHILD_SESSION, which turns off");
@@ -19818,6 +21094,8 @@ async function main() {
19818
21094
  );
19819
21095
  const notes = new NotesStore();
19820
21096
  await notes.load();
21097
+ const drafts = new DraftStore();
21098
+ await drafts.load();
19821
21099
  const conversations = new ConversationHub(registry, agents);
19822
21100
  const repos = new RepoHub(registry);
19823
21101
  const memory = new MemoryHub(registry);
@@ -19825,16 +21103,31 @@ async function main() {
19825
21103
  const app = express();
19826
21104
  app.disable("x-powered-by");
19827
21105
  const httpServer = createServer(app);
19828
- await new Promise((resolve, reject) => {
19829
- httpServer.once("error", reject);
19830
- httpServer.listen(0, LOOPBACK_HOST, resolve);
19831
- });
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);
19832
21119
  const address = httpServer.address();
19833
21120
  if (address === null || typeof address === "string") {
19834
21121
  throw new Error("Couldn't determine the assigned port.");
19835
21122
  }
19836
21123
  const { port } = address;
19837
- 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));
19838
21131
  const closeUi = isProduction ? mountBuiltUi(app) : await mountDevUi(app, httpServer);
19839
21132
  const detachSocket = attachTerminalSocket({
19840
21133
  pasteStore,
@@ -19846,18 +21139,21 @@ async function main() {
19846
21139
  index,
19847
21140
  archived,
19848
21141
  notes,
21142
+ drafts,
19849
21143
  conversations,
19850
21144
  repos,
19851
21145
  memory,
19852
21146
  agents,
19853
21147
  defaultCwd,
19854
- vault
21148
+ vault,
21149
+ remote
19855
21150
  });
19856
21151
  void pasteStore.purgeStale();
19857
21152
  void index.start();
19858
21153
  const stopWatching = watchSessions(index, conversations, agents);
19859
21154
  if (process.env["AGENT_WORKBENCH_NO_RESTORE"] !== "1") {
19860
21155
  void store.load().then((state) => {
21156
+ drafts.retainOnly(workspaceDraftOwners(state));
19861
21157
  if (state.tabs.length === 0 && state.foreignTabs.length === 0) return;
19862
21158
  if (agents.anyAvailable()) {
19863
21159
  console.log(`Restoring ${state.tabs.length} tab(s) from the previous run...`);
@@ -19866,7 +21162,14 @@ async function main() {
19866
21162
  });
19867
21163
  }
19868
21164
  const url = `http://${LOOPBACK_HOST}:${port}/?${TOKEN_QUERY_PARAM}=${token}`;
19869
- 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
+ );
19870
21173
  if (process.env["AGENT_WORKBENCH_NO_OPEN"] !== "1") openBrowser(url);
19871
21174
  let shuttingDown = false;
19872
21175
  const shutdown = () => {
@@ -19881,7 +21184,8 @@ async function main() {
19881
21184
  repos.disposeAll();
19882
21185
  void memory.disposeAll();
19883
21186
  registry.disposeAll();
19884
- 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(() => {
19885
21189
  httpServer.close(() => process.exit(0));
19886
21190
  setTimeout(() => process.exit(0), 2e3).unref();
19887
21191
  });