agent-workbench 0.3.1 → 0.4.1

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
  }
@@ -161,6 +170,7 @@ function noticeText(part) {
161
170
  return part.detail.length > 0 ? `La CLI devolvi\xF3 un error: ${part.detail}` : "La CLI devolvi\xF3 un error";
162
171
  }
163
172
  }
173
+ var MAX_SUBAGENT_DESCRIPTION = 120;
164
174
  var EMPTY_CONTEXT_USAGE = {
165
175
  lastRequestTokens: 0,
166
176
  lastOutputTokens: 0,
@@ -354,6 +364,41 @@ function modelFamilyOf(value, options = MODEL_OPTIONS) {
354
364
  return candidates.find((option) => value.includes(option.family))?.family ?? null;
355
365
  }
356
366
 
367
+ // packages/shared/src/composer-draft.ts
368
+ var MAX_COMPOSER_DRAFT_CHARS = 1e6;
369
+ var MAX_COMPOSER_DRAFT_PASTES = 100;
370
+ var MAX_PASTE_NUMBER = 999999;
371
+ function composerDraftChars(draft) {
372
+ return draft.pasted.reduce((total, paste) => total + paste.text.length, draft.text.length);
373
+ }
374
+ function isEmptyComposerDraft(draft) {
375
+ return draft.text.trim().length === 0 && draft.pasted.every((paste) => paste.text.trim().length === 0);
376
+ }
377
+ function sameComposerDraft(a, b) {
378
+ if (a.text !== b.text || a.pasted.length !== b.pasted.length) return false;
379
+ return a.pasted.every((paste, index) => {
380
+ const other = b.pasted[index];
381
+ return other !== void 0 && other.number === paste.number && other.text === paste.text;
382
+ });
383
+ }
384
+ function parseComposerDraftPaste(value) {
385
+ const record = asRecord(value);
386
+ if (record === null) return null;
387
+ const number = record["number"];
388
+ const text = asString(record["text"]);
389
+ if (typeof number !== "number" || !Number.isInteger(number) || number < 1 || number > MAX_PASTE_NUMBER) return null;
390
+ return text === null ? null : { number, text };
391
+ }
392
+ function parseComposerDraft(value) {
393
+ const record = asRecord(value);
394
+ if (record === null) return null;
395
+ const text = asString(record["text"]);
396
+ const pasted2 = asArrayOf(record["pasted"], parseComposerDraftPaste);
397
+ if (text === null || pasted2 === null || pasted2.length > MAX_COMPOSER_DRAFT_PASTES) return null;
398
+ if (new Set(pasted2.map((paste) => paste.number)).size !== pasted2.length) return null;
399
+ return { text, pasted: pasted2 };
400
+ }
401
+
357
402
  // packages/shared/src/memory.ts
358
403
  var MEMORY_DIR = ".agents/memory";
359
404
  var MEMORY_INDEX_FILE = "MEMORY.md";
@@ -620,6 +665,21 @@ function parseVaultBodyLine(value) {
620
665
  return null;
621
666
  }
622
667
 
668
+ // packages/shared/src/remote.ts
669
+ var REMOTE_DEFAULT_PORT = 24837;
670
+ var REMOTE_MIN_PORT = 1024;
671
+ var REMOTE_MAX_PORT = 65535;
672
+ var PAIR_QUERY_PARAM = "pair";
673
+ var PAIRING_TTL_MS = 5 * 6e4;
674
+ var REMOTE_DEVICE_LABEL_MAX_CHARS = 60;
675
+ var REMOTE_REVOKED_CLOSE_CODE = 4001;
676
+ function isRemotePort(value) {
677
+ return typeof value === "number" && Number.isInteger(value) && value >= REMOTE_MIN_PORT && value <= REMOTE_MAX_PORT;
678
+ }
679
+ function cleanDeviceLabel(label) {
680
+ return label.replace(/\s+/g, " ").trim().slice(0, REMOTE_DEVICE_LABEL_MAX_CHARS);
681
+ }
682
+
623
683
  // packages/shared/src/protocol.ts
624
684
  var PROTOCOL_VERSION = 8;
625
685
  var WS_PATH = "/ws";
@@ -679,6 +739,8 @@ function parseClientMessage(raw) {
679
739
  if (files.length > 0) message.files = files;
680
740
  }
681
741
  if (record["send"] === false) message.send = false;
742
+ const requestId = asNonEmptyString(record["requestId"]);
743
+ if (requestId !== null) message.requestId = requestId;
682
744
  return message;
683
745
  }
684
746
  case "agent.mode": {
@@ -971,6 +1033,43 @@ function parseClientMessage(raw) {
971
1033
  const searchId = asNonEmptyString(record["searchId"]);
972
1034
  return searchId === null ? null : { type: "search.cancel", searchId };
973
1035
  }
1036
+ case "remote.configure": {
1037
+ const rawEnabled = record["enabled"];
1038
+ const rawPort = record["port"];
1039
+ const enabled2 = rawEnabled === void 0 ? void 0 : asBoolean(rawEnabled);
1040
+ const port = rawPort === void 0 ? void 0 : asFiniteNumber(rawPort);
1041
+ if (enabled2 === null || port === null || enabled2 === void 0 && port === void 0) return null;
1042
+ return {
1043
+ type: "remote.configure",
1044
+ ...enabled2 !== void 0 ? { enabled: enabled2 } : {},
1045
+ ...port !== void 0 ? { port } : {}
1046
+ };
1047
+ }
1048
+ case "remote.pairing.start":
1049
+ return { type: "remote.pairing.start" };
1050
+ case "remote.pairing.cancel":
1051
+ return { type: "remote.pairing.cancel" };
1052
+ case "remote.device.rename": {
1053
+ const deviceId = asNonEmptyString(record["deviceId"]);
1054
+ const rawLabel = asString(record["label"]);
1055
+ const label = rawLabel === null ? "" : cleanDeviceLabel(rawLabel);
1056
+ return deviceId === null || label.length === 0 ? null : { type: "remote.device.rename", deviceId, label };
1057
+ }
1058
+ case "remote.device.revoke": {
1059
+ const deviceId = asNonEmptyString(record["deviceId"]);
1060
+ return deviceId === null ? null : { type: "remote.device.revoke", deviceId };
1061
+ }
1062
+ case "remote.phone.start":
1063
+ return { type: "remote.phone.start" };
1064
+ case "remote.phone.cancel":
1065
+ return { type: "remote.phone.cancel" };
1066
+ case "remote.device.forgetSelf":
1067
+ return { type: "remote.device.forgetSelf" };
1068
+ case "composer.draft": {
1069
+ const terminalId = asNonEmptyString(record["terminalId"]);
1070
+ const draft = parseComposerDraft(record["draft"]);
1071
+ return terminalId === null || draft === null ? null : { type: "composer.draft", terminalId, draft };
1072
+ }
974
1073
  default:
975
1074
  return null;
976
1075
  }
