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