agent-workbench 0.5.4 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/server.js CHANGED
@@ -3,7 +3,7 @@
3
3
  // packages/server/src/index.ts
4
4
  import { existsSync as existsSync4 } from "node:fs";
5
5
  import { createServer } from "node:http";
6
- import path51 from "node:path";
6
+ import path59 from "node:path";
7
7
  import { fileURLToPath as fileURLToPath2 } from "node:url";
8
8
  import express from "express";
9
9
 
@@ -93,7 +93,7 @@ var ServerTextError = class extends Error {
93
93
  };
94
94
 
95
95
  // packages/shared/src/agents.ts
96
- var AGENT_IDS = ["claude-code", "codex", "opencode", "antigravity"];
96
+ var AGENT_IDS = ["claude-code", "codex", "opencode", "antigravity", "pi"];
97
97
  var IMPORTED_AGENT_IDS = ["gemini-cli", "antigravity-ide"];
98
98
  var SESSION_AGENT_IDS = [...AGENT_IDS, ...IMPORTED_AGENT_IDS];
99
99
  var IMPORTED_AGENT_LABELS = {
@@ -204,7 +204,7 @@ function fitWindowToObserved(window, observedTokens) {
204
204
  if (window === null || observedTokens <= window) {
205
205
  return { window, estimated: false };
206
206
  }
207
- const next = WINDOW_LADDER.find((step) => step > observedTokens);
207
+ const next = WINDOW_LADDER.find((step2) => step2 > observedTokens);
208
208
  return next === void 0 ? (
209
209
  // Mas grande que el mayor escalon conocido: el limite real se nos escapa,
210
210
  // y decirlo es mejor que dibujar una barra contra un numero inventado.
@@ -411,7 +411,8 @@ var MEMORY_AGENT_LABELS = {
411
411
  "claude-code": "Claude Code",
412
412
  codex: "Codex",
413
413
  antigravity: "Antigravity",
414
- opencode: "OpenCode"
414
+ opencode: "OpenCode",
415
+ pi: "Pi"
415
416
  };
416
417
  var MEMORY_BLOCK_BODY = [
417
418
  "## Project memory",
@@ -646,12 +647,12 @@ function parseVaultBodyLine(value) {
646
647
  const record = asRecord(value);
647
648
  if (record === null) return null;
648
649
  if (record["kind"] === "event") {
649
- const event = parseConversationEvent(record["event"]);
650
+ const event2 = parseConversationEvent(record["event"]);
650
651
  const images = asArrayOf(record["images"], parseVaultImageRef);
651
- if (event === null || images === null) return null;
652
- const imageParts = event.parts.filter((part) => part.kind === "image").length;
652
+ if (event2 === null || images === null) return null;
653
+ const imageParts = event2.parts.filter((part) => part.kind === "image").length;
653
654
  if (images.length !== imageParts) return null;
654
- return { kind: "event", event, images };
655
+ return { kind: "event", event: event2, images };
655
656
  }
656
657
  if (record["kind"] === "document") {
657
658
  const name = asNonEmptyString(record["name"]);
@@ -721,6 +722,8 @@ var MAX_SUBMIT_IMAGE_BYTES = 12 * 1024 * 1024;
721
722
  var MAX_SUBMIT_IMAGES = 8;
722
723
  var MAX_SUBMIT_FILE_BYTES = 10 * 1024 * 1024;
723
724
  var MAX_SUBMIT_FILES = 4;
725
+ var FILES_CHECK_MAX_PATHS = 100;
726
+ var FILES_CHECK_MAX_PATH_CHARS = 512;
724
727
  var CONTINUE_LABEL_MAX_CHARS = 120;
725
728
  function parseSubmitImage(value) {
726
729
  const record = asRecord(value);
@@ -774,6 +777,7 @@ function parseClientMessage(raw) {
774
777
  if (record["send"] === false) message.send = false;
775
778
  const requestId = asNonEmptyString(record["requestId"]);
776
779
  if (requestId !== null) message.requestId = requestId;
780
+ if (record["keepDraft"] === true) message.keepDraft = true;
777
781
  return message;
778
782
  }
779
783
  case "agent.mode": {
@@ -916,6 +920,20 @@ function parseClientMessage(raw) {
916
920
  const readPath = asNonEmptyString(record["path"]);
917
921
  return terminalId === null || readPath === null ? null : { type: "files.read", terminalId, path: readPath };
918
922
  }
923
+ case "files.check": {
924
+ const terminalId = asNonEmptyString(record["terminalId"]);
925
+ const requestId = asNonEmptyString(record["requestId"]);
926
+ const rawPaths = record["paths"];
927
+ if (terminalId === null || requestId === null || !Array.isArray(rawPaths)) return null;
928
+ const paths = [];
929
+ for (const candidate of rawPaths) {
930
+ if (paths.length >= FILES_CHECK_MAX_PATHS) break;
931
+ if (typeof candidate === "string" && candidate.length > 0 && candidate.length <= FILES_CHECK_MAX_PATH_CHARS) {
932
+ paths.push(candidate);
933
+ }
934
+ }
935
+ return { type: "files.check", terminalId, requestId, paths };
936
+ }
919
937
  case "plans.read": {
920
938
  const terminalId = asNonEmptyString(record["terminalId"]);
921
939
  const fileName = asNonEmptyString(record["fileName"]);
@@ -1379,12 +1397,17 @@ async function readVersion(file, prefixArgs) {
1379
1397
  return null;
1380
1398
  }
1381
1399
  }
1400
+ function launcherFor(resolvedPath) {
1401
+ const useInterpreter = needsCommandInterpreter(resolvedPath);
1402
+ return {
1403
+ file: useInterpreter ? process.env["ComSpec"] ?? "cmd.exe" : resolvedPath,
1404
+ prefixArgs: useInterpreter ? ["/c", resolvedPath] : []
1405
+ };
1406
+ }
1382
1407
  async function locateCommand(command) {
1383
1408
  const resolvedPath = await findInPath(command);
1384
1409
  if (resolvedPath === null) return null;
1385
- const useInterpreter = needsCommandInterpreter(resolvedPath);
1386
- const file = useInterpreter ? process.env["ComSpec"] ?? "cmd.exe" : resolvedPath;
1387
- const prefixArgs = useInterpreter ? ["/c", resolvedPath] : [];
1410
+ const { file, prefixArgs } = launcherFor(resolvedPath);
1388
1411
  return {
1389
1412
  resolvedPath,
1390
1413
  file,
@@ -2554,7 +2577,7 @@ var JsonlFollower = class {
2554
2577
  return this.state;
2555
2578
  }
2556
2579
  find(eventId) {
2557
- return this.events.find((event) => event.eventId === eventId);
2580
+ return this.events.find((event2) => event2.eventId === eventId);
2558
2581
  }
2559
2582
  eventCount() {
2560
2583
  return this.events.length;
@@ -2569,7 +2592,7 @@ var JsonlFollower = class {
2569
2592
  }
2570
2593
  /** Tramo anterior a un evento ya entregado. Vacio si ese id ya no esta. */
2571
2594
  getPageBefore(beforeEventId, limit) {
2572
- const index = this.events.findIndex((event) => event.eventId === beforeEventId);
2595
+ const index = this.events.findIndex((event2) => event2.eventId === beforeEventId);
2573
2596
  if (index <= 0) return { events: [], hasMore: false };
2574
2597
  const start = Math.max(0, index - limit);
2575
2598
  return {
@@ -2653,8 +2676,8 @@ var JsonlFollower = class {
2653
2676
  while (newlineIndex !== -1) {
2654
2677
  const line = combined.subarray(start, newlineIndex).toString("utf8");
2655
2678
  start = newlineIndex + 1;
2656
- const event = this.consumeLine(line);
2657
- if (event !== null) added.push(event);
2679
+ const event2 = this.consumeLine(line);
2680
+ if (event2 !== null) added.push(event2);
2658
2681
  newlineIndex = combined.indexOf(NEWLINE, start);
2659
2682
  }
2660
2683
  this.pending = Buffer.from(combined.subarray(start));
@@ -2676,14 +2699,14 @@ var JsonlFollower = class {
2676
2699
  if (line.length === 0) return null;
2677
2700
  const record = parseJsonlLine(line);
2678
2701
  if (record === null) return null;
2679
- const event = this.sink.consume(record, this.lineNumber, this);
2680
- if (event === null) return null;
2681
- this.events.push(event);
2702
+ const event2 = this.sink.consume(record, this.lineNumber, this);
2703
+ if (event2 === null) return null;
2704
+ this.events.push(event2);
2682
2705
  if (this.events.length > this.maxEvents) {
2683
2706
  this.events.shift();
2684
2707
  this.dropped += 1;
2685
2708
  }
2686
- return event;
2709
+ return event2;
2687
2710
  }
2688
2711
  };
2689
2712
 
@@ -2705,9 +2728,9 @@ function cut(raw, limit) {
2705
2728
  if (raw.length <= limit) return { text: raw, truncated: false };
2706
2729
  return { text: raw.slice(0, limit), truncated: true };
2707
2730
  }
2708
- function limitEvent(event, limits) {
2731
+ function limitEvent(event2, limits) {
2709
2732
  let changed = false;
2710
- const parts = event.parts.map((part) => {
2733
+ const parts = event2.parts.map((part) => {
2711
2734
  if (part.kind === "text" && part.text.length > limits.textMaxChars) {
2712
2735
  changed = true;
2713
2736
  return { ...part, text: part.text.slice(0, limits.textMaxChars), truncated: true };
@@ -2722,7 +2745,7 @@ function limitEvent(event, limits) {
2722
2745
  }
2723
2746
  return part;
2724
2747
  });
2725
- return changed ? { ...event, parts } : event;
2748
+ return changed ? { ...event2, parts } : event2;
2726
2749
  }
2727
2750
 
2728
2751
  // packages/server/src/agents/antigravity/model-label.ts
@@ -2751,12 +2774,12 @@ function blockPattern(name) {
2751
2774
  return new RegExp(`<${name}>([\\s\\S]*?)</${name}>`, "g");
2752
2775
  }
2753
2776
  function unwrapUserInput(content) {
2754
- const open8 = content.indexOf(REQUEST_OPEN);
2755
- const close = open8 === -1 ? -1 : content.indexOf(REQUEST_CLOSE, open8 + REQUEST_OPEN.length);
2777
+ const open9 = content.indexOf(REQUEST_OPEN);
2778
+ const close = open9 === -1 ? -1 : content.indexOf(REQUEST_CLOSE, open9 + REQUEST_OPEN.length);
2756
2779
  let text;
2757
2780
  let rest;
2758
- if (open8 !== -1 && close !== -1) {
2759
- text = content.slice(open8 + REQUEST_OPEN.length, close).trim();
2781
+ if (open9 !== -1 && close !== -1) {
2782
+ text = content.slice(open9 + REQUEST_OPEN.length, close).trim();
2760
2783
  rest = content.slice(close + REQUEST_CLOSE.length);
2761
2784
  } else {
2762
2785
  text = content.replace(blockPattern("ADDITIONAL_METADATA"), "").replace(blockPattern("USER_SETTINGS_CHANGE"), "").trim();
@@ -3068,7 +3091,7 @@ var AntigravitySessionFollower = class {
3068
3091
  if (jsonl !== null && mapper !== null) {
3069
3092
  mapper.takeParts(/* @__PURE__ */ new Set());
3070
3093
  const { reset, added } = await jsonl.poll();
3071
- const fresh = new Set(added.map((event) => event.eventId));
3094
+ const fresh = new Set(added.map((event2) => event2.eventId));
3072
3095
  result = { reset, added, turns: [], plans: [], parts: reset ? [] : mapper.takeParts(fresh) };
3073
3096
  if (jsonl.getState() === "waiting") {
3074
3097
  this.jsonl = null;
@@ -3261,7 +3284,7 @@ function createAntigravityHistory(deps = {}) {
3261
3284
  const compact = await stamp(paths.compact);
3262
3285
  return compact === null ? null : { file: paths.compact, info: compact };
3263
3286
  };
3264
- const itemOf3 = async (ref) => {
3287
+ const itemOf4 = async (ref) => {
3265
3288
  if (!CONVERSATION_ID_PATTERN.test(ref)) return null;
3266
3289
  if (!await isDirectory(path7.join(brainRoot(), ref))) return null;
3267
3290
  const files = conversationFiles(ref);
@@ -3336,7 +3359,7 @@ function createAntigravityHistory(deps = {}) {
3336
3359
  }
3337
3360
  const items = [];
3338
3361
  for (const name of names) {
3339
- const item = await itemOf3(name);
3362
+ const item = await itemOf4(name);
3340
3363
  if (item !== null) items.push(item);
3341
3364
  }
3342
3365
  return items;
@@ -3354,7 +3377,7 @@ function createAntigravityHistory(deps = {}) {
3354
3377
  }
3355
3378
  return null;
3356
3379
  },
3357
- item: (ref) => itemOf3(ref),
3380
+ item: (ref) => itemOf4(ref),
3358
3381
  async scan(item) {
3359
3382
  const id = item.ref;
3360
3383
  const { history, lastWorkspace, summary } = catalog.get(id);
@@ -4690,6 +4713,11 @@ async function resolveRef(ref, target) {
4690
4713
  return null;
4691
4714
  }
4692
4715
  }
4716
+ function shownPathOf(ref, target) {
4717
+ if (ref.origin === "cli-plans") return path15.join(plansRoot(), ref.path);
4718
+ if (ref.origin === "scratch") return path15.join(scratchRoot(target.cwd, target.sessionId), ref.path);
4719
+ return path15.join(target.cwd, ref.path);
4720
+ }
4693
4721
  function titleOf(relative) {
4694
4722
  return (path15.posix.basename(relative) || relative).replace(/\.md$/i, "");
4695
4723
  }
@@ -4704,6 +4732,8 @@ async function describePlans(target, refs) {
4704
4732
  origin: parsed.origin,
4705
4733
  // La carpeta de un plan de la CLI no aporta nada: es siempre la misma.
4706
4734
  path: parsed.origin === "cli-plans" ? "" : parsed.path,
4735
+ // Solo de uno que esta en disco y paso el guardia: abajo.
4736
+ absolutePath: "",
4707
4737
  exists: false,
4708
4738
  modifiedAt: 0,
4709
4739
  sizeBytes: 0
@@ -4714,6 +4744,7 @@ async function describePlans(target, refs) {
4714
4744
  const info = await stat9(absolute);
4715
4745
  if (info.isFile()) {
4716
4746
  plan.exists = true;
4747
+ plan.absolutePath = shownPathOf(parsed, target);
4717
4748
  plan.modifiedAt = info.mtimeMs;
4718
4749
  plan.sizeBytes = info.size;
4719
4750
  }
@@ -4781,8 +4812,9 @@ var HARNESS_TAGS = [
4781
4812
  "bash-stderr"
4782
4813
  ];
4783
4814
  var HARNESS_BLOCK = new RegExp(`<(${HARNESS_TAGS.join("|")})>[\\s\\S]*?</\\1>`, "g");
4815
+ var PASTED_CONTENT = /<pasted_content id="([0-9A-Za-z_-]{1,64})">\n?([\s\S]*?)\n?<\/pasted_content id="\1">/g;
4784
4816
  function cleanUserText(raw) {
4785
- return raw.replace(HARNESS_BLOCK, "").trim();
4817
+ return raw.replace(PASTED_CONTENT, "$2").replace(HARNESS_BLOCK, "").trim();
4786
4818
  }
4787
4819
  function isHumanOrigin(origin) {
4788
4820
  if (typeof origin !== "object" || origin === null) return true;
@@ -5484,7 +5516,7 @@ var ConversationFollower = class {
5484
5516
  this.partUpdates = [];
5485
5517
  this.freshPlans = [];
5486
5518
  const { reset, added } = await this.jsonl.poll();
5487
- const fresh = new Set(added.map((event) => event.eventId));
5519
+ const fresh = new Set(added.map((event2) => event2.eventId));
5488
5520
  const turns = this.turns.filter((turn) => !fresh.has(turn.eventId));
5489
5521
  const parts = [
5490
5522
  ...new Map(
@@ -5563,10 +5595,10 @@ var ConversationFollower = class {
5563
5595
  this.applyImageAttachment(attachment, events);
5564
5596
  return null;
5565
5597
  }
5566
- const event = toConversationEvent(record, lineNumber, this.limits);
5567
- if (event === null) return null;
5568
- this.accumulate(event);
5569
- return event;
5598
+ const event2 = toConversationEvent(record, lineNumber, this.limits);
5599
+ if (event2 === null) return null;
5600
+ this.accumulate(event2);
5601
+ return event2;
5570
5602
  }
5571
5603
  /**
5572
5604
  * De que mensaje cuelga cada linea `attachment` ya vista.
@@ -5658,12 +5690,12 @@ var ConversationFollower = class {
5658
5690
  * `input_tokens` daria cifras absurdamente bajas, porque con cache casi todo
5659
5691
  * el contexto viaja como `cache_read`.
5660
5692
  */
5661
- accumulate(event) {
5662
- const usage = event.usage;
5663
- if (usage === null || event.role !== "assistant") return;
5693
+ accumulate(event2) {
5694
+ const usage = event2.usage;
5695
+ if (usage === null || event2.role !== "assistant") return;
5664
5696
  const lastRequestTokens = usage.inputTokens + usage.cacheReadInputTokens + usage.cacheCreationInputTokens;
5665
- this.lastRawModel = event.model;
5666
- const model2 = this.resolveModel(event.model);
5697
+ this.lastRawModel = event2.model;
5698
+ const model2 = this.resolveModel(event2.model);
5667
5699
  const fitted = fitWindowToObserved(contextWindowFor(model2), lastRequestTokens);
5668
5700
  this.usage = {
5669
5701
  lastRequestTokens,
@@ -6054,7 +6086,7 @@ async function scanSessionFile(filePath, sessionId) {
6054
6086
  let cwd = null;
6055
6087
  let customTitle = null;
6056
6088
  let aiTitle = null;
6057
- let firstUserText2 = null;
6089
+ let firstUserText3 = null;
6058
6090
  const modelIds = /* @__PURE__ */ new Set();
6059
6091
  const inspect = (line) => {
6060
6092
  const record = parseJsonlLine(line);
@@ -6072,10 +6104,10 @@ async function scanSessionFile(filePath, sessionId) {
6072
6104
  customTitle = record["customTitle"];
6073
6105
  } else if (type === "ai-title" && typeof record["aiTitle"] === "string") {
6074
6106
  aiTitle = record["aiTitle"];
6075
- } else if (firstUserText2 === null && isRealUserMessage(record)) {
6107
+ } else if (firstUserText3 === null && isRealUserMessage(record)) {
6076
6108
  const message = record["message"];
6077
6109
  if (typeof message === "object" && message !== null) {
6078
- firstUserText2 = extractMessageText(message["content"]);
6110
+ firstUserText3 = extractMessageText(message["content"]);
6079
6111
  }
6080
6112
  }
6081
6113
  };
@@ -6089,8 +6121,8 @@ async function scanSessionFile(filePath, sessionId) {
6089
6121
  } else if (aiTitle !== null) {
6090
6122
  title = toTitle(aiTitle);
6091
6123
  titleSource = "ai";
6092
- } else if (firstUserText2 !== null) {
6093
- title = toTitle(firstUserText2);
6124
+ } else if (firstUserText3 !== null) {
6125
+ title = toTitle(firstUserText3);
6094
6126
  titleSource = "first-message";
6095
6127
  } else {
6096
6128
  title = UNTITLED_SESSION_TITLE;
@@ -7274,9 +7306,9 @@ var CodexRolloutSink = class {
7274
7306
  }));
7275
7307
  if (text.length > 0) parts.push({ kind: "text", ...cut(text, this.limits.textMaxChars) });
7276
7308
  if (parts.length === 0) return null;
7277
- const event = this.event("user", lineNumber, at, parts);
7278
- if (images.length > 0) this.contentLines.set(event.eventId, lineNumber - 1);
7279
- return event;
7309
+ const event2 = this.event("user", lineNumber, at, parts);
7310
+ if (images.length > 0) this.contentLines.set(event2.eventId, lineNumber - 1);
7311
+ return event2;
7280
7312
  }
7281
7313
  assistantMessage(payload, lineNumber, at) {
7282
7314
  const content = payload["content"];
@@ -7385,7 +7417,7 @@ var CodexSessionFollower = class {
7385
7417
  }
7386
7418
  this.sink.takeTurns(/* @__PURE__ */ new Set());
7387
7419
  const { reset, added } = await this.jsonl.poll();
7388
- const fresh = new Set(added.map((event) => event.eventId));
7420
+ const fresh = new Set(added.map((event2) => event2.eventId));
7389
7421
  return { reset, added, turns: this.sink.takeTurns(fresh), plans: [], parts: [] };
7390
7422
  }
7391
7423
  async lookup() {
@@ -8154,8 +8186,8 @@ function applyTurnDurations(messages, lastEventOf) {
8154
8186
  if (latest2.completed_at === null || latest2.finish === "tool-calls") continue;
8155
8187
  let target;
8156
8188
  for (let index = steps.length - 1; index >= 0 && target === void 0; index -= 1) {
8157
- const step = steps[index];
8158
- if (step !== void 0) target = lastEventOf.get(step.id);
8189
+ const step2 = steps[index];
8190
+ if (step2 !== void 0) target = lastEventOf.get(step2.id);
8159
8191
  }
8160
8192
  const durationMs = latest2.completed_at - (user.created_at ?? user.time_created);
8161
8193
  if (target === void 0 || durationMs < 0) continue;
@@ -8530,7 +8562,7 @@ var OpenCodeSessionFollower = class {
8530
8562
  }
8531
8563
  getPageBefore(beforeEventId, limit) {
8532
8564
  const { events: all, dropped } = this.window();
8533
- const index = all.findIndex((event) => event.eventId === beforeEventId);
8565
+ const index = all.findIndex((event2) => event2.eventId === beforeEventId);
8534
8566
  if (index <= 0) return { events: [], hasMore: false };
8535
8567
  const start = Math.max(0, index - limit);
8536
8568
  return { events: all.slice(start, index), hasMore: start > 0 || dropped > 0 };
@@ -8593,30 +8625,30 @@ function maxUpdated(rows) {
8593
8625
  return max;
8594
8626
  }
8595
8627
  function diffEvents(previous, next) {
8596
- const nextIndex = new Map(next.map((event, index) => [event.eventId, index]));
8628
+ const nextIndex = new Map(next.map((event2, index) => [event2.eventId, index]));
8597
8629
  const known = /* @__PURE__ */ new Set();
8598
8630
  const parts = [];
8599
8631
  const turns = [];
8600
8632
  let lastIndex = -1;
8601
- for (const event of previous) {
8602
- const index = nextIndex.get(event.eventId);
8633
+ for (const event2 of previous) {
8634
+ const index = nextIndex.get(event2.eventId);
8603
8635
  if (index === void 0 || index < lastIndex) return null;
8604
8636
  lastIndex = index;
8605
- known.add(event.eventId);
8637
+ known.add(event2.eventId);
8606
8638
  const rebuilt = next[index];
8607
8639
  if (rebuilt === void 0) return null;
8608
- if (JSON.stringify(rebuilt.parts) !== JSON.stringify(event.parts)) {
8640
+ if (JSON.stringify(rebuilt.parts) !== JSON.stringify(event2.parts)) {
8609
8641
  parts.push({ eventId: rebuilt.eventId, parts: rebuilt.parts });
8610
8642
  }
8611
- if (rebuilt.durationMs !== null && rebuilt.durationMs !== event.durationMs) {
8643
+ if (rebuilt.durationMs !== null && rebuilt.durationMs !== event2.durationMs) {
8612
8644
  turns.push({ eventId: rebuilt.eventId, durationMs: rebuilt.durationMs });
8613
8645
  }
8614
8646
  }
8615
8647
  const added = [];
8616
- for (const [index, event] of next.entries()) {
8617
- if (known.has(event.eventId)) continue;
8648
+ for (const [index, event2] of next.entries()) {
8649
+ if (known.has(event2.eventId)) continue;
8618
8650
  if (index < lastIndex) return null;
8619
- added.push(event);
8651
+ added.push(event2);
8620
8652
  }
8621
8653
  return { added, parts, turns };
8622
8654
  }
@@ -8899,7 +8931,7 @@ function parseSseChunk(state, chunk) {
8899
8931
  let afterCarriageReturn = state.afterCarriageReturn;
8900
8932
  let atStart = state.atStart;
8901
8933
  let dataLines = [...state.dataLines];
8902
- let event = state.event;
8934
+ let event2 = state.event;
8903
8935
  let lastId = state.lastId;
8904
8936
  if (afterCarriageReturn && bytes.length > 0) {
8905
8937
  if (bytes[0] === LF) bytes = bytes.subarray(1);
@@ -8928,9 +8960,9 @@ function parseSseChunk(state, chunk) {
8928
8960
  index++;
8929
8961
  lineStart = index;
8930
8962
  if (line.length === 0) {
8931
- if (dataLines.length > 0) messages.push({ event: event ?? "message", data: dataLines.join("\n"), id: lastId });
8963
+ if (dataLines.length > 0) messages.push({ event: event2 ?? "message", data: dataLines.join("\n"), id: lastId });
8932
8964
  dataLines = [];
8933
- event = null;
8965
+ event2 = null;
8934
8966
  continue;
8935
8967
  }
8936
8968
  if (line.startsWith(":")) continue;
@@ -8939,7 +8971,7 @@ function parseSseChunk(state, chunk) {
8939
8971
  let value = colon === -1 ? "" : line.slice(colon + 1);
8940
8972
  if (value.startsWith(" ")) value = value.slice(1);
8941
8973
  if (field === "data") dataLines.push(value);
8942
- else if (field === "event") event = value;
8974
+ else if (field === "event") event2 = value;
8943
8975
  else if (field === "id" && !value.includes("\0")) lastId = value;
8944
8976
  }
8945
8977
  return {
@@ -8948,7 +8980,7 @@ function parseSseChunk(state, chunk) {
8948
8980
  afterCarriageReturn,
8949
8981
  atStart,
8950
8982
  dataLines,
8951
- event,
8983
+ event: event2,
8952
8984
  lastId
8953
8985
  },
8954
8986
  messages
@@ -9157,10 +9189,10 @@ var OpenCodeServeClient = class {
9157
9189
  const parsed = parseSseChunk(state, value);
9158
9190
  state = parsed.state;
9159
9191
  for (const message of parsed.messages) {
9160
- const event = parseGlobalEvent(message.data);
9161
- if (event === null) continue;
9192
+ const event2 = parseGlobalEvent(message.data);
9193
+ if (event2 === null) continue;
9162
9194
  try {
9163
- onEvent(event);
9195
+ onEvent(event2);
9164
9196
  } catch {
9165
9197
  }
9166
9198
  }
@@ -9593,6 +9625,7 @@ var SHIFT_TAB = "\x1B[Z";
9593
9625
  var SUBMIT = "\r";
9594
9626
  var END_KEY = "\x1B[F";
9595
9627
  var INTERRUPT = "\x1B";
9628
+ var CLEAR_EDITOR = "";
9596
9629
  var MAX_SUBMIT_CHARS = 2e5;
9597
9630
  function sanitizeForPaste(text) {
9598
9631
  return text.replace(/\x1b\[20[01]~/g, "").replace(/\r\n?/g, "\n").replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g, "");
@@ -9605,7 +9638,13 @@ var IMAGE_REFERENCE_BUILDERS = {
9605
9638
  "at-quoted": (absolutePath) => `@"${absolutePath}"`,
9606
9639
  // La ruta sola: la CLI adjunta si el pegado entero es la ruta de una imagen.
9607
9640
  // Sin comillas, porque las comillas harian que ya no lo fuera.
9608
- "bare-path-paste": (absolutePath) => absolutePath
9641
+ "bare-path-paste": (absolutePath) => absolutePath,
9642
+ // Una linea por imagen, en ingles como todo lo que se le dice a una CLI: la
9643
+ // CLI no adjunta nada y el modelo abre la ruta con su herramienta de lectura.
9644
+ "quoted-path-line": (absolutePath) => `Attached image: "${absolutePath}"`
9645
+ };
9646
+ var IMAGE_REFERENCE_SEPARATORS = {
9647
+ "quoted-path-line": "\n"
9609
9648
  };
9610
9649
  var TAB = " ";
9611
9650
  var ANSWER_KEY_INTERVAL_MS = 120;
@@ -9698,13 +9737,22 @@ function buildInterruptKeys(presses) {
9698
9737
  const count = Number.isInteger(presses) && presses > 1 ? presses : 1;
9699
9738
  return Array.from({ length: count }, () => INTERRUPT);
9700
9739
  }
9701
- function joinedPayload(text, attachments) {
9740
+ function joinedPayload(text, attachments, separator = " ") {
9702
9741
  const pieces = [...attachments];
9703
9742
  const clean = sanitizeForPaste(text).trim();
9704
9743
  if (clean.length > 0) pieces.push(clean);
9705
- const payload = pieces.join(" ").slice(0, MAX_SUBMIT_CHARS);
9744
+ const payload = pieces.join(separator).slice(0, MAX_SUBMIT_CHARS);
9706
9745
  return payload.length === 0 ? null : payload;
9707
9746
  }
9747
+ function submissionPayload(text, imagePaths, style) {
9748
+ if (imagePaths.length > 0 && (style === null || style === "bare-path-paste")) return null;
9749
+ const effective = style ?? "at-quoted";
9750
+ return joinedPayload(
9751
+ text,
9752
+ imagePaths.map((imagePath) => fileReference(imagePath, effective)),
9753
+ IMAGE_REFERENCE_SEPARATORS[effective] ?? " "
9754
+ );
9755
+ }
9708
9756
  function pasted(payload, markers) {
9709
9757
  return markers ? `${PASTE_START}${payload}${PASTE_END}` : payload;
9710
9758
  }
@@ -9714,10 +9762,7 @@ function buildSubmissionWrites(text, imagePaths, shape, options = {}) {
9714
9762
  if (imagePaths.length > 0 && style === null) return null;
9715
9763
  const submit = shape.endBeforeSubmit === true ? `${END_KEY}${SUBMIT}` : SUBMIT;
9716
9764
  if (style !== "bare-path-paste") {
9717
- const payload = joinedPayload(
9718
- text,
9719
- imagePaths.map((imagePath) => fileReference(imagePath, style ?? "at-quoted"))
9720
- );
9765
+ const payload = submissionPayload(text, imagePaths, style);
9721
9766
  if (payload === null) return null;
9722
9767
  if (shape.enterSeparately === true) {
9723
9768
  return send ? [pasted(payload, shape.pasteMarkers), submit] : [pasted(payload, shape.pasteMarkers)];
@@ -9818,24 +9863,24 @@ function activityOfTracked(session) {
9818
9863
  if (session.status === "busy" || session.status === "retry") return { activity: "busy", waitingFor: null };
9819
9864
  return { activity: "idle", waitingFor: null };
9820
9865
  }
9821
- function applyEvent(session, { event, fromChild }) {
9822
- switch (event.type) {
9866
+ function applyEvent(session, { event: event2, fromChild }) {
9867
+ switch (event2.type) {
9823
9868
  case "session.status":
9824
- if (!fromChild) session.status = event.status;
9869
+ if (!fromChild) session.status = event2.status;
9825
9870
  break;
9826
9871
  case "permission.asked":
9827
- session.permissions.add(event.requestId);
9872
+ session.permissions.add(event2.requestId);
9828
9873
  break;
9829
9874
  case "permission.replied":
9830
- session.permissions.delete(event.requestId);
9875
+ session.permissions.delete(event2.requestId);
9831
9876
  break;
9832
9877
  case "question.asked":
9833
- (fromChild ? session.childQuestions : session.questions).add(event.requestId);
9878
+ (fromChild ? session.childQuestions : session.questions).add(event2.requestId);
9834
9879
  break;
9835
9880
  case "question.replied":
9836
9881
  case "question.rejected":
9837
- session.questions.delete(event.requestId);
9838
- session.childQuestions.delete(event.requestId);
9882
+ session.questions.delete(event2.requestId);
9883
+ session.childQuestions.delete(event2.requestId);
9839
9884
  break;
9840
9885
  }
9841
9886
  }
@@ -9964,7 +10009,7 @@ var OpenCodeServeStatus = class {
9964
10009
  this.forget();
9965
10010
  this.client = client;
9966
10011
  this.closeEvents = client.events(
9967
- (event) => this.onEvent(event),
10012
+ (event2) => this.onEvent(event2),
9968
10013
  () => this.onConnected()
9969
10014
  );
9970
10015
  }
@@ -9980,8 +10025,8 @@ var OpenCodeServeStatus = class {
9980
10025
  const directories = new Set([...this.tracked.values()].map((session) => session.directory));
9981
10026
  for (const directory of directories) void this.snapshot(directory);
9982
10027
  }
9983
- onEvent(event) {
9984
- const routed = this.route(event);
10028
+ onEvent(event2) {
10029
+ const routed = this.route(event2);
9985
10030
  if (routed === null) return;
9986
10031
  const session = this.tracked.get(routed.rootId);
9987
10032
  if (session === void 0) return;
@@ -9994,12 +10039,12 @@ var OpenCodeServeStatus = class {
9994
10039
  * sub-agente. El estado de otra sesion no se atribuye a nadie, y no hace
9995
10040
  * buscar ningun padre.
9996
10041
  */
9997
- route(event) {
9998
- if (this.tracked.has(event.sessionId)) return { event, rootId: event.sessionId, fromChild: false };
9999
- if (event.type === "session.status") return null;
10000
- const rootId = this.rootOf(event.sessionId);
10001
- if (rootId === null || rootId === event.sessionId || !this.tracked.has(rootId)) return null;
10002
- return { event, rootId, fromChild: true };
10042
+ route(event2) {
10043
+ if (this.tracked.has(event2.sessionId)) return { event: event2, rootId: event2.sessionId, fromChild: false };
10044
+ if (event2.type === "session.status") return null;
10045
+ const rootId = this.rootOf(event2.sessionId);
10046
+ if (rootId === null || rootId === event2.sessionId || !this.tracked.has(rootId)) return null;
10047
+ return { event: event2, rootId, fromChild: true };
10003
10048
  }
10004
10049
  /**
10005
10050
  * La raiz de una sesion subiendo por `parent_id`, o null si la base no la
@@ -10166,169 +10211,2111 @@ var OPENCODE_CAPABILITIES = {
10166
10211
  contextWindowSource: "usage-with-catalog",
10167
10212
  // No hay planes en archivo.
10168
10213
  plans: false,
10169
- /*
10170
- D12: con un permiso o una pregunta abiertos, el pegado y su Enter caerian
10171
- en el menu del TUI. El candado de §10.8 (`hasOpenToolCall`) no sirve: se
10172
- apaga con `statusSource`.
10173
- */
10214
+ /*
10215
+ D12: con un permiso o una pregunta abiertos, el pegado y su Enter caerian
10216
+ en el menu del TUI. El candado de §10.8 (`hasOpenToolCall`) no sirve: se
10217
+ apaga con `statusSource`.
10218
+ */
10219
+ waitingBlocksSubmit: true
10220
+ };
10221
+ var OPENCODE_INPUT = {
10222
+ imageReference: "bare-path-paste",
10223
+ pieceGapMs: 400,
10224
+ pasteMarkers: true,
10225
+ enterSeparately: true,
10226
+ interruptPresses: 2,
10227
+ // `@` abre un buscador en el TUI (11.11): el transcript va entre comillas.
10228
+ transcriptReference: "quoted-path"
10229
+ };
10230
+ function copyEnvironment(base) {
10231
+ const env = {};
10232
+ for (const [key, value] of Object.entries(base)) {
10233
+ if (value !== void 0) env[key] = value;
10234
+ }
10235
+ return env;
10236
+ }
10237
+ function createOpenCodeAdapter(options = {}) {
10238
+ const platform = options.platform ?? process.platform;
10239
+ const env = options.env ?? process.env;
10240
+ const sqlite = options.sqlite ?? loadSqlite;
10241
+ const paths = resolveOpenCodePaths(env, options.home ?? homedir8(), platform);
10242
+ const db = new ReadOnlyDatabase(paths.dbFile ?? "", {
10243
+ idleCloseMs: OPENCODE_DB_IDLE_CLOSE_MS,
10244
+ required: OPENCODE_REQUIRED_COLUMNS,
10245
+ label: OPENCODE_LABEL,
10246
+ sqlite
10247
+ });
10248
+ const signal = new DbChangeSignal(
10249
+ paths.dbFile === null || paths.walFile === null ? null : { db: paths.dbFile, wal: paths.walFile }
10250
+ );
10251
+ const catalog = new ModelCatalog(paths.modelsFile);
10252
+ const serveEnv = options.serveEnv ?? (() => copyEnvironment(process.env));
10253
+ let serve = null;
10254
+ const status = new OpenCodeServeStatus({ ...options.serveStatus, parentOf: (id) => readSessionParent(db, id) });
10255
+ let current = null;
10256
+ const serveFor = (location) => {
10257
+ if (serve !== null) return serve;
10258
+ const created = new OpenCodeServeProcess({ ...options.serveProcess, location, baseEnv: serveEnv });
10259
+ created.onExit((endpoint, exit) => {
10260
+ status.detach(endpoint, exit.requested ? null : "server-closed");
10261
+ if (current?.endpoint === endpoint) current = null;
10262
+ });
10263
+ serve = created;
10264
+ return created;
10265
+ };
10266
+ const clientFor = (endpoint) => {
10267
+ if (current?.endpoint === endpoint) return current.client;
10268
+ current = { endpoint, client: new OpenCodeServeClient(endpoint, void 0, options.serveClient) };
10269
+ return current.client;
10270
+ };
10271
+ const questions = new OpenCodeQuestions({
10272
+ lookupCall: (partId, sessionId) => readPartCall(db, partId, sessionId),
10273
+ client: () => current?.client ?? null
10274
+ });
10275
+ return {
10276
+ id: "opencode",
10277
+ label: OPENCODE_LABEL,
10278
+ command: OPENCODE_COMMAND,
10279
+ installUrl: OPENCODE_INSTALL_URL,
10280
+ capabilities: OPENCODE_CAPABILITIES,
10281
+ input: OPENCODE_INPUT,
10282
+ // `--version` es seguro: medido, no toca la base ni escribe log.
10283
+ locate: () => locateCommand(OPENCODE_COMMAND),
10284
+ missingMessage: () => commandNotFoundMessage(OPENCODE_COMMAND, OPENCODE_INSTALL_URL),
10285
+ /**
10286
+ * `attach <url> --dir . --session <id> --password <hex>` (D5), con la
10287
+ * sesion nueva creada por API (D6). No es `async` (M1b): un id de
10288
+ * reanudacion invalido lanza aca mismo, sin tocar el `serve`, porque detras
10289
+ * de `cmd.exe /c` un `&` se ejecuta. Lo que falla despues —el `serve` que no
10290
+ * arranca, la sesion que no se crea— rechaza, y el registro lo muestra como
10291
+ * `spawn-failed` con el motivo (D1). `proposedSessionId` no se usa: el id lo
10292
+ * pone OpenCode.
10293
+ */
10294
+ launch(input) {
10295
+ return launchWithServe(
10296
+ {
10297
+ ensure: () => serveFor(input.location).ensure(),
10298
+ createSession: (endpoint, directory) => clientFor(endpoint).createSession(directory),
10299
+ track: (endpoint, sessionId, directory) => status.track(sessionId, directory, clientFor(endpoint))
10300
+ },
10301
+ input
10302
+ );
10303
+ },
10304
+ /** Copia del entorno, sin quitar nada: OpenCode no hereda ningun marcador que le estorbe. */
10305
+ environment(base) {
10306
+ return { env: copyEnvironment(base), notice: null };
10307
+ },
10308
+ /*
10309
+ Toda pestana lanzada, nueva o reanudada, retiene el `serve` mientras viva
10310
+ su pty, y al cerrarse corta la sesion si quedo trabajando o esperando
10311
+ (D13): con `attach`, el trabajo seguiria en el `serve` sin ningun TUI que
10312
+ lo muestre. El corte va al cliente vigente: el de un `serve` que ya murio
10313
+ no esta, y su estado ya es null.
10314
+ */
10315
+ onSpawned(context) {
10316
+ if (context.sessionId.length === 0) return null;
10317
+ const { sessionId, cwd } = context;
10318
+ return createServeLaunchHook({
10319
+ sessionId,
10320
+ cwd,
10321
+ release: serve?.retain() ?? (() => {
10322
+ }),
10323
+ current: () => status.current(sessionId),
10324
+ abort: (directory, id) => current?.client.abort(directory, id) ?? Promise.resolve()
10325
+ });
10326
+ },
10327
+ history: createOpenCodeHistory({
10328
+ dbFile: paths.dbFile,
10329
+ db,
10330
+ signal,
10331
+ catalog,
10332
+ platform,
10333
+ sqlite
10334
+ }),
10335
+ status,
10336
+ questions,
10337
+ /**
10338
+ * La base que se lee, para el arranque. Solo si hay algo que decir, para que
10339
+ * quien no usa OpenCode no vea una linea nueva:
10340
+ *
10341
+ * - con la base en memoria (`OPENCODE_DB=:memory:`, que alguien puso a
10342
+ * proposito), que no hay nada que leer;
10343
+ * - sin `node:sqlite`, el aviso de la version de Node, si hay una base que
10344
+ * no se va a ver o la CLI esta instalada;
10345
+ * - con la base en disco, su ruta.
10346
+ *
10347
+ * Con la base ausente no dice nada: `status()` es perezoso y no sabe si
10348
+ * existe hasta la primera consulta, asi que se mira el archivo.
10349
+ */
10350
+ startupHistoryNote(cliAvailable) {
10351
+ if (env["OPENCODE_DB"] === ":memory:") return "in memory (nothing to read)";
10352
+ if (paths.dbFile === null) return null;
10353
+ const present = existsSync2(paths.dbFile);
10354
+ if ("unavailable" in sqlite()) return present || cliAvailable ? `not read: ${sqliteUnavailableText(OPENCODE_LABEL)}` : null;
10355
+ return present ? `${paths.dbFile} (read-only)` : null;
10356
+ },
10357
+ // `opencode.json` no se abre (regla 2.1).
10358
+ defaults: () => Promise.resolve(null),
10359
+ /**
10360
+ * Las cuatro carpetas de OpenCode: datos (la base, `auth.json`),
10361
+ * configuracion, cache y estado. No se listan ni se entran (regla 2.1). La
10362
+ * carpeta de un `OPENCODE_DB` propio no: puede ser la de un proyecto.
10363
+ */
10364
+ protectedDirs: () => [paths.dataDir, paths.configDir, paths.cacheDir, paths.stateDir],
10365
+ /**
10366
+ * Suelta el estado (sus oyentes reciben null y el flujo se cierra) antes de
10367
+ * matar el `serve`: los ganchos de las pestanas, que se cancelan despues en
10368
+ * el apagado, ya no piden ningun corte (M7). La promesa espera a que el
10369
+ * proceso salga; la orden de matarlo sale antes de devolver.
10370
+ */
10371
+ dispose: () => {
10372
+ status.dispose();
10373
+ current = null;
10374
+ signal.dispose();
10375
+ db.close();
10376
+ return serve?.dispose() ?? Promise.resolve();
10377
+ }
10378
+ };
10379
+ }
10380
+
10381
+ // packages/server/src/agents/pi/index.ts
10382
+ import { stat as stat21 } from "node:fs/promises";
10383
+ import { homedir as homedir10 } from "node:os";
10384
+ import path37 from "node:path";
10385
+
10386
+ // packages/server/src/agents/pi/catalog.ts
10387
+ import { readFile as readFileFromDisk2, stat as statFromDisk2 } from "node:fs/promises";
10388
+ import path30 from "node:path";
10389
+ var PI_CATALOG_RECHECK_MS = 3e4;
10390
+ var PI_MODEL_LIST_MAX = 300;
10391
+ var STORE_MAX_BYTES = 8 * 1024 * 1024;
10392
+ var PROVIDER_ID_PATTERN = /^[a-z0-9][a-z0-9._-]{0,63}$/i;
10393
+ function storeModels(parsed) {
10394
+ const providers = asRecord(parsed);
10395
+ if (providers === null) return [];
10396
+ const models = [];
10397
+ for (const [provider, value] of Object.entries(providers)) {
10398
+ const list = asRecord(value)?.["models"];
10399
+ if (!Array.isArray(list)) continue;
10400
+ for (const item of list) {
10401
+ const model2 = asRecord(item);
10402
+ const id = model2?.["id"];
10403
+ if (model2 === null || typeof id !== "string" || id.length === 0) continue;
10404
+ const name = model2["name"];
10405
+ const window = model2["contextWindow"];
10406
+ models.push({
10407
+ provider,
10408
+ id,
10409
+ name: typeof name === "string" && name.trim().length > 0 ? name.trim() : id,
10410
+ contextWindow: typeof window === "number" && Number.isFinite(window) && window > 0 ? window : null
10411
+ });
10412
+ }
10413
+ }
10414
+ return models;
10415
+ }
10416
+ function packageWindows(parsed) {
10417
+ const windows = /* @__PURE__ */ new Map();
10418
+ const apis = asRecord(parsed);
10419
+ if (apis === null) return windows;
10420
+ for (const group of Object.values(apis)) {
10421
+ const entries = asRecord(group);
10422
+ if (entries === null) continue;
10423
+ for (const item of Object.values(entries)) {
10424
+ const model2 = asRecord(item);
10425
+ const id = model2?.["id"];
10426
+ const window = model2?.["contextWindow"];
10427
+ if (typeof id === "string" && id.length > 0 && typeof window === "number" && Number.isFinite(window) && window > 0) {
10428
+ windows.set(id, window);
10429
+ }
10430
+ }
10431
+ }
10432
+ return windows;
10433
+ }
10434
+ function modelOptions(models) {
10435
+ const several = new Set(models.map((model2) => model2.provider)).size > 1;
10436
+ const seen = /* @__PURE__ */ new Set();
10437
+ const options = [];
10438
+ for (const model2 of models) {
10439
+ const value = `${model2.provider}/${model2.id}`;
10440
+ if (seen.has(value) || /\s/.test(value)) continue;
10441
+ seen.add(value);
10442
+ options.push({
10443
+ value,
10444
+ label: several ? `${model2.name} (${model2.provider})` : model2.name,
10445
+ family: value,
10446
+ long: false,
10447
+ window: model2.contextWindow
10448
+ });
10449
+ if (options.length >= PI_MODEL_LIST_MAX) break;
10450
+ }
10451
+ return options;
10452
+ }
10453
+ var PiModelCatalog = class {
10454
+ constructor(options) {
10455
+ this.options = options;
10456
+ this.readFile = options.readFile ?? ((file) => readFileFromDisk2(file, "utf8"));
10457
+ this.stat = options.stat ?? statFromDisk2;
10458
+ this.now = options.now ?? Date.now;
10459
+ this.recheckMs = options.recheckMs ?? PI_CATALOG_RECHECK_MS;
10460
+ }
10461
+ options;
10462
+ readFile;
10463
+ stat;
10464
+ now;
10465
+ recheckMs;
10466
+ models = [];
10467
+ windows = /* @__PURE__ */ new Map();
10468
+ /** La firma del archivo que dio `models`; null si nunca se leyo bien. */
10469
+ loadedStamp = null;
10470
+ checkedAt = null;
10471
+ refreshing = null;
10472
+ /** Proveedor -> ventanas del paquete. Un proveedor sin archivo queda con un mapa vacio. */
10473
+ packageCache = /* @__PURE__ */ new Map();
10474
+ /** La lista de la cuenta tal como quedo en la ultima lectura. Sincronica: la usan las capacidades. */
10475
+ list() {
10476
+ return this.models;
10477
+ }
10478
+ /**
10479
+ * Vuelve a leer `models-store.json` si cambio. true si la lista de modelos
10480
+ * quedo distinta. Nunca lanza.
10481
+ */
10482
+ async reload() {
10483
+ const before = JSON.stringify(this.models);
10484
+ this.checkedAt = this.now();
10485
+ await this.load();
10486
+ return JSON.stringify(this.models) !== before;
10487
+ }
10488
+ /** La ventana de ese modelo en ese proveedor, o null. Nunca lanza. */
10489
+ async contextWindow(provider, modelId) {
10490
+ if (provider === null || modelId === null || provider.length === 0 || modelId.length === 0) return null;
10491
+ await this.refresh();
10492
+ const known = this.windows.get(`${provider}/${modelId}`);
10493
+ if (known !== void 0) return known;
10494
+ return (await this.packageWindowsOf(provider)).get(modelId) ?? null;
10495
+ }
10496
+ refresh() {
10497
+ const now = this.now();
10498
+ if (this.checkedAt !== null && now - this.checkedAt < this.recheckMs) return this.refreshing ?? Promise.resolve();
10499
+ if (this.refreshing === null) {
10500
+ this.checkedAt = now;
10501
+ this.refreshing = this.load().finally(() => {
10502
+ this.refreshing = null;
10503
+ });
10504
+ }
10505
+ return this.refreshing;
10506
+ }
10507
+ async load() {
10508
+ const file = this.options.storeFile();
10509
+ const forget = () => {
10510
+ this.models = [];
10511
+ this.windows = /* @__PURE__ */ new Map();
10512
+ this.loadedStamp = null;
10513
+ };
10514
+ if (file === null) return forget();
10515
+ let stamp2;
10516
+ try {
10517
+ const info = await this.stat(file);
10518
+ if (info.size > STORE_MAX_BYTES) return forget();
10519
+ stamp2 = `${info.mtimeMs}:${info.size}`;
10520
+ } catch {
10521
+ return forget();
10522
+ }
10523
+ if (stamp2 === this.loadedStamp) return;
10524
+ try {
10525
+ const models = storeModels(JSON.parse(await this.readFile(file)));
10526
+ this.models = models;
10527
+ this.windows = new Map(
10528
+ models.flatMap(
10529
+ (model2) => model2.contextWindow === null ? [] : [[`${model2.provider}/${model2.id}`, model2.contextWindow]]
10530
+ )
10531
+ );
10532
+ this.loadedStamp = stamp2;
10533
+ } catch {
10534
+ forget();
10535
+ }
10536
+ }
10537
+ async packageWindowsOf(provider) {
10538
+ const cached = this.packageCache.get(provider);
10539
+ if (cached !== void 0) return cached;
10540
+ const packageDir = this.options.packageDir();
10541
+ if (packageDir === null || !PROVIDER_ID_PATTERN.test(provider)) return /* @__PURE__ */ new Map();
10542
+ let windows = /* @__PURE__ */ new Map();
10543
+ for (const dataDir of packageDataDirs(packageDir)) {
10544
+ try {
10545
+ windows = packageWindows(JSON.parse(await this.readFile(path30.join(dataDir, `${provider}.json`))));
10546
+ break;
10547
+ } catch {
10548
+ }
10549
+ }
10550
+ this.packageCache.set(provider, windows);
10551
+ return windows;
10552
+ }
10553
+ };
10554
+ function packageDataDirs(packageDir) {
10555
+ const tail = ["pi-ai", "dist", "providers", "data"];
10556
+ return [
10557
+ path30.join(packageDir, "node_modules", "@earendil-works", ...tail),
10558
+ path30.join(packageDir, "..", ...tail)
10559
+ ];
10560
+ }
10561
+
10562
+ // packages/server/src/agents/pi/constants.ts
10563
+ var PI_COMMAND = "pi";
10564
+ var PI_LABEL = "Pi";
10565
+ var PI_INSTALL_URL = "https://pi.dev";
10566
+ var PI_PACKAGE_NAME = "@earendil-works/pi-coding-agent";
10567
+ var PI_EFFORT_OPTIONS = [
10568
+ { value: "off", label: "Apagado" },
10569
+ { value: "minimal", label: "Minimo" },
10570
+ { value: "low", label: "Bajo" },
10571
+ { value: "medium", label: "Medio" },
10572
+ { value: "high", label: "Alto" },
10573
+ { value: "xhigh", label: "Muy alto" },
10574
+ { value: "max", label: "Maximo" }
10575
+ ];
10576
+ var SESSION_ID_PATTERN = /^[A-Za-z0-9](?:[A-Za-z0-9._-]*[A-Za-z0-9])?$/;
10577
+ var SESSION_ID_MAX_LENGTH = 128;
10578
+ function isPiSessionId(value) {
10579
+ return value.length <= SESSION_ID_MAX_LENGTH && SESSION_ID_PATTERN.test(value);
10580
+ }
10581
+ var PI_READY_AFTER_LAUNCH_MS = 3e3;
10582
+ var PI_CLEAR_GAP_MS = 750;
10583
+
10584
+ // packages/server/src/agents/pi/history.ts
10585
+ import { stat as stat18 } from "node:fs/promises";
10586
+ import path34 from "node:path";
10587
+
10588
+ // packages/server/src/agents/pi/paths.ts
10589
+ import { readdir as readdir9, stat as stat16 } from "node:fs/promises";
10590
+ import { homedir as homedir9 } from "node:os";
10591
+ import path31 from "node:path";
10592
+ var warned = /* @__PURE__ */ new Set();
10593
+ function isFullyQualified3(candidate) {
10594
+ if (!path31.isAbsolute(candidate)) return false;
10595
+ return process.platform !== "win32" || /^([a-zA-Z]:[\\/]|[\\/]{2})/.test(candidate);
10596
+ }
10597
+ function resolveConfigured(name, value) {
10598
+ if (value === "~") return homedir9();
10599
+ if (value.startsWith("~/") || value.startsWith("~\\")) return path31.join(homedir9(), value.slice(2));
10600
+ if (isFullyQualified3(value)) return path31.normalize(value);
10601
+ const key = `${name}=${value}`;
10602
+ if (!warned.has(key)) {
10603
+ warned.add(key);
10604
+ console.warn(`[pi] ${name} isn't an absolute path (${value}): the Pi history isn't read.`);
10605
+ }
10606
+ return null;
10607
+ }
10608
+ function piAgentDir() {
10609
+ const configured = process.env["PI_CODING_AGENT_DIR"];
10610
+ if (configured === void 0 || configured.length === 0) return path31.join(homedir9(), ".pi", "agent");
10611
+ return resolveConfigured("PI_CODING_AGENT_DIR", configured);
10612
+ }
10613
+ function piSessionsLayout() {
10614
+ const configured = process.env["PI_CODING_AGENT_SESSION_DIR"];
10615
+ if (configured !== void 0 && configured.length > 0) {
10616
+ const root = resolveConfigured("PI_CODING_AGENT_SESSION_DIR", configured);
10617
+ return root === null ? null : { root, flat: true };
10618
+ }
10619
+ const agentDir = piAgentDir();
10620
+ return agentDir === null ? null : { root: path31.join(agentDir, "sessions"), flat: false };
10621
+ }
10622
+ function piModelsStorePath() {
10623
+ const agentDir = piAgentDir();
10624
+ return agentDir === null ? null : path31.join(agentDir, "models-store.json");
10625
+ }
10626
+ function projectFolderName(cwd) {
10627
+ const resolved = path31.resolve(cwd);
10628
+ return `--${resolved.replace(/^[/\\]/, "").replace(/[/\\:]/g, "-")}--`;
10629
+ }
10630
+ function isProjectFolderName(name) {
10631
+ return name.length > 4 && name.startsWith("--") && name.endsWith("--");
10632
+ }
10633
+ var SESSION_FILE_STAMP = /^(\d{4})-(\d{2})-(\d{2})T(\d{2})-(\d{2})-(\d{2})-(\d{3})Z$/;
10634
+ var STAMP_LENGTH = 24;
10635
+ function parseSessionFileName(name) {
10636
+ if (!name.endsWith(".jsonl") || name.charAt(STAMP_LENGTH) !== "_") return null;
10637
+ const stamp2 = SESSION_FILE_STAMP.exec(name.slice(0, STAMP_LENGTH));
10638
+ if (stamp2 === null) return null;
10639
+ const sessionId = name.slice(STAMP_LENGTH + 1, -".jsonl".length);
10640
+ if (!isPiSessionId(sessionId)) return null;
10641
+ const [, year, month, day, hour, minute, second, millis] = stamp2;
10642
+ const startedAt = Date.UTC(
10643
+ Number(year),
10644
+ Number(month) - 1,
10645
+ Number(day),
10646
+ Number(hour),
10647
+ Number(minute),
10648
+ Number(second),
10649
+ Number(millis)
10650
+ );
10651
+ return Number.isFinite(startedAt) ? { sessionId, startedAt } : null;
10652
+ }
10653
+ function isInside3(root, filePath) {
10654
+ const relative = path31.relative(root, filePath);
10655
+ return relative.length > 0 && !relative.startsWith("..") && !path31.isAbsolute(relative);
10656
+ }
10657
+ function sessionDirFor(cwd, layout = piSessionsLayout()) {
10658
+ if (layout === null) return null;
10659
+ return layout.flat ? layout.root : path31.join(layout.root, projectFolderName(cwd));
10660
+ }
10661
+ async function sessionFilesIn(dir) {
10662
+ try {
10663
+ const entries = await readdir9(dir, { withFileTypes: true });
10664
+ return entries.filter((entry) => entry.isFile() && parseSessionFileName(entry.name) !== null).map((entry) => entry.name).sort();
10665
+ } catch {
10666
+ return [];
10667
+ }
10668
+ }
10669
+ async function projectFolders(layout) {
10670
+ if (layout.flat) return [];
10671
+ try {
10672
+ const entries = await readdir9(layout.root, { withFileTypes: true });
10673
+ return entries.filter((entry) => entry.isDirectory() && isProjectFolderName(entry.name)).map((entry) => path31.join(layout.root, entry.name)).sort();
10674
+ } catch {
10675
+ return [];
10676
+ }
10677
+ }
10678
+ async function listSessionFiles(layout) {
10679
+ const dirs = layout.flat ? [layout.root] : await projectFolders(layout);
10680
+ const files = [];
10681
+ for (const dir of dirs) {
10682
+ for (const name of await sessionFilesIn(dir)) files.push(path31.join(dir, name));
10683
+ }
10684
+ return files;
10685
+ }
10686
+ async function findSessionFile(sessionId, cwd) {
10687
+ if (!isPiSessionId(sessionId)) return null;
10688
+ const layout = piSessionsLayout();
10689
+ if (layout === null) return null;
10690
+ const own = cwd === null || cwd.length === 0 ? null : sessionDirFor(cwd, layout);
10691
+ const dirs = own === null ? [] : [own];
10692
+ if (!layout.flat) {
10693
+ for (const dir of await projectFolders(layout)) {
10694
+ if (!dirs.includes(dir)) dirs.push(dir);
10695
+ }
10696
+ }
10697
+ for (const dir of dirs) {
10698
+ const matches = (await sessionFilesIn(dir)).filter((name) => parseSessionFileName(name)?.sessionId === sessionId);
10699
+ if (matches.length === 0) continue;
10700
+ if (matches.length === 1) return path31.join(dir, matches[0] ?? "");
10701
+ let best = null;
10702
+ for (const name of matches) {
10703
+ try {
10704
+ const info = await stat16(path31.join(dir, name));
10705
+ if (best === null || info.mtimeMs > best.mtimeMs) best = { file: path31.join(dir, name), mtimeMs: info.mtimeMs };
10706
+ } catch {
10707
+ }
10708
+ }
10709
+ if (best !== null) return best.file;
10710
+ }
10711
+ return null;
10712
+ }
10713
+
10714
+ // packages/server/src/agents/pi/session-follower.ts
10715
+ import path33 from "node:path";
10716
+
10717
+ // packages/server/src/agents/pi/session-entries.ts
10718
+ var NOTICE_DETAIL_MAX_CHARS2 = 300;
10719
+ function stampOf(value) {
10720
+ if (typeof value !== "string") return 0;
10721
+ const parsed = Date.parse(value);
10722
+ return Number.isFinite(parsed) ? parsed : 0;
10723
+ }
10724
+ function numberOf(value) {
10725
+ return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : 0;
10726
+ }
10727
+ function readHeader(record) {
10728
+ if (record["type"] !== "session") return null;
10729
+ const sessionId = record["id"];
10730
+ const cwd = record["cwd"];
10731
+ if (typeof sessionId !== "string" || sessionId.length === 0) return null;
10732
+ if (typeof cwd !== "string" || cwd.length === 0) return null;
10733
+ const version = record["version"];
10734
+ const parentSession = record["parentSession"];
10735
+ return {
10736
+ sessionId,
10737
+ cwd,
10738
+ version: typeof version === "number" && Number.isFinite(version) ? version : null,
10739
+ parentSession: typeof parentSession === "string" && parentSession.length > 0 ? parentSession : null,
10740
+ startedAt: stampOf(record["timestamp"])
10741
+ };
10742
+ }
10743
+ var ATTACHED_IMAGE_LINE = /^Attached image: "([^"\r\n]+)"$/;
10744
+ function pastedImagePaths(text) {
10745
+ const paths = [];
10746
+ for (const line of text.split("\n")) {
10747
+ const match = ATTACHED_IMAGE_LINE.exec(line.trim());
10748
+ const target = match?.[1];
10749
+ if (target !== void 0 && pastedImageMediaType(target) !== null) paths.push(target);
10750
+ }
10751
+ return paths;
10752
+ }
10753
+ function withoutPastedImageLines(text) {
10754
+ return text.split("\n").filter((line) => {
10755
+ const target = ATTACHED_IMAGE_LINE.exec(line.trim())?.[1];
10756
+ return target === void 0 || pastedImageMediaType(target) === null;
10757
+ }).join("\n");
10758
+ }
10759
+ function blocksOf(content) {
10760
+ if (typeof content === "string") return content.length > 0 ? [{ type: "text", text: content }] : [];
10761
+ if (!Array.isArray(content)) return [];
10762
+ const blocks = [];
10763
+ for (const item of content) {
10764
+ const block = asRecord(item);
10765
+ if (block !== null) blocks.push(block);
10766
+ }
10767
+ return blocks;
10768
+ }
10769
+ function textOf(blocks) {
10770
+ return blocks.filter((block) => block["type"] === "text" && typeof block["text"] === "string").map((block) => block["text"]).join("\n");
10771
+ }
10772
+ function contentImageTypes(content) {
10773
+ return blocksOf(content).filter((block) => block["type"] === "image").map((block) => typeof block["mimeType"] === "string" && block["mimeType"].length > 0 ? block["mimeType"] : "image/png");
10774
+ }
10775
+ function readUser(message, limits) {
10776
+ const blocks = blocksOf(message["content"]);
10777
+ const raw = textOf(blocks);
10778
+ const parts = contentImageTypes(message["content"]).map((mediaType, index) => ({
10779
+ kind: "image",
10780
+ index,
10781
+ mediaType,
10782
+ source: "content"
10783
+ }));
10784
+ for (const [index, target] of pastedImagePaths(raw).entries()) {
10785
+ parts.push({ kind: "image", index, mediaType: pastedImageMediaType(target) ?? "image/png", source: "pasted" });
10786
+ }
10787
+ const text = withoutPastedImageLines(raw).trim();
10788
+ if (text.length > 0) parts.push({ kind: "text", ...cut(text, limits.textMaxChars) });
10789
+ return { kind: "user", parts, plain: plainText(raw) };
10790
+ }
10791
+ var PLAIN_MAX_CHARS = 8e3;
10792
+ var PLAIN_NEEDLE_CHARS = 2e3;
10793
+ function plainText(raw) {
10794
+ return raw.replace(/\s+/g, " ").trim().slice(0, PLAIN_MAX_CHARS);
10795
+ }
10796
+ function plainMatches(plain, submitted) {
10797
+ const needle = plainText(submitted).slice(0, PLAIN_NEEDLE_CHARS);
10798
+ return needle.length > 0 && plain.includes(needle);
10799
+ }
10800
+ function readUsage2(value) {
10801
+ const usage = asRecord(value);
10802
+ if (usage === null) return null;
10803
+ const input = numberOf(usage["input"]);
10804
+ const output = numberOf(usage["output"]);
10805
+ const cacheRead = numberOf(usage["cacheRead"]);
10806
+ const cacheWrite = numberOf(usage["cacheWrite"]);
10807
+ const total = numberOf(usage["totalTokens"]);
10808
+ return { input, output, cacheRead, cacheWrite, totalTokens: total > 0 ? total : input + output + cacheRead + cacheWrite };
10809
+ }
10810
+ function prettyArguments(value) {
10811
+ if (typeof value === "string") return value;
10812
+ try {
10813
+ return JSON.stringify(value ?? {}, null, 2);
10814
+ } catch {
10815
+ return "";
10816
+ }
10817
+ }
10818
+ function readAssistant(message, limits) {
10819
+ const parts = [];
10820
+ let thought = false;
10821
+ for (const block of blocksOf(message["content"])) {
10822
+ const type = block["type"];
10823
+ if (type === "text") {
10824
+ const text2 = block["text"];
10825
+ if (typeof text2 === "string" && text2.length > 0) parts.push({ kind: "text", ...cut(text2, limits.textMaxChars) });
10826
+ } else if (type === "thinking") {
10827
+ if (!thought) parts.push({ kind: "thinking" });
10828
+ thought = true;
10829
+ } else if (type === "toolCall") {
10830
+ const id = block["id"];
10831
+ const name = block["name"];
10832
+ if (typeof id !== "string" || id.length === 0 || typeof name !== "string" || name.length === 0) continue;
10833
+ const input = cut(prettyArguments(block["arguments"]), limits.toolInputMaxChars);
10834
+ parts.push({ kind: "tool-call", toolUseId: id, name, input: input.text, truncated: input.truncated });
10835
+ }
10836
+ }
10837
+ const text = (key) => {
10838
+ const value = message[key];
10839
+ return typeof value === "string" && value.length > 0 ? value : null;
10840
+ };
10841
+ return {
10842
+ kind: "assistant",
10843
+ parts,
10844
+ provider: text("provider"),
10845
+ model: text("model"),
10846
+ usage: readUsage2(message["usage"]),
10847
+ stopReason: text("stopReason"),
10848
+ thinkingLevel: text("thinkingLevel"),
10849
+ errorMessage: text("errorMessage")
10850
+ };
10851
+ }
10852
+ function readToolResult(message, limits) {
10853
+ const toolCallId = message["toolCallId"];
10854
+ if (typeof toolCallId !== "string" || toolCallId.length === 0) return { kind: "other" };
10855
+ const blocks = blocksOf(message["content"]);
10856
+ const output = cut(textOf(blocks), limits.toolResultMaxChars);
10857
+ return {
10858
+ kind: "toolResult",
10859
+ toolCallId,
10860
+ part: {
10861
+ kind: "tool-result",
10862
+ toolUseId: toolCallId,
10863
+ text: output.text,
10864
+ isError: message["isError"] === true,
10865
+ truncated: output.truncated,
10866
+ imageCount: blocks.filter((block) => block["type"] === "image").length
10867
+ }
10868
+ };
10869
+ }
10870
+ function readBash(message, limits) {
10871
+ const command = message["command"];
10872
+ if (typeof command !== "string" || command.length === 0) return { kind: "other" };
10873
+ const output = cut(typeof message["output"] === "string" ? message["output"] : "", limits.toolResultMaxChars);
10874
+ const exitCode = message["exitCode"];
10875
+ return {
10876
+ kind: "bash",
10877
+ command: cut(command, limits.toolInputMaxChars).text,
10878
+ output: output.text,
10879
+ failed: message["cancelled"] === true || typeof exitCode === "number" && exitCode !== 0,
10880
+ truncated: output.truncated || message["truncated"] === true
10881
+ };
10882
+ }
10883
+ function readFact(record, limits) {
10884
+ switch (record["type"]) {
10885
+ case "model_change": {
10886
+ const provider = record["provider"];
10887
+ const modelId = record["modelId"];
10888
+ return typeof provider === "string" && provider.length > 0 && typeof modelId === "string" && modelId.length > 0 ? { kind: "model", provider, modelId } : { kind: "other" };
10889
+ }
10890
+ case "thinking_level_change": {
10891
+ const level = record["thinkingLevel"];
10892
+ return typeof level === "string" && level.length > 0 ? { kind: "thinking", level } : { kind: "other" };
10893
+ }
10894
+ case "session_info": {
10895
+ const name = record["name"];
10896
+ return typeof name === "string" && name.trim().length > 0 ? { kind: "info", name: name.trim() } : { kind: "other" };
10897
+ }
10898
+ case "compaction":
10899
+ case "branch_summary":
10900
+ return { kind: "compaction" };
10901
+ case "message": {
10902
+ const message = asRecord(record["message"]);
10903
+ if (message === null) return { kind: "other" };
10904
+ switch (message["role"]) {
10905
+ case "user":
10906
+ return readUser(message, limits);
10907
+ case "assistant":
10908
+ return readAssistant(message, limits);
10909
+ case "toolResult":
10910
+ return readToolResult(message, limits);
10911
+ case "bashExecution":
10912
+ return readBash(message, limits);
10913
+ default:
10914
+ return { kind: "other" };
10915
+ }
10916
+ }
10917
+ default:
10918
+ return { kind: "other" };
10919
+ }
10920
+ }
10921
+ function readEntry(record, lineNumber, limits) {
10922
+ const type = record["type"];
10923
+ if (typeof type !== "string" || type === "session") return null;
10924
+ const id = record["id"];
10925
+ const parentId = record["parentId"];
10926
+ return {
10927
+ id: typeof id === "string" && id.length > 0 ? id : `line-${lineNumber}`,
10928
+ parentId: typeof parentId === "string" && parentId.length > 0 ? parentId : parentId === null ? null : void 0,
10929
+ at: stampOf(record["timestamp"]),
10930
+ fact: readFact(record, limits)
10931
+ };
10932
+ }
10933
+
10934
+ // packages/server/src/agents/pi/session-image.ts
10935
+ import { createReadStream as createReadStream2 } from "node:fs";
10936
+ import { readFile as readFile10, stat as stat17 } from "node:fs/promises";
10937
+ import path32 from "node:path";
10938
+ import { createInterface as createInterface2 } from "node:readline";
10939
+ function tooBig2(data) {
10940
+ return data.length > MAX_SUBMIT_IMAGE_BYTES * 1.4;
10941
+ }
10942
+ function imageOf(block) {
10943
+ if (block === null || block["type"] !== "image") return null;
10944
+ const data = block["data"];
10945
+ const mediaType = block["mimeType"];
10946
+ if (typeof data !== "string" || data.length === 0 || tooBig2(data)) return null;
10947
+ return { mediaType: typeof mediaType === "string" && mediaType.length > 0 ? mediaType : "image/png", data };
10948
+ }
10949
+ function blocksOf2(message) {
10950
+ const content = message?.["content"];
10951
+ if (!Array.isArray(content)) return [];
10952
+ return content.map((item) => asRecord(item)).filter((item) => item !== null);
10953
+ }
10954
+ function textOf2(message) {
10955
+ const content = message?.["content"];
10956
+ if (typeof content === "string") return content;
10957
+ return blocksOf2(message).filter((block) => block["type"] === "text" && typeof block["text"] === "string").map((block) => block["text"]).join("\n");
10958
+ }
10959
+ function sameFile(a, b) {
10960
+ const normalize = (value) => {
10961
+ const normalized = path32.normalize(value);
10962
+ return process.platform === "win32" ? normalized.toLowerCase() : normalized;
10963
+ };
10964
+ return normalize(a) === normalize(b);
10965
+ }
10966
+ async function readPastedFile2(target, pastedDir) {
10967
+ try {
10968
+ const inside = await resolveInside(pastedDir, path32.relative(pastedDir, target), { mustExist: true });
10969
+ const info = await stat17(inside);
10970
+ if (!info.isFile() || info.size === 0 || info.size > MAX_SUBMIT_IMAGE_BYTES) return null;
10971
+ const bytes = await readFile10(inside);
10972
+ const format = detectImageFormat(bytes);
10973
+ return format === null ? null : { mediaType: format.mediaType, data: bytes.toString("base64") };
10974
+ } catch {
10975
+ return null;
10976
+ }
10977
+ }
10978
+ function readCallOf(record, target) {
10979
+ const message = asRecord(record["message"]);
10980
+ if (record["type"] !== "message" || message?.["role"] !== "assistant") return null;
10981
+ for (const block of blocksOf2(message)) {
10982
+ if (block["type"] !== "toolCall") continue;
10983
+ const id = block["id"];
10984
+ const filePath = asRecord(block["arguments"])?.["path"];
10985
+ if (typeof id === "string" && id.length > 0 && typeof filePath === "string" && sameFile(filePath, target)) return id;
10986
+ }
10987
+ return null;
10988
+ }
10989
+ async function loadPiImage(file, eventId, index, source, pastedDir = pasteRoot()) {
10990
+ if (source === "attachment" || !Number.isInteger(index) || index < 0) return null;
10991
+ try {
10992
+ if (!(await stat17(file)).isFile()) return null;
10993
+ } catch {
10994
+ return null;
10995
+ }
10996
+ const stream = createReadStream2(file, { encoding: "utf8" });
10997
+ const lines = createInterface2({ input: stream, crlfDelay: Number.POSITIVE_INFINITY });
10998
+ stream.on("error", () => lines.close());
10999
+ let target = null;
11000
+ let readId = null;
11001
+ try {
11002
+ for await (const line of lines) {
11003
+ if (target === null) {
11004
+ if (!line.includes(eventId)) continue;
11005
+ const record2 = parseJsonlLine(line);
11006
+ if (record2 === null || record2["id"] !== eventId || record2["type"] !== "message") continue;
11007
+ const message2 = asRecord(record2["message"]);
11008
+ if (message2?.["role"] !== "user") return null;
11009
+ if (source === "content") {
11010
+ const images = blocksOf2(message2).filter((block) => block["type"] === "image");
11011
+ return imageOf(images[index] ?? null);
11012
+ }
11013
+ target = pastedImagePaths(textOf2(message2))[index] ?? null;
11014
+ if (target === null) return null;
11015
+ const fromDisk = await readPastedFile2(target, pastedDir);
11016
+ if (fromDisk !== null) return fromDisk;
11017
+ continue;
11018
+ }
11019
+ if (readId === null) {
11020
+ if (!line.includes('"toolCall"')) continue;
11021
+ const record2 = parseJsonlLine(line);
11022
+ readId = record2 === null ? null : readCallOf(record2, target);
11023
+ continue;
11024
+ }
11025
+ if (!line.includes('"toolResult"')) continue;
11026
+ const record = parseJsonlLine(line);
11027
+ const message = record === null ? null : asRecord(record["message"]);
11028
+ if (message === null || message["role"] !== "toolResult" || message["toolCallId"] !== readId) continue;
11029
+ for (const block of blocksOf2(message)) {
11030
+ const image = imageOf(block);
11031
+ if (image !== null) return image;
11032
+ }
11033
+ readId = null;
11034
+ }
11035
+ } catch {
11036
+ return null;
11037
+ } finally {
11038
+ lines.close();
11039
+ stream.destroy();
11040
+ }
11041
+ return null;
11042
+ }
11043
+
11044
+ // packages/server/src/agents/pi/session-path.ts
11045
+ function emptyState() {
11046
+ return {
11047
+ events: [],
11048
+ dropped: 0,
11049
+ meter: {
11050
+ provider: null,
11051
+ modelId: null,
11052
+ effort: null,
11053
+ lastRequestTokens: 0,
11054
+ lastOutputTokens: 0,
11055
+ totalInputTokens: 0,
11056
+ totalOutputTokens: 0,
11057
+ totalCacheReadTokens: 0,
11058
+ assistantMessages: 0
11059
+ },
11060
+ inFlight: false,
11061
+ turnStartedAt: null
11062
+ };
11063
+ }
11064
+ function modelKey(provider, modelId) {
11065
+ if (modelId === null || modelId.length === 0) return null;
11066
+ return provider === null || provider.length === 0 ? modelId : `${provider}/${modelId}`;
11067
+ }
11068
+ function messageUsage(usage) {
11069
+ return {
11070
+ inputTokens: usage.input,
11071
+ outputTokens: usage.output,
11072
+ cacheCreationInputTokens: usage.cacheWrite,
11073
+ cacheReadInputTokens: usage.cacheRead
11074
+ };
11075
+ }
11076
+ function event(eventId, role, at, parts, extra = {}) {
11077
+ return {
11078
+ eventId,
11079
+ role,
11080
+ at,
11081
+ parts,
11082
+ model: extra.model ?? null,
11083
+ usage: extra.usage ?? null,
11084
+ effort: extra.effort ?? null,
11085
+ durationMs: extra.durationMs ?? null,
11086
+ queued: extra.queued ?? false
11087
+ };
11088
+ }
11089
+ function step(state, entry) {
11090
+ const { fact } = entry;
11091
+ const added = [];
11092
+ switch (fact.kind) {
11093
+ // El nombre (`info`) no depende de la rama: lo lee `session-scan.ts` de la cola del archivo.
11094
+ case "other":
11095
+ case "info":
11096
+ break;
11097
+ case "model":
11098
+ state.meter = { ...state.meter, provider: fact.provider, modelId: fact.modelId };
11099
+ break;
11100
+ case "thinking":
11101
+ state.meter = { ...state.meter, effort: fact.level };
11102
+ break;
11103
+ case "compaction":
11104
+ added.push(event(`${entry.id}:notice`, "assistant", entry.at, [{ kind: "notice", notice: "compacted", detail: "" }]));
11105
+ break;
11106
+ case "user": {
11107
+ const queued = state.inFlight;
11108
+ if (!state.inFlight) state.turnStartedAt = entry.at;
11109
+ state.inFlight = true;
11110
+ if (fact.parts.length > 0) added.push(event(entry.id, "user", entry.at, fact.parts, { queued }));
11111
+ break;
11112
+ }
11113
+ case "assistant": {
11114
+ const provider = fact.provider ?? state.meter.provider;
11115
+ const modelId = fact.model ?? state.meter.modelId;
11116
+ const effort = fact.thinkingLevel ?? state.meter.effort;
11117
+ state.meter = { ...state.meter, provider, modelId, effort };
11118
+ const usage = fact.usage;
11119
+ const counted = usage !== null && usage.totalTokens > 0;
11120
+ if (usage !== null && counted) {
11121
+ state.meter = {
11122
+ ...state.meter,
11123
+ lastRequestTokens: usage.totalTokens,
11124
+ lastOutputTokens: usage.output,
11125
+ totalInputTokens: state.meter.totalInputTokens + usage.input + usage.cacheWrite,
11126
+ totalOutputTokens: state.meter.totalOutputTokens + usage.output,
11127
+ totalCacheReadTokens: state.meter.totalCacheReadTokens + usage.cacheRead,
11128
+ assistantMessages: state.meter.assistantMessages + 1
11129
+ };
11130
+ }
11131
+ const final = fact.stopReason !== "toolUse";
11132
+ let durationMs = null;
11133
+ if (final) {
11134
+ if (state.turnStartedAt !== null && state.turnStartedAt > 0 && entry.at >= state.turnStartedAt) {
11135
+ durationMs = entry.at - state.turnStartedAt;
11136
+ }
11137
+ state.inFlight = false;
11138
+ state.turnStartedAt = null;
11139
+ } else {
11140
+ state.inFlight = true;
11141
+ }
11142
+ const notice = fact.stopReason === "aborted" ? { kind: "notice", notice: "interrupted", detail: "" } : fact.stopReason === "error" ? { kind: "notice", notice: "error", detail: (fact.errorMessage ?? "").slice(0, NOTICE_DETAIL_MAX_CHARS2) } : null;
11143
+ if (fact.parts.length > 0) {
11144
+ added.push(
11145
+ event(entry.id, "assistant", entry.at, fact.parts, {
11146
+ model: modelKey(provider, modelId),
11147
+ usage: usage !== null && counted ? messageUsage(usage) : null,
11148
+ effort,
11149
+ // Con un aviso detras, la duracion va en el aviso: es el ultimo del turno.
11150
+ durationMs: notice === null ? durationMs : null
11151
+ })
11152
+ );
11153
+ }
11154
+ if (notice !== null) added.push(event(`${entry.id}:notice`, "assistant", entry.at, [notice], { durationMs }));
11155
+ break;
11156
+ }
11157
+ case "toolResult":
11158
+ state.inFlight = true;
11159
+ added.push(event(entry.id, "user", entry.at, [fact.part]));
11160
+ break;
11161
+ case "bash": {
11162
+ const toolUseId = `bash:${entry.id}`;
11163
+ added.push(
11164
+ event(entry.id, "user", entry.at, [
11165
+ { kind: "tool-call", toolUseId, name: "bash", input: fact.command, truncated: false },
11166
+ {
11167
+ kind: "tool-result",
11168
+ toolUseId,
11169
+ text: fact.output,
11170
+ isError: fact.failed,
11171
+ truncated: fact.truncated,
11172
+ imageCount: 0
11173
+ }
11174
+ ])
11175
+ );
11176
+ break;
11177
+ }
11178
+ }
11179
+ return added;
11180
+ }
11181
+ var PiSessionTree = class {
11182
+ /** Todas las entradas del archivo, con su padre ya resuelto. */
11183
+ nodes = /* @__PURE__ */ new Map();
11184
+ /** La ultima entrada del archivo: la punta de la rama activa. */
11185
+ leafId = null;
11186
+ state = emptyState();
11187
+ maxEvents;
11188
+ constructor(options = {}) {
11189
+ this.maxEvents = options.maxEvents ?? Number.POSITIVE_INFINITY;
11190
+ }
11191
+ reset() {
11192
+ this.nodes.clear();
11193
+ this.leafId = null;
11194
+ this.state = emptyState();
11195
+ }
11196
+ /** Agrega una entrada, en el orden del archivo. */
11197
+ add(entry) {
11198
+ const parent = entry.parentId === void 0 ? this.leafId : entry.parentId;
11199
+ const linear = parent === this.leafId;
11200
+ this.nodes.set(entry.id, { entry, parent });
11201
+ this.leafId = entry.id;
11202
+ if (linear) {
11203
+ const added = step(this.state, entry);
11204
+ this.keep(added);
11205
+ return { rebuilt: false, added };
11206
+ }
11207
+ this.rebuild();
11208
+ return { rebuilt: true, added: [] };
11209
+ }
11210
+ /** Rehace el estado recorriendo el camino de la raiz a la punta. */
11211
+ rebuild() {
11212
+ const chain = [];
11213
+ const seen = /* @__PURE__ */ new Set();
11214
+ let cursor = this.leafId;
11215
+ while (cursor !== null && !seen.has(cursor)) {
11216
+ seen.add(cursor);
11217
+ const node = this.nodes.get(cursor);
11218
+ if (node === void 0) break;
11219
+ chain.push(node.entry);
11220
+ cursor = node.parent;
11221
+ }
11222
+ this.state = emptyState();
11223
+ for (let index = chain.length - 1; index >= 0; index -= 1) {
11224
+ const entry = chain[index];
11225
+ if (entry !== void 0) this.keep(step(this.state, entry));
11226
+ }
11227
+ }
11228
+ keep(added) {
11229
+ for (const each of added) this.state.events.push(each);
11230
+ while (this.state.events.length > this.maxEvents) {
11231
+ this.state.events.shift();
11232
+ this.state.dropped += 1;
11233
+ }
11234
+ }
11235
+ /** Los eventos de la rama activa, en orden. No copiar ni modificar. */
11236
+ get events() {
11237
+ return this.state.events;
11238
+ }
11239
+ /** Eventos viejos descartados por el tope. */
11240
+ get dropped() {
11241
+ return this.state.dropped;
11242
+ }
11243
+ get meter() {
11244
+ return this.state.meter;
11245
+ }
11246
+ };
11247
+
11248
+ // packages/server/src/agents/pi/session-follower.ts
11249
+ var LOOKUP_INTERVAL_MS2 = 2e3;
11250
+ var PI_MAX_EVENTS = 4e3;
11251
+ var PI_FINGERPRINT_BYTES = 64;
11252
+ var PiSessionFollower = class {
11253
+ constructor(cwd, sessionId, options) {
11254
+ this.cwd = cwd;
11255
+ this.sessionId = sessionId;
11256
+ const limits = options.limits ?? TRANSPORT_LIMITS;
11257
+ this.catalog = options.catalog;
11258
+ this.now = options.now ?? Date.now;
11259
+ this.findFile = options.findFile ?? findSessionFile;
11260
+ this.tree = new PiSessionTree({ maxEvents: options.maxEvents ?? PI_MAX_EVENTS });
11261
+ const sink = {
11262
+ // Nunca devuelve un evento: los guarda el arbol, que sabe de que rama son.
11263
+ consume: (record, lineNumber) => {
11264
+ const entry = readEntry(record, lineNumber, limits);
11265
+ if (entry !== null) this.incoming.push(entry);
11266
+ return null;
11267
+ },
11268
+ reset: () => {
11269
+ this.tree.reset();
11270
+ this.incoming = [];
11271
+ }
11272
+ };
11273
+ this.jsonl = new JsonlFollower(null, sink, { fingerprintBytes: PI_FINGERPRINT_BYTES });
11274
+ }
11275
+ cwd;
11276
+ sessionId;
11277
+ tree;
11278
+ jsonl;
11279
+ catalog;
11280
+ now;
11281
+ findFile;
11282
+ /** Las entradas leidas en esta vuelta, en orden de archivo, todavia sin aplicar. */
11283
+ incoming = [];
11284
+ usage = { ...EMPTY_CONTEXT_USAGE };
11285
+ /** El modelo para el que vale `window`. */
11286
+ windowKey = null;
11287
+ window = null;
11288
+ lastLookupAt = null;
11289
+ get label() {
11290
+ const filePath = this.jsonl.filePath;
11291
+ if (filePath !== null) return filePath;
11292
+ return this.sessionId.length > 0 ? `pi:${this.sessionId.slice(0, 8)}` : "pi:(no session)";
11293
+ }
11294
+ async start() {
11295
+ await this.lookup();
11296
+ }
11297
+ async poll() {
11298
+ if (this.jsonl.filePath === null && (this.lastLookupAt === null || this.now() - this.lastLookupAt >= LOOKUP_INTERVAL_MS2)) {
11299
+ await this.lookup();
11300
+ }
11301
+ const before = JSON.stringify(this.usage);
11302
+ const { reset } = await this.jsonl.poll();
11303
+ let rebuilt = reset;
11304
+ const added = [];
11305
+ for (const entry of this.incoming.splice(0)) {
11306
+ const result = this.tree.add(entry);
11307
+ if (result.rebuilt) rebuilt = true;
11308
+ else if (!rebuilt) added.push(...result.added);
11309
+ }
11310
+ await this.refreshUsage();
11311
+ if (rebuilt) return { reset: true, added: [], turns: [], plans: [], parts: [] };
11312
+ return {
11313
+ reset: false,
11314
+ added,
11315
+ turns: [],
11316
+ plans: [],
11317
+ parts: [],
11318
+ // `/model` y `/thinking` escriben su entrada en el acto (16.10): cambian el uso sin eventos.
11319
+ usageChanged: added.length === 0 && JSON.stringify(this.usage) !== before
11320
+ };
11321
+ }
11322
+ async lookup() {
11323
+ if (!isPiSessionId(this.sessionId) || this.jsonl.filePath !== null) return;
11324
+ this.lastLookupAt = this.now();
11325
+ const found = await this.findFile(this.sessionId, this.cwd);
11326
+ if (found !== null && this.jsonl.filePath === null) this.jsonl.setPath(found);
11327
+ }
11328
+ /** El medidor de la rama activa, con la ventana que el catalogo le da a su modelo. */
11329
+ async refreshUsage() {
11330
+ const meter = this.tree.meter;
11331
+ const key = modelKey(meter.provider, meter.modelId);
11332
+ if (key !== this.windowKey) {
11333
+ this.window = await this.catalog.contextWindow(meter.provider, meter.modelId);
11334
+ this.windowKey = key;
11335
+ } else if (this.window === null && key !== null) {
11336
+ this.window = await this.catalog.contextWindow(meter.provider, meter.modelId);
11337
+ }
11338
+ this.usage = {
11339
+ lastRequestTokens: meter.lastRequestTokens,
11340
+ lastOutputTokens: meter.lastOutputTokens,
11341
+ lastModel: key,
11342
+ contextWindow: this.window,
11343
+ contextWindowEstimated: false,
11344
+ totalInputTokens: meter.totalInputTokens,
11345
+ totalOutputTokens: meter.totalOutputTokens,
11346
+ totalCacheReadTokens: meter.totalCacheReadTokens,
11347
+ assistantMessages: meter.assistantMessages,
11348
+ ...meter.effort !== null ? { lastEffort: meter.effort } : {}
11349
+ };
11350
+ }
11351
+ /**
11352
+ * true si el aviso es de su archivo. Si todavia no tiene uno y el aviso es un
11353
+ * archivo con su id, se queda con ese: es el watcher llegando antes que el
11354
+ * reintento de busqueda.
11355
+ */
11356
+ noticeChange(filePath) {
11357
+ const own = this.jsonl.filePath;
11358
+ if (own !== null) {
11359
+ return own === filePath || normalizeCwdKey(own, process.platform) === normalizeCwdKey(filePath, process.platform);
11360
+ }
11361
+ if (!isPiSessionId(this.sessionId)) return false;
11362
+ if (parseSessionFileName(path33.basename(filePath))?.sessionId !== this.sessionId) return false;
11363
+ this.jsonl.setPath(filePath);
11364
+ return true;
11365
+ }
11366
+ async readImage(eventId, index, source) {
11367
+ const filePath = this.jsonl.filePath;
11368
+ if (filePath === null) return null;
11369
+ return loadPiImage(filePath, eventId, index, source);
11370
+ }
11371
+ /**
11372
+ * Siempre false: Pi no pide permiso antes de usar sus herramientas, asi que
11373
+ * una llamada sin resultado nunca es un menu de aprobacion abierto. Lo unico
11374
+ * que frena el cuadro es su dialogo de confianza, y eso lo dice el estado
11375
+ * (`status.ts`).
11376
+ */
11377
+ hasOpenToolCall() {
11378
+ return false;
11379
+ }
11380
+ getState() {
11381
+ return this.jsonl.getState();
11382
+ }
11383
+ getUsage() {
11384
+ return this.usage;
11385
+ }
11386
+ /** Pi no tiene modos de permiso. */
11387
+ getPermissionMode() {
11388
+ return null;
11389
+ }
11390
+ getTail(limit) {
11391
+ const all = this.tree.events;
11392
+ const events = all.slice(-limit);
11393
+ return { events, hasMore: all.length > events.length || this.tree.dropped > 0 };
11394
+ }
11395
+ getPageBefore(beforeEventId, limit) {
11396
+ const all = this.tree.events;
11397
+ const index = all.findIndex((event2) => event2.eventId === beforeEventId);
11398
+ if (index <= 0) return { events: [], hasMore: false };
11399
+ const start = Math.max(0, index - limit);
11400
+ return { events: all.slice(start, index), hasMore: start > 0 || this.tree.dropped > 0 };
11401
+ }
11402
+ getPlanFiles() {
11403
+ return [];
11404
+ }
11405
+ };
11406
+
11407
+ // packages/server/src/agents/pi/session-scan.ts
11408
+ var PI_HEAD_MAX_LINES = 40;
11409
+ var PI_HEAD_MAX_BYTES = 512 * 1024;
11410
+ var TAIL_MAX_BYTES3 = 128 * 1024;
11411
+ async function readSessionHeader(file) {
11412
+ const [first] = await readHeadLines(file, { maxLines: 1, maxBytes: 64 * 1024 });
11413
+ const record = first === void 0 ? null : parseJsonlLine(first);
11414
+ return record === null ? null : readHeader(record);
11415
+ }
11416
+ function firstUserText2(lines) {
11417
+ for (const [index, line] of lines.entries()) {
11418
+ const record = parseJsonlLine(line);
11419
+ const entry = record === null ? null : readEntry(record, index + 1, TRANSPORT_LIMITS);
11420
+ if (entry === null || entry.fact.kind !== "user") continue;
11421
+ for (const part of entry.fact.parts) {
11422
+ if (part.kind === "text" && part.text.trim().length > 0) return part.text;
11423
+ }
11424
+ }
11425
+ return "";
11426
+ }
11427
+ function lastSessionName(lines) {
11428
+ for (let index = lines.length - 1; index >= 0; index -= 1) {
11429
+ const line = lines[index];
11430
+ if (line === void 0 || !line.includes('"session_info"')) continue;
11431
+ const record = parseJsonlLine(line);
11432
+ const entry = record === null ? null : readEntry(record, index + 1, TRANSPORT_LIMITS);
11433
+ if (entry !== null && entry.fact.kind === "info") return entry.fact.name;
11434
+ }
11435
+ return "";
11436
+ }
11437
+ async function scanPiSession(item) {
11438
+ const found = { header: null };
11439
+ const head = await readHeadLines(item.ref, {
11440
+ maxLines: PI_HEAD_MAX_LINES,
11441
+ maxBytes: PI_HEAD_MAX_BYTES,
11442
+ // Se decide con la primera linea: si no es una cabecera, no se lee mas.
11443
+ stopAfter: (line, index) => {
11444
+ if (index > 0) return false;
11445
+ const record = parseJsonlLine(line);
11446
+ found.header = record === null ? null : readHeader(record);
11447
+ return found.header === null;
11448
+ }
11449
+ });
11450
+ const header = found.header;
11451
+ if (header === null || header.sessionId !== item.sessionId) return null;
11452
+ const tail = await readTailLines(item.ref, { maxBytes: TAIL_MAX_BYTES3 });
11453
+ const name = toTitle(lastSessionName(tail) || lastSessionName(head));
11454
+ const first = name.length > 0 ? "" : toTitle(firstUserText2(head));
11455
+ const title = name.length > 0 ? name : first;
11456
+ return {
11457
+ cwd: header.cwd,
11458
+ summary: {
11459
+ sessionId: item.sessionId,
11460
+ title: title.length > 0 ? title : UNTITLED_SESSION_TITLE,
11461
+ titleSource: name.length > 0 ? "custom" : first.length > 0 ? "first-message" : "none",
11462
+ updatedAt: item.mtimeMs,
11463
+ sizeBytes: item.sizeBytes
11464
+ },
11465
+ extra: {}
11466
+ };
11467
+ }
11468
+
11469
+ // packages/server/src/agents/pi/history.ts
11470
+ var PI_FOLLOW_POLL_MS = 1e3;
11471
+ var accepts2 = (filePath) => parseSessionFileName(path34.basename(filePath)) !== null;
11472
+ function toItem3(ref, info) {
11473
+ const name = parseSessionFileName(path34.basename(ref));
11474
+ if (name === null) return null;
11475
+ return {
11476
+ ref,
11477
+ sessionId: name.sessionId,
11478
+ // La carpeta de proyecto: respaldo del `cwd` si la primera linea no lo trajera.
11479
+ group: path34.basename(path34.dirname(ref)),
11480
+ mtimeMs: info.mtimeMs,
11481
+ sizeBytes: info.size
11482
+ };
11483
+ }
11484
+ async function itemOf3(ref) {
11485
+ try {
11486
+ const info = await stat18(ref);
11487
+ return info.isFile() ? toItem3(ref, info) : null;
11488
+ } catch {
11489
+ return null;
11490
+ }
11491
+ }
11492
+ function ignoresPath(root, filePath) {
11493
+ const relative = path34.relative(root, filePath).split(path34.sep).join("/");
11494
+ if (relative.length === 0) return false;
11495
+ if (relative.startsWith("..")) return true;
11496
+ const parts = relative.split("/");
11497
+ const [folder, file] = parts;
11498
+ if (folder === void 0 || !isProjectFolderName(folder)) return true;
11499
+ if (parts.length === 1) return false;
11500
+ return parts.length > 2 || file === void 0 || parseSessionFileName(file) === null;
11501
+ }
11502
+ function createPiHistory(catalog) {
11503
+ return {
11504
+ /*
11505
+ La raiz siempre, exista o no: `sessions/` no existe hasta que Pi se abre
11506
+ por primera vez. Que hacer con una raiz que todavia no esta es del
11507
+ watcher, que la espera sin vigilar a su padre.
11508
+ */
11509
+ roots() {
11510
+ const layout = piSessionsLayout();
11511
+ if (layout === null) return [];
11512
+ const awaitWriteFinish = { stabilityThreshold: 300, pollInterval: 100 };
11513
+ if (layout.flat) return [{ path: layout.root, depth: 0, accepts: accepts2, awaitWriteFinish }];
11514
+ const root = layout.root;
11515
+ return [{ path: root, depth: 1, accepts: accepts2, ignore: (filePath) => ignoresPath(root, filePath), awaitWriteFinish }];
11516
+ },
11517
+ async list() {
11518
+ const layout = piSessionsLayout();
11519
+ if (layout === null) return null;
11520
+ try {
11521
+ if (!(await stat18(layout.root)).isDirectory()) return null;
11522
+ } catch {
11523
+ return null;
11524
+ }
11525
+ const items = [];
11526
+ for (const ref of await listSessionFiles(layout)) {
11527
+ const item = await itemOf3(ref);
11528
+ if (item !== null) items.push(item);
11529
+ }
11530
+ return items;
11531
+ },
11532
+ async changedRefs(filePath) {
11533
+ if (!accepts2(filePath)) return null;
11534
+ const layout = piSessionsLayout();
11535
+ return layout !== null && isInside3(layout.root, filePath) ? [filePath] : null;
11536
+ },
11537
+ item: (ref) => itemOf3(ref),
11538
+ scan: (item) => scanPiSession(item),
11539
+ // Pi no aprende nada de la instalacion al leer una sesion.
11540
+ restored: () => void 0,
11541
+ /** true si esa sesion tiene archivo: Pi lo crea con el primer mensaje. */
11542
+ exists: async (cwd, sessionId) => await findSessionFile(sessionId, cwd) !== null,
11543
+ follow(target, options) {
11544
+ return new PiSessionFollower(target.cwd, target.sessionId, {
11545
+ catalog,
11546
+ limits: options?.limits,
11547
+ maxEvents: options?.maxEvents
11548
+ });
11549
+ },
11550
+ // `follow` respeta `FollowOptions`: el respaldo puede leer de aca.
11551
+ wholeRead: true,
11552
+ plans: null,
11553
+ /*
11554
+ Pi escribe cada entrada cuando termina su mensaje. Releer cada segundo es
11555
+ un `stat` y los bytes nuevos, y no depende de que el watcher vea lo que se
11556
+ agrega a un archivo que otro proceso tiene abierto (lo que obligo a lo
11557
+ mismo con Codex).
11558
+ */
11559
+ followPollMs: PI_FOLLOW_POLL_MS
11560
+ };
11561
+ }
11562
+
11563
+ // packages/server/src/agents/pi/live.ts
11564
+ import { open as open6, stat as stat19 } from "node:fs/promises";
11565
+ var PI_LIVE_POLL_MS = 500;
11566
+ var PI_ERROR_CALM_MS = 2e3;
11567
+ var PI_TRUST_WAITING = "trust dialog";
11568
+ var USER_MESSAGES_KEPT = 30;
11569
+ var CLOCK_SLACK_MS = 2e3;
11570
+ var READ_CHUNK_BYTES2 = 256 * 1024;
11571
+ var NEWLINE3 = 10;
11572
+ function isPiCommandText(text) {
11573
+ return /^\s*[/!]/.test(text);
11574
+ }
11575
+ var PiLiveSessions = class {
11576
+ sessions = /* @__PURE__ */ new Map();
11577
+ subscriptions = /* @__PURE__ */ new Set();
11578
+ findFile;
11579
+ now;
11580
+ pollMs;
11581
+ timer = null;
11582
+ constructor(deps = {}) {
11583
+ this.findFile = deps.findFile ?? findSessionFile;
11584
+ this.now = deps.now ?? Date.now;
11585
+ this.pollMs = deps.pollMs ?? PI_LIVE_POLL_MS;
11586
+ }
11587
+ /**
11588
+ * Una sesion gana un proceso. `file` es su archivo si ya se conoce; `fromStart`
11589
+ * lo lee desde el principio (una sesion a la que la pestana acaba de pasar),
11590
+ * y si no, desde donde esta ahora: lo anterior es de otro proceso.
11591
+ */
11592
+ attach(sessionId, options) {
11593
+ const live = {
11594
+ sessionId,
11595
+ cwd: options.cwd,
11596
+ launchedAt: options.launchedAt,
11597
+ file: options.file ?? null,
11598
+ offset: 0,
11599
+ pending: Buffer.alloc(0),
11600
+ lineNumber: 0,
11601
+ blocked: false,
11602
+ turn: null,
11603
+ userMessages: [],
11604
+ settledAt: null,
11605
+ reading: Promise.resolve()
11606
+ };
11607
+ this.sessions.set(sessionId, live);
11608
+ live.reading = this.prime(live, options.fromStart === true).then(() => this.publish(sessionId));
11609
+ this.start();
11610
+ }
11611
+ /** El proceso de esa sesion termino, o la pestana paso a otra. */
11612
+ detach(sessionId) {
11613
+ if (!this.sessions.delete(sessionId)) return;
11614
+ this.publish(sessionId);
11615
+ if (this.sessions.size === 0) this.stop();
11616
+ }
11617
+ /** El archivo que se esta leyendo para esa sesion, o null. */
11618
+ fileOf(sessionId) {
11619
+ return this.sessions.get(sessionId)?.file ?? null;
11620
+ }
11621
+ /** El dialogo de confianza se abrio o se cerro. */
11622
+ setBlocked(sessionId, blocked) {
11623
+ const live = this.sessions.get(sessionId);
11624
+ if (live === void 0 || live.blocked === blocked) return;
11625
+ live.blocked = blocked;
11626
+ this.publish(sessionId);
11627
+ }
11628
+ /** true si la sesion ya recibio un mensaje del usuario con ese texto, mandado en `since` o despues. */
11629
+ delivered(sessionId, submission) {
11630
+ const live = this.sessions.get(sessionId);
11631
+ if (live === void 0) return false;
11632
+ return live.userMessages.some(
11633
+ (message) => message.at >= submission.at - CLOCK_SLACK_MS && plainMatches(message.plain, submission.text)
11634
+ );
11635
+ }
11636
+ /** true si la sesion recibio algun mensaje del usuario en `since` o despues. */
11637
+ userMessageSince(sessionId, since) {
11638
+ return this.sessions.get(sessionId)?.userMessages.some((message) => message.at >= since) ?? false;
11639
+ }
11640
+ /**
11641
+ * Despues de interrumpir desde la app: si el turno ya quedo cerrado en el
11642
+ * archivo, y cuales de esos mensajes no llegaron nunca a la sesion (los que Pi
11643
+ * tenia en cola y devolvio a su editor, 16.5). Lee el archivo en el acto.
11644
+ */
11645
+ async check(sessionId, submissions, interruptedAt) {
11646
+ const live = this.sessions.get(sessionId);
11647
+ if (live === void 0) return { settled: false, undelivered: [] };
11648
+ await this.advance(live);
11649
+ return {
11650
+ settled: live.settledAt !== null && live.settledAt >= interruptedAt - CLOCK_SLACK_MS / 4,
11651
+ undelivered: submissions.filter((submission) => !isPiCommandText(submission.text) && !this.delivered(sessionId, submission)).map((each) => each.text)
11652
+ };
11653
+ }
11654
+ /** El estado de esa sesion ahora, o null si no tiene proceso. */
11655
+ statusOf(sessionId) {
11656
+ const live = this.sessions.get(sessionId);
11657
+ if (live === void 0) return null;
11658
+ if (live.blocked) return { activity: "waiting", waitingFor: PI_TRUST_WAITING };
11659
+ const turn = live.turn;
11660
+ if (turn === null) return { activity: "idle", waitingFor: null };
11661
+ if (turn.state === "working") return { activity: "busy", waitingFor: null };
11662
+ if (turn.state === "errored" && this.now() - turn.seenAt < PI_ERROR_CALM_MS) return { activity: "busy", waitingFor: null };
11663
+ return { activity: "idle", waitingFor: null };
11664
+ }
11665
+ subscribe(sessionId, listener) {
11666
+ const subscription = { sessionId, listener, last: null };
11667
+ this.subscriptions.add(subscription);
11668
+ this.deliver(subscription);
11669
+ return () => {
11670
+ this.subscriptions.delete(subscription);
11671
+ };
11672
+ }
11673
+ /** Lee ya lo nuevo de esa sesion y avisa lo que cambio. */
11674
+ async refresh(sessionId) {
11675
+ const live = this.sessions.get(sessionId);
11676
+ if (live === void 0) return;
11677
+ await this.advance(live);
11678
+ this.publish(sessionId);
11679
+ }
11680
+ /** Una vuelta del sondeo. Publica para el chequeo. */
11681
+ async tick() {
11682
+ for (const live of [...this.sessions.values()]) {
11683
+ await this.advance(live);
11684
+ this.publish(live.sessionId);
11685
+ }
11686
+ }
11687
+ dispose() {
11688
+ this.subscriptions.clear();
11689
+ this.sessions.clear();
11690
+ this.stop();
11691
+ }
11692
+ start() {
11693
+ if (this.timer !== null) return;
11694
+ this.timer = setInterval(() => void this.tick(), this.pollMs);
11695
+ this.timer.unref();
11696
+ }
11697
+ stop() {
11698
+ if (this.timer === null) return;
11699
+ clearInterval(this.timer);
11700
+ this.timer = null;
11701
+ }
11702
+ publish(sessionId) {
11703
+ for (const subscription of this.subscriptions) {
11704
+ if (subscription.sessionId === sessionId) this.deliver(subscription);
11705
+ }
11706
+ }
11707
+ deliver(subscription) {
11708
+ const status = this.statusOf(subscription.sessionId);
11709
+ const key = status === null ? "none" : `${status.activity}|${status.waitingFor ?? ""}`;
11710
+ if (key === subscription.last) return;
11711
+ subscription.last = key;
11712
+ subscription.listener(status);
11713
+ }
11714
+ /** Fija desde donde se lee: el final de lo que ya habia, salvo `fromStart`. */
11715
+ async prime(live, fromStart) {
11716
+ if (live.file === null) live.file = await this.findFile(live.sessionId, live.cwd).catch(() => null);
11717
+ if (live.file === null || fromStart) return;
11718
+ try {
11719
+ live.offset = (await stat19(live.file)).size;
11720
+ } catch {
11721
+ live.offset = 0;
11722
+ }
11723
+ }
11724
+ /** Lee las lineas nuevas del archivo, en serie con la lectura anterior. Nunca lanza. */
11725
+ advance(live) {
11726
+ live.reading = live.reading.then(() => this.readNew(live)).catch(() => void 0);
11727
+ return live.reading;
11728
+ }
11729
+ async readNew(live) {
11730
+ if (this.sessions.get(live.sessionId) !== live) return;
11731
+ if (live.file === null) {
11732
+ live.file = await this.findFile(live.sessionId, live.cwd).catch(() => null);
11733
+ if (live.file === null) return;
11734
+ }
11735
+ let size;
11736
+ try {
11737
+ size = (await stat19(live.file)).size;
11738
+ } catch {
11739
+ return;
11740
+ }
11741
+ if (size < live.offset) {
11742
+ live.offset = 0;
11743
+ live.pending = Buffer.alloc(0);
11744
+ live.lineNumber = 0;
11745
+ }
11746
+ if (size === live.offset) return;
11747
+ const handle = await open6(live.file, "r");
11748
+ try {
11749
+ const buffer = Buffer.allocUnsafe(READ_CHUNK_BYTES2);
11750
+ while (live.offset < size) {
11751
+ const { bytesRead } = await handle.read(buffer, 0, Math.min(READ_CHUNK_BYTES2, size - live.offset), live.offset);
11752
+ if (bytesRead === 0) break;
11753
+ live.offset += bytesRead;
11754
+ const combined = Buffer.concat([live.pending, buffer.subarray(0, bytesRead)]);
11755
+ let start = 0;
11756
+ let newline = combined.indexOf(NEWLINE3, start);
11757
+ while (newline !== -1) {
11758
+ this.consume(live, combined.subarray(start, newline).toString("utf8"));
11759
+ start = newline + 1;
11760
+ newline = combined.indexOf(NEWLINE3, start);
11761
+ }
11762
+ live.pending = Buffer.from(combined.subarray(start));
11763
+ }
11764
+ } finally {
11765
+ await handle.close();
11766
+ }
11767
+ }
11768
+ consume(live, line) {
11769
+ live.lineNumber += 1;
11770
+ if (line.trim().length === 0) return;
11771
+ const record = parseJsonlLine(line);
11772
+ const entry = record === null ? null : readEntry(record, live.lineNumber, TRANSPORT_LIMITS);
11773
+ if (entry === null) return;
11774
+ if (entry.at !== 0 && entry.at < live.launchedAt - CLOCK_SLACK_MS) return;
11775
+ const seenAt = this.now();
11776
+ const { fact } = entry;
11777
+ if (fact.kind === "user") {
11778
+ live.turn = { state: "working", at: entry.at, seenAt };
11779
+ live.userMessages.push({ plain: fact.plain, at: entry.at });
11780
+ if (live.userMessages.length > USER_MESSAGES_KEPT) live.userMessages.shift();
11781
+ } else if (fact.kind === "toolResult") {
11782
+ live.turn = { state: "working", at: entry.at, seenAt };
11783
+ } else if (fact.kind === "assistant") {
11784
+ if (fact.stopReason === "toolUse") {
11785
+ live.turn = { state: "working", at: entry.at, seenAt };
11786
+ } else {
11787
+ live.turn = { state: fact.stopReason === "error" ? "errored" : "idle", at: entry.at, seenAt };
11788
+ live.settledAt = entry.at === 0 ? seenAt : entry.at;
11789
+ }
11790
+ } else if (fact.kind === "bash") {
11791
+ live.settledAt = entry.at === 0 ? seenAt : entry.at;
11792
+ if (live.turn === null || live.turn.state !== "working") live.turn = { state: "idle", at: entry.at, seenAt };
11793
+ }
11794
+ }
11795
+ };
11796
+ var PiStatusSource = class {
11797
+ constructor(live) {
11798
+ this.live = live;
11799
+ }
11800
+ live;
11801
+ subscribe(sessionId, listener) {
11802
+ return this.live.subscribe(sessionId, listener);
11803
+ }
11804
+ /**
11805
+ * Siempre false: que el archivo diga "libre" no dice que la interfaz de Pi
11806
+ * este lista para un pegado, y antes del primer mensaje ni siquiera hay
11807
+ * archivo (`readySignal` es false).
11808
+ */
11809
+ waitUntilReady() {
11810
+ return Promise.resolve(false);
11811
+ }
11812
+ refresh(sessionId) {
11813
+ return this.live.refresh(sessionId);
11814
+ }
11815
+ dispose() {
11816
+ this.live.dispose();
11817
+ }
11818
+ };
11819
+
11820
+ // packages/server/src/agents/pi/locate.ts
11821
+ import { readFile as readFile11, realpath as realpath2 } from "node:fs/promises";
11822
+ import path35 from "node:path";
11823
+ var PACKAGE_PARTS = PI_PACKAGE_NAME.split("/");
11824
+ var MANAGED_VERSION_PATTERN = /^[0-9A-Za-z._+-]+$/;
11825
+ var WALK_UP_LEVELS = 6;
11826
+ async function packageVersion(dir, read) {
11827
+ try {
11828
+ const manifest = asRecord(JSON.parse(await read(path35.join(dir, "package.json"))));
11829
+ if (manifest === null || manifest["name"] !== PI_PACKAGE_NAME) return null;
11830
+ const version = manifest["version"];
11831
+ return typeof version === "string" && version.length > 0 ? version : null;
11832
+ } catch {
11833
+ return null;
11834
+ }
11835
+ }
11836
+ async function candidateDirs(resolvedPath, deps) {
11837
+ const dir = path35.dirname(resolvedPath);
11838
+ const candidates = [
11839
+ path35.join(dir, "node_modules", ...PACKAGE_PARTS),
11840
+ path35.join(dir, "..", "lib", "node_modules", ...PACKAGE_PARTS)
11841
+ ];
11842
+ try {
11843
+ const installRoot = path35.join(dir, "..", "install");
11844
+ const version = (await deps.readFile(path35.join(installRoot, "current-version"))).trim();
11845
+ if (version !== "." && version !== ".." && MANAGED_VERSION_PATTERN.test(version)) {
11846
+ candidates.push(path35.join(installRoot, "releases", version, "node_modules", ...PACKAGE_PARTS));
11847
+ }
11848
+ } catch {
11849
+ }
11850
+ try {
11851
+ let cursor = path35.dirname(await deps.realpath(resolvedPath));
11852
+ for (let level = 0; level < WALK_UP_LEVELS; level += 1) {
11853
+ candidates.push(cursor);
11854
+ const parent = path35.dirname(cursor);
11855
+ if (parent === cursor) break;
11856
+ cursor = parent;
11857
+ }
11858
+ } catch {
11859
+ }
11860
+ return candidates;
11861
+ }
11862
+ var warnedForeign = /* @__PURE__ */ new Set();
11863
+ async function locatePi(overrides = {}) {
11864
+ const deps = {
11865
+ findInPath: overrides.findInPath ?? findInPath,
11866
+ readFile: overrides.readFile ?? ((file) => readFile11(file, "utf8")),
11867
+ realpath: overrides.realpath ?? realpath2
11868
+ };
11869
+ const resolvedPath = await deps.findInPath(PI_COMMAND);
11870
+ if (resolvedPath === null) return null;
11871
+ for (const packageDir of await candidateDirs(resolvedPath, deps)) {
11872
+ const version = await packageVersion(packageDir, deps.readFile);
11873
+ if (version !== null) return { resolvedPath, ...launcherFor(resolvedPath), version, packageDir };
11874
+ }
11875
+ if (!warnedForeign.has(resolvedPath)) {
11876
+ warnedForeign.add(resolvedPath);
11877
+ console.warn(
11878
+ `[pi] found a "pi" command at ${resolvedPath}, but not the ${PI_PACKAGE_NAME} package next to it: it isn't used.`
11879
+ );
11880
+ }
11881
+ return null;
11882
+ }
11883
+
11884
+ // packages/server/src/agents/pi/tab-watch.ts
11885
+ import { readdir as readdir10, stat as stat20 } from "node:fs/promises";
11886
+ import path36 from "node:path";
11887
+ var TRUST_TITLE = "Trust project folder?";
11888
+ var INTERFACE_STARTED = "\x1B[?1049h";
11889
+ function trustDialogOpen(output) {
11890
+ const title = output.lastIndexOf(TRUST_TITLE);
11891
+ return title !== -1 && output.lastIndexOf(INTERFACE_STARTED) < title;
11892
+ }
11893
+ function interfaceStarted(output) {
11894
+ return output.includes(INTERFACE_STARTED) && !trustDialogOpen(output);
11895
+ }
11896
+ var STARTUP_CHECK_MS = 300;
11897
+ var STARTUP_DEADLINE_MS = 6e4;
11898
+ var SCAN_DELAYS_MS = [400, 1500, 4e3];
11899
+ var TYPED_WINDOW_MS = 3e3;
11900
+ var CLOCK_SLACK_MS2 = 2e3;
11901
+ var SUBMISSIONS_KEPT = 20;
11902
+ var SUBMISSION_MAX_AGE_MS = 5 * 6e4;
11903
+ var PEEK_TAIL_BYTES = 256 * 1024;
11904
+ async function listSessionsIn(dir) {
11905
+ let names;
11906
+ try {
11907
+ names = await readdir10(dir);
11908
+ } catch {
11909
+ return [];
11910
+ }
11911
+ const files = [];
11912
+ for (const name of names) {
11913
+ if (parseSessionFileName(name) === null) continue;
11914
+ try {
11915
+ const info = await stat20(path36.join(dir, name));
11916
+ if (info.isFile()) files.push({ file: path36.join(dir, name), size: info.size, mtimeMs: info.mtimeMs });
11917
+ } catch {
11918
+ }
11919
+ }
11920
+ return files;
11921
+ }
11922
+ async function peekSession(file) {
11923
+ try {
11924
+ const header = await readSessionHeader(file);
11925
+ if (header === null) return null;
11926
+ const tail = await readTailLines(file, { maxBytes: PEEK_TAIL_BYTES });
11927
+ for (let index = tail.length - 1; index >= 0; index -= 1) {
11928
+ const line = tail[index];
11929
+ if (line === void 0 || !line.includes('"user"')) continue;
11930
+ const record = parseJsonlLine(line);
11931
+ const entry = record === null ? null : readEntry(record, index + 1, TRANSPORT_LIMITS);
11932
+ if (entry !== null && entry.fact.kind === "user") return { header, lastUser: { plain: entry.fact.plain, at: entry.at } };
11933
+ }
11934
+ return { header, lastUser: null };
11935
+ } catch {
11936
+ return null;
11937
+ }
11938
+ }
11939
+ var PiTabWatch = class {
11940
+ constructor(context, deps) {
11941
+ this.context = context;
11942
+ this.live = deps.live;
11943
+ this.tabs = deps.tabs;
11944
+ this.listSessions = deps.listSessions ?? listSessionsIn;
11945
+ this.peek = deps.peek ?? peekSession;
11946
+ this.sessionDir = deps.sessionDir ?? ((cwd) => sessionDirFor(cwd));
11947
+ this.now = deps.now ?? Date.now;
11948
+ this.platform = deps.platform ?? process.platform;
11949
+ this.useTimers = deps.timers ?? true;
11950
+ this.sessionId = isPiSessionId(context.sessionId) ? context.sessionId : "";
11951
+ }
11952
+ context;
11953
+ live;
11954
+ tabs;
11955
+ listSessions;
11956
+ peek;
11957
+ sessionDir;
11958
+ now;
11959
+ platform;
11960
+ useTimers;
11961
+ /** La sesion en la que esta la pestana ahora. `''` si el id con que nacio no es de Pi. */
11962
+ sessionId;
11963
+ /** El archivo de esa sesion, si ya se sabe cual es. */
11964
+ file = null;
11965
+ /** Tamano de cada archivo de la carpeta la ultima vez que se miro. null: todavia no se miro. */
11966
+ sizes = null;
11967
+ submissions = [];
11968
+ enterAt = null;
11969
+ closed = false;
11970
+ scanning = Promise.resolve();
11971
+ startupTimer = null;
11972
+ scanTimers = /* @__PURE__ */ new Set();
11973
+ /** El archivo de la sesion en curso, para que otra pestana no se la quede. */
11974
+ get currentFile() {
11975
+ return this.file ?? (this.sessionId.length > 0 ? this.live.fileOf(this.sessionId) : null);
11976
+ }
11977
+ get cwd() {
11978
+ return this.context.cwd;
11979
+ }
11980
+ /** Epoch ms del ultimo Enter escrito en la pty de esta pestana, o null. */
11981
+ get lastEnterAt() {
11982
+ return this.enterAt;
11983
+ }
11984
+ start() {
11985
+ if (this.sessionId.length > 0) {
11986
+ this.live.attach(this.sessionId, { cwd: this.context.cwd, launchedAt: this.context.launchedAt });
11987
+ }
11988
+ void this.scan();
11989
+ if (!this.useTimers) return;
11990
+ this.startupTimer = setInterval(() => this.checkTrust(), STARTUP_CHECK_MS);
11991
+ this.startupTimer.unref();
11992
+ }
11993
+ hook() {
11994
+ return {
11995
+ cancel: () => this.close(),
11996
+ onInput: (data) => {
11997
+ if (data.includes("\r")) this.noteEnter();
11998
+ },
11999
+ onSubmitted: (text) => this.noteSubmitted(text)
12000
+ };
12001
+ }
12002
+ /** Mira la salida: el dialogo de confianza abierto frena el cuadro. Publica para el chequeo. */
12003
+ checkTrust() {
12004
+ if (this.closed || this.sessionId.length === 0) return;
12005
+ const output = this.context.readOutput();
12006
+ const open9 = trustDialogOpen(output);
12007
+ this.live.setBlocked(this.sessionId, open9);
12008
+ if (open9) return;
12009
+ if (interfaceStarted(output) || this.now() - this.context.launchedAt > STARTUP_DEADLINE_MS) {
12010
+ if (this.startupTimer !== null) clearInterval(this.startupTimer);
12011
+ this.startupTimer = null;
12012
+ }
12013
+ }
12014
+ /** Alguien apreto Enter en la pty: puede ser el que cambia de sesion. */
12015
+ noteEnter() {
12016
+ if (this.closed) return;
12017
+ this.enterAt = this.now();
12018
+ this.scheduleScans();
12019
+ }
12020
+ /** La app va a mandar este texto. */
12021
+ noteSubmitted(text) {
12022
+ if (this.closed || text.trim().length === 0) return;
12023
+ const now = this.now();
12024
+ this.submissions = [
12025
+ ...this.submissions.filter((each) => now - each.at < SUBMISSION_MAX_AGE_MS),
12026
+ { text, at: now }
12027
+ ].slice(-SUBMISSIONS_KEPT);
12028
+ }
12029
+ scheduleScans() {
12030
+ if (!this.useTimers) return;
12031
+ for (const timer of this.scanTimers) clearTimeout(timer);
12032
+ this.scanTimers.clear();
12033
+ for (const delay2 of SCAN_DELAYS_MS) {
12034
+ const timer = setTimeout(() => {
12035
+ this.scanTimers.delete(timer);
12036
+ void this.scan();
12037
+ }, delay2);
12038
+ timer.unref();
12039
+ this.scanTimers.add(timer);
12040
+ }
12041
+ }
12042
+ /** Una pasada por la carpeta de sesiones del proyecto, en serie con la anterior. Nunca lanza. */
12043
+ scan() {
12044
+ this.scanning = this.scanning.then(() => this.scanOnce()).catch(() => void 0);
12045
+ return this.scanning;
12046
+ }
12047
+ async scanOnce() {
12048
+ if (this.closed) return;
12049
+ const dir = this.sessionDir(this.context.cwd);
12050
+ if (dir === null) return;
12051
+ const files = await this.listSessions(dir);
12052
+ if (this.closed) return;
12053
+ const previous = this.sizes;
12054
+ this.sizes = new Map(files.map((each) => [each.file, each.size]));
12055
+ if (this.file === null && this.sessionId.length > 0) {
12056
+ this.file = files.find((each) => parseSessionFileName(path36.basename(each.file))?.sessionId === this.sessionId)?.file ?? null;
12057
+ }
12058
+ if (previous === null) return;
12059
+ const own = this.currentFile;
12060
+ const taken = /* @__PURE__ */ new Set();
12061
+ for (const tab of this.tabs()) {
12062
+ const file = tab === this ? null : tab.currentFile;
12063
+ if (file !== null) taken.add(this.fileKey(file));
12064
+ }
12065
+ const candidates = files.filter((each) => {
12066
+ if (own !== null && this.fileKey(each.file) === this.fileKey(own)) return false;
12067
+ if (taken.has(this.fileKey(each.file))) return false;
12068
+ const before = previous.get(each.file);
12069
+ if (before !== void 0 && each.size <= before) return false;
12070
+ return each.mtimeMs >= this.context.launchedAt - CLOCK_SLACK_MS2;
12071
+ }).sort((a, b) => b.mtimeMs - a.mtimeMs);
12072
+ for (const candidate of candidates) {
12073
+ const info = await this.peek(candidate.file);
12074
+ if (this.closed) return;
12075
+ if (info === null || !isPiSessionId(info.header.sessionId)) continue;
12076
+ if (normalizeCwdKey(info.header.cwd, this.platform) !== normalizeCwdKey(this.context.cwd, this.platform)) continue;
12077
+ const reason2 = this.claims(info, !previous.has(candidate.file));
12078
+ if (reason2 === null) continue;
12079
+ this.switchTo(info.header.sessionId, candidate.file, reason2);
12080
+ return;
12081
+ }
12082
+ }
12083
+ fileKey(file) {
12084
+ return normalizeCwdKey(file, this.platform);
12085
+ }
12086
+ /** Por que ese archivo es la sesion nueva de esta pestana, o null si no se puede decir. */
12087
+ claims(info, isNew) {
12088
+ const own = this.currentFile;
12089
+ if (isNew && own !== null && info.header.parentSession !== null && this.fileKey(info.header.parentSession) === this.fileKey(own)) {
12090
+ return "fork";
12091
+ }
12092
+ const last = info.lastUser;
12093
+ if (last === null) return null;
12094
+ const now = this.now();
12095
+ const sent = this.submissions.some(
12096
+ (each) => now - each.at < SUBMISSION_MAX_AGE_MS && last.at >= each.at - CLOCK_SLACK_MS2 && plainMatches(last.plain, each.text) && // Si ya llego a la sesion en curso, no es de la otra.
12097
+ (this.sessionId.length === 0 || !this.live.delivered(this.sessionId, each))
12098
+ );
12099
+ if (sent) return "submitted";
12100
+ const enterAt = this.enterAt;
12101
+ if (enterAt === null || last.at < enterAt - CLOCK_SLACK_MS2 / 4 || last.at > enterAt + TYPED_WINDOW_MS) return null;
12102
+ if (this.sessionId.length > 0 && this.live.userMessageSince(this.sessionId, enterAt - CLOCK_SLACK_MS2 / 4)) return null;
12103
+ const key = normalizeCwdKey(this.context.cwd, this.platform);
12104
+ let rivals = 0;
12105
+ for (const tab of this.tabs()) {
12106
+ if (tab === this || normalizeCwdKey(tab.cwd, this.platform) !== key) continue;
12107
+ const other = tab.lastEnterAt;
12108
+ if (other === null || last.at < other - CLOCK_SLACK_MS2 / 4 || last.at > other + TYPED_WINDOW_MS) continue;
12109
+ if (Math.abs(last.at - other) < Math.abs(last.at - enterAt)) return null;
12110
+ rivals += 1;
12111
+ }
12112
+ if (rivals > 0) {
12113
+ console.warn(
12114
+ `[pi] ${rivals + 1} Pi tabs in ${this.context.cwd} typed at the same time: session ${info.header.sessionId.slice(0, 8)} was assigned to the closest one without confirmation.`
12115
+ );
12116
+ }
12117
+ return "typed";
12118
+ }
12119
+ switchTo(sessionId, file, reason2) {
12120
+ const previous = this.sessionId;
12121
+ if (sessionId === previous) return;
12122
+ debugLog("registry", `pi tab ${this.context.terminalId.slice(0, 8)}: session ${sessionId.slice(0, 8)} (${reason2})`);
12123
+ this.sessionId = sessionId;
12124
+ this.file = file;
12125
+ this.submissions = [];
12126
+ this.live.attach(sessionId, { cwd: this.context.cwd, launchedAt: this.context.launchedAt, file, fromStart: true });
12127
+ this.context.reportSessionId(sessionId);
12128
+ if (previous.length > 0) this.live.detach(previous);
12129
+ }
12130
+ close() {
12131
+ if (this.closed) return;
12132
+ this.closed = true;
12133
+ if (this.startupTimer !== null) clearInterval(this.startupTimer);
12134
+ this.startupTimer = null;
12135
+ for (const timer of this.scanTimers) clearTimeout(timer);
12136
+ this.scanTimers.clear();
12137
+ if (this.sessionId.length > 0) this.live.detach(this.sessionId);
12138
+ }
12139
+ };
12140
+
12141
+ // packages/server/src/agents/pi/index.ts
12142
+ var PI_MODELS_POLL_MS = 2e3;
12143
+ var PI_BASE_CAPABILITIES = {
12144
+ // `--session-id <id>` con un id que pone la app (16.4).
12145
+ sessionIdAtLaunch: true,
12146
+ // El mismo comando abre la sesion si existe.
12147
+ resume: true,
12148
+ // Del archivo de sesion: trabajando desde el mensaje hasta la respuesta final (16.6).
12149
+ statusSource: true,
12150
+ // "Libre" no dice que la interfaz ya acepte un pegado, y antes del primer mensaje no hay archivo.
12151
+ readySignal: false,
12152
+ // Pi no tiene modos de permiso.
12153
+ permissionCycle: null,
12154
+ // Los de la cuenta, cuando se leen (`capabilities`).
12155
+ models: null,
12156
+ // `/thinking <nivel>` se aplica directo y escribe su entrada en el acto (16.10).
12157
+ efforts: PI_EFFORT_OPTIONS,
12158
+ // No tiene herramienta de preguntas.
12159
+ questionCards: false,
12160
+ // Pi no adjunta lo pegado: la ruta va en el texto y el modelo la abre (16.5).
12161
+ imagesByPath: "quoted-path-line",
12162
+ // `@` abre un buscador de archivos y queda como texto.
12163
+ fileMentions: null,
12164
+ // Dos Esc abren `/tree`: otro flujo.
12165
+ rewind: false,
12166
+ // Tokens por respuesta en la sesion, y la ventana en el catalogo (16.9).
12167
+ contextWindowSource: "usage-with-catalog",
12168
+ plans: false,
12169
+ // Con el dialogo de confianza abierto, el Enter de un mensaje lo aceptaria (16.11).
10174
12170
  waitingBlocksSubmit: true
10175
12171
  };
10176
- var OPENCODE_INPUT = {
10177
- imageReference: "bare-path-paste",
10178
- pieceGapMs: 400,
12172
+ var PI_INPUT = {
12173
+ imageReference: "quoted-path-line",
12174
+ pieceGapMs: 0,
10179
12175
  pasteMarkers: true,
10180
- enterSeparately: true,
10181
- interruptPresses: 2,
10182
- // `@` abre un buscador en el TUI (11.11): el transcript va entre comillas.
10183
- transcriptReference: "quoted-path"
12176
+ enterSeparately: false,
12177
+ interruptPresses: 1,
12178
+ // Con la ruta entre comillas el modelo abre el archivo (medido); `@` abre un buscador.
12179
+ transcriptReference: "quoted-path",
12180
+ // Antes de que dibuje su interfaz, un Enter no envia: el texto queda en su editor (16.4).
12181
+ readyAfterLaunchMs: PI_READY_AFTER_LAUNCH_MS
10184
12182
  };
10185
- function copyEnvironment(base) {
10186
- const env = {};
10187
- for (const [key, value] of Object.entries(base)) {
10188
- if (value !== void 0) env[key] = value;
10189
- }
10190
- return env;
10191
- }
10192
- function createOpenCodeAdapter(options = {}) {
10193
- const platform = options.platform ?? process.platform;
10194
- const env = options.env ?? process.env;
10195
- const sqlite = options.sqlite ?? loadSqlite;
10196
- const paths = resolveOpenCodePaths(env, options.home ?? homedir8(), platform);
10197
- const db = new ReadOnlyDatabase(paths.dbFile ?? "", {
10198
- idleCloseMs: OPENCODE_DB_IDLE_CLOSE_MS,
10199
- required: OPENCODE_REQUIRED_COLUMNS,
10200
- label: OPENCODE_LABEL,
10201
- sqlite
10202
- });
10203
- const signal = new DbChangeSignal(
10204
- paths.dbFile === null || paths.walFile === null ? null : { db: paths.dbFile, wal: paths.walFile }
10205
- );
10206
- const catalog = new ModelCatalog(paths.modelsFile);
10207
- const serveEnv = options.serveEnv ?? (() => copyEnvironment(process.env));
10208
- let serve = null;
10209
- const status = new OpenCodeServeStatus({ ...options.serveStatus, parentOf: (id) => readSessionParent(db, id) });
10210
- let current = null;
10211
- const serveFor = (location) => {
10212
- if (serve !== null) return serve;
10213
- const created = new OpenCodeServeProcess({ ...options.serveProcess, location, baseEnv: serveEnv });
10214
- created.onExit((endpoint, exit) => {
10215
- status.detach(endpoint, exit.requested ? null : "server-closed");
10216
- if (current?.endpoint === endpoint) current = null;
10217
- });
10218
- serve = created;
10219
- return created;
10220
- };
10221
- const clientFor = (endpoint) => {
10222
- if (current?.endpoint === endpoint) return current.client;
10223
- current = { endpoint, client: new OpenCodeServeClient(endpoint, void 0, options.serveClient) };
10224
- return current.client;
12183
+ function createPiAdapter(options = {}) {
12184
+ let packageDir = null;
12185
+ const catalog = new PiModelCatalog({ storeFile: piModelsStorePath, packageDir: () => packageDir });
12186
+ const live = new PiLiveSessions(options.live);
12187
+ const status = new PiStatusSource(live);
12188
+ const tabs = /* @__PURE__ */ new Set();
12189
+ const pollers = /* @__PURE__ */ new Set();
12190
+ const storeStamp = async () => {
12191
+ const file = piModelsStorePath();
12192
+ if (file === null) return "none";
12193
+ try {
12194
+ const info = await stat21(file);
12195
+ return `${info.mtimeMs}:${info.size}`;
12196
+ } catch {
12197
+ return "none";
12198
+ }
10225
12199
  };
10226
- const questions = new OpenCodeQuestions({
10227
- lookupCall: (partId, sessionId) => readPartCall(db, partId, sessionId),
10228
- client: () => current?.client ?? null
10229
- });
10230
12200
  return {
10231
- id: "opencode",
10232
- label: OPENCODE_LABEL,
10233
- command: OPENCODE_COMMAND,
10234
- installUrl: OPENCODE_INSTALL_URL,
10235
- capabilities: OPENCODE_CAPABILITIES,
10236
- input: OPENCODE_INPUT,
10237
- // `--version` es seguro: medido, no toca la base ni escribe log.
10238
- locate: () => locateCommand(OPENCODE_COMMAND),
10239
- missingMessage: () => commandNotFoundMessage(OPENCODE_COMMAND, OPENCODE_INSTALL_URL),
12201
+ id: "pi",
12202
+ label: PI_LABEL,
12203
+ command: PI_COMMAND,
12204
+ installUrl: PI_INSTALL_URL,
12205
+ get capabilities() {
12206
+ const models = modelOptions(catalog.list());
12207
+ return models.length === 0 ? PI_BASE_CAPABILITIES : { ...PI_BASE_CAPABILITIES, models };
12208
+ },
12209
+ input: PI_INPUT,
12210
+ async locate() {
12211
+ const found = await locatePi(options.locate);
12212
+ packageDir = found?.packageDir ?? null;
12213
+ if (found === null) return null;
12214
+ return { resolvedPath: found.resolvedPath, file: found.file, prefixArgs: found.prefixArgs, version: found.version };
12215
+ },
12216
+ missingMessage: () => commandNotFoundMessage(PI_COMMAND, PI_INSTALL_URL),
10240
12217
  /**
10241
- * `attach <url> --dir . --session <id> --password <hex>` (D5), con la
10242
- * sesion nueva creada por API (D6). No es `async` (M1b): un id de
10243
- * reanudacion invalido lanza aca mismo, sin tocar el `serve`, porque detras
10244
- * de `cmd.exe /c` un `&` se ejecuta. Lo que falla despues —el `serve` que no
10245
- * arranca, la sesion que no se crea— rechaza, y el registro lo muestra como
10246
- * `spawn-failed` con el motivo (D1). `proposedSessionId` no se usa: el id lo
10247
- * pone OpenCode.
12218
+ * `--session-id <id>` para una sesion nueva y para una que se reanuda: Pi
12219
+ * decide sola cual es. El id se valida antes de tocar la linea de comando,
12220
+ * porque va detras de `cmd /c` (CLAUDE.md 3.1): con la forma que exige Pi es
12221
+ * un token sin espacios ni comillas, y no puede leerse como una opcion.
10248
12222
  */
10249
12223
  launch(input) {
10250
- return launchWithServe(
10251
- {
10252
- ensure: () => serveFor(input.location).ensure(),
10253
- createSession: (endpoint, directory) => clientFor(endpoint).createSession(directory),
10254
- track: (endpoint, sessionId, directory) => status.track(sessionId, directory, clientFor(endpoint))
10255
- },
10256
- input
10257
- );
12224
+ const sessionId = input.resumeSessionId ?? input.proposedSessionId;
12225
+ if (!isPiSessionId(sessionId)) throw new Error("Invalid Pi session id.");
12226
+ return {
12227
+ file: input.location.file,
12228
+ args: [...input.location.prefixArgs, "--session-id", sessionId],
12229
+ session: { kind: "known", sessionId }
12230
+ };
10258
12231
  },
10259
- /** Copia del entorno, sin quitar nada: OpenCode no hereda ningun marcador que le estorbe. */
12232
+ /**
12233
+ * Copia del entorno, sin quitar nada y sin agregar nada: ni
12234
+ * `PI_PROGRAM_STATUS`, que encenderia los avisos de estado, ni
12235
+ * `PI_TELEMETRY`, que apagaria el aviso de instalacion a pi.dev. Las dos son
12236
+ * del usuario (regla 2.1).
12237
+ */
10260
12238
  environment(base) {
10261
- return { env: copyEnvironment(base), notice: null };
12239
+ const env = {};
12240
+ for (const [key, value] of Object.entries(base)) {
12241
+ if (value !== void 0) env[key] = value;
12242
+ }
12243
+ return { env, notice: null };
10262
12244
  },
10263
12245
  /*
10264
- Toda pestana lanzada, nueva o reanudada, retiene el `serve` mientras viva
10265
- su pty, y al cerrarse corta la sesion si quedo trabajando o esperando
10266
- (D13): con `attach`, el trabajo seguiria en el `serve` sin ningun TUI que
10267
- lo muestre. El corte va al cliente vigente: el de un `serve` que ya murio
10268
- no esta, y su estado ya es null.
12246
+ El gancho vive toda la vida de la pestana: mira el dialogo de confianza al
12247
+ arrancar y, despues de cada Enter, si Pi paso a escribir en otra sesion
12248
+ (`/new`, `/fork`, `/resume`). Declara `onInput` para que el primer write no
12249
+ lo suelte.
10269
12250
  */
10270
12251
  onSpawned(context) {
10271
- if (context.sessionId.length === 0) return null;
10272
- const { sessionId, cwd } = context;
10273
- return createServeLaunchHook({
10274
- sessionId,
10275
- cwd,
10276
- release: serve?.retain() ?? (() => {
10277
- }),
10278
- current: () => status.current(sessionId),
10279
- abort: (directory, id) => current?.client.abort(directory, id) ?? Promise.resolve()
10280
- });
12252
+ const watch = new PiTabWatch(context, { live, tabs: () => tabs });
12253
+ tabs.add(watch);
12254
+ watch.start();
12255
+ const hook = watch.hook();
12256
+ return {
12257
+ ...hook,
12258
+ cancel: () => {
12259
+ tabs.delete(watch);
12260
+ hook.cancel();
12261
+ }
12262
+ };
10281
12263
  },
10282
- history: createOpenCodeHistory({
10283
- dbFile: paths.dbFile,
10284
- db,
10285
- signal,
10286
- catalog,
10287
- platform,
10288
- sqlite
10289
- }),
12264
+ history: createPiHistory(catalog),
10290
12265
  status,
10291
- questions,
10292
- /**
10293
- * La base que se lee, para el arranque. Solo si hay algo que decir, para que
10294
- * quien no usa OpenCode no vea una linea nueva:
10295
- *
10296
- * - con la base en memoria (`OPENCODE_DB=:memory:`, que alguien puso a
10297
- * proposito), que no hay nada que leer;
10298
- * - sin `node:sqlite`, el aviso de la version de Node, si hay una base que
10299
- * no se va a ver o la CLI esta instalada;
10300
- * - con la base en disco, su ruta.
10301
- *
10302
- * Con la base ausente no dice nada: `status()` es perezoso y no sabe si
10303
- * existe hasta la primera consulta, asi que se mira el archivo.
10304
- */
10305
- startupHistoryNote(cliAvailable) {
10306
- if (env["OPENCODE_DB"] === ":memory:") return "in memory (nothing to read)";
10307
- if (paths.dbFile === null) return null;
10308
- const present = existsSync2(paths.dbFile);
10309
- if ("unavailable" in sqlite()) return present || cliAvailable ? `not read: ${sqliteUnavailableText(OPENCODE_LABEL)}` : null;
10310
- return present ? `${paths.dbFile} (read-only)` : null;
12266
+ queuedSubmits: {
12267
+ clearGapMs: PI_CLEAR_GAP_MS,
12268
+ check: (target, submissions, interruptedAt) => live.check(target.sessionId, submissions, interruptedAt)
10311
12269
  },
10312
- // `opencode.json` no se abre (regla 2.1).
12270
+ // `settings.json` no se abre: sin modelo por defecto que anunciar. Al reanudar,
12271
+ // el modelo y el nivel salen de la propia sesion.
10313
12272
  defaults: () => Promise.resolve(null),
10314
12273
  /**
10315
- * Las cuatro carpetas de OpenCode: datos (la base, `auth.json`),
10316
- * configuracion, cache y estado. No se listan ni se entran (regla 2.1). La
10317
- * carpeta de un `OPENCODE_DB` propio no: puede ser la de un proyecto.
10318
- */
10319
- protectedDirs: () => [paths.dataDir, paths.configDir, paths.cacheDir, paths.stateDir],
10320
- /**
10321
- * Suelta el estado (sus oyentes reciben null y el flujo se cierra) antes de
10322
- * matar el `serve`: los ganchos de las pestanas, que se cancelan despues en
10323
- * el apagado, ya no piden ningun corte (M7). La promesa espera a que el
10324
- * proceso salga; la orden de matarlo sale antes de devolver.
12274
+ * `~/.pi` entera y, si `PI_CODING_AGENT_DIR` apunta a otro lado, esa
12275
+ * tambien: las credenciales de antes de cambiarla siguen en la primera. La
12276
+ * carpeta de sesiones de `PI_CODING_AGENT_SESSION_DIR` no: puede ser la de
12277
+ * un proyecto.
10325
12278
  */
12279
+ protectedDirs: () => {
12280
+ const dirs = [path37.join(homedir10(), ".pi")];
12281
+ const agentDir = piAgentDir();
12282
+ if (agentDir !== null && !dirs.some((dir) => agentDir === dir || agentDir.startsWith(`${dir}${path37.sep}`))) {
12283
+ dirs.unshift(agentDir);
12284
+ }
12285
+ return dirs;
12286
+ },
12287
+ /** Lee el catalogo de la cuenta. Solo corre con Pi encontrada. */
12288
+ async prepare() {
12289
+ await catalog.reload();
12290
+ },
12291
+ refreshSetup: () => catalog.reload(),
12292
+ /** Avisa cuando la lista de modelos de la cuenta cambia: un `/login` nuevo, o Pi que la refresca. */
12293
+ subscribeChanges(listener) {
12294
+ let last = null;
12295
+ let busy = false;
12296
+ const timer = setInterval(() => {
12297
+ if (busy) return;
12298
+ busy = true;
12299
+ void (async () => {
12300
+ const stamp2 = await storeStamp();
12301
+ if (last !== null && stamp2 !== last && await catalog.reload()) listener();
12302
+ last = stamp2;
12303
+ })().catch(() => void 0).finally(() => {
12304
+ busy = false;
12305
+ });
12306
+ }, options.modelsPollMs ?? PI_MODELS_POLL_MS);
12307
+ timer.unref();
12308
+ pollers.add(timer);
12309
+ return () => {
12310
+ clearInterval(timer);
12311
+ pollers.delete(timer);
12312
+ };
12313
+ },
10326
12314
  dispose: () => {
12315
+ for (const timer of pollers) clearInterval(timer);
12316
+ pollers.clear();
12317
+ tabs.clear();
10327
12318
  status.dispose();
10328
- current = null;
10329
- signal.dispose();
10330
- db.close();
10331
- return serve?.dispose() ?? Promise.resolve();
10332
12319
  }
10333
12320
  };
10334
12321
  }
@@ -10536,7 +12523,8 @@ function createAgentRegistry(disabled = []) {
10536
12523
  createClaudeCodeAdapter(),
10537
12524
  createCodexAdapter(),
10538
12525
  createOpenCodeAdapter({ serveEnv: () => registry.composedEnvironment(process.env) }),
10539
- createAntigravityAdapter()
12526
+ createAntigravityAdapter(),
12527
+ createPiAdapter()
10540
12528
  ], disabled);
10541
12529
  return registry;
10542
12530
  }
@@ -10554,7 +12542,7 @@ async function migrateLegacyConfigDir() {
10554
12542
 
10555
12543
  // packages/server/src/conversation-hub.ts
10556
12544
  import { EventEmitter } from "node:events";
10557
- import path30 from "node:path";
12545
+ import path38 from "node:path";
10558
12546
  var INITIAL_EVENT_LIMIT = 300;
10559
12547
  var MAX_PAGE_LIMIT = 500;
10560
12548
  var PENDING_QUESTION_LOOKBACK = 50;
@@ -10615,7 +12603,7 @@ var ConversationHub = class extends EventEmitter {
10615
12603
  await entry.follower.start();
10616
12604
  debugLog(
10617
12605
  "conversation",
10618
- `following ${path30.basename(entry.follower.label)} for ${terminalId.slice(0, 8)}`
12606
+ `following ${path38.basename(entry.follower.label)} for ${terminalId.slice(0, 8)}`
10619
12607
  );
10620
12608
  entry = this.entries.get(terminalId) ?? entry;
10621
12609
  }
@@ -10721,10 +12709,10 @@ var ConversationHub = class extends EventEmitter {
10721
12709
  */
10722
12710
  refreshOpenToolCall(terminalId, entry) {
10723
12711
  const launchedAt = this.isBlind(entry) ? this.registry.launchedAtOf(terminalId) : null;
10724
- const open8 = launchedAt !== null && entry.follower.hasOpenToolCall(launchedAt);
10725
- if (open8 === entry.openToolCall) return;
10726
- entry.openToolCall = open8;
10727
- this.emit("toolCall", terminalId, open8);
12712
+ const open9 = launchedAt !== null && entry.follower.hasOpenToolCall(launchedAt);
12713
+ if (open9 === entry.openToolCall) return;
12714
+ entry.openToolCall = open9;
12715
+ this.emit("toolCall", terminalId, open9);
10728
12716
  }
10729
12717
  /**
10730
12718
  * Recalcula los subagentes que siguen trabajando y avisa si cambiaron
@@ -10780,7 +12768,7 @@ var ConversationHub = class extends EventEmitter {
10780
12768
  this.startFollowTicker(terminalId, next);
10781
12769
  debugLog(
10782
12770
  "conversation",
10783
- `${terminalId.slice(0, 8)} changed session: following ${path30.basename(next.follower.label)}`
12771
+ `${terminalId.slice(0, 8)} changed session: following ${path38.basename(next.follower.label)}`
10784
12772
  );
10785
12773
  await next.follower.start();
10786
12774
  next.primed = true;
@@ -11015,8 +13003,8 @@ var ConversationHub = class extends EventEmitter {
11015
13003
  if (entry === void 0) return null;
11016
13004
  const { events } = entry.follower.getTail(PENDING_QUESTION_LOOKBACK);
11017
13005
  const answered = /* @__PURE__ */ new Set();
11018
- for (const event of events) {
11019
- for (const part of event.parts) {
13006
+ for (const event2 of events) {
13007
+ for (const part of event2.parts) {
11020
13008
  if (part.kind === "tool-result") answered.add(part.toolUseId);
11021
13009
  }
11022
13010
  }
@@ -11144,18 +13132,18 @@ function openBrowser(url) {
11144
13132
  // packages/server/src/pty-helper.ts
11145
13133
  import { chmodSync, statSync } from "node:fs";
11146
13134
  import { createRequire } from "node:module";
11147
- import path31 from "node:path";
13135
+ import path39 from "node:path";
11148
13136
  function helperCandidates(packageDir, platform, arch) {
11149
13137
  return [
11150
- path31.join(packageDir, "build", "Release", "spawn-helper"),
11151
- path31.join(packageDir, "build", "Debug", "spawn-helper"),
11152
- path31.join(packageDir, "prebuilds", `${platform}-${arch}`, "spawn-helper")
13138
+ path39.join(packageDir, "build", "Release", "spawn-helper"),
13139
+ path39.join(packageDir, "build", "Debug", "spawn-helper"),
13140
+ path39.join(packageDir, "prebuilds", `${platform}-${arch}`, "spawn-helper")
11153
13141
  ];
11154
13142
  }
11155
13143
  function locateNodePty() {
11156
13144
  try {
11157
13145
  const require2 = createRequire(import.meta.url);
11158
- return path31.resolve(path31.dirname(require2.resolve("node-pty")), "..");
13146
+ return path39.resolve(path39.dirname(require2.resolve("node-pty")), "..");
11159
13147
  } catch {
11160
13148
  return null;
11161
13149
  }
@@ -11816,24 +13804,24 @@ var RemoteAccessService = class {
11816
13804
  };
11817
13805
 
11818
13806
  // packages/server/src/remote-devices-store.ts
11819
- import { chmod, mkdir as mkdir6, readFile as readFile10, stat as stat17 } from "node:fs/promises";
11820
- import path33 from "node:path";
13807
+ import { chmod, mkdir as mkdir6, readFile as readFile12, stat as stat23 } from "node:fs/promises";
13808
+ import path41 from "node:path";
11821
13809
 
11822
13810
  // packages/server/src/vault/write.ts
11823
13811
  import { randomBytes as randomBytes2 } from "node:crypto";
11824
- import { mkdir as mkdir5, rename as rename3, rm as rm2, stat as stat16, writeFile as writeFile3 } from "node:fs/promises";
13812
+ import { mkdir as mkdir5, rename as rename3, rm as rm2, stat as stat22, writeFile as writeFile3 } from "node:fs/promises";
11825
13813
 
11826
13814
  // packages/server/src/vault/paths.ts
11827
13815
  import { createHash as createHash2 } from "node:crypto";
11828
- import path32 from "node:path";
13816
+ import path40 from "node:path";
11829
13817
  function defaultVaultDir() {
11830
- return path32.join(appConfigDir(), "vault");
13818
+ return path40.join(appConfigDir(), "vault");
11831
13819
  }
11832
13820
  function vaultMarkerFile(dir) {
11833
- return path32.join(dir, "vault.json");
13821
+ return path40.join(dir, "vault.json");
11834
13822
  }
11835
13823
  function sessionsRoot2(dir) {
11836
- return path32.join(dir, "sessions");
13824
+ return path40.join(dir, "sessions");
11837
13825
  }
11838
13826
  function isSafeSessionId(sessionId) {
11839
13827
  return isVaultSessionId(sessionId);
@@ -11847,41 +13835,41 @@ function assertSessionKey(agent, sessionId) {
11847
13835
  }
11848
13836
  function agentSessionsDir(dir, agent) {
11849
13837
  if (!isSessionAgentId(agent)) throw new Error(`Unknown source for the local copy: ${JSON.stringify(agent)}`);
11850
- return path32.join(sessionsRoot2(dir), agent);
13838
+ return path40.join(sessionsRoot2(dir), agent);
11851
13839
  }
11852
13840
  function sessionFile(dir, agent, sessionId) {
11853
13841
  assertSessionKey(agent, sessionId);
11854
- return path32.join(sessionsRoot2(dir), agent, `${sessionId}.jsonl`);
13842
+ return path40.join(sessionsRoot2(dir), agent, `${sessionId}.jsonl`);
11855
13843
  }
11856
13844
  function assetsDir(dir, agent, sessionId) {
11857
13845
  assertSessionKey(agent, sessionId);
11858
- return path32.join(sessionsRoot2(dir), agent, `${sessionId}.assets`);
13846
+ return path40.join(sessionsRoot2(dir), agent, `${sessionId}.assets`);
11859
13847
  }
11860
13848
  function assetFile(dir, agent, sessionId, asset) {
11861
13849
  if (!VAULT_ASSET_NAME_PATTERN.test(asset)) throw new Error(`Invalid asset name: ${JSON.stringify(asset)}`);
11862
- return path32.join(assetsDir(dir, agent, sessionId), asset);
13850
+ return path40.join(assetsDir(dir, agent, sessionId), asset);
11863
13851
  }
11864
- function projectFolderName(cwd, platform) {
13852
+ function projectFolderName2(cwd, platform) {
11865
13853
  const hash = createHash2("sha256").update(normalizeCwdKey(cwd, platform)).digest("hex").slice(0, 8);
11866
- const base = (platform === "win32" ? path32.win32 : path32.posix).basename(cwd);
13854
+ const base = (platform === "win32" ? path40.win32 : path40.posix).basename(cwd);
11867
13855
  const name = base.length === 0 ? "proyecto" : sanitizeFileName(base, 60);
11868
13856
  return `${name}-${hash}`;
11869
13857
  }
11870
13858
  function memoryDir(dir, cwd, platform) {
11871
- return path32.join(dir, "memory", projectFolderName(cwd, platform));
13859
+ return path40.join(dir, "memory", projectFolderName2(cwd, platform));
11872
13860
  }
11873
13861
  function projectExportDir(dir, cwd, platform) {
11874
- return path32.join(dir, "export", projectFolderName(cwd, platform));
13862
+ return path40.join(dir, "export", projectFolderName2(cwd, platform));
11875
13863
  }
11876
13864
  function projectExportDirFor(dir, project, platform) {
11877
13865
  if (project.cwd.length > 0) return projectExportDir(dir, project.cwd, platform);
11878
13866
  const hash = createHash2("sha256").update(project.key).digest("hex").slice(0, 8);
11879
13867
  const name = project.fallbackName.length > 0 ? sanitizeFileName(project.fallbackName, 60) : "proyecto";
11880
- return path32.join(dir, "export", `${name}-${hash}`);
13868
+ return path40.join(dir, "export", `${name}-${hash}`);
11881
13869
  }
11882
13870
  function sessionExportFile(dir, agent, sessionId) {
11883
13871
  assertSessionKey(agent, sessionId);
11884
- return path32.join(dir, "export", "sesiones", `${agent}-${sessionId}.md`);
13872
+ return path40.join(dir, "export", "sesiones", `${agent}-${sessionId}.md`);
11885
13873
  }
11886
13874
  var RESERVED_WINDOWS_NAME = /^(con|prn|aux|nul|com[1-9]|lpt[1-9])$/i;
11887
13875
  function sanitizeFileName(text, max = 80) {
@@ -11936,7 +13924,7 @@ async function writeFileAtomic(target, data, deps = DEFAULT_WRITE_DEPS) {
11936
13924
  async function ensureVaultMarker(dir, deps = DEFAULT_WRITE_DEPS) {
11937
13925
  const marker = vaultMarkerFile(dir);
11938
13926
  try {
11939
- await stat16(marker);
13927
+ await stat22(marker);
11940
13928
  return;
11941
13929
  } catch {
11942
13930
  }
@@ -11946,7 +13934,7 @@ async function ensureVaultMarker(dir, deps = DEFAULT_WRITE_DEPS) {
11946
13934
  }
11947
13935
  async function assetPresent(file, size) {
11948
13936
  try {
11949
- const info = await stat16(file);
13937
+ const info = await stat22(file);
11950
13938
  return info.isFile() && info.size === size;
11951
13939
  } catch {
11952
13940
  return false;
@@ -11988,7 +13976,7 @@ var RemoteDevicesStore = class {
11988
13976
  async load() {
11989
13977
  let raw;
11990
13978
  try {
11991
- raw = await readFile10(this.filePath, "utf8");
13979
+ raw = await readFile12(this.filePath, "utf8");
11992
13980
  } catch {
11993
13981
  return [];
11994
13982
  }
@@ -12009,7 +13997,7 @@ var RemoteDevicesStore = class {
12009
13997
  save(devices) {
12010
13998
  const text = serializeRemoteDevices(devices);
12011
13999
  const run = async () => {
12012
- await mkdir6(path33.dirname(this.filePath), { recursive: true });
14000
+ await mkdir6(path41.dirname(this.filePath), { recursive: true });
12013
14001
  await writeFileAtomic(this.filePath, text);
12014
14002
  await chmod(this.filePath, 384).catch(() => void 0);
12015
14003
  };
@@ -12028,7 +14016,7 @@ var RemoteDevicesStore = class {
12028
14016
  */
12029
14017
  async stamp() {
12030
14018
  try {
12031
- const info = await stat17(this.filePath);
14019
+ const info = await stat23(this.filePath);
12032
14020
  return `${info.mtimeMs}:${info.size}`;
12033
14021
  } catch {
12034
14022
  return "";
@@ -12142,8 +14130,8 @@ import chokidar from "chokidar";
12142
14130
 
12143
14131
  // packages/server/src/git-repo.ts
12144
14132
  import { execFile as execFile4 } from "node:child_process";
12145
- import { readFile as readFile11 } from "node:fs/promises";
12146
- import path34 from "node:path";
14133
+ import { readFile as readFile13 } from "node:fs/promises";
14134
+ import path42 from "node:path";
12147
14135
  function gitOutputText(output, command) {
12148
14136
  return output.length > 0 ? serverText("raw", { text: output }) : serverText("gitCommandFailed", { command });
12149
14137
  }
@@ -12204,7 +14192,7 @@ async function resolveRepo(cwd) {
12204
14192
  const result = await runGit(cwd, ["rev-parse", "--show-toplevel"]);
12205
14193
  if (result.code === 0) {
12206
14194
  const root = result.stdout.trim();
12207
- if (root.length > 0) return { state: "ready", root: path34.resolve(root), message: null };
14195
+ if (root.length > 0) return { state: "ready", root: path42.resolve(root), message: null };
12208
14196
  }
12209
14197
  const stderr = result.stderr.trim();
12210
14198
  if (/not a git repository|no es un repositorio/i.test(stderr)) {
@@ -12231,7 +14219,7 @@ async function findGitDir(cwd) {
12231
14219
  const result = await runGit(cwd, ["rev-parse", "--absolute-git-dir"]);
12232
14220
  if (result.code !== 0) return null;
12233
14221
  const dir = result.stdout.trim();
12234
- return dir.length > 0 ? path34.resolve(dir) : null;
14222
+ return dir.length > 0 ? path42.resolve(dir) : null;
12235
14223
  }
12236
14224
  function letterToKind(letter) {
12237
14225
  switch (letter) {
@@ -12334,7 +14322,7 @@ function parseWorktrees(raw, currentRoot) {
12334
14322
  worktrees.push({
12335
14323
  path: current.path,
12336
14324
  branch: current.branch,
12337
- isCurrent: path34.resolve(current.path) === path34.resolve(currentRoot)
14325
+ isCurrent: path42.resolve(current.path) === path42.resolve(currentRoot)
12338
14326
  });
12339
14327
  current = null;
12340
14328
  };
@@ -12477,7 +14465,7 @@ function looksBinary(buffer) {
12477
14465
  async function readUntrackedAsDiff(absolutePath, relativePath) {
12478
14466
  let content;
12479
14467
  try {
12480
- content = await readFile11(absolutePath);
14468
+ content = await readFile13(absolutePath);
12481
14469
  } catch {
12482
14470
  return {
12483
14471
  path: relativePath,
@@ -12525,7 +14513,7 @@ async function readUntrackedAsDiff(absolutePath, relativePath) {
12525
14513
  async function readDiff(options) {
12526
14514
  const { repoRoot: repoRoot2, relativePath, staged, untracked } = options;
12527
14515
  if (untracked && !staged) {
12528
- return readUntrackedAsDiff(path34.join(repoRoot2, relativePath), relativePath);
14516
+ return readUntrackedAsDiff(path42.join(repoRoot2, relativePath), relativePath);
12529
14517
  }
12530
14518
  const args = [
12531
14519
  "diff",
@@ -12744,8 +14732,8 @@ var RepoHub = class extends EventEmitter2 {
12744
14732
 
12745
14733
  // packages/server/src/memory-hub.ts
12746
14734
  import { EventEmitter as EventEmitter3 } from "node:events";
12747
- import { homedir as homedir10 } from "node:os";
12748
- import path36 from "node:path";
14735
+ import { homedir as homedir12 } from "node:os";
14736
+ import path44 from "node:path";
12749
14737
  import chokidar2 from "chokidar";
12750
14738
 
12751
14739
  // packages/server/src/memory-bridge.ts
@@ -12753,16 +14741,16 @@ import { execFile as execFile5 } from "node:child_process";
12753
14741
  import { randomBytes as randomBytes4 } from "node:crypto";
12754
14742
  import {
12755
14743
  mkdir as mkdir7,
12756
- open as open6,
12757
- readdir as readdir9,
12758
- readFile as readFile12,
12759
- realpath as realpath2,
14744
+ open as open7,
14745
+ readdir as readdir11,
14746
+ readFile as readFile14,
14747
+ realpath as realpath3,
12760
14748
  rename as rename4,
12761
- stat as stat18,
14749
+ stat as stat24,
12762
14750
  unlink as unlink3
12763
14751
  } from "node:fs/promises";
12764
- import { homedir as homedir9 } from "node:os";
12765
- import path35 from "node:path";
14752
+ import { homedir as homedir11 } from "node:os";
14753
+ import path43 from "node:path";
12766
14754
  var MemoryBridgeError = class extends ServerTextError {
12767
14755
  };
12768
14756
  var MAX_LISTED_NOTES = 500;
@@ -12778,11 +14766,11 @@ var GITIGNORE_REL = ".gitignore";
12778
14766
  var GITIGNORE_COMMENT = "# Memoria compartida de los agentes (Agent Workbench)";
12779
14767
  var GITIGNORE_MEMORY_LINE = `${MEMORY_DIR}/`;
12780
14768
  var BOM2 = Buffer.from([239, 187, 191]);
12781
- async function inspectMemory(cwd, home = homedir9()) {
14769
+ async function inspectMemory(cwd, home = homedir11()) {
12782
14770
  await assertMemoryDirInside(cwd);
12783
- const folder = path35.join(cwd, MEMORY_DIR);
14771
+ const folder = path43.join(cwd, MEMORY_DIR);
12784
14772
  const folderExists = await isDirectory2(folder);
12785
- const indexExists = folderExists && await isFile(path35.join(folder, MEMORY_INDEX_FILE));
14773
+ const indexExists = folderExists && await isFile(path43.join(folder, MEMORY_INDEX_FILE));
12786
14774
  const noteNames = folderExists ? await listNoteFiles(folder) : [];
12787
14775
  const notes = await describeNotes(folder, noteNames);
12788
14776
  const files = await Promise.all(
@@ -12803,18 +14791,18 @@ async function inspectMemory(cwd, home = homedir9()) {
12803
14791
  globalFragments: buildGlobalFragments(home)
12804
14792
  };
12805
14793
  }
12806
- async function planMemoryInstall(cwd, options, home = homedir9()) {
14794
+ async function planMemoryInstall(cwd, options, home = homedir11()) {
12807
14795
  const plan = await buildInstallPlan(cwd, options, home);
12808
14796
  return plan.changes();
12809
14797
  }
12810
- async function installMemory(cwd, options, home = homedir9()) {
14798
+ async function installMemory(cwd, options, home = homedir11()) {
12811
14799
  const plan = await buildInstallPlan(cwd, options, home);
12812
14800
  await plan.apply();
12813
14801
  return plan.changes();
12814
14802
  }
12815
- async function importNativeMemory(cwd, home = homedir9()) {
14803
+ async function importNativeMemory(cwd, home = homedir11()) {
12816
14804
  await assertMemoryDirInside(cwd);
12817
- if (!await isDirectory2(path35.join(cwd, MEMORY_DIR))) {
14805
+ if (!await isDirectory2(path43.join(cwd, MEMORY_DIR))) {
12818
14806
  throw new MemoryBridgeError(serverText("memoryNotInstalled"));
12819
14807
  }
12820
14808
  const plan = new PlanBuilder(cwd);
@@ -12832,9 +14820,9 @@ async function readMemoryNote(cwd, name) {
12832
14820
  }
12833
14821
  const absolute = await resolveInside(cwd, `${MEMORY_DIR}/${name}`);
12834
14822
  try {
12835
- const info = await stat18(absolute);
14823
+ const info = await stat24(absolute);
12836
14824
  if (!info.isFile()) return null;
12837
- const content = await readFile12(absolute);
14825
+ const content = await readFile14(absolute);
12838
14826
  return {
12839
14827
  name,
12840
14828
  text: content.subarray(0, MAX_NOTE_BYTES).toString("utf8"),
@@ -12849,7 +14837,7 @@ function isNoteName(name) {
12849
14837
  if (name.length === 0 || name.length > 255) return false;
12850
14838
  if (name.includes("\0")) return false;
12851
14839
  if (name === "." || name === "..") return false;
12852
- if (path35.basename(name) !== name) return false;
14840
+ if (path43.basename(name) !== name) return false;
12853
14841
  if (/[\\/:*?"<>|\u0000-\u001f]/.test(name)) return false;
12854
14842
  return name.toLowerCase().endsWith(".md");
12855
14843
  }
@@ -12886,7 +14874,7 @@ var PlanBuilder = class {
12886
14874
  /** Nombres de nota que habria en la carpeta, en minusculas. */
12887
14875
  async noteNames() {
12888
14876
  const names = /* @__PURE__ */ new Map();
12889
- for (const name of await listNoteFiles(path35.join(this.cwd, MEMORY_DIR))) {
14877
+ for (const name of await listNoteFiles(path43.join(this.cwd, MEMORY_DIR))) {
12890
14878
  names.set(name.toLowerCase(), name);
12891
14879
  }
12892
14880
  for (const rel of this.files.keys()) {
@@ -12928,8 +14916,8 @@ var PlanBuilder = class {
12928
14916
  file.target = absolute;
12929
14917
  continue;
12930
14918
  }
12931
- const target = await realpath2(absolute);
12932
- const info = await stat18(target);
14919
+ const target = await realpath3(absolute);
14920
+ const info = await stat24(target);
12933
14921
  if (info.nlink > 1) {
12934
14922
  throw new MemoryBridgeError(serverText("memoryHardLinks", { file: file.rel }));
12935
14923
  }
@@ -12948,7 +14936,7 @@ var PlanBuilder = class {
12948
14936
  );
12949
14937
  if (pending.length === 0) return;
12950
14938
  if (pending.some((file) => file.rel.startsWith(`${MEMORY_DIR}/`))) {
12951
- await mkdir7(path35.join(this.cwd, MEMORY_DIR), { recursive: true });
14939
+ await mkdir7(path43.join(this.cwd, MEMORY_DIR), { recursive: true });
12952
14940
  }
12953
14941
  for (const file of pending) {
12954
14942
  if (file.target === null) throw new Error(`Unvalidated plan: ${file.rel}`);
@@ -13059,7 +15047,7 @@ async function planNativeImport(plan, cwd, home) {
13059
15047
  const local = await plan.noteNames();
13060
15048
  for (const name of await listNoteFiles(folder)) {
13061
15049
  const key = name.toLowerCase();
13062
- const content = await readIfFile(path35.join(folder, name), name);
15050
+ const content = await readIfFile(path43.join(folder, name), name);
13063
15051
  if (content === null) continue;
13064
15052
  const already = chosen.get(key);
13065
15053
  if (already !== void 0) {
@@ -13073,7 +15061,7 @@ async function planNativeImport(plan, cwd, home) {
13073
15061
  if (existing !== null && !existing.equals(content)) skipped.push(name);
13074
15062
  continue;
13075
15063
  }
13076
- plan.put(`${MEMORY_DIR}/${name}`, null, content, "copy", "", path35.join(folder, name));
15064
+ plan.put(`${MEMORY_DIR}/${name}`, null, content, "copy", "", path43.join(folder, name));
13077
15065
  copied.push(name);
13078
15066
  }
13079
15067
  }
@@ -13083,7 +15071,7 @@ async function planNativeImport(plan, cwd, home) {
13083
15071
  const known = new Set(linkTargets(index.toString("utf8")));
13084
15072
  const additions = [];
13085
15073
  for (const folder of folders) {
13086
- const nativeIndex = await readIfFile(path35.join(folder, MEMORY_INDEX_FILE), MEMORY_INDEX_FILE);
15074
+ const nativeIndex = await readIfFile(path43.join(folder, MEMORY_INDEX_FILE), MEMORY_INDEX_FILE);
13087
15075
  if (nativeIndex === null) continue;
13088
15076
  for (const line of nativeIndex.toString("utf8").replace(/^\uFEFF/, "").split(/\r?\n/)) {
13089
15077
  const target = linkTargets(line)[0];
@@ -13158,9 +15146,9 @@ function findMarkers(content, lines, ignoredFences) {
13158
15146
  }
13159
15147
  continue;
13160
15148
  }
13161
- const open8 = FENCE_OPEN.exec(text);
13162
- const opening = open8?.[1];
13163
- if (opening !== void 0 && !ignoredFences.has(index) && !(opening[0] === "`" && (open8?.[2] ?? "").includes("`"))) {
15149
+ const open9 = FENCE_OPEN.exec(text);
15150
+ const opening = open9?.[1];
15151
+ if (opening !== void 0 && !ignoredFences.has(index) && !(opening[0] === "`" && (open9?.[2] ?? "").includes("`"))) {
13164
15152
  fence = { line: index, char: opening[0] ?? "`", length: opening.length };
13165
15153
  continue;
13166
15154
  }
@@ -13301,14 +15289,18 @@ function computeReach(folderExists, indexExists, files) {
13301
15289
  ),
13302
15290
  entry("codex", agentsHasBlock, viaAgents),
13303
15291
  entry("antigravity", agentsHasBlock, viaAgents),
13304
- entry("opencode", openCode.reaches, openCode.via)
15292
+ entry("opencode", openCode.reaches, openCode.via),
15293
+ // Pi busca igual que OpenCode: `AGENTS.md`, y `CLAUDE.md` solo si el primero no
15294
+ // esta (medido que con los dos carga solo el primero; el respaldo, segun su
15295
+ // documentacion, §16.11).
15296
+ entry("pi", openCode.reaches, openCode.via)
13305
15297
  ];
13306
15298
  }
13307
15299
  async function describeNotes(folder, names) {
13308
15300
  const stated = [];
13309
15301
  for (const name of names) {
13310
15302
  try {
13311
- const info = await stat18(path35.join(folder, name));
15303
+ const info = await stat24(path43.join(folder, name));
13312
15304
  if (info.isFile()) stated.push({ name, sizeBytes: info.size, modifiedAt: info.mtimeMs });
13313
15305
  } catch {
13314
15306
  }
@@ -13316,7 +15308,7 @@ async function describeNotes(folder, names) {
13316
15308
  stated.sort((a, b) => b.modifiedAt - a.modifiedAt || a.name.localeCompare(b.name));
13317
15309
  const notes = [];
13318
15310
  for (const item of stated.slice(0, MAX_LISTED_NOTES)) {
13319
- const head = await readHead(path35.join(folder, item.name));
15311
+ const head = await readHead(path43.join(folder, item.name));
13320
15312
  notes.push({ ...item, ...describeNoteText(head, item.name) });
13321
15313
  }
13322
15314
  return notes;
@@ -13374,28 +15366,29 @@ async function countNativeMemory(cwd, home, sharedNames) {
13374
15366
  return { available: names.size, pending };
13375
15367
  }
13376
15368
  async function nativeMemoryFolders(cwd, home) {
13377
- const root = path35.join(home, ".claude", "projects");
15369
+ const root = path43.join(home, ".claude", "projects");
13378
15370
  const slug = projectSlugFor(cwd).toLowerCase();
13379
15371
  try {
13380
- const entries = await readdir9(root, { withFileTypes: true });
13381
- return entries.filter((entry) => entry.isDirectory() && entry.name.toLowerCase() === slug).map((entry) => entry.name).sort().map((name) => path35.join(root, name, "memory"));
15372
+ const entries = await readdir11(root, { withFileTypes: true });
15373
+ return entries.filter((entry) => entry.isDirectory() && entry.name.toLowerCase() === slug).map((entry) => entry.name).sort().map((name) => path43.join(root, name, "memory"));
13382
15374
  } catch {
13383
15375
  return [];
13384
15376
  }
13385
15377
  }
13386
15378
  function buildGlobalFragments(home) {
13387
15379
  const windows = process.platform === "win32";
13388
- const globalFile = path35.join(home, ...GLOBAL_MEMORY_RELATIVE.split("/"));
15380
+ const globalFile = path43.join(home, ...GLOBAL_MEMORY_RELATIVE.split("/"));
13389
15381
  const hardLink = (link) => windows ? `mklink /H "${link}" "${globalFile}"` : `ln "${globalFile}" "${link}"`;
13390
15382
  const hardLinkNote = windows ? serverText("memoryNoteHardLinkCmd", { path: GLOBAL_MEMORY_RELATIVE }) : serverText("memoryNoteHardLink", { path: GLOBAL_MEMORY_RELATIVE });
13391
- const codexTarget = path35.join(home, ".codex", "AGENTS.md");
13392
- const geminiTarget = path35.join(home, ".gemini", "GEMINI.md");
15383
+ const codexTarget = path43.join(home, ".codex", "AGENTS.md");
15384
+ const geminiTarget = path43.join(home, ".gemini", "GEMINI.md");
15385
+ const piTarget = path43.join(home, ".pi", "agent", "AGENTS.md");
13393
15386
  const homeSlashes = home.replace(/\\/g, "/").replace(/\/+$/, "");
13394
15387
  return [
13395
15388
  {
13396
15389
  agent: "claude-code",
13397
15390
  label: MEMORY_AGENT_LABELS["claude-code"],
13398
- target: path35.join(home, ".claude", "CLAUDE.md"),
15391
+ target: path43.join(home, ".claude", "CLAUDE.md"),
13399
15392
  kind: "line",
13400
15393
  text: `@~/${GLOBAL_MEMORY_RELATIVE}`,
13401
15394
  note: serverText("memoryNoteClaude")
@@ -13419,10 +15412,18 @@ function buildGlobalFragments(home) {
13419
15412
  {
13420
15413
  agent: "opencode",
13421
15414
  label: MEMORY_AGENT_LABELS.opencode,
13422
- target: path35.join(home, ".config", "opencode", "opencode.json"),
15415
+ target: path43.join(home, ".config", "opencode", "opencode.json"),
13423
15416
  kind: "json",
13424
15417
  text: `"instructions": ["${homeSlashes}/${GLOBAL_MEMORY_RELATIVE}"]`,
13425
15418
  note: serverText("memoryNoteOpenCode")
15419
+ },
15420
+ {
15421
+ agent: "pi",
15422
+ label: MEMORY_AGENT_LABELS.pi,
15423
+ target: piTarget,
15424
+ kind: "command",
15425
+ text: hardLink(piTarget),
15426
+ note: hardLinkNote
13426
15427
  }
13427
15428
  ];
13428
15429
  }
@@ -13467,17 +15468,17 @@ async function readGit(cwd) {
13467
15468
  if (!toplevel || !gitDir || !commonDir) return fail(serverText("gitUnexpected", { command: "rev-parse" }));
13468
15469
  const ignored = await checkIgnored(cwd, INDEX_REL);
13469
15470
  if (typeof ignored !== "boolean") return fail(ignored);
13470
- const absoluteGitDir = path35.resolve(cwd, gitDir);
13471
- const absoluteCommonDir = path35.resolve(cwd, commonDir);
15471
+ const absoluteGitDir = path43.resolve(cwd, gitDir);
15472
+ const absoluteCommonDir = path43.resolve(cwd, commonDir);
13472
15473
  let worktree = null;
13473
15474
  let main2 = null;
13474
15475
  if (!await samePath(absoluteGitDir, absoluteCommonDir)) {
13475
- if (path35.basename(absoluteCommonDir).toLowerCase() === ".git") {
13476
- const mainRoot = path35.dirname(absoluteCommonDir);
13477
- const inner = path35.relative(path35.resolve(toplevel), path35.resolve(cwd));
13478
- const sameFolder = inner.length === 0 || inner.startsWith("..") || path35.isAbsolute(inner);
13479
- const mainPath = sameFolder ? mainRoot : path35.join(mainRoot, inner);
13480
- const memoryRel = sameFolder ? MEMORY_DIR : [...inner.split(path35.sep), MEMORY_DIR].join("/");
15476
+ if (path43.basename(absoluteCommonDir).toLowerCase() === ".git") {
15477
+ const mainRoot = path43.dirname(absoluteCommonDir);
15478
+ const inner = path43.relative(path43.resolve(toplevel), path43.resolve(cwd));
15479
+ const sameFolder = inner.length === 0 || inner.startsWith("..") || path43.isAbsolute(inner);
15480
+ const mainPath = sameFolder ? mainRoot : path43.join(mainRoot, inner);
15481
+ const memoryRel = sameFolder ? MEMORY_DIR : [...inner.split(path43.sep), MEMORY_DIR].join("/");
13481
15482
  main2 = { root: mainRoot, memoryRel };
13482
15483
  worktree = {
13483
15484
  mainPath,
@@ -13500,9 +15501,9 @@ async function checkIgnored(cwd, rel) {
13500
15501
  async function samePath(a, b) {
13501
15502
  const resolve = async (value) => {
13502
15503
  try {
13503
- return await realpath2(value);
15504
+ return await realpath3(value);
13504
15505
  } catch {
13505
- return path35.resolve(value);
15506
+ return path43.resolve(value);
13506
15507
  }
13507
15508
  };
13508
15509
  const [left, right] = await Promise.all([resolve(a), resolve(b)]);
@@ -13534,7 +15535,7 @@ async function existingInside(root, rel, kind) {
13534
15535
  }
13535
15536
  async function listNoteFiles(folder) {
13536
15537
  try {
13537
- const entries = await readdir9(folder, { withFileTypes: true });
15538
+ const entries = await readdir11(folder, { withFileTypes: true });
13538
15539
  return entries.filter(
13539
15540
  (entry) => entry.isFile() && isNoteName(entry.name) && entry.name.toLowerCase() !== MEMORY_INDEX_FILE.toLowerCase()
13540
15541
  ).map((entry) => entry.name).sort();
@@ -13544,9 +15545,9 @@ async function listNoteFiles(folder) {
13544
15545
  }
13545
15546
  async function readIfFile(absolute, label) {
13546
15547
  try {
13547
- const info = await stat18(absolute);
15548
+ const info = await stat24(absolute);
13548
15549
  if (!info.isFile()) throw new MemoryBridgeError(serverText("memoryNotAFile", { label }));
13549
- return await readFile12(absolute);
15550
+ return await readFile14(absolute);
13550
15551
  } catch (error) {
13551
15552
  if (isNotFound(error)) return null;
13552
15553
  throw error;
@@ -13554,7 +15555,7 @@ async function readIfFile(absolute, label) {
13554
15555
  }
13555
15556
  async function readHead(absolute) {
13556
15557
  try {
13557
- const handle = await open6(absolute, "r");
15558
+ const handle = await open7(absolute, "r");
13558
15559
  try {
13559
15560
  const buffer = Buffer.alloc(NOTE_HEAD_BYTES);
13560
15561
  const { bytesRead } = await handle.read(buffer, 0, NOTE_HEAD_BYTES, 0);
@@ -13568,14 +15569,14 @@ async function readHead(absolute) {
13568
15569
  }
13569
15570
  async function isFile(absolute) {
13570
15571
  try {
13571
- return (await stat18(absolute)).isFile();
15572
+ return (await stat24(absolute)).isFile();
13572
15573
  } catch {
13573
15574
  return false;
13574
15575
  }
13575
15576
  }
13576
15577
  async function isDirectory2(absolute) {
13577
15578
  try {
13578
- return (await stat18(absolute)).isDirectory();
15579
+ return (await stat24(absolute)).isDirectory();
13579
15580
  } catch {
13580
15581
  return false;
13581
15582
  }
@@ -13593,7 +15594,7 @@ async function writeAtomic(target, content, mustNotExist) {
13593
15594
  await handle.close();
13594
15595
  }
13595
15596
  if (mustNotExist && await exists2(target)) {
13596
- throw new MemoryBridgeError(serverText("memoryAppeared", { file: path35.basename(target) }));
15597
+ throw new MemoryBridgeError(serverText("memoryAppeared", { file: path43.basename(target) }));
13597
15598
  }
13598
15599
  await renameWithRetry(temporary, target);
13599
15600
  } catch (error) {
@@ -13604,12 +15605,12 @@ async function writeAtomic(target, content, mustNotExist) {
13604
15605
  async function createTemporary(target) {
13605
15606
  for (let attempt = 1; ; attempt += 1) {
13606
15607
  const suffix = randomBytes4(6).toString("hex");
13607
- const temporary = path35.join(
13608
- path35.dirname(target),
13609
- `.${path35.basename(target)}.${process.pid}.${suffix}.tmp`
15608
+ const temporary = path43.join(
15609
+ path43.dirname(target),
15610
+ `.${path43.basename(target)}.${process.pid}.${suffix}.tmp`
13610
15611
  );
13611
15612
  try {
13612
- return { temporary, handle: await open6(temporary, "wx") };
15613
+ return { temporary, handle: await open7(temporary, "wx") };
13613
15614
  } catch (error) {
13614
15615
  const code = error.code;
13615
15616
  if (code !== "EEXIST" || attempt >= TEMPORARY_ATTEMPTS) throw error;
@@ -13618,7 +15619,7 @@ async function createTemporary(target) {
13618
15619
  }
13619
15620
  async function exists2(absolute) {
13620
15621
  try {
13621
- await stat18(absolute);
15622
+ await stat24(absolute);
13622
15623
  return true;
13623
15624
  } catch {
13624
15625
  return false;
@@ -13648,7 +15649,7 @@ var UnknownTerminalError = class extends Error {
13648
15649
  };
13649
15650
  var MemoryHub = class extends EventEmitter3 {
13650
15651
  /** `home` es inyectable por lo mismo que en el puente: probar sin tocar el real. */
13651
- constructor(registry, home = homedir10()) {
15652
+ constructor(registry, home = homedir12()) {
13652
15653
  super();
13653
15654
  this.registry = registry;
13654
15655
  this.home = home;
@@ -13844,13 +15845,13 @@ var MemoryHub = class extends EventEmitter3 {
13844
15845
  }
13845
15846
  };
13846
15847
  function keyFor(cwd) {
13847
- const resolved = path36.resolve(cwd);
15848
+ const resolved = path44.resolve(cwd);
13848
15849
  return process.platform === "win32" ? resolved.toLowerCase() : resolved;
13849
15850
  }
13850
15851
  function isRelevantPath(cwd, candidate) {
13851
- const relative = path36.relative(cwd, candidate);
15852
+ const relative = path44.relative(cwd, candidate);
13852
15853
  if (relative.length === 0) return true;
13853
- if (relative.startsWith("..") || path36.isAbsolute(relative)) return false;
15854
+ if (relative.startsWith("..") || path44.isAbsolute(relative)) return false;
13854
15855
  const same = (a, b) => process.platform === "win32" ? a.toLowerCase() === b.toLowerCase() : a === b;
13855
15856
  const parts = relative.split(/[\\/]/);
13856
15857
  const [first = "", second = "", third = ""] = parts;
@@ -13868,8 +15869,8 @@ function isRelevantPath(cwd, candidate) {
13868
15869
 
13869
15870
  // packages/server/src/paste-store.ts
13870
15871
  import { createHash as createHash3, randomUUID as randomUUID3 } from "node:crypto";
13871
- import { mkdir as mkdir8, readdir as readdir10, rm as rm3, stat as stat19, writeFile as writeFile4 } from "node:fs/promises";
13872
- import path37 from "node:path";
15872
+ import { mkdir as mkdir8, readdir as readdir12, rm as rm3, stat as stat25, writeFile as writeFile4 } from "node:fs/promises";
15873
+ import path45 from "node:path";
13873
15874
 
13874
15875
  // packages/server/src/vault/markdown.ts
13875
15876
  var pad2 = (value) => String(value).padStart(2, "0");
@@ -13960,10 +15961,10 @@ ${fenced(part.input)}`;
13960
15961
  return `> ${sentence(noticeText(part))}`;
13961
15962
  }
13962
15963
  }
13963
- function renderEvent(event, images, previousDay, options) {
15964
+ function renderEvent(event2, images, previousDay, options) {
13964
15965
  const blocks = [];
13965
15966
  let imageNumber = 0;
13966
- for (const part of event.parts) {
15967
+ for (const part of event2.parts) {
13967
15968
  let image;
13968
15969
  if (part.kind === "image") {
13969
15970
  image = images[imageNumber];
@@ -13973,17 +15974,17 @@ function renderEvent(event, images, previousDay, options) {
13973
15974
  if (rendered !== null) blocks.push(rendered);
13974
15975
  }
13975
15976
  if (blocks.length === 0) return null;
13976
- const day = event.at > 0 ? formatLocalDay(event.at) : null;
13977
- const heading = [event.role === "user" ? "Usuario" : "Asistente"];
15977
+ const day = event2.at > 0 ? formatLocalDay(event2.at) : null;
15978
+ const heading = [event2.role === "user" ? "Usuario" : "Asistente"];
13978
15979
  if (day !== null) {
13979
- heading.push(event.role === "user" || day !== previousDay ? formatLocalDateTime(event.at) : formatTime(event.at));
15980
+ heading.push(event2.role === "user" || day !== previousDay ? formatLocalDateTime(event2.at) : formatTime(event2.at));
13980
15981
  }
13981
- if (event.role === "assistant") {
13982
- if (event.model !== null && event.model.length > 0) heading.push(event.model);
13983
- if (event.effort !== null && event.effort.length > 0) heading.push(event.effort);
13984
- if (event.durationMs !== null) heading.push(formatDuration(event.durationMs));
15982
+ if (event2.role === "assistant") {
15983
+ if (event2.model !== null && event2.model.length > 0) heading.push(event2.model);
15984
+ if (event2.effort !== null && event2.effort.length > 0) heading.push(event2.effort);
15985
+ if (event2.durationMs !== null) heading.push(formatDuration(event2.durationMs));
13985
15986
  }
13986
- if (event.queued) heading.push("enviado mientras trabajaba");
15987
+ if (event2.queued) heading.push("enviado mientras trabajaba");
13987
15988
  return { text: `## ${heading.join(" \xB7 ")}
13988
15989
 
13989
15990
  ${blocks.join("\n\n")}`, day: day ?? previousDay };
@@ -14075,17 +16076,17 @@ var QUESTION_LABEL_CHARS = 200;
14075
16076
  var TEXT_CHAR_STEPS = [4e3, 2e3, 1e3, 500, 250];
14076
16077
  var HANDOFF_EMPTY_TEXT = serverText("handoffEmpty");
14077
16078
  var HANDOFF_NO_TRANSCRIPT_TEXT = serverText("handoffNoTranscript");
14078
- function opensTurn(event) {
14079
- return event.role === "user" && event.parts.some((part) => part.kind === "text" && part.text.trim().length > 0);
16079
+ function opensTurn(event2) {
16080
+ return event2.role === "user" && event2.parts.some((part) => part.kind === "text" && part.text.trim().length > 0);
14080
16081
  }
14081
16082
  function splitTurns(events) {
14082
16083
  const turns = [];
14083
- for (const event of events) {
14084
- if (opensTurn(event)) {
14085
- turns.push({ number: turns.length + 1, events: [event] });
16084
+ for (const event2 of events) {
16085
+ if (opensTurn(event2)) {
16086
+ turns.push({ number: turns.length + 1, events: [event2] });
14086
16087
  continue;
14087
16088
  }
14088
- turns.at(-1)?.events.push(event);
16089
+ turns.at(-1)?.events.push(event2);
14089
16090
  }
14090
16091
  return turns;
14091
16092
  }
@@ -14172,9 +16173,9 @@ function renderQuestionLine(part, result) {
14172
16173
  const answer = `"${cut2 ? `${text}\u2026` : text}"`;
14173
16174
  return `${title}: ${result.isError ? "not answered" : "answered"} ${answer}`;
14174
16175
  }
14175
- function renderRequestBody(event, textChars) {
16176
+ function renderRequestBody(event2, textChars) {
14176
16177
  const blocks = [];
14177
- for (const part of event.parts) {
16178
+ for (const part of event2.parts) {
14178
16179
  if (part.kind === "text") {
14179
16180
  const text = renderText(part.text, part.truncated, textChars);
14180
16181
  if (text !== null) blocks.push(text);
@@ -14189,8 +16190,8 @@ function renderTurn(turn, textChars) {
14189
16190
  if (request === void 0) return `## Turn ${turn.number} \xB7 user`;
14190
16191
  const results = /* @__PURE__ */ new Map();
14191
16192
  const calls = /* @__PURE__ */ new Set();
14192
- for (const event of turn.events) {
14193
- for (const part of event.parts) {
16193
+ for (const event2 of turn.events) {
16194
+ for (const part of event2.parts) {
14194
16195
  if (part.kind === "tool-result" && !results.has(part.toolUseId)) results.set(part.toolUseId, part);
14195
16196
  if (part.kind === "tool-call" || part.kind === "question") calls.add(part.toolUseId);
14196
16197
  }
@@ -14209,7 +16210,7 @@ ${line}`;
14209
16210
  answer.push(text);
14210
16211
  listOpen = false;
14211
16212
  };
14212
- const renderAnswerPart = (event, part) => {
16213
+ const renderAnswerPart = (event2, part) => {
14213
16214
  switch (part.kind) {
14214
16215
  case "text": {
14215
16216
  const text = renderText(part.text, part.truncated, textChars);
@@ -14226,7 +16227,7 @@ ${line}`;
14226
16227
  if (!calls.has(part.toolUseId)) pushLine(`- Result of an earlier tool call \u2192 ${renderResult(part)}`);
14227
16228
  return;
14228
16229
  case "image":
14229
- pushLine(event.role === "user" ? `- ${imagesText(1)} (from the user)` : `- ${imagesText(1)}`);
16230
+ pushLine(event2.role === "user" ? `- ${imagesText(1)} (from the user)` : `- ${imagesText(1)}`);
14230
16231
  return;
14231
16232
  case "notice":
14232
16233
  pushLine(`- Notice: ${oneLine2(noticeLine(part))}`);
@@ -14238,8 +16239,8 @@ ${line}`;
14238
16239
  for (const part of request.parts) {
14239
16240
  if (part.kind !== "text" && part.kind !== "image") renderAnswerPart(request, part);
14240
16241
  }
14241
- for (const event of rest) {
14242
- for (const part of event.parts) renderAnswerPart(event, part);
16242
+ for (const event2 of rest) {
16243
+ for (const part of event2.parts) renderAnswerPart(event2, part);
14243
16244
  }
14244
16245
  const blocks = [userBlocks.join("\n\n")];
14245
16246
  if (answer.length > 0) blocks.push([`## Turn ${turn.number} \xB7 assistant`, ...answer].join("\n\n"));
@@ -14384,8 +16385,8 @@ function buildContinuationMessage(input) {
14384
16385
  }
14385
16386
  return sanitizeForPaste(lines.join("\n"));
14386
16387
  }
14387
- function transcriptReferenceFor(path52, style) {
14388
- return style === "at-quoted" ? fileReference(path52, "at-quoted") : `"${path52}"`;
16388
+ function transcriptReferenceFor(path60, style) {
16389
+ return style === "at-quoted" ? fileReference(path60, "at-quoted") : `"${path60}"`;
14389
16390
  }
14390
16391
  function planTranscript(input) {
14391
16392
  const turns = splitTurns(input.events);
@@ -14561,11 +16562,11 @@ var PasteStore = class {
14561
16562
  if (signature === null) {
14562
16563
  throw new PasteImageError(serverText("imageUnknownFormat"));
14563
16564
  }
14564
- const directory = path37.join(this.root, safeSegment(terminalId));
16565
+ const directory = path45.join(this.root, safeSegment(terminalId));
14565
16566
  await mkdir8(directory, { recursive: true, mode: DIRECTORY_MODE });
14566
16567
  this.counter += 1;
14567
16568
  const name = `pegada-${this.counter}-${randomUUID3().slice(0, 8)}.${signature.ext}`;
14568
- const file = path37.join(directory, name);
16569
+ const file = path45.join(directory, name);
14569
16570
  await writeFile4(file, bytes, { mode: FILE_MODE });
14570
16571
  return { path: file, bytes: bytes.length };
14571
16572
  }
@@ -14583,11 +16584,11 @@ var PasteStore = class {
14583
16584
  if (bytes.length > MAX_TEXT_BYTES) {
14584
16585
  throw new PasteTextError(`The text is ${bytes.length} bytes; the maximum is ${MAX_TEXT_BYTES}.`);
14585
16586
  }
14586
- const directory = path37.join(this.root, safeSegment(terminalId));
16587
+ const directory = path45.join(this.root, safeSegment(terminalId));
14587
16588
  await mkdir8(directory, { recursive: true, mode: DIRECTORY_MODE });
14588
16589
  this.counter += 1;
14589
16590
  const name = `${baseName}-${this.counter}-${randomUUID3().slice(0, 8)}.md`;
14590
- const file = path37.join(directory, name);
16591
+ const file = path45.join(directory, name);
14591
16592
  await writeFile4(file, bytes, { mode: FILE_MODE });
14592
16593
  return { path: file, bytes: bytes.length };
14593
16594
  }
@@ -14617,17 +16618,17 @@ var PasteStore = class {
14617
16618
  })
14618
16619
  );
14619
16620
  }
14620
- const directory = path37.join(this.root, safeSegment(terminalId));
16621
+ const directory = path45.join(this.root, safeSegment(terminalId));
14621
16622
  await mkdir8(directory, { recursive: true, mode: DIRECTORY_MODE });
14622
16623
  this.counter += 1;
14623
16624
  const name = `adjunto-${this.counter}-${randomUUID3().slice(0, 8)}-${safe}`;
14624
- const file = path37.join(directory, name);
16625
+ const file = path45.join(directory, name);
14625
16626
  await writeFile4(file, bytes, { mode: FILE_MODE });
14626
16627
  return { path: file, bytes: bytes.length, name };
14627
16628
  }
14628
16629
  /** Borra lo de una pestana. Se llama al cerrarla. */
14629
16630
  async clearTerminal(terminalId) {
14630
- await rm3(path37.join(this.root, safeSegment(terminalId)), {
16631
+ await rm3(path45.join(this.root, safeSegment(terminalId)), {
14631
16632
  recursive: true,
14632
16633
  force: true
14633
16634
  }).catch(() => {
@@ -14643,15 +16644,15 @@ var PasteStore = class {
14643
16644
  async purgeStale(now = Date.now()) {
14644
16645
  let entries;
14645
16646
  try {
14646
- entries = await readdir10(this.root);
16647
+ entries = await readdir12(this.root);
14647
16648
  } catch {
14648
16649
  return;
14649
16650
  }
14650
16651
  await Promise.all(
14651
16652
  entries.map(async (entry) => {
14652
- const full = path37.join(this.root, entry);
16653
+ const full = path45.join(this.root, entry);
14653
16654
  try {
14654
- const info = await stat19(full);
16655
+ const info = await stat25(full);
14655
16656
  if (now - info.mtimeMs > STALE_AFTER_MS) {
14656
16657
  await rm3(full, { recursive: true, force: true });
14657
16658
  }
@@ -14668,8 +16669,8 @@ function safeSegment(value) {
14668
16669
 
14669
16670
  // packages/server/src/session-index.ts
14670
16671
  import { EventEmitter as EventEmitter4 } from "node:events";
14671
- import { mkdir as mkdir9, readFile as readFile13, stat as stat20, writeFile as writeFile5 } from "node:fs/promises";
14672
- import path38 from "node:path";
16672
+ import { mkdir as mkdir9, readFile as readFile15, stat as stat26, writeFile as writeFile5 } from "node:fs/promises";
16673
+ import path46 from "node:path";
14673
16674
  var CACHE_VERSION = 4;
14674
16675
  var vaultPairKey = (agent, sessionId) => `${agent}/${sessionId}`;
14675
16676
  function vaultOnlySessions(natives, vault, listState, ready) {
@@ -14721,7 +16722,7 @@ function cacheKey(agent, ref) {
14721
16722
  }
14722
16723
  async function pathExists3(candidate) {
14723
16724
  try {
14724
- await stat20(candidate);
16725
+ await stat26(candidate);
14725
16726
  return true;
14726
16727
  } catch {
14727
16728
  return false;
@@ -14771,7 +16772,7 @@ function migrateEntryV3(filePath, value) {
14771
16772
  return {
14772
16773
  agent: "claude-code",
14773
16774
  ref: filePath,
14774
- group: path38.basename(path38.dirname(filePath)),
16775
+ group: path46.basename(path46.dirname(filePath)),
14775
16776
  mtimeMs,
14776
16777
  sizeBytes,
14777
16778
  cwd,
@@ -15036,7 +17037,7 @@ var SessionIndex = class extends EventEmitter4 {
15036
17037
  }
15037
17038
  async loadCache() {
15038
17039
  try {
15039
- const raw = await readFile13(sessionIndexCachePath(), "utf8");
17040
+ const raw = await readFile15(sessionIndexCachePath(), "utf8");
15040
17041
  const parsed = JSON.parse(raw);
15041
17042
  const migrated = migrateIndexCache(parsed);
15042
17043
  if (migrated === null) return;
@@ -15264,8 +17265,8 @@ var SessionIndex = class extends EventEmitter4 {
15264
17265
  };
15265
17266
 
15266
17267
  // packages/server/src/settings-store.ts
15267
- import { mkdir as mkdir10, readFile as readFile14, stat as stat21 } from "node:fs/promises";
15268
- import path39 from "node:path";
17268
+ import { mkdir as mkdir10, readFile as readFile16, stat as stat27 } from "node:fs/promises";
17269
+ import path47 from "node:path";
15269
17270
  var SETTINGS_VERSION = 1;
15270
17271
  var VAULT_TOOL_RESULT_DEFAULT_CHARS = 64e3;
15271
17272
  var VAULT_TOOL_RESULT_MIN_CHARS = 4e3;
@@ -15282,7 +17283,7 @@ function defaultAppSettings() {
15282
17283
  function isAbsoluteDir(dir, platform) {
15283
17284
  if (dir.length === 0 || dir.includes("\0")) return false;
15284
17285
  if (platform === "win32") return /^[A-Za-z]:[\\/]/.test(dir) || /^\\\\[^\\/]+[\\/][^\\/]+/.test(dir);
15285
- return path39.posix.isAbsolute(dir);
17286
+ return path47.posix.isAbsolute(dir);
15286
17287
  }
15287
17288
  function clampToolResultMaxChars(value) {
15288
17289
  if (typeof value !== "number" || !Number.isFinite(value)) return VAULT_TOOL_RESULT_DEFAULT_CHARS;
@@ -15341,7 +17342,7 @@ var SettingsStore = class {
15341
17342
  async load() {
15342
17343
  let raw;
15343
17344
  try {
15344
- raw = await readFile14(this.filePath, "utf8");
17345
+ raw = await readFile16(this.filePath, "utf8");
15345
17346
  } catch {
15346
17347
  this.current = frozen(defaultAppSettings());
15347
17348
  return;
@@ -15369,7 +17370,7 @@ var SettingsStore = class {
15369
17370
  async reloadRemote() {
15370
17371
  let parsed;
15371
17372
  try {
15372
- parsed = JSON.parse(await readFile14(this.filePath, "utf8"));
17373
+ parsed = JSON.parse(await readFile16(this.filePath, "utf8"));
15373
17374
  } catch {
15374
17375
  return;
15375
17376
  }
@@ -15380,7 +17381,7 @@ var SettingsStore = class {
15380
17381
  /** Una marca que cambia cuando el archivo cambia (fecha y tamano), o '' si no esta. */
15381
17382
  async stamp() {
15382
17383
  try {
15383
- const info = await stat21(this.filePath);
17384
+ const info = await stat27(this.filePath);
15384
17385
  return `${info.mtimeMs}:${info.size}`;
15385
17386
  } catch {
15386
17387
  return "";
@@ -15424,7 +17425,7 @@ var SettingsStore = class {
15424
17425
  if (!isRemotePort(remote.port)) throw new Error(`The remote access port isn't valid: ${JSON.stringify(remote.port)}`);
15425
17426
  next.remote.port = remote.port;
15426
17427
  }
15427
- await mkdir10(path39.dirname(this.filePath), { recursive: true });
17428
+ await mkdir10(path47.dirname(this.filePath), { recursive: true });
15428
17429
  await writeFileAtomic(this.filePath, `${JSON.stringify(next, null, 2)}
15429
17430
  `);
15430
17431
  this.current = frozen(next);
@@ -15512,8 +17513,8 @@ function supportStartupLineColored() {
15512
17513
 
15513
17514
  // packages/server/src/notes-store.ts
15514
17515
  import { randomUUID as randomUUID4 } from "node:crypto";
15515
- import { mkdir as mkdir11, readFile as readFile15, rename as rename5, rm as rm4, writeFile as writeFile6 } from "node:fs/promises";
15516
- import path40 from "node:path";
17516
+ import { mkdir as mkdir11, readFile as readFile17, rename as rename5, rm as rm4, writeFile as writeFile6 } from "node:fs/promises";
17517
+ import path48 from "node:path";
15517
17518
  var STATE_VERSION2 = 1;
15518
17519
  var WRITE_DEBOUNCE_MS2 = 500;
15519
17520
  var NotesError = class extends ServerTextError {
@@ -15556,7 +17557,7 @@ var NotesStore = class {
15556
17557
  }
15557
17558
  async load() {
15558
17559
  try {
15559
- const raw = await readFile15(this.statePath, "utf8");
17560
+ const raw = await readFile17(this.statePath, "utf8");
15560
17561
  this.notes = parseState2(raw) ?? [];
15561
17562
  } catch {
15562
17563
  this.notes = [];
@@ -15673,7 +17674,7 @@ var NotesStore = class {
15673
17674
  const image = this.findImage(imageId);
15674
17675
  if (image === null) return null;
15675
17676
  try {
15676
- const bytes = await readFile15(this.imagePath(image));
17677
+ const bytes = await readFile17(this.imagePath(image));
15677
17678
  return { mediaType: image.mediaType, data: bytes.toString("base64") };
15678
17679
  } catch {
15679
17680
  return null;
@@ -15693,7 +17694,7 @@ var NotesStore = class {
15693
17694
  const images = [];
15694
17695
  for (const image of note.images) {
15695
17696
  try {
15696
- const bytes = await readFile15(this.imagePath(image));
17697
+ const bytes = await readFile17(this.imagePath(image));
15697
17698
  images.push({ mediaType: image.mediaType, data: bytes.toString("base64") });
15698
17699
  } catch {
15699
17700
  }
@@ -15717,7 +17718,7 @@ var NotesStore = class {
15717
17718
  }
15718
17719
  imagePath(image) {
15719
17720
  const safe = image.imageId.replace(/[^a-zA-Z0-9-]/g, "");
15720
- return path40.join(this.imagesDir, `${safe}.${extensionFor(image.mediaType)}`);
17721
+ return path48.join(this.imagesDir, `${safe}.${extensionFor(image.mediaType)}`);
15721
17722
  }
15722
17723
  async unlinkImage(image) {
15723
17724
  await rm4(this.imagePath(image), { force: true }).catch(() => {
@@ -15735,8 +17736,8 @@ var NotesStore = class {
15735
17736
  async writeNow() {
15736
17737
  this.dirty = false;
15737
17738
  try {
15738
- await mkdir11(path40.dirname(this.statePath), { recursive: true });
15739
- const temporary = path40.join(path40.dirname(this.statePath), `notes.${process.pid}.tmp`);
17739
+ await mkdir11(path48.dirname(this.statePath), { recursive: true });
17740
+ const temporary = path48.join(path48.dirname(this.statePath), `notes.${process.pid}.tmp`);
15740
17741
  await writeFile6(
15741
17742
  temporary,
15742
17743
  JSON.stringify({ version: STATE_VERSION2, notes: this.notes }, null, 2),
@@ -15750,11 +17751,14 @@ var NotesStore = class {
15750
17751
  };
15751
17752
 
15752
17753
  // packages/server/src/draft-store.ts
15753
- import { mkdir as mkdir12, readFile as readFile16, rename as rename6, writeFile as writeFile7 } from "node:fs/promises";
15754
- import path41 from "node:path";
17754
+ import { mkdir as mkdir12, readFile as readFile18, rename as rename6, writeFile as writeFile7 } from "node:fs/promises";
17755
+ import path49 from "node:path";
15755
17756
  var STATE_VERSION3 = 1;
15756
17757
  var WRITE_DEBOUNCE_MS3 = 500;
15757
17758
  var MAX_STORED_DRAFTS = 100;
17759
+ function submitClearsDraft(message) {
17760
+ return message.keepDraft !== true;
17761
+ }
15758
17762
  function draftOwnerOf(descriptor) {
15759
17763
  if (descriptor === null || descriptor.kind !== "agent" || descriptor.agent === null) return null;
15760
17764
  if (descriptor.sessionId.length === 0) return null;
@@ -15854,7 +17858,7 @@ var DraftStore = class {
15854
17858
  }
15855
17859
  async load() {
15856
17860
  try {
15857
- this.drafts = parseState3(await readFile16(this.statePath, "utf8"));
17861
+ this.drafts = parseState3(await readFile18(this.statePath, "utf8"));
15858
17862
  } catch {
15859
17863
  this.drafts = /* @__PURE__ */ new Map();
15860
17864
  }
@@ -15945,7 +17949,7 @@ var DraftStore = class {
15945
17949
  async writeNow() {
15946
17950
  let onDisk = /* @__PURE__ */ new Map();
15947
17951
  try {
15948
- onDisk = parseState3(await readFile16(this.statePath, "utf8"));
17952
+ onDisk = parseState3(await readFile18(this.statePath, "utf8"));
15949
17953
  } catch {
15950
17954
  }
15951
17955
  const changed = this.changed;
@@ -15959,8 +17963,8 @@ var DraftStore = class {
15959
17963
  trimOldest(merged);
15960
17964
  this.drafts = new Map(merged);
15961
17965
  try {
15962
- await mkdir12(path41.dirname(this.statePath), { recursive: true });
15963
- const temporary = path41.join(path41.dirname(this.statePath), `composer-drafts.${process.pid}.tmp`);
17966
+ await mkdir12(path49.dirname(this.statePath), { recursive: true });
17967
+ const temporary = path49.join(path49.dirname(this.statePath), `composer-drafts.${process.pid}.tmp`);
15964
17968
  const payload = {
15965
17969
  version: STATE_VERSION3,
15966
17970
  drafts: [...merged.values()].map(({ agent, sessionId, updatedAt, draft }) => ({ agent, sessionId, updatedAt, draft }))
@@ -15977,7 +17981,7 @@ var DraftStore = class {
15977
17981
  // packages/server/src/shell-locator.ts
15978
17982
  import { access as access2 } from "node:fs/promises";
15979
17983
  import { constants as fsConstants2 } from "node:fs";
15980
- import path42 from "node:path";
17984
+ import path50 from "node:path";
15981
17985
  async function isExecutable(candidate) {
15982
17986
  try {
15983
17987
  await access2(candidate, process.platform === "win32" ? fsConstants2.R_OK : fsConstants2.X_OK);
@@ -15995,21 +17999,21 @@ var SYSTEM_PROBE = {
15995
17999
  function windowsPowerShellPath(env) {
15996
18000
  const systemRoot = env["SystemRoot"] ?? env["windir"];
15997
18001
  if (systemRoot === void 0 || systemRoot.length === 0) return null;
15998
- return path42.win32.join(systemRoot, "System32", "WindowsPowerShell", "v1.0", "powershell.exe");
18002
+ return path50.win32.join(systemRoot, "System32", "WindowsPowerShell", "v1.0", "powershell.exe");
15999
18003
  }
16000
18004
  function gitBashCandidates(gitPath, env) {
16001
18005
  const candidates = [];
16002
18006
  if (gitPath !== null) {
16003
- let dir = path42.win32.dirname(gitPath);
18007
+ let dir = path50.win32.dirname(gitPath);
16004
18008
  for (let level = 0; level < 3; level++) {
16005
- candidates.push(path42.win32.join(dir, "bin", "bash.exe"));
16006
- const parent = path42.win32.dirname(dir);
18009
+ candidates.push(path50.win32.join(dir, "bin", "bash.exe"));
18010
+ const parent = path50.win32.dirname(dir);
16007
18011
  if (parent === dir) break;
16008
18012
  dir = parent;
16009
18013
  }
16010
18014
  }
16011
- for (const base of [env["ProgramFiles"], env["LOCALAPPDATA"] === void 0 ? void 0 : path42.win32.join(env["LOCALAPPDATA"], "Programs")]) {
16012
- if (base !== void 0 && base.length > 0) candidates.push(path42.win32.join(base, "Git", "bin", "bash.exe"));
18015
+ for (const base of [env["ProgramFiles"], env["LOCALAPPDATA"] === void 0 ? void 0 : path50.win32.join(env["LOCALAPPDATA"], "Programs")]) {
18016
+ if (base !== void 0 && base.length > 0) candidates.push(path50.win32.join(base, "Git", "bin", "bash.exe"));
16013
18017
  }
16014
18018
  const systemRoot = (env["SystemRoot"] ?? env["windir"] ?? "").toLowerCase();
16015
18019
  return [...new Set(candidates)].filter((candidate) => systemRoot.length === 0 || !candidate.toLowerCase().startsWith(systemRoot));
@@ -16040,7 +18044,7 @@ async function locatePosixShells(probe) {
16040
18044
  const found = [];
16041
18045
  const preferred = probe.env["SHELL"];
16042
18046
  if (preferred !== void 0 && preferred.length > 0 && await probe.isExecutable(preferred)) {
16043
- const name = path42.posix.basename(preferred);
18047
+ const name = path50.posix.basename(preferred);
16044
18048
  found.push({ id: "login", label: name, location: { file: preferred, args: [], name } });
16045
18049
  }
16046
18050
  for (const [id, file] of [["bash", "/bin/bash"], ["sh", "/bin/sh"], ["zsh", "/bin/zsh"]]) {
@@ -16112,13 +18116,13 @@ var AppSettingsService = class extends EventEmitter5 {
16112
18116
  };
16113
18117
 
16114
18118
  // packages/server/src/session-watcher.ts
16115
- import { stat as stat22 } from "node:fs/promises";
18119
+ import { stat as stat28 } from "node:fs/promises";
16116
18120
  import chokidar3 from "chokidar";
16117
18121
  var INDEX_DEBOUNCE_MS = 750;
16118
18122
  var ROOT_PROBE_MS = 5e3;
16119
18123
  async function isDirectory3(target) {
16120
18124
  try {
16121
- return (await stat22(target)).isDirectory();
18125
+ return (await stat28(target)).isDirectory();
16122
18126
  } catch {
16123
18127
  return false;
16124
18128
  }
@@ -16221,7 +18225,7 @@ function watchSessions(index, hub, agents, options = {}) {
16221
18225
  // packages/server/src/terminal-registry.ts
16222
18226
  import { randomUUID as randomUUID5 } from "node:crypto";
16223
18227
  import { EventEmitter as EventEmitter6 } from "node:events";
16224
- import { stat as stat23 } from "node:fs/promises";
18228
+ import { stat as stat29 } from "node:fs/promises";
16225
18229
 
16226
18230
  // packages/server/src/launch-hook-slot.ts
16227
18231
  var LaunchHookSlot = class {
@@ -16483,8 +18487,8 @@ async function resolveLaunchPlan(launch) {
16483
18487
  }
16484
18488
 
16485
18489
  // packages/server/src/workspace-store.ts
16486
- import { mkdir as mkdir13, readFile as readFile17, writeFile as writeFile8 } from "node:fs/promises";
16487
- import path43 from "node:path";
18490
+ import { mkdir as mkdir13, readFile as readFile19, writeFile as writeFile8 } from "node:fs/promises";
18491
+ import path51 from "node:path";
16488
18492
  var STATE_VERSION4 = 1;
16489
18493
  var WRITE_DEBOUNCE_MS4 = 400;
16490
18494
  var LEGACY_TAB_AGENT = "claude-code";
@@ -16606,7 +18610,7 @@ var WorkspaceStore = class {
16606
18610
  pending = null;
16607
18611
  async load() {
16608
18612
  try {
16609
- const raw = await readFile17(workspaceStatePath(), "utf8");
18613
+ const raw = await readFile19(workspaceStatePath(), "utf8");
16610
18614
  return parseState4(raw) ?? EMPTY_STATE();
16611
18615
  } catch {
16612
18616
  return EMPTY_STATE();
@@ -16638,7 +18642,7 @@ var WorkspaceStore = class {
16638
18642
  try {
16639
18643
  await mkdir13(appConfigDir(), { recursive: true });
16640
18644
  const target = workspaceStatePath();
16641
- const temporary = path43.join(path43.dirname(target), `workspace.${process.pid}.tmp`);
18645
+ const temporary = path51.join(path51.dirname(target), `workspace.${process.pid}.tmp`);
16642
18646
  await writeFile8(temporary, JSON.stringify(serializeState(state), null, 2), "utf8");
16643
18647
  const { rename: rename7 } = await import("node:fs/promises");
16644
18648
  await rename7(temporary, target);
@@ -16876,7 +18880,7 @@ var TerminalRegistry = class extends EventEmitter6 {
16876
18880
  async assertDirectory(cwd) {
16877
18881
  let info;
16878
18882
  try {
16879
- info = await stat23(cwd);
18883
+ info = await stat29(cwd);
16880
18884
  } catch {
16881
18885
  throw new TerminalOpenError("invalid-cwd", serverText("cwdMissing", { cwd }));
16882
18886
  }
@@ -17307,26 +19311,26 @@ import { WebSocketServer } from "ws";
17307
19311
 
17308
19312
  // packages/server/src/directory-picker.ts
17309
19313
  import { randomUUID as randomUUID6 } from "node:crypto";
17310
- import { lstat, mkdir as mkdir14, readdir as readdir11, stat as stat24 } from "node:fs/promises";
17311
- import { homedir as homedir11 } from "node:os";
17312
- import path44 from "node:path";
19314
+ import { lstat, mkdir as mkdir14, readdir as readdir13, stat as stat30 } from "node:fs/promises";
19315
+ import { homedir as homedir13 } from "node:os";
19316
+ import path52 from "node:path";
17313
19317
  var MAX_ENTRIES = 500;
17314
19318
  var MAX_PICKERS = 8;
17315
19319
  var SKIPPED = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "bin", "obj", "$RECYCLE.BIN"]);
17316
19320
  var DirectoryPickerError = class extends ServerTextError {
17317
19321
  };
17318
19322
  function isInsideProtected(target, dirs) {
17319
- const resolved = path44.resolve(target);
19323
+ const resolved = path52.resolve(target);
17320
19324
  return dirs.some((dir) => {
17321
- const protectedDir = path44.resolve(dir);
17322
- return resolved === protectedDir || resolved.startsWith(protectedDir + path44.sep);
19325
+ const protectedDir = path52.resolve(dir);
19326
+ return resolved === protectedDir || resolved.startsWith(protectedDir + path52.sep);
17323
19327
  });
17324
19328
  }
17325
19329
  async function isListedDirectory(parent, entry, readLink = lstat) {
17326
19330
  if (entry.isDirectory()) return true;
17327
19331
  if (!entry.isSymbolicLink()) return false;
17328
19332
  try {
17329
- const info = await readLink(path44.join(parent, entry.name));
19333
+ const info = await readLink(path52.join(parent, entry.name));
17330
19334
  return info.isDirectory() && !info.isSymbolicLink();
17331
19335
  } catch {
17332
19336
  return false;
@@ -17334,14 +19338,14 @@ async function isListedDirectory(parent, entry, readLink = lstat) {
17334
19338
  }
17335
19339
  async function listRoots() {
17336
19340
  const roots = [];
17337
- const home = homedir11();
19341
+ const home = homedir13();
17338
19342
  if (process.platform === "win32") {
17339
19343
  const letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ".split("");
17340
19344
  const found = await Promise.all(
17341
19345
  letters.map(async (letter) => {
17342
19346
  const drive = `${letter}:\\`;
17343
19347
  try {
17344
- await stat24(drive);
19348
+ await stat30(drive);
17345
19349
  return drive;
17346
19350
  } catch {
17347
19351
  return null;
@@ -17368,7 +19372,7 @@ var DirectoryPickers = class {
17368
19372
  if (this.pickers.size >= MAX_PICKERS) {
17369
19373
  throw new DirectoryPickerError(serverText("pickerTooMany"));
17370
19374
  }
17371
- const picker = { id: randomUUID6(), current: homedir11(), entries: /* @__PURE__ */ new Set() };
19375
+ const picker = { id: randomUUID6(), current: homedir13(), entries: /* @__PURE__ */ new Set() };
17372
19376
  this.pickers.set(picker.id, picker);
17373
19377
  return this.listing(picker);
17374
19378
  }
@@ -17393,14 +19397,14 @@ var DirectoryPickers = class {
17393
19397
  async enter(pickerId, name) {
17394
19398
  const picker = this.require(pickerId);
17395
19399
  if (name === "..") {
17396
- const parent = path44.dirname(picker.current);
19400
+ const parent = path52.dirname(picker.current);
17397
19401
  if (parent !== picker.current) picker.current = parent;
17398
19402
  return this.listing(picker);
17399
19403
  }
17400
19404
  if (!picker.entries.has(name)) {
17401
19405
  throw new DirectoryPickerError(serverText("pickerNotListed"));
17402
19406
  }
17403
- const target = path44.join(picker.current, name);
19407
+ const target = path52.join(picker.current, name);
17404
19408
  if (isInsideProtected(target, this.protectedDirs)) {
17405
19409
  throw new DirectoryPickerError(serverText("pickerCliFolder"));
17406
19410
  }
@@ -17440,7 +19444,7 @@ var DirectoryPickers = class {
17440
19444
  throw new DirectoryPickerError(serverText("pickerNameChars"));
17441
19445
  }
17442
19446
  }
17443
- const target = path44.join(picker.current, clean);
19447
+ const target = path52.join(picker.current, clean);
17444
19448
  if (isInsideProtected(target, this.protectedDirs)) {
17445
19449
  throw new DirectoryPickerError(serverText("pickerNoProjectsHere"));
17446
19450
  }
@@ -17476,9 +19480,9 @@ var DirectoryPickers = class {
17476
19480
  let names = [];
17477
19481
  let truncated = false;
17478
19482
  try {
17479
- const found = await readdir11(picker.current, { withFileTypes: true });
19483
+ const found = await readdir13(picker.current, { withFileTypes: true });
17480
19484
  const listed = await Promise.all(found.map((entry) => isListedDirectory(picker.current, entry)));
17481
- const dirs = found.filter((_, index) => listed[index] === true).map((entry) => entry.name).filter((name) => !SKIPPED.has(name)).filter((name) => !isInsideProtected(path44.join(picker.current, name), this.protectedDirs)).sort((a, b) => a.localeCompare(b));
19485
+ const dirs = found.filter((_, index) => listed[index] === true).map((entry) => entry.name).filter((name) => !SKIPPED.has(name)).filter((name) => !isInsideProtected(path52.join(picker.current, name), this.protectedDirs)).sort((a, b) => a.localeCompare(b));
17482
19486
  truncated = dirs.length > MAX_ENTRIES;
17483
19487
  names = dirs.slice(0, MAX_ENTRIES);
17484
19488
  } catch {
@@ -17489,15 +19493,15 @@ var DirectoryPickers = class {
17489
19493
  path: picker.current,
17490
19494
  entries: names,
17491
19495
  roots,
17492
- canGoUp: path44.dirname(picker.current) !== picker.current,
19496
+ canGoUp: path52.dirname(picker.current) !== picker.current,
17493
19497
  truncated
17494
19498
  };
17495
19499
  }
17496
19500
  };
17497
19501
 
17498
19502
  // packages/server/src/file-browser.ts
17499
- import { readFile as readFile18, readdir as readdir12, stat as stat25 } from "node:fs/promises";
17500
- import path45 from "node:path";
19503
+ import { readFile as readFile20, readdir as readdir14, stat as stat31 } from "node:fs/promises";
19504
+ import path53 from "node:path";
17501
19505
  var MAX_ENTRIES2 = 1e3;
17502
19506
  var SEARCH_MAX_RESULTS = 200;
17503
19507
  var SEARCH_MAX_DIRECTORIES = 4e3;
@@ -17560,7 +19564,7 @@ function languageFor(fileName) {
17560
19564
  const lower = fileName.toLowerCase();
17561
19565
  const byName = LANGUAGE_BY_NAME[lower];
17562
19566
  if (byName !== void 0) return byName;
17563
- const extension = path45.extname(lower);
19567
+ const extension = path53.extname(lower);
17564
19568
  return LANGUAGE_BY_EXTENSION[extension] ?? null;
17565
19569
  }
17566
19570
  function looksBinary2(buffer) {
@@ -17592,7 +19596,7 @@ async function listDirectory(cwd, relativePath, options = {}) {
17592
19596
  let sizeBytes = 0;
17593
19597
  let modifiedAt = 0;
17594
19598
  try {
17595
- const info = await stat25(path45.join(absolute, candidate.name));
19599
+ const info = await stat31(path53.join(absolute, candidate.name));
17596
19600
  sizeBytes = candidate.isDirectory ? 0 : info.size;
17597
19601
  modifiedAt = info.mtimeMs;
17598
19602
  } catch {
@@ -17618,14 +19622,14 @@ async function scanLevel(absolute, repoRoot2) {
17618
19622
  return level;
17619
19623
  }
17620
19624
  async function readLevel(absolute) {
17621
- const dirents = await readdir12(absolute, { withFileTypes: true });
19625
+ const dirents = await readdir14(absolute, { withFileTypes: true });
17622
19626
  let skipped = 0;
17623
19627
  const candidates = [];
17624
19628
  for (const dirent of dirents) {
17625
19629
  const always = ALWAYS_SKIP.has(dirent.name.toLowerCase());
17626
19630
  if (dirent.isSymbolicLink()) {
17627
19631
  try {
17628
- const target = await stat25(path45.join(absolute, dirent.name));
19632
+ const target = await stat31(path53.join(absolute, dirent.name));
17629
19633
  candidates.push({
17630
19634
  name: dirent.name,
17631
19635
  isDirectory: target.isDirectory(),
@@ -17654,7 +19658,7 @@ async function markIgnored(repoRoot2, levels) {
17654
19658
  for (const level of levels) {
17655
19659
  for (const entry of level.entries) {
17656
19660
  if (entry.hidden !== null) continue;
17657
- keys.set(entry, toPosixPath(path45.relative(repoRoot2, path45.join(level.absolute, entry.name))));
19661
+ keys.set(entry, toPosixPath(path53.relative(repoRoot2, path53.join(level.absolute, entry.name))));
17658
19662
  }
17659
19663
  }
17660
19664
  if (keys.size === 0) return;
@@ -17715,7 +19719,7 @@ async function searchFiles(cwd, query, options = {}) {
17715
19719
  let sizeBytes = 0;
17716
19720
  let modifiedAt = 0;
17717
19721
  try {
17718
- const info = await stat25(path45.join(level.absolute, candidate.name));
19722
+ const info = await stat31(path53.join(level.absolute, candidate.name));
17719
19723
  sizeBytes = candidate.isDirectory ? 0 : info.size;
17720
19724
  modifiedAt = info.mtimeMs;
17721
19725
  } catch {
@@ -17747,14 +19751,25 @@ async function searchFiles(cwd, query, options = {}) {
17747
19751
  truncated: truncated || found.length > SEARCH_MAX_RESULTS
17748
19752
  };
17749
19753
  }
19754
+ async function existingFiles(cwd, relativePaths) {
19755
+ const existing = [];
19756
+ for (const relativePath of relativePaths) {
19757
+ try {
19758
+ const absolute = await resolveInside(cwd, relativePath, { mustExist: true });
19759
+ if ((await stat31(absolute)).isFile()) existing.push(relativePath);
19760
+ } catch {
19761
+ }
19762
+ }
19763
+ return existing;
19764
+ }
17750
19765
  async function readPreview(cwd, relativePath) {
17751
19766
  const absolute = await resolveInside(cwd, relativePath, { mustExist: true });
17752
- const info = await stat25(absolute);
19767
+ const info = await stat31(absolute);
17753
19768
  if (info.isDirectory()) {
17754
19769
  throw new Error("It's a directory, not a file.");
17755
19770
  }
17756
- const content = await readFile18(absolute);
17757
- const fileName = path45.basename(absolute);
19771
+ const content = await readFile20(absolute);
19772
+ const fileName = path53.basename(absolute);
17758
19773
  if (looksBinary2(content)) {
17759
19774
  return {
17760
19775
  path: relativePath,
@@ -17883,7 +19898,7 @@ async function runGlobalSearch(deps, rawQuery, options) {
17883
19898
  }
17884
19899
  return false;
17885
19900
  };
17886
- const add = (session, event, text, found) => {
19901
+ const add = (session, event2, text, found) => {
17887
19902
  if (hits.length >= GLOBAL_SEARCH_MAX_RESULTS) {
17888
19903
  truncated = "results";
17889
19904
  return false;
@@ -17893,9 +19908,9 @@ async function runGlobalSearch(deps, rawQuery, options) {
17893
19908
  sessionId: session.sessionId,
17894
19909
  cwd: session.cwd.length > 0 ? session.cwd : null,
17895
19910
  title: session.title,
17896
- eventId: event?.eventId ?? null,
17897
- role: event?.role ?? null,
17898
- at: event !== null && event.at > 0 ? event.at : null,
19911
+ eventId: event2?.eventId ?? null,
19912
+ role: event2?.role ?? null,
19913
+ at: event2 !== null && event2.at > 0 ? event2.at : null,
17899
19914
  ...makeSnippet(text, found.start, found.end)
17900
19915
  });
17901
19916
  return true;
@@ -17918,13 +19933,13 @@ async function runGlobalSearch(deps, rawQuery, options) {
17918
19933
  }
17919
19934
  if (line.length > GLOBAL_SEARCH_LINE_MAX_CHARS) continue;
17920
19935
  if (!lineMayMatch(line, needle, usePrefilter)) continue;
17921
- const event = decode(line);
17922
- if (event === null) continue;
17923
- for (const part of event.parts) {
19936
+ const event2 = decode(line);
19937
+ if (event2 === null) continue;
19938
+ for (const part of event2.parts) {
17924
19939
  if (part.kind !== "text") continue;
17925
19940
  const found = findFolded(part.text, needle);
17926
19941
  if (found === null) continue;
17927
- if (!add(session, event, part.text, found)) break sessionLoop;
19942
+ if (!add(session, event2, part.text, found)) break sessionLoop;
17928
19943
  break;
17929
19944
  }
17930
19945
  }
@@ -18242,6 +20257,77 @@ async function readEvents(deps, agent, summary) {
18242
20257
  return read === null ? null : { ...read, partial: false };
18243
20258
  }
18244
20259
 
20260
+ // packages/server/src/queued-submits.ts
20261
+ var SUBMISSIONS_KEPT2 = 20;
20262
+ var SUBMISSION_MAX_AGE_MS2 = 30 * 6e4;
20263
+ var RECLAIM_TIMEOUT_MS = 4e3;
20264
+ var RECLAIM_POLL_MS = 250;
20265
+ var QueuedSubmitBook = class {
20266
+ submissions = /* @__PURE__ */ new Map();
20267
+ clearKeyAt = /* @__PURE__ */ new Map();
20268
+ /** Un mensaje termino de escribirse en la pty, Enter incluido. */
20269
+ note(terminalId, text, at) {
20270
+ if (text.trim().length === 0) return;
20271
+ const kept = (this.submissions.get(terminalId) ?? []).filter((each) => at - each.at < SUBMISSION_MAX_AGE_MS2);
20272
+ this.submissions.set(terminalId, [...kept, { text, at }].slice(-SUBMISSIONS_KEPT2));
20273
+ }
20274
+ /** Se lleva lo anotado de esa pestana. */
20275
+ take(terminalId) {
20276
+ const taken = this.submissions.get(terminalId) ?? [];
20277
+ this.submissions.delete(terminalId);
20278
+ return taken;
20279
+ }
20280
+ /** La pestana quedo libre, sin proceso, o en otra sesion: nada de lo anotado sigue en cola. */
20281
+ forget(terminalId) {
20282
+ this.submissions.delete(terminalId);
20283
+ }
20284
+ /** La pestana se cerro. */
20285
+ drop(terminalId) {
20286
+ this.submissions.delete(terminalId);
20287
+ this.clearKeyAt.delete(terminalId);
20288
+ }
20289
+ /** Paso una tecla de vaciar por la pty de esa pestana: de la app, o tecleada por el usuario. */
20290
+ noteClearKey(terminalId, at) {
20291
+ this.clearKeyAt.set(terminalId, at);
20292
+ }
20293
+ lastClearKeyAt(terminalId) {
20294
+ return this.clearKeyAt.get(terminalId) ?? null;
20295
+ }
20296
+ /** Pestanas con algo anotado. Para el chequeo. */
20297
+ size() {
20298
+ return this.submissions.size;
20299
+ }
20300
+ };
20301
+ async function reclaimQueuedSubmits(deps) {
20302
+ const submissions = deps.submissions.filter((each) => each.at <= deps.interruptedAt);
20303
+ if (submissions.length === 0) return "nothing";
20304
+ const timeoutMs = deps.timeoutMs ?? RECLAIM_TIMEOUT_MS;
20305
+ const pollMs = deps.pollMs ?? RECLAIM_POLL_MS;
20306
+ const deadline = deps.now() + timeoutMs;
20307
+ let undelivered = [];
20308
+ for (; ; ) {
20309
+ const target = deps.target();
20310
+ if (target === null) return "gone";
20311
+ const result = await deps.source.check(target, submissions, deps.interruptedAt);
20312
+ if (result.undelivered.length === 0) return "nothing";
20313
+ if (result.settled) {
20314
+ undelivered = result.undelivered;
20315
+ break;
20316
+ }
20317
+ if (deps.now() >= deadline) return "not-settled";
20318
+ await deps.wait(pollMs);
20319
+ }
20320
+ const last = deps.lastClearKeyAt();
20321
+ if (last !== null) {
20322
+ const remaining = deps.source.clearGapMs - (deps.now() - last);
20323
+ if (remaining > 0) await deps.wait(remaining);
20324
+ }
20325
+ if (deps.target() === null) return "gone";
20326
+ if (!deps.writeClearKey()) return "refused";
20327
+ deps.prefill(undelivered.join("\n\n"));
20328
+ return "reclaimed";
20329
+ }
20330
+
18245
20331
  // packages/server/src/reveal.ts
18246
20332
  import { spawn as spawn3 } from "node:child_process";
18247
20333
  function revealPath(absolutePath) {
@@ -18343,14 +20429,14 @@ var TerminalWriteQueue = class {
18343
20429
  };
18344
20430
 
18345
20431
  // packages/server/src/vault/service.ts
18346
- import { mkdir as mkdir17, stat as stat29 } from "node:fs/promises";
18347
- import { homedir as homedir12 } from "node:os";
18348
- import path49 from "node:path";
20432
+ import { mkdir as mkdir17, stat as stat35 } from "node:fs/promises";
20433
+ import { homedir as homedir14 } from "node:os";
20434
+ import path57 from "node:path";
18349
20435
 
18350
20436
  // packages/server/src/vault/move.ts
18351
20437
  import { constants } from "node:fs";
18352
- import { copyFile as copyFile2, lstat as lstat2, mkdir as mkdir15, readdir as readdir13, rm as rm5, stat as stat26, utimes, writeFile as writeFile9 } from "node:fs/promises";
18353
- import path46 from "node:path";
20438
+ import { copyFile as copyFile2, lstat as lstat2, mkdir as mkdir15, readdir as readdir15, rm as rm5, stat as stat32, utimes, writeFile as writeFile9 } from "node:fs/promises";
20439
+ import path54 from "node:path";
18354
20440
  var MOVED_ENTRIES = ["vault.json", "sessions", "memory"];
18355
20441
  var OWN_TEMP_FILE = /\.\d+\.[0-9a-f]{12}\.tmp$/;
18356
20442
  async function copyTree(source, target, result) {
@@ -18362,13 +20448,13 @@ async function copyTree(source, target, result) {
18362
20448
  }
18363
20449
  if (info.isDirectory()) {
18364
20450
  await mkdir15(target, { recursive: true });
18365
- for (const name of await readdir13(source)) {
18366
- await copyTree(path46.join(source, name), path46.join(target, name), result);
20451
+ for (const name of await readdir15(source)) {
20452
+ await copyTree(path54.join(source, name), path54.join(target, name), result);
18367
20453
  }
18368
20454
  return;
18369
20455
  }
18370
20456
  if (!info.isFile() || OWN_TEMP_FILE.test(source)) return;
18371
- await mkdir15(path46.dirname(target), { recursive: true });
20457
+ await mkdir15(path54.dirname(target), { recursive: true });
18372
20458
  try {
18373
20459
  await copyFile2(source, target, constants.COPYFILE_EXCL);
18374
20460
  } catch (error) {
@@ -18385,7 +20471,7 @@ async function moveVault(from, to) {
18385
20471
  const result = { copied: 0, skipped: 0 };
18386
20472
  await mkdir15(to, { recursive: true });
18387
20473
  for (const entry of MOVED_ENTRIES) {
18388
- await copyTree(path46.join(from, entry), path46.join(to, entry), result);
20474
+ await copyTree(path54.join(from, entry), path54.join(to, entry), result);
18389
20475
  }
18390
20476
  return result;
18391
20477
  }
@@ -18417,10 +20503,10 @@ function checkVaultTarget(current, target, options) {
18417
20503
  return { kind: "ok" };
18418
20504
  }
18419
20505
  async function canWriteInto(dir) {
18420
- const probe = temporaryPathFor(path46.join(dir, ".escritura"));
20506
+ const probe = temporaryPathFor(path54.join(dir, ".escritura"));
18421
20507
  try {
18422
20508
  await mkdir15(dir, { recursive: true });
18423
- if (!(await stat26(dir)).isDirectory()) return false;
20509
+ if (!(await stat32(dir)).isDirectory()) return false;
18424
20510
  await writeFile9(probe, "");
18425
20511
  return true;
18426
20512
  } catch {
@@ -18468,11 +20554,11 @@ function buildHeader(input) {
18468
20554
  }
18469
20555
  return header;
18470
20556
  }
18471
- function imageRefsOf(event, images, assets, named) {
20557
+ function imageRefsOf(event2, images, assets, named) {
18472
20558
  const refs = [];
18473
- for (const part of event.parts) {
20559
+ for (const part of event2.parts) {
18474
20560
  if (part.kind !== "image") continue;
18475
- const input = images.get(imageKey(event.eventId, part.source, part.index));
20561
+ const input = images.get(imageKey(event2.eventId, part.source, part.index));
18476
20562
  let asset = null;
18477
20563
  let bytes = 0;
18478
20564
  if (input?.kind === "loaded") {
@@ -18494,8 +20580,8 @@ function serializeSession(input) {
18494
20580
  const assets = /* @__PURE__ */ new Map();
18495
20581
  const named = /* @__PURE__ */ new Set();
18496
20582
  const bodyLines = [];
18497
- for (const event of input.events) {
18498
- const line = { kind: "event", event, images: imageRefsOf(event, images, assets, named) };
20583
+ for (const event2 of input.events) {
20584
+ const line = { kind: "event", event: event2, images: imageRefsOf(event2, images, assets, named) };
18499
20585
  bodyLines.push(JSON.stringify(line));
18500
20586
  }
18501
20587
  for (const document of documents) {
@@ -18523,12 +20609,12 @@ function serializeSession(input) {
18523
20609
  }
18524
20610
 
18525
20611
  // packages/server/src/vault/writer.ts
18526
- import { readdir as readdir15, rm as rm6, stat as stat28 } from "node:fs/promises";
18527
- import path48 from "node:path";
20612
+ import { readdir as readdir17, rm as rm6, stat as stat34 } from "node:fs/promises";
20613
+ import path56 from "node:path";
18528
20614
 
18529
20615
  // packages/server/src/vault/memory-copy.ts
18530
- import { mkdir as mkdir16, readdir as readdir14, readFile as readFile19, stat as stat27, utimes as utimes2 } from "node:fs/promises";
18531
- import path47 from "node:path";
20616
+ import { mkdir as mkdir16, readdir as readdir16, readFile as readFile21, stat as stat33, utimes as utimes2 } from "node:fs/promises";
20617
+ import path55 from "node:path";
18532
20618
  var MEMORY_FILE_MAX_BYTES = 1024 * 1024;
18533
20619
  var MEMORY_MAX_FILES = 500;
18534
20620
  var MEMORY_SOURCE_FILE = "source.json";
@@ -18543,7 +20629,7 @@ async function listMemoryNotes(cwd) {
18543
20629
  }
18544
20630
  let entries;
18545
20631
  try {
18546
- entries = await readdir14(folder, { withFileTypes: true });
20632
+ entries = await readdir16(folder, { withFileTypes: true });
18547
20633
  } catch {
18548
20634
  return [];
18549
20635
  }
@@ -18553,7 +20639,7 @@ async function listMemoryNotes(cwd) {
18553
20639
  let absolute;
18554
20640
  try {
18555
20641
  absolute = await resolveInside(cwd, `${MEMORY_DIR}/${name}`, { mustExist: true });
18556
- const info = await stat27(absolute);
20642
+ const info = await stat33(absolute);
18557
20643
  if (!info.isFile() || info.size > MEMORY_FILE_MAX_BYTES) continue;
18558
20644
  notes.push({ name, absolute, size: info.size, mtimeMs: info.mtimeMs });
18559
20645
  } catch {
@@ -18567,7 +20653,7 @@ async function measureProjectMemory(cwd) {
18567
20653
  }
18568
20654
  async function isUpToDate(target, note) {
18569
20655
  try {
18570
- const info = await stat27(target);
20656
+ const info = await stat33(target);
18571
20657
  return info.isFile() && info.size === note.size && Math.abs(info.mtimeMs - note.mtimeMs) < SAME_MTIME_MS;
18572
20658
  } catch {
18573
20659
  return false;
@@ -18575,7 +20661,7 @@ async function isUpToDate(target, note) {
18575
20661
  }
18576
20662
  async function exists3(file) {
18577
20663
  try {
18578
- await stat27(file);
20664
+ await stat33(file);
18579
20665
  return true;
18580
20666
  } catch {
18581
20667
  return false;
@@ -18594,11 +20680,11 @@ async function copyProjectMemory(dir, cwd, platform, deps = DEFAULT_WRITE_DEPS)
18594
20680
  await mkdir16(target, { recursive: true });
18595
20681
  };
18596
20682
  for (const note of notes) {
18597
- const destination = path47.join(target, note.name);
20683
+ const destination = path55.join(target, note.name);
18598
20684
  if (await isUpToDate(destination, note)) continue;
18599
20685
  let content;
18600
20686
  try {
18601
- content = await readFile19(note.absolute);
20687
+ content = await readFile21(note.absolute);
18602
20688
  } catch {
18603
20689
  continue;
18604
20690
  }
@@ -18610,7 +20696,7 @@ async function copyProjectMemory(dir, cwd, platform, deps = DEFAULT_WRITE_DEPS)
18610
20696
  result.files += 1;
18611
20697
  result.bytes += content.length;
18612
20698
  }
18613
- const source = path47.join(target, MEMORY_SOURCE_FILE);
20699
+ const source = path55.join(target, MEMORY_SOURCE_FILE);
18614
20700
  if (result.files > 0 || !await exists3(source)) {
18615
20701
  await prepare();
18616
20702
  await writeFileAtomic(source, `${JSON.stringify({ cwd, copiedAt: deps.now() })}
@@ -19044,10 +21130,10 @@ var VaultWriter = class {
19044
21130
  if (whole.state !== "live" || whole.events.length === 0) return { kind: "empty" };
19045
21131
  const previous = reuse === null ? /* @__PURE__ */ new Map() : await this.previousImages(reuse, agent, summary.sessionId, whole.events);
19046
21132
  const images = /* @__PURE__ */ new Map();
19047
- for (const event of whole.events) {
19048
- for (const part of event.parts) {
21133
+ for (const event2 of whole.events) {
21134
+ for (const part of event2.parts) {
19049
21135
  if (part.kind !== "image") continue;
19050
- const keyOfImage = imageKey(event.eventId, part.source, part.index);
21136
+ const keyOfImage = imageKey(event2.eventId, part.source, part.index);
19051
21137
  if (images.has(keyOfImage)) continue;
19052
21138
  const old = previous.get(keyOfImage);
19053
21139
  if (reuse !== null && old !== void 0 && old.asset !== null && old.mediaType === part.mediaType && await this.assetOnDisk(reuse.dir, agent, summary.sessionId, old.asset, old.bytes)) {
@@ -19055,7 +21141,7 @@ var VaultWriter = class {
19055
21141
  continue;
19056
21142
  }
19057
21143
  try {
19058
- const loaded = await whole.readImage(event.eventId, part.index, part.source);
21144
+ const loaded = await whole.readImage(event2.eventId, part.index, part.source);
19059
21145
  if (loaded !== null) images.set(keyOfImage, { kind: "loaded", data: Buffer.from(loaded.data, "base64") });
19060
21146
  } catch (error) {
19061
21147
  this.log.warn(`[vault] ${whole.label}: couldn't read an image: ${messageOf2(error)}`);
@@ -19093,7 +21179,7 @@ var VaultWriter = class {
19093
21179
  async previousImages(reuse, agent, sessionId, events) {
19094
21180
  const refs = /* @__PURE__ */ new Map();
19095
21181
  const { header } = reuse;
19096
- if (header === null || header.source.kind !== "native" || header.source.writerRevision !== this.revision || header.imageCount === 0 || !events.some((event) => event.parts.some((part) => part.kind === "image"))) {
21182
+ if (header === null || header.source.kind !== "native" || header.source.writerRevision !== this.revision || header.imageCount === 0 || !events.some((event2) => event2.parts.some((part) => part.kind === "image"))) {
19097
21183
  return refs;
19098
21184
  }
19099
21185
  for await (const line of this.options.catalog.readBody(agent, sessionId)) {
@@ -19104,7 +21190,7 @@ var VaultWriter = class {
19104
21190
  }
19105
21191
  async assetOnDisk(dir, agent, sessionId, asset, bytes) {
19106
21192
  try {
19107
- const info = await stat28(assetFile(dir, agent, sessionId, asset));
21193
+ const info = await stat34(assetFile(dir, agent, sessionId, asset));
19108
21194
  return info.isFile() && info.size === bytes;
19109
21195
  } catch {
19110
21196
  return false;
@@ -19204,29 +21290,29 @@ var VaultWriter = class {
19204
21290
  const folders = [dir];
19205
21291
  const subfolders = async (parent) => {
19206
21292
  try {
19207
- return (await readdir15(parent, { withFileTypes: true })).filter((entry) => entry.isDirectory()).map((entry) => path48.join(parent, entry.name));
21293
+ return (await readdir17(parent, { withFileTypes: true })).filter((entry) => entry.isDirectory()).map((entry) => path56.join(parent, entry.name));
19208
21294
  } catch {
19209
21295
  return [];
19210
21296
  }
19211
21297
  };
19212
21298
  for (const agentDir of await subfolders(sessionsRoot2(dir))) {
19213
- if (!SESSION_AGENT_IDS.includes(path48.basename(agentDir))) continue;
21299
+ if (!SESSION_AGENT_IDS.includes(path56.basename(agentDir))) continue;
19214
21300
  folders.push(agentDir);
19215
21301
  folders.push(...(await subfolders(agentDir)).filter((folder) => folder.endsWith(".assets")));
19216
21302
  }
19217
- folders.push(...await subfolders(path48.join(dir, "memory")));
21303
+ folders.push(...await subfolders(path56.join(dir, "memory")));
19218
21304
  for (const folder of folders) {
19219
21305
  let names;
19220
21306
  try {
19221
- names = await readdir15(folder);
21307
+ names = await readdir17(folder);
19222
21308
  } catch {
19223
21309
  continue;
19224
21310
  }
19225
21311
  for (const name of names) {
19226
21312
  if (!OWN_TEMP_FILE2.test(name)) continue;
19227
- const file = path48.join(folder, name);
21313
+ const file = path56.join(folder, name);
19228
21314
  try {
19229
- const info = await stat28(file);
21315
+ const info = await stat34(file);
19230
21316
  if (info.isFile() && now - info.mtimeMs > TEMP_MAX_AGE_MS) await rm6(file, { force: true });
19231
21317
  } catch {
19232
21318
  }
@@ -19414,14 +21500,14 @@ var VaultService = class {
19414
21500
  async setDir(target) {
19415
21501
  const { platform, agents, settings: settings2, catalog } = this.options;
19416
21502
  const current = this.dir();
19417
- const protectedDirs = [...agents.protectedDirs(), path49.join(this.options.homeDir ?? homedir12(), ".gemini")];
21503
+ const protectedDirs = [...agents.protectedDirs(), path57.join(this.options.homeDir ?? homedir14(), ".gemini")];
19418
21504
  const check = checkVaultTarget(current, target, { protectedDirs, platform });
19419
21505
  if (check.kind === "refused") throw new VaultError(check.text);
19420
21506
  if (check.kind === "same") return;
19421
21507
  try {
19422
21508
  await this.writer.exclusive("moving", async () => {
19423
21509
  if (!await canWriteInto(target)) throw new VaultError(serverText("vaultCannotWrite"));
19424
- const hadCopy = await stat29(vaultMarkerFile(current)).then(
21510
+ const hadCopy = await stat35(vaultMarkerFile(current)).then(
19425
21511
  () => true,
19426
21512
  () => false
19427
21513
  );
@@ -19455,7 +21541,7 @@ var VaultService = class {
19455
21541
  /** Abre la carpeta de la copia con el explorador del sistema. */
19456
21542
  async reveal() {
19457
21543
  const dir = this.activeDir();
19458
- const isFolder = await stat29(dir).then(
21544
+ const isFolder = await stat35(dir).then(
19459
21545
  (info) => info.isDirectory(),
19460
21546
  () => false
19461
21547
  );
@@ -19479,7 +21565,7 @@ var VaultService = class {
19479
21565
  fromCopy: true
19480
21566
  });
19481
21567
  const file = sessionExportFile(dir, agent, sessionId);
19482
- await mkdir17(path49.dirname(file), { recursive: true });
21568
+ await mkdir17(path57.dirname(file), { recursive: true });
19483
21569
  await writeFileAtomic(file, text, this.writeDeps);
19484
21570
  this.openWithSystem(file);
19485
21571
  return file;
@@ -19583,7 +21669,7 @@ var VaultService = class {
19583
21669
  continue;
19584
21670
  }
19585
21671
  const fileName = this.exportFileName(summary, names);
19586
- await writeFileAtomic(path49.join(folder, fileName), text, this.writeDeps);
21672
+ await writeFileAtomic(path57.join(folder, fileName), text, this.writeDeps);
19587
21673
  entries.push({
19588
21674
  updatedAt: summary.updatedAt,
19589
21675
  agentLabel: this.labelOf(summary.agent),
@@ -19594,9 +21680,9 @@ var VaultService = class {
19594
21680
  });
19595
21681
  await nextTurn3();
19596
21682
  }
19597
- const name = project.cwd.length > 0 ? path49.basename(project.cwd) || project.cwd : project.fallbackName;
21683
+ const name = project.cwd.length > 0 ? path57.basename(project.cwd) || project.cwd : project.fallbackName;
19598
21684
  const index = renderProjectIndex({ name, cwd: project.cwd, exportedAt: this.now(), entries, skipped });
19599
- await writeFileAtomic(path49.join(folder, "index.md"), index, this.writeDeps);
21685
+ await writeFileAtomic(path57.join(folder, "index.md"), index, this.writeDeps);
19600
21686
  this.openWithSystem(folder);
19601
21687
  return { sessions: entries.length, skipped, folder };
19602
21688
  }
@@ -19668,10 +21754,10 @@ var VaultService = class {
19668
21754
  documentCount: 0,
19669
21755
  imageCount: 0
19670
21756
  });
19671
- const lines = whole.events.map((event) => ({
21757
+ const lines = whole.events.map((event2) => ({
19672
21758
  kind: "event",
19673
- event,
19674
- images: event.parts.flatMap(
21759
+ event: event2,
21760
+ images: event2.parts.flatMap(
19675
21761
  (part) => part.kind === "image" ? [{ index: part.index, source: part.source, mediaType: part.mediaType, asset: null, bytes: 0 }] : []
19676
21762
  )
19677
21763
  }));
@@ -19763,6 +21849,7 @@ function attachTerminalSocket(options) {
19763
21849
  } = options;
19764
21850
  const pasteStore = options.pasteStore ?? new PasteStore();
19765
21851
  const writeQueue = new TerminalWriteQueue();
21852
+ const queuedSubmits = new QueuedSubmitBook();
19766
21853
  const wss = new WebSocketServer({ noServer: true });
19767
21854
  const clients = /* @__PURE__ */ new Set();
19768
21855
  const send = (socket, message) => {
@@ -19843,9 +21930,12 @@ function attachTerminalSocket(options) {
19843
21930
  if (entries.length > 0) broadcast({ type: "composer.drafts", drafts: entries });
19844
21931
  };
19845
21932
  const onRegistryExit = (terminalId, exitCode, signal) => broadcast({ type: "terminal.exit", terminalId, exitCode, signal });
19846
- const onRegistryActivity = (terminalId, activity, offlineReason) => broadcast(
19847
- offlineReason === null ? { type: "terminal.activity", terminalId, activity } : { type: "terminal.activity", terminalId, activity, offlineReason }
19848
- );
21933
+ const onRegistryActivity = (terminalId, activity, offlineReason) => {
21934
+ if (activity === "idle" || activity === "offline") queuedSubmits.forget(terminalId);
21935
+ broadcast(
21936
+ offlineReason === null ? { type: "terminal.activity", terminalId, activity } : { type: "terminal.activity", terminalId, activity, offlineReason }
21937
+ );
21938
+ };
19849
21939
  const onIndexStatus = (status) => broadcast({ type: "index.status", status });
19850
21940
  const onIndexProjects = (projects, replace) => broadcast({ type: "index.projects", projects, replace });
19851
21941
  const onConversationAppend = (terminalId, events, usage, permissionMode) => {
@@ -19857,7 +21947,7 @@ function attachTerminalSocket(options) {
19857
21947
  const onConversationState = (terminalId, state) => broadcast({ type: "conversation.state", terminalId, state });
19858
21948
  const onConversationMode = (terminalId, mode) => broadcast({ type: "conversation.mode", terminalId, mode });
19859
21949
  const onConversationWaiting = (terminalId, waitingFor) => broadcast({ type: "conversation.waiting", terminalId, waitingFor });
19860
- const onConversationToolCall = (terminalId, open8) => broadcast({ type: "conversation.toolCall", terminalId, open: open8 });
21950
+ const onConversationToolCall = (terminalId, open9) => broadcast({ type: "conversation.toolCall", terminalId, open: open9 });
19861
21951
  const onConversationSubagents = (terminalId, subagents) => broadcast({ type: "conversation.subagents", terminalId, subagents });
19862
21952
  const onConversationPlans = (terminalId, plans) => broadcast({ type: "conversation.plans", terminalId, plans });
19863
21953
  const onConversationTurns = (terminalId, turns) => broadcast({ type: "conversation.turns", terminalId, turns });
@@ -19876,6 +21966,7 @@ function attachTerminalSocket(options) {
19876
21966
  conversations.drop(terminalId);
19877
21967
  repos.drop(terminalId);
19878
21968
  memory.drop(terminalId);
21969
+ queuedSubmits.drop(terminalId);
19879
21970
  void pasteStore.clearTerminal(terminalId);
19880
21971
  broadcast({ type: "terminal.closed", terminalId });
19881
21972
  return true;
@@ -20064,7 +22155,8 @@ function attachTerminalSocket(options) {
20064
22155
  platform: process.platform,
20065
22156
  defaultCwd,
20066
22157
  shellName: appSettings.currentShell()?.name ?? null,
20067
- remoteClient: deviceId !== null
22158
+ remoteClient: deviceId !== null,
22159
+ fileCheck: true
20068
22160
  });
20069
22161
  send(socket, terminalListMessage());
20070
22162
  for (const entry of registry.activitySnapshot()) {
@@ -20096,6 +22188,7 @@ function attachTerminalSocket(options) {
20096
22188
  if (!registry.write(message.terminalId, message.data) && registry.get(message.terminalId) === null) {
20097
22189
  sendError(socket, "unknown-terminal", serverText("terminalGone"));
20098
22190
  }
22191
+ if (message.data.includes(CLEAR_EDITOR)) queuedSubmits.noteClearKey(message.terminalId, Date.now());
20099
22192
  break;
20100
22193
  /*
20101
22194
  Un mensaje del cuadro de escritura.
@@ -20117,10 +22210,11 @@ function attachTerminalSocket(options) {
20117
22210
  settled = true;
20118
22211
  send(socket, { type: "agent.submitted", terminalId, requestId: message.requestId, delivered });
20119
22212
  };
22213
+ const clearsDraft = submitClearsDraft(message);
20120
22214
  const submittedTab = registry.get(terminalId);
20121
22215
  const submittedOwner = draftOwnerOf(submittedTab);
20122
- if (submittedOwner !== null) drafts.delete(submittedOwner);
20123
- if (submittedTab !== null) {
22216
+ if (clearsDraft && submittedOwner !== null) drafts.delete(submittedOwner);
22217
+ if (clearsDraft && submittedTab !== null) {
20124
22218
  broadcast({
20125
22219
  type: "composer.drafts",
20126
22220
  drafts: submittedOwner === null ? [{ terminalId, draft: { text: "", pasted: [] } }] : draftEntriesOf(registry.list(), drafts, submittedOwner)
@@ -20236,6 +22330,13 @@ function attachTerminalSocket(options) {
20236
22330
  { startAfterMs }
20237
22331
  );
20238
22332
  settle(outcome === "written");
22333
+ if (outcome === "written" && message.send !== false && submitting?.queuedSubmits !== void 0) {
22334
+ queuedSubmits.note(
22335
+ terminalId,
22336
+ submissionPayload(fullText, imagePaths, input.imageReference) ?? fullText,
22337
+ Date.now()
22338
+ );
22339
+ }
20239
22340
  if (outcome === "interrupted") interrupted();
20240
22341
  if (outcome === "blocked") {
20241
22342
  const waitingNow = blocksWhileWaiting && registry.activityOf(terminalId) === "waiting";
@@ -20385,10 +22486,10 @@ function attachTerminalSocket(options) {
20385
22486
  nunca esconde nada.
20386
22487
  */
20387
22488
  case "session.archive": {
20388
- const open8 = new Set(
22489
+ const open9 = new Set(
20389
22490
  registry.list().map((terminal) => terminal.sessionId).filter((id) => id.length > 0)
20390
22491
  );
20391
- const targets = message.archived ? message.sessionIds.filter((id) => !open8.has(id)) : message.sessionIds;
22492
+ const targets = message.archived ? message.sessionIds.filter((id) => !open9.has(id)) : message.sessionIds;
20392
22493
  if (archived.set(targets, message.archived)) index.refresh();
20393
22494
  if (message.archived && targets.length < message.sessionIds.length) {
20394
22495
  sendError(
@@ -20408,6 +22509,44 @@ function attachTerminalSocket(options) {
20408
22509
  writeQueue.interrupt(message.terminalId);
20409
22510
  const [first, ...rest] = buildInterruptKeys(inputOf(message.terminalId).interruptPresses);
20410
22511
  if (first === void 0 || !writeToTerminal(socket, message.terminalId, first)) break;
22512
+ const reclaimFrom = adapterOf(message.terminalId)?.queuedSubmits;
22513
+ if (reclaimFrom !== void 0) {
22514
+ const reclaimId = message.terminalId;
22515
+ const taken = queuedSubmits.take(reclaimId);
22516
+ const interruptedAt = Date.now();
22517
+ if (taken.length > 0) {
22518
+ void writeQueue.enqueue(
22519
+ reclaimId,
22520
+ async () => {
22521
+ const outcome = await reclaimQueuedSubmits({
22522
+ source: reclaimFrom,
22523
+ target: () => {
22524
+ const tab = registry.get(reclaimId);
22525
+ return tab === null || !tab.alive || tab.sessionId.length === 0 ? null : { cwd: tab.cwd, sessionId: tab.sessionId };
22526
+ },
22527
+ submissions: taken,
22528
+ interruptedAt,
22529
+ lastClearKeyAt: () => queuedSubmits.lastClearKeyAt(reclaimId),
22530
+ writeClearKey: () => {
22531
+ if (!registry.write(reclaimId, CLEAR_EDITOR)) return false;
22532
+ queuedSubmits.noteClearKey(reclaimId, Date.now());
22533
+ return true;
22534
+ },
22535
+ prefill: (returned) => send(socket, {
22536
+ type: "composer.prefill",
22537
+ terminalId: reclaimId,
22538
+ text: returned,
22539
+ reason: "returned-by-cli"
22540
+ }),
22541
+ wait: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
22542
+ now: Date.now
22543
+ });
22544
+ debugLog("write", `queued messages of ${reclaimId.slice(0, 8)} after an interrupt: ${outcome}`);
22545
+ },
22546
+ { interruptible: false }
22547
+ );
22548
+ }
22549
+ }
20411
22550
  if (rest.length > 0) {
20412
22551
  const terminalId = message.terminalId;
20413
22552
  void writeQueue.enqueue(
@@ -20826,6 +22965,20 @@ function attachTerminalSocket(options) {
20826
22965
  }
20827
22966
  })();
20828
22967
  break;
22968
+ /*
22969
+ Cuales de esas rutas son archivos del proyecto (§6.33): lo que el
22970
+ hilo vuelve enlace. Sin error para ningun caso —una pestana que ya no
22971
+ esta, una ruta que cae fuera—: nada existe, y el hilo queda como
22972
+ texto. Un cartel por una ruta que escribio el agente no le dice nada
22973
+ al usuario.
22974
+ */
22975
+ case "files.check":
22976
+ void (async () => {
22977
+ const cwd = cwdOf(message.terminalId);
22978
+ const existing = cwd === null ? [] : await existingFiles(cwd, message.paths).catch(() => []);
22979
+ send(socket, { type: "files.checked", terminalId: message.terminalId, requestId: message.requestId, existing });
22980
+ })();
22981
+ break;
20829
22982
  case "files.reveal":
20830
22983
  void (async () => {
20831
22984
  const cwd = cwdOf(message.terminalId);
@@ -21267,9 +23420,9 @@ function attachTerminalSocket(options) {
21267
23420
  }
21268
23421
 
21269
23422
  // packages/server/src/vault/catalog.ts
21270
- import { open as open7, readdir as readdir16, stat as stat30 } from "node:fs/promises";
21271
- import path50 from "node:path";
21272
- import { createInterface as createInterface2 } from "node:readline";
23423
+ import { open as open8, readdir as readdir18, stat as stat36 } from "node:fs/promises";
23424
+ import path58 from "node:path";
23425
+ import { createInterface as createInterface3 } from "node:readline";
21273
23426
  var HEADER_MAX_BYTES = 64 * 1024;
21274
23427
  var LOAD_CONCURRENCY = 16;
21275
23428
  var keyOf2 = (agent, sessionId) => `${agent}/${sessionId}`;
@@ -21280,7 +23433,7 @@ async function assetBytesOf(dir, agent, sessionId) {
21280
23433
  const folder = assetsDir(dir, agent, sessionId);
21281
23434
  let names;
21282
23435
  try {
21283
- names = await readdir16(folder);
23436
+ names = await readdir18(folder);
21284
23437
  } catch {
21285
23438
  return 0;
21286
23439
  }
@@ -21288,18 +23441,18 @@ async function assetBytesOf(dir, agent, sessionId) {
21288
23441
  for (const name of names) {
21289
23442
  if (!VAULT_ASSET_NAME_PATTERN.test(name)) continue;
21290
23443
  try {
21291
- const info = await stat30(path50.join(folder, name));
23444
+ const info = await stat36(path58.join(folder, name));
21292
23445
  if (info.isFile()) total += info.size;
21293
23446
  } catch {
21294
23447
  }
21295
23448
  }
21296
23449
  return total;
21297
23450
  }
21298
- async function readEntry(dir, agent, sessionId) {
23451
+ async function readEntry2(dir, agent, sessionId) {
21299
23452
  const file = sessionFile(dir, agent, sessionId);
21300
23453
  let sizeBytes;
21301
23454
  try {
21302
- const info = await stat30(file);
23455
+ const info = await stat36(file);
21303
23456
  if (!info.isFile()) return { kind: "absent" };
21304
23457
  sizeBytes = info.size;
21305
23458
  } catch {
@@ -21356,7 +23509,7 @@ var VaultCatalog = class {
21356
23509
  const keys = [];
21357
23510
  let agentFolders = [];
21358
23511
  try {
21359
- agentFolders = await readdir16(sessionsRoot2(dir));
23512
+ agentFolders = await readdir18(sessionsRoot2(dir));
21360
23513
  } catch {
21361
23514
  agentFolders = [];
21362
23515
  }
@@ -21364,7 +23517,7 @@ var VaultCatalog = class {
21364
23517
  if (!isSessionAgentId2(folder)) continue;
21365
23518
  let names;
21366
23519
  try {
21367
- names = await readdir16(agentSessionsDir(dir, folder));
23520
+ names = await readdir18(agentSessionsDir(dir, folder));
21368
23521
  } catch {
21369
23522
  continue;
21370
23523
  }
@@ -21374,7 +23527,7 @@ var VaultCatalog = class {
21374
23527
  if (isSafeSessionId(sessionId)) keys.push({ agent: folder, sessionId });
21375
23528
  }
21376
23529
  }
21377
- const reads = await mapLimited(keys, LOAD_CONCURRENCY, (key) => readEntry(dir, key.agent, key.sessionId));
23530
+ const reads = await mapLimited(keys, LOAD_CONCURRENCY, (key) => readEntry2(dir, key.agent, key.sessionId));
21378
23531
  if (generation !== this.generation) return;
21379
23532
  const entries = /* @__PURE__ */ new Map();
21380
23533
  const foreign = /* @__PURE__ */ new Set();
@@ -21393,7 +23546,7 @@ var VaultCatalog = class {
21393
23546
  const dir = this.dir;
21394
23547
  if (dir === null || !isSafeSessionId(sessionId)) return;
21395
23548
  const generation = this.generation;
21396
- const read = await readEntry(dir, agent, sessionId);
23549
+ const read = await readEntry2(dir, agent, sessionId);
21397
23550
  if (generation !== this.generation) return;
21398
23551
  const key = keyOf2(agent, sessionId);
21399
23552
  const before = JSON.stringify(this.entries.get(key) ?? null);
@@ -21453,10 +23606,10 @@ var VaultCatalog = class {
21453
23606
  async *bodyLines(agent, sessionId) {
21454
23607
  const dir = this.dir;
21455
23608
  if (dir === null || !this.entries.has(keyOf2(agent, sessionId))) return;
21456
- const handle = await open7(sessionFile(dir, agent, sessionId), "r").catch(() => null);
23609
+ const handle = await open8(sessionFile(dir, agent, sessionId), "r").catch(() => null);
21457
23610
  if (handle === null) return;
21458
23611
  const input = handle.createReadStream({ encoding: "utf8" });
21459
- const lines = createInterface2({ input, crlfDelay: Number.POSITIVE_INFINITY });
23612
+ const lines = createInterface3({ input, crlfDelay: Number.POSITIVE_INFINITY });
21460
23613
  let sawHeader = false;
21461
23614
  try {
21462
23615
  for await (const raw of lines) {
@@ -21508,15 +23661,15 @@ var VaultCatalog = class {
21508
23661
  };
21509
23662
 
21510
23663
  // packages/server/src/index.ts
21511
- var serverDir = path51.dirname(fileURLToPath2(import.meta.url));
23664
+ var serverDir = path59.dirname(fileURLToPath2(import.meta.url));
21512
23665
  var isPackaged = true;
21513
- var webRoot = isPackaged ? serverDir : path51.resolve(serverDir, "../../web");
21514
- var webDist = isPackaged ? path51.join(serverDir, "web") : path51.join(webRoot, "dist");
21515
- var repoRoot = path51.resolve(serverDir, "../../..");
23666
+ var webRoot = isPackaged ? serverDir : path59.resolve(serverDir, "../../web");
23667
+ var webDist = isPackaged ? path59.join(serverDir, "web") : path59.join(webRoot, "dist");
23668
+ var repoRoot = path59.resolve(serverDir, "../../..");
21516
23669
  var isProduction = isPackaged || process.argv.includes("--prod");
21517
23670
  function resolveDefaultCwd() {
21518
23671
  const override = process.env["AGENT_WORKBENCH_CWD"];
21519
- if (override !== void 0 && override.length > 0) return path51.resolve(override);
23672
+ if (override !== void 0 && override.length > 0) return path59.resolve(override);
21520
23673
  if (isPackaged) return process.cwd();
21521
23674
  return repoRoot;
21522
23675
  }
@@ -21591,7 +23744,7 @@ async function mountDevUi(app, httpServer) {
21591
23744
  }
21592
23745
  var LOOKS_LIKE_FILE = /\.[a-zA-Z0-9]{1,8}$/;
21593
23746
  function mountBuiltUi(app) {
21594
- const indexHtml = path51.join(webDist, "index.html");
23747
+ const indexHtml = path59.join(webDist, "index.html");
21595
23748
  if (!existsSync4(indexHtml)) {
21596
23749
  throw new Error(
21597
23750
  `There's no compiled interface in ${webDist}.