@@ -1030,6 +1129,9 @@ function pastedBoundaryPattern() {
1030
1129
  return /\[(?:Start|End) of pasted text #\d{1,6}\]/g;
1031
1130
  }
1032
1131
 
1132
+ // packages/shared/src/support.ts
1133
+ var SUPPORT_URL = "https://buymeacoffee.com/cvelasquez";
1134
+
1033
1135
  // packages/server/src/archived-sessions.ts
1034
1136
  import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
1035
1137
  import path2 from "node:path";
@@ -1068,12 +1170,18 @@ function archivedSessionsPath() {
1068
1170
  function appSettingsPath() {
1069
1171
  return path.join(appConfigDir(), "settings.json");
1070
1172
  }
1173
+ function remoteDevicesPath() {
1174
+ return path.join(appConfigDir(), "remote-devices.json");
1175
+ }
1071
1176
  function notesStatePath() {
1072
1177
  return path.join(appConfigDir(), "notes.json");
1073
1178
  }
1074
1179
  function notesImagesDir() {
1075
1180
  return path.join(appConfigDir(), "notes-images");
1076
1181
  }
1182
+ function composerDraftsPath() {
1183
+ return path.join(appConfigDir(), "composer-drafts.json");
1184
+ }
1077
1185
  function catalogTempDir() {
1078
1186
  return path.join(tmpdir(), "agent-workbench", "agy-catalog");
1079
1187
  }
@@ -4964,6 +5072,135 @@ function toTurnDuration(record) {
4964
5072
  return { eventId, durationMs };
4965
5073
  }
4966
5074
 
5075
+ // packages/server/src/agents/claude-code/subagent-tracker.ts
5076
+ var SUBAGENT_TOOLS = /* @__PURE__ */ new Set(["Agent", "Task"]);
5077
+ var MAX_TRACKED = 50;
5078
+ var NOTIFICATION = /<task-notification>([\s\S]*?)<\/task-notification>/g;
5079
+ var TOOL_USE_ID = /<tool-use-id>\s*([^<\s]+)\s*<\/tool-use-id>/;
5080
+ function labelOf(value) {
5081
+ if (typeof value !== "string") return "";
5082
+ return value.replace(/\s+/g, " ").trim().slice(0, MAX_SUBAGENT_DESCRIPTION);
5083
+ }
5084
+ function notifiedToolUseIds(text) {
5085
+ if (!text.includes("<task-notification>")) return [];
5086
+ const ids = [];
5087
+ for (const match of text.matchAll(NOTIFICATION)) {
5088
+ const id = TOOL_USE_ID.exec(match[1] ?? "")?.[1];
5089
+ if (id !== void 0) ids.push(id);
5090
+ }
5091
+ return ids;
5092
+ }
5093
+ function contentText(content) {
5094
+ if (typeof content === "string") return content;
5095
+ if (!Array.isArray(content)) return "";
5096
+ let text = "";
5097
+ for (const block of content) {
5098
+ const record = recordOf(block);
5099
+ if (record !== null && record["type"] === "text" && typeof record["text"] === "string") {
5100
+ text += record["text"];
5101
+ }
5102
+ }
5103
+ return text;
5104
+ }
5105
+ function launchedInBackground(toolUseResult) {
5106
+ const result = recordOf(toolUseResult);
5107
+ return result !== null && (result["status"] === "async_launched" || result["isAsync"] === true);
5108
+ }
5109
+ var SubagentTracker = class {
5110
+ /** Los que no terminaron, en orden de lanzamiento (`Map` conserva el de insercion). */
5111
+ pending = /* @__PURE__ */ new Map();
5112
+ /** Una linea del JSONL, ya parseada. Nunca lanza: el esquema cambia. */
5113
+ observe(record) {
5114
+ if (record["isSidechain"] === true) return;
5115
+ switch (record["type"]) {
5116
+ case "assistant":
5117
+ this.observeLaunches(record);
5118
+ return;
5119
+ case "user":
5120
+ this.observeResults(record);
5121
+ this.finish(notifiedToolUseIds(contentText(recordOf(record["message"])?.["content"])));
5122
+ return;
5123
+ case "queue-operation":
5124
+ if (record["operation"] === "enqueue" && typeof record["content"] === "string") {
5125
+ this.finish(notifiedToolUseIds(record["content"]));
5126
+ }
5127
+ return;
5128
+ case "attachment": {
5129
+ const attachment = recordOf(record["attachment"]);
5130
+ if (attachment?.["type"] === "queued_command" && typeof attachment["prompt"] === "string") {
5131
+ this.finish(notifiedToolUseIds(attachment["prompt"]));
5132
+ }
5133
+ return;
5134
+ }
5135
+ default:
5136
+ return;
5137
+ }
5138
+ }
5139
+ /**
5140
+ * Los que siguen trabajando y los lanzo el proceso de `launchedAt` o uno
5141
+ * posterior. Uno sin hora no se puede atribuir a ningun proceso y no cuenta.
5142
+ */
5143
+ running(launchedAt) {
5144
+ const result = [];
5145
+ for (const subagent of this.pending.values()) {
5146
+ if (subagent.startedAt > 0 && subagent.startedAt >= launchedAt) result.push({ ...subagent });
5147
+ }
5148
+ return result;
5149
+ }
5150
+ /** El archivo se reemplazo: lo anotado era de otra lectura. */
5151
+ reset() {
5152
+ this.pending.clear();
5153
+ }
5154
+ observeLaunches(record) {
5155
+ const content = recordOf(record["message"])?.["content"];
5156
+ if (!Array.isArray(content)) return;
5157
+ const startedAt = readTimestamp(record["timestamp"]);
5158
+ for (const block of content) {
5159
+ const call = recordOf(block);
5160
+ if (call === null || call["type"] !== "tool_use") continue;
5161
+ const toolUseId = call["id"];
5162
+ if (typeof toolUseId !== "string" || toolUseId.length === 0) continue;
5163
+ if (typeof call["name"] !== "string" || !SUBAGENT_TOOLS.has(call["name"])) continue;
5164
+ if (this.pending.has(toolUseId)) continue;
5165
+ const input = recordOf(call["input"]);
5166
+ this.pending.set(toolUseId, {
5167
+ toolUseId,
5168
+ description: labelOf(input?.["description"]),
5169
+ background: input?.["run_in_background"] === true,
5170
+ startedAt
5171
+ });
5172
+ if (this.pending.size > MAX_TRACKED) {
5173
+ const oldest = this.pending.keys().next();
5174
+ if (!oldest.done) this.pending.delete(oldest.value);
5175
+ }
5176
+ }
5177
+ }
5178
+ /**
5179
+ * El resultado de la llamada. En segundo plano dice que quedo andando, y
5180
+ * sigue; si no, termino —o no llego a lanzarse, que para el hilo es lo mismo—.
5181
+ */
5182
+ observeResults(record) {
5183
+ const content = recordOf(record["message"])?.["content"];
5184
+ if (!Array.isArray(content)) return;
5185
+ for (const block of content) {
5186
+ const result = recordOf(block);
5187
+ if (result === null || result["type"] !== "tool_result") continue;
5188
+ const toolUseId = result["tool_use_id"];
5189
+ if (typeof toolUseId !== "string") continue;
5190
+ const subagent = this.pending.get(toolUseId);
5191
+ if (subagent === void 0) continue;
5192
+ if (result["is_error"] !== true && launchedInBackground(record["toolUseResult"])) {
5193
+ subagent.background = true;
5194
+ } else {
5195
+ this.pending.delete(toolUseId);
5196
+ }
5197
+ }
5198
+ }
5199
+ finish(toolUseIds) {
5200
+ for (const toolUseId of toolUseIds) this.pending.delete(toolUseId);
5201
+ }
5202
+ };
5203
+
4967
5204
  // packages/server/src/agents/claude-code/conversation-follower.ts
4968
5205
  var MAX_ATTACHMENT_LINKS = 100;
4969
5206
  var MAX_PLAN_REFS = 50;
@@ -5050,6 +5287,11 @@ var ConversationFollower = class {
5050
5287
  * ninguna—, y ahi vale con que se lanzo la pestana (`LAUNCH_PERMISSION_MODE`).
5051
5288
  */
5052
5289
  permissionMode = null;
5290
+ /**
5291
+ * Los subagentes que no terminaron (§4.15). Mira lineas que no son tarjetas
5292
+ * —el aviso de fin es una `queue-operation`—, asi que va antes de todo filtro.
5293
+ */
5294
+ subagents = new SubagentTracker();
5053
5295
  /** Los topes de cada parte. Los de transporte salvo que se lea para la copia propia. */
5054
5296
  limits;
5055
5297
  /** La pestana cuyos documentos se reconocen. Ver el constructor. */
@@ -5102,6 +5344,10 @@ var ConversationFollower = class {
5102
5344
  getUsage() {
5103
5345
  return this.usage;
5104
5346
  }
5347
+ /** Los subagentes del proceso de `launchedAt` que siguen trabajando (§4.15). */
5348
+ runningSubagents(launchedAt) {
5349
+ return this.subagents.running(launchedAt);
5350
+ }
5105
5351
  /** Ultimos `limit` eventos, que es lo que se quiere ver al abrir el panel. */
5106
5352
  getTail(limit) {
5107
5353
  return this.jsonl.getTail(limit);
@@ -5147,6 +5393,7 @@ var ConversationFollower = class {
5147
5393
  this.permissionMode = null;
5148
5394
  this.attachmentRoots.clear();
5149
5395
  this.planFiles = [];
5396
+ this.subagents.reset();
5150
5397
  }
5151
5398
  /** true si la linea es la invocacion de `/model` que la CLI guarda. */
5152
5399
  mentionsModelCommand(record) {
@@ -5163,6 +5410,7 @@ var ConversationFollower = class {
5163
5410
  * los que ya guardo, para pegarle a uno anterior la duracion o una imagen.
5164
5411
  */
5165
5412
  consume(record, lineNumber, events) {
5413
+ this.subagents.observe(record);
5166
5414
  if (record["type"] === "cost-state") {
5167
5415
  this.sessionVariants.observeCostState(record, Date.now());
5168
5416
  this.recomputeModel();
@@ -5542,6 +5790,10 @@ var ClaudeCodeSessionFollower = class {
5542
5790
  hasOpenToolCall() {
5543
5791
  return false;
5544
5792
  }
5793
+ /** Los subagentes del proceso de `launchedAt` que siguen trabajando (§4.15). */
5794
+ runningSubagents(launchedAt) {
5795
+ return this.inner.runningSubagents(launchedAt);
5796
+ }
5545
5797
  };
5546
5798
 
5547
5799
  // packages/server/src/agents/claude-code/session-scan.ts
@@ -10063,6 +10315,9 @@ var INITIAL_EVENT_LIMIT = 300;
10063
10315
  var MAX_PAGE_LIMIT = 500;
10064
10316
  var PENDING_QUESTION_LOOKBACK = 50;
10065
10317
  var POLL_DEBOUNCE_MS = 150;
10318
+ function sameSubagents(a, b) {
10319
+ return a.length === b.length && a.every((item, index) => item.toolUseId === b[index]?.toolUseId && item.background === b[index]?.background);
10320
+ }
10066
10321
  var ConversationHub = class extends EventEmitter {
10067
10322
  constructor(registry, agents) {
10068
10323
  super();
@@ -10070,7 +10325,10 @@ var ConversationHub = class extends EventEmitter {
10070
10325
  this.agents = agents;
10071
10326
  registry.on("session", (terminalId) => void this.rebind(terminalId));
10072
10327
  registry.on("changed", () => {
10073
- for (const [terminalId, entry] of this.entries) this.refreshOpenToolCall(terminalId, entry);
10328
+ for (const [terminalId, entry] of this.entries) {
10329
+ this.refreshOpenToolCall(terminalId, entry);
10330
+ this.refreshSubagents(terminalId, entry);
10331
+ }
10074
10332
  });
10075
10333
  }
10076
10334
  registry;
@@ -10094,6 +10352,7 @@ var ConversationHub = class extends EventEmitter {
10094
10352
  usage: { ...EMPTY_CONTEXT_USAGE },
10095
10353
  waitingFor: null,
10096
10354
  openToolCall: false,
10355
+ subagents: [],
10097
10356
  plans: []
10098
10357
  };
10099
10358
  }
@@ -10130,6 +10389,7 @@ var ConversationHub = class extends EventEmitter {
10130
10389
  permissionMode: entry.observedMode ?? entry.assumedMode,
10131
10390
  waitingFor: entry.waitingFor,
10132
10391
  openToolCall: entry.openToolCall,
10392
+ subagents: entry.subagents,
10133
10393
  plans: plans === null ? [] : await plans.describe(
10134
10394
  { cwd: entry.cwd, sessionId: entry.sessionId },
10135
10395
  entry.follower.getPlanFiles()
@@ -10179,6 +10439,7 @@ var ConversationHub = class extends EventEmitter {
10179
10439
  waitingFor: null,
10180
10440
  statusActivity: null,
10181
10441
  openToolCall: false,
10442
+ subagents: [],
10182
10443
  primed: false,
10183
10444
  stopWatchingStatus: null,
10184
10445
  followTicker: null
@@ -10221,6 +10482,23 @@ var ConversationHub = class extends EventEmitter {
10221
10482
  entry.openToolCall = open8;
10222
10483
  this.emit("toolCall", terminalId, open8);
10223
10484
  }
10485
+ /**
10486
+ * Recalcula los subagentes que siguen trabajando y avisa si cambiaron
10487
+ * (§4.15). Solo los del proceso vivo: sin pty no queda ninguno, y los de un
10488
+ * proceso anterior murieron con el sin escribir el aviso de fin. Una CLI cuyo
10489
+ * seguidor no los declara no tiene nunca.
10490
+ */
10491
+ refreshSubagents(terminalId, entry) {
10492
+ const launchedAt = this.registry.launchedAtOf(terminalId);
10493
+ const next = launchedAt === null || entry.follower.runningSubagents === void 0 ? [] : [...entry.follower.runningSubagents(launchedAt)];
10494
+ if (sameSubagents(next, entry.subagents)) return;
10495
+ entry.subagents = next;
10496
+ this.emit("subagents", terminalId, next);
10497
+ }
10498
+ /** Los subagentes que siguen trabajando, lo ultimo avisado. [] si nadie sigue la pestana. */
10499
+ getSubagents(terminalId) {
10500
+ return this.entries.get(terminalId)?.subagents ?? [];
10501
+ }
10224
10502
  /**
10225
10503
  * La pestana cambio de sesion: se suelta el seguidor viejo y se sigue la
10226
10504
  * nueva, sin perder a quien estaba mirando.
@@ -10401,6 +10679,7 @@ var ConversationHub = class extends EventEmitter {
10401
10679
  permissionMode: this.resetMode(entry),
10402
10680
  waitingFor: entry.waitingFor,
10403
10681
  openToolCall: entry.openToolCall,
10682
+ subagents: entry.subagents,
10404
10683
  // Este snapshot es sincronico y describir un plan pide el disco. Quien
10405
10684
  // necesita los planes usa `subscribe`, que si puede esperar.
10406
10685
  plans: []
@@ -10557,6 +10836,7 @@ var ConversationHub = class extends EventEmitter {
10557
10836
  return;
10558
10837
  }
10559
10838
  this.refreshOpenToolCall(terminalId, entry);
10839
+ this.refreshSubagents(terminalId, entry);
10560
10840
  if (options.silent) return;
10561
10841
  const observed = entry.follower.getPermissionMode();
10562
10842
  if (observed !== null) {
@@ -10665,162 +10945,1023 @@ function ensurePtyHelperExecutable(options = {}) {
10665
10945
  return fixed;
10666
10946
  }
10667
10947
 
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");
10948
+ // packages/server/src/remote-access-service.ts
10949
+ import { execFile as execFile3 } from "node:child_process";
10950
+ import { randomBytes as cryptoRandomBytes, randomUUID as randomUUID2 } from "node:crypto";
10951
+ import os from "node:os";
10952
+
10953
+ // packages/server/src/phone-pairing.ts
10954
+ import { createPrivateKey, createPublicKey } from "node:crypto";
10955
+ var PHONE_PAYLOAD_PREFIX = "agentworkbench://pair?";
10956
+ var PHONE_PAYLOAD_VERSION = 1;
10957
+ var PHONE_SSH_PORT = 22;
10958
+ var PHONE_KEY_COMMENT = "agent-workbench-phone";
10959
+ var PHONE_SEED_BYTES = 32;
10960
+ var ED25519_PKCS8_PREFIX = Buffer.from("302e020100300506032b657004220420", "hex");
10961
+ function ed25519PublicKey(seed) {
10962
+ if (seed.length !== PHONE_SEED_BYTES) throw new Error(`an Ed25519 seed has ${PHONE_SEED_BYTES} bytes`);
10963
+ const privateKey = createPrivateKey({ key: Buffer.concat([ED25519_PKCS8_PREFIX, seed]), format: "der", type: "pkcs8" });
10964
+ const spki = createPublicKey(privateKey).export({ format: "der", type: "spki" });
10965
+ return Buffer.from(spki.subarray(spki.length - 32));
10966
+ }
10967
+ function sshString(bytes) {
10968
+ const length = Buffer.alloc(4);
10969
+ length.writeUInt32BE(bytes.length);
10970
+ return Buffer.concat([length, bytes]);
10971
+ }
10972
+ function sshEd25519PublicKey(publicKey) {
10973
+ const blob = Buffer.concat([sshString(Buffer.from("ssh-ed25519", "ascii")), sshString(publicKey)]);
10974
+ return `ssh-ed25519 ${blob.toString("base64")}`;
10975
+ }
10976
+ function phoneAuthorizedKeysEntry(publicKey, appPort) {
10977
+ return `restrict,port-forwarding,permitopen="127.0.0.1:${appPort}",command="exit" ${sshEd25519PublicKey(publicKey)} ${PHONE_KEY_COMMENT}`;
10978
+ }
10979
+ function phoneAuthorizeCommand(entry, shell) {
10980
+ switch (shell) {
10981
+ case "powershell-admin":
10982
+ 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'`;
10983
+ case "powershell":
10984
+ return `New-Item -ItemType Directory -Force "$HOME\\.ssh" | Out-Null; Add-Content -Path "$HOME\\.ssh\\authorized_keys" -Encoding ascii -Value @('', '${entry}')`;
10985
+ case "terminal":
10986
+ return `mkdir -p ~/.ssh && chmod 700 ~/.ssh && printf '\\n%s\\n' '${entry}' >> ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys`;
10987
+ }
10988
+ }
10989
+ function authorizeShellFromGroups(whoamiGroups) {
10990
+ return /S-1-5-32-544/.test(whoamiGroups) ? "powershell-admin" : "powershell";
10991
+ }
10992
+ function phonePairingPayload(fields) {
10993
+ const params = new URLSearchParams();
10994
+ params.set("v", String(PHONE_PAYLOAD_VERSION));
10995
+ params.set("n", fields.pcName);
10996
+ params.set("h", fields.hosts.join(","));
10997
+ params.set("p", String(fields.sshPort));
10998
+ params.set("u", fields.user);
10999
+ params.set("a", String(fields.appPort));
11000
+ params.set("c", fields.code);
11001
+ params.set("k", fields.seed.toString("base64url"));
11002
+ return PHONE_PAYLOAD_PREFIX + params.toString();
10676
11003
  }
10677
- function constantTimeEquals(a, b) {
11004
+
11005
+ // packages/server/src/remote-access.ts
11006
+ import { createHash, timingSafeEqual } from "node:crypto";
11007
+ var PAIRING_ALPHABET = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
11008
+ var PAIRING_CODE_CHARS = 8;
11009
+ var PAIRING_MAX_FAILURES = 5;
11010
+ function formatPairingCode(raw) {
11011
+ const half = PAIRING_CODE_CHARS / 2;
11012
+ return `${raw.slice(0, half)}-${raw.slice(half)}`;
11013
+ }
11014
+ function normalizePairingCode(input) {
11015
+ const raw = input.replace(/[\s-]/g, "").toUpperCase();
11016
+ if (raw.length !== PAIRING_CODE_CHARS) return null;
11017
+ for (const char of raw) {
11018
+ if (!PAIRING_ALPHABET.includes(char)) return null;
11019
+ }
11020
+ return raw;
11021
+ }
11022
+ function sameText(a, b) {
10678
11023
  const bufferA = Buffer.from(a, "utf8");
10679
11024
  const bufferB = Buffer.from(b, "utf8");
10680
11025
  if (bufferA.length !== bufferB.length) return false;
10681
11026
  return timingSafeEqual(bufferA, bufferB);
10682
11027
  }
10683
- function requestUrl(request) {
10684
- try {
10685
- return new URL(request.url ?? "/", `http://${request.headers.host ?? LOOPBACK_HOST}`);
10686
- } catch {
10687
- return null;
11028
+ var PairingDesk = class {
11029
+ constructor(deps) {
11030
+ this.deps = deps;
10688
11031
  }
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;
11032
+ deps;
11033
+ current = null;
11034
+ /** Un código nuevo. Reemplaza al que hubiera: nunca hay dos vigentes. */
11035
+ start() {
11036
+ const bytes = this.deps.randomBytes(PAIRING_CODE_CHARS);
11037
+ let raw = "";
11038
+ for (let i = 0; i < PAIRING_CODE_CHARS; i++) raw += PAIRING_ALPHABET[(bytes[i] ?? 0) % PAIRING_ALPHABET.length];
11039
+ const expiresAt = this.deps.now() + PAIRING_TTL_MS;
11040
+ this.current = { raw, expiresAt, failures: 0 };
11041
+ return { code: formatPairingCode(raw), expiresAt };
10699
11042
  }
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" };
11043
+ cancel() {
11044
+ this.current = null;
10707
11045
  }
10708
- const fromHeader = request.headers[TOKEN_HEADER];
10709
- if (typeof fromHeader === "string" && constantTimeEquals(fromHeader, expected)) {
10710
- return { source: "header" };
11046
+ isActive() {
11047
+ if (this.current !== null && this.deps.now() >= this.current.expiresAt) this.current = null;
11048
+ return this.current !== null;
10711
11049
  }
10712
- const fromCookie = readCookie(request, TOKEN_COOKIE);
10713
- if (fromCookie !== null && constantTimeEquals(fromCookie, expected)) {
10714
- return { source: "cookie" };
11050
+ /** Cuándo vence el vigente, o null. */
11051
+ expiresAt() {
11052
+ return this.isActive() ? this.current?.expiresAt ?? null : null;
10715
11053
  }
10716
- return null;
10717
- }
10718
- function hasValidToken(request, expected) {
10719
- return matchToken(request, expected) !== null;
11054
+ redeem(input) {
11055
+ if (!this.isActive() || this.current === null) return "none";
11056
+ const raw = normalizePairingCode(input);
11057
+ if (raw === null) return "none";
11058
+ if (sameText(raw, this.current.raw)) {
11059
+ this.current = null;
11060
+ return "ok";
11061
+ }
11062
+ this.current.failures += 1;
11063
+ if (this.current.failures >= PAIRING_MAX_FAILURES) this.current = null;
11064
+ return "wrong";
11065
+ }
11066
+ };
11067
+ var REMOTE_DEVICES_VERSION = 1;
11068
+ var REMOTE_MAX_DEVICES = 20;
11069
+ function hashCredential(credential) {
11070
+ return createHash("sha256").update(credential, "utf8").digest("hex");
10720
11071
  }
10721
- function buildTokenCookie(token) {
10722
- return `${TOKEN_COOKIE}=${encodeURIComponent(token)}; Path=/; SameSite=Strict; HttpOnly`;
11072
+ var DeviceBook = class {
11073
+ constructor(deps) {
11074
+ this.deps = deps;
11075
+ }
11076
+ deps;
11077
+ devices = [];
11078
+ load(devices) {
11079
+ this.devices = devices.slice(0, REMOTE_MAX_DEVICES).map((device) => ({ ...device }));
11080
+ }
11081
+ /** Para guardar: con el hash. */
11082
+ snapshot() {
11083
+ return this.devices.map((device) => ({ ...device }));
11084
+ }
11085
+ /** Para mostrar: sin el hash. */
11086
+ list() {
11087
+ return this.devices.map(({ id, label, createdAt, lastSeenAt }) => ({ id, label, createdAt, lastSeenAt }));
11088
+ }
11089
+ get size() {
11090
+ return this.devices.length;
11091
+ }
11092
+ /** Un equipo nuevo y su credencial, que no queda guardada en ningún lado; null si ya no entran más. */
11093
+ enroll(label) {
11094
+ if (this.devices.length >= REMOTE_MAX_DEVICES) return null;
11095
+ const credential = this.deps.randomBytes(32).toString("base64url");
11096
+ const device = {
11097
+ id: this.deps.randomId(),
11098
+ label: cleanDeviceLabel(label) || "Device",
11099
+ hash: hashCredential(credential),
11100
+ createdAt: this.deps.now(),
11101
+ lastSeenAt: null
11102
+ };
11103
+ this.devices.push(device);
11104
+ return { device: { ...device }, credential };
11105
+ }
11106
+ match(credential) {
11107
+ if (credential.length === 0) return null;
11108
+ const hash = hashCredential(credential);
11109
+ let found = null;
11110
+ for (const device of this.devices) {
11111
+ if (sameText(device.hash, hash)) found = device;
11112
+ }
11113
+ return found === null ? null : { ...found };
11114
+ }
11115
+ /** Anota cuándo se lo vio: ahora, o la marca que se le pase. */
11116
+ touch(id, at = this.deps.now()) {
11117
+ const device = this.devices.find((candidate) => candidate.id === id);
11118
+ if (device === void 0) return false;
11119
+ device.lastSeenAt = at;
11120
+ return true;
11121
+ }
11122
+ rename(id, label) {
11123
+ const device = this.devices.find((candidate) => candidate.id === id);
11124
+ const clean = cleanDeviceLabel(label);
11125
+ if (device === void 0 || clean.length === 0) return false;
11126
+ device.label = clean;
11127
+ return true;
11128
+ }
11129
+ revoke(id) {
11130
+ const before = this.devices.length;
11131
+ this.devices = this.devices.filter((device) => device.id !== id);
11132
+ return this.devices.length !== before;
11133
+ }
11134
+ };
11135
+ function parseRemoteDevice(value) {
11136
+ const record = asRecord(value);
11137
+ if (record === null) return null;
11138
+ const id = asNonEmptyString(record["id"]);
11139
+ const label = asString(record["label"]);
11140
+ const hash = asString(record["hash"]);
11141
+ const createdAt = asFiniteNumber(record["createdAt"]);
11142
+ if (id === null || label === null || hash === null || createdAt === null) return null;
11143
+ if (!/^[0-9a-f]{64}$/.test(hash)) return null;
11144
+ const lastSeenAt = record["lastSeenAt"] === null || record["lastSeenAt"] === void 0 ? null : asFiniteNumber(record["lastSeenAt"]);
11145
+ return { id, label: cleanDeviceLabel(label) || "Device", hash, createdAt, lastSeenAt };
10723
11146
  }
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);
11147
+ function parseRemoteDevicesFile(value) {
11148
+ const record = asRecord(value);
11149
+ if (record === null || record["version"] !== REMOTE_DEVICES_VERSION) return null;
11150
+ return asArrayFiltered(record["devices"], parseRemoteDevice) ?? [];
10732
11151
  }
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);
11152
+ function serializeRemoteDevices(devices) {
11153
+ return `${JSON.stringify({ version: REMOTE_DEVICES_VERSION, devices }, null, 2)}
11154
+ `;
10745
11155
  }
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";
11156
+ function remoteAccessState(remote, listening) {
11157
+ if (!remote.enabled) return "off";
11158
+ if (listening.fixedPortBusy) return "port-busy";
11159
+ if (!listening.fixed || listening.port !== remote.port) return "restart-needed";
11160
+ return "active";
11161
+ }
11162
+ function isPrivateIpv4(address) {
11163
+ const parts = address.split(".").map((part) => Number(part));
11164
+ if (parts.length !== 4 || parts.some((part) => !Number.isInteger(part) || part < 0 || part > 255)) return false;
11165
+ const [a, b] = parts;
11166
+ return a === 10 || a === 172 && b >= 16 && b <= 31 || a === 192 && b === 168;
11167
+ }
11168
+ function privateIpv4Addresses(interfaces) {
11169
+ const addresses = [];
11170
+ for (const list of Object.values(interfaces)) {
11171
+ for (const info of list ?? []) {
11172
+ if (info.internal || info.family !== "IPv4" || !isPrivateIpv4(info.address)) continue;
11173
+ if (!addresses.includes(info.address)) addresses.push(info.address);
11174
+ }
11175
+ }
11176
+ return addresses;
11177
+ }
11178
+ var PHONE_APP_USER_AGENT = /AgentWorkbenchAndroid\/\S+ \(Android [^;)]*; ([^)]{1,80})\)/;
11179
+ function deviceLabelFromUserAgent(userAgent) {
11180
+ if (userAgent === void 0) return "Device";
11181
+ const phone = PHONE_APP_USER_AGENT.exec(userAgent);
11182
+ if (phone !== null) {
11183
+ const name = cleanDeviceLabel(phone[1] ?? "");
11184
+ return name.length > 0 ? cleanDeviceLabel(`${name} \xB7 Android`) : "Android";
11185
+ }
11186
+ const browser = /Edg\//.test(userAgent) ? "Edge" : /OPR\//.test(userAgent) ? "Opera" : /Firefox\//.test(userAgent) ? "Firefox" : /Chrome\//.test(userAgent) ? "Chrome" : /Safari\//.test(userAgent) ? "Safari" : null;
11187
+ 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;
11188
+ if (browser === null || system === null) return "Device";
11189
+ return `${browser} \xB7 ${system}`;
11190
+ }
11191
+ var HOST_ONLY = /* @__PURE__ */ new Set([
11192
+ "remote.configure",
11193
+ "remote.pairing.start",
11194
+ "remote.pairing.cancel",
11195
+ "remote.device.rename",
11196
+ "remote.device.revoke",
11197
+ "remote.phone.start",
11198
+ "remote.phone.cancel"
11199
+ ]);
11200
+ var OPENS_ON_HOST = /* @__PURE__ */ new Set([
11201
+ "files.reveal",
11202
+ "vault.reveal",
11203
+ "vault.openSession",
11204
+ "vault.exportProject"
11205
+ ]);
11206
+ function remoteRefusal(type, remoteClient) {
11207
+ if (!remoteClient) return null;
11208
+ if (HOST_ONLY.has(type)) return serverText("remoteHostOnly");
11209
+ if (OPENS_ON_HOST.has(type)) return serverText("remoteOpensOnHost");
10750
11210
  return null;
10751
11211
  }
10752
11212
 
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
- );
11213
+ // packages/server/src/remote-access-service.ts
11214
+ var TOUCH_SAVE_DELAY_MS = 5e3;
11215
+ var FILES_POLL_MS = 2e3;
11216
+ function detectAuthorizeShell() {
11217
+ if (process.platform !== "win32") return Promise.resolve("terminal");
11218
+ return new Promise((resolve) => {
11219
+ execFile3("whoami", ["/groups", "/fo", "csv", "/nh"], { windowsHide: true, timeout: 5e3 }, (error, stdout) => {
11220
+ resolve(error === null ? authorizeShellFromGroups(String(stdout)) : "powershell-admin");
11221
+ });
10812
11222
  });
10813
11223
  }
10814
- function toPosix(value) {
10815
- return value.split("\\").join("/");
11224
+ function detectHostInfo() {
11225
+ let user = "";
11226
+ try {
11227
+ user = os.userInfo().username;
11228
+ } catch {
11229
+ user = process.env["USERNAME"] ?? process.env["USER"] ?? "";
11230
+ }
11231
+ return { user, hostNames: [os.hostname(), ...privateIpv4Addresses(os.networkInterfaces())] };
10816
11232
  }
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 };
11233
+ var RemoteAccessService = class {
11234
+ settings;
11235
+ store;
11236
+ listening;
11237
+ host;
11238
+ now;
11239
+ log;
11240
+ book;
11241
+ desk;
11242
+ randomBytes;
11243
+ authorizeShell;
11244
+ authorizeShellCache = null;
11245
+ /**
11246
+ * La semilla de la llave del teléfono que se está emparejando (hito 38). Sólo
11247
+ * en memoria, y sólo mientras dure ese emparejamiento: se olvida al
11248
+ * cancelarlo, al canjearse el código o al cerrarse la ventana que lo pidió.
11249
+ */
11250
+ phoneSeed = null;
11251
+ changeListeners = /* @__PURE__ */ new Set();
11252
+ revokedListeners = /* @__PURE__ */ new Set();
11253
+ /** Cuántas ventanas tiene abiertas cada equipo. */
11254
+ connections = /* @__PURE__ */ new Map();
11255
+ /** Las "última vez visto" que todavía no se guardaron: releer el archivo no se las lleva. */
11256
+ pendingSeen = /* @__PURE__ */ new Map();
11257
+ pairingTimer = null;
11258
+ touchTimer = null;
11259
+ pollTimer = null;
11260
+ /** Cómo estaban los dos archivos la última vez que se leyeron o escribieron desde acá. */
11261
+ stamps = { devices: "", settings: "" };
11262
+ /** Las sincronizaciones, en fila: la del sondeo y la de un cambio no se cruzan. */
11263
+ syncing = Promise.resolve();
11264
+ constructor(options) {
11265
+ this.settings = options.settings;
11266
+ this.store = options.store;
11267
+ this.listening = options.listening;
11268
+ this.host = options.host ?? null;
11269
+ this.now = options.now ?? (() => Date.now());
11270
+ this.log = options.log ?? console;
11271
+ const randomBytes5 = options.randomBytes ?? ((size) => cryptoRandomBytes(size));
11272
+ this.randomBytes = randomBytes5;
11273
+ this.authorizeShell = options.authorizeShell ?? detectAuthorizeShell;
11274
+ this.book = new DeviceBook({ now: this.now, randomBytes: randomBytes5, randomId: () => randomUUID2() });
11275
+ this.desk = new PairingDesk({ now: this.now, randomBytes: randomBytes5 });
10822
11276
  }
10823
- const stderr = result.stderr.trim();
11277
+ async load() {
11278
+ this.book.load(await this.store.load());
11279
+ this.stamps = { devices: await this.store.stamp(), settings: await this.settings.stamp() };
11280
+ this.updatePolling();
11281
+ }
11282
+ // ---- Qué corre ---------------------------------------------------------------
11283
+ remote() {
11284
+ return this.settings.get().remote;
11285
+ }
11286
+ /** Entran equipos: encendido, y el servidor escucha en el puerto fijo con el que arrancó. */
11287
+ accepting() {
11288
+ return this.remote().enabled && this.listening.fixed;
11289
+ }
11290
+ /** A quién pertenece una credencial, o null si ahora no entra nadie. */
11291
+ verifier() {
11292
+ return this.accepting() ? this.book : null;
11293
+ }
11294
+ get pairedDevices() {
11295
+ return this.book.size;
11296
+ }
11297
+ status() {
11298
+ const remote = this.remote();
11299
+ const host = this.host ?? detectHostInfo();
11300
+ return {
11301
+ enabled: remote.enabled,
11302
+ port: remote.port,
11303
+ state: remoteAccessState(remote, this.listening),
11304
+ listeningPort: this.listening.port,
11305
+ sshUser: host.user,
11306
+ hostNames: [...host.hostNames],
11307
+ pairingActive: this.desk.isActive(),
11308
+ devices: this.book.list().map((device) => ({
11309
+ ...device,
11310
+ connected: (this.connections.get(device.id) ?? 0) > 0
11311
+ }))
11312
+ };
11313
+ }
11314
+ // ---- Lo que cambió otra instancia ----------------------------------------------
11315
+ /**
11316
+ * Trae lo que haya cambiado en los dos archivos desde la última vez. Un equipo
11317
+ * que ya no está se avisa como revocado, y el acceso apagado desde otro lado
11318
+ * corta igual que apagado desde acá.
11319
+ */
11320
+ sync() {
11321
+ const result = this.syncing.then(() => this.syncNow());
11322
+ this.syncing = result.catch(() => void 0);
11323
+ return result;
11324
+ }
11325
+ async syncNow() {
11326
+ let changed = false;
11327
+ const settingsStamp = await this.settings.stamp();
11328
+ if (settingsStamp !== this.stamps.settings) {
11329
+ this.stamps.settings = settingsStamp;
11330
+ const wasAccepting = this.accepting();
11331
+ await this.settings.reloadRemote();
11332
+ if (wasAccepting && !this.accepting()) this.cutOff();
11333
+ changed = true;
11334
+ }
11335
+ const devicesStamp = await this.store.stamp();
11336
+ if (devicesStamp !== this.stamps.devices) {
11337
+ this.stamps.devices = devicesStamp;
11338
+ const before = this.book.list().map((device) => device.id);
11339
+ this.book.load(await this.store.load());
11340
+ for (const [deviceId, at] of this.pendingSeen) this.book.touch(deviceId, at);
11341
+ const still = new Set(this.book.list().map((device) => device.id));
11342
+ for (const deviceId of before) {
11343
+ if (still.has(deviceId)) continue;
11344
+ this.connections.delete(deviceId);
11345
+ for (const listener of this.revokedListeners) listener(deviceId);
11346
+ }
11347
+ changed = true;
11348
+ }
11349
+ if (!changed) return;
11350
+ this.updatePolling();
11351
+ this.emitChange();
11352
+ }
11353
+ /** Sondea sólo con el acceso encendido: apagado, esta instancia no hace nada nuevo. */
11354
+ updatePolling() {
11355
+ const wanted = this.remote().enabled;
11356
+ if (wanted && this.pollTimer === null) {
11357
+ this.pollTimer = setInterval(() => void this.sync().catch(() => void 0), FILES_POLL_MS);
11358
+ this.pollTimer.unref();
11359
+ } else if (!wanted && this.pollTimer !== null) {
11360
+ clearInterval(this.pollTimer);
11361
+ this.pollTimer = null;
11362
+ }
11363
+ }
11364
+ /** El acceso dejó de estar encendido: el código muere y se avisa cerrar las ventanas de todos. */
11365
+ cutOff() {
11366
+ this.stopPairing();
11367
+ this.log.log("[remote] remote access turned off: paired devices are disconnected");
11368
+ for (const listener of this.revokedListeners) listener(null);
11369
+ }
11370
+ // ---- Cambios desde el anfitrión ----------------------------------------------
11371
+ /** Lanza un `ServerTextError` si el puerto no sirve; los ajustes quedan como estaban. */
11372
+ async configure(patch) {
11373
+ if (patch.port !== void 0 && !isRemotePort(patch.port)) {
11374
+ throw new ServerTextError(serverText("remotePortInvalid", { max: REMOTE_MAX_PORT, min: REMOTE_MIN_PORT }));
11375
+ }
11376
+ await this.sync();
11377
+ const wasAccepting = this.accepting();
11378
+ await this.settings.update({ remote: patch });
11379
+ this.stamps.settings = await this.settings.stamp();
11380
+ if (wasAccepting && !this.accepting()) this.cutOff();
11381
+ this.updatePolling();
11382
+ this.emitChange();
11383
+ }
11384
+ startPairing() {
11385
+ if (!this.accepting()) throw new ServerTextError(serverText("remoteNotActive"));
11386
+ if (this.book.size >= REMOTE_MAX_DEVICES) {
11387
+ throw new ServerTextError(serverText("remoteTooManyDevices", { max: REMOTE_MAX_DEVICES }));
11388
+ }
11389
+ this.stopPairing();
11390
+ const pairing = this.desk.start();
11391
+ this.pairingTimer = setTimeout(() => {
11392
+ this.pairingTimer = null;
11393
+ this.emitChange();
11394
+ }, Math.max(0, pairing.expiresAt - this.now()) + 50);
11395
+ this.pairingTimer.unref();
11396
+ this.emitChange();
11397
+ return pairing;
11398
+ }
11399
+ cancelPairing() {
11400
+ if (!this.desk.isActive()) return;
11401
+ this.stopPairing();
11402
+ this.emitChange();
11403
+ }
11404
+ /**
11405
+ * Empareja un teléfono (hito 38, §15): un código de un solo uso y la llave SSH
11406
+ * que va con él en el QR. La llave es la misma mientras no se cancele: si el
11407
+ * código vence antes de que el teléfono lo use, el usuario pide otro y no
11408
+ * tiene que autorizar otra llave.
11409
+ *
11410
+ * La dirección lleva el puerto en el que escucha este arranque, que es el que
11411
+ * el teléfono va a reenviar.
11412
+ */
11413
+ async startPhonePairing() {
11414
+ const pairing = this.startPairing();
11415
+ this.phoneSeed ??= this.randomBytes(PHONE_SEED_BYTES);
11416
+ const seed = this.phoneSeed;
11417
+ this.authorizeShellCache ??= this.authorizeShell();
11418
+ const shell = await this.authorizeShellCache;
11419
+ const host = this.host ?? detectHostInfo();
11420
+ const [pcName = "", ...addresses] = host.hostNames;
11421
+ const appPort = this.listening.port;
11422
+ return {
11423
+ payload: phonePairingPayload({
11424
+ pcName,
11425
+ hosts: addresses.length > 0 ? addresses : [pcName],
11426
+ sshPort: PHONE_SSH_PORT,
11427
+ user: host.user,
11428
+ appPort,
11429
+ code: pairing.code.replace("-", ""),
11430
+ seed
11431
+ }),
11432
+ authorizeCommand: phoneAuthorizeCommand(phoneAuthorizedKeysEntry(ed25519PublicKey(seed), appPort), shell),
11433
+ shell,
11434
+ code: pairing.code,
11435
+ expiresAt: pairing.expiresAt
11436
+ };
11437
+ }
11438
+ /** Olvida la llave del teléfono y da de baja el código. */
11439
+ cancelPhonePairing() {
11440
+ this.phoneSeed = null;
11441
+ this.cancelPairing();
11442
+ }
11443
+ /** Para el chequeo: si todavía hay una llave de teléfono en memoria. */
11444
+ get phoneKeyHeld() {
11445
+ return this.phoneSeed !== null;
11446
+ }
11447
+ stopPairing() {
11448
+ this.desk.cancel();
11449
+ if (this.pairingTimer !== null) clearTimeout(this.pairingTimer);
11450
+ this.pairingTimer = null;
11451
+ }
11452
+ async renameDevice(deviceId, label) {
11453
+ await this.sync();
11454
+ if (!this.book.rename(deviceId, label)) return false;
11455
+ await this.persist();
11456
+ this.emitChange();
11457
+ return true;
11458
+ }
11459
+ async revokeDevice(deviceId) {
11460
+ await this.sync();
11461
+ if (!this.book.revoke(deviceId)) return false;
11462
+ this.connections.delete(deviceId);
11463
+ this.log.log("[remote] a paired device was revoked");
11464
+ for (const listener of this.revokedListeners) listener(deviceId);
11465
+ await this.persist();
11466
+ this.emitChange();
11467
+ return true;
11468
+ }
11469
+ // ---- Lo que llega de afuera --------------------------------------------------
11470
+ /**
11471
+ * Canjea un código por un equipo nuevo, o null. El nombre sale del navegador
11472
+ * que lo presentó; el usuario lo cambia después.
11473
+ */
11474
+ async redeemPairing(input, userAgent) {
11475
+ if (!this.accepting()) return null;
11476
+ const outcome = this.desk.redeem(input);
11477
+ if (outcome === "none") return null;
11478
+ if (outcome === "wrong") {
11479
+ this.log.warn("[remote] a wrong pairing code was presented");
11480
+ if (!this.desk.isActive()) {
11481
+ this.stopPairing();
11482
+ this.log.warn("[remote] too many wrong codes: the pairing code is no longer valid");
11483
+ this.emitChange();
11484
+ }
11485
+ return null;
11486
+ }
11487
+ this.stopPairing();
11488
+ this.phoneSeed = null;
11489
+ await this.sync();
11490
+ const enrolled = this.accepting() ? this.book.enroll(deviceLabelFromUserAgent(userAgent)) : null;
11491
+ if (enrolled === null) {
11492
+ this.emitChange();
11493
+ return null;
11494
+ }
11495
+ await this.persist();
11496
+ this.log.log(`[remote] paired a new device: ${enrolled.device.label}`);
11497
+ this.emitChange();
11498
+ return { deviceId: enrolled.device.id, credential: enrolled.credential };
11499
+ }
11500
+ noteConnected(deviceId) {
11501
+ this.connections.set(deviceId, (this.connections.get(deviceId) ?? 0) + 1);
11502
+ this.touch(deviceId);
11503
+ this.emitChange();
11504
+ }
11505
+ noteDisconnected(deviceId) {
11506
+ const left = (this.connections.get(deviceId) ?? 0) - 1;
11507
+ if (left > 0) this.connections.set(deviceId, left);
11508
+ else this.connections.delete(deviceId);
11509
+ this.touch(deviceId);
11510
+ this.emitChange();
11511
+ }
11512
+ /** La "última vez visto" no vale una escritura por conexión: se junta. */
11513
+ touch(deviceId) {
11514
+ const at = this.now();
11515
+ if (!this.book.touch(deviceId, at)) return;
11516
+ this.pendingSeen.set(deviceId, at);
11517
+ if (this.touchTimer !== null) return;
11518
+ this.touchTimer = setTimeout(() => {
11519
+ this.touchTimer = null;
11520
+ void this.persistTouches();
11521
+ }, TOUCH_SAVE_DELAY_MS);
11522
+ this.touchTimer.unref();
11523
+ }
11524
+ /**
11525
+ * Guarda las "última vez visto". Relee antes: una fecha no puede devolverle
11526
+ * la vida a un equipo que otra instancia revocó mientras tanto —al releer, el
11527
+ * que ya no está no recibe la suya—.
11528
+ */
11529
+ async persistTouches() {
11530
+ if (this.pendingSeen.size === 0) return;
11531
+ await this.sync();
11532
+ this.pendingSeen.clear();
11533
+ await this.persist();
11534
+ }
11535
+ // ---- Avisos y disco ----------------------------------------------------------
11536
+ onChange(listener) {
11537
+ this.changeListeners.add(listener);
11538
+ return () => this.changeListeners.delete(listener);
11539
+ }
11540
+ /** Hay que cerrar las ventanas de ese equipo; con null, las de todos. */
11541
+ onRevoked(listener) {
11542
+ this.revokedListeners.add(listener);
11543
+ return () => this.revokedListeners.delete(listener);
11544
+ }
11545
+ emitChange() {
11546
+ for (const listener of this.changeListeners) listener();
11547
+ }
11548
+ /** Escribe los equipos y anota cómo quedó el archivo, para no tomarlo por un cambio ajeno. Nunca rechaza. */
11549
+ async persist() {
11550
+ try {
11551
+ await this.store.save(this.book.snapshot());
11552
+ this.stamps.devices = await this.store.stamp();
11553
+ } catch (error) {
11554
+ this.log.warn("[remote] couldn't save the paired devices:", error instanceof Error ? error.message : String(error));
11555
+ }
11556
+ }
11557
+ /** Escribe lo pendiente y espera. Nunca rechaza. */
11558
+ async flush() {
11559
+ if (this.touchTimer !== null) clearTimeout(this.touchTimer);
11560
+ this.touchTimer = null;
11561
+ await this.persistTouches().catch(() => void 0);
11562
+ await this.store.flush();
11563
+ }
11564
+ /** Frena los relojes. Lo que quede por guardar lo escribe `flush()`. */
11565
+ dispose() {
11566
+ this.stopPairing();
11567
+ this.phoneSeed = null;
11568
+ if (this.pollTimer !== null) clearInterval(this.pollTimer);
11569
+ this.pollTimer = null;
11570
+ }
11571
+ };
11572
+
11573
+ // packages/server/src/remote-devices-store.ts
11574
+ import { chmod, mkdir as mkdir6, readFile as readFile9, stat as stat16 } from "node:fs/promises";
11575
+ import path31 from "node:path";
11576
+
11577
+ // packages/server/src/vault/write.ts
11578
+ import { randomBytes as randomBytes2 } from "node:crypto";
11579
+ import { mkdir as mkdir5, rename as rename3, rm as rm2, stat as stat15, writeFile as writeFile3 } from "node:fs/promises";
11580
+
11581
+ // packages/server/src/vault/paths.ts
11582
+ import { createHash as createHash2 } from "node:crypto";
11583
+ import path30 from "node:path";
11584
+ function defaultVaultDir() {
11585
+ return path30.join(appConfigDir(), "vault");
11586
+ }
11587
+ function vaultMarkerFile(dir) {
11588
+ return path30.join(dir, "vault.json");
11589
+ }
11590
+ function sessionsRoot2(dir) {
11591
+ return path30.join(dir, "sessions");
11592
+ }
11593
+ function isSafeSessionId(sessionId) {
11594
+ return isVaultSessionId(sessionId);
11595
+ }
11596
+ function isSessionAgentId(value) {
11597
+ return SESSION_AGENT_IDS.includes(value);
11598
+ }
11599
+ function assertSessionKey(agent, sessionId) {
11600
+ if (!isSessionAgentId(agent)) throw new Error(`Unknown source for the local copy: ${JSON.stringify(agent)}`);
11601
+ if (!isSafeSessionId(sessionId)) throw new Error(`Session id that isn't copied: ${JSON.stringify(sessionId)}`);
11602
+ }
11603
+ function agentSessionsDir(dir, agent) {
11604
+ if (!isSessionAgentId(agent)) throw new Error(`Unknown source for the local copy: ${JSON.stringify(agent)}`);
11605
+ return path30.join(sessionsRoot2(dir), agent);
11606
+ }
11607
+ function sessionFile(dir, agent, sessionId) {
11608
+ assertSessionKey(agent, sessionId);
11609
+ return path30.join(sessionsRoot2(dir), agent, `${sessionId}.jsonl`);
11610
+ }
11611
+ function assetsDir(dir, agent, sessionId) {
11612
+ assertSessionKey(agent, sessionId);
11613
+ return path30.join(sessionsRoot2(dir), agent, `${sessionId}.assets`);
11614
+ }
11615
+ function assetFile(dir, agent, sessionId, asset) {
11616
+ if (!VAULT_ASSET_NAME_PATTERN.test(asset)) throw new Error(`Invalid asset name: ${JSON.stringify(asset)}`);
11617
+ return path30.join(assetsDir(dir, agent, sessionId), asset);
11618
+ }
11619
+ function projectFolderName(cwd, platform) {
11620
+ const hash = createHash2("sha256").update(normalizeCwdKey(cwd, platform)).digest("hex").slice(0, 8);
11621
+ const base = (platform === "win32" ? path30.win32 : path30.posix).basename(cwd);
11622
+ const name = base.length === 0 ? "proyecto" : sanitizeFileName(base, 60);
11623
+ return `${name}-${hash}`;
11624
+ }
11625
+ function memoryDir(dir, cwd, platform) {
11626
+ return path30.join(dir, "memory", projectFolderName(cwd, platform));
11627
+ }
11628
+ function projectExportDir(dir, cwd, platform) {
11629
+ return path30.join(dir, "export", projectFolderName(cwd, platform));
11630
+ }
11631
+ function projectExportDirFor(dir, project, platform) {
11632
+ if (project.cwd.length > 0) return projectExportDir(dir, project.cwd, platform);
11633
+ const hash = createHash2("sha256").update(project.key).digest("hex").slice(0, 8);
11634
+ const name = project.fallbackName.length > 0 ? sanitizeFileName(project.fallbackName, 60) : "proyecto";
11635
+ return path30.join(dir, "export", `${name}-${hash}`);
11636
+ }
11637
+ function sessionExportFile(dir, agent, sessionId) {
11638
+ assertSessionKey(agent, sessionId);
11639
+ return path30.join(dir, "export", "sesiones", `${agent}-${sessionId}.md`);
11640
+ }
11641
+ var RESERVED_WINDOWS_NAME = /^(con|prn|aux|nul|com[1-9]|lpt[1-9])$/i;
11642
+ function sanitizeFileName(text, max = 80) {
11643
+ const limit = Math.max(1, Math.floor(max));
11644
+ const kept = text.normalize("NFC").replace(/[^\p{L}\p{M}\p{N} ._-]/gu, "_");
11645
+ const trimEnds = (value) => value.replace(/^ +/u, "").replace(/[. ]+$/u, "");
11646
+ let name = trimEnds(Array.from(trimEnds(kept)).slice(0, limit).join(""));
11647
+ if (name.length === 0) return "_";
11648
+ const stem = (name.split(".")[0] ?? "").replace(/ +$/u, "");
11649
+ if (RESERVED_WINDOWS_NAME.test(stem)) {
11650
+ name = `_${Array.from(name).slice(0, limit - 1).join("")}`;
11651
+ name = name.replace(/[. ]+$/u, "");
11652
+ }
11653
+ return name;
11654
+ }
11655
+
11656
+ // packages/server/src/vault/write.ts
11657
+ var RENAME_RETRIES = 3;
11658
+ var RENAME_RETRY_MS = 100;
11659
+ var DEFAULT_WRITE_DEPS = {
11660
+ rename: rename3,
11661
+ platform: process.platform,
11662
+ delay: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
11663
+ now: () => Date.now()
11664
+ };
11665
+ function temporaryPathFor(target) {
11666
+ return `${target}.${process.pid}.${randomBytes2(6).toString("hex")}.tmp`;
11667
+ }
11668
+ function isRetryableRename(error, platform) {
11669
+ if (platform !== "win32") return false;
11670
+ const code = error?.code;
11671
+ return code === "EPERM" || code === "EBUSY";
11672
+ }
11673
+ async function writeFileAtomic(target, data, deps = DEFAULT_WRITE_DEPS) {
11674
+ const temporary = temporaryPathFor(target);
11675
+ try {
11676
+ await writeFile3(temporary, data);
11677
+ for (let attempt = 0; ; attempt += 1) {
11678
+ try {
11679
+ await deps.rename(temporary, target);
11680
+ return;
11681
+ } catch (error) {
11682
+ if (attempt >= RENAME_RETRIES || !isRetryableRename(error, deps.platform)) throw error;
11683
+ await deps.delay(RENAME_RETRY_MS);
11684
+ }
11685
+ }
11686
+ } catch (error) {
11687
+ await rm2(temporary, { force: true }).catch(() => void 0);
11688
+ throw error;
11689
+ }
11690
+ }
11691
+ async function ensureVaultMarker(dir, deps = DEFAULT_WRITE_DEPS) {
11692
+ const marker = vaultMarkerFile(dir);
11693
+ try {
11694
+ await stat15(marker);
11695
+ return;
11696
+ } catch {
11697
+ }
11698
+ await mkdir5(dir, { recursive: true });
11699
+ await writeFileAtomic(marker, `${JSON.stringify({ format: VAULT_FORMAT, createdAt: deps.now() })}
11700
+ `, deps);
11701
+ }
11702
+ async function assetPresent(file, size) {
11703
+ try {
11704
+ const info = await stat15(file);
11705
+ return info.isFile() && info.size === size;
11706
+ } catch {
11707
+ return false;
11708
+ }
11709
+ }
11710
+ async function writeSessionFile(dir, serialized, deps = DEFAULT_WRITE_DEPS) {
11711
+ const { agent, sessionId } = serialized.header;
11712
+ const file = sessionFile(dir, agent, sessionId);
11713
+ await ensureVaultMarker(dir, deps);
11714
+ await mkdir5(agentSessionsDir(dir, agent), { recursive: true });
11715
+ let assetsWritten = 0;
11716
+ let assetBytesWritten = 0;
11717
+ if (serialized.assets.size > 0) {
11718
+ await mkdir5(assetsDir(dir, agent, sessionId), { recursive: true });
11719
+ for (const [name, bytes] of serialized.assets) {
11720
+ const target = assetFile(dir, agent, sessionId, name);
11721
+ if (await assetPresent(target, bytes.length)) continue;
11722
+ await writeFileAtomic(target, bytes, deps);
11723
+ assetsWritten += 1;
11724
+ assetBytesWritten += bytes.length;
11725
+ }
11726
+ }
11727
+ const text = Buffer.from(serialized.text, "utf8");
11728
+ await writeFileAtomic(file, text, deps);
11729
+ return { file, bytes: text.length, assetsWritten, assetBytesWritten };
11730
+ }
11731
+
11732
+ // packages/server/src/remote-devices-store.ts
11733
+ var RemoteDevicesStore = class {
11734
+ /** La ruta y el aviso son inyectables para las pruebas. */
11735
+ constructor(filePath = remoteDevicesPath(), log = console) {
11736
+ this.filePath = filePath;
11737
+ this.log = log;
11738
+ }
11739
+ filePath;
11740
+ log;
11741
+ /** Las escrituras, en fila: dos seguidas no se pisan. */
11742
+ queue = Promise.resolve();
11743
+ async load() {
11744
+ let raw;
11745
+ try {
11746
+ raw = await readFile9(this.filePath, "utf8");
11747
+ } catch {
11748
+ return [];
11749
+ }
11750
+ let parsed;
11751
+ try {
11752
+ parsed = JSON.parse(raw);
11753
+ } catch {
11754
+ this.log.warn("[remote] remote-devices.json isn't valid JSON: no paired devices are loaded");
11755
+ return [];
11756
+ }
11757
+ const devices = parseRemoteDevicesFile(parsed);
11758
+ if (devices === null) {
11759
+ this.log.warn("[remote] remote-devices.json is from another version: no paired devices are loaded");
11760
+ return [];
11761
+ }
11762
+ return devices;
11763
+ }
11764
+ save(devices) {
11765
+ const text = serializeRemoteDevices(devices);
11766
+ const run = async () => {
11767
+ await mkdir6(path31.dirname(this.filePath), { recursive: true });
11768
+ await writeFileAtomic(this.filePath, text);
11769
+ await chmod(this.filePath, 384).catch(() => void 0);
11770
+ };
11771
+ const result = this.queue.then(run);
11772
+ this.queue = result.catch(() => void 0);
11773
+ return result;
11774
+ }
11775
+ /** Espera lo que quede por escribir. Nunca rechaza. */
11776
+ flush() {
11777
+ return this.queue;
11778
+ }
11779
+ /**
11780
+ * Una marca que cambia cuando el archivo cambia (fecha y tamaño), o '' si no
11781
+ * está. Es lo que mira otra instancia de la app para enterarse de que acá se
11782
+ * revocó un equipo: comparten la carpeta de configuración.
11783
+ */
11784
+ async stamp() {
11785
+ try {
11786
+ const info = await stat16(this.filePath);
11787
+ return `${info.mtimeMs}:${info.size}`;
11788
+ } catch {
11789
+ return "";
11790
+ }
11791
+ }
11792
+ };
11793
+
11794
+ // packages/server/src/security.ts
11795
+ import { randomBytes as randomBytes3, timingSafeEqual as timingSafeEqual2 } from "node:crypto";
11796
+ var LOOPBACK_HOST = "127.0.0.1";
11797
+ var ALLOWED_HOSTNAMES = /* @__PURE__ */ new Set(["127.0.0.1", "localhost", "[::1]", "::1"]);
11798
+ var TOKEN_HEADER = "x-agent-workbench-token";
11799
+ var TOKEN_COOKIE = "agent_workbench_token";
11800
+ function createSessionToken() {
11801
+ return randomBytes3(32).toString("base64url");
11802
+ }
11803
+ function constantTimeEquals(a, b) {
11804
+ const bufferA = Buffer.from(a, "utf8");
11805
+ const bufferB = Buffer.from(b, "utf8");
11806
+ if (bufferA.length !== bufferB.length) return false;
11807
+ return timingSafeEqual2(bufferA, bufferB);
11808
+ }
11809
+ function requestUrl(request) {
11810
+ try {
11811
+ return new URL(request.url ?? "/", `http://${request.headers.host ?? LOOPBACK_HOST}`);
11812
+ } catch {
11813
+ return null;
11814
+ }
11815
+ }
11816
+ function readCookie(request, name) {
11817
+ const header = request.headers.cookie;
11818
+ if (typeof header !== "string") return null;
11819
+ for (const pair of header.split(";")) {
11820
+ const separator = pair.indexOf("=");
11821
+ if (separator === -1) continue;
11822
+ if (pair.slice(0, separator).trim() !== name) continue;
11823
+ const value = pair.slice(separator + 1).trim();
11824
+ return value.length > 0 ? decodeURIComponent(value) : null;
11825
+ }
11826
+ return null;
11827
+ }
11828
+ function matchToken(request, expected) {
11829
+ const url = requestUrl(request);
11830
+ const fromQuery = url?.searchParams.get(TOKEN_QUERY_PARAM) ?? null;
11831
+ if (fromQuery !== null && constantTimeEquals(fromQuery, expected)) {
11832
+ return { source: "query" };
11833
+ }
11834
+ const fromHeader = request.headers[TOKEN_HEADER];
11835
+ if (typeof fromHeader === "string" && constantTimeEquals(fromHeader, expected)) {
11836
+ return { source: "header" };
11837
+ }
11838
+ const fromCookie = readCookie(request, TOKEN_COOKIE);
11839
+ if (fromCookie !== null && constantTimeEquals(fromCookie, expected)) {
11840
+ return { source: "cookie" };
11841
+ }
11842
+ return null;
11843
+ }
11844
+ function buildTokenCookie(token) {
11845
+ return `${TOKEN_COOKIE}=${encodeURIComponent(token)}; Path=/; SameSite=Strict; HttpOnly`;
11846
+ }
11847
+ var DEVICE_COOKIE = "agent_workbench_device";
11848
+ var DEVICE_COOKIE_MAX_AGE_S = 400 * 24 * 60 * 60;
11849
+ function buildDeviceCookie(credential) {
11850
+ return `${DEVICE_COOKIE}=${encodeURIComponent(credential)}; Path=/; Max-Age=${DEVICE_COOKIE_MAX_AGE_S}; SameSite=Strict; HttpOnly`;
11851
+ }
11852
+ function matchCredential(request, token, devices) {
11853
+ const session = matchToken(request, token);
11854
+ if (session !== null) return { kind: "session", source: session.source };
11855
+ if (devices === null) return null;
11856
+ const credential = readCookie(request, DEVICE_COOKIE);
11857
+ if (credential === null) return null;
11858
+ const device = devices.match(credential);
11859
+ return device === null ? null : { kind: "device", deviceId: device.id, credential };
11860
+ }
11861
+ function readPairingCode(request) {
11862
+ const code = requestUrl(request)?.searchParams.get(PAIR_QUERY_PARAM) ?? null;
11863
+ return code !== null && code.length > 0 ? code : null;
11864
+ }
11865
+ function hasValidHost(request, port) {
11866
+ const host = request.headers.host;
11867
+ if (typeof host !== "string") return false;
11868
+ const separator = host.lastIndexOf(":");
11869
+ const hostname = separator === -1 ? host : host.slice(0, separator);
11870
+ const hostPort = separator === -1 ? "" : host.slice(separator + 1);
11871
+ if (!ALLOWED_HOSTNAMES.has(hostname)) return false;
11872
+ return hostPort === "" || hostPort === String(port);
11873
+ }
11874
+ function hasValidOrigin(request, port) {
11875
+ const origin = request.headers.origin;
11876
+ if (typeof origin !== "string" || origin.length === 0) return true;
11877
+ let parsed;
11878
+ try {
11879
+ parsed = new URL(origin);
11880
+ } catch {
11881
+ return false;
11882
+ }
11883
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return false;
11884
+ if (!ALLOWED_HOSTNAMES.has(parsed.hostname)) return false;
11885
+ return parsed.port === String(port);
11886
+ }
11887
+ function authorizeRequest(request, port, token, devices) {
11888
+ if (!hasValidHost(request, port)) return { ok: false, reason: "bad-host" };
11889
+ if (!hasValidOrigin(request, port)) return { ok: false, reason: "bad-origin" };
11890
+ const credential = matchCredential(request, token, devices);
11891
+ return credential === null ? { ok: false, reason: "bad-token" } : { ok: true, credential };
11892
+ }
11893
+
11894
+ // packages/server/src/repo-hub.ts
11895
+ import { EventEmitter as EventEmitter2 } from "node:events";
11896
+ import chokidar from "chokidar";
11897
+
11898
+ // packages/server/src/git-repo.ts
11899
+ import { execFile as execFile4 } from "node:child_process";
11900
+ import { readFile as readFile10 } from "node:fs/promises";
11901
+ import path32 from "node:path";
11902
+ function gitOutputText(output, command) {
11903
+ return output.length > 0 ? serverText("raw", { text: output }) : serverText("gitCommandFailed", { command });
11904
+ }
11905
+ var MAX_CHANGES = 2e3;
11906
+ var MAX_DIFF_LINES = 4e3;
11907
+ var MAX_DIFF_BYTES = 512 * 1024;
11908
+ var GIT_TIMEOUT_MS = 15e3;
11909
+ var GIT_MAX_BUFFER = 32 * 1024 * 1024;
11910
+ function isMissingBinary(error) {
11911
+ return typeof error === "object" && error !== null && error.code === "ENOENT";
11912
+ }
11913
+ var GitMissingError = class extends Error {
11914
+ constructor() {
11915
+ super("git wasn't found in the PATH.");
11916
+ this.name = "GitMissingError";
11917
+ }
11918
+ };
11919
+ function runGit(cwd, args) {
11920
+ return new Promise((resolve, reject) => {
11921
+ execFile4(
11922
+ "git",
11923
+ [
11924
+ // Sin esto git escapa los no-ASCII en octal y un archivo con acento
11925
+ // vuelve ilegible.
11926
+ "-c",
11927
+ "core.quotepath=false",
11928
+ // Esta app corre `git status` sola, cada pocos segundos, mientras el
11929
+ // usuario trabaja. Sin esta bandera `status` refresca y **escribe** el
11930
+ // indice, y eso trae dos problemas: toma `index.lock`, con lo que puede
11931
+ // hacer fallar un `git add` que el usuario esta escribiendo en la
11932
+ // terminal, y modifica `.git/index`, que es justo lo que observa
11933
+ // nuestro watcher — o sea que cada lectura se dispara a si misma.
11934
+ "--no-optional-locks",
11935
+ ...args
11936
+ ],
11937
+ {
11938
+ cwd,
11939
+ timeout: GIT_TIMEOUT_MS,
11940
+ maxBuffer: GIT_MAX_BUFFER,
11941
+ windowsHide: true,
11942
+ encoding: "utf8"
11943
+ },
11944
+ (error, stdout, stderr) => {
11945
+ if (error !== null && isMissingBinary(error)) {
11946
+ reject(new GitMissingError());
11947
+ return;
11948
+ }
11949
+ const code = error !== null && typeof error.code === "number" ? error.code : error !== null ? 1 : 0;
11950
+ resolve({ stdout, stderr, code });
11951
+ }
11952
+ );
11953
+ });
11954
+ }
11955
+ function toPosix(value) {
11956
+ return value.split("\\").join("/");
11957
+ }
11958
+ async function resolveRepo(cwd) {
11959
+ const result = await runGit(cwd, ["rev-parse", "--show-toplevel"]);
11960
+ if (result.code === 0) {
11961
+ const root = result.stdout.trim();
11962
+ if (root.length > 0) return { state: "ready", root: path32.resolve(root), message: null };
11963
+ }
11964
+ const stderr = result.stderr.trim();
10824
11965
  if (/not a git repository|no es un repositorio/i.test(stderr)) {
10825
11966
  return { state: "not-a-repo", root: "", message: null };
10826
11967
  }
@@ -10845,7 +11986,7 @@ async function findGitDir(cwd) {
10845
11986
  const result = await runGit(cwd, ["rev-parse", "--absolute-git-dir"]);
10846
11987
  if (result.code !== 0) return null;
10847
11988
  const dir = result.stdout.trim();
10848
- return dir.length > 0 ? path30.resolve(dir) : null;
11989
+ return dir.length > 0 ? path32.resolve(dir) : null;
10849
11990
  }
10850
11991
  function letterToKind(letter) {
10851
11992
  switch (letter) {
@@ -10948,7 +12089,7 @@ function parseWorktrees(raw, currentRoot) {
10948
12089
  worktrees.push({
10949
12090
  path: current.path,
10950
12091
  branch: current.branch,
10951
- isCurrent: path30.resolve(current.path) === path30.resolve(currentRoot)
12092
+ isCurrent: path32.resolve(current.path) === path32.resolve(currentRoot)
10952
12093
  });
10953
12094
  current = null;
10954
12095
  };
@@ -11091,7 +12232,7 @@ function looksBinary(buffer) {
11091
12232
  async function readUntrackedAsDiff(absolutePath, relativePath) {
11092
12233
  let content;
11093
12234
  try {
11094
- content = await readFile9(absolutePath);
12235
+ content = await readFile10(absolutePath);
11095
12236
  } catch {
11096
12237
  return {
11097
12238
  path: relativePath,
@@ -11139,7 +12280,7 @@ async function readUntrackedAsDiff(absolutePath, relativePath) {
11139
12280
  async function readDiff(options) {
11140
12281
  const { repoRoot: repoRoot2, relativePath, staged, untracked } = options;
11141
12282
  if (untracked && !staged) {
11142
- return readUntrackedAsDiff(path30.join(repoRoot2, relativePath), relativePath);
12283
+ return readUntrackedAsDiff(path32.join(repoRoot2, relativePath), relativePath);
11143
12284
  }
11144
12285
  const args = [
11145
12286
  "diff",
@@ -11178,7 +12319,7 @@ async function filterIgnored(repoRoot2, relativePaths) {
11178
12319
  const ignored = /* @__PURE__ */ new Set();
11179
12320
  if (relativePaths.length === 0) return ignored;
11180
12321
  const result = await new Promise((resolve) => {
11181
- const child = execFile3(
12322
+ const child = execFile4(
11182
12323
  "git",
11183
12324
  ["-c", "core.quotepath=false", "check-ignore", "--stdin", "-z"],
11184
12325
  { cwd: repoRoot2, timeout: GIT_TIMEOUT_MS, maxBuffer: GIT_MAX_BUFFER, windowsHide: true },
@@ -11359,24 +12500,24 @@ var RepoHub = class extends EventEmitter2 {
11359
12500
  // packages/server/src/memory-hub.ts
11360
12501
  import { EventEmitter as EventEmitter3 } from "node:events";
11361
12502
  import { homedir as homedir10 } from "node:os";
11362
- import path32 from "node:path";
12503
+ import path34 from "node:path";
11363
12504
  import chokidar2 from "chokidar";
11364
12505
 
11365
12506
  // packages/server/src/memory-bridge.ts
11366
- import { execFile as execFile4 } from "node:child_process";
11367
- import { randomBytes as randomBytes3 } from "node:crypto";
12507
+ import { execFile as execFile5 } from "node:child_process";
12508
+ import { randomBytes as randomBytes4 } from "node:crypto";
11368
12509
  import {
11369
- mkdir as mkdir5,
12510
+ mkdir as mkdir7,
11370
12511
  open as open6,
11371
12512
  readdir as readdir9,
11372
- readFile as readFile10,
12513
+ readFile as readFile11,
11373
12514
  realpath as realpath2,
11374
- rename as rename3,
11375
- stat as stat15,
12515
+ rename as rename4,
12516
+ stat as stat17,
11376
12517
  unlink as unlink3
11377
12518
  } from "node:fs/promises";
11378
12519
  import { homedir as homedir9 } from "node:os";
11379
- import path31 from "node:path";
12520
+ import path33 from "node:path";
11380
12521
  var MemoryBridgeError = class extends ServerTextError {
11381
12522
  };
11382
12523
  var MAX_LISTED_NOTES = 500;
@@ -11394,9 +12535,9 @@ var GITIGNORE_MEMORY_LINE = `${MEMORY_DIR}/`;
11394
12535
  var BOM2 = Buffer.from([239, 187, 191]);
11395
12536
  async function inspectMemory(cwd, home = homedir9()) {
11396
12537
  await assertMemoryDirInside(cwd);
11397
- const folder = path31.join(cwd, MEMORY_DIR);
12538
+ const folder = path33.join(cwd, MEMORY_DIR);
11398
12539
  const folderExists = await isDirectory2(folder);
11399
- const indexExists = folderExists && await isFile(path31.join(folder, MEMORY_INDEX_FILE));
12540
+ const indexExists = folderExists && await isFile(path33.join(folder, MEMORY_INDEX_FILE));
11400
12541
  const noteNames = folderExists ? await listNoteFiles(folder) : [];
11401
12542
  const notes = await describeNotes(folder, noteNames);
11402
12543
  const files = await Promise.all(
@@ -11428,7 +12569,7 @@ async function installMemory(cwd, options, home = homedir9()) {
11428
12569
  }
11429
12570
  async function importNativeMemory(cwd, home = homedir9()) {
11430
12571
  await assertMemoryDirInside(cwd);
11431
- if (!await isDirectory2(path31.join(cwd, MEMORY_DIR))) {
12572
+ if (!await isDirectory2(path33.join(cwd, MEMORY_DIR))) {
11432
12573
  throw new MemoryBridgeError(serverText("memoryNotInstalled"));
11433
12574
  }
11434
12575
  const plan = new PlanBuilder(cwd);
@@ -11446,9 +12587,9 @@ async function readMemoryNote(cwd, name) {
11446
12587
  }
11447
12588
  const absolute = await resolveInside(cwd, `${MEMORY_DIR}/${name}`);
11448
12589
  try {
11449
- const info = await stat15(absolute);
12590
+ const info = await stat17(absolute);
11450
12591
  if (!info.isFile()) return null;
11451
- const content = await readFile10(absolute);
12592
+ const content = await readFile11(absolute);
11452
12593
  return {
11453
12594
  name,
11454
12595
  text: content.subarray(0, MAX_NOTE_BYTES).toString("utf8"),
@@ -11463,7 +12604,7 @@ function isNoteName(name) {
11463
12604
  if (name.length === 0 || name.length > 255) return false;
11464
12605
  if (name.includes("\0")) return false;
11465
12606
  if (name === "." || name === "..") return false;
11466
- if (path31.basename(name) !== name) return false;
12607
+ if (path33.basename(name) !== name) return false;
11467
12608
  if (/[\\/:*?"<>|\u0000-\u001f]/.test(name)) return false;
11468
12609
  return name.toLowerCase().endsWith(".md");
11469
12610
  }
@@ -11500,7 +12641,7 @@ var PlanBuilder = class {
11500
12641
  /** Nombres de nota que habria en la carpeta, en minusculas. */
11501
12642
  async noteNames() {
11502
12643
  const names = /* @__PURE__ */ new Map();
11503
- for (const name of await listNoteFiles(path31.join(this.cwd, MEMORY_DIR))) {
12644
+ for (const name of await listNoteFiles(path33.join(this.cwd, MEMORY_DIR))) {
11504
12645
  names.set(name.toLowerCase(), name);
11505
12646
  }
11506
12647
  for (const rel of this.files.keys()) {
@@ -11543,7 +12684,7 @@ var PlanBuilder = class {
11543
12684
  continue;
11544
12685
  }
11545
12686
  const target = await realpath2(absolute);
11546
- const info = await stat15(target);
12687
+ const info = await stat17(target);
11547
12688
  if (info.nlink > 1) {
11548
12689
  throw new MemoryBridgeError(serverText("memoryHardLinks", { file: file.rel }));
11549
12690
  }
@@ -11562,7 +12703,7 @@ var PlanBuilder = class {
11562
12703
  );
11563
12704
  if (pending.length === 0) return;
11564
12705
  if (pending.some((file) => file.rel.startsWith(`${MEMORY_DIR}/`))) {
11565
- await mkdir5(path31.join(this.cwd, MEMORY_DIR), { recursive: true });
12706
+ await mkdir7(path33.join(this.cwd, MEMORY_DIR), { recursive: true });
11566
12707
  }
11567
12708
  for (const file of pending) {
11568
12709
  if (file.target === null) throw new Error(`Unvalidated plan: ${file.rel}`);
@@ -11673,7 +12814,7 @@ async function planNativeImport(plan, cwd, home) {
11673
12814
  const local = await plan.noteNames();
11674
12815
  for (const name of await listNoteFiles(folder)) {
11675
12816
  const key = name.toLowerCase();
11676
- const content = await readIfFile(path31.join(folder, name), name);
12817
+ const content = await readIfFile(path33.join(folder, name), name);
11677
12818
  if (content === null) continue;
11678
12819
  const already = chosen.get(key);
11679
12820
  if (already !== void 0) {
@@ -11687,7 +12828,7 @@ async function planNativeImport(plan, cwd, home) {
11687
12828
  if (existing !== null && !existing.equals(content)) skipped.push(name);
11688
12829
  continue;
11689
12830
  }
11690
- plan.put(`${MEMORY_DIR}/${name}`, null, content, "copy", "", path31.join(folder, name));
12831
+ plan.put(`${MEMORY_DIR}/${name}`, null, content, "copy", "", path33.join(folder, name));
11691
12832
  copied.push(name);
11692
12833
  }
11693
12834
  }
@@ -11697,7 +12838,7 @@ async function planNativeImport(plan, cwd, home) {
11697
12838
  const known = new Set(linkTargets(index.toString("utf8")));
11698
12839
  const additions = [];
11699
12840
  for (const folder of folders) {
11700
- const nativeIndex = await readIfFile(path31.join(folder, MEMORY_INDEX_FILE), MEMORY_INDEX_FILE);
12841
+ const nativeIndex = await readIfFile(path33.join(folder, MEMORY_INDEX_FILE), MEMORY_INDEX_FILE);
11701
12842
  if (nativeIndex === null) continue;
11702
12843
  for (const line of nativeIndex.toString("utf8").replace(/^\uFEFF/, "").split(/\r?\n/)) {
11703
12844
  const target = linkTargets(line)[0];
@@ -11922,7 +13063,7 @@ async function describeNotes(folder, names) {
11922
13063
  const stated = [];
11923
13064
  for (const name of names) {
11924
13065
  try {
11925
- const info = await stat15(path31.join(folder, name));
13066
+ const info = await stat17(path33.join(folder, name));
11926
13067
  if (info.isFile()) stated.push({ name, sizeBytes: info.size, modifiedAt: info.mtimeMs });
11927
13068
  } catch {
11928
13069
  }
@@ -11930,7 +13071,7 @@ async function describeNotes(folder, names) {
11930
13071
  stated.sort((a, b) => b.modifiedAt - a.modifiedAt || a.name.localeCompare(b.name));
11931
13072
  const notes = [];
11932
13073
  for (const item of stated.slice(0, MAX_LISTED_NOTES)) {
11933
- const head = await readHead(path31.join(folder, item.name));
13074
+ const head = await readHead(path33.join(folder, item.name));
11934
13075
  notes.push({ ...item, ...describeNoteText(head, item.name) });
11935
13076
  }
11936
13077
  return notes;
@@ -11988,28 +13129,28 @@ async function countNativeMemory(cwd, home, sharedNames) {
11988
13129
  return { available: names.size, pending };
11989
13130
  }
11990
13131
  async function nativeMemoryFolders(cwd, home) {
11991
- const root = path31.join(home, ".claude", "projects");
13132
+ const root = path33.join(home, ".claude", "projects");
11992
13133
  const slug = projectSlugFor(cwd).toLowerCase();
11993
13134
  try {
11994
13135
  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"));
13136
+ return entries.filter((entry) => entry.isDirectory() && entry.name.toLowerCase() === slug).map((entry) => entry.name).sort().map((name) => path33.join(root, name, "memory"));
11996
13137
  } catch {
11997
13138
  return [];
11998
13139
  }
11999
13140
  }
12000
13141
  function buildGlobalFragments(home) {
12001
13142
  const windows = process.platform === "win32";
12002
- const globalFile = path31.join(home, ...GLOBAL_MEMORY_RELATIVE.split("/"));
13143
+ const globalFile = path33.join(home, ...GLOBAL_MEMORY_RELATIVE.split("/"));
12003
13144
  const hardLink = (link) => windows ? `mklink /H "${link}" "${globalFile}"` : `ln "${globalFile}" "${link}"`;
12004
13145
  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");
13146
+ const codexTarget = path33.join(home, ".codex", "AGENTS.md");
13147
+ const geminiTarget = path33.join(home, ".gemini", "GEMINI.md");
12007
13148
  const homeSlashes = home.replace(/\\/g, "/").replace(/\/+$/, "");
12008
13149
  return [
12009
13150
  {
12010
13151
  agent: "claude-code",
12011
13152
  label: MEMORY_AGENT_LABELS["claude-code"],
12012
- target: path31.join(home, ".claude", "CLAUDE.md"),
13153
+ target: path33.join(home, ".claude", "CLAUDE.md"),
12013
13154
  kind: "line",
12014
13155
  text: `@~/${GLOBAL_MEMORY_RELATIVE}`,
12015
13156
  note: serverText("memoryNoteClaude")
@@ -12033,7 +13174,7 @@ function buildGlobalFragments(home) {
12033
13174
  {
12034
13175
  agent: "opencode",
12035
13176
  label: MEMORY_AGENT_LABELS.opencode,
12036
- target: path31.join(home, ".config", "opencode", "opencode.json"),
13177
+ target: path33.join(home, ".config", "opencode", "opencode.json"),
12037
13178
  kind: "json",
12038
13179
  text: `"instructions": ["${homeSlashes}/${GLOBAL_MEMORY_RELATIVE}"]`,
12039
13180
  note: serverText("memoryNoteOpenCode")
@@ -12042,7 +13183,7 @@ function buildGlobalFragments(home) {
12042
13183
  }
12043
13184
  function runGit2(cwd, args) {
12044
13185
  return new Promise((resolve) => {
12045
- execFile4(
13186
+ execFile5(
12046
13187
  "git",
12047
13188
  ["-c", "core.quotepath=false", "--no-optional-locks", ...args],
12048
13189
  { cwd, timeout: GIT_TIMEOUT_MS2, windowsHide: true, encoding: "utf8", maxBuffer: 1024 * 1024 },
@@ -12081,17 +13222,17 @@ async function readGit(cwd) {
12081
13222
  if (!toplevel || !gitDir || !commonDir) return fail(serverText("gitUnexpected", { command: "rev-parse" }));
12082
13223
  const ignored = await checkIgnored(cwd, INDEX_REL);
12083
13224
  if (typeof ignored !== "boolean") return fail(ignored);
12084
- const absoluteGitDir = path31.resolve(cwd, gitDir);
12085
- const absoluteCommonDir = path31.resolve(cwd, commonDir);
13225
+ const absoluteGitDir = path33.resolve(cwd, gitDir);
13226
+ const absoluteCommonDir = path33.resolve(cwd, commonDir);
12086
13227
  let worktree = null;
12087
13228
  let main2 = null;
12088
13229
  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("/");
13230
+ if (path33.basename(absoluteCommonDir).toLowerCase() === ".git") {
13231
+ const mainRoot = path33.dirname(absoluteCommonDir);
13232
+ const inner = path33.relative(path33.resolve(toplevel), path33.resolve(cwd));
13233
+ const sameFolder = inner.length === 0 || inner.startsWith("..") || path33.isAbsolute(inner);
13234
+ const mainPath = sameFolder ? mainRoot : path33.join(mainRoot, inner);
13235
+ const memoryRel = sameFolder ? MEMORY_DIR : [...inner.split(path33.sep), MEMORY_DIR].join("/");
12095
13236
  main2 = { root: mainRoot, memoryRel };
12096
13237
  worktree = {
12097
13238
  mainPath,
@@ -12116,7 +13257,7 @@ async function samePath(a, b) {
12116
13257
  try {
12117
13258
  return await realpath2(value);
12118
13259
  } catch {
12119
- return path31.resolve(value);
13260
+ return path33.resolve(value);
12120
13261
  }
12121
13262
  };
12122
13263
  const [left, right] = await Promise.all([resolve(a), resolve(b)]);
@@ -12158,9 +13299,9 @@ async function listNoteFiles(folder) {
12158
13299
  }
12159
13300
  async function readIfFile(absolute, label) {
12160
13301
  try {
12161
- const info = await stat15(absolute);
13302
+ const info = await stat17(absolute);
12162
13303
  if (!info.isFile()) throw new MemoryBridgeError(serverText("memoryNotAFile", { label }));
12163
- return await readFile10(absolute);
13304
+ return await readFile11(absolute);
12164
13305
  } catch (error) {
12165
13306
  if (isNotFound(error)) return null;
12166
13307
  throw error;
@@ -12182,14 +13323,14 @@ async function readHead(absolute) {
12182
13323
  }
12183
13324
  async function isFile(absolute) {
12184
13325
  try {
12185
- return (await stat15(absolute)).isFile();
13326
+ return (await stat17(absolute)).isFile();
12186
13327
  } catch {
12187
13328
  return false;
12188
13329
  }
12189
13330
  }
12190
13331
  async function isDirectory2(absolute) {
12191
13332
  try {
12192
- return (await stat15(absolute)).isDirectory();
13333
+ return (await stat17(absolute)).isDirectory();
12193
13334
  } catch {
12194
13335
  return false;
12195
13336
  }
@@ -12207,7 +13348,7 @@ async function writeAtomic(target, content, mustNotExist) {
12207
13348
  await handle.close();
12208
13349
  }
12209
13350
  if (mustNotExist && await exists2(target)) {
12210
- throw new MemoryBridgeError(serverText("memoryAppeared", { file: path31.basename(target) }));
13351
+ throw new MemoryBridgeError(serverText("memoryAppeared", { file: path33.basename(target) }));
12211
13352
  }
12212
13353
  await renameWithRetry(temporary, target);
12213
13354
  } catch (error) {
@@ -12217,10 +13358,10 @@ async function writeAtomic(target, content, mustNotExist) {
12217
13358
  }
12218
13359
  async function createTemporary(target) {
12219
13360
  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`
13361
+ const suffix = randomBytes4(6).toString("hex");
13362
+ const temporary = path33.join(
13363
+ path33.dirname(target),
13364
+ `.${path33.basename(target)}.${process.pid}.${suffix}.tmp`
12224
13365
  );
12225
13366
  try {
12226
13367
  return { temporary, handle: await open6(temporary, "wx") };
@@ -12232,7 +13373,7 @@ async function createTemporary(target) {
12232
13373
  }
12233
13374
  async function exists2(absolute) {
12234
13375
  try {
12235
- await stat15(absolute);
13376
+ await stat17(absolute);
12236
13377
  return true;
12237
13378
  } catch {
12238
13379
  return false;
@@ -12241,7 +13382,7 @@ async function exists2(absolute) {
12241
13382
  async function renameWithRetry(from, to) {
12242
13383
  for (let attempt = 1; ; attempt += 1) {
12243
13384
  try {
12244
- await rename3(from, to);
13385
+ await rename4(from, to);
12245
13386
  return;
12246
13387
  } catch (error) {
12247
13388
  const code = error.code;
@@ -12458,13 +13599,13 @@ var MemoryHub = class extends EventEmitter3 {
12458
13599
  }
12459
13600
  };
12460
13601
  function keyFor(cwd) {
12461
- const resolved = path32.resolve(cwd);
13602
+ const resolved = path34.resolve(cwd);
12462
13603
  return process.platform === "win32" ? resolved.toLowerCase() : resolved;
12463
13604
  }
12464
13605
  function isRelevantPath(cwd, candidate) {
12465
- const relative = path32.relative(cwd, candidate);
13606
+ const relative = path34.relative(cwd, candidate);
12466
13607
  if (relative.length === 0) return true;
12467
- if (relative.startsWith("..") || path32.isAbsolute(relative)) return false;
13608
+ if (relative.startsWith("..") || path34.isAbsolute(relative)) return false;
12468
13609
  const same = (a, b) => process.platform === "win32" ? a.toLowerCase() === b.toLowerCase() : a === b;
12469
13610
  const parts = relative.split(/[\\/]/);
12470
13611
  const [first = "", second = "", third = ""] = parts;
@@ -12481,10 +13622,10 @@ function isRelevantPath(cwd, candidate) {
12481
13622
  }
12482
13623
 
12483
13624
  // 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";
13625
+ import { createHash as createHash3, randomUUID as randomUUID3 } from "node:crypto";
13626
+ import { mkdir as mkdir8, readdir as readdir10, rm as rm3, stat as stat18, writeFile as writeFile4 } from "node:fs/promises";
12486
13627
  import { tmpdir as tmpdir4 } from "node:os";
12487
- import path33 from "node:path";
13628
+ import path35 from "node:path";
12488
13629
 
12489
13630
  // packages/server/src/vault/markdown.ts
12490
13631
  var pad2 = (value) => String(value).padStart(2, "0");
@@ -12999,8 +14140,8 @@ function buildContinuationMessage(input) {
12999
14140
  }
13000
14141
  return sanitizeForPaste(lines.join("\n"));
13001
14142
  }
13002
- function transcriptReferenceFor(path48, style) {
13003
- return style === "at-quoted" ? fileReference(path48, "at-quoted") : `"${path48}"`;
14143
+ function transcriptReferenceFor(path50, style) {
14144
+ return style === "at-quoted" ? fileReference(path50, "at-quoted") : `"${path50}"`;
13004
14145
  }
13005
14146
  function planTranscript(input) {
13006
14147
  const turns = splitTurns(input.events);
@@ -13175,7 +14316,7 @@ var PasteStore = class {
13175
14316
  root;
13176
14317
  counter = 0;
13177
14318
  constructor(root) {
13178
- this.root = root ?? path33.join(tmpdir4(), "agent-workbench", "pasted");
14319
+ this.root = root ?? path35.join(tmpdir4(), "agent-workbench", "pasted");
13179
14320
  }
13180
14321
  /**
13181
14322
  * Guarda una imagen y devuelve su ruta.
@@ -13202,12 +14343,12 @@ var PasteStore = class {
13202
14343
  if (signature === null) {
13203
14344
  throw new PasteImageError(serverText("imageUnknownFormat"));
13204
14345
  }
13205
- const directory = path33.join(this.root, safeSegment(terminalId));
13206
- await mkdir6(directory, { recursive: true, mode: DIRECTORY_MODE });
14346
+ const directory = path35.join(this.root, safeSegment(terminalId));
14347
+ await mkdir8(directory, { recursive: true, mode: DIRECTORY_MODE });
13207
14348
  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 });
14349
+ const name = `pegada-${this.counter}-${randomUUID3().slice(0, 8)}.${signature.ext}`;
14350
+ const file = path35.join(directory, name);
14351
+ await writeFile4(file, bytes, { mode: FILE_MODE });
13211
14352
  return { path: file, bytes: bytes.length };
13212
14353
  }
13213
14354
  /**
@@ -13224,12 +14365,12 @@ var PasteStore = class {
13224
14365
  if (bytes.length > MAX_TEXT_BYTES) {
13225
14366
  throw new PasteTextError(`The text is ${bytes.length} bytes; the maximum is ${MAX_TEXT_BYTES}.`);
13226
14367
  }
13227
- const directory = path33.join(this.root, safeSegment(terminalId));
13228
- await mkdir6(directory, { recursive: true, mode: DIRECTORY_MODE });
14368
+ const directory = path35.join(this.root, safeSegment(terminalId));
14369
+ await mkdir8(directory, { recursive: true, mode: DIRECTORY_MODE });
13229
14370
  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 });
14371
+ const name = `${baseName}-${this.counter}-${randomUUID3().slice(0, 8)}.md`;
14372
+ const file = path35.join(directory, name);
14373
+ await writeFile4(file, bytes, { mode: FILE_MODE });
13233
14374
  return { path: file, bytes: bytes.length };
13234
14375
  }
13235
14376
  /**
@@ -13258,17 +14399,17 @@ var PasteStore = class {
13258
14399
  })
13259
14400
  );
13260
14401
  }
13261
- const directory = path33.join(this.root, safeSegment(terminalId));
13262
- await mkdir6(directory, { recursive: true, mode: DIRECTORY_MODE });
14402
+ const directory = path35.join(this.root, safeSegment(terminalId));
14403
+ await mkdir8(directory, { recursive: true, mode: DIRECTORY_MODE });
13263
14404
  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 });
14405
+ const name = `adjunto-${this.counter}-${randomUUID3().slice(0, 8)}-${safe}`;
14406
+ const file = path35.join(directory, name);
14407
+ await writeFile4(file, bytes, { mode: FILE_MODE });
13267
14408
  return { path: file, bytes: bytes.length, name };
13268
14409
  }
13269
14410
  /** Borra lo de una pestana. Se llama al cerrarla. */
13270
14411
  async clearTerminal(terminalId) {
13271
- await rm2(path33.join(this.root, safeSegment(terminalId)), {
14412
+ await rm3(path35.join(this.root, safeSegment(terminalId)), {
13272
14413
  recursive: true,
13273
14414
  force: true
13274
14415
  }).catch(() => {
@@ -13290,11 +14431,11 @@ var PasteStore = class {
13290
14431
  }
13291
14432
  await Promise.all(
13292
14433
  entries.map(async (entry) => {
13293
- const full = path33.join(this.root, entry);
14434
+ const full = path35.join(this.root, entry);
13294
14435
  try {
13295
- const info = await stat16(full);
14436
+ const info = await stat18(full);
13296
14437
  if (now - info.mtimeMs > STALE_AFTER_MS) {
13297
- await rm2(full, { recursive: true, force: true });
14438
+ await rm3(full, { recursive: true, force: true });
13298
14439
  }
13299
14440
  } catch {
13300
14441
  }
@@ -13304,13 +14445,13 @@ var PasteStore = class {
13304
14445
  };
13305
14446
  function safeSegment(value) {
13306
14447
  const clean = value.replace(/[^a-zA-Z0-9_-]/g, "");
13307
- return clean.length > 0 ? clean : createHash("sha1").update(value).digest("hex").slice(0, 16);
14448
+ return clean.length > 0 ? clean : createHash3("sha1").update(value).digest("hex").slice(0, 16);
13308
14449
  }
13309
14450
 
13310
14451
  // packages/server/src/session-index.ts
13311
14452
  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";
14453
+ import { mkdir as mkdir9, readFile as readFile12, stat as stat19, writeFile as writeFile5 } from "node:fs/promises";
14454
+ import path36 from "node:path";
13314
14455
  var CACHE_VERSION = 4;
13315
14456
  var vaultPairKey = (agent, sessionId) => `${agent}/${sessionId}`;
13316
14457
  function vaultOnlySessions(natives, vault, listState, ready) {
@@ -13359,7 +14500,7 @@ function cacheKey(agent, ref) {
13359
14500
  }
13360
14501
  async function pathExists3(candidate) {
13361
14502
  try {
13362
- await stat17(candidate);
14503
+ await stat19(candidate);
13363
14504
  return true;
13364
14505
  } catch {
13365
14506
  return false;
@@ -13409,7 +14550,7 @@ function migrateEntryV3(filePath, value) {
13409
14550
  return {
13410
14551
  agent: "claude-code",
13411
14552
  ref: filePath,
13412
- group: path34.basename(path34.dirname(filePath)),
14553
+ group: path36.basename(path36.dirname(filePath)),
13413
14554
  mtimeMs,
13414
14555
  sizeBytes,
13415
14556
  cwd,
@@ -13674,7 +14815,7 @@ var SessionIndex = class extends EventEmitter4 {
13674
14815
  }
13675
14816
  async loadCache() {
13676
14817
  try {
13677
- const raw = await readFile11(sessionIndexCachePath(), "utf8");
14818
+ const raw = await readFile12(sessionIndexCachePath(), "utf8");
13678
14819
  const parsed = JSON.parse(raw);
13679
14820
  const migrated = migrateIndexCache(parsed);
13680
14821
  if (migrated === null) return;
@@ -13687,8 +14828,8 @@ var SessionIndex = class extends EventEmitter4 {
13687
14828
  if (!this.cacheDirty) return;
13688
14829
  this.cacheDirty = false;
13689
14830
  try {
13690
- await mkdir7(appConfigDir(), { recursive: true });
13691
- await writeFile4(sessionIndexCachePath(), JSON.stringify(this.cache), "utf8");
14831
+ await mkdir9(appConfigDir(), { recursive: true });
14832
+ await writeFile5(sessionIndexCachePath(), JSON.stringify(this.cache), "utf8");
13692
14833
  } catch (error) {
13693
14834
  console.warn("[index] couldn't save the cache:", error);
13694
14835
  }
@@ -13867,197 +15008,40 @@ var SessionIndex = class extends EventEmitter4 {
13867
15008
  refs = await history.changedRefs(filePath);
13868
15009
  } catch {
13869
15010
  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) {
15011
+ }
15012
+ if (refs === null || refs.length === 0) return;
15013
+ let changed = false;
15014
+ for (const ref of refs) {
15015
+ const key = cacheKey(agent, ref);
15016
+ const cachedBefore = this.cache.entries[key];
15017
+ if (cachedBefore !== void 0) {
15018
+ delete this.cache.entries[key];
15019
+ this.cacheDirty = true;
15020
+ }
15021
+ let indexed;
14006
15022
  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);
15023
+ const item = await history.item(ref);
15024
+ indexed = item === null ? null : await this.readOrThrow(agent, history, item);
15025
+ } catch {
15026
+ if (cachedBefore !== void 0 && !(key in this.cache.entries)) this.cache.entries[key] = cachedBefore;
15027
+ continue;
14012
15028
  }
15029
+ if (indexed === null) {
15030
+ if (this.sessions.delete(key)) changed = true;
15031
+ continue;
15032
+ }
15033
+ if (indexed.cwd !== null) this.cwdExists.set(indexed.cwd, await pathExists3(indexed.cwd));
15034
+ this.sessions.set(key, indexed);
15035
+ changed = true;
14013
15036
  }
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
- }
15037
+ if (changed) this.emitProjects();
15038
+ await this.saveCache();
14054
15039
  }
14055
- const text = Buffer.from(serialized.text, "utf8");
14056
- await writeFileAtomic(file, text, deps);
14057
- return { file, bytes: text.length, assetsWritten, assetBytesWritten };
14058
- }
15040
+ };
14059
15041
 
14060
15042
  // packages/server/src/settings-store.ts
15043
+ import { mkdir as mkdir10, readFile as readFile13, stat as stat20 } from "node:fs/promises";
15044
+ import path37 from "node:path";
14061
15045
  var SETTINGS_VERSION = 1;
14062
15046
  var VAULT_TOOL_RESULT_DEFAULT_CHARS = 64e3;
14063
15047
  var VAULT_TOOL_RESULT_MIN_CHARS = 4e3;
@@ -14065,13 +15049,14 @@ var VAULT_TOOL_RESULT_MAX_CHARS = 1e6;
14065
15049
  function defaultAppSettings() {
14066
15050
  return {
14067
15051
  version: SETTINGS_VERSION,
14068
- vault: { enabled: false, dir: null, toolResultMaxChars: VAULT_TOOL_RESULT_DEFAULT_CHARS }
15052
+ vault: { enabled: false, dir: null, toolResultMaxChars: VAULT_TOOL_RESULT_DEFAULT_CHARS },
15053
+ remote: { enabled: false, port: REMOTE_DEFAULT_PORT }
14069
15054
  };
14070
15055
  }
14071
15056
  function isAbsoluteDir(dir, platform) {
14072
15057
  if (dir.length === 0 || dir.includes("\0")) return false;
14073
15058
  if (platform === "win32") return /^[A-Za-z]:[\\/]/.test(dir) || /^\\\\[^\\/]+[\\/][^\\/]+/.test(dir);
14074
- return path36.posix.isAbsolute(dir);
15059
+ return path37.posix.isAbsolute(dir);
14075
15060
  }
14076
15061
  function clampToolResultMaxChars(value) {
14077
15062
  if (typeof value !== "number" || !Number.isFinite(value)) return VAULT_TOOL_RESULT_DEFAULT_CHARS;
@@ -14081,6 +15066,11 @@ function parseAppSettings(value, platform, warn = () => void 0) {
14081
15066
  const record = asRecord(value);
14082
15067
  if (record === null || record["version"] !== SETTINGS_VERSION) return null;
14083
15068
  const settings2 = defaultAppSettings();
15069
+ const remote = asRecord(record["remote"]);
15070
+ if (remote !== null) {
15071
+ settings2.remote.enabled = asBoolean(remote["enabled"]) ?? false;
15072
+ if (isRemotePort(remote["port"])) settings2.remote.port = remote["port"];
15073
+ }
14084
15074
  const vault = asRecord(record["vault"]);
14085
15075
  if (vault === null) return settings2;
14086
15076
  settings2.vault.enabled = asBoolean(vault["enabled"]) ?? false;
@@ -14094,7 +15084,11 @@ function parseAppSettings(value, platform, warn = () => void 0) {
14094
15084
  return settings2;
14095
15085
  }
14096
15086
  function frozen(settings2) {
14097
- return Object.freeze({ ...settings2, vault: Object.freeze({ ...settings2.vault }) });
15087
+ return Object.freeze({
15088
+ ...settings2,
15089
+ vault: Object.freeze({ ...settings2.vault }),
15090
+ remote: Object.freeze({ ...settings2.remote })
15091
+ });
14098
15092
  }
14099
15093
  var SettingsStore = class {
14100
15094
  /** La ruta es inyectable para las pruebas; por defecto, la de la carpeta de configuracion. */
@@ -14113,7 +15107,7 @@ var SettingsStore = class {
14113
15107
  async load() {
14114
15108
  let raw;
14115
15109
  try {
14116
- raw = await readFile12(this.filePath, "utf8");
15110
+ raw = await readFile13(this.filePath, "utf8");
14117
15111
  } catch {
14118
15112
  this.current = frozen(defaultAppSettings());
14119
15113
  return;
@@ -14132,17 +15126,49 @@ var SettingsStore = class {
14132
15126
  }
14133
15127
  this.current = frozen(settings2 ?? defaultAppSettings());
14134
15128
  }
15129
+ /**
15130
+ * Relee **solo** lo del acceso remoto (hito 37). Otra instancia de la app
15131
+ * comparte este archivo, y "apagar" desde una tiene que valer en la que tiene
15132
+ * el puerto; lo de la copia propia no se toca, que cada instancia lo lleva en
15133
+ * memoria como siempre. Sin archivo o ilegible, queda lo que habia.
15134
+ */
15135
+ async reloadRemote() {
15136
+ let parsed;
15137
+ try {
15138
+ parsed = JSON.parse(await readFile13(this.filePath, "utf8"));
15139
+ } catch {
15140
+ return;
15141
+ }
15142
+ const settings2 = parseAppSettings(parsed, this.platform);
15143
+ if (settings2 === null) return;
15144
+ this.current = frozen({ ...this.current, remote: settings2.remote });
15145
+ }
15146
+ /** Una marca que cambia cuando el archivo cambia (fecha y tamano), o '' si no esta. */
15147
+ async stamp() {
15148
+ try {
15149
+ const info = await stat20(this.filePath);
15150
+ return `${info.mtimeMs}:${info.size}`;
15151
+ } catch {
15152
+ return "";
15153
+ }
15154
+ }
14135
15155
  /** Lo ultimo que quedo escrito. Congelado: nadie de afuera lo cambia sin pasar por `update`. */
14136
15156
  get() {
14137
15157
  return this.current;
14138
15158
  }
14139
15159
  /**
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.
15160
+ * Aplica un cambio y lo escribe. Lanza si la carpeta no es absoluta, si el
15161
+ * puerto no sirve o si no se pudo escribir; en todos los casos `get()` sigue
15162
+ * dando lo anterior.
14142
15163
  */
14143
15164
  update(patch) {
14144
15165
  const run = async () => {
14145
- const next = { version: SETTINGS_VERSION, vault: { ...this.current.vault } };
15166
+ if (patch.remote === void 0) await this.reloadRemote();
15167
+ const next = {
15168
+ version: SETTINGS_VERSION,
15169
+ vault: { ...this.current.vault },
15170
+ remote: { ...this.current.remote }
15171
+ };
14146
15172
  const vault = patch.vault ?? {};
14147
15173
  if (vault.enabled !== void 0) next.vault.enabled = vault.enabled;
14148
15174
  if (vault.dir !== void 0) {
@@ -14154,7 +15180,13 @@ var SettingsStore = class {
14154
15180
  if (vault.toolResultMaxChars !== void 0) {
14155
15181
  next.vault.toolResultMaxChars = clampToolResultMaxChars(vault.toolResultMaxChars);
14156
15182
  }
14157
- await mkdir9(path36.dirname(this.filePath), { recursive: true });
15183
+ const remote = patch.remote ?? {};
15184
+ if (remote.enabled !== void 0) next.remote.enabled = remote.enabled;
15185
+ if (remote.port !== void 0) {
15186
+ if (!isRemotePort(remote.port)) throw new Error(`The remote access port isn't valid: ${JSON.stringify(remote.port)}`);
15187
+ next.remote.port = remote.port;
15188
+ }
15189
+ await mkdir10(path37.dirname(this.filePath), { recursive: true });
14158
15190
  await writeFileAtomic(this.filePath, `${JSON.stringify(next, null, 2)}
14159
15191
  `);
14160
15192
  this.current = frozen(next);
@@ -14181,6 +15213,19 @@ function statusLineStartupText(state) {
14181
15213
  return "couldn't read its settings.json";
14182
15214
  }
14183
15215
  }
15216
+ var REMOTE_ACCESS_LABEL = "Remote access";
15217
+ function remoteAccessStartupLine(state, port, pairedDevices) {
15218
+ switch (state) {
15219
+ case "off":
15220
+ return null;
15221
+ case "active":
15222
+ return ` ${REMOTE_ACCESS_LABEL} on, port ${port}, ${pairedDevices} paired device${pairedDevices === 1 ? "" : "s"}`;
15223
+ case "port-busy":
15224
+ return ` ${REMOTE_ACCESS_LABEL} NOT WORKING: port ${port} is busy (another Agent Workbench, or another program)`;
15225
+ case "restart-needed":
15226
+ return ` ${REMOTE_ACCESS_LABEL} on, takes effect after a restart`;
15227
+ }
15228
+ }
14184
15229
  var AVAILABLE_AGENTS_LABEL = "Available CLIs";
14185
15230
  var HISTORY_LABEL = "History";
14186
15231
  function missingText(text) {
@@ -14219,11 +15264,18 @@ function startupAgentLines(agents) {
14219
15264
  }
14220
15265
  return lines;
14221
15266
  }
15267
+ var SUPPORT_LABEL = "Support";
15268
+ function supportStartupLine() {
15269
+ return ` ${SUPPORT_LABEL} If Agent Workbench is useful to you, consider supporting it: ${SUPPORT_URL}`;
15270
+ }
15271
+ function supportStartupLineColored() {
15272
+ 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`;
15273
+ }
14222
15274
 
14223
15275
  // 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";
15276
+ import { randomUUID as randomUUID4 } from "node:crypto";
15277
+ import { mkdir as mkdir11, readFile as readFile14, rename as rename5, rm as rm4, writeFile as writeFile6 } from "node:fs/promises";
15278
+ import path38 from "node:path";
14227
15279
  var STATE_VERSION2 = 1;
14228
15280
  var WRITE_DEBOUNCE_MS2 = 500;
14229
15281
  var NotesError = class extends ServerTextError {
@@ -14266,7 +15318,7 @@ var NotesStore = class {
14266
15318
  }
14267
15319
  async load() {
14268
15320
  try {
14269
- const raw = await readFile13(this.statePath, "utf8");
15321
+ const raw = await readFile14(this.statePath, "utf8");
14270
15322
  this.notes = parseState2(raw) ?? [];
14271
15323
  } catch {
14272
15324
  this.notes = [];
@@ -14350,11 +15402,11 @@ var NotesStore = class {
14350
15402
  throw new NotesError(serverText("imageUnknownFormat"));
14351
15403
  }
14352
15404
  const image = {
14353
- imageId: randomUUID3(),
15405
+ imageId: randomUUID4(),
14354
15406
  mediaType: format.mediaType,
14355
15407
  bytes: bytes.length
14356
15408
  };
14357
- await mkdir10(this.imagesDir, { recursive: true });
15409
+ await mkdir11(this.imagesDir, { recursive: true });
14358
15410
  await writeFile6(this.imagePath(image), bytes);
14359
15411
  note.images.push(image);
14360
15412
  note.updatedAt = now;
@@ -14383,7 +15435,7 @@ var NotesStore = class {
14383
15435
  const image = this.findImage(imageId);
14384
15436
  if (image === null) return null;
14385
15437
  try {
14386
- const bytes = await readFile13(this.imagePath(image));
15438
+ const bytes = await readFile14(this.imagePath(image));
14387
15439
  return { mediaType: image.mediaType, data: bytes.toString("base64") };
14388
15440
  } catch {
14389
15441
  return null;
@@ -14403,7 +15455,7 @@ var NotesStore = class {
14403
15455
  const images = [];
14404
15456
  for (const image of note.images) {
14405
15457
  try {
14406
- const bytes = await readFile13(this.imagePath(image));
15458
+ const bytes = await readFile14(this.imagePath(image));
14407
15459
  images.push({ mediaType: image.mediaType, data: bytes.toString("base64") });
14408
15460
  } catch {
14409
15461
  }
@@ -14427,7 +15479,7 @@ var NotesStore = class {
14427
15479
  }
14428
15480
  imagePath(image) {
14429
15481
  const safe = image.imageId.replace(/[^a-zA-Z0-9-]/g, "");
14430
- return path37.join(this.imagesDir, `${safe}.${extensionFor(image.mediaType)}`);
15482
+ return path38.join(this.imagesDir, `${safe}.${extensionFor(image.mediaType)}`);
14431
15483
  }
14432
15484
  async unlinkImage(image) {
14433
15485
  await rm4(this.imagePath(image), { force: true }).catch(() => {
@@ -14445,8 +15497,8 @@ var NotesStore = class {
14445
15497
  async writeNow() {
14446
15498
  this.dirty = false;
14447
15499
  try {
14448
- await mkdir10(path37.dirname(this.statePath), { recursive: true });
14449
- const temporary = path37.join(path37.dirname(this.statePath), `notes.${process.pid}.tmp`);
15500
+ await mkdir11(path38.dirname(this.statePath), { recursive: true });
15501
+ const temporary = path38.join(path38.dirname(this.statePath), `notes.${process.pid}.tmp`);
14450
15502
  await writeFile6(
14451
15503
  temporary,
14452
15504
  JSON.stringify({ version: STATE_VERSION2, notes: this.notes }, null, 2),
@@ -14459,10 +15511,226 @@ var NotesStore = class {
14459
15511
  }
14460
15512
  };
14461
15513
 
15514
+ // packages/server/src/draft-store.ts
15515
+ import { mkdir as mkdir12, readFile as readFile15, rename as rename6, writeFile as writeFile7 } from "node:fs/promises";
15516
+ import path39 from "node:path";
15517
+ var STATE_VERSION3 = 1;
15518
+ var WRITE_DEBOUNCE_MS3 = 500;
15519
+ var MAX_STORED_DRAFTS = 100;
15520
+ function draftOwnerOf(descriptor) {
15521
+ if (descriptor === null || descriptor.kind !== "agent" || descriptor.agent === null) return null;
15522
+ if (descriptor.sessionId.length === 0) return null;
15523
+ return { agent: descriptor.agent, sessionId: descriptor.sessionId };
15524
+ }
15525
+ function workspaceDraftOwners(state) {
15526
+ const owners = state.tabs.map(({ agent, sessionId }) => ({ agent, sessionId }));
15527
+ for (const { raw } of state.foreignTabs) {
15528
+ const agent = raw["agent"];
15529
+ const sessionId = raw["sessionId"];
15530
+ if (typeof agent === "string" && typeof sessionId === "string") owners.push({ agent, sessionId });
15531
+ }
15532
+ return owners;
15533
+ }
15534
+ var DraftTabs = class {
15535
+ seen = /* @__PURE__ */ new Set();
15536
+ owners = /* @__PURE__ */ new Map();
15537
+ update(descriptors) {
15538
+ const appeared = [];
15539
+ const moved = [];
15540
+ const present = /* @__PURE__ */ new Set();
15541
+ for (const descriptor of descriptors) {
15542
+ const { terminalId } = descriptor;
15543
+ present.add(terminalId);
15544
+ if (!this.seen.has(terminalId)) appeared.push(descriptor);
15545
+ const current = draftOwnerOf(descriptor);
15546
+ if (current === null) continue;
15547
+ const previous = this.owners.get(terminalId);
15548
+ if (previous !== void 0 && keyOf(previous) !== keyOf(current)) moved.push({ from: previous, to: current });
15549
+ this.owners.set(terminalId, current);
15550
+ }
15551
+ for (const terminalId of [...this.owners.keys()]) {
15552
+ if (!present.has(terminalId)) this.owners.delete(terminalId);
15553
+ }
15554
+ this.seen = present;
15555
+ return { appeared, moved };
15556
+ }
15557
+ };
15558
+ function keyOf(owner) {
15559
+ return JSON.stringify([owner.agent, owner.sessionId]);
15560
+ }
15561
+ function copyOf(draft) {
15562
+ return { text: draft.text, pasted: draft.pasted.map((paste) => ({ ...paste })) };
15563
+ }
15564
+ function parseEntry(value) {
15565
+ const record = asRecord(value);
15566
+ if (record === null) return null;
15567
+ const agent = asNonEmptyString(record["agent"]);
15568
+ const sessionId = asNonEmptyString(record["sessionId"]);
15569
+ const draft = parseComposerDraft(record["draft"]);
15570
+ const updatedAt = asFiniteNumber(record["updatedAt"]);
15571
+ if (agent === null || sessionId === null || draft === null || updatedAt === null) return null;
15572
+ if (isEmptyComposerDraft(draft) || composerDraftChars(draft) > MAX_COMPOSER_DRAFT_CHARS) return null;
15573
+ return { agent, sessionId, draft, updatedAt };
15574
+ }
15575
+ function parseState3(raw) {
15576
+ const drafts = /* @__PURE__ */ new Map();
15577
+ let parsed;
15578
+ try {
15579
+ parsed = JSON.parse(raw);
15580
+ } catch {
15581
+ return drafts;
15582
+ }
15583
+ const record = asRecord(parsed);
15584
+ if (record === null || record["version"] !== STATE_VERSION3) return drafts;
15585
+ for (const entry of asArrayFiltered(record["drafts"], parseEntry) ?? []) drafts.set(keyOf(entry), entry);
15586
+ return drafts;
15587
+ }
15588
+ function trimOldest(drafts) {
15589
+ if (drafts.size <= MAX_STORED_DRAFTS) return [];
15590
+ const oldest = [...drafts.entries()].sort(([, a], [, b]) => a.updatedAt - b.updatedAt).slice(0, drafts.size - MAX_STORED_DRAFTS).map(([key]) => key);
15591
+ for (const key of oldest) drafts.delete(key);
15592
+ return oldest;
15593
+ }
15594
+ var DraftStore = class {
15595
+ drafts = /* @__PURE__ */ new Map();
15596
+ /** Las claves que cambiaron aca desde la ultima escritura: lo unico que se escribe. */
15597
+ changed = /* @__PURE__ */ new Set();
15598
+ writeTimer = null;
15599
+ /** La escritura en curso: van de a una, porque dos se pisarian el temporal. */
15600
+ writing = Promise.resolve();
15601
+ statePath;
15602
+ now;
15603
+ /** Ruta y reloj inyectables para las pruebas; por defecto, la carpeta de configuracion. */
15604
+ constructor(options = {}) {
15605
+ this.statePath = options.statePath ?? composerDraftsPath();
15606
+ this.now = options.now ?? Date.now;
15607
+ }
15608
+ async load() {
15609
+ try {
15610
+ this.drafts = parseState3(await readFile15(this.statePath, "utf8"));
15611
+ } catch {
15612
+ this.drafts = /* @__PURE__ */ new Map();
15613
+ }
15614
+ }
15615
+ /** El borrador guardado de una conversacion, o null. Una copia. */
15616
+ get(owner) {
15617
+ const stored = this.drafts.get(keyOf(owner));
15618
+ return stored === void 0 ? null : copyOf(stored.draft);
15619
+ }
15620
+ /** Cuantos hay. Para las pruebas. */
15621
+ get size() {
15622
+ return this.drafts.size;
15623
+ }
15624
+ save(owner, draft) {
15625
+ if (composerDraftChars(draft) > MAX_COMPOSER_DRAFT_CHARS) {
15626
+ this.delete(owner);
15627
+ return "too-long";
15628
+ }
15629
+ if (isEmptyComposerDraft(draft)) return this.delete(owner) ? "deleted" : "unchanged";
15630
+ const key = keyOf(owner);
15631
+ const current = this.drafts.get(key);
15632
+ if (current !== void 0 && sameComposerDraft(current.draft, draft)) return "unchanged";
15633
+ this.drafts.set(key, { agent: owner.agent, sessionId: owner.sessionId, draft: copyOf(draft), updatedAt: this.now() });
15634
+ this.changed.add(key);
15635
+ for (const gone of trimOldest(this.drafts)) this.changed.add(gone);
15636
+ this.schedule();
15637
+ return "saved";
15638
+ }
15639
+ /** Borra el de esa conversacion: se mando, o se cerro su pestana. false si no habia. */
15640
+ delete(owner) {
15641
+ const key = keyOf(owner);
15642
+ if (!this.drafts.delete(key)) return false;
15643
+ this.changed.add(key);
15644
+ this.schedule();
15645
+ return true;
15646
+ }
15647
+ /**
15648
+ * La pestana cambio de conversacion —una CLI que la descubre, o que empieza
15649
+ * otra con `/clear`— y su borrador va con ella: es lo que se restaura con la
15650
+ * pestana, que desde ahora se guarda con el id nuevo.
15651
+ */
15652
+ move(from, to) {
15653
+ const fromKey = keyOf(from);
15654
+ const stored = this.drafts.get(fromKey);
15655
+ if (stored === void 0 || fromKey === keyOf(to)) return;
15656
+ this.delete(from);
15657
+ this.save(to, stored.draft);
15658
+ }
15659
+ /**
15660
+ * Deja solo los de estas conversaciones: las pestanas del archivo del
15661
+ * arranque. Lo demas es de una pestana que ya no esta —se cerro con una build
15662
+ * anterior, o su carpeta se borro— y nadie lo va a pedir.
15663
+ */
15664
+ retainOnly(owners) {
15665
+ const keep = new Set([...owners].map(keyOf));
15666
+ let removed = false;
15667
+ for (const key of [...this.drafts.keys()]) {
15668
+ if (keep.has(key)) continue;
15669
+ this.drafts.delete(key);
15670
+ this.changed.add(key);
15671
+ removed = true;
15672
+ }
15673
+ if (removed) this.schedule();
15674
+ }
15675
+ /** Escritura inmediata, para el apagado. */
15676
+ async flush() {
15677
+ if (this.writeTimer !== null) {
15678
+ clearTimeout(this.writeTimer);
15679
+ this.writeTimer = null;
15680
+ }
15681
+ await this.write();
15682
+ }
15683
+ schedule() {
15684
+ if (this.writeTimer !== null) return;
15685
+ this.writeTimer = setTimeout(() => {
15686
+ this.writeTimer = null;
15687
+ void this.write();
15688
+ }, WRITE_DEBOUNCE_MS3);
15689
+ this.writeTimer.unref();
15690
+ }
15691
+ write() {
15692
+ const run = async () => {
15693
+ if (this.changed.size > 0) await this.writeNow();
15694
+ };
15695
+ this.writing = this.writing.then(run, run);
15696
+ return this.writing;
15697
+ }
15698
+ async writeNow() {
15699
+ let onDisk = /* @__PURE__ */ new Map();
15700
+ try {
15701
+ onDisk = parseState3(await readFile15(this.statePath, "utf8"));
15702
+ } catch {
15703
+ }
15704
+ const changed = this.changed;
15705
+ this.changed = /* @__PURE__ */ new Set();
15706
+ const merged = onDisk;
15707
+ for (const key of changed) {
15708
+ const mine = this.drafts.get(key);
15709
+ if (mine === void 0) merged.delete(key);
15710
+ else merged.set(key, mine);
15711
+ }
15712
+ trimOldest(merged);
15713
+ this.drafts = new Map(merged);
15714
+ try {
15715
+ await mkdir12(path39.dirname(this.statePath), { recursive: true });
15716
+ const temporary = path39.join(path39.dirname(this.statePath), `composer-drafts.${process.pid}.tmp`);
15717
+ const payload = {
15718
+ version: STATE_VERSION3,
15719
+ drafts: [...merged.values()].map(({ agent, sessionId, updatedAt, draft }) => ({ agent, sessionId, updatedAt, draft }))
15720
+ };
15721
+ await writeFile7(temporary, JSON.stringify(payload, null, 2), { encoding: "utf8", mode: 384 });
15722
+ await rename6(temporary, this.statePath);
15723
+ } catch (error) {
15724
+ for (const key of changed) this.changed.add(key);
15725
+ console.warn("[drafts] couldn't save the message box drafts:", error);
15726
+ }
15727
+ }
15728
+ };
15729
+
14462
15730
  // packages/server/src/shell-locator.ts
14463
15731
  import { access as access2 } from "node:fs/promises";
14464
15732
  import { constants as fsConstants2 } from "node:fs";
14465
- import path38 from "node:path";
15733
+ import path40 from "node:path";
14466
15734
  async function isExecutable(candidate) {
14467
15735
  try {
14468
15736
  await access2(candidate, process.platform === "win32" ? fsConstants2.R_OK : fsConstants2.X_OK);
@@ -14474,7 +15742,7 @@ async function isExecutable(candidate) {
14474
15742
  function windowsPowerShellPath() {
14475
15743
  const systemRoot = process.env["SystemRoot"] ?? process.env["windir"];
14476
15744
  if (systemRoot === void 0 || systemRoot.length === 0) return null;
14477
- return path38.join(systemRoot, "System32", "WindowsPowerShell", "v1.0", "powershell.exe");
15745
+ return path40.join(systemRoot, "System32", "WindowsPowerShell", "v1.0", "powershell.exe");
14478
15746
  }
14479
15747
  async function locateWindowsShell() {
14480
15748
  const pwsh = await findInPath("pwsh");
@@ -14494,7 +15762,7 @@ async function locateWindowsShell() {
14494
15762
  async function locatePosixShell() {
14495
15763
  const preferred = process.env["SHELL"];
14496
15764
  if (preferred !== void 0 && preferred.length > 0 && await isExecutable(preferred)) {
14497
- return { file: preferred, args: [], name: path38.basename(preferred) };
15765
+ return { file: preferred, args: [], name: path40.basename(preferred) };
14498
15766
  }
14499
15767
  if (await isExecutable("/bin/bash")) return { file: "/bin/bash", args: [], name: "bash" };
14500
15768
  if (await isExecutable("/bin/sh")) return { file: "/bin/sh", args: [], name: "sh" };
@@ -14505,13 +15773,13 @@ async function locateShell() {
14505
15773
  }
14506
15774
 
14507
15775
  // packages/server/src/session-watcher.ts
14508
- import { stat as stat19 } from "node:fs/promises";
15776
+ import { stat as stat21 } from "node:fs/promises";
14509
15777
  import chokidar3 from "chokidar";
14510
15778
  var INDEX_DEBOUNCE_MS = 750;
14511
15779
  var ROOT_PROBE_MS = 5e3;
14512
15780
  async function isDirectory3(target) {
14513
15781
  try {
14514
- return (await stat19(target)).isDirectory();
15782
+ return (await stat21(target)).isDirectory();
14515
15783
  } catch {
14516
15784
  return false;
14517
15785
  }
@@ -14612,9 +15880,9 @@ function watchSessions(index, hub, agents, options = {}) {
14612
15880
  }
14613
15881
 
14614
15882
  // packages/server/src/terminal-registry.ts
14615
- import { randomUUID as randomUUID4 } from "node:crypto";
15883
+ import { randomUUID as randomUUID5 } from "node:crypto";
14616
15884
  import { EventEmitter as EventEmitter5 } from "node:events";
14617
- import { stat as stat20 } from "node:fs/promises";
15885
+ import { stat as stat22 } from "node:fs/promises";
14618
15886
 
14619
15887
  // packages/server/src/launch-hook-slot.ts
14620
15888
  var LaunchHookSlot = class {
@@ -14876,10 +16144,10 @@ async function resolveLaunchPlan(launch) {
14876
16144
  }
14877
16145
 
14878
16146
  // 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;
16147
+ import { mkdir as mkdir13, readFile as readFile16, writeFile as writeFile8 } from "node:fs/promises";
16148
+ import path41 from "node:path";
16149
+ var STATE_VERSION4 = 1;
16150
+ var WRITE_DEBOUNCE_MS4 = 400;
14883
16151
  var LEGACY_TAB_AGENT = "claude-code";
14884
16152
  function placeByPosition(base, placed) {
14885
16153
  const result = [...base];
@@ -14935,11 +16203,11 @@ function readTab(tab, missingAgent) {
14935
16203
  if (agent === null) return null;
14936
16204
  return { kind: "known", tab: { agent, cwd, sessionId, label: typeof label === "string" ? label : "" } };
14937
16205
  }
14938
- function parseState3(raw) {
16206
+ function parseState4(raw) {
14939
16207
  try {
14940
16208
  const parsed = JSON.parse(raw);
14941
16209
  if (!isRecord(parsed)) return null;
14942
- if (parsed["version"] !== STATE_VERSION3) return null;
16210
+ if (parsed["version"] !== STATE_VERSION4) return null;
14943
16211
  const rawTabs = parsed["tabs"];
14944
16212
  if (!Array.isArray(rawTabs)) return null;
14945
16213
  const base = [];
@@ -14977,7 +16245,7 @@ function serializeState(state) {
14977
16245
  otherTabs.push({ ...entry.tab, position });
14978
16246
  }
14979
16247
  }
14980
- return otherTabs.length === 0 ? { version: STATE_VERSION3, tabs } : { version: STATE_VERSION3, tabs, otherTabs };
16248
+ return otherTabs.length === 0 ? { version: STATE_VERSION4, tabs } : { version: STATE_VERSION4, tabs, otherTabs };
14981
16249
  }
14982
16250
  var EMPTY_STATE = () => ({ tabs: [], foreignTabs: [] });
14983
16251
  function persistableTabs(descriptors) {
@@ -14999,8 +16267,8 @@ var WorkspaceStore = class {
14999
16267
  pending = null;
15000
16268
  async load() {
15001
16269
  try {
15002
- const raw = await readFile14(workspaceStatePath(), "utf8");
15003
- return parseState3(raw) ?? EMPTY_STATE();
16270
+ const raw = await readFile16(workspaceStatePath(), "utf8");
16271
+ return parseState4(raw) ?? EMPTY_STATE();
15004
16272
  } catch {
15005
16273
  return EMPTY_STATE();
15006
16274
  }
@@ -15014,7 +16282,7 @@ var WorkspaceStore = class {
15014
16282
  const toWrite = this.pending;
15015
16283
  this.pending = null;
15016
16284
  if (toWrite !== null) void this.writeNow(toWrite);
15017
- }, WRITE_DEBOUNCE_MS3);
16285
+ }, WRITE_DEBOUNCE_MS4);
15018
16286
  this.writeTimer.unref();
15019
16287
  }
15020
16288
  /** Escritura inmediata, para el apagado. */
@@ -15029,12 +16297,12 @@ var WorkspaceStore = class {
15029
16297
  }
15030
16298
  async writeNow(state) {
15031
16299
  try {
15032
- await mkdir11(appConfigDir(), { recursive: true });
16300
+ await mkdir13(appConfigDir(), { recursive: true });
15033
16301
  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);
16302
+ const temporary = path41.join(path41.dirname(target), `workspace.${process.pid}.tmp`);
16303
+ await writeFile8(temporary, JSON.stringify(serializeState(state), null, 2), "utf8");
16304
+ const { rename: rename7 } = await import("node:fs/promises");
16305
+ await rename7(temporary, target);
15038
16306
  } catch (error) {
15039
16307
  console.warn("[workspace] couldn't save the state:", error);
15040
16308
  }
@@ -15151,14 +16419,14 @@ var TerminalRegistry = class extends EventEmitter5 {
15151
16419
  }
15152
16420
  await this.assertDirectory(options.cwd);
15153
16421
  const resumed = kind === "agent" && options.resumeSessionId !== void 0;
15154
- const terminalId = randomUUID4();
16422
+ const terminalId = randomUUID5();
15155
16423
  const descriptor = {
15156
16424
  terminalId,
15157
16425
  kind,
15158
16426
  agent,
15159
16427
  cwd: options.cwd,
15160
16428
  // Provisional: `spawn` lo confirma con lo que diga el adaptador.
15161
- sessionId: kind === "agent" ? options.resumeSessionId ?? randomUUID4() : "",
16429
+ sessionId: kind === "agent" ? options.resumeSessionId ?? randomUUID5() : "",
15162
16430
  label: options.label ?? "",
15163
16431
  resumed,
15164
16432
  createdAt: Date.now(),
@@ -15262,7 +16530,7 @@ var TerminalRegistry = class extends EventEmitter5 {
15262
16530
  async assertDirectory(cwd) {
15263
16531
  let info;
15264
16532
  try {
15265
- info = await stat20(cwd);
16533
+ info = await stat22(cwd);
15266
16534
  } catch {
15267
16535
  throw new TerminalOpenError("invalid-cwd", serverText("cwdMissing", { cwd }));
15268
16536
  }
@@ -15290,7 +16558,7 @@ var TerminalRegistry = class extends EventEmitter5 {
15290
16558
  let launch;
15291
16559
  let adapter = null;
15292
16560
  let launchedSessionId = sessionId;
15293
- const launchToken = randomUUID4();
16561
+ const launchToken = randomUUID5();
15294
16562
  if (launcher.kind === "agent") {
15295
16563
  adapter = launcher.agent.adapter;
15296
16564
  const { location } = launcher.agent;
@@ -15526,7 +16794,7 @@ var TerminalRegistry = class extends EventEmitter5 {
15526
16794
  console.warn(`[workspace] the tab of ${tab.cwd} wasn't restored: ${reason2}`);
15527
16795
  continue;
15528
16796
  }
15529
- const terminalId = randomUUID4();
16797
+ const terminalId = randomUUID5();
15530
16798
  const descriptor = {
15531
16799
  terminalId,
15532
16800
  kind: "agent",
@@ -15688,20 +16956,20 @@ var TerminalRegistry = class extends EventEmitter5 {
15688
16956
  import { WebSocketServer } from "ws";
15689
16957
 
15690
16958
  // 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";
16959
+ import { randomUUID as randomUUID6 } from "node:crypto";
16960
+ import { mkdir as mkdir14, readdir as readdir11, stat as stat23 } from "node:fs/promises";
15693
16961
  import { homedir as homedir11 } from "node:os";
15694
- import path40 from "node:path";
16962
+ import path42 from "node:path";
15695
16963
  var MAX_ENTRIES = 500;
15696
16964
  var MAX_PICKERS = 8;
15697
16965
  var SKIPPED = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "bin", "obj", "$RECYCLE.BIN"]);
15698
16966
  var DirectoryPickerError = class extends ServerTextError {
15699
16967
  };
15700
16968
  function isInsideProtected(target, dirs) {
15701
- const resolved = path40.resolve(target);
16969
+ const resolved = path42.resolve(target);
15702
16970
  return dirs.some((dir) => {
15703
- const protectedDir = path40.resolve(dir);
15704
- return resolved === protectedDir || resolved.startsWith(protectedDir + path40.sep);
16971
+ const protectedDir = path42.resolve(dir);
16972
+ return resolved === protectedDir || resolved.startsWith(protectedDir + path42.sep);
15705
16973
  });
15706
16974
  }
15707
16975
  async function listRoots() {
@@ -15713,7 +16981,7 @@ async function listRoots() {
15713
16981
  letters.map(async (letter) => {
15714
16982
  const drive = `${letter}:\\`;
15715
16983
  try {
15716
- await stat21(drive);
16984
+ await stat23(drive);
15717
16985
  return drive;
15718
16986
  } catch {
15719
16987
  return null;
@@ -15740,7 +17008,7 @@ var DirectoryPickers = class {
15740
17008
  if (this.pickers.size >= MAX_PICKERS) {
15741
17009
  throw new DirectoryPickerError(serverText("pickerTooMany"));
15742
17010
  }
15743
- const picker = { id: randomUUID5(), current: homedir11(), entries: /* @__PURE__ */ new Set() };
17011
+ const picker = { id: randomUUID6(), current: homedir11(), entries: /* @__PURE__ */ new Set() };
15744
17012
  this.pickers.set(picker.id, picker);
15745
17013
  return this.listing(picker);
15746
17014
  }
@@ -15765,14 +17033,14 @@ var DirectoryPickers = class {
15765
17033
  async enter(pickerId, name) {
15766
17034
  const picker = this.require(pickerId);
15767
17035
  if (name === "..") {
15768
- const parent = path40.dirname(picker.current);
17036
+ const parent = path42.dirname(picker.current);
15769
17037
  if (parent !== picker.current) picker.current = parent;
15770
17038
  return this.listing(picker);
15771
17039
  }
15772
17040
  if (!picker.entries.has(name)) {
15773
17041
  throw new DirectoryPickerError(serverText("pickerNotListed"));
15774
17042
  }
15775
- const target = path40.join(picker.current, name);
17043
+ const target = path42.join(picker.current, name);
15776
17044
  if (isInsideProtected(target, this.protectedDirs)) {
15777
17045
  throw new DirectoryPickerError(serverText("pickerCliFolder"));
15778
17046
  }
@@ -15812,12 +17080,12 @@ var DirectoryPickers = class {
15812
17080
  throw new DirectoryPickerError(serverText("pickerNameChars"));
15813
17081
  }
15814
17082
  }
15815
- const target = path40.join(picker.current, clean);
17083
+ const target = path42.join(picker.current, clean);
15816
17084
  if (isInsideProtected(target, this.protectedDirs)) {
15817
17085
  throw new DirectoryPickerError(serverText("pickerNoProjectsHere"));
15818
17086
  }
15819
17087
  try {
15820
- await mkdir12(target);
17088
+ await mkdir14(target);
15821
17089
  } catch (error) {
15822
17090
  const code = error.code;
15823
17091
  throw new DirectoryPickerError(
@@ -15849,7 +17117,7 @@ var DirectoryPickers = class {
15849
17117
  let truncated = false;
15850
17118
  try {
15851
17119
  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));
17120
+ 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
17121
  truncated = dirs.length > MAX_ENTRIES;
15854
17122
  names = dirs.slice(0, MAX_ENTRIES);
15855
17123
  } catch {
@@ -15860,15 +17128,15 @@ var DirectoryPickers = class {
15860
17128
  path: picker.current,
15861
17129
  entries: names,
15862
17130
  roots,
15863
- canGoUp: path40.dirname(picker.current) !== picker.current,
17131
+ canGoUp: path42.dirname(picker.current) !== picker.current,
15864
17132
  truncated
15865
17133
  };
15866
17134
  }
15867
17135
  };
15868
17136
 
15869
17137
  // 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";
17138
+ import { readFile as readFile17, readdir as readdir12, stat as stat24 } from "node:fs/promises";
17139
+ import path43 from "node:path";
15872
17140
  var MAX_ENTRIES2 = 1e3;
15873
17141
  var SEARCH_MAX_RESULTS = 200;
15874
17142
  var SEARCH_MAX_DIRECTORIES = 4e3;
@@ -15931,7 +17199,7 @@ function languageFor(fileName) {
15931
17199
  const lower = fileName.toLowerCase();
15932
17200
  const byName = LANGUAGE_BY_NAME[lower];
15933
17201
  if (byName !== void 0) return byName;
15934
- const extension = path41.extname(lower);
17202
+ const extension = path43.extname(lower);
15935
17203
  return LANGUAGE_BY_EXTENSION[extension] ?? null;
15936
17204
  }
15937
17205
  function looksBinary2(buffer) {
@@ -15963,7 +17231,7 @@ async function listDirectory(cwd, relativePath, options = {}) {
15963
17231
  let sizeBytes = 0;
15964
17232
  let modifiedAt = 0;
15965
17233
  try {
15966
- const info = await stat22(path41.join(absolute, candidate.name));
17234
+ const info = await stat24(path43.join(absolute, candidate.name));
15967
17235
  sizeBytes = candidate.isDirectory ? 0 : info.size;
15968
17236
  modifiedAt = info.mtimeMs;
15969
17237
  } catch {
@@ -15996,7 +17264,7 @@ async function readLevel(absolute) {
15996
17264
  const always = ALWAYS_SKIP.has(dirent.name.toLowerCase());
15997
17265
  if (dirent.isSymbolicLink()) {
15998
17266
  try {
15999
- const target = await stat22(path41.join(absolute, dirent.name));
17267
+ const target = await stat24(path43.join(absolute, dirent.name));
16000
17268
  candidates.push({
16001
17269
  name: dirent.name,
16002
17270
  isDirectory: target.isDirectory(),
@@ -16025,7 +17293,7 @@ async function markIgnored(repoRoot2, levels) {
16025
17293
  for (const level of levels) {
16026
17294
  for (const entry of level.entries) {
16027
17295
  if (entry.hidden !== null) continue;
16028
- keys.set(entry, toPosixPath(path41.relative(repoRoot2, path41.join(level.absolute, entry.name))));
17296
+ keys.set(entry, toPosixPath(path43.relative(repoRoot2, path43.join(level.absolute, entry.name))));
16029
17297
  }
16030
17298
  }
16031
17299
  if (keys.size === 0) return;
@@ -16086,7 +17354,7 @@ async function searchFiles(cwd, query, options = {}) {
16086
17354
  let sizeBytes = 0;
16087
17355
  let modifiedAt = 0;
16088
17356
  try {
16089
- const info = await stat22(path41.join(level.absolute, candidate.name));
17357
+ const info = await stat24(path43.join(level.absolute, candidate.name));
16090
17358
  sizeBytes = candidate.isDirectory ? 0 : info.size;
16091
17359
  modifiedAt = info.mtimeMs;
16092
17360
  } catch {
@@ -16120,12 +17388,12 @@ async function searchFiles(cwd, query, options = {}) {
16120
17388
  }
16121
17389
  async function readPreview(cwd, relativePath) {
16122
17390
  const absolute = await resolveInside(cwd, relativePath, { mustExist: true });
16123
- const info = await stat22(absolute);
17391
+ const info = await stat24(absolute);
16124
17392
  if (info.isDirectory()) {
16125
17393
  throw new Error("It's a directory, not a file.");
16126
17394
  }
16127
- const content = await readFile15(absolute);
16128
- const fileName = path41.basename(absolute);
17395
+ const content = await readFile17(absolute);
17396
+ const fileName = path43.basename(absolute);
16129
17397
  if (looksBinary2(content)) {
16130
17398
  return {
16131
17399
  path: relativePath,
@@ -16714,14 +17982,14 @@ var TerminalWriteQueue = class {
16714
17982
  };
16715
17983
 
16716
17984
  // packages/server/src/vault/service.ts
16717
- import { mkdir as mkdir15, stat as stat26 } from "node:fs/promises";
17985
+ import { mkdir as mkdir17, stat as stat28 } from "node:fs/promises";
16718
17986
  import { homedir as homedir12 } from "node:os";
16719
- import path45 from "node:path";
17987
+ import path47 from "node:path";
16720
17988
 
16721
17989
  // packages/server/src/vault/move.ts
16722
17990
  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";
17991
+ 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";
17992
+ import path44 from "node:path";
16725
17993
  var MOVED_ENTRIES = ["vault.json", "sessions", "memory"];
16726
17994
  var OWN_TEMP_FILE = /\.\d+\.[0-9a-f]{12}\.tmp$/;
16727
17995
  async function copyTree(source, target, result) {
@@ -16732,14 +18000,14 @@ async function copyTree(source, target, result) {
16732
18000
  return;
16733
18001
  }
16734
18002
  if (info.isDirectory()) {
16735
- await mkdir13(target, { recursive: true });
18003
+ await mkdir15(target, { recursive: true });
16736
18004
  for (const name of await readdir13(source)) {
16737
- await copyTree(path42.join(source, name), path42.join(target, name), result);
18005
+ await copyTree(path44.join(source, name), path44.join(target, name), result);
16738
18006
  }
16739
18007
  return;
16740
18008
  }
16741
18009
  if (!info.isFile() || OWN_TEMP_FILE.test(source)) return;
16742
- await mkdir13(path42.dirname(target), { recursive: true });
18010
+ await mkdir15(path44.dirname(target), { recursive: true });
16743
18011
  try {
16744
18012
  await copyFile2(source, target, constants.COPYFILE_EXCL);
16745
18013
  } catch (error) {
@@ -16754,9 +18022,9 @@ async function copyTree(source, target, result) {
16754
18022
  }
16755
18023
  async function moveVault(from, to) {
16756
18024
  const result = { copied: 0, skipped: 0 };
16757
- await mkdir13(to, { recursive: true });
18025
+ await mkdir15(to, { recursive: true });
16758
18026
  for (const entry of MOVED_ENTRIES) {
16759
- await copyTree(path42.join(from, entry), path42.join(to, entry), result);
18027
+ await copyTree(path44.join(from, entry), path44.join(to, entry), result);
16760
18028
  }
16761
18029
  return result;
16762
18030
  }
@@ -16788,11 +18056,11 @@ function checkVaultTarget(current, target, options) {
16788
18056
  return { kind: "ok" };
16789
18057
  }
16790
18058
  async function canWriteInto(dir) {
16791
- const probe = temporaryPathFor(path42.join(dir, ".escritura"));
18059
+ const probe = temporaryPathFor(path44.join(dir, ".escritura"));
16792
18060
  try {
16793
- await mkdir13(dir, { recursive: true });
16794
- if (!(await stat23(dir)).isDirectory()) return false;
16795
- await writeFile8(probe, "");
18061
+ await mkdir15(dir, { recursive: true });
18062
+ if (!(await stat25(dir)).isDirectory()) return false;
18063
+ await writeFile9(probe, "");
16796
18064
  return true;
16797
18065
  } catch {
16798
18066
  return false;
@@ -16802,12 +18070,12 @@ async function canWriteInto(dir) {
16802
18070
  }
16803
18071
 
16804
18072
  // packages/server/src/vault/serialize.ts
16805
- import { createHash as createHash3 } from "node:crypto";
18073
+ import { createHash as createHash4 } from "node:crypto";
16806
18074
  function imageKey(eventId, source, index) {
16807
18075
  return `${eventId}|${source}|${index}`;
16808
18076
  }
16809
18077
  function assetName(bytes) {
16810
- const hash = createHash3("sha256").update(bytes).digest("hex").slice(0, 32);
18078
+ const hash = createHash4("sha256").update(bytes).digest("hex").slice(0, 32);
16811
18079
  return `${hash}.${detectImageFormat(bytes)?.ext ?? "bin"}`;
16812
18080
  }
16813
18081
  function buildHeader(input) {
@@ -16894,12 +18162,12 @@ function serializeSession(input) {
16894
18162
  }
16895
18163
 
16896
18164
  // 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";
18165
+ import { readdir as readdir15, rm as rm6, stat as stat27 } from "node:fs/promises";
18166
+ import path46 from "node:path";
16899
18167
 
16900
18168
  // 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";
18169
+ import { mkdir as mkdir16, readdir as readdir14, readFile as readFile18, stat as stat26, utimes as utimes2 } from "node:fs/promises";
18170
+ import path45 from "node:path";
16903
18171
  var MEMORY_FILE_MAX_BYTES = 1024 * 1024;
16904
18172
  var MEMORY_MAX_FILES = 500;
16905
18173
  var MEMORY_SOURCE_FILE = "source.json";
@@ -16924,7 +18192,7 @@ async function listMemoryNotes(cwd) {
16924
18192
  let absolute;
16925
18193
  try {
16926
18194
  absolute = await resolveInside(cwd, `${MEMORY_DIR}/${name}`, { mustExist: true });
16927
- const info = await stat24(absolute);
18195
+ const info = await stat26(absolute);
16928
18196
  if (!info.isFile() || info.size > MEMORY_FILE_MAX_BYTES) continue;
16929
18197
  notes.push({ name, absolute, size: info.size, mtimeMs: info.mtimeMs });
16930
18198
  } catch {
@@ -16938,7 +18206,7 @@ async function measureProjectMemory(cwd) {
16938
18206
  }
16939
18207
  async function isUpToDate(target, note) {
16940
18208
  try {
16941
- const info = await stat24(target);
18209
+ const info = await stat26(target);
16942
18210
  return info.isFile() && info.size === note.size && Math.abs(info.mtimeMs - note.mtimeMs) < SAME_MTIME_MS;
16943
18211
  } catch {
16944
18212
  return false;
@@ -16946,7 +18214,7 @@ async function isUpToDate(target, note) {
16946
18214
  }
16947
18215
  async function exists3(file) {
16948
18216
  try {
16949
- await stat24(file);
18217
+ await stat26(file);
16950
18218
  return true;
16951
18219
  } catch {
16952
18220
  return false;
@@ -16962,14 +18230,14 @@ async function copyProjectMemory(dir, cwd, platform, deps = DEFAULT_WRITE_DEPS)
16962
18230
  if (prepared) return;
16963
18231
  prepared = true;
16964
18232
  await ensureVaultMarker(dir, deps);
16965
- await mkdir14(target, { recursive: true });
18233
+ await mkdir16(target, { recursive: true });
16966
18234
  };
16967
18235
  for (const note of notes) {
16968
- const destination = path43.join(target, note.name);
18236
+ const destination = path45.join(target, note.name);
16969
18237
  if (await isUpToDate(destination, note)) continue;
16970
18238
  let content;
16971
18239
  try {
16972
- content = await readFile16(note.absolute);
18240
+ content = await readFile18(note.absolute);
16973
18241
  } catch {
16974
18242
  continue;
16975
18243
  }
@@ -16981,7 +18249,7 @@ async function copyProjectMemory(dir, cwd, platform, deps = DEFAULT_WRITE_DEPS)
16981
18249
  result.files += 1;
16982
18250
  result.bytes += content.length;
16983
18251
  }
16984
- const source = path43.join(target, MEMORY_SOURCE_FILE);
18252
+ const source = path45.join(target, MEMORY_SOURCE_FILE);
16985
18253
  if (result.files > 0 || !await exists3(source)) {
16986
18254
  await prepare();
16987
18255
  await writeFileAtomic(source, `${JSON.stringify({ cwd, copiedAt: deps.now() })}
@@ -17475,7 +18743,7 @@ var VaultWriter = class {
17475
18743
  }
17476
18744
  async assetOnDisk(dir, agent, sessionId, asset, bytes) {
17477
18745
  try {
17478
- const info = await stat25(assetFile(dir, agent, sessionId, asset));
18746
+ const info = await stat27(assetFile(dir, agent, sessionId, asset));
17479
18747
  return info.isFile() && info.size === bytes;
17480
18748
  } catch {
17481
18749
  return false;
@@ -17575,17 +18843,17 @@ var VaultWriter = class {
17575
18843
  const folders = [dir];
17576
18844
  const subfolders = async (parent) => {
17577
18845
  try {
17578
- return (await readdir15(parent, { withFileTypes: true })).filter((entry) => entry.isDirectory()).map((entry) => path44.join(parent, entry.name));
18846
+ return (await readdir15(parent, { withFileTypes: true })).filter((entry) => entry.isDirectory()).map((entry) => path46.join(parent, entry.name));
17579
18847
  } catch {
17580
18848
  return [];
17581
18849
  }
17582
18850
  };
17583
18851
  for (const agentDir of await subfolders(sessionsRoot2(dir))) {
17584
- if (!SESSION_AGENT_IDS.includes(path44.basename(agentDir))) continue;
18852
+ if (!SESSION_AGENT_IDS.includes(path46.basename(agentDir))) continue;
17585
18853
  folders.push(agentDir);
17586
18854
  folders.push(...(await subfolders(agentDir)).filter((folder) => folder.endsWith(".assets")));
17587
18855
  }
17588
- folders.push(...await subfolders(path44.join(dir, "memory")));
18856
+ folders.push(...await subfolders(path46.join(dir, "memory")));
17589
18857
  for (const folder of folders) {
17590
18858
  let names;
17591
18859
  try {
@@ -17595,9 +18863,9 @@ var VaultWriter = class {
17595
18863
  }
17596
18864
  for (const name of names) {
17597
18865
  if (!OWN_TEMP_FILE2.test(name)) continue;
17598
- const file = path44.join(folder, name);
18866
+ const file = path46.join(folder, name);
17599
18867
  try {
17600
- const info = await stat25(file);
18868
+ const info = await stat27(file);
17601
18869
  if (info.isFile() && now - info.mtimeMs > TEMP_MAX_AGE_MS) await rm6(file, { force: true });
17602
18870
  } catch {
17603
18871
  }
@@ -17785,14 +19053,14 @@ var VaultService = class {
17785
19053
  async setDir(target) {
17786
19054
  const { platform, agents, settings: settings2, catalog } = this.options;
17787
19055
  const current = this.dir();
17788
- const protectedDirs = [...agents.protectedDirs(), path45.join(this.options.homeDir ?? homedir12(), ".gemini")];
19056
+ const protectedDirs = [...agents.protectedDirs(), path47.join(this.options.homeDir ?? homedir12(), ".gemini")];
17789
19057
  const check = checkVaultTarget(current, target, { protectedDirs, platform });
17790
19058
  if (check.kind === "refused") throw new VaultError(check.text);
17791
19059
  if (check.kind === "same") return;
17792
19060
  try {
17793
19061
  await this.writer.exclusive("moving", async () => {
17794
19062
  if (!await canWriteInto(target)) throw new VaultError(serverText("vaultCannotWrite"));
17795
- const hadCopy = await stat26(vaultMarkerFile(current)).then(
19063
+ const hadCopy = await stat28(vaultMarkerFile(current)).then(
17796
19064
  () => true,
17797
19065
  () => false
17798
19066
  );
@@ -17826,7 +19094,7 @@ var VaultService = class {
17826
19094
  /** Abre la carpeta de la copia con el explorador del sistema. */
17827
19095
  async reveal() {
17828
19096
  const dir = this.activeDir();
17829
- const isFolder = await stat26(dir).then(
19097
+ const isFolder = await stat28(dir).then(
17830
19098
  (info) => info.isDirectory(),
17831
19099
  () => false
17832
19100
  );
@@ -17850,7 +19118,7 @@ var VaultService = class {
17850
19118
  fromCopy: true
17851
19119
  });
17852
19120
  const file = sessionExportFile(dir, agent, sessionId);
17853
- await mkdir15(path45.dirname(file), { recursive: true });
19121
+ await mkdir17(path47.dirname(file), { recursive: true });
17854
19122
  await writeFileAtomic(file, text, this.writeDeps);
17855
19123
  this.openWithSystem(file);
17856
19124
  return file;
@@ -17937,7 +19205,7 @@ var VaultService = class {
17937
19205
  const project = this.options.index.getProjects().find((candidate) => candidate.key === projectKey);
17938
19206
  if (project === void 0) throw new VaultError(serverText("vaultProjectGone"));
17939
19207
  const folder = projectExportDirFor(this.activeDir(), project, this.options.platform);
17940
- await mkdir15(folder, { recursive: true });
19208
+ await mkdir17(folder, { recursive: true });
17941
19209
  const entries = [];
17942
19210
  const names = /* @__PURE__ */ new Set();
17943
19211
  let skipped = 0;
@@ -17954,7 +19222,7 @@ var VaultService = class {
17954
19222
  continue;
17955
19223
  }
17956
19224
  const fileName = this.exportFileName(summary, names);
17957
- await writeFileAtomic(path45.join(folder, fileName), text, this.writeDeps);
19225
+ await writeFileAtomic(path47.join(folder, fileName), text, this.writeDeps);
17958
19226
  entries.push({
17959
19227
  updatedAt: summary.updatedAt,
17960
19228
  agentLabel: this.labelOf(summary.agent),
@@ -17965,9 +19233,9 @@ var VaultService = class {
17965
19233
  });
17966
19234
  await nextTurn3();
17967
19235
  }
17968
- const name = project.cwd.length > 0 ? path45.basename(project.cwd) || project.cwd : project.fallbackName;
19236
+ const name = project.cwd.length > 0 ? path47.basename(project.cwd) || project.cwd : project.fallbackName;
17969
19237
  const index = renderProjectIndex({ name, cwd: project.cwd, exportedAt: this.now(), entries, skipped });
17970
- await writeFileAtomic(path45.join(folder, "index.md"), index, this.writeDeps);
19238
+ await writeFileAtomic(path47.join(folder, "index.md"), index, this.writeDeps);
17971
19239
  this.openWithSystem(folder);
17972
19240
  return { sessions: entries.length, skipped, folder };
17973
19241
  }
@@ -18123,12 +19391,14 @@ function attachTerminalSocket(options) {
18123
19391
  index,
18124
19392
  archived,
18125
19393
  notes,
19394
+ drafts,
18126
19395
  conversations,
18127
19396
  repos,
18128
19397
  memory,
18129
19398
  agents,
18130
19399
  defaultCwd,
18131
- vault
19400
+ vault,
19401
+ remote
18132
19402
  } = options;
18133
19403
  const pasteStore = options.pasteStore ?? new PasteStore();
18134
19404
  const writeQueue = new TerminalWriteQueue();
@@ -18143,6 +19413,14 @@ function attachTerminalSocket(options) {
18143
19413
  if (socket.readyState === socket.OPEN) socket.send(payload);
18144
19414
  }
18145
19415
  };
19416
+ const remoteSockets = /* @__PURE__ */ new Map();
19417
+ let phonePairingOwner = null;
19418
+ const broadcastToHost = (message) => {
19419
+ const payload = encodeServerMessage(message);
19420
+ for (const socket of clients) {
19421
+ if (!remoteSockets.has(socket) && socket.readyState === socket.OPEN) socket.send(payload);
19422
+ }
19423
+ };
18146
19424
  const broadcastExcept = (except, message) => {
18147
19425
  const payload = encodeServerMessage(message);
18148
19426
  for (const socket of clients) {
@@ -18193,7 +19471,25 @@ function attachTerminalSocket(options) {
18193
19471
  }
18194
19472
  return false;
18195
19473
  };
18196
- const onRegistryChanged = () => broadcast(terminalListMessage());
19474
+ const draftEntriesOf = (descriptors) => {
19475
+ const entries = [];
19476
+ for (const descriptor of descriptors) {
19477
+ const owner = draftOwnerOf(descriptor);
19478
+ const draft = owner === null ? null : drafts.get(owner);
19479
+ if (draft !== null) entries.push({ terminalId: descriptor.terminalId, draft });
19480
+ }
19481
+ return entries;
19482
+ };
19483
+ const draftTabs = new DraftTabs();
19484
+ draftTabs.update(registry.list());
19485
+ const onRegistryChanged = () => {
19486
+ const terminals = registry.list();
19487
+ broadcast({ type: "terminal.list", terminals, order: registry.getOrder() });
19488
+ const { appeared, moved } = draftTabs.update(terminals);
19489
+ for (const { from, to } of moved) drafts.move(from, to);
19490
+ const entries = draftEntriesOf(appeared);
19491
+ if (entries.length > 0) broadcast({ type: "composer.drafts", drafts: entries });
19492
+ };
18197
19493
  const onRegistryExit = (terminalId, exitCode, signal) => broadcast({ type: "terminal.exit", terminalId, exitCode, signal });
18198
19494
  const onRegistryActivity = (terminalId, activity, offlineReason) => broadcast(
18199
19495
  offlineReason === null ? { type: "terminal.activity", terminalId, activity } : { type: "terminal.activity", terminalId, activity, offlineReason }
@@ -18210,6 +19506,7 @@ function attachTerminalSocket(options) {
18210
19506
  const onConversationMode = (terminalId, mode) => broadcast({ type: "conversation.mode", terminalId, mode });
18211
19507
  const onConversationWaiting = (terminalId, waitingFor) => broadcast({ type: "conversation.waiting", terminalId, waitingFor });
18212
19508
  const onConversationToolCall = (terminalId, open8) => broadcast({ type: "conversation.toolCall", terminalId, open: open8 });
19509
+ const onConversationSubagents = (terminalId, subagents) => broadcast({ type: "conversation.subagents", terminalId, subagents });
18213
19510
  const onConversationPlans = (terminalId, plans) => broadcast({ type: "conversation.plans", terminalId, plans });
18214
19511
  const onConversationTurns = (terminalId, turns) => broadcast({ type: "conversation.turns", terminalId, turns });
18215
19512
  const onConversationParts = (terminalId, updates) => broadcast({ type: "conversation.parts", terminalId, updates });
@@ -18221,7 +19518,9 @@ function attachTerminalSocket(options) {
18221
19518
  };
18222
19519
  const pieceWriter = (socket, terminalId) => (piece) => writeToTerminal(socket, terminalId, piece);
18223
19520
  const dropTab = (terminalId) => {
19521
+ const draftOwner = draftOwnerOf(registry.get(terminalId));
18224
19522
  if (!registry.close(terminalId)) return false;
19523
+ if (draftOwner !== null) drafts.delete(draftOwner);
18225
19524
  conversations.drop(terminalId);
18226
19525
  repos.drop(terminalId);
18227
19526
  memory.drop(terminalId);
@@ -18271,7 +19570,8 @@ function attachTerminalSocket(options) {
18271
19570
  waitingFor: snapshot.waitingFor,
18272
19571
  // El de ahora y no el del snapshot: leer la configuracion es asincrono,
18273
19572
  // y un aviso que salio mientras tanto no puede quedar pisado por uno viejo.
18274
- openToolCall: conversations.isToolCallOpen(terminalId)
19573
+ openToolCall: conversations.isToolCallOpen(terminalId),
19574
+ subagents: conversations.getSubagents(terminalId)
18275
19575
  });
18276
19576
  });
18277
19577
  };
@@ -18286,6 +19586,7 @@ function attachTerminalSocket(options) {
18286
19586
  conversations.on("mode", onConversationMode);
18287
19587
  conversations.on("waiting", onConversationWaiting);
18288
19588
  conversations.on("toolCall", onConversationToolCall);
19589
+ conversations.on("subagents", onConversationSubagents);
18289
19590
  conversations.on("turns", onConversationTurns);
18290
19591
  conversations.on("plans", onConversationPlans);
18291
19592
  conversations.on("parts", onConversationParts);
@@ -18293,6 +19594,23 @@ function attachTerminalSocket(options) {
18293
19594
  memory.on("status", onMemoryStatus);
18294
19595
  const stopAgentChanges = agents.subscribeChanges(() => broadcast(agentsMessage()));
18295
19596
  const stopVaultStatus = vault.onStatus((status) => broadcast({ type: "vault.status", status }));
19597
+ const stopRemoteStatus = remote.onChange(
19598
+ () => broadcastToHost({ type: "remote.status", status: remote.status() })
19599
+ );
19600
+ const stopRemoteRevoked = remote.onRevoked((deviceId) => {
19601
+ for (const [socket, id] of remoteSockets) {
19602
+ if (deviceId === null || id === deviceId) socket.close(REMOTE_REVOKED_CLOSE_CODE, "revoked");
19603
+ }
19604
+ });
19605
+ const sendRemoteError = (socket, error) => {
19606
+ if (error instanceof ServerTextError) {
19607
+ sendError(socket, "remote-failed", error.text);
19608
+ return;
19609
+ }
19610
+ const detail = error instanceof Error ? error.message : String(error);
19611
+ console.warn("[remote] a request failed:", detail);
19612
+ sendError(socket, "remote-failed", serverText("remoteFailed"), detail);
19613
+ };
18296
19614
  const sendVaultError = (socket, error) => {
18297
19615
  if (error instanceof VaultError) {
18298
19616
  sendError(socket, "vault-failed", error.text);
@@ -18310,20 +19628,25 @@ function attachTerminalSocket(options) {
18310
19628
  return;
18311
19629
  }
18312
19630
  if (pathname !== WS_PATH) return;
18313
- const rejection = rejectRequest(request, port, token);
18314
- if (rejection !== null) {
19631
+ const authorization = authorizeRequest(request, port, token, remote.verifier());
19632
+ if (!authorization.ok) {
18315
19633
  socket.write("HTTP/1.1 403 Forbidden\r\nConnection: close\r\n\r\n");
18316
19634
  socket.destroy();
18317
- console.warn(`[security] upgrade rejected on ${WS_PATH}: ${rejection}`);
19635
+ console.warn(`[security] upgrade rejected on ${WS_PATH}: ${authorization.reason}`);
18318
19636
  return;
18319
19637
  }
18320
19638
  wss.handleUpgrade(request, socket, head, (client) => {
18321
- wss.emit("connection", client, request);
19639
+ wss.emit("connection", client, request, authorization.credential);
18322
19640
  });
18323
19641
  };
18324
19642
  httpServer.on("upgrade", onUpgrade);
18325
- wss.on("connection", (socket) => {
19643
+ wss.on("connection", (socket, _request, credential) => {
18326
19644
  clients.add(socket);
19645
+ const deviceId = credential.kind === "device" ? credential.deviceId : null;
19646
+ if (deviceId !== null) {
19647
+ remoteSockets.set(socket, deviceId);
19648
+ remote.noteConnected(deviceId);
19649
+ }
18327
19650
  const listener = (terminalId, chunk) => {
18328
19651
  send(socket, { type: "terminal.output", terminalId, data: chunk });
18329
19652
  };
@@ -18386,7 +19709,8 @@ function attachTerminalSocket(options) {
18386
19709
  defaultAgent: agents.defaultAgent(),
18387
19710
  platform: process.platform,
18388
19711
  defaultCwd,
18389
- shellName: shell?.name ?? null
19712
+ shellName: shell?.name ?? null,
19713
+ remoteClient: deviceId !== null
18390
19714
  });
18391
19715
  send(socket, terminalListMessage());
18392
19716
  for (const entry of registry.activitySnapshot()) {
@@ -18395,7 +19719,9 @@ function attachTerminalSocket(options) {
18395
19719
  send(socket, { type: "index.status", status: index.getStatus() });
18396
19720
  send(socket, { type: "index.projects", projects: index.getProjects(), replace: true });
18397
19721
  send(socket, { type: "notes.list", notes: notes.list() });
19722
+ send(socket, { type: "composer.drafts", drafts: draftEntriesOf(registry.list()) });
18398
19723
  send(socket, { type: "vault.status", status: vault.status() });
19724
+ if (deviceId === null) send(socket, { type: "remote.status", status: remote.status() });
18399
19725
  socket.on("message", (raw) => {
18400
19726
  const text = Array.isArray(raw) ? Buffer.concat(raw).toString("utf8") : Buffer.from(raw).toString("utf8");
18401
19727
  const message = parseClientMessage(text);
@@ -18405,9 +19731,16 @@ function attachTerminalSocket(options) {
18405
19731
  return;
18406
19732
  }
18407
19733
  if (message.type !== "input") debugLog("socket", `received ${message.type}`);
19734
+ const refusal = remoteRefusal(message.type, deviceId !== null);
19735
+ if (refusal !== null) {
19736
+ sendError(socket, "remote-refused", refusal);
19737
+ return;
19738
+ }
18408
19739
  switch (message.type) {
18409
19740
  case "input":
18410
- writeToTerminal(socket, message.terminalId, message.data);
19741
+ if (!registry.write(message.terminalId, message.data) && registry.get(message.terminalId) === null) {
19742
+ sendError(socket, "unknown-terminal", serverText("terminalGone"));
19743
+ }
18411
19744
  break;
18412
19745
  /*
18413
19746
  Un mensaje del cuadro de escritura.
@@ -18423,6 +19756,14 @@ function attachTerminalSocket(options) {
18423
19756
  */
18424
19757
  case "agent.submit": {
18425
19758
  const { terminalId, text: text2, images } = message;
19759
+ let settled = false;
19760
+ const settle = (delivered) => {
19761
+ if (settled || message.requestId === void 0) return;
19762
+ settled = true;
19763
+ send(socket, { type: "agent.submitted", terminalId, requestId: message.requestId, delivered });
19764
+ };
19765
+ const submittedOwner = draftOwnerOf(registry.get(terminalId));
19766
+ if (submittedOwner !== null) drafts.delete(submittedOwner);
18426
19767
  const files = message.files ?? [];
18427
19768
  if (files.length > MAX_FILES_PER_SUBMIT) {
18428
19769
  sendError(
@@ -18430,6 +19771,7 @@ function attachTerminalSocket(options) {
18430
19771
  "submit-failed",
18431
19772
  serverText("filesPerMessage", { max: MAX_FILES_PER_SUBMIT })
18432
19773
  );
19774
+ settle(false);
18433
19775
  break;
18434
19776
  }
18435
19777
  if (images.length > MAX_IMAGES_PER_SUBMIT) {
@@ -18438,9 +19780,11 @@ function attachTerminalSocket(options) {
18438
19780
  "submit-failed",
18439
19781
  serverText("imagesPerMessage", { max: MAX_IMAGES_PER_SUBMIT })
18440
19782
  );
19783
+ settle(false);
18441
19784
  break;
18442
19785
  }
18443
19786
  if (images.length > 0 && imageStyleFor(socket, terminalId, serverText("imagesUnsupported")) === null) {
19787
+ settle(false);
18444
19788
  break;
18445
19789
  }
18446
19790
  const submitting = adapterOf(terminalId);
@@ -18450,7 +19794,7 @@ function attachTerminalSocket(options) {
18450
19794
  if (submitting !== null) {
18451
19795
  const launchedAt = registry.launchedAtOf(terminalId);
18452
19796
  const blind = isBlind();
18453
- const refusal = submitRefusal({
19797
+ const refusal2 = submitRefusal({
18454
19798
  label: submitting.label,
18455
19799
  approvesPendingOnCycle: guardsPending,
18456
19800
  waitingFor: conversations.getWaitingFor(terminalId),
@@ -18459,8 +19803,9 @@ function attachTerminalSocket(options) {
18459
19803
  blind,
18460
19804
  openToolCall: blind && launchedAt !== null && conversations.hasOpenToolCall(terminalId, launchedAt)
18461
19805
  });
18462
- if (refusal !== null) {
18463
- sendError(socket, "submit-failed", refusal);
19806
+ if (refusal2 !== null) {
19807
+ sendError(socket, "submit-failed", refusal2);
19808
+ settle(false);
18464
19809
  break;
18465
19810
  }
18466
19811
  }
@@ -18528,6 +19873,7 @@ function attachTerminalSocket(options) {
18528
19873
  approvalGuard,
18529
19874
  { startAfterMs }
18530
19875
  );
19876
+ settle(outcome === "written");
18531
19877
  if (outcome === "interrupted") interrupted();
18532
19878
  if (outcome === "blocked") {
18533
19879
  const waitingNow = blocksWhileWaiting && registry.activityOf(terminalId) === "waiting";
@@ -18539,7 +19885,7 @@ function attachTerminalSocket(options) {
18539
19885
  }
18540
19886
  },
18541
19887
  { onDropped: interrupted }
18542
- );
19888
+ ).then(() => settle(false));
18543
19889
  break;
18544
19890
  }
18545
19891
  /*
@@ -18857,7 +20203,8 @@ function attachTerminalSocket(options) {
18857
20203
  permissionMode: snapshot.permissionMode,
18858
20204
  defaults: await defaultsFor(message.terminalId),
18859
20205
  waitingFor: snapshot.waitingFor,
18860
- openToolCall: conversations.isToolCallOpen(message.terminalId)
20206
+ openToolCall: conversations.isToolCallOpen(message.terminalId),
20207
+ subagents: conversations.getSubagents(message.terminalId)
18861
20208
  });
18862
20209
  if (snapshot.plans.length > 0) {
18863
20210
  send(socket, {
@@ -19131,6 +20478,19 @@ function attachTerminalSocket(options) {
19131
20478
  }
19132
20479
  })();
19133
20480
  break;
20481
+ /*
20482
+ El borrador del cuadro de una pestana, despues de una pausa del
20483
+ teclado (§6.29). No se contesta. Una consola, o una pestana cuya CLI
20484
+ todavia no dijo su conversacion, no tiene donde guardarlo: lo escrito
20485
+ sigue en el cuadro, y se guarda con la pausa siguiente a que aparezca.
20486
+ */
20487
+ case "composer.draft": {
20488
+ const owner = draftOwnerOf(registry.get(message.terminalId));
20489
+ if (owner === null) break;
20490
+ const outcome = drafts.save(owner, message.draft);
20491
+ debugLog("drafts", `${message.terminalId.slice(0, 8)}: ${outcome}`);
20492
+ break;
20493
+ }
19134
20494
  /*
19135
20495
  Notas sueltas. No van dirigidas a ninguna pestana.
19136
20496
 
@@ -19415,9 +20775,70 @@ function attachTerminalSocket(options) {
19415
20775
  case "search.cancel":
19416
20776
  globalSearch.cancel(message.searchId);
19417
20777
  break;
20778
+ /*
20779
+ El acceso remoto (hito 37). Hasta aca solo llega una ventana del
20780
+ anfitrion: a un equipo remoto lo freno `remoteRefusal`. El estado
20781
+ nuevo sale a las ventanas del anfitrion por `remote.onChange`; aca
20782
+ solo se contesta lo que falla, y el codigo, que va **solo** a quien lo
20783
+ pidio.
20784
+ */
20785
+ case "remote.configure": {
20786
+ const patch = {};
20787
+ if (message.enabled !== void 0) patch.enabled = message.enabled;
20788
+ if (message.port !== void 0) patch.port = message.port;
20789
+ void remote.configure(patch).catch((error) => sendRemoteError(socket, error));
20790
+ break;
20791
+ }
20792
+ case "remote.pairing.start":
20793
+ try {
20794
+ send(socket, { type: "remote.pairing.code", pairing: remote.startPairing() });
20795
+ } catch (error) {
20796
+ sendRemoteError(socket, error);
20797
+ }
20798
+ break;
20799
+ case "remote.pairing.cancel":
20800
+ remote.cancelPairing();
20801
+ break;
20802
+ /*
20803
+ El telefono (hito 38): la respuesta lleva su llave privada dentro del
20804
+ texto del QR, asi que va solo a esta ventana, y si esta ventana se
20805
+ cierra sin cancelar, la llave se olvida igual (`teardown`).
20806
+ */
20807
+ case "remote.phone.start":
20808
+ phonePairingOwner = socket;
20809
+ void remote.startPhonePairing().then((pairing) => send(socket, { type: "remote.phone.pairing", pairing })).catch((error) => sendRemoteError(socket, error));
20810
+ break;
20811
+ case "remote.phone.cancel":
20812
+ if (phonePairingOwner === socket) phonePairingOwner = null;
20813
+ remote.cancelPhonePairing();
20814
+ break;
20815
+ case "remote.device.rename":
20816
+ void remote.renameDevice(message.deviceId, message.label).then((renamed) => {
20817
+ if (!renamed) sendError(socket, "remote-failed", serverText("remoteDeviceGone"));
20818
+ }).catch((error) => sendRemoteError(socket, error));
20819
+ break;
20820
+ case "remote.device.revoke":
20821
+ void remote.revokeDevice(message.deviceId).then((revoked) => {
20822
+ if (!revoked) sendError(socket, "remote-failed", serverText("remoteDeviceGone"));
20823
+ }).catch((error) => sendRemoteError(socket, error));
20824
+ break;
20825
+ /*
20826
+ "Olvidar este equipo" del teléfono: se revoca el equipo con el que
20827
+ entró esta ventana, y ningún otro. Revocar cierra sus ventanas con
20828
+ 4001, esta incluida: ése es el acuse. Una ventana del anfitrión no es
20829
+ un equipo, y no tiene nada que olvidar.
20830
+ */
20831
+ case "remote.device.forgetSelf":
20832
+ if (deviceId === null) break;
20833
+ void remote.revokeDevice(deviceId).catch((error) => sendRemoteError(socket, error));
20834
+ break;
19418
20835
  }
19419
20836
  });
19420
20837
  const teardown = () => {
20838
+ if (phonePairingOwner === socket) {
20839
+ phonePairingOwner = null;
20840
+ remote.cancelPhonePairing();
20841
+ }
19421
20842
  globalSearch.cancel();
19422
20843
  pickers.closeAll();
19423
20844
  registry.detachAll(listener);
@@ -19428,6 +20849,7 @@ function attachTerminalSocket(options) {
19428
20849
  for (const terminalId of memorySubscriptions) memory.unsubscribe(terminalId);
19429
20850
  memorySubscriptions.clear();
19430
20851
  clients.delete(socket);
20852
+ if (deviceId !== null && remoteSockets.delete(socket)) remote.noteDisconnected(deviceId);
19431
20853
  };
19432
20854
  socket.on("close", teardown);
19433
20855
  socket.on("error", teardown);
@@ -19448,22 +20870,25 @@ function attachTerminalSocket(options) {
19448
20870
  conversations.off("mode", onConversationMode);
19449
20871
  conversations.off("waiting", onConversationWaiting);
19450
20872
  conversations.off("toolCall", onConversationToolCall);
20873
+ conversations.off("subagents", onConversationSubagents);
19451
20874
  repos.off("status", onGitStatus);
19452
20875
  memory.off("status", onMemoryStatus);
19453
20876
  stopAgentChanges();
19454
20877
  stopVaultStatus();
20878
+ stopRemoteStatus();
20879
+ stopRemoteRevoked();
19455
20880
  for (const client of wss.clients) client.terminate();
19456
20881
  wss.close();
19457
20882
  };
19458
20883
  }
19459
20884
 
19460
20885
  // 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";
20886
+ import { open as open7, readdir as readdir16, stat as stat29 } from "node:fs/promises";
20887
+ import path48 from "node:path";
19463
20888
  import { createInterface as createInterface2 } from "node:readline";
19464
20889
  var HEADER_MAX_BYTES = 64 * 1024;
19465
20890
  var LOAD_CONCURRENCY = 16;
19466
- var keyOf = (agent, sessionId) => `${agent}/${sessionId}`;
20891
+ var keyOf2 = (agent, sessionId) => `${agent}/${sessionId}`;
19467
20892
  function isSessionAgentId2(value) {
19468
20893
  return SESSION_AGENT_IDS.includes(value);
19469
20894
  }
@@ -19479,7 +20904,7 @@ async function assetBytesOf(dir, agent, sessionId) {
19479
20904
  for (const name of names) {
19480
20905
  if (!VAULT_ASSET_NAME_PATTERN.test(name)) continue;
19481
20906
  try {
19482
- const info = await stat27(path46.join(folder, name));
20907
+ const info = await stat29(path48.join(folder, name));
19483
20908
  if (info.isFile()) total += info.size;
19484
20909
  } catch {
19485
20910
  }
@@ -19490,7 +20915,7 @@ async function readEntry(dir, agent, sessionId) {
19490
20915
  const file = sessionFile(dir, agent, sessionId);
19491
20916
  let sizeBytes;
19492
20917
  try {
19493
- const info = await stat27(file);
20918
+ const info = await stat29(file);
19494
20919
  if (!info.isFile()) return { kind: "absent" };
19495
20920
  sizeBytes = info.size;
19496
20921
  } catch {
@@ -19571,8 +20996,8 @@ var VaultCatalog = class {
19571
20996
  const foreign = /* @__PURE__ */ new Set();
19572
20997
  keys.forEach((key, position) => {
19573
20998
  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));
20999
+ if (read?.kind === "listed") entries.set(keyOf2(key.agent, key.sessionId), read.entry);
21000
+ else if (read?.kind === "foreign") foreign.add(keyOf2(key.agent, key.sessionId));
19576
21001
  });
19577
21002
  this.entries = entries;
19578
21003
  this.foreign = foreign;
@@ -19586,7 +21011,7 @@ var VaultCatalog = class {
19586
21011
  const generation = this.generation;
19587
21012
  const read = await readEntry(dir, agent, sessionId);
19588
21013
  if (generation !== this.generation) return;
19589
- const key = keyOf(agent, sessionId);
21014
+ const key = keyOf2(agent, sessionId);
19590
21015
  const before = JSON.stringify(this.entries.get(key) ?? null);
19591
21016
  const wasForeign = this.foreign.has(key);
19592
21017
  this.entries.delete(key);
@@ -19616,11 +21041,11 @@ var VaultCatalog = class {
19616
21041
  return this.summaryCache;
19617
21042
  }
19618
21043
  header(agent, sessionId) {
19619
- return this.entries.get(keyOf(agent, sessionId))?.header ?? null;
21044
+ return this.entries.get(keyOf2(agent, sessionId))?.header ?? null;
19620
21045
  }
19621
21046
  /** true si en disco hay una copia de esa sesion con un formato que esta app no conoce. */
19622
21047
  hasForeignFormat(agent, sessionId) {
19623
- return this.foreign.has(keyOf(agent, sessionId));
21048
+ return this.foreign.has(keyOf2(agent, sessionId));
19624
21049
  }
19625
21050
  /**
19626
21051
  * El cuerpo de una sesion listada, linea por linea: eventos y documentos.
@@ -19643,7 +21068,7 @@ var VaultCatalog = class {
19643
21068
  */
19644
21069
  async *bodyLines(agent, sessionId) {
19645
21070
  const dir = this.dir;
19646
- if (dir === null || !this.entries.has(keyOf(agent, sessionId))) return;
21071
+ if (dir === null || !this.entries.has(keyOf2(agent, sessionId))) return;
19647
21072
  const handle = await open7(sessionFile(dir, agent, sessionId), "r").catch(() => null);
19648
21073
  if (handle === null) return;
19649
21074
  const input = handle.createReadStream({ encoding: "utf8" });
@@ -19699,38 +21124,77 @@ var VaultCatalog = class {
19699
21124
  };
19700
21125
 
19701
21126
  // packages/server/src/index.ts
19702
- var serverDir = path47.dirname(fileURLToPath2(import.meta.url));
21127
+ var serverDir = path49.dirname(fileURLToPath2(import.meta.url));
19703
21128
  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, "../../..");
21129
+ var webRoot = isPackaged ? serverDir : path49.resolve(serverDir, "../../web");
21130
+ var webDist = isPackaged ? path49.join(serverDir, "web") : path49.join(webRoot, "dist");
21131
+ var repoRoot = path49.resolve(serverDir, "../../..");
19707
21132
  var isProduction = isPackaged || process.argv.includes("--prod");
19708
21133
  function resolveDefaultCwd() {
19709
21134
  const override = process.env["AGENT_WORKBENCH_CWD"];
19710
- if (override !== void 0 && override.length > 0) return path47.resolve(override);
21135
+ if (override !== void 0 && override.length > 0) return path49.resolve(override);
19711
21136
  if (isPackaged) return process.cwd();
19712
21137
  return repoRoot;
19713
21138
  }
19714
21139
  var ORIGIN_REFUSED_TEXT = "Origin not allowed.\nOrigen no permitido.";
19715
21140
  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
21141
  var NOT_FOUND_TEXT = "Not found.\nNo encontrado.";
19717
- function createAuthMiddleware(port, token) {
21142
+ 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".';
21143
+ var PAIRING_REFUSED_TEXT = `That pairing code isn't valid or has expired. Ask for a new one in "Remote access".
21144
+ Ese c\xF3digo para emparejar no es v\xE1lido o venci\xF3. Pide uno nuevo en "Acceso remoto".`;
21145
+ function createAuthMiddleware(port, token, remote) {
19718
21146
  return (request, response, next) => {
19719
- if (!hasValidHost(request, port) || !hasValidOrigin(request, port)) {
19720
- response.status(403).type("text/plain").send(ORIGIN_REFUSED_TEXT);
21147
+ const authorization = authorizeRequest(request, port, token, remote.verifier());
21148
+ if (authorization.ok) {
21149
+ const { credential } = authorization;
21150
+ if (credential.kind === "session" && credential.source === "query") {
21151
+ response.setHeader("Set-Cookie", buildTokenCookie(token));
21152
+ }
21153
+ if (credential.kind === "device" && request.method === "GET" && request.path === "/") {
21154
+ response.setHeader("Set-Cookie", buildDeviceCookie(credential.credential));
21155
+ }
21156
+ next();
19721
21157
  return;
19722
21158
  }
19723
- const match = matchToken(request, token);
19724
- if (match === null) {
19725
- response.status(401).type("text/plain").send(TOKEN_MISSING_TEXT);
21159
+ if (authorization.reason !== "bad-token") {
21160
+ response.status(403).type("text/plain").send(ORIGIN_REFUSED_TEXT);
19726
21161
  return;
19727
21162
  }
19728
- if (match.source === "query") {
19729
- response.setHeader("Set-Cookie", buildTokenCookie(token));
21163
+ const code = request.method === "GET" && request.path === "/" ? readPairingCode(request) : null;
21164
+ if (code !== null && remote.verifier() !== null) {
21165
+ response.setHeader("Cache-Control", "no-store");
21166
+ void remote.redeemPairing(code, request.headers["user-agent"]).catch(() => null).then((redeemed) => {
21167
+ if (redeemed === null) {
21168
+ response.status(401).type("text/plain").send(PAIRING_REFUSED_TEXT);
21169
+ return;
21170
+ }
21171
+ response.setHeader("Set-Cookie", buildDeviceCookie(redeemed.credential));
21172
+ response.redirect(303, "/");
21173
+ });
21174
+ return;
19730
21175
  }
19731
- next();
21176
+ response.status(401).type("text/plain").send(remote.verifier() === null ? TOKEN_MISSING_TEXT : TOKEN_MISSING_TEXT + PAIRING_HINT_TEXT);
19732
21177
  };
19733
21178
  }
21179
+ function isPortUnavailable(error) {
21180
+ const code = error?.code;
21181
+ return code === "EADDRINUSE" || code === "EACCES";
21182
+ }
21183
+ function listenOnLoopback(httpServer, port) {
21184
+ return new Promise((resolve, reject) => {
21185
+ const onError = (error) => {
21186
+ httpServer.off("listening", onListening);
21187
+ reject(error);
21188
+ };
21189
+ const onListening = () => {
21190
+ httpServer.off("error", onError);
21191
+ resolve();
21192
+ };
21193
+ httpServer.once("error", onError);
21194
+ httpServer.once("listening", onListening);
21195
+ httpServer.listen(port, LOOPBACK_HOST);
21196
+ });
21197
+ }
19734
21198
  async function mountDevUi(app, httpServer) {
19735
21199
  const { createServer: createViteServer } = await import("vite");
19736
21200
  const vite = await createViteServer({
@@ -19743,7 +21207,7 @@ async function mountDevUi(app, httpServer) {
19743
21207
  }
19744
21208
  var LOOKS_LIKE_FILE = /\.[a-zA-Z0-9]{1,8}$/;
19745
21209
  function mountBuiltUi(app) {
19746
- const indexHtml = path47.join(webDist, "index.html");
21210
+ const indexHtml = path49.join(webDist, "index.html");
19747
21211
  if (!existsSync4(indexHtml)) {
19748
21212
  throw new Error(
19749
21213
  `There's no compiled interface in ${webDist}.
@@ -19760,7 +21224,7 @@ Run "pnpm build" before "pnpm start", or use "pnpm dev".`
19760
21224
  });
19761
21225
  return () => Promise.resolve();
19762
21226
  }
19763
- function describeStartup(url, cwd, agents, shell) {
21227
+ function describeStartup(url, cwd, agents, shell, remoteLine) {
19764
21228
  const agentList = agents.list();
19765
21229
  const line = "-".repeat(64);
19766
21230
  console.log(`
@@ -19782,6 +21246,9 @@ ${line}`);
19782
21246
  }));
19783
21247
  for (const agentLine of startupAgentLines(startupAgents)) console.log(agentLine);
19784
21248
  console.log(` Console ${shell === null ? "not found" : shell.file}`);
21249
+ if (remoteLine !== null) console.log(remoteLine);
21250
+ const colored = process.stdout.isTTY === true && process.env["NO_COLOR"] === void 0;
21251
+ console.log(colored ? supportStartupLineColored() : supportStartupLine());
19785
21252
  if (agentList.some((info) => info.environmentNotice === "child-session-marker")) {
19786
21253
  console.log("");
19787
21254
  console.log(" Note: this process inherited CLAUDE_CODE_CHILD_SESSION, which turns off");
@@ -19818,6 +21285,8 @@ async function main() {
19818
21285
  );
19819
21286
  const notes = new NotesStore();
19820
21287
  await notes.load();
21288
+ const drafts = new DraftStore();
21289
+ await drafts.load();
19821
21290
  const conversations = new ConversationHub(registry, agents);
19822
21291
  const repos = new RepoHub(registry);
19823
21292
  const memory = new MemoryHub(registry);
@@ -19825,16 +21294,31 @@ async function main() {
19825
21294
  const app = express();
19826
21295
  app.disable("x-powered-by");
19827
21296
  const httpServer = createServer(app);
19828
- await new Promise((resolve, reject) => {
19829
- httpServer.once("error", reject);
19830
- httpServer.listen(0, LOOPBACK_HOST, resolve);
19831
- });
21297
+ const remoteSettings = settings2.get().remote;
21298
+ let listeningFixed = false;
21299
+ let fixedPortBusy = false;
21300
+ if (remoteSettings.enabled) {
21301
+ try {
21302
+ await listenOnLoopback(httpServer, remoteSettings.port);
21303
+ listeningFixed = true;
21304
+ } catch (error) {
21305
+ if (!isPortUnavailable(error)) throw error;
21306
+ fixedPortBusy = true;
21307
+ }
21308
+ }
21309
+ if (!listeningFixed) await listenOnLoopback(httpServer, 0);
19832
21310
  const address = httpServer.address();
19833
21311
  if (address === null || typeof address === "string") {
19834
21312
  throw new Error("Couldn't determine the assigned port.");
19835
21313
  }
19836
21314
  const { port } = address;
19837
- app.use(createAuthMiddleware(port, token));
21315
+ const remote = new RemoteAccessService({
21316
+ settings: settings2,
21317
+ store: new RemoteDevicesStore(),
21318
+ listening: { port, fixed: listeningFixed, fixedPortBusy }
21319
+ });
21320
+ await remote.load();
21321
+ app.use(createAuthMiddleware(port, token, remote));
19838
21322
  const closeUi = isProduction ? mountBuiltUi(app) : await mountDevUi(app, httpServer);
19839
21323
  const detachSocket = attachTerminalSocket({
19840
21324
  pasteStore,
@@ -19846,18 +21330,21 @@ async function main() {
19846
21330
  index,
19847
21331
  archived,
19848
21332
  notes,
21333
+ drafts,
19849
21334
  conversations,
19850
21335
  repos,
19851
21336
  memory,
19852
21337
  agents,
19853
21338
  defaultCwd,
19854
- vault
21339
+ vault,
21340
+ remote
19855
21341
  });
19856
21342
  void pasteStore.purgeStale();
19857
21343
  void index.start();
19858
21344
  const stopWatching = watchSessions(index, conversations, agents);
19859
21345
  if (process.env["AGENT_WORKBENCH_NO_RESTORE"] !== "1") {
19860
21346
  void store.load().then((state) => {
21347
+ drafts.retainOnly(workspaceDraftOwners(state));
19861
21348
  if (state.tabs.length === 0 && state.foreignTabs.length === 0) return;
19862
21349
  if (agents.anyAvailable()) {
19863
21350
  console.log(`Restoring ${state.tabs.length} tab(s) from the previous run...`);
@@ -19866,7 +21353,14 @@ async function main() {
19866
21353
  });
19867
21354
  }
19868
21355
  const url = `http://${LOOPBACK_HOST}:${port}/?${TOKEN_QUERY_PARAM}=${token}`;
19869
- describeStartup(url, defaultCwd, agents, shell);
21356
+ const remoteStatus = remote.status();
21357
+ describeStartup(
21358
+ url,
21359
+ defaultCwd,
21360
+ agents,
21361
+ shell,
21362
+ remoteAccessStartupLine(remoteStatus.state, remoteStatus.port, remote.pairedDevices)
21363
+ );
19870
21364
  if (process.env["AGENT_WORKBENCH_NO_OPEN"] !== "1") openBrowser(url);
19871
21365
  let shuttingDown = false;
19872
21366
  const shutdown = () => {
@@ -19881,7 +21375,8 @@ async function main() {
19881
21375
  repos.disposeAll();
19882
21376
  void memory.disposeAll();
19883
21377
  registry.disposeAll();
19884
- void Promise.all([store.flush(), archived.flush(), notes.flush(), agentsDisposed]).catch(() => void 0).then(() => closeUi()).catch(() => void 0).finally(() => {
21378
+ remote.dispose();
21379
+ void Promise.all([store.flush(), archived.flush(), notes.flush(), drafts.flush(), remote.flush(), agentsDisposed]).catch(() => void 0).then(() => closeUi()).catch(() => void 0).finally(() => {
19885
21380
  httpServer.close(() => process.exit(0));
19886
21381
  setTimeout(() => process.exit(0), 2e3).unref();
19887
21382
  });