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/README.md +50 -26
- package/dist/server.js +2689 -536
- package/dist/web/assets/de-B1W5R6hb.js +1 -0
- package/dist/web/assets/es-7g4wzPIf.js +1 -0
- package/dist/web/assets/fr-DaY5ttUk.js +1 -0
- package/dist/web/assets/{highlight-BeEbartE.js → highlight-BXBl9DQv.js} +1 -1
- package/dist/web/assets/index-Bk0CUikM.css +32 -0
- package/dist/web/assets/index-D8_sieMX.js +139 -0
- package/dist/web/assets/ja-C1rp47bG.js +1 -0
- package/dist/web/assets/ko-D0Bwy-Ri.js +1 -0
- package/dist/web/assets/pt-BR-BVIxhwAC.js +1 -0
- package/dist/web/assets/ru-CCRfyRi5.js +1 -0
- package/dist/web/assets/zh-CN-CaghDQ5n.js +1 -0
- package/dist/web/index.html +2 -2
- package/package.json +3 -2
- package/dist/web/assets/de-Dn90shDf.js +0 -1
- package/dist/web/assets/es-ssi17uPF.js +0 -1
- package/dist/web/assets/fr-CqnouzuH.js +0 -1
- package/dist/web/assets/index-D4OE_EfT.css +0 -32
- package/dist/web/assets/index-zFKk_FJ_.js +0 -139
- package/dist/web/assets/ja-1U-Ks_ON.js +0 -1
- package/dist/web/assets/ko-Ce2gOy44.js +0 -1
- package/dist/web/assets/pt-BR-BC8Km40N.js +0 -1
- package/dist/web/assets/ru-DFCLMfWY.js +0 -1
- package/dist/web/assets/zh-CN-VwZ7SeiK.js +0 -1
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
|
|
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((
|
|
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
|
|
650
|
+
const event2 = parseConversationEvent(record["event"]);
|
|
650
651
|
const images = asArrayOf(record["images"], parseVaultImageRef);
|
|
651
|
-
if (
|
|
652
|
-
const imageParts =
|
|
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
|
|
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((
|
|
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((
|
|
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
|
|
2657
|
-
if (
|
|
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
|
|
2680
|
-
if (
|
|
2681
|
-
this.events.push(
|
|
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
|
|
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(
|
|
2731
|
+
function limitEvent(event2, limits) {
|
|
2709
2732
|
let changed = false;
|
|
2710
|
-
const parts =
|
|
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 ? { ...
|
|
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
|
|
2755
|
-
const close =
|
|
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 (
|
|
2759
|
-
text = content.slice(
|
|
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((
|
|
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
|
|
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
|
|
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) =>
|
|
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((
|
|
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
|
|
5567
|
-
if (
|
|
5568
|
-
this.accumulate(
|
|
5569
|
-
return
|
|
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(
|
|
5662
|
-
const usage =
|
|
5663
|
-
if (usage === null ||
|
|
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 =
|
|
5666
|
-
const model2 = this.resolveModel(
|
|
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
|
|
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 (
|
|
6107
|
+
} else if (firstUserText3 === null && isRealUserMessage(record)) {
|
|
6076
6108
|
const message = record["message"];
|
|
6077
6109
|
if (typeof message === "object" && message !== null) {
|
|
6078
|
-
|
|
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 (
|
|
6093
|
-
title = toTitle(
|
|
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
|
|
7278
|
-
if (images.length > 0) this.contentLines.set(
|
|
7279
|
-
return
|
|
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((
|
|
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
|
|
8158
|
-
if (
|
|
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((
|
|
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((
|
|
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
|
|
8602
|
-
const index = nextIndex.get(
|
|
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(
|
|
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(
|
|
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 !==
|
|
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,
|
|
8617
|
-
if (known.has(
|
|
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(
|
|
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
|
|
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:
|
|
8963
|
+
if (dataLines.length > 0) messages.push({ event: event2 ?? "message", data: dataLines.join("\n"), id: lastId });
|
|
8932
8964
|
dataLines = [];
|
|
8933
|
-
|
|
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")
|
|
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
|
|
9161
|
-
if (
|
|
9192
|
+
const event2 = parseGlobalEvent(message.data);
|
|
9193
|
+
if (event2 === null) continue;
|
|
9162
9194
|
try {
|
|
9163
|
-
onEvent(
|
|
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(
|
|
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 =
|
|
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 (
|
|
9866
|
+
function applyEvent(session, { event: event2, fromChild }) {
|
|
9867
|
+
switch (event2.type) {
|
|
9823
9868
|
case "session.status":
|
|
9824
|
-
if (!fromChild) session.status =
|
|
9869
|
+
if (!fromChild) session.status = event2.status;
|
|
9825
9870
|
break;
|
|
9826
9871
|
case "permission.asked":
|
|
9827
|
-
session.permissions.add(
|
|
9872
|
+
session.permissions.add(event2.requestId);
|
|
9828
9873
|
break;
|
|
9829
9874
|
case "permission.replied":
|
|
9830
|
-
session.permissions.delete(
|
|
9875
|
+
session.permissions.delete(event2.requestId);
|
|
9831
9876
|
break;
|
|
9832
9877
|
case "question.asked":
|
|
9833
|
-
(fromChild ? session.childQuestions : session.questions).add(
|
|
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(
|
|
9838
|
-
session.childQuestions.delete(
|
|
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
|
-
(
|
|
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(
|
|
9984
|
-
const routed = this.route(
|
|
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(
|
|
9998
|
-
if (this.tracked.has(
|
|
9999
|
-
if (
|
|
10000
|
-
const rootId = this.rootOf(
|
|
10001
|
-
if (rootId === null || rootId ===
|
|
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
|
|
10177
|
-
imageReference: "
|
|
10178
|
-
pieceGapMs:
|
|
12172
|
+
var PI_INPUT = {
|
|
12173
|
+
imageReference: "quoted-path-line",
|
|
12174
|
+
pieceGapMs: 0,
|
|
10179
12175
|
pasteMarkers: true,
|
|
10180
|
-
enterSeparately:
|
|
10181
|
-
interruptPresses:
|
|
10182
|
-
//
|
|
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
|
|
10186
|
-
|
|
10187
|
-
|
|
10188
|
-
|
|
10189
|
-
|
|
10190
|
-
|
|
10191
|
-
|
|
10192
|
-
|
|
10193
|
-
|
|
10194
|
-
|
|
10195
|
-
|
|
10196
|
-
|
|
10197
|
-
|
|
10198
|
-
|
|
10199
|
-
|
|
10200
|
-
|
|
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: "
|
|
10232
|
-
label:
|
|
10233
|
-
command:
|
|
10234
|
-
installUrl:
|
|
10235
|
-
capabilities
|
|
10236
|
-
|
|
10237
|
-
|
|
10238
|
-
|
|
10239
|
-
|
|
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
|
-
*
|
|
10242
|
-
*
|
|
10243
|
-
*
|
|
10244
|
-
*
|
|
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
|
-
|
|
10251
|
-
|
|
10252
|
-
|
|
10253
|
-
|
|
10254
|
-
|
|
10255
|
-
}
|
|
10256
|
-
|
|
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
|
-
/**
|
|
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
|
-
|
|
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
|
-
|
|
10265
|
-
|
|
10266
|
-
(
|
|
10267
|
-
lo
|
|
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
|
-
|
|
10272
|
-
|
|
10273
|
-
|
|
10274
|
-
|
|
10275
|
-
|
|
10276
|
-
|
|
10277
|
-
|
|
10278
|
-
|
|
10279
|
-
|
|
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:
|
|
10283
|
-
dbFile: paths.dbFile,
|
|
10284
|
-
db,
|
|
10285
|
-
signal,
|
|
10286
|
-
catalog,
|
|
10287
|
-
platform,
|
|
10288
|
-
sqlite
|
|
10289
|
-
}),
|
|
12264
|
+
history: createPiHistory(catalog),
|
|
10290
12265
|
status,
|
|
10291
|
-
|
|
10292
|
-
|
|
10293
|
-
|
|
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
|
-
// `
|
|
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
|
-
*
|
|
10316
|
-
*
|
|
10317
|
-
* carpeta de
|
|
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
|
|
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 ${
|
|
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
|
|
10725
|
-
if (
|
|
10726
|
-
entry.openToolCall =
|
|
10727
|
-
this.emit("toolCall", terminalId,
|
|
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 ${
|
|
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
|
|
11019
|
-
for (const part of
|
|
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
|
|
13135
|
+
import path39 from "node:path";
|
|
11148
13136
|
function helperCandidates(packageDir, platform, arch) {
|
|
11149
13137
|
return [
|
|
11150
|
-
|
|
11151
|
-
|
|
11152
|
-
|
|
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
|
|
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
|
|
11820
|
-
import
|
|
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
|
|
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
|
|
13816
|
+
import path40 from "node:path";
|
|
11829
13817
|
function defaultVaultDir() {
|
|
11830
|
-
return
|
|
13818
|
+
return path40.join(appConfigDir(), "vault");
|
|
11831
13819
|
}
|
|
11832
13820
|
function vaultMarkerFile(dir) {
|
|
11833
|
-
return
|
|
13821
|
+
return path40.join(dir, "vault.json");
|
|
11834
13822
|
}
|
|
11835
13823
|
function sessionsRoot2(dir) {
|
|
11836
|
-
return
|
|
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
|
|
13838
|
+
return path40.join(sessionsRoot2(dir), agent);
|
|
11851
13839
|
}
|
|
11852
13840
|
function sessionFile(dir, agent, sessionId) {
|
|
11853
13841
|
assertSessionKey(agent, sessionId);
|
|
11854
|
-
return
|
|
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
|
|
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
|
|
13850
|
+
return path40.join(assetsDir(dir, agent, sessionId), asset);
|
|
11863
13851
|
}
|
|
11864
|
-
function
|
|
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" ?
|
|
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
|
|
13859
|
+
return path40.join(dir, "memory", projectFolderName2(cwd, platform));
|
|
11872
13860
|
}
|
|
11873
13861
|
function projectExportDir(dir, cwd, platform) {
|
|
11874
|
-
return
|
|
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
|
|
13868
|
+
return path40.join(dir, "export", `${name}-${hash}`);
|
|
11881
13869
|
}
|
|
11882
13870
|
function sessionExportFile(dir, agent, sessionId) {
|
|
11883
13871
|
assertSessionKey(agent, sessionId);
|
|
11884
|
-
return
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
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
|
|
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
|
|
12146
|
-
import
|
|
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:
|
|
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 ?
|
|
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:
|
|
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
|
|
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(
|
|
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
|
|
12748
|
-
import
|
|
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
|
|
12757
|
-
readdir as
|
|
12758
|
-
readFile as
|
|
12759
|
-
realpath as
|
|
14744
|
+
open as open7,
|
|
14745
|
+
readdir as readdir11,
|
|
14746
|
+
readFile as readFile14,
|
|
14747
|
+
realpath as realpath3,
|
|
12760
14748
|
rename as rename4,
|
|
12761
|
-
stat as
|
|
14749
|
+
stat as stat24,
|
|
12762
14750
|
unlink as unlink3
|
|
12763
14751
|
} from "node:fs/promises";
|
|
12764
|
-
import { homedir as
|
|
12765
|
-
import
|
|
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 =
|
|
14769
|
+
async function inspectMemory(cwd, home = homedir11()) {
|
|
12782
14770
|
await assertMemoryDirInside(cwd);
|
|
12783
|
-
const folder =
|
|
14771
|
+
const folder = path43.join(cwd, MEMORY_DIR);
|
|
12784
14772
|
const folderExists = await isDirectory2(folder);
|
|
12785
|
-
const indexExists = folderExists && await isFile(
|
|
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 =
|
|
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 =
|
|
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 =
|
|
14803
|
+
async function importNativeMemory(cwd, home = homedir11()) {
|
|
12816
14804
|
await assertMemoryDirInside(cwd);
|
|
12817
|
-
if (!await isDirectory2(
|
|
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
|
|
14823
|
+
const info = await stat24(absolute);
|
|
12836
14824
|
if (!info.isFile()) return null;
|
|
12837
|
-
const content = await
|
|
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 (
|
|
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(
|
|
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
|
|
12932
|
-
const info = await
|
|
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(
|
|
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(
|
|
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", "",
|
|
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(
|
|
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
|
|
13162
|
-
const opening =
|
|
13163
|
-
if (opening !== void 0 && !ignoredFences.has(index) && !(opening[0] === "`" && (
|
|
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
|
|
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(
|
|
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 =
|
|
15369
|
+
const root = path43.join(home, ".claude", "projects");
|
|
13378
15370
|
const slug = projectSlugFor(cwd).toLowerCase();
|
|
13379
15371
|
try {
|
|
13380
|
-
const entries = await
|
|
13381
|
-
return entries.filter((entry) => entry.isDirectory() && entry.name.toLowerCase() === slug).map((entry) => entry.name).sort().map((name) =>
|
|
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 =
|
|
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 =
|
|
13392
|
-
const geminiTarget =
|
|
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:
|
|
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:
|
|
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 =
|
|
13471
|
-
const absoluteCommonDir =
|
|
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 (
|
|
13476
|
-
const mainRoot =
|
|
13477
|
-
const inner =
|
|
13478
|
-
const sameFolder = inner.length === 0 || inner.startsWith("..") ||
|
|
13479
|
-
const mainPath = sameFolder ? mainRoot :
|
|
13480
|
-
const memoryRel = sameFolder ? MEMORY_DIR : [...inner.split(
|
|
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
|
|
15504
|
+
return await realpath3(value);
|
|
13504
15505
|
} catch {
|
|
13505
|
-
return
|
|
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
|
|
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
|
|
15548
|
+
const info = await stat24(absolute);
|
|
13548
15549
|
if (!info.isFile()) throw new MemoryBridgeError(serverText("memoryNotAFile", { label }));
|
|
13549
|
-
return await
|
|
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
|
|
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
|
|
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
|
|
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:
|
|
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 =
|
|
13608
|
-
|
|
13609
|
-
`.${
|
|
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
|
|
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
|
|
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 =
|
|
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 =
|
|
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 =
|
|
15852
|
+
const relative = path44.relative(cwd, candidate);
|
|
13852
15853
|
if (relative.length === 0) return true;
|
|
13853
|
-
if (relative.startsWith("..") ||
|
|
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
|
|
13872
|
-
import
|
|
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(
|
|
15964
|
+
function renderEvent(event2, images, previousDay, options) {
|
|
13964
15965
|
const blocks = [];
|
|
13965
15966
|
let imageNumber = 0;
|
|
13966
|
-
for (const part of
|
|
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 =
|
|
13977
|
-
const heading = [
|
|
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(
|
|
15980
|
+
heading.push(event2.role === "user" || day !== previousDay ? formatLocalDateTime(event2.at) : formatTime(event2.at));
|
|
13980
15981
|
}
|
|
13981
|
-
if (
|
|
13982
|
-
if (
|
|
13983
|
-
if (
|
|
13984
|
-
if (
|
|
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 (
|
|
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(
|
|
14079
|
-
return
|
|
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
|
|
14084
|
-
if (opensTurn(
|
|
14085
|
-
turns.push({ number: turns.length + 1, events: [
|
|
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(
|
|
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(
|
|
16176
|
+
function renderRequestBody(event2, textChars) {
|
|
14176
16177
|
const blocks = [];
|
|
14177
|
-
for (const part of
|
|
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
|
|
14193
|
-
for (const part of
|
|
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 = (
|
|
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(
|
|
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
|
|
14242
|
-
for (const part of
|
|
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(
|
|
14388
|
-
return style === "at-quoted" ? fileReference(
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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(
|
|
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
|
|
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 =
|
|
16653
|
+
const full = path45.join(this.root, entry);
|
|
14653
16654
|
try {
|
|
14654
|
-
const info = await
|
|
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
|
|
14672
|
-
import
|
|
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
|
|
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:
|
|
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
|
|
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
|
|
15268
|
-
import
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
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
|
|
15516
|
-
import
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
15739
|
-
const temporary =
|
|
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
|
|
15754
|
-
import
|
|
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
|
|
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
|
|
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(
|
|
15963
|
-
const temporary =
|
|
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
|
|
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
|
|
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 =
|
|
18007
|
+
let dir = path50.win32.dirname(gitPath);
|
|
16004
18008
|
for (let level = 0; level < 3; level++) {
|
|
16005
|
-
candidates.push(
|
|
16006
|
-
const parent =
|
|
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 :
|
|
16012
|
-
if (base !== void 0 && base.length > 0) candidates.push(
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
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
|
|
16487
|
-
import
|
|
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
|
|
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 =
|
|
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
|
|
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
|
|
17311
|
-
import { homedir as
|
|
17312
|
-
import
|
|
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 =
|
|
19323
|
+
const resolved = path52.resolve(target);
|
|
17320
19324
|
return dirs.some((dir) => {
|
|
17321
|
-
const protectedDir =
|
|
17322
|
-
return resolved === protectedDir || resolved.startsWith(protectedDir +
|
|
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(
|
|
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 =
|
|
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
|
|
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:
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
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(
|
|
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:
|
|
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
|
|
17500
|
-
import
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
17757
|
-
const fileName =
|
|
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,
|
|
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:
|
|
17897
|
-
role:
|
|
17898
|
-
at:
|
|
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
|
|
17922
|
-
if (
|
|
17923
|
-
for (const part of
|
|
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,
|
|
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
|
|
18347
|
-
import { homedir as
|
|
18348
|
-
import
|
|
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
|
|
18353
|
-
import
|
|
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
|
|
18366
|
-
await copyTree(
|
|
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(
|
|
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(
|
|
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(
|
|
20506
|
+
const probe = temporaryPathFor(path54.join(dir, ".escritura"));
|
|
18421
20507
|
try {
|
|
18422
20508
|
await mkdir15(dir, { recursive: true });
|
|
18423
|
-
if (!(await
|
|
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(
|
|
20557
|
+
function imageRefsOf(event2, images, assets, named) {
|
|
18472
20558
|
const refs = [];
|
|
18473
|
-
for (const part of
|
|
20559
|
+
for (const part of event2.parts) {
|
|
18474
20560
|
if (part.kind !== "image") continue;
|
|
18475
|
-
const input = images.get(imageKey(
|
|
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
|
|
18498
|
-
const line = { kind: "event", event, images: imageRefsOf(
|
|
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
|
|
18527
|
-
import
|
|
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
|
|
18531
|
-
import
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
|
19048
|
-
for (const part of
|
|
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(
|
|
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(
|
|
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((
|
|
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
|
|
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
|
|
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(
|
|
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(
|
|
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
|
|
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 =
|
|
21313
|
+
const file = path56.join(folder, name);
|
|
19228
21314
|
try {
|
|
19229
|
-
const info = await
|
|
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(),
|
|
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
|
|
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
|
|
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(
|
|
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(
|
|
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 ?
|
|
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(
|
|
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((
|
|
21757
|
+
const lines = whole.events.map((event2) => ({
|
|
19672
21758
|
kind: "event",
|
|
19673
|
-
event,
|
|
19674
|
-
images:
|
|
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) =>
|
|
19847
|
-
|
|
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,
|
|
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
|
|
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) => !
|
|
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
|
|
21271
|
-
import
|
|
21272
|
-
import { createInterface as
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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) =>
|
|
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
|
|
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
|
|
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 =
|
|
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 =
|
|
23664
|
+
var serverDir = path59.dirname(fileURLToPath2(import.meta.url));
|
|
21512
23665
|
var isPackaged = true;
|
|
21513
|
-
var webRoot = isPackaged ? serverDir :
|
|
21514
|
-
var webDist = isPackaged ?
|
|
21515
|
-
var repoRoot =
|
|
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
|
|
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 =
|
|
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}.
|