agent-workbench 0.3.1 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +182 -8
- package/dist/server.js +1920 -616
- package/dist/web/assets/de-fq_F8zRo.js +1 -0
- package/dist/web/assets/es-BCB5Npb3.js +1 -0
- package/dist/web/assets/fr-ClL0BO43.js +1 -0
- package/dist/web/assets/{highlight-BdawNAzW.js → highlight-2xGvJy1W.js} +1 -1
- package/dist/web/assets/index-BoXw3kE_.css +32 -0
- package/dist/web/assets/index-Bu0LGhCK.js +139 -0
- package/dist/web/assets/ja-C7KI4Gtm.js +1 -0
- package/dist/web/assets/ko-C8der8AK.js +1 -0
- package/dist/web/assets/pt-BR-rXhfKNdD.js +1 -0
- package/dist/web/assets/ru-BjrRxMeM.js +1 -0
- package/dist/web/assets/zh-CN-DDY7nBK5.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
|
}
|
|
@@ -354,6 +363,41 @@ function modelFamilyOf(value, options = MODEL_OPTIONS) {
|
|
|
354
363
|
return candidates.find((option) => value.includes(option.family))?.family ?? null;
|
|
355
364
|
}
|
|
356
365
|
|
|
366
|
+
// packages/shared/src/composer-draft.ts
|
|
367
|
+
var MAX_COMPOSER_DRAFT_CHARS = 1e6;
|
|
368
|
+
var MAX_COMPOSER_DRAFT_PASTES = 100;
|
|
369
|
+
var MAX_PASTE_NUMBER = 999999;
|
|
370
|
+
function composerDraftChars(draft) {
|
|
371
|
+
return draft.pasted.reduce((total, paste) => total + paste.text.length, draft.text.length);
|
|
372
|
+
}
|
|
373
|
+
function isEmptyComposerDraft(draft) {
|
|
374
|
+
return draft.text.trim().length === 0 && draft.pasted.every((paste) => paste.text.trim().length === 0);
|
|
375
|
+
}
|
|
376
|
+
function sameComposerDraft(a, b) {
|
|
377
|
+
if (a.text !== b.text || a.pasted.length !== b.pasted.length) return false;
|
|
378
|
+
return a.pasted.every((paste, index) => {
|
|
379
|
+
const other = b.pasted[index];
|
|
380
|
+
return other !== void 0 && other.number === paste.number && other.text === paste.text;
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
function parseComposerDraftPaste(value) {
|
|
384
|
+
const record = asRecord(value);
|
|
385
|
+
if (record === null) return null;
|
|
386
|
+
const number = record["number"];
|
|
387
|
+
const text = asString(record["text"]);
|
|
388
|
+
if (typeof number !== "number" || !Number.isInteger(number) || number < 1 || number > MAX_PASTE_NUMBER) return null;
|
|
389
|
+
return text === null ? null : { number, text };
|
|
390
|
+
}
|
|
391
|
+
function parseComposerDraft(value) {
|
|
392
|
+
const record = asRecord(value);
|
|
393
|
+
if (record === null) return null;
|
|
394
|
+
const text = asString(record["text"]);
|
|
395
|
+
const pasted2 = asArrayOf(record["pasted"], parseComposerDraftPaste);
|
|
396
|
+
if (text === null || pasted2 === null || pasted2.length > MAX_COMPOSER_DRAFT_PASTES) return null;
|
|
397
|
+
if (new Set(pasted2.map((paste) => paste.number)).size !== pasted2.length) return null;
|
|
398
|
+
return { text, pasted: pasted2 };
|
|
399
|
+
}
|
|
400
|
+
|
|
357
401
|
// packages/shared/src/memory.ts
|
|
358
402
|
var MEMORY_DIR = ".agents/memory";
|
|
359
403
|
var MEMORY_INDEX_FILE = "MEMORY.md";
|
|
@@ -620,6 +664,21 @@ function parseVaultBodyLine(value) {
|
|
|
620
664
|
return null;
|
|
621
665
|
}
|
|
622
666
|
|
|
667
|
+
// packages/shared/src/remote.ts
|
|
668
|
+
var REMOTE_DEFAULT_PORT = 24837;
|
|
669
|
+
var REMOTE_MIN_PORT = 1024;
|
|
670
|
+
var REMOTE_MAX_PORT = 65535;
|
|
671
|
+
var PAIR_QUERY_PARAM = "pair";
|
|
672
|
+
var PAIRING_TTL_MS = 5 * 6e4;
|
|
673
|
+
var REMOTE_DEVICE_LABEL_MAX_CHARS = 60;
|
|
674
|
+
var REMOTE_REVOKED_CLOSE_CODE = 4001;
|
|
675
|
+
function isRemotePort(value) {
|
|
676
|
+
return typeof value === "number" && Number.isInteger(value) && value >= REMOTE_MIN_PORT && value <= REMOTE_MAX_PORT;
|
|
677
|
+
}
|
|
678
|
+
function cleanDeviceLabel(label) {
|
|
679
|
+
return label.replace(/\s+/g, " ").trim().slice(0, REMOTE_DEVICE_LABEL_MAX_CHARS);
|
|
680
|
+
}
|
|
681
|
+
|
|
623
682
|
// packages/shared/src/protocol.ts
|
|
624
683
|
var PROTOCOL_VERSION = 8;
|
|
625
684
|
var WS_PATH = "/ws";
|
|
@@ -971,6 +1030,43 @@ function parseClientMessage(raw) {
|
|
|
971
1030
|
const searchId = asNonEmptyString(record["searchId"]);
|
|
972
1031
|
return searchId === null ? null : { type: "search.cancel", searchId };
|
|
973
1032
|
}
|
|
1033
|
+
case "remote.configure": {
|
|
1034
|
+
const rawEnabled = record["enabled"];
|
|
1035
|
+
const rawPort = record["port"];
|
|
1036
|
+
const enabled2 = rawEnabled === void 0 ? void 0 : asBoolean(rawEnabled);
|
|
1037
|
+
const port = rawPort === void 0 ? void 0 : asFiniteNumber(rawPort);
|
|
1038
|
+
if (enabled2 === null || port === null || enabled2 === void 0 && port === void 0) return null;
|
|
1039
|
+
return {
|
|
1040
|
+
type: "remote.configure",
|
|
1041
|
+
...enabled2 !== void 0 ? { enabled: enabled2 } : {},
|
|
1042
|
+
...port !== void 0 ? { port } : {}
|
|
1043
|
+
};
|
|
1044
|
+
}
|
|
1045
|
+
case "remote.pairing.start":
|
|
1046
|
+
return { type: "remote.pairing.start" };
|
|
1047
|
+
case "remote.pairing.cancel":
|
|
1048
|
+
return { type: "remote.pairing.cancel" };
|
|
1049
|
+
case "remote.device.rename": {
|
|
1050
|
+
const deviceId = asNonEmptyString(record["deviceId"]);
|
|
1051
|
+
const rawLabel = asString(record["label"]);
|
|
1052
|
+
const label = rawLabel === null ? "" : cleanDeviceLabel(rawLabel);
|
|
1053
|
+
return deviceId === null || label.length === 0 ? null : { type: "remote.device.rename", deviceId, label };
|
|
1054
|
+
}
|
|
1055
|
+
case "remote.device.revoke": {
|
|
1056
|
+
const deviceId = asNonEmptyString(record["deviceId"]);
|
|
1057
|
+
return deviceId === null ? null : { type: "remote.device.revoke", deviceId };
|
|
1058
|
+
}
|
|
1059
|
+
case "remote.phone.start":
|
|
1060
|
+
return { type: "remote.phone.start" };
|
|
1061
|
+
case "remote.phone.cancel":
|
|
1062
|
+
return { type: "remote.phone.cancel" };
|
|
1063
|
+
case "remote.device.forgetSelf":
|
|
1064
|
+
return { type: "remote.device.forgetSelf" };
|
|
1065
|
+
case "composer.draft": {
|
|
1066
|
+
const terminalId = asNonEmptyString(record["terminalId"]);
|
|
1067
|
+
const draft = parseComposerDraft(record["draft"]);
|
|
1068
|
+
return terminalId === null || draft === null ? null : { type: "composer.draft", terminalId, draft };
|
|
1069
|
+
}
|
|
974
1070
|
default:
|
|
975
1071
|
return null;
|
|
976
1072
|
}
|
|
@@ -1030,6 +1126,9 @@ function pastedBoundaryPattern() {
|
|
|
1030
1126
|
return /\[(?:Start|End) of pasted text #\d{1,6}\]/g;
|
|
1031
1127
|
}
|
|
1032
1128
|
|
|
1129
|
+
// packages/shared/src/support.ts
|
|
1130
|
+
var SUPPORT_URL = "https://buymeacoffee.com/cvelasquez";
|
|
1131
|
+
|
|
1033
1132
|
// packages/server/src/archived-sessions.ts
|
|
1034
1133
|
import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
|
|
1035
1134
|
import path2 from "node:path";
|
|
@@ -1068,12 +1167,18 @@ function archivedSessionsPath() {
|
|
|
1068
1167
|
function appSettingsPath() {
|
|
1069
1168
|
return path.join(appConfigDir(), "settings.json");
|
|
1070
1169
|
}
|
|
1170
|
+
function remoteDevicesPath() {
|
|
1171
|
+
return path.join(appConfigDir(), "remote-devices.json");
|
|
1172
|
+
}
|
|
1071
1173
|
function notesStatePath() {
|
|
1072
1174
|
return path.join(appConfigDir(), "notes.json");
|
|
1073
1175
|
}
|
|
1074
1176
|
function notesImagesDir() {
|
|
1075
1177
|
return path.join(appConfigDir(), "notes-images");
|
|
1076
1178
|
}
|
|
1179
|
+
function composerDraftsPath() {
|
|
1180
|
+
return path.join(appConfigDir(), "composer-drafts.json");
|
|
1181
|
+
}
|
|
1077
1182
|
function catalogTempDir() {
|
|
1078
1183
|
return path.join(tmpdir(), "agent-workbench", "agy-catalog");
|
|
1079
1184
|
}
|
|
@@ -10665,214 +10770,1075 @@ function ensurePtyHelperExecutable(options = {}) {
|
|
|
10665
10770
|
return fixed;
|
|
10666
10771
|
}
|
|
10667
10772
|
|
|
10668
|
-
// packages/server/src/
|
|
10669
|
-
import {
|
|
10670
|
-
|
|
10671
|
-
|
|
10672
|
-
|
|
10673
|
-
|
|
10674
|
-
|
|
10675
|
-
|
|
10773
|
+
// packages/server/src/remote-access-service.ts
|
|
10774
|
+
import { execFile as execFile3 } from "node:child_process";
|
|
10775
|
+
import { randomBytes as cryptoRandomBytes, randomUUID as randomUUID2 } from "node:crypto";
|
|
10776
|
+
import os from "node:os";
|
|
10777
|
+
|
|
10778
|
+
// packages/server/src/phone-pairing.ts
|
|
10779
|
+
import { createPrivateKey, createPublicKey } from "node:crypto";
|
|
10780
|
+
var PHONE_PAYLOAD_PREFIX = "agentworkbench://pair?";
|
|
10781
|
+
var PHONE_PAYLOAD_VERSION = 1;
|
|
10782
|
+
var PHONE_SSH_PORT = 22;
|
|
10783
|
+
var PHONE_KEY_COMMENT = "agent-workbench-phone";
|
|
10784
|
+
var PHONE_SEED_BYTES = 32;
|
|
10785
|
+
var ED25519_PKCS8_PREFIX = Buffer.from("302e020100300506032b657004220420", "hex");
|
|
10786
|
+
function ed25519PublicKey(seed) {
|
|
10787
|
+
if (seed.length !== PHONE_SEED_BYTES) throw new Error(`an Ed25519 seed has ${PHONE_SEED_BYTES} bytes`);
|
|
10788
|
+
const privateKey = createPrivateKey({ key: Buffer.concat([ED25519_PKCS8_PREFIX, seed]), format: "der", type: "pkcs8" });
|
|
10789
|
+
const spki = createPublicKey(privateKey).export({ format: "der", type: "spki" });
|
|
10790
|
+
return Buffer.from(spki.subarray(spki.length - 32));
|
|
10791
|
+
}
|
|
10792
|
+
function sshString(bytes) {
|
|
10793
|
+
const length = Buffer.alloc(4);
|
|
10794
|
+
length.writeUInt32BE(bytes.length);
|
|
10795
|
+
return Buffer.concat([length, bytes]);
|
|
10796
|
+
}
|
|
10797
|
+
function sshEd25519PublicKey(publicKey) {
|
|
10798
|
+
const blob = Buffer.concat([sshString(Buffer.from("ssh-ed25519", "ascii")), sshString(publicKey)]);
|
|
10799
|
+
return `ssh-ed25519 ${blob.toString("base64")}`;
|
|
10800
|
+
}
|
|
10801
|
+
function phoneAuthorizedKeysEntry(publicKey, appPort) {
|
|
10802
|
+
return `restrict,port-forwarding,permitopen="127.0.0.1:${appPort}",command="exit" ${sshEd25519PublicKey(publicKey)} ${PHONE_KEY_COMMENT}`;
|
|
10803
|
+
}
|
|
10804
|
+
function phoneAuthorizeCommand(entry, shell) {
|
|
10805
|
+
switch (shell) {
|
|
10806
|
+
case "powershell-admin":
|
|
10807
|
+
return `$f = "$env:ProgramData\\ssh\\administrators_authorized_keys"; Add-Content -Path $f -Encoding ascii -Value @('', '${entry}'); icacls.exe $f /inheritance:r /grant '*S-1-5-32-544:F' /grant '*S-1-5-18:F'`;
|
|
10808
|
+
case "powershell":
|
|
10809
|
+
return `New-Item -ItemType Directory -Force "$HOME\\.ssh" | Out-Null; Add-Content -Path "$HOME\\.ssh\\authorized_keys" -Encoding ascii -Value @('', '${entry}')`;
|
|
10810
|
+
case "terminal":
|
|
10811
|
+
return `mkdir -p ~/.ssh && chmod 700 ~/.ssh && printf '\\n%s\\n' '${entry}' >> ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys`;
|
|
10812
|
+
}
|
|
10813
|
+
}
|
|
10814
|
+
function authorizeShellFromGroups(whoamiGroups) {
|
|
10815
|
+
return /S-1-5-32-544/.test(whoamiGroups) ? "powershell-admin" : "powershell";
|
|
10816
|
+
}
|
|
10817
|
+
function phonePairingPayload(fields) {
|
|
10818
|
+
const params = new URLSearchParams();
|
|
10819
|
+
params.set("v", String(PHONE_PAYLOAD_VERSION));
|
|
10820
|
+
params.set("n", fields.pcName);
|
|
10821
|
+
params.set("h", fields.hosts.join(","));
|
|
10822
|
+
params.set("p", String(fields.sshPort));
|
|
10823
|
+
params.set("u", fields.user);
|
|
10824
|
+
params.set("a", String(fields.appPort));
|
|
10825
|
+
params.set("c", fields.code);
|
|
10826
|
+
params.set("k", fields.seed.toString("base64url"));
|
|
10827
|
+
return PHONE_PAYLOAD_PREFIX + params.toString();
|
|
10676
10828
|
}
|
|
10677
|
-
|
|
10829
|
+
|
|
10830
|
+
// packages/server/src/remote-access.ts
|
|
10831
|
+
import { createHash, timingSafeEqual } from "node:crypto";
|
|
10832
|
+
var PAIRING_ALPHABET = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
|
|
10833
|
+
var PAIRING_CODE_CHARS = 8;
|
|
10834
|
+
var PAIRING_MAX_FAILURES = 5;
|
|
10835
|
+
function formatPairingCode(raw) {
|
|
10836
|
+
const half = PAIRING_CODE_CHARS / 2;
|
|
10837
|
+
return `${raw.slice(0, half)}-${raw.slice(half)}`;
|
|
10838
|
+
}
|
|
10839
|
+
function normalizePairingCode(input) {
|
|
10840
|
+
const raw = input.replace(/[\s-]/g, "").toUpperCase();
|
|
10841
|
+
if (raw.length !== PAIRING_CODE_CHARS) return null;
|
|
10842
|
+
for (const char of raw) {
|
|
10843
|
+
if (!PAIRING_ALPHABET.includes(char)) return null;
|
|
10844
|
+
}
|
|
10845
|
+
return raw;
|
|
10846
|
+
}
|
|
10847
|
+
function sameText(a, b) {
|
|
10678
10848
|
const bufferA = Buffer.from(a, "utf8");
|
|
10679
10849
|
const bufferB = Buffer.from(b, "utf8");
|
|
10680
10850
|
if (bufferA.length !== bufferB.length) return false;
|
|
10681
10851
|
return timingSafeEqual(bufferA, bufferB);
|
|
10682
10852
|
}
|
|
10683
|
-
|
|
10684
|
-
|
|
10685
|
-
|
|
10686
|
-
} catch {
|
|
10687
|
-
return null;
|
|
10853
|
+
var PairingDesk = class {
|
|
10854
|
+
constructor(deps) {
|
|
10855
|
+
this.deps = deps;
|
|
10688
10856
|
}
|
|
10689
|
-
|
|
10690
|
-
|
|
10691
|
-
|
|
10692
|
-
|
|
10693
|
-
|
|
10694
|
-
|
|
10695
|
-
|
|
10696
|
-
|
|
10697
|
-
|
|
10698
|
-
return
|
|
10857
|
+
deps;
|
|
10858
|
+
current = null;
|
|
10859
|
+
/** Un código nuevo. Reemplaza al que hubiera: nunca hay dos vigentes. */
|
|
10860
|
+
start() {
|
|
10861
|
+
const bytes = this.deps.randomBytes(PAIRING_CODE_CHARS);
|
|
10862
|
+
let raw = "";
|
|
10863
|
+
for (let i = 0; i < PAIRING_CODE_CHARS; i++) raw += PAIRING_ALPHABET[(bytes[i] ?? 0) % PAIRING_ALPHABET.length];
|
|
10864
|
+
const expiresAt = this.deps.now() + PAIRING_TTL_MS;
|
|
10865
|
+
this.current = { raw, expiresAt, failures: 0 };
|
|
10866
|
+
return { code: formatPairingCode(raw), expiresAt };
|
|
10699
10867
|
}
|
|
10700
|
-
|
|
10701
|
-
|
|
10702
|
-
function matchToken(request, expected) {
|
|
10703
|
-
const url = requestUrl(request);
|
|
10704
|
-
const fromQuery = url?.searchParams.get(TOKEN_QUERY_PARAM) ?? null;
|
|
10705
|
-
if (fromQuery !== null && constantTimeEquals(fromQuery, expected)) {
|
|
10706
|
-
return { source: "query" };
|
|
10868
|
+
cancel() {
|
|
10869
|
+
this.current = null;
|
|
10707
10870
|
}
|
|
10708
|
-
|
|
10709
|
-
|
|
10710
|
-
return
|
|
10871
|
+
isActive() {
|
|
10872
|
+
if (this.current !== null && this.deps.now() >= this.current.expiresAt) this.current = null;
|
|
10873
|
+
return this.current !== null;
|
|
10711
10874
|
}
|
|
10712
|
-
|
|
10713
|
-
|
|
10714
|
-
return
|
|
10875
|
+
/** Cuándo vence el vigente, o null. */
|
|
10876
|
+
expiresAt() {
|
|
10877
|
+
return this.isActive() ? this.current?.expiresAt ?? null : null;
|
|
10715
10878
|
}
|
|
10716
|
-
|
|
10717
|
-
|
|
10718
|
-
|
|
10719
|
-
|
|
10879
|
+
redeem(input) {
|
|
10880
|
+
if (!this.isActive() || this.current === null) return "none";
|
|
10881
|
+
const raw = normalizePairingCode(input);
|
|
10882
|
+
if (raw === null) return "none";
|
|
10883
|
+
if (sameText(raw, this.current.raw)) {
|
|
10884
|
+
this.current = null;
|
|
10885
|
+
return "ok";
|
|
10886
|
+
}
|
|
10887
|
+
this.current.failures += 1;
|
|
10888
|
+
if (this.current.failures >= PAIRING_MAX_FAILURES) this.current = null;
|
|
10889
|
+
return "wrong";
|
|
10890
|
+
}
|
|
10891
|
+
};
|
|
10892
|
+
var REMOTE_DEVICES_VERSION = 1;
|
|
10893
|
+
var REMOTE_MAX_DEVICES = 20;
|
|
10894
|
+
function hashCredential(credential) {
|
|
10895
|
+
return createHash("sha256").update(credential, "utf8").digest("hex");
|
|
10720
10896
|
}
|
|
10721
|
-
|
|
10722
|
-
|
|
10897
|
+
var DeviceBook = class {
|
|
10898
|
+
constructor(deps) {
|
|
10899
|
+
this.deps = deps;
|
|
10900
|
+
}
|
|
10901
|
+
deps;
|
|
10902
|
+
devices = [];
|
|
10903
|
+
load(devices) {
|
|
10904
|
+
this.devices = devices.slice(0, REMOTE_MAX_DEVICES).map((device) => ({ ...device }));
|
|
10905
|
+
}
|
|
10906
|
+
/** Para guardar: con el hash. */
|
|
10907
|
+
snapshot() {
|
|
10908
|
+
return this.devices.map((device) => ({ ...device }));
|
|
10909
|
+
}
|
|
10910
|
+
/** Para mostrar: sin el hash. */
|
|
10911
|
+
list() {
|
|
10912
|
+
return this.devices.map(({ id, label, createdAt, lastSeenAt }) => ({ id, label, createdAt, lastSeenAt }));
|
|
10913
|
+
}
|
|
10914
|
+
get size() {
|
|
10915
|
+
return this.devices.length;
|
|
10916
|
+
}
|
|
10917
|
+
/** Un equipo nuevo y su credencial, que no queda guardada en ningún lado; null si ya no entran más. */
|
|
10918
|
+
enroll(label) {
|
|
10919
|
+
if (this.devices.length >= REMOTE_MAX_DEVICES) return null;
|
|
10920
|
+
const credential = this.deps.randomBytes(32).toString("base64url");
|
|
10921
|
+
const device = {
|
|
10922
|
+
id: this.deps.randomId(),
|
|
10923
|
+
label: cleanDeviceLabel(label) || "Device",
|
|
10924
|
+
hash: hashCredential(credential),
|
|
10925
|
+
createdAt: this.deps.now(),
|
|
10926
|
+
lastSeenAt: null
|
|
10927
|
+
};
|
|
10928
|
+
this.devices.push(device);
|
|
10929
|
+
return { device: { ...device }, credential };
|
|
10930
|
+
}
|
|
10931
|
+
match(credential) {
|
|
10932
|
+
if (credential.length === 0) return null;
|
|
10933
|
+
const hash = hashCredential(credential);
|
|
10934
|
+
let found = null;
|
|
10935
|
+
for (const device of this.devices) {
|
|
10936
|
+
if (sameText(device.hash, hash)) found = device;
|
|
10937
|
+
}
|
|
10938
|
+
return found === null ? null : { ...found };
|
|
10939
|
+
}
|
|
10940
|
+
/** Anota cuándo se lo vio: ahora, o la marca que se le pase. */
|
|
10941
|
+
touch(id, at = this.deps.now()) {
|
|
10942
|
+
const device = this.devices.find((candidate) => candidate.id === id);
|
|
10943
|
+
if (device === void 0) return false;
|
|
10944
|
+
device.lastSeenAt = at;
|
|
10945
|
+
return true;
|
|
10946
|
+
}
|
|
10947
|
+
rename(id, label) {
|
|
10948
|
+
const device = this.devices.find((candidate) => candidate.id === id);
|
|
10949
|
+
const clean = cleanDeviceLabel(label);
|
|
10950
|
+
if (device === void 0 || clean.length === 0) return false;
|
|
10951
|
+
device.label = clean;
|
|
10952
|
+
return true;
|
|
10953
|
+
}
|
|
10954
|
+
revoke(id) {
|
|
10955
|
+
const before = this.devices.length;
|
|
10956
|
+
this.devices = this.devices.filter((device) => device.id !== id);
|
|
10957
|
+
return this.devices.length !== before;
|
|
10958
|
+
}
|
|
10959
|
+
};
|
|
10960
|
+
function parseRemoteDevice(value) {
|
|
10961
|
+
const record = asRecord(value);
|
|
10962
|
+
if (record === null) return null;
|
|
10963
|
+
const id = asNonEmptyString(record["id"]);
|
|
10964
|
+
const label = asString(record["label"]);
|
|
10965
|
+
const hash = asString(record["hash"]);
|
|
10966
|
+
const createdAt = asFiniteNumber(record["createdAt"]);
|
|
10967
|
+
if (id === null || label === null || hash === null || createdAt === null) return null;
|
|
10968
|
+
if (!/^[0-9a-f]{64}$/.test(hash)) return null;
|
|
10969
|
+
const lastSeenAt = record["lastSeenAt"] === null || record["lastSeenAt"] === void 0 ? null : asFiniteNumber(record["lastSeenAt"]);
|
|
10970
|
+
return { id, label: cleanDeviceLabel(label) || "Device", hash, createdAt, lastSeenAt };
|
|
10723
10971
|
}
|
|
10724
|
-
function
|
|
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);
|
|
10972
|
+
function parseRemoteDevicesFile(value) {
|
|
10973
|
+
const record = asRecord(value);
|
|
10974
|
+
if (record === null || record["version"] !== REMOTE_DEVICES_VERSION) return null;
|
|
10975
|
+
return asArrayFiltered(record["devices"], parseRemoteDevice) ?? [];
|
|
10732
10976
|
}
|
|
10733
|
-
function
|
|
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);
|
|
10977
|
+
function serializeRemoteDevices(devices) {
|
|
10978
|
+
return `${JSON.stringify({ version: REMOTE_DEVICES_VERSION, devices }, null, 2)}
|
|
10979
|
+
`;
|
|
10745
10980
|
}
|
|
10746
|
-
function
|
|
10747
|
-
if (!
|
|
10748
|
-
if (
|
|
10749
|
-
if (!
|
|
10981
|
+
function remoteAccessState(remote, listening) {
|
|
10982
|
+
if (!remote.enabled) return "off";
|
|
10983
|
+
if (listening.fixedPortBusy) return "port-busy";
|
|
10984
|
+
if (!listening.fixed || listening.port !== remote.port) return "restart-needed";
|
|
10985
|
+
return "active";
|
|
10986
|
+
}
|
|
10987
|
+
function isPrivateIpv4(address) {
|
|
10988
|
+
const parts = address.split(".").map((part) => Number(part));
|
|
10989
|
+
if (parts.length !== 4 || parts.some((part) => !Number.isInteger(part) || part < 0 || part > 255)) return false;
|
|
10990
|
+
const [a, b] = parts;
|
|
10991
|
+
return a === 10 || a === 172 && b >= 16 && b <= 31 || a === 192 && b === 168;
|
|
10992
|
+
}
|
|
10993
|
+
function privateIpv4Addresses(interfaces) {
|
|
10994
|
+
const addresses = [];
|
|
10995
|
+
for (const list of Object.values(interfaces)) {
|
|
10996
|
+
for (const info of list ?? []) {
|
|
10997
|
+
if (info.internal || info.family !== "IPv4" || !isPrivateIpv4(info.address)) continue;
|
|
10998
|
+
if (!addresses.includes(info.address)) addresses.push(info.address);
|
|
10999
|
+
}
|
|
11000
|
+
}
|
|
11001
|
+
return addresses;
|
|
11002
|
+
}
|
|
11003
|
+
var PHONE_APP_USER_AGENT = /AgentWorkbenchAndroid\/\S+ \(Android [^;)]*; ([^)]{1,80})\)/;
|
|
11004
|
+
function deviceLabelFromUserAgent(userAgent) {
|
|
11005
|
+
if (userAgent === void 0) return "Device";
|
|
11006
|
+
const phone = PHONE_APP_USER_AGENT.exec(userAgent);
|
|
11007
|
+
if (phone !== null) {
|
|
11008
|
+
const name = cleanDeviceLabel(phone[1] ?? "");
|
|
11009
|
+
return name.length > 0 ? cleanDeviceLabel(`${name} \xB7 Android`) : "Android";
|
|
11010
|
+
}
|
|
11011
|
+
const browser = /Edg\//.test(userAgent) ? "Edge" : /OPR\//.test(userAgent) ? "Opera" : /Firefox\//.test(userAgent) ? "Firefox" : /Chrome\//.test(userAgent) ? "Chrome" : /Safari\//.test(userAgent) ? "Safari" : null;
|
|
11012
|
+
const system = /Windows/.test(userAgent) ? "Windows" : /Android/.test(userAgent) ? "Android" : /iPhone|iPad/.test(userAgent) ? "iOS" : /Mac OS X/.test(userAgent) ? "macOS" : /Linux/.test(userAgent) ? "Linux" : null;
|
|
11013
|
+
if (browser === null || system === null) return "Device";
|
|
11014
|
+
return `${browser} \xB7 ${system}`;
|
|
11015
|
+
}
|
|
11016
|
+
var HOST_ONLY = /* @__PURE__ */ new Set([
|
|
11017
|
+
"remote.configure",
|
|
11018
|
+
"remote.pairing.start",
|
|
11019
|
+
"remote.pairing.cancel",
|
|
11020
|
+
"remote.device.rename",
|
|
11021
|
+
"remote.device.revoke",
|
|
11022
|
+
"remote.phone.start",
|
|
11023
|
+
"remote.phone.cancel"
|
|
11024
|
+
]);
|
|
11025
|
+
var OPENS_ON_HOST = /* @__PURE__ */ new Set([
|
|
11026
|
+
"files.reveal",
|
|
11027
|
+
"vault.reveal",
|
|
11028
|
+
"vault.openSession",
|
|
11029
|
+
"vault.exportProject"
|
|
11030
|
+
]);
|
|
11031
|
+
function remoteRefusal(type, remoteClient) {
|
|
11032
|
+
if (!remoteClient) return null;
|
|
11033
|
+
if (HOST_ONLY.has(type)) return serverText("remoteHostOnly");
|
|
11034
|
+
if (OPENS_ON_HOST.has(type)) return serverText("remoteOpensOnHost");
|
|
10750
11035
|
return null;
|
|
10751
11036
|
}
|
|
10752
11037
|
|
|
10753
|
-
// packages/server/src/
|
|
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
|
-
);
|
|
11038
|
+
// packages/server/src/remote-access-service.ts
|
|
11039
|
+
var TOUCH_SAVE_DELAY_MS = 5e3;
|
|
11040
|
+
var FILES_POLL_MS = 2e3;
|
|
11041
|
+
function detectAuthorizeShell() {
|
|
11042
|
+
if (process.platform !== "win32") return Promise.resolve("terminal");
|
|
11043
|
+
return new Promise((resolve) => {
|
|
11044
|
+
execFile3("whoami", ["/groups", "/fo", "csv", "/nh"], { windowsHide: true, timeout: 5e3 }, (error, stdout) => {
|
|
11045
|
+
resolve(error === null ? authorizeShellFromGroups(String(stdout)) : "powershell-admin");
|
|
11046
|
+
});
|
|
10812
11047
|
});
|
|
10813
11048
|
}
|
|
10814
|
-
function
|
|
10815
|
-
|
|
11049
|
+
function detectHostInfo() {
|
|
11050
|
+
let user = "";
|
|
11051
|
+
try {
|
|
11052
|
+
user = os.userInfo().username;
|
|
11053
|
+
} catch {
|
|
11054
|
+
user = process.env["USERNAME"] ?? process.env["USER"] ?? "";
|
|
11055
|
+
}
|
|
11056
|
+
return { user, hostNames: [os.hostname(), ...privateIpv4Addresses(os.networkInterfaces())] };
|
|
10816
11057
|
}
|
|
10817
|
-
|
|
10818
|
-
|
|
10819
|
-
|
|
10820
|
-
|
|
10821
|
-
|
|
11058
|
+
var RemoteAccessService = class {
|
|
11059
|
+
settings;
|
|
11060
|
+
store;
|
|
11061
|
+
listening;
|
|
11062
|
+
host;
|
|
11063
|
+
now;
|
|
11064
|
+
log;
|
|
11065
|
+
book;
|
|
11066
|
+
desk;
|
|
11067
|
+
randomBytes;
|
|
11068
|
+
authorizeShell;
|
|
11069
|
+
authorizeShellCache = null;
|
|
11070
|
+
/**
|
|
11071
|
+
* La semilla de la llave del teléfono que se está emparejando (hito 38). Sólo
|
|
11072
|
+
* en memoria, y sólo mientras dure ese emparejamiento: se olvida al
|
|
11073
|
+
* cancelarlo, al canjearse el código o al cerrarse la ventana que lo pidió.
|
|
11074
|
+
*/
|
|
11075
|
+
phoneSeed = null;
|
|
11076
|
+
changeListeners = /* @__PURE__ */ new Set();
|
|
11077
|
+
revokedListeners = /* @__PURE__ */ new Set();
|
|
11078
|
+
/** Cuántas ventanas tiene abiertas cada equipo. */
|
|
11079
|
+
connections = /* @__PURE__ */ new Map();
|
|
11080
|
+
/** Las "última vez visto" que todavía no se guardaron: releer el archivo no se las lleva. */
|
|
11081
|
+
pendingSeen = /* @__PURE__ */ new Map();
|
|
11082
|
+
pairingTimer = null;
|
|
11083
|
+
touchTimer = null;
|
|
11084
|
+
pollTimer = null;
|
|
11085
|
+
/** Cómo estaban los dos archivos la última vez que se leyeron o escribieron desde acá. */
|
|
11086
|
+
stamps = { devices: "", settings: "" };
|
|
11087
|
+
/** Las sincronizaciones, en fila: la del sondeo y la de un cambio no se cruzan. */
|
|
11088
|
+
syncing = Promise.resolve();
|
|
11089
|
+
constructor(options) {
|
|
11090
|
+
this.settings = options.settings;
|
|
11091
|
+
this.store = options.store;
|
|
11092
|
+
this.listening = options.listening;
|
|
11093
|
+
this.host = options.host ?? null;
|
|
11094
|
+
this.now = options.now ?? (() => Date.now());
|
|
11095
|
+
this.log = options.log ?? console;
|
|
11096
|
+
const randomBytes5 = options.randomBytes ?? ((size) => cryptoRandomBytes(size));
|
|
11097
|
+
this.randomBytes = randomBytes5;
|
|
11098
|
+
this.authorizeShell = options.authorizeShell ?? detectAuthorizeShell;
|
|
11099
|
+
this.book = new DeviceBook({ now: this.now, randomBytes: randomBytes5, randomId: () => randomUUID2() });
|
|
11100
|
+
this.desk = new PairingDesk({ now: this.now, randomBytes: randomBytes5 });
|
|
10822
11101
|
}
|
|
10823
|
-
|
|
10824
|
-
|
|
10825
|
-
|
|
11102
|
+
async load() {
|
|
11103
|
+
this.book.load(await this.store.load());
|
|
11104
|
+
this.stamps = { devices: await this.store.stamp(), settings: await this.settings.stamp() };
|
|
11105
|
+
this.updatePolling();
|
|
10826
11106
|
}
|
|
10827
|
-
|
|
11107
|
+
// ---- Qué corre ---------------------------------------------------------------
|
|
11108
|
+
remote() {
|
|
11109
|
+
return this.settings.get().remote;
|
|
11110
|
+
}
|
|
11111
|
+
/** Entran equipos: encendido, y el servidor escucha en el puerto fijo con el que arrancó. */
|
|
11112
|
+
accepting() {
|
|
11113
|
+
return this.remote().enabled && this.listening.fixed;
|
|
11114
|
+
}
|
|
11115
|
+
/** A quién pertenece una credencial, o null si ahora no entra nadie. */
|
|
11116
|
+
verifier() {
|
|
11117
|
+
return this.accepting() ? this.book : null;
|
|
11118
|
+
}
|
|
11119
|
+
get pairedDevices() {
|
|
11120
|
+
return this.book.size;
|
|
11121
|
+
}
|
|
11122
|
+
status() {
|
|
11123
|
+
const remote = this.remote();
|
|
11124
|
+
const host = this.host ?? detectHostInfo();
|
|
10828
11125
|
return {
|
|
10829
|
-
|
|
10830
|
-
|
|
10831
|
-
|
|
11126
|
+
enabled: remote.enabled,
|
|
11127
|
+
port: remote.port,
|
|
11128
|
+
state: remoteAccessState(remote, this.listening),
|
|
11129
|
+
listeningPort: this.listening.port,
|
|
11130
|
+
sshUser: host.user,
|
|
11131
|
+
hostNames: [...host.hostNames],
|
|
11132
|
+
pairingActive: this.desk.isActive(),
|
|
11133
|
+
devices: this.book.list().map((device) => ({
|
|
11134
|
+
...device,
|
|
11135
|
+
connected: (this.connections.get(device.id) ?? 0) > 0
|
|
11136
|
+
}))
|
|
10832
11137
|
};
|
|
10833
11138
|
}
|
|
10834
|
-
|
|
10835
|
-
|
|
10836
|
-
|
|
10837
|
-
|
|
10838
|
-
|
|
10839
|
-
|
|
10840
|
-
|
|
10841
|
-
|
|
10842
|
-
|
|
10843
|
-
|
|
10844
|
-
async function findGitDir(cwd) {
|
|
10845
|
-
const result = await runGit(cwd, ["rev-parse", "--absolute-git-dir"]);
|
|
10846
|
-
if (result.code !== 0) return null;
|
|
10847
|
-
const dir = result.stdout.trim();
|
|
10848
|
-
return dir.length > 0 ? path30.resolve(dir) : null;
|
|
10849
|
-
}
|
|
10850
|
-
function letterToKind(letter) {
|
|
10851
|
-
switch (letter) {
|
|
10852
|
-
case "A":
|
|
10853
|
-
return "added";
|
|
10854
|
-
case "M":
|
|
10855
|
-
return "modified";
|
|
10856
|
-
case "D":
|
|
10857
|
-
return "deleted";
|
|
10858
|
-
case "R":
|
|
10859
|
-
return "renamed";
|
|
10860
|
-
case "C":
|
|
10861
|
-
return "copied";
|
|
10862
|
-
case "T":
|
|
10863
|
-
return "type-changed";
|
|
10864
|
-
default:
|
|
10865
|
-
return "modified";
|
|
11139
|
+
// ---- Lo que cambió otra instancia ----------------------------------------------
|
|
11140
|
+
/**
|
|
11141
|
+
* Trae lo que haya cambiado en los dos archivos desde la última vez. Un equipo
|
|
11142
|
+
* que ya no está se avisa como revocado, y el acceso apagado desde otro lado
|
|
11143
|
+
* corta igual que apagado desde acá.
|
|
11144
|
+
*/
|
|
11145
|
+
sync() {
|
|
11146
|
+
const result = this.syncing.then(() => this.syncNow());
|
|
11147
|
+
this.syncing = result.catch(() => void 0);
|
|
11148
|
+
return result;
|
|
10866
11149
|
}
|
|
10867
|
-
|
|
10868
|
-
|
|
10869
|
-
|
|
10870
|
-
|
|
10871
|
-
|
|
10872
|
-
|
|
10873
|
-
|
|
10874
|
-
|
|
10875
|
-
|
|
11150
|
+
async syncNow() {
|
|
11151
|
+
let changed = false;
|
|
11152
|
+
const settingsStamp = await this.settings.stamp();
|
|
11153
|
+
if (settingsStamp !== this.stamps.settings) {
|
|
11154
|
+
this.stamps.settings = settingsStamp;
|
|
11155
|
+
const wasAccepting = this.accepting();
|
|
11156
|
+
await this.settings.reloadRemote();
|
|
11157
|
+
if (wasAccepting && !this.accepting()) this.cutOff();
|
|
11158
|
+
changed = true;
|
|
11159
|
+
}
|
|
11160
|
+
const devicesStamp = await this.store.stamp();
|
|
11161
|
+
if (devicesStamp !== this.stamps.devices) {
|
|
11162
|
+
this.stamps.devices = devicesStamp;
|
|
11163
|
+
const before = this.book.list().map((device) => device.id);
|
|
11164
|
+
this.book.load(await this.store.load());
|
|
11165
|
+
for (const [deviceId, at] of this.pendingSeen) this.book.touch(deviceId, at);
|
|
11166
|
+
const still = new Set(this.book.list().map((device) => device.id));
|
|
11167
|
+
for (const deviceId of before) {
|
|
11168
|
+
if (still.has(deviceId)) continue;
|
|
11169
|
+
this.connections.delete(deviceId);
|
|
11170
|
+
for (const listener of this.revokedListeners) listener(deviceId);
|
|
11171
|
+
}
|
|
11172
|
+
changed = true;
|
|
11173
|
+
}
|
|
11174
|
+
if (!changed) return;
|
|
11175
|
+
this.updatePolling();
|
|
11176
|
+
this.emitChange();
|
|
11177
|
+
}
|
|
11178
|
+
/** Sondea sólo con el acceso encendido: apagado, esta instancia no hace nada nuevo. */
|
|
11179
|
+
updatePolling() {
|
|
11180
|
+
const wanted = this.remote().enabled;
|
|
11181
|
+
if (wanted && this.pollTimer === null) {
|
|
11182
|
+
this.pollTimer = setInterval(() => void this.sync().catch(() => void 0), FILES_POLL_MS);
|
|
11183
|
+
this.pollTimer.unref();
|
|
11184
|
+
} else if (!wanted && this.pollTimer !== null) {
|
|
11185
|
+
clearInterval(this.pollTimer);
|
|
11186
|
+
this.pollTimer = null;
|
|
11187
|
+
}
|
|
11188
|
+
}
|
|
11189
|
+
/** El acceso dejó de estar encendido: el código muere y se avisa cerrar las ventanas de todos. */
|
|
11190
|
+
cutOff() {
|
|
11191
|
+
this.stopPairing();
|
|
11192
|
+
this.log.log("[remote] remote access turned off: paired devices are disconnected");
|
|
11193
|
+
for (const listener of this.revokedListeners) listener(null);
|
|
11194
|
+
}
|
|
11195
|
+
// ---- Cambios desde el anfitrión ----------------------------------------------
|
|
11196
|
+
/** Lanza un `ServerTextError` si el puerto no sirve; los ajustes quedan como estaban. */
|
|
11197
|
+
async configure(patch) {
|
|
11198
|
+
if (patch.port !== void 0 && !isRemotePort(patch.port)) {
|
|
11199
|
+
throw new ServerTextError(serverText("remotePortInvalid", { max: REMOTE_MAX_PORT, min: REMOTE_MIN_PORT }));
|
|
11200
|
+
}
|
|
11201
|
+
await this.sync();
|
|
11202
|
+
const wasAccepting = this.accepting();
|
|
11203
|
+
await this.settings.update({ remote: patch });
|
|
11204
|
+
this.stamps.settings = await this.settings.stamp();
|
|
11205
|
+
if (wasAccepting && !this.accepting()) this.cutOff();
|
|
11206
|
+
this.updatePolling();
|
|
11207
|
+
this.emitChange();
|
|
11208
|
+
}
|
|
11209
|
+
startPairing() {
|
|
11210
|
+
if (!this.accepting()) throw new ServerTextError(serverText("remoteNotActive"));
|
|
11211
|
+
if (this.book.size >= REMOTE_MAX_DEVICES) {
|
|
11212
|
+
throw new ServerTextError(serverText("remoteTooManyDevices", { max: REMOTE_MAX_DEVICES }));
|
|
11213
|
+
}
|
|
11214
|
+
this.stopPairing();
|
|
11215
|
+
const pairing = this.desk.start();
|
|
11216
|
+
this.pairingTimer = setTimeout(() => {
|
|
11217
|
+
this.pairingTimer = null;
|
|
11218
|
+
this.emitChange();
|
|
11219
|
+
}, Math.max(0, pairing.expiresAt - this.now()) + 50);
|
|
11220
|
+
this.pairingTimer.unref();
|
|
11221
|
+
this.emitChange();
|
|
11222
|
+
return pairing;
|
|
11223
|
+
}
|
|
11224
|
+
cancelPairing() {
|
|
11225
|
+
if (!this.desk.isActive()) return;
|
|
11226
|
+
this.stopPairing();
|
|
11227
|
+
this.emitChange();
|
|
11228
|
+
}
|
|
11229
|
+
/**
|
|
11230
|
+
* Empareja un teléfono (hito 38, §15): un código de un solo uso y la llave SSH
|
|
11231
|
+
* que va con él en el QR. La llave es la misma mientras no se cancele: si el
|
|
11232
|
+
* código vence antes de que el teléfono lo use, el usuario pide otro y no
|
|
11233
|
+
* tiene que autorizar otra llave.
|
|
11234
|
+
*
|
|
11235
|
+
* La dirección lleva el puerto en el que escucha este arranque, que es el que
|
|
11236
|
+
* el teléfono va a reenviar.
|
|
11237
|
+
*/
|
|
11238
|
+
async startPhonePairing() {
|
|
11239
|
+
const pairing = this.startPairing();
|
|
11240
|
+
this.phoneSeed ??= this.randomBytes(PHONE_SEED_BYTES);
|
|
11241
|
+
const seed = this.phoneSeed;
|
|
11242
|
+
this.authorizeShellCache ??= this.authorizeShell();
|
|
11243
|
+
const shell = await this.authorizeShellCache;
|
|
11244
|
+
const host = this.host ?? detectHostInfo();
|
|
11245
|
+
const [pcName = "", ...addresses] = host.hostNames;
|
|
11246
|
+
const appPort = this.listening.port;
|
|
11247
|
+
return {
|
|
11248
|
+
payload: phonePairingPayload({
|
|
11249
|
+
pcName,
|
|
11250
|
+
hosts: addresses.length > 0 ? addresses : [pcName],
|
|
11251
|
+
sshPort: PHONE_SSH_PORT,
|
|
11252
|
+
user: host.user,
|
|
11253
|
+
appPort,
|
|
11254
|
+
code: pairing.code.replace("-", ""),
|
|
11255
|
+
seed
|
|
11256
|
+
}),
|
|
11257
|
+
authorizeCommand: phoneAuthorizeCommand(phoneAuthorizedKeysEntry(ed25519PublicKey(seed), appPort), shell),
|
|
11258
|
+
shell,
|
|
11259
|
+
code: pairing.code,
|
|
11260
|
+
expiresAt: pairing.expiresAt
|
|
11261
|
+
};
|
|
11262
|
+
}
|
|
11263
|
+
/** Olvida la llave del teléfono y da de baja el código. */
|
|
11264
|
+
cancelPhonePairing() {
|
|
11265
|
+
this.phoneSeed = null;
|
|
11266
|
+
this.cancelPairing();
|
|
11267
|
+
}
|
|
11268
|
+
/** Para el chequeo: si todavía hay una llave de teléfono en memoria. */
|
|
11269
|
+
get phoneKeyHeld() {
|
|
11270
|
+
return this.phoneSeed !== null;
|
|
11271
|
+
}
|
|
11272
|
+
stopPairing() {
|
|
11273
|
+
this.desk.cancel();
|
|
11274
|
+
if (this.pairingTimer !== null) clearTimeout(this.pairingTimer);
|
|
11275
|
+
this.pairingTimer = null;
|
|
11276
|
+
}
|
|
11277
|
+
async renameDevice(deviceId, label) {
|
|
11278
|
+
await this.sync();
|
|
11279
|
+
if (!this.book.rename(deviceId, label)) return false;
|
|
11280
|
+
await this.persist();
|
|
11281
|
+
this.emitChange();
|
|
11282
|
+
return true;
|
|
11283
|
+
}
|
|
11284
|
+
async revokeDevice(deviceId) {
|
|
11285
|
+
await this.sync();
|
|
11286
|
+
if (!this.book.revoke(deviceId)) return false;
|
|
11287
|
+
this.connections.delete(deviceId);
|
|
11288
|
+
this.log.log("[remote] a paired device was revoked");
|
|
11289
|
+
for (const listener of this.revokedListeners) listener(deviceId);
|
|
11290
|
+
await this.persist();
|
|
11291
|
+
this.emitChange();
|
|
11292
|
+
return true;
|
|
11293
|
+
}
|
|
11294
|
+
// ---- Lo que llega de afuera --------------------------------------------------
|
|
11295
|
+
/**
|
|
11296
|
+
* Canjea un código por un equipo nuevo, o null. El nombre sale del navegador
|
|
11297
|
+
* que lo presentó; el usuario lo cambia después.
|
|
11298
|
+
*/
|
|
11299
|
+
async redeemPairing(input, userAgent) {
|
|
11300
|
+
if (!this.accepting()) return null;
|
|
11301
|
+
const outcome = this.desk.redeem(input);
|
|
11302
|
+
if (outcome === "none") return null;
|
|
11303
|
+
if (outcome === "wrong") {
|
|
11304
|
+
this.log.warn("[remote] a wrong pairing code was presented");
|
|
11305
|
+
if (!this.desk.isActive()) {
|
|
11306
|
+
this.stopPairing();
|
|
11307
|
+
this.log.warn("[remote] too many wrong codes: the pairing code is no longer valid");
|
|
11308
|
+
this.emitChange();
|
|
11309
|
+
}
|
|
11310
|
+
return null;
|
|
11311
|
+
}
|
|
11312
|
+
this.stopPairing();
|
|
11313
|
+
this.phoneSeed = null;
|
|
11314
|
+
await this.sync();
|
|
11315
|
+
const enrolled = this.accepting() ? this.book.enroll(deviceLabelFromUserAgent(userAgent)) : null;
|
|
11316
|
+
if (enrolled === null) {
|
|
11317
|
+
this.emitChange();
|
|
11318
|
+
return null;
|
|
11319
|
+
}
|
|
11320
|
+
await this.persist();
|
|
11321
|
+
this.log.log(`[remote] paired a new device: ${enrolled.device.label}`);
|
|
11322
|
+
this.emitChange();
|
|
11323
|
+
return { deviceId: enrolled.device.id, credential: enrolled.credential };
|
|
11324
|
+
}
|
|
11325
|
+
noteConnected(deviceId) {
|
|
11326
|
+
this.connections.set(deviceId, (this.connections.get(deviceId) ?? 0) + 1);
|
|
11327
|
+
this.touch(deviceId);
|
|
11328
|
+
this.emitChange();
|
|
11329
|
+
}
|
|
11330
|
+
noteDisconnected(deviceId) {
|
|
11331
|
+
const left = (this.connections.get(deviceId) ?? 0) - 1;
|
|
11332
|
+
if (left > 0) this.connections.set(deviceId, left);
|
|
11333
|
+
else this.connections.delete(deviceId);
|
|
11334
|
+
this.touch(deviceId);
|
|
11335
|
+
this.emitChange();
|
|
11336
|
+
}
|
|
11337
|
+
/** La "última vez visto" no vale una escritura por conexión: se junta. */
|
|
11338
|
+
touch(deviceId) {
|
|
11339
|
+
const at = this.now();
|
|
11340
|
+
if (!this.book.touch(deviceId, at)) return;
|
|
11341
|
+
this.pendingSeen.set(deviceId, at);
|
|
11342
|
+
if (this.touchTimer !== null) return;
|
|
11343
|
+
this.touchTimer = setTimeout(() => {
|
|
11344
|
+
this.touchTimer = null;
|
|
11345
|
+
void this.persistTouches();
|
|
11346
|
+
}, TOUCH_SAVE_DELAY_MS);
|
|
11347
|
+
this.touchTimer.unref();
|
|
11348
|
+
}
|
|
11349
|
+
/**
|
|
11350
|
+
* Guarda las "última vez visto". Relee antes: una fecha no puede devolverle
|
|
11351
|
+
* la vida a un equipo que otra instancia revocó mientras tanto —al releer, el
|
|
11352
|
+
* que ya no está no recibe la suya—.
|
|
11353
|
+
*/
|
|
11354
|
+
async persistTouches() {
|
|
11355
|
+
if (this.pendingSeen.size === 0) return;
|
|
11356
|
+
await this.sync();
|
|
11357
|
+
this.pendingSeen.clear();
|
|
11358
|
+
await this.persist();
|
|
11359
|
+
}
|
|
11360
|
+
// ---- Avisos y disco ----------------------------------------------------------
|
|
11361
|
+
onChange(listener) {
|
|
11362
|
+
this.changeListeners.add(listener);
|
|
11363
|
+
return () => this.changeListeners.delete(listener);
|
|
11364
|
+
}
|
|
11365
|
+
/** Hay que cerrar las ventanas de ese equipo; con null, las de todos. */
|
|
11366
|
+
onRevoked(listener) {
|
|
11367
|
+
this.revokedListeners.add(listener);
|
|
11368
|
+
return () => this.revokedListeners.delete(listener);
|
|
11369
|
+
}
|
|
11370
|
+
emitChange() {
|
|
11371
|
+
for (const listener of this.changeListeners) listener();
|
|
11372
|
+
}
|
|
11373
|
+
/** Escribe los equipos y anota cómo quedó el archivo, para no tomarlo por un cambio ajeno. Nunca rechaza. */
|
|
11374
|
+
async persist() {
|
|
11375
|
+
try {
|
|
11376
|
+
await this.store.save(this.book.snapshot());
|
|
11377
|
+
this.stamps.devices = await this.store.stamp();
|
|
11378
|
+
} catch (error) {
|
|
11379
|
+
this.log.warn("[remote] couldn't save the paired devices:", error instanceof Error ? error.message : String(error));
|
|
11380
|
+
}
|
|
11381
|
+
}
|
|
11382
|
+
/** Escribe lo pendiente y espera. Nunca rechaza. */
|
|
11383
|
+
async flush() {
|
|
11384
|
+
if (this.touchTimer !== null) clearTimeout(this.touchTimer);
|
|
11385
|
+
this.touchTimer = null;
|
|
11386
|
+
await this.persistTouches().catch(() => void 0);
|
|
11387
|
+
await this.store.flush();
|
|
11388
|
+
}
|
|
11389
|
+
/** Frena los relojes. Lo que quede por guardar lo escribe `flush()`. */
|
|
11390
|
+
dispose() {
|
|
11391
|
+
this.stopPairing();
|
|
11392
|
+
this.phoneSeed = null;
|
|
11393
|
+
if (this.pollTimer !== null) clearInterval(this.pollTimer);
|
|
11394
|
+
this.pollTimer = null;
|
|
11395
|
+
}
|
|
11396
|
+
};
|
|
11397
|
+
|
|
11398
|
+
// packages/server/src/remote-devices-store.ts
|
|
11399
|
+
import { chmod, mkdir as mkdir6, readFile as readFile9, stat as stat16 } from "node:fs/promises";
|
|
11400
|
+
import path31 from "node:path";
|
|
11401
|
+
|
|
11402
|
+
// packages/server/src/vault/write.ts
|
|
11403
|
+
import { randomBytes as randomBytes2 } from "node:crypto";
|
|
11404
|
+
import { mkdir as mkdir5, rename as rename3, rm as rm2, stat as stat15, writeFile as writeFile3 } from "node:fs/promises";
|
|
11405
|
+
|
|
11406
|
+
// packages/server/src/vault/paths.ts
|
|
11407
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
11408
|
+
import path30 from "node:path";
|
|
11409
|
+
function defaultVaultDir() {
|
|
11410
|
+
return path30.join(appConfigDir(), "vault");
|
|
11411
|
+
}
|
|
11412
|
+
function vaultMarkerFile(dir) {
|
|
11413
|
+
return path30.join(dir, "vault.json");
|
|
11414
|
+
}
|
|
11415
|
+
function sessionsRoot2(dir) {
|
|
11416
|
+
return path30.join(dir, "sessions");
|
|
11417
|
+
}
|
|
11418
|
+
function isSafeSessionId(sessionId) {
|
|
11419
|
+
return isVaultSessionId(sessionId);
|
|
11420
|
+
}
|
|
11421
|
+
function isSessionAgentId(value) {
|
|
11422
|
+
return SESSION_AGENT_IDS.includes(value);
|
|
11423
|
+
}
|
|
11424
|
+
function assertSessionKey(agent, sessionId) {
|
|
11425
|
+
if (!isSessionAgentId(agent)) throw new Error(`Unknown source for the local copy: ${JSON.stringify(agent)}`);
|
|
11426
|
+
if (!isSafeSessionId(sessionId)) throw new Error(`Session id that isn't copied: ${JSON.stringify(sessionId)}`);
|
|
11427
|
+
}
|
|
11428
|
+
function agentSessionsDir(dir, agent) {
|
|
11429
|
+
if (!isSessionAgentId(agent)) throw new Error(`Unknown source for the local copy: ${JSON.stringify(agent)}`);
|
|
11430
|
+
return path30.join(sessionsRoot2(dir), agent);
|
|
11431
|
+
}
|
|
11432
|
+
function sessionFile(dir, agent, sessionId) {
|
|
11433
|
+
assertSessionKey(agent, sessionId);
|
|
11434
|
+
return path30.join(sessionsRoot2(dir), agent, `${sessionId}.jsonl`);
|
|
11435
|
+
}
|
|
11436
|
+
function assetsDir(dir, agent, sessionId) {
|
|
11437
|
+
assertSessionKey(agent, sessionId);
|
|
11438
|
+
return path30.join(sessionsRoot2(dir), agent, `${sessionId}.assets`);
|
|
11439
|
+
}
|
|
11440
|
+
function assetFile(dir, agent, sessionId, asset) {
|
|
11441
|
+
if (!VAULT_ASSET_NAME_PATTERN.test(asset)) throw new Error(`Invalid asset name: ${JSON.stringify(asset)}`);
|
|
11442
|
+
return path30.join(assetsDir(dir, agent, sessionId), asset);
|
|
11443
|
+
}
|
|
11444
|
+
function projectFolderName(cwd, platform) {
|
|
11445
|
+
const hash = createHash2("sha256").update(normalizeCwdKey(cwd, platform)).digest("hex").slice(0, 8);
|
|
11446
|
+
const base = (platform === "win32" ? path30.win32 : path30.posix).basename(cwd);
|
|
11447
|
+
const name = base.length === 0 ? "proyecto" : sanitizeFileName(base, 60);
|
|
11448
|
+
return `${name}-${hash}`;
|
|
11449
|
+
}
|
|
11450
|
+
function memoryDir(dir, cwd, platform) {
|
|
11451
|
+
return path30.join(dir, "memory", projectFolderName(cwd, platform));
|
|
11452
|
+
}
|
|
11453
|
+
function projectExportDir(dir, cwd, platform) {
|
|
11454
|
+
return path30.join(dir, "export", projectFolderName(cwd, platform));
|
|
11455
|
+
}
|
|
11456
|
+
function projectExportDirFor(dir, project, platform) {
|
|
11457
|
+
if (project.cwd.length > 0) return projectExportDir(dir, project.cwd, platform);
|
|
11458
|
+
const hash = createHash2("sha256").update(project.key).digest("hex").slice(0, 8);
|
|
11459
|
+
const name = project.fallbackName.length > 0 ? sanitizeFileName(project.fallbackName, 60) : "proyecto";
|
|
11460
|
+
return path30.join(dir, "export", `${name}-${hash}`);
|
|
11461
|
+
}
|
|
11462
|
+
function sessionExportFile(dir, agent, sessionId) {
|
|
11463
|
+
assertSessionKey(agent, sessionId);
|
|
11464
|
+
return path30.join(dir, "export", "sesiones", `${agent}-${sessionId}.md`);
|
|
11465
|
+
}
|
|
11466
|
+
var RESERVED_WINDOWS_NAME = /^(con|prn|aux|nul|com[1-9]|lpt[1-9])$/i;
|
|
11467
|
+
function sanitizeFileName(text, max = 80) {
|
|
11468
|
+
const limit = Math.max(1, Math.floor(max));
|
|
11469
|
+
const kept = text.normalize("NFC").replace(/[^\p{L}\p{M}\p{N} ._-]/gu, "_");
|
|
11470
|
+
const trimEnds = (value) => value.replace(/^ +/u, "").replace(/[. ]+$/u, "");
|
|
11471
|
+
let name = trimEnds(Array.from(trimEnds(kept)).slice(0, limit).join(""));
|
|
11472
|
+
if (name.length === 0) return "_";
|
|
11473
|
+
const stem = (name.split(".")[0] ?? "").replace(/ +$/u, "");
|
|
11474
|
+
if (RESERVED_WINDOWS_NAME.test(stem)) {
|
|
11475
|
+
name = `_${Array.from(name).slice(0, limit - 1).join("")}`;
|
|
11476
|
+
name = name.replace(/[. ]+$/u, "");
|
|
11477
|
+
}
|
|
11478
|
+
return name;
|
|
11479
|
+
}
|
|
11480
|
+
|
|
11481
|
+
// packages/server/src/vault/write.ts
|
|
11482
|
+
var RENAME_RETRIES = 3;
|
|
11483
|
+
var RENAME_RETRY_MS = 100;
|
|
11484
|
+
var DEFAULT_WRITE_DEPS = {
|
|
11485
|
+
rename: rename3,
|
|
11486
|
+
platform: process.platform,
|
|
11487
|
+
delay: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
|
|
11488
|
+
now: () => Date.now()
|
|
11489
|
+
};
|
|
11490
|
+
function temporaryPathFor(target) {
|
|
11491
|
+
return `${target}.${process.pid}.${randomBytes2(6).toString("hex")}.tmp`;
|
|
11492
|
+
}
|
|
11493
|
+
function isRetryableRename(error, platform) {
|
|
11494
|
+
if (platform !== "win32") return false;
|
|
11495
|
+
const code = error?.code;
|
|
11496
|
+
return code === "EPERM" || code === "EBUSY";
|
|
11497
|
+
}
|
|
11498
|
+
async function writeFileAtomic(target, data, deps = DEFAULT_WRITE_DEPS) {
|
|
11499
|
+
const temporary = temporaryPathFor(target);
|
|
11500
|
+
try {
|
|
11501
|
+
await writeFile3(temporary, data);
|
|
11502
|
+
for (let attempt = 0; ; attempt += 1) {
|
|
11503
|
+
try {
|
|
11504
|
+
await deps.rename(temporary, target);
|
|
11505
|
+
return;
|
|
11506
|
+
} catch (error) {
|
|
11507
|
+
if (attempt >= RENAME_RETRIES || !isRetryableRename(error, deps.platform)) throw error;
|
|
11508
|
+
await deps.delay(RENAME_RETRY_MS);
|
|
11509
|
+
}
|
|
11510
|
+
}
|
|
11511
|
+
} catch (error) {
|
|
11512
|
+
await rm2(temporary, { force: true }).catch(() => void 0);
|
|
11513
|
+
throw error;
|
|
11514
|
+
}
|
|
11515
|
+
}
|
|
11516
|
+
async function ensureVaultMarker(dir, deps = DEFAULT_WRITE_DEPS) {
|
|
11517
|
+
const marker = vaultMarkerFile(dir);
|
|
11518
|
+
try {
|
|
11519
|
+
await stat15(marker);
|
|
11520
|
+
return;
|
|
11521
|
+
} catch {
|
|
11522
|
+
}
|
|
11523
|
+
await mkdir5(dir, { recursive: true });
|
|
11524
|
+
await writeFileAtomic(marker, `${JSON.stringify({ format: VAULT_FORMAT, createdAt: deps.now() })}
|
|
11525
|
+
`, deps);
|
|
11526
|
+
}
|
|
11527
|
+
async function assetPresent(file, size) {
|
|
11528
|
+
try {
|
|
11529
|
+
const info = await stat15(file);
|
|
11530
|
+
return info.isFile() && info.size === size;
|
|
11531
|
+
} catch {
|
|
11532
|
+
return false;
|
|
11533
|
+
}
|
|
11534
|
+
}
|
|
11535
|
+
async function writeSessionFile(dir, serialized, deps = DEFAULT_WRITE_DEPS) {
|
|
11536
|
+
const { agent, sessionId } = serialized.header;
|
|
11537
|
+
const file = sessionFile(dir, agent, sessionId);
|
|
11538
|
+
await ensureVaultMarker(dir, deps);
|
|
11539
|
+
await mkdir5(agentSessionsDir(dir, agent), { recursive: true });
|
|
11540
|
+
let assetsWritten = 0;
|
|
11541
|
+
let assetBytesWritten = 0;
|
|
11542
|
+
if (serialized.assets.size > 0) {
|
|
11543
|
+
await mkdir5(assetsDir(dir, agent, sessionId), { recursive: true });
|
|
11544
|
+
for (const [name, bytes] of serialized.assets) {
|
|
11545
|
+
const target = assetFile(dir, agent, sessionId, name);
|
|
11546
|
+
if (await assetPresent(target, bytes.length)) continue;
|
|
11547
|
+
await writeFileAtomic(target, bytes, deps);
|
|
11548
|
+
assetsWritten += 1;
|
|
11549
|
+
assetBytesWritten += bytes.length;
|
|
11550
|
+
}
|
|
11551
|
+
}
|
|
11552
|
+
const text = Buffer.from(serialized.text, "utf8");
|
|
11553
|
+
await writeFileAtomic(file, text, deps);
|
|
11554
|
+
return { file, bytes: text.length, assetsWritten, assetBytesWritten };
|
|
11555
|
+
}
|
|
11556
|
+
|
|
11557
|
+
// packages/server/src/remote-devices-store.ts
|
|
11558
|
+
var RemoteDevicesStore = class {
|
|
11559
|
+
/** La ruta y el aviso son inyectables para las pruebas. */
|
|
11560
|
+
constructor(filePath = remoteDevicesPath(), log = console) {
|
|
11561
|
+
this.filePath = filePath;
|
|
11562
|
+
this.log = log;
|
|
11563
|
+
}
|
|
11564
|
+
filePath;
|
|
11565
|
+
log;
|
|
11566
|
+
/** Las escrituras, en fila: dos seguidas no se pisan. */
|
|
11567
|
+
queue = Promise.resolve();
|
|
11568
|
+
async load() {
|
|
11569
|
+
let raw;
|
|
11570
|
+
try {
|
|
11571
|
+
raw = await readFile9(this.filePath, "utf8");
|
|
11572
|
+
} catch {
|
|
11573
|
+
return [];
|
|
11574
|
+
}
|
|
11575
|
+
let parsed;
|
|
11576
|
+
try {
|
|
11577
|
+
parsed = JSON.parse(raw);
|
|
11578
|
+
} catch {
|
|
11579
|
+
this.log.warn("[remote] remote-devices.json isn't valid JSON: no paired devices are loaded");
|
|
11580
|
+
return [];
|
|
11581
|
+
}
|
|
11582
|
+
const devices = parseRemoteDevicesFile(parsed);
|
|
11583
|
+
if (devices === null) {
|
|
11584
|
+
this.log.warn("[remote] remote-devices.json is from another version: no paired devices are loaded");
|
|
11585
|
+
return [];
|
|
11586
|
+
}
|
|
11587
|
+
return devices;
|
|
11588
|
+
}
|
|
11589
|
+
save(devices) {
|
|
11590
|
+
const text = serializeRemoteDevices(devices);
|
|
11591
|
+
const run = async () => {
|
|
11592
|
+
await mkdir6(path31.dirname(this.filePath), { recursive: true });
|
|
11593
|
+
await writeFileAtomic(this.filePath, text);
|
|
11594
|
+
await chmod(this.filePath, 384).catch(() => void 0);
|
|
11595
|
+
};
|
|
11596
|
+
const result = this.queue.then(run);
|
|
11597
|
+
this.queue = result.catch(() => void 0);
|
|
11598
|
+
return result;
|
|
11599
|
+
}
|
|
11600
|
+
/** Espera lo que quede por escribir. Nunca rechaza. */
|
|
11601
|
+
flush() {
|
|
11602
|
+
return this.queue;
|
|
11603
|
+
}
|
|
11604
|
+
/**
|
|
11605
|
+
* Una marca que cambia cuando el archivo cambia (fecha y tamaño), o '' si no
|
|
11606
|
+
* está. Es lo que mira otra instancia de la app para enterarse de que acá se
|
|
11607
|
+
* revocó un equipo: comparten la carpeta de configuración.
|
|
11608
|
+
*/
|
|
11609
|
+
async stamp() {
|
|
11610
|
+
try {
|
|
11611
|
+
const info = await stat16(this.filePath);
|
|
11612
|
+
return `${info.mtimeMs}:${info.size}`;
|
|
11613
|
+
} catch {
|
|
11614
|
+
return "";
|
|
11615
|
+
}
|
|
11616
|
+
}
|
|
11617
|
+
};
|
|
11618
|
+
|
|
11619
|
+
// packages/server/src/security.ts
|
|
11620
|
+
import { randomBytes as randomBytes3, timingSafeEqual as timingSafeEqual2 } from "node:crypto";
|
|
11621
|
+
var LOOPBACK_HOST = "127.0.0.1";
|
|
11622
|
+
var ALLOWED_HOSTNAMES = /* @__PURE__ */ new Set(["127.0.0.1", "localhost", "[::1]", "::1"]);
|
|
11623
|
+
var TOKEN_HEADER = "x-agent-workbench-token";
|
|
11624
|
+
var TOKEN_COOKIE = "agent_workbench_token";
|
|
11625
|
+
function createSessionToken() {
|
|
11626
|
+
return randomBytes3(32).toString("base64url");
|
|
11627
|
+
}
|
|
11628
|
+
function constantTimeEquals(a, b) {
|
|
11629
|
+
const bufferA = Buffer.from(a, "utf8");
|
|
11630
|
+
const bufferB = Buffer.from(b, "utf8");
|
|
11631
|
+
if (bufferA.length !== bufferB.length) return false;
|
|
11632
|
+
return timingSafeEqual2(bufferA, bufferB);
|
|
11633
|
+
}
|
|
11634
|
+
function requestUrl(request) {
|
|
11635
|
+
try {
|
|
11636
|
+
return new URL(request.url ?? "/", `http://${request.headers.host ?? LOOPBACK_HOST}`);
|
|
11637
|
+
} catch {
|
|
11638
|
+
return null;
|
|
11639
|
+
}
|
|
11640
|
+
}
|
|
11641
|
+
function readCookie(request, name) {
|
|
11642
|
+
const header = request.headers.cookie;
|
|
11643
|
+
if (typeof header !== "string") return null;
|
|
11644
|
+
for (const pair of header.split(";")) {
|
|
11645
|
+
const separator = pair.indexOf("=");
|
|
11646
|
+
if (separator === -1) continue;
|
|
11647
|
+
if (pair.slice(0, separator).trim() !== name) continue;
|
|
11648
|
+
const value = pair.slice(separator + 1).trim();
|
|
11649
|
+
return value.length > 0 ? decodeURIComponent(value) : null;
|
|
11650
|
+
}
|
|
11651
|
+
return null;
|
|
11652
|
+
}
|
|
11653
|
+
function matchToken(request, expected) {
|
|
11654
|
+
const url = requestUrl(request);
|
|
11655
|
+
const fromQuery = url?.searchParams.get(TOKEN_QUERY_PARAM) ?? null;
|
|
11656
|
+
if (fromQuery !== null && constantTimeEquals(fromQuery, expected)) {
|
|
11657
|
+
return { source: "query" };
|
|
11658
|
+
}
|
|
11659
|
+
const fromHeader = request.headers[TOKEN_HEADER];
|
|
11660
|
+
if (typeof fromHeader === "string" && constantTimeEquals(fromHeader, expected)) {
|
|
11661
|
+
return { source: "header" };
|
|
11662
|
+
}
|
|
11663
|
+
const fromCookie = readCookie(request, TOKEN_COOKIE);
|
|
11664
|
+
if (fromCookie !== null && constantTimeEquals(fromCookie, expected)) {
|
|
11665
|
+
return { source: "cookie" };
|
|
11666
|
+
}
|
|
11667
|
+
return null;
|
|
11668
|
+
}
|
|
11669
|
+
function buildTokenCookie(token) {
|
|
11670
|
+
return `${TOKEN_COOKIE}=${encodeURIComponent(token)}; Path=/; SameSite=Strict; HttpOnly`;
|
|
11671
|
+
}
|
|
11672
|
+
var DEVICE_COOKIE = "agent_workbench_device";
|
|
11673
|
+
var DEVICE_COOKIE_MAX_AGE_S = 400 * 24 * 60 * 60;
|
|
11674
|
+
function buildDeviceCookie(credential) {
|
|
11675
|
+
return `${DEVICE_COOKIE}=${encodeURIComponent(credential)}; Path=/; Max-Age=${DEVICE_COOKIE_MAX_AGE_S}; SameSite=Strict; HttpOnly`;
|
|
11676
|
+
}
|
|
11677
|
+
function matchCredential(request, token, devices) {
|
|
11678
|
+
const session = matchToken(request, token);
|
|
11679
|
+
if (session !== null) return { kind: "session", source: session.source };
|
|
11680
|
+
if (devices === null) return null;
|
|
11681
|
+
const credential = readCookie(request, DEVICE_COOKIE);
|
|
11682
|
+
if (credential === null) return null;
|
|
11683
|
+
const device = devices.match(credential);
|
|
11684
|
+
return device === null ? null : { kind: "device", deviceId: device.id, credential };
|
|
11685
|
+
}
|
|
11686
|
+
function readPairingCode(request) {
|
|
11687
|
+
const code = requestUrl(request)?.searchParams.get(PAIR_QUERY_PARAM) ?? null;
|
|
11688
|
+
return code !== null && code.length > 0 ? code : null;
|
|
11689
|
+
}
|
|
11690
|
+
function hasValidHost(request, port) {
|
|
11691
|
+
const host = request.headers.host;
|
|
11692
|
+
if (typeof host !== "string") return false;
|
|
11693
|
+
const separator = host.lastIndexOf(":");
|
|
11694
|
+
const hostname = separator === -1 ? host : host.slice(0, separator);
|
|
11695
|
+
const hostPort = separator === -1 ? "" : host.slice(separator + 1);
|
|
11696
|
+
if (!ALLOWED_HOSTNAMES.has(hostname)) return false;
|
|
11697
|
+
return hostPort === "" || hostPort === String(port);
|
|
11698
|
+
}
|
|
11699
|
+
function hasValidOrigin(request, port) {
|
|
11700
|
+
const origin = request.headers.origin;
|
|
11701
|
+
if (typeof origin !== "string" || origin.length === 0) return true;
|
|
11702
|
+
let parsed;
|
|
11703
|
+
try {
|
|
11704
|
+
parsed = new URL(origin);
|
|
11705
|
+
} catch {
|
|
11706
|
+
return false;
|
|
11707
|
+
}
|
|
11708
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return false;
|
|
11709
|
+
if (!ALLOWED_HOSTNAMES.has(parsed.hostname)) return false;
|
|
11710
|
+
return parsed.port === String(port);
|
|
11711
|
+
}
|
|
11712
|
+
function authorizeRequest(request, port, token, devices) {
|
|
11713
|
+
if (!hasValidHost(request, port)) return { ok: false, reason: "bad-host" };
|
|
11714
|
+
if (!hasValidOrigin(request, port)) return { ok: false, reason: "bad-origin" };
|
|
11715
|
+
const credential = matchCredential(request, token, devices);
|
|
11716
|
+
return credential === null ? { ok: false, reason: "bad-token" } : { ok: true, credential };
|
|
11717
|
+
}
|
|
11718
|
+
|
|
11719
|
+
// packages/server/src/repo-hub.ts
|
|
11720
|
+
import { EventEmitter as EventEmitter2 } from "node:events";
|
|
11721
|
+
import chokidar from "chokidar";
|
|
11722
|
+
|
|
11723
|
+
// packages/server/src/git-repo.ts
|
|
11724
|
+
import { execFile as execFile4 } from "node:child_process";
|
|
11725
|
+
import { readFile as readFile10 } from "node:fs/promises";
|
|
11726
|
+
import path32 from "node:path";
|
|
11727
|
+
function gitOutputText(output, command) {
|
|
11728
|
+
return output.length > 0 ? serverText("raw", { text: output }) : serverText("gitCommandFailed", { command });
|
|
11729
|
+
}
|
|
11730
|
+
var MAX_CHANGES = 2e3;
|
|
11731
|
+
var MAX_DIFF_LINES = 4e3;
|
|
11732
|
+
var MAX_DIFF_BYTES = 512 * 1024;
|
|
11733
|
+
var GIT_TIMEOUT_MS = 15e3;
|
|
11734
|
+
var GIT_MAX_BUFFER = 32 * 1024 * 1024;
|
|
11735
|
+
function isMissingBinary(error) {
|
|
11736
|
+
return typeof error === "object" && error !== null && error.code === "ENOENT";
|
|
11737
|
+
}
|
|
11738
|
+
var GitMissingError = class extends Error {
|
|
11739
|
+
constructor() {
|
|
11740
|
+
super("git wasn't found in the PATH.");
|
|
11741
|
+
this.name = "GitMissingError";
|
|
11742
|
+
}
|
|
11743
|
+
};
|
|
11744
|
+
function runGit(cwd, args) {
|
|
11745
|
+
return new Promise((resolve, reject) => {
|
|
11746
|
+
execFile4(
|
|
11747
|
+
"git",
|
|
11748
|
+
[
|
|
11749
|
+
// Sin esto git escapa los no-ASCII en octal y un archivo con acento
|
|
11750
|
+
// vuelve ilegible.
|
|
11751
|
+
"-c",
|
|
11752
|
+
"core.quotepath=false",
|
|
11753
|
+
// Esta app corre `git status` sola, cada pocos segundos, mientras el
|
|
11754
|
+
// usuario trabaja. Sin esta bandera `status` refresca y **escribe** el
|
|
11755
|
+
// indice, y eso trae dos problemas: toma `index.lock`, con lo que puede
|
|
11756
|
+
// hacer fallar un `git add` que el usuario esta escribiendo en la
|
|
11757
|
+
// terminal, y modifica `.git/index`, que es justo lo que observa
|
|
11758
|
+
// nuestro watcher — o sea que cada lectura se dispara a si misma.
|
|
11759
|
+
"--no-optional-locks",
|
|
11760
|
+
...args
|
|
11761
|
+
],
|
|
11762
|
+
{
|
|
11763
|
+
cwd,
|
|
11764
|
+
timeout: GIT_TIMEOUT_MS,
|
|
11765
|
+
maxBuffer: GIT_MAX_BUFFER,
|
|
11766
|
+
windowsHide: true,
|
|
11767
|
+
encoding: "utf8"
|
|
11768
|
+
},
|
|
11769
|
+
(error, stdout, stderr) => {
|
|
11770
|
+
if (error !== null && isMissingBinary(error)) {
|
|
11771
|
+
reject(new GitMissingError());
|
|
11772
|
+
return;
|
|
11773
|
+
}
|
|
11774
|
+
const code = error !== null && typeof error.code === "number" ? error.code : error !== null ? 1 : 0;
|
|
11775
|
+
resolve({ stdout, stderr, code });
|
|
11776
|
+
}
|
|
11777
|
+
);
|
|
11778
|
+
});
|
|
11779
|
+
}
|
|
11780
|
+
function toPosix(value) {
|
|
11781
|
+
return value.split("\\").join("/");
|
|
11782
|
+
}
|
|
11783
|
+
async function resolveRepo(cwd) {
|
|
11784
|
+
const result = await runGit(cwd, ["rev-parse", "--show-toplevel"]);
|
|
11785
|
+
if (result.code === 0) {
|
|
11786
|
+
const root = result.stdout.trim();
|
|
11787
|
+
if (root.length > 0) return { state: "ready", root: path32.resolve(root), message: null };
|
|
11788
|
+
}
|
|
11789
|
+
const stderr = result.stderr.trim();
|
|
11790
|
+
if (/not a git repository|no es un repositorio/i.test(stderr)) {
|
|
11791
|
+
return { state: "not-a-repo", root: "", message: null };
|
|
11792
|
+
}
|
|
11793
|
+
if (stderr.length === 0 && result.code !== 0) {
|
|
11794
|
+
return {
|
|
11795
|
+
state: "error",
|
|
11796
|
+
root: "",
|
|
11797
|
+
message: serverText("gitRunFailed")
|
|
11798
|
+
};
|
|
11799
|
+
}
|
|
11800
|
+
return {
|
|
11801
|
+
state: "error",
|
|
11802
|
+
root: "",
|
|
11803
|
+
message: gitOutputText(stderr.slice(0, 400), "rev-parse")
|
|
11804
|
+
};
|
|
11805
|
+
}
|
|
11806
|
+
async function findRepoRoot(cwd) {
|
|
11807
|
+
const lookup = await resolveRepo(cwd);
|
|
11808
|
+
return lookup.state === "ready" ? lookup.root : null;
|
|
11809
|
+
}
|
|
11810
|
+
async function findGitDir(cwd) {
|
|
11811
|
+
const result = await runGit(cwd, ["rev-parse", "--absolute-git-dir"]);
|
|
11812
|
+
if (result.code !== 0) return null;
|
|
11813
|
+
const dir = result.stdout.trim();
|
|
11814
|
+
return dir.length > 0 ? path32.resolve(dir) : null;
|
|
11815
|
+
}
|
|
11816
|
+
function letterToKind(letter) {
|
|
11817
|
+
switch (letter) {
|
|
11818
|
+
case "A":
|
|
11819
|
+
return "added";
|
|
11820
|
+
case "M":
|
|
11821
|
+
return "modified";
|
|
11822
|
+
case "D":
|
|
11823
|
+
return "deleted";
|
|
11824
|
+
case "R":
|
|
11825
|
+
return "renamed";
|
|
11826
|
+
case "C":
|
|
11827
|
+
return "copied";
|
|
11828
|
+
case "T":
|
|
11829
|
+
return "type-changed";
|
|
11830
|
+
default:
|
|
11831
|
+
return "modified";
|
|
11832
|
+
}
|
|
11833
|
+
}
|
|
11834
|
+
function parsePorcelainV2(raw) {
|
|
11835
|
+
const fields = raw.split("\0");
|
|
11836
|
+
const parsed = {
|
|
11837
|
+
branch: null,
|
|
11838
|
+
head: null,
|
|
11839
|
+
upstream: null,
|
|
11840
|
+
ahead: 0,
|
|
11841
|
+
behind: 0,
|
|
10876
11842
|
changes: [],
|
|
10877
11843
|
truncated: false
|
|
10878
11844
|
};
|
|
@@ -10948,7 +11914,7 @@ function parseWorktrees(raw, currentRoot) {
|
|
|
10948
11914
|
worktrees.push({
|
|
10949
11915
|
path: current.path,
|
|
10950
11916
|
branch: current.branch,
|
|
10951
|
-
isCurrent:
|
|
11917
|
+
isCurrent: path32.resolve(current.path) === path32.resolve(currentRoot)
|
|
10952
11918
|
});
|
|
10953
11919
|
current = null;
|
|
10954
11920
|
};
|
|
@@ -11091,7 +12057,7 @@ function looksBinary(buffer) {
|
|
|
11091
12057
|
async function readUntrackedAsDiff(absolutePath, relativePath) {
|
|
11092
12058
|
let content;
|
|
11093
12059
|
try {
|
|
11094
|
-
content = await
|
|
12060
|
+
content = await readFile10(absolutePath);
|
|
11095
12061
|
} catch {
|
|
11096
12062
|
return {
|
|
11097
12063
|
path: relativePath,
|
|
@@ -11139,7 +12105,7 @@ async function readUntrackedAsDiff(absolutePath, relativePath) {
|
|
|
11139
12105
|
async function readDiff(options) {
|
|
11140
12106
|
const { repoRoot: repoRoot2, relativePath, staged, untracked } = options;
|
|
11141
12107
|
if (untracked && !staged) {
|
|
11142
|
-
return readUntrackedAsDiff(
|
|
12108
|
+
return readUntrackedAsDiff(path32.join(repoRoot2, relativePath), relativePath);
|
|
11143
12109
|
}
|
|
11144
12110
|
const args = [
|
|
11145
12111
|
"diff",
|
|
@@ -11178,7 +12144,7 @@ async function filterIgnored(repoRoot2, relativePaths) {
|
|
|
11178
12144
|
const ignored = /* @__PURE__ */ new Set();
|
|
11179
12145
|
if (relativePaths.length === 0) return ignored;
|
|
11180
12146
|
const result = await new Promise((resolve) => {
|
|
11181
|
-
const child =
|
|
12147
|
+
const child = execFile4(
|
|
11182
12148
|
"git",
|
|
11183
12149
|
["-c", "core.quotepath=false", "check-ignore", "--stdin", "-z"],
|
|
11184
12150
|
{ cwd: repoRoot2, timeout: GIT_TIMEOUT_MS, maxBuffer: GIT_MAX_BUFFER, windowsHide: true },
|
|
@@ -11359,24 +12325,24 @@ var RepoHub = class extends EventEmitter2 {
|
|
|
11359
12325
|
// packages/server/src/memory-hub.ts
|
|
11360
12326
|
import { EventEmitter as EventEmitter3 } from "node:events";
|
|
11361
12327
|
import { homedir as homedir10 } from "node:os";
|
|
11362
|
-
import
|
|
12328
|
+
import path34 from "node:path";
|
|
11363
12329
|
import chokidar2 from "chokidar";
|
|
11364
12330
|
|
|
11365
12331
|
// packages/server/src/memory-bridge.ts
|
|
11366
|
-
import { execFile as
|
|
11367
|
-
import { randomBytes as
|
|
12332
|
+
import { execFile as execFile5 } from "node:child_process";
|
|
12333
|
+
import { randomBytes as randomBytes4 } from "node:crypto";
|
|
11368
12334
|
import {
|
|
11369
|
-
mkdir as
|
|
12335
|
+
mkdir as mkdir7,
|
|
11370
12336
|
open as open6,
|
|
11371
12337
|
readdir as readdir9,
|
|
11372
|
-
readFile as
|
|
12338
|
+
readFile as readFile11,
|
|
11373
12339
|
realpath as realpath2,
|
|
11374
|
-
rename as
|
|
11375
|
-
stat as
|
|
12340
|
+
rename as rename4,
|
|
12341
|
+
stat as stat17,
|
|
11376
12342
|
unlink as unlink3
|
|
11377
12343
|
} from "node:fs/promises";
|
|
11378
12344
|
import { homedir as homedir9 } from "node:os";
|
|
11379
|
-
import
|
|
12345
|
+
import path33 from "node:path";
|
|
11380
12346
|
var MemoryBridgeError = class extends ServerTextError {
|
|
11381
12347
|
};
|
|
11382
12348
|
var MAX_LISTED_NOTES = 500;
|
|
@@ -11394,9 +12360,9 @@ var GITIGNORE_MEMORY_LINE = `${MEMORY_DIR}/`;
|
|
|
11394
12360
|
var BOM2 = Buffer.from([239, 187, 191]);
|
|
11395
12361
|
async function inspectMemory(cwd, home = homedir9()) {
|
|
11396
12362
|
await assertMemoryDirInside(cwd);
|
|
11397
|
-
const folder =
|
|
12363
|
+
const folder = path33.join(cwd, MEMORY_DIR);
|
|
11398
12364
|
const folderExists = await isDirectory2(folder);
|
|
11399
|
-
const indexExists = folderExists && await isFile(
|
|
12365
|
+
const indexExists = folderExists && await isFile(path33.join(folder, MEMORY_INDEX_FILE));
|
|
11400
12366
|
const noteNames = folderExists ? await listNoteFiles(folder) : [];
|
|
11401
12367
|
const notes = await describeNotes(folder, noteNames);
|
|
11402
12368
|
const files = await Promise.all(
|
|
@@ -11428,7 +12394,7 @@ async function installMemory(cwd, options, home = homedir9()) {
|
|
|
11428
12394
|
}
|
|
11429
12395
|
async function importNativeMemory(cwd, home = homedir9()) {
|
|
11430
12396
|
await assertMemoryDirInside(cwd);
|
|
11431
|
-
if (!await isDirectory2(
|
|
12397
|
+
if (!await isDirectory2(path33.join(cwd, MEMORY_DIR))) {
|
|
11432
12398
|
throw new MemoryBridgeError(serverText("memoryNotInstalled"));
|
|
11433
12399
|
}
|
|
11434
12400
|
const plan = new PlanBuilder(cwd);
|
|
@@ -11446,9 +12412,9 @@ async function readMemoryNote(cwd, name) {
|
|
|
11446
12412
|
}
|
|
11447
12413
|
const absolute = await resolveInside(cwd, `${MEMORY_DIR}/${name}`);
|
|
11448
12414
|
try {
|
|
11449
|
-
const info = await
|
|
12415
|
+
const info = await stat17(absolute);
|
|
11450
12416
|
if (!info.isFile()) return null;
|
|
11451
|
-
const content = await
|
|
12417
|
+
const content = await readFile11(absolute);
|
|
11452
12418
|
return {
|
|
11453
12419
|
name,
|
|
11454
12420
|
text: content.subarray(0, MAX_NOTE_BYTES).toString("utf8"),
|
|
@@ -11463,7 +12429,7 @@ function isNoteName(name) {
|
|
|
11463
12429
|
if (name.length === 0 || name.length > 255) return false;
|
|
11464
12430
|
if (name.includes("\0")) return false;
|
|
11465
12431
|
if (name === "." || name === "..") return false;
|
|
11466
|
-
if (
|
|
12432
|
+
if (path33.basename(name) !== name) return false;
|
|
11467
12433
|
if (/[\\/:*?"<>|\u0000-\u001f]/.test(name)) return false;
|
|
11468
12434
|
return name.toLowerCase().endsWith(".md");
|
|
11469
12435
|
}
|
|
@@ -11500,7 +12466,7 @@ var PlanBuilder = class {
|
|
|
11500
12466
|
/** Nombres de nota que habria en la carpeta, en minusculas. */
|
|
11501
12467
|
async noteNames() {
|
|
11502
12468
|
const names = /* @__PURE__ */ new Map();
|
|
11503
|
-
for (const name of await listNoteFiles(
|
|
12469
|
+
for (const name of await listNoteFiles(path33.join(this.cwd, MEMORY_DIR))) {
|
|
11504
12470
|
names.set(name.toLowerCase(), name);
|
|
11505
12471
|
}
|
|
11506
12472
|
for (const rel of this.files.keys()) {
|
|
@@ -11543,7 +12509,7 @@ var PlanBuilder = class {
|
|
|
11543
12509
|
continue;
|
|
11544
12510
|
}
|
|
11545
12511
|
const target = await realpath2(absolute);
|
|
11546
|
-
const info = await
|
|
12512
|
+
const info = await stat17(target);
|
|
11547
12513
|
if (info.nlink > 1) {
|
|
11548
12514
|
throw new MemoryBridgeError(serverText("memoryHardLinks", { file: file.rel }));
|
|
11549
12515
|
}
|
|
@@ -11562,7 +12528,7 @@ var PlanBuilder = class {
|
|
|
11562
12528
|
);
|
|
11563
12529
|
if (pending.length === 0) return;
|
|
11564
12530
|
if (pending.some((file) => file.rel.startsWith(`${MEMORY_DIR}/`))) {
|
|
11565
|
-
await
|
|
12531
|
+
await mkdir7(path33.join(this.cwd, MEMORY_DIR), { recursive: true });
|
|
11566
12532
|
}
|
|
11567
12533
|
for (const file of pending) {
|
|
11568
12534
|
if (file.target === null) throw new Error(`Unvalidated plan: ${file.rel}`);
|
|
@@ -11673,7 +12639,7 @@ async function planNativeImport(plan, cwd, home) {
|
|
|
11673
12639
|
const local = await plan.noteNames();
|
|
11674
12640
|
for (const name of await listNoteFiles(folder)) {
|
|
11675
12641
|
const key = name.toLowerCase();
|
|
11676
|
-
const content = await readIfFile(
|
|
12642
|
+
const content = await readIfFile(path33.join(folder, name), name);
|
|
11677
12643
|
if (content === null) continue;
|
|
11678
12644
|
const already = chosen.get(key);
|
|
11679
12645
|
if (already !== void 0) {
|
|
@@ -11687,7 +12653,7 @@ async function planNativeImport(plan, cwd, home) {
|
|
|
11687
12653
|
if (existing !== null && !existing.equals(content)) skipped.push(name);
|
|
11688
12654
|
continue;
|
|
11689
12655
|
}
|
|
11690
|
-
plan.put(`${MEMORY_DIR}/${name}`, null, content, "copy", "",
|
|
12656
|
+
plan.put(`${MEMORY_DIR}/${name}`, null, content, "copy", "", path33.join(folder, name));
|
|
11691
12657
|
copied.push(name);
|
|
11692
12658
|
}
|
|
11693
12659
|
}
|
|
@@ -11697,7 +12663,7 @@ async function planNativeImport(plan, cwd, home) {
|
|
|
11697
12663
|
const known = new Set(linkTargets(index.toString("utf8")));
|
|
11698
12664
|
const additions = [];
|
|
11699
12665
|
for (const folder of folders) {
|
|
11700
|
-
const nativeIndex = await readIfFile(
|
|
12666
|
+
const nativeIndex = await readIfFile(path33.join(folder, MEMORY_INDEX_FILE), MEMORY_INDEX_FILE);
|
|
11701
12667
|
if (nativeIndex === null) continue;
|
|
11702
12668
|
for (const line of nativeIndex.toString("utf8").replace(/^\uFEFF/, "").split(/\r?\n/)) {
|
|
11703
12669
|
const target = linkTargets(line)[0];
|
|
@@ -11922,7 +12888,7 @@ async function describeNotes(folder, names) {
|
|
|
11922
12888
|
const stated = [];
|
|
11923
12889
|
for (const name of names) {
|
|
11924
12890
|
try {
|
|
11925
|
-
const info = await
|
|
12891
|
+
const info = await stat17(path33.join(folder, name));
|
|
11926
12892
|
if (info.isFile()) stated.push({ name, sizeBytes: info.size, modifiedAt: info.mtimeMs });
|
|
11927
12893
|
} catch {
|
|
11928
12894
|
}
|
|
@@ -11930,7 +12896,7 @@ async function describeNotes(folder, names) {
|
|
|
11930
12896
|
stated.sort((a, b) => b.modifiedAt - a.modifiedAt || a.name.localeCompare(b.name));
|
|
11931
12897
|
const notes = [];
|
|
11932
12898
|
for (const item of stated.slice(0, MAX_LISTED_NOTES)) {
|
|
11933
|
-
const head = await readHead(
|
|
12899
|
+
const head = await readHead(path33.join(folder, item.name));
|
|
11934
12900
|
notes.push({ ...item, ...describeNoteText(head, item.name) });
|
|
11935
12901
|
}
|
|
11936
12902
|
return notes;
|
|
@@ -11988,28 +12954,28 @@ async function countNativeMemory(cwd, home, sharedNames) {
|
|
|
11988
12954
|
return { available: names.size, pending };
|
|
11989
12955
|
}
|
|
11990
12956
|
async function nativeMemoryFolders(cwd, home) {
|
|
11991
|
-
const root =
|
|
12957
|
+
const root = path33.join(home, ".claude", "projects");
|
|
11992
12958
|
const slug = projectSlugFor(cwd).toLowerCase();
|
|
11993
12959
|
try {
|
|
11994
12960
|
const entries = await readdir9(root, { withFileTypes: true });
|
|
11995
|
-
return entries.filter((entry) => entry.isDirectory() && entry.name.toLowerCase() === slug).map((entry) => entry.name).sort().map((name) =>
|
|
12961
|
+
return entries.filter((entry) => entry.isDirectory() && entry.name.toLowerCase() === slug).map((entry) => entry.name).sort().map((name) => path33.join(root, name, "memory"));
|
|
11996
12962
|
} catch {
|
|
11997
12963
|
return [];
|
|
11998
12964
|
}
|
|
11999
12965
|
}
|
|
12000
12966
|
function buildGlobalFragments(home) {
|
|
12001
12967
|
const windows = process.platform === "win32";
|
|
12002
|
-
const globalFile =
|
|
12968
|
+
const globalFile = path33.join(home, ...GLOBAL_MEMORY_RELATIVE.split("/"));
|
|
12003
12969
|
const hardLink = (link) => windows ? `mklink /H "${link}" "${globalFile}"` : `ln "${globalFile}" "${link}"`;
|
|
12004
12970
|
const hardLinkNote = windows ? serverText("memoryNoteHardLinkCmd", { path: GLOBAL_MEMORY_RELATIVE }) : serverText("memoryNoteHardLink", { path: GLOBAL_MEMORY_RELATIVE });
|
|
12005
|
-
const codexTarget =
|
|
12006
|
-
const geminiTarget =
|
|
12971
|
+
const codexTarget = path33.join(home, ".codex", "AGENTS.md");
|
|
12972
|
+
const geminiTarget = path33.join(home, ".gemini", "GEMINI.md");
|
|
12007
12973
|
const homeSlashes = home.replace(/\\/g, "/").replace(/\/+$/, "");
|
|
12008
12974
|
return [
|
|
12009
12975
|
{
|
|
12010
12976
|
agent: "claude-code",
|
|
12011
12977
|
label: MEMORY_AGENT_LABELS["claude-code"],
|
|
12012
|
-
target:
|
|
12978
|
+
target: path33.join(home, ".claude", "CLAUDE.md"),
|
|
12013
12979
|
kind: "line",
|
|
12014
12980
|
text: `@~/${GLOBAL_MEMORY_RELATIVE}`,
|
|
12015
12981
|
note: serverText("memoryNoteClaude")
|
|
@@ -12033,7 +12999,7 @@ function buildGlobalFragments(home) {
|
|
|
12033
12999
|
{
|
|
12034
13000
|
agent: "opencode",
|
|
12035
13001
|
label: MEMORY_AGENT_LABELS.opencode,
|
|
12036
|
-
target:
|
|
13002
|
+
target: path33.join(home, ".config", "opencode", "opencode.json"),
|
|
12037
13003
|
kind: "json",
|
|
12038
13004
|
text: `"instructions": ["${homeSlashes}/${GLOBAL_MEMORY_RELATIVE}"]`,
|
|
12039
13005
|
note: serverText("memoryNoteOpenCode")
|
|
@@ -12042,7 +13008,7 @@ function buildGlobalFragments(home) {
|
|
|
12042
13008
|
}
|
|
12043
13009
|
function runGit2(cwd, args) {
|
|
12044
13010
|
return new Promise((resolve) => {
|
|
12045
|
-
|
|
13011
|
+
execFile5(
|
|
12046
13012
|
"git",
|
|
12047
13013
|
["-c", "core.quotepath=false", "--no-optional-locks", ...args],
|
|
12048
13014
|
{ cwd, timeout: GIT_TIMEOUT_MS2, windowsHide: true, encoding: "utf8", maxBuffer: 1024 * 1024 },
|
|
@@ -12081,17 +13047,17 @@ async function readGit(cwd) {
|
|
|
12081
13047
|
if (!toplevel || !gitDir || !commonDir) return fail(serverText("gitUnexpected", { command: "rev-parse" }));
|
|
12082
13048
|
const ignored = await checkIgnored(cwd, INDEX_REL);
|
|
12083
13049
|
if (typeof ignored !== "boolean") return fail(ignored);
|
|
12084
|
-
const absoluteGitDir =
|
|
12085
|
-
const absoluteCommonDir =
|
|
13050
|
+
const absoluteGitDir = path33.resolve(cwd, gitDir);
|
|
13051
|
+
const absoluteCommonDir = path33.resolve(cwd, commonDir);
|
|
12086
13052
|
let worktree = null;
|
|
12087
13053
|
let main2 = null;
|
|
12088
13054
|
if (!await samePath(absoluteGitDir, absoluteCommonDir)) {
|
|
12089
|
-
if (
|
|
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(
|
|
13055
|
+
if (path33.basename(absoluteCommonDir).toLowerCase() === ".git") {
|
|
13056
|
+
const mainRoot = path33.dirname(absoluteCommonDir);
|
|
13057
|
+
const inner = path33.relative(path33.resolve(toplevel), path33.resolve(cwd));
|
|
13058
|
+
const sameFolder = inner.length === 0 || inner.startsWith("..") || path33.isAbsolute(inner);
|
|
13059
|
+
const mainPath = sameFolder ? mainRoot : path33.join(mainRoot, inner);
|
|
13060
|
+
const memoryRel = sameFolder ? MEMORY_DIR : [...inner.split(path33.sep), MEMORY_DIR].join("/");
|
|
12095
13061
|
main2 = { root: mainRoot, memoryRel };
|
|
12096
13062
|
worktree = {
|
|
12097
13063
|
mainPath,
|
|
@@ -12116,7 +13082,7 @@ async function samePath(a, b) {
|
|
|
12116
13082
|
try {
|
|
12117
13083
|
return await realpath2(value);
|
|
12118
13084
|
} catch {
|
|
12119
|
-
return
|
|
13085
|
+
return path33.resolve(value);
|
|
12120
13086
|
}
|
|
12121
13087
|
};
|
|
12122
13088
|
const [left, right] = await Promise.all([resolve(a), resolve(b)]);
|
|
@@ -12158,9 +13124,9 @@ async function listNoteFiles(folder) {
|
|
|
12158
13124
|
}
|
|
12159
13125
|
async function readIfFile(absolute, label) {
|
|
12160
13126
|
try {
|
|
12161
|
-
const info = await
|
|
13127
|
+
const info = await stat17(absolute);
|
|
12162
13128
|
if (!info.isFile()) throw new MemoryBridgeError(serverText("memoryNotAFile", { label }));
|
|
12163
|
-
return await
|
|
13129
|
+
return await readFile11(absolute);
|
|
12164
13130
|
} catch (error) {
|
|
12165
13131
|
if (isNotFound(error)) return null;
|
|
12166
13132
|
throw error;
|
|
@@ -12182,14 +13148,14 @@ async function readHead(absolute) {
|
|
|
12182
13148
|
}
|
|
12183
13149
|
async function isFile(absolute) {
|
|
12184
13150
|
try {
|
|
12185
|
-
return (await
|
|
13151
|
+
return (await stat17(absolute)).isFile();
|
|
12186
13152
|
} catch {
|
|
12187
13153
|
return false;
|
|
12188
13154
|
}
|
|
12189
13155
|
}
|
|
12190
13156
|
async function isDirectory2(absolute) {
|
|
12191
13157
|
try {
|
|
12192
|
-
return (await
|
|
13158
|
+
return (await stat17(absolute)).isDirectory();
|
|
12193
13159
|
} catch {
|
|
12194
13160
|
return false;
|
|
12195
13161
|
}
|
|
@@ -12207,7 +13173,7 @@ async function writeAtomic(target, content, mustNotExist) {
|
|
|
12207
13173
|
await handle.close();
|
|
12208
13174
|
}
|
|
12209
13175
|
if (mustNotExist && await exists2(target)) {
|
|
12210
|
-
throw new MemoryBridgeError(serverText("memoryAppeared", { file:
|
|
13176
|
+
throw new MemoryBridgeError(serverText("memoryAppeared", { file: path33.basename(target) }));
|
|
12211
13177
|
}
|
|
12212
13178
|
await renameWithRetry(temporary, target);
|
|
12213
13179
|
} catch (error) {
|
|
@@ -12217,10 +13183,10 @@ async function writeAtomic(target, content, mustNotExist) {
|
|
|
12217
13183
|
}
|
|
12218
13184
|
async function createTemporary(target) {
|
|
12219
13185
|
for (let attempt = 1; ; attempt += 1) {
|
|
12220
|
-
const suffix =
|
|
12221
|
-
const temporary =
|
|
12222
|
-
|
|
12223
|
-
`.${
|
|
13186
|
+
const suffix = randomBytes4(6).toString("hex");
|
|
13187
|
+
const temporary = path33.join(
|
|
13188
|
+
path33.dirname(target),
|
|
13189
|
+
`.${path33.basename(target)}.${process.pid}.${suffix}.tmp`
|
|
12224
13190
|
);
|
|
12225
13191
|
try {
|
|
12226
13192
|
return { temporary, handle: await open6(temporary, "wx") };
|
|
@@ -12232,7 +13198,7 @@ async function createTemporary(target) {
|
|
|
12232
13198
|
}
|
|
12233
13199
|
async function exists2(absolute) {
|
|
12234
13200
|
try {
|
|
12235
|
-
await
|
|
13201
|
+
await stat17(absolute);
|
|
12236
13202
|
return true;
|
|
12237
13203
|
} catch {
|
|
12238
13204
|
return false;
|
|
@@ -12241,7 +13207,7 @@ async function exists2(absolute) {
|
|
|
12241
13207
|
async function renameWithRetry(from, to) {
|
|
12242
13208
|
for (let attempt = 1; ; attempt += 1) {
|
|
12243
13209
|
try {
|
|
12244
|
-
await
|
|
13210
|
+
await rename4(from, to);
|
|
12245
13211
|
return;
|
|
12246
13212
|
} catch (error) {
|
|
12247
13213
|
const code = error.code;
|
|
@@ -12458,13 +13424,13 @@ var MemoryHub = class extends EventEmitter3 {
|
|
|
12458
13424
|
}
|
|
12459
13425
|
};
|
|
12460
13426
|
function keyFor(cwd) {
|
|
12461
|
-
const resolved =
|
|
13427
|
+
const resolved = path34.resolve(cwd);
|
|
12462
13428
|
return process.platform === "win32" ? resolved.toLowerCase() : resolved;
|
|
12463
13429
|
}
|
|
12464
13430
|
function isRelevantPath(cwd, candidate) {
|
|
12465
|
-
const relative =
|
|
13431
|
+
const relative = path34.relative(cwd, candidate);
|
|
12466
13432
|
if (relative.length === 0) return true;
|
|
12467
|
-
if (relative.startsWith("..") ||
|
|
13433
|
+
if (relative.startsWith("..") || path34.isAbsolute(relative)) return false;
|
|
12468
13434
|
const same = (a, b) => process.platform === "win32" ? a.toLowerCase() === b.toLowerCase() : a === b;
|
|
12469
13435
|
const parts = relative.split(/[\\/]/);
|
|
12470
13436
|
const [first = "", second = "", third = ""] = parts;
|
|
@@ -12481,10 +13447,10 @@ function isRelevantPath(cwd, candidate) {
|
|
|
12481
13447
|
}
|
|
12482
13448
|
|
|
12483
13449
|
// packages/server/src/paste-store.ts
|
|
12484
|
-
import { createHash, randomUUID as
|
|
12485
|
-
import { mkdir as
|
|
13450
|
+
import { createHash as createHash3, randomUUID as randomUUID3 } from "node:crypto";
|
|
13451
|
+
import { mkdir as mkdir8, readdir as readdir10, rm as rm3, stat as stat18, writeFile as writeFile4 } from "node:fs/promises";
|
|
12486
13452
|
import { tmpdir as tmpdir4 } from "node:os";
|
|
12487
|
-
import
|
|
13453
|
+
import path35 from "node:path";
|
|
12488
13454
|
|
|
12489
13455
|
// packages/server/src/vault/markdown.ts
|
|
12490
13456
|
var pad2 = (value) => String(value).padStart(2, "0");
|
|
@@ -12999,8 +13965,8 @@ function buildContinuationMessage(input) {
|
|
|
12999
13965
|
}
|
|
13000
13966
|
return sanitizeForPaste(lines.join("\n"));
|
|
13001
13967
|
}
|
|
13002
|
-
function transcriptReferenceFor(
|
|
13003
|
-
return style === "at-quoted" ? fileReference(
|
|
13968
|
+
function transcriptReferenceFor(path50, style) {
|
|
13969
|
+
return style === "at-quoted" ? fileReference(path50, "at-quoted") : `"${path50}"`;
|
|
13004
13970
|
}
|
|
13005
13971
|
function planTranscript(input) {
|
|
13006
13972
|
const turns = splitTurns(input.events);
|
|
@@ -13175,7 +14141,7 @@ var PasteStore = class {
|
|
|
13175
14141
|
root;
|
|
13176
14142
|
counter = 0;
|
|
13177
14143
|
constructor(root) {
|
|
13178
|
-
this.root = root ??
|
|
14144
|
+
this.root = root ?? path35.join(tmpdir4(), "agent-workbench", "pasted");
|
|
13179
14145
|
}
|
|
13180
14146
|
/**
|
|
13181
14147
|
* Guarda una imagen y devuelve su ruta.
|
|
@@ -13202,12 +14168,12 @@ var PasteStore = class {
|
|
|
13202
14168
|
if (signature === null) {
|
|
13203
14169
|
throw new PasteImageError(serverText("imageUnknownFormat"));
|
|
13204
14170
|
}
|
|
13205
|
-
const directory =
|
|
13206
|
-
await
|
|
14171
|
+
const directory = path35.join(this.root, safeSegment(terminalId));
|
|
14172
|
+
await mkdir8(directory, { recursive: true, mode: DIRECTORY_MODE });
|
|
13207
14173
|
this.counter += 1;
|
|
13208
|
-
const name = `pegada-${this.counter}-${
|
|
13209
|
-
const file =
|
|
13210
|
-
await
|
|
14174
|
+
const name = `pegada-${this.counter}-${randomUUID3().slice(0, 8)}.${signature.ext}`;
|
|
14175
|
+
const file = path35.join(directory, name);
|
|
14176
|
+
await writeFile4(file, bytes, { mode: FILE_MODE });
|
|
13211
14177
|
return { path: file, bytes: bytes.length };
|
|
13212
14178
|
}
|
|
13213
14179
|
/**
|
|
@@ -13224,12 +14190,12 @@ var PasteStore = class {
|
|
|
13224
14190
|
if (bytes.length > MAX_TEXT_BYTES) {
|
|
13225
14191
|
throw new PasteTextError(`The text is ${bytes.length} bytes; the maximum is ${MAX_TEXT_BYTES}.`);
|
|
13226
14192
|
}
|
|
13227
|
-
const directory =
|
|
13228
|
-
await
|
|
14193
|
+
const directory = path35.join(this.root, safeSegment(terminalId));
|
|
14194
|
+
await mkdir8(directory, { recursive: true, mode: DIRECTORY_MODE });
|
|
13229
14195
|
this.counter += 1;
|
|
13230
|
-
const name = `${baseName}-${this.counter}-${
|
|
13231
|
-
const file =
|
|
13232
|
-
await
|
|
14196
|
+
const name = `${baseName}-${this.counter}-${randomUUID3().slice(0, 8)}.md`;
|
|
14197
|
+
const file = path35.join(directory, name);
|
|
14198
|
+
await writeFile4(file, bytes, { mode: FILE_MODE });
|
|
13233
14199
|
return { path: file, bytes: bytes.length };
|
|
13234
14200
|
}
|
|
13235
14201
|
/**
|
|
@@ -13258,17 +14224,17 @@ var PasteStore = class {
|
|
|
13258
14224
|
})
|
|
13259
14225
|
);
|
|
13260
14226
|
}
|
|
13261
|
-
const directory =
|
|
13262
|
-
await
|
|
14227
|
+
const directory = path35.join(this.root, safeSegment(terminalId));
|
|
14228
|
+
await mkdir8(directory, { recursive: true, mode: DIRECTORY_MODE });
|
|
13263
14229
|
this.counter += 1;
|
|
13264
|
-
const name = `adjunto-${this.counter}-${
|
|
13265
|
-
const file =
|
|
13266
|
-
await
|
|
14230
|
+
const name = `adjunto-${this.counter}-${randomUUID3().slice(0, 8)}-${safe}`;
|
|
14231
|
+
const file = path35.join(directory, name);
|
|
14232
|
+
await writeFile4(file, bytes, { mode: FILE_MODE });
|
|
13267
14233
|
return { path: file, bytes: bytes.length, name };
|
|
13268
14234
|
}
|
|
13269
14235
|
/** Borra lo de una pestana. Se llama al cerrarla. */
|
|
13270
14236
|
async clearTerminal(terminalId) {
|
|
13271
|
-
await
|
|
14237
|
+
await rm3(path35.join(this.root, safeSegment(terminalId)), {
|
|
13272
14238
|
recursive: true,
|
|
13273
14239
|
force: true
|
|
13274
14240
|
}).catch(() => {
|
|
@@ -13290,11 +14256,11 @@ var PasteStore = class {
|
|
|
13290
14256
|
}
|
|
13291
14257
|
await Promise.all(
|
|
13292
14258
|
entries.map(async (entry) => {
|
|
13293
|
-
const full =
|
|
14259
|
+
const full = path35.join(this.root, entry);
|
|
13294
14260
|
try {
|
|
13295
|
-
const info = await
|
|
14261
|
+
const info = await stat18(full);
|
|
13296
14262
|
if (now - info.mtimeMs > STALE_AFTER_MS) {
|
|
13297
|
-
await
|
|
14263
|
+
await rm3(full, { recursive: true, force: true });
|
|
13298
14264
|
}
|
|
13299
14265
|
} catch {
|
|
13300
14266
|
}
|
|
@@ -13304,13 +14270,13 @@ var PasteStore = class {
|
|
|
13304
14270
|
};
|
|
13305
14271
|
function safeSegment(value) {
|
|
13306
14272
|
const clean = value.replace(/[^a-zA-Z0-9_-]/g, "");
|
|
13307
|
-
return clean.length > 0 ? clean :
|
|
14273
|
+
return clean.length > 0 ? clean : createHash3("sha1").update(value).digest("hex").slice(0, 16);
|
|
13308
14274
|
}
|
|
13309
14275
|
|
|
13310
14276
|
// packages/server/src/session-index.ts
|
|
13311
14277
|
import { EventEmitter as EventEmitter4 } from "node:events";
|
|
13312
|
-
import { mkdir as
|
|
13313
|
-
import
|
|
14278
|
+
import { mkdir as mkdir9, readFile as readFile12, stat as stat19, writeFile as writeFile5 } from "node:fs/promises";
|
|
14279
|
+
import path36 from "node:path";
|
|
13314
14280
|
var CACHE_VERSION = 4;
|
|
13315
14281
|
var vaultPairKey = (agent, sessionId) => `${agent}/${sessionId}`;
|
|
13316
14282
|
function vaultOnlySessions(natives, vault, listState, ready) {
|
|
@@ -13359,7 +14325,7 @@ function cacheKey(agent, ref) {
|
|
|
13359
14325
|
}
|
|
13360
14326
|
async function pathExists3(candidate) {
|
|
13361
14327
|
try {
|
|
13362
|
-
await
|
|
14328
|
+
await stat19(candidate);
|
|
13363
14329
|
return true;
|
|
13364
14330
|
} catch {
|
|
13365
14331
|
return false;
|
|
@@ -13409,7 +14375,7 @@ function migrateEntryV3(filePath, value) {
|
|
|
13409
14375
|
return {
|
|
13410
14376
|
agent: "claude-code",
|
|
13411
14377
|
ref: filePath,
|
|
13412
|
-
group:
|
|
14378
|
+
group: path36.basename(path36.dirname(filePath)),
|
|
13413
14379
|
mtimeMs,
|
|
13414
14380
|
sizeBytes,
|
|
13415
14381
|
cwd,
|
|
@@ -13674,7 +14640,7 @@ var SessionIndex = class extends EventEmitter4 {
|
|
|
13674
14640
|
}
|
|
13675
14641
|
async loadCache() {
|
|
13676
14642
|
try {
|
|
13677
|
-
const raw = await
|
|
14643
|
+
const raw = await readFile12(sessionIndexCachePath(), "utf8");
|
|
13678
14644
|
const parsed = JSON.parse(raw);
|
|
13679
14645
|
const migrated = migrateIndexCache(parsed);
|
|
13680
14646
|
if (migrated === null) return;
|
|
@@ -13687,8 +14653,8 @@ var SessionIndex = class extends EventEmitter4 {
|
|
|
13687
14653
|
if (!this.cacheDirty) return;
|
|
13688
14654
|
this.cacheDirty = false;
|
|
13689
14655
|
try {
|
|
13690
|
-
await
|
|
13691
|
-
await
|
|
14656
|
+
await mkdir9(appConfigDir(), { recursive: true });
|
|
14657
|
+
await writeFile5(sessionIndexCachePath(), JSON.stringify(this.cache), "utf8");
|
|
13692
14658
|
} catch (error) {
|
|
13693
14659
|
console.warn("[index] couldn't save the cache:", error);
|
|
13694
14660
|
}
|
|
@@ -13865,199 +14831,42 @@ var SessionIndex = class extends EventEmitter4 {
|
|
|
13865
14831
|
let refs;
|
|
13866
14832
|
try {
|
|
13867
14833
|
refs = await history.changedRefs(filePath);
|
|
13868
|
-
} catch {
|
|
13869
|
-
refs = null;
|
|
13870
|
-
}
|
|
13871
|
-
if (refs === null || refs.length === 0) return;
|
|
13872
|
-
let changed = false;
|
|
13873
|
-
for (const ref of refs) {
|
|
13874
|
-
const key = cacheKey(agent, ref);
|
|
13875
|
-
const cachedBefore = this.cache.entries[key];
|
|
13876
|
-
if (cachedBefore !== void 0) {
|
|
13877
|
-
delete this.cache.entries[key];
|
|
13878
|
-
this.cacheDirty = true;
|
|
13879
|
-
}
|
|
13880
|
-
let indexed;
|
|
13881
|
-
try {
|
|
13882
|
-
const item = await history.item(ref);
|
|
13883
|
-
indexed = item === null ? null : await this.readOrThrow(agent, history, item);
|
|
13884
|
-
} catch {
|
|
13885
|
-
if (cachedBefore !== void 0 && !(key in this.cache.entries)) this.cache.entries[key] = cachedBefore;
|
|
13886
|
-
continue;
|
|
13887
|
-
}
|
|
13888
|
-
if (indexed === null) {
|
|
13889
|
-
if (this.sessions.delete(key)) changed = true;
|
|
13890
|
-
continue;
|
|
13891
|
-
}
|
|
13892
|
-
if (indexed.cwd !== null) this.cwdExists.set(indexed.cwd, await pathExists3(indexed.cwd));
|
|
13893
|
-
this.sessions.set(key, indexed);
|
|
13894
|
-
changed = true;
|
|
13895
|
-
}
|
|
13896
|
-
if (changed) this.emitProjects();
|
|
13897
|
-
await this.saveCache();
|
|
13898
|
-
}
|
|
13899
|
-
};
|
|
13900
|
-
|
|
13901
|
-
// packages/server/src/settings-store.ts
|
|
13902
|
-
import { mkdir as mkdir9, readFile as readFile12 } from "node:fs/promises";
|
|
13903
|
-
import path36 from "node:path";
|
|
13904
|
-
|
|
13905
|
-
// packages/server/src/vault/write.ts
|
|
13906
|
-
import { randomBytes as randomBytes4 } from "node:crypto";
|
|
13907
|
-
import { mkdir as mkdir8, rename as rename4, rm as rm3, stat as stat18, writeFile as writeFile5 } from "node:fs/promises";
|
|
13908
|
-
|
|
13909
|
-
// packages/server/src/vault/paths.ts
|
|
13910
|
-
import { createHash as createHash2 } from "node:crypto";
|
|
13911
|
-
import path35 from "node:path";
|
|
13912
|
-
function defaultVaultDir() {
|
|
13913
|
-
return path35.join(appConfigDir(), "vault");
|
|
13914
|
-
}
|
|
13915
|
-
function vaultMarkerFile(dir) {
|
|
13916
|
-
return path35.join(dir, "vault.json");
|
|
13917
|
-
}
|
|
13918
|
-
function sessionsRoot2(dir) {
|
|
13919
|
-
return path35.join(dir, "sessions");
|
|
13920
|
-
}
|
|
13921
|
-
function isSafeSessionId(sessionId) {
|
|
13922
|
-
return isVaultSessionId(sessionId);
|
|
13923
|
-
}
|
|
13924
|
-
function isSessionAgentId(value) {
|
|
13925
|
-
return SESSION_AGENT_IDS.includes(value);
|
|
13926
|
-
}
|
|
13927
|
-
function assertSessionKey(agent, sessionId) {
|
|
13928
|
-
if (!isSessionAgentId(agent)) throw new Error(`Unknown source for the local copy: ${JSON.stringify(agent)}`);
|
|
13929
|
-
if (!isSafeSessionId(sessionId)) throw new Error(`Session id that isn't copied: ${JSON.stringify(sessionId)}`);
|
|
13930
|
-
}
|
|
13931
|
-
function agentSessionsDir(dir, agent) {
|
|
13932
|
-
if (!isSessionAgentId(agent)) throw new Error(`Unknown source for the local copy: ${JSON.stringify(agent)}`);
|
|
13933
|
-
return path35.join(sessionsRoot2(dir), agent);
|
|
13934
|
-
}
|
|
13935
|
-
function sessionFile(dir, agent, sessionId) {
|
|
13936
|
-
assertSessionKey(agent, sessionId);
|
|
13937
|
-
return path35.join(sessionsRoot2(dir), agent, `${sessionId}.jsonl`);
|
|
13938
|
-
}
|
|
13939
|
-
function assetsDir(dir, agent, sessionId) {
|
|
13940
|
-
assertSessionKey(agent, sessionId);
|
|
13941
|
-
return path35.join(sessionsRoot2(dir), agent, `${sessionId}.assets`);
|
|
13942
|
-
}
|
|
13943
|
-
function assetFile(dir, agent, sessionId, asset) {
|
|
13944
|
-
if (!VAULT_ASSET_NAME_PATTERN.test(asset)) throw new Error(`Invalid asset name: ${JSON.stringify(asset)}`);
|
|
13945
|
-
return path35.join(assetsDir(dir, agent, sessionId), asset);
|
|
13946
|
-
}
|
|
13947
|
-
function projectFolderName(cwd, platform) {
|
|
13948
|
-
const hash = createHash2("sha256").update(normalizeCwdKey(cwd, platform)).digest("hex").slice(0, 8);
|
|
13949
|
-
const base = (platform === "win32" ? path35.win32 : path35.posix).basename(cwd);
|
|
13950
|
-
const name = base.length === 0 ? "proyecto" : sanitizeFileName(base, 60);
|
|
13951
|
-
return `${name}-${hash}`;
|
|
13952
|
-
}
|
|
13953
|
-
function memoryDir(dir, cwd, platform) {
|
|
13954
|
-
return path35.join(dir, "memory", projectFolderName(cwd, platform));
|
|
13955
|
-
}
|
|
13956
|
-
function projectExportDir(dir, cwd, platform) {
|
|
13957
|
-
return path35.join(dir, "export", projectFolderName(cwd, platform));
|
|
13958
|
-
}
|
|
13959
|
-
function projectExportDirFor(dir, project, platform) {
|
|
13960
|
-
if (project.cwd.length > 0) return projectExportDir(dir, project.cwd, platform);
|
|
13961
|
-
const hash = createHash2("sha256").update(project.key).digest("hex").slice(0, 8);
|
|
13962
|
-
const name = project.fallbackName.length > 0 ? sanitizeFileName(project.fallbackName, 60) : "proyecto";
|
|
13963
|
-
return path35.join(dir, "export", `${name}-${hash}`);
|
|
13964
|
-
}
|
|
13965
|
-
function sessionExportFile(dir, agent, sessionId) {
|
|
13966
|
-
assertSessionKey(agent, sessionId);
|
|
13967
|
-
return path35.join(dir, "export", "sesiones", `${agent}-${sessionId}.md`);
|
|
13968
|
-
}
|
|
13969
|
-
var RESERVED_WINDOWS_NAME = /^(con|prn|aux|nul|com[1-9]|lpt[1-9])$/i;
|
|
13970
|
-
function sanitizeFileName(text, max = 80) {
|
|
13971
|
-
const limit = Math.max(1, Math.floor(max));
|
|
13972
|
-
const kept = text.normalize("NFC").replace(/[^\p{L}\p{M}\p{N} ._-]/gu, "_");
|
|
13973
|
-
const trimEnds = (value) => value.replace(/^ +/u, "").replace(/[. ]+$/u, "");
|
|
13974
|
-
let name = trimEnds(Array.from(trimEnds(kept)).slice(0, limit).join(""));
|
|
13975
|
-
if (name.length === 0) return "_";
|
|
13976
|
-
const stem = (name.split(".")[0] ?? "").replace(/ +$/u, "");
|
|
13977
|
-
if (RESERVED_WINDOWS_NAME.test(stem)) {
|
|
13978
|
-
name = `_${Array.from(name).slice(0, limit - 1).join("")}`;
|
|
13979
|
-
name = name.replace(/[. ]+$/u, "");
|
|
13980
|
-
}
|
|
13981
|
-
return name;
|
|
13982
|
-
}
|
|
13983
|
-
|
|
13984
|
-
// packages/server/src/vault/write.ts
|
|
13985
|
-
var RENAME_RETRIES = 3;
|
|
13986
|
-
var RENAME_RETRY_MS = 100;
|
|
13987
|
-
var DEFAULT_WRITE_DEPS = {
|
|
13988
|
-
rename: rename4,
|
|
13989
|
-
platform: process.platform,
|
|
13990
|
-
delay: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
|
|
13991
|
-
now: () => Date.now()
|
|
13992
|
-
};
|
|
13993
|
-
function temporaryPathFor(target) {
|
|
13994
|
-
return `${target}.${process.pid}.${randomBytes4(6).toString("hex")}.tmp`;
|
|
13995
|
-
}
|
|
13996
|
-
function isRetryableRename(error, platform) {
|
|
13997
|
-
if (platform !== "win32") return false;
|
|
13998
|
-
const code = error?.code;
|
|
13999
|
-
return code === "EPERM" || code === "EBUSY";
|
|
14000
|
-
}
|
|
14001
|
-
async function writeFileAtomic(target, data, deps = DEFAULT_WRITE_DEPS) {
|
|
14002
|
-
const temporary = temporaryPathFor(target);
|
|
14003
|
-
try {
|
|
14004
|
-
await writeFile5(temporary, data);
|
|
14005
|
-
for (let attempt = 0; ; attempt += 1) {
|
|
14834
|
+
} catch {
|
|
14835
|
+
refs = null;
|
|
14836
|
+
}
|
|
14837
|
+
if (refs === null || refs.length === 0) return;
|
|
14838
|
+
let changed = false;
|
|
14839
|
+
for (const ref of refs) {
|
|
14840
|
+
const key = cacheKey(agent, ref);
|
|
14841
|
+
const cachedBefore = this.cache.entries[key];
|
|
14842
|
+
if (cachedBefore !== void 0) {
|
|
14843
|
+
delete this.cache.entries[key];
|
|
14844
|
+
this.cacheDirty = true;
|
|
14845
|
+
}
|
|
14846
|
+
let indexed;
|
|
14006
14847
|
try {
|
|
14007
|
-
await
|
|
14008
|
-
|
|
14009
|
-
} catch
|
|
14010
|
-
if (
|
|
14011
|
-
|
|
14848
|
+
const item = await history.item(ref);
|
|
14849
|
+
indexed = item === null ? null : await this.readOrThrow(agent, history, item);
|
|
14850
|
+
} catch {
|
|
14851
|
+
if (cachedBefore !== void 0 && !(key in this.cache.entries)) this.cache.entries[key] = cachedBefore;
|
|
14852
|
+
continue;
|
|
14012
14853
|
}
|
|
14854
|
+
if (indexed === null) {
|
|
14855
|
+
if (this.sessions.delete(key)) changed = true;
|
|
14856
|
+
continue;
|
|
14857
|
+
}
|
|
14858
|
+
if (indexed.cwd !== null) this.cwdExists.set(indexed.cwd, await pathExists3(indexed.cwd));
|
|
14859
|
+
this.sessions.set(key, indexed);
|
|
14860
|
+
changed = true;
|
|
14013
14861
|
}
|
|
14014
|
-
|
|
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
|
-
}
|
|
14862
|
+
if (changed) this.emitProjects();
|
|
14863
|
+
await this.saveCache();
|
|
14054
14864
|
}
|
|
14055
|
-
|
|
14056
|
-
await writeFileAtomic(file, text, deps);
|
|
14057
|
-
return { file, bytes: text.length, assetsWritten, assetBytesWritten };
|
|
14058
|
-
}
|
|
14865
|
+
};
|
|
14059
14866
|
|
|
14060
14867
|
// packages/server/src/settings-store.ts
|
|
14868
|
+
import { mkdir as mkdir10, readFile as readFile13, stat as stat20 } from "node:fs/promises";
|
|
14869
|
+
import path37 from "node:path";
|
|
14061
14870
|
var SETTINGS_VERSION = 1;
|
|
14062
14871
|
var VAULT_TOOL_RESULT_DEFAULT_CHARS = 64e3;
|
|
14063
14872
|
var VAULT_TOOL_RESULT_MIN_CHARS = 4e3;
|
|
@@ -14065,13 +14874,14 @@ var VAULT_TOOL_RESULT_MAX_CHARS = 1e6;
|
|
|
14065
14874
|
function defaultAppSettings() {
|
|
14066
14875
|
return {
|
|
14067
14876
|
version: SETTINGS_VERSION,
|
|
14068
|
-
vault: { enabled: false, dir: null, toolResultMaxChars: VAULT_TOOL_RESULT_DEFAULT_CHARS }
|
|
14877
|
+
vault: { enabled: false, dir: null, toolResultMaxChars: VAULT_TOOL_RESULT_DEFAULT_CHARS },
|
|
14878
|
+
remote: { enabled: false, port: REMOTE_DEFAULT_PORT }
|
|
14069
14879
|
};
|
|
14070
14880
|
}
|
|
14071
14881
|
function isAbsoluteDir(dir, platform) {
|
|
14072
14882
|
if (dir.length === 0 || dir.includes("\0")) return false;
|
|
14073
14883
|
if (platform === "win32") return /^[A-Za-z]:[\\/]/.test(dir) || /^\\\\[^\\/]+[\\/][^\\/]+/.test(dir);
|
|
14074
|
-
return
|
|
14884
|
+
return path37.posix.isAbsolute(dir);
|
|
14075
14885
|
}
|
|
14076
14886
|
function clampToolResultMaxChars(value) {
|
|
14077
14887
|
if (typeof value !== "number" || !Number.isFinite(value)) return VAULT_TOOL_RESULT_DEFAULT_CHARS;
|
|
@@ -14081,6 +14891,11 @@ function parseAppSettings(value, platform, warn = () => void 0) {
|
|
|
14081
14891
|
const record = asRecord(value);
|
|
14082
14892
|
if (record === null || record["version"] !== SETTINGS_VERSION) return null;
|
|
14083
14893
|
const settings2 = defaultAppSettings();
|
|
14894
|
+
const remote = asRecord(record["remote"]);
|
|
14895
|
+
if (remote !== null) {
|
|
14896
|
+
settings2.remote.enabled = asBoolean(remote["enabled"]) ?? false;
|
|
14897
|
+
if (isRemotePort(remote["port"])) settings2.remote.port = remote["port"];
|
|
14898
|
+
}
|
|
14084
14899
|
const vault = asRecord(record["vault"]);
|
|
14085
14900
|
if (vault === null) return settings2;
|
|
14086
14901
|
settings2.vault.enabled = asBoolean(vault["enabled"]) ?? false;
|
|
@@ -14094,7 +14909,11 @@ function parseAppSettings(value, platform, warn = () => void 0) {
|
|
|
14094
14909
|
return settings2;
|
|
14095
14910
|
}
|
|
14096
14911
|
function frozen(settings2) {
|
|
14097
|
-
return Object.freeze({
|
|
14912
|
+
return Object.freeze({
|
|
14913
|
+
...settings2,
|
|
14914
|
+
vault: Object.freeze({ ...settings2.vault }),
|
|
14915
|
+
remote: Object.freeze({ ...settings2.remote })
|
|
14916
|
+
});
|
|
14098
14917
|
}
|
|
14099
14918
|
var SettingsStore = class {
|
|
14100
14919
|
/** La ruta es inyectable para las pruebas; por defecto, la de la carpeta de configuracion. */
|
|
@@ -14113,7 +14932,7 @@ var SettingsStore = class {
|
|
|
14113
14932
|
async load() {
|
|
14114
14933
|
let raw;
|
|
14115
14934
|
try {
|
|
14116
|
-
raw = await
|
|
14935
|
+
raw = await readFile13(this.filePath, "utf8");
|
|
14117
14936
|
} catch {
|
|
14118
14937
|
this.current = frozen(defaultAppSettings());
|
|
14119
14938
|
return;
|
|
@@ -14132,17 +14951,49 @@ var SettingsStore = class {
|
|
|
14132
14951
|
}
|
|
14133
14952
|
this.current = frozen(settings2 ?? defaultAppSettings());
|
|
14134
14953
|
}
|
|
14954
|
+
/**
|
|
14955
|
+
* Relee **solo** lo del acceso remoto (hito 37). Otra instancia de la app
|
|
14956
|
+
* comparte este archivo, y "apagar" desde una tiene que valer en la que tiene
|
|
14957
|
+
* el puerto; lo de la copia propia no se toca, que cada instancia lo lleva en
|
|
14958
|
+
* memoria como siempre. Sin archivo o ilegible, queda lo que habia.
|
|
14959
|
+
*/
|
|
14960
|
+
async reloadRemote() {
|
|
14961
|
+
let parsed;
|
|
14962
|
+
try {
|
|
14963
|
+
parsed = JSON.parse(await readFile13(this.filePath, "utf8"));
|
|
14964
|
+
} catch {
|
|
14965
|
+
return;
|
|
14966
|
+
}
|
|
14967
|
+
const settings2 = parseAppSettings(parsed, this.platform);
|
|
14968
|
+
if (settings2 === null) return;
|
|
14969
|
+
this.current = frozen({ ...this.current, remote: settings2.remote });
|
|
14970
|
+
}
|
|
14971
|
+
/** Una marca que cambia cuando el archivo cambia (fecha y tamano), o '' si no esta. */
|
|
14972
|
+
async stamp() {
|
|
14973
|
+
try {
|
|
14974
|
+
const info = await stat20(this.filePath);
|
|
14975
|
+
return `${info.mtimeMs}:${info.size}`;
|
|
14976
|
+
} catch {
|
|
14977
|
+
return "";
|
|
14978
|
+
}
|
|
14979
|
+
}
|
|
14135
14980
|
/** Lo ultimo que quedo escrito. Congelado: nadie de afuera lo cambia sin pasar por `update`. */
|
|
14136
14981
|
get() {
|
|
14137
14982
|
return this.current;
|
|
14138
14983
|
}
|
|
14139
14984
|
/**
|
|
14140
|
-
* Aplica un cambio y lo escribe. Lanza si la carpeta no es absoluta
|
|
14141
|
-
* se pudo escribir; en los
|
|
14985
|
+
* Aplica un cambio y lo escribe. Lanza si la carpeta no es absoluta, si el
|
|
14986
|
+
* puerto no sirve o si no se pudo escribir; en todos los casos `get()` sigue
|
|
14987
|
+
* dando lo anterior.
|
|
14142
14988
|
*/
|
|
14143
14989
|
update(patch) {
|
|
14144
14990
|
const run = async () => {
|
|
14145
|
-
|
|
14991
|
+
if (patch.remote === void 0) await this.reloadRemote();
|
|
14992
|
+
const next = {
|
|
14993
|
+
version: SETTINGS_VERSION,
|
|
14994
|
+
vault: { ...this.current.vault },
|
|
14995
|
+
remote: { ...this.current.remote }
|
|
14996
|
+
};
|
|
14146
14997
|
const vault = patch.vault ?? {};
|
|
14147
14998
|
if (vault.enabled !== void 0) next.vault.enabled = vault.enabled;
|
|
14148
14999
|
if (vault.dir !== void 0) {
|
|
@@ -14154,7 +15005,13 @@ var SettingsStore = class {
|
|
|
14154
15005
|
if (vault.toolResultMaxChars !== void 0) {
|
|
14155
15006
|
next.vault.toolResultMaxChars = clampToolResultMaxChars(vault.toolResultMaxChars);
|
|
14156
15007
|
}
|
|
14157
|
-
|
|
15008
|
+
const remote = patch.remote ?? {};
|
|
15009
|
+
if (remote.enabled !== void 0) next.remote.enabled = remote.enabled;
|
|
15010
|
+
if (remote.port !== void 0) {
|
|
15011
|
+
if (!isRemotePort(remote.port)) throw new Error(`The remote access port isn't valid: ${JSON.stringify(remote.port)}`);
|
|
15012
|
+
next.remote.port = remote.port;
|
|
15013
|
+
}
|
|
15014
|
+
await mkdir10(path37.dirname(this.filePath), { recursive: true });
|
|
14158
15015
|
await writeFileAtomic(this.filePath, `${JSON.stringify(next, null, 2)}
|
|
14159
15016
|
`);
|
|
14160
15017
|
this.current = frozen(next);
|
|
@@ -14181,6 +15038,19 @@ function statusLineStartupText(state) {
|
|
|
14181
15038
|
return "couldn't read its settings.json";
|
|
14182
15039
|
}
|
|
14183
15040
|
}
|
|
15041
|
+
var REMOTE_ACCESS_LABEL = "Remote access";
|
|
15042
|
+
function remoteAccessStartupLine(state, port, pairedDevices) {
|
|
15043
|
+
switch (state) {
|
|
15044
|
+
case "off":
|
|
15045
|
+
return null;
|
|
15046
|
+
case "active":
|
|
15047
|
+
return ` ${REMOTE_ACCESS_LABEL} on, port ${port}, ${pairedDevices} paired device${pairedDevices === 1 ? "" : "s"}`;
|
|
15048
|
+
case "port-busy":
|
|
15049
|
+
return ` ${REMOTE_ACCESS_LABEL} NOT WORKING: port ${port} is busy (another Agent Workbench, or another program)`;
|
|
15050
|
+
case "restart-needed":
|
|
15051
|
+
return ` ${REMOTE_ACCESS_LABEL} on, takes effect after a restart`;
|
|
15052
|
+
}
|
|
15053
|
+
}
|
|
14184
15054
|
var AVAILABLE_AGENTS_LABEL = "Available CLIs";
|
|
14185
15055
|
var HISTORY_LABEL = "History";
|
|
14186
15056
|
function missingText(text) {
|
|
@@ -14219,11 +15089,18 @@ function startupAgentLines(agents) {
|
|
|
14219
15089
|
}
|
|
14220
15090
|
return lines;
|
|
14221
15091
|
}
|
|
15092
|
+
var SUPPORT_LABEL = "Support";
|
|
15093
|
+
function supportStartupLine() {
|
|
15094
|
+
return ` ${SUPPORT_LABEL} If Agent Workbench is useful to you, consider supporting it: ${SUPPORT_URL}`;
|
|
15095
|
+
}
|
|
15096
|
+
function supportStartupLineColored() {
|
|
15097
|
+
return ` \x1B[95m${SUPPORT_LABEL}\x1B[0m \x1B[33mIf Agent Workbench is useful to you, consider supporting it:\x1B[0m \x1B[4;94m${SUPPORT_URL}\x1B[0m`;
|
|
15098
|
+
}
|
|
14222
15099
|
|
|
14223
15100
|
// packages/server/src/notes-store.ts
|
|
14224
|
-
import { randomUUID as
|
|
14225
|
-
import { mkdir as
|
|
14226
|
-
import
|
|
15101
|
+
import { randomUUID as randomUUID4 } from "node:crypto";
|
|
15102
|
+
import { mkdir as mkdir11, readFile as readFile14, rename as rename5, rm as rm4, writeFile as writeFile6 } from "node:fs/promises";
|
|
15103
|
+
import path38 from "node:path";
|
|
14227
15104
|
var STATE_VERSION2 = 1;
|
|
14228
15105
|
var WRITE_DEBOUNCE_MS2 = 500;
|
|
14229
15106
|
var NotesError = class extends ServerTextError {
|
|
@@ -14266,7 +15143,7 @@ var NotesStore = class {
|
|
|
14266
15143
|
}
|
|
14267
15144
|
async load() {
|
|
14268
15145
|
try {
|
|
14269
|
-
const raw = await
|
|
15146
|
+
const raw = await readFile14(this.statePath, "utf8");
|
|
14270
15147
|
this.notes = parseState2(raw) ?? [];
|
|
14271
15148
|
} catch {
|
|
14272
15149
|
this.notes = [];
|
|
@@ -14350,11 +15227,11 @@ var NotesStore = class {
|
|
|
14350
15227
|
throw new NotesError(serverText("imageUnknownFormat"));
|
|
14351
15228
|
}
|
|
14352
15229
|
const image = {
|
|
14353
|
-
imageId:
|
|
15230
|
+
imageId: randomUUID4(),
|
|
14354
15231
|
mediaType: format.mediaType,
|
|
14355
15232
|
bytes: bytes.length
|
|
14356
15233
|
};
|
|
14357
|
-
await
|
|
15234
|
+
await mkdir11(this.imagesDir, { recursive: true });
|
|
14358
15235
|
await writeFile6(this.imagePath(image), bytes);
|
|
14359
15236
|
note.images.push(image);
|
|
14360
15237
|
note.updatedAt = now;
|
|
@@ -14383,7 +15260,7 @@ var NotesStore = class {
|
|
|
14383
15260
|
const image = this.findImage(imageId);
|
|
14384
15261
|
if (image === null) return null;
|
|
14385
15262
|
try {
|
|
14386
|
-
const bytes = await
|
|
15263
|
+
const bytes = await readFile14(this.imagePath(image));
|
|
14387
15264
|
return { mediaType: image.mediaType, data: bytes.toString("base64") };
|
|
14388
15265
|
} catch {
|
|
14389
15266
|
return null;
|
|
@@ -14403,7 +15280,7 @@ var NotesStore = class {
|
|
|
14403
15280
|
const images = [];
|
|
14404
15281
|
for (const image of note.images) {
|
|
14405
15282
|
try {
|
|
14406
|
-
const bytes = await
|
|
15283
|
+
const bytes = await readFile14(this.imagePath(image));
|
|
14407
15284
|
images.push({ mediaType: image.mediaType, data: bytes.toString("base64") });
|
|
14408
15285
|
} catch {
|
|
14409
15286
|
}
|
|
@@ -14427,7 +15304,7 @@ var NotesStore = class {
|
|
|
14427
15304
|
}
|
|
14428
15305
|
imagePath(image) {
|
|
14429
15306
|
const safe = image.imageId.replace(/[^a-zA-Z0-9-]/g, "");
|
|
14430
|
-
return
|
|
15307
|
+
return path38.join(this.imagesDir, `${safe}.${extensionFor(image.mediaType)}`);
|
|
14431
15308
|
}
|
|
14432
15309
|
async unlinkImage(image) {
|
|
14433
15310
|
await rm4(this.imagePath(image), { force: true }).catch(() => {
|
|
@@ -14445,8 +15322,8 @@ var NotesStore = class {
|
|
|
14445
15322
|
async writeNow() {
|
|
14446
15323
|
this.dirty = false;
|
|
14447
15324
|
try {
|
|
14448
|
-
await
|
|
14449
|
-
const temporary =
|
|
15325
|
+
await mkdir11(path38.dirname(this.statePath), { recursive: true });
|
|
15326
|
+
const temporary = path38.join(path38.dirname(this.statePath), `notes.${process.pid}.tmp`);
|
|
14450
15327
|
await writeFile6(
|
|
14451
15328
|
temporary,
|
|
14452
15329
|
JSON.stringify({ version: STATE_VERSION2, notes: this.notes }, null, 2),
|
|
@@ -14459,10 +15336,226 @@ var NotesStore = class {
|
|
|
14459
15336
|
}
|
|
14460
15337
|
};
|
|
14461
15338
|
|
|
15339
|
+
// packages/server/src/draft-store.ts
|
|
15340
|
+
import { mkdir as mkdir12, readFile as readFile15, rename as rename6, writeFile as writeFile7 } from "node:fs/promises";
|
|
15341
|
+
import path39 from "node:path";
|
|
15342
|
+
var STATE_VERSION3 = 1;
|
|
15343
|
+
var WRITE_DEBOUNCE_MS3 = 500;
|
|
15344
|
+
var MAX_STORED_DRAFTS = 100;
|
|
15345
|
+
function draftOwnerOf(descriptor) {
|
|
15346
|
+
if (descriptor === null || descriptor.kind !== "agent" || descriptor.agent === null) return null;
|
|
15347
|
+
if (descriptor.sessionId.length === 0) return null;
|
|
15348
|
+
return { agent: descriptor.agent, sessionId: descriptor.sessionId };
|
|
15349
|
+
}
|
|
15350
|
+
function workspaceDraftOwners(state) {
|
|
15351
|
+
const owners = state.tabs.map(({ agent, sessionId }) => ({ agent, sessionId }));
|
|
15352
|
+
for (const { raw } of state.foreignTabs) {
|
|
15353
|
+
const agent = raw["agent"];
|
|
15354
|
+
const sessionId = raw["sessionId"];
|
|
15355
|
+
if (typeof agent === "string" && typeof sessionId === "string") owners.push({ agent, sessionId });
|
|
15356
|
+
}
|
|
15357
|
+
return owners;
|
|
15358
|
+
}
|
|
15359
|
+
var DraftTabs = class {
|
|
15360
|
+
seen = /* @__PURE__ */ new Set();
|
|
15361
|
+
owners = /* @__PURE__ */ new Map();
|
|
15362
|
+
update(descriptors) {
|
|
15363
|
+
const appeared = [];
|
|
15364
|
+
const moved = [];
|
|
15365
|
+
const present = /* @__PURE__ */ new Set();
|
|
15366
|
+
for (const descriptor of descriptors) {
|
|
15367
|
+
const { terminalId } = descriptor;
|
|
15368
|
+
present.add(terminalId);
|
|
15369
|
+
if (!this.seen.has(terminalId)) appeared.push(descriptor);
|
|
15370
|
+
const current = draftOwnerOf(descriptor);
|
|
15371
|
+
if (current === null) continue;
|
|
15372
|
+
const previous = this.owners.get(terminalId);
|
|
15373
|
+
if (previous !== void 0 && keyOf(previous) !== keyOf(current)) moved.push({ from: previous, to: current });
|
|
15374
|
+
this.owners.set(terminalId, current);
|
|
15375
|
+
}
|
|
15376
|
+
for (const terminalId of [...this.owners.keys()]) {
|
|
15377
|
+
if (!present.has(terminalId)) this.owners.delete(terminalId);
|
|
15378
|
+
}
|
|
15379
|
+
this.seen = present;
|
|
15380
|
+
return { appeared, moved };
|
|
15381
|
+
}
|
|
15382
|
+
};
|
|
15383
|
+
function keyOf(owner) {
|
|
15384
|
+
return JSON.stringify([owner.agent, owner.sessionId]);
|
|
15385
|
+
}
|
|
15386
|
+
function copyOf(draft) {
|
|
15387
|
+
return { text: draft.text, pasted: draft.pasted.map((paste) => ({ ...paste })) };
|
|
15388
|
+
}
|
|
15389
|
+
function parseEntry(value) {
|
|
15390
|
+
const record = asRecord(value);
|
|
15391
|
+
if (record === null) return null;
|
|
15392
|
+
const agent = asNonEmptyString(record["agent"]);
|
|
15393
|
+
const sessionId = asNonEmptyString(record["sessionId"]);
|
|
15394
|
+
const draft = parseComposerDraft(record["draft"]);
|
|
15395
|
+
const updatedAt = asFiniteNumber(record["updatedAt"]);
|
|
15396
|
+
if (agent === null || sessionId === null || draft === null || updatedAt === null) return null;
|
|
15397
|
+
if (isEmptyComposerDraft(draft) || composerDraftChars(draft) > MAX_COMPOSER_DRAFT_CHARS) return null;
|
|
15398
|
+
return { agent, sessionId, draft, updatedAt };
|
|
15399
|
+
}
|
|
15400
|
+
function parseState3(raw) {
|
|
15401
|
+
const drafts = /* @__PURE__ */ new Map();
|
|
15402
|
+
let parsed;
|
|
15403
|
+
try {
|
|
15404
|
+
parsed = JSON.parse(raw);
|
|
15405
|
+
} catch {
|
|
15406
|
+
return drafts;
|
|
15407
|
+
}
|
|
15408
|
+
const record = asRecord(parsed);
|
|
15409
|
+
if (record === null || record["version"] !== STATE_VERSION3) return drafts;
|
|
15410
|
+
for (const entry of asArrayFiltered(record["drafts"], parseEntry) ?? []) drafts.set(keyOf(entry), entry);
|
|
15411
|
+
return drafts;
|
|
15412
|
+
}
|
|
15413
|
+
function trimOldest(drafts) {
|
|
15414
|
+
if (drafts.size <= MAX_STORED_DRAFTS) return [];
|
|
15415
|
+
const oldest = [...drafts.entries()].sort(([, a], [, b]) => a.updatedAt - b.updatedAt).slice(0, drafts.size - MAX_STORED_DRAFTS).map(([key]) => key);
|
|
15416
|
+
for (const key of oldest) drafts.delete(key);
|
|
15417
|
+
return oldest;
|
|
15418
|
+
}
|
|
15419
|
+
var DraftStore = class {
|
|
15420
|
+
drafts = /* @__PURE__ */ new Map();
|
|
15421
|
+
/** Las claves que cambiaron aca desde la ultima escritura: lo unico que se escribe. */
|
|
15422
|
+
changed = /* @__PURE__ */ new Set();
|
|
15423
|
+
writeTimer = null;
|
|
15424
|
+
/** La escritura en curso: van de a una, porque dos se pisarian el temporal. */
|
|
15425
|
+
writing = Promise.resolve();
|
|
15426
|
+
statePath;
|
|
15427
|
+
now;
|
|
15428
|
+
/** Ruta y reloj inyectables para las pruebas; por defecto, la carpeta de configuracion. */
|
|
15429
|
+
constructor(options = {}) {
|
|
15430
|
+
this.statePath = options.statePath ?? composerDraftsPath();
|
|
15431
|
+
this.now = options.now ?? Date.now;
|
|
15432
|
+
}
|
|
15433
|
+
async load() {
|
|
15434
|
+
try {
|
|
15435
|
+
this.drafts = parseState3(await readFile15(this.statePath, "utf8"));
|
|
15436
|
+
} catch {
|
|
15437
|
+
this.drafts = /* @__PURE__ */ new Map();
|
|
15438
|
+
}
|
|
15439
|
+
}
|
|
15440
|
+
/** El borrador guardado de una conversacion, o null. Una copia. */
|
|
15441
|
+
get(owner) {
|
|
15442
|
+
const stored = this.drafts.get(keyOf(owner));
|
|
15443
|
+
return stored === void 0 ? null : copyOf(stored.draft);
|
|
15444
|
+
}
|
|
15445
|
+
/** Cuantos hay. Para las pruebas. */
|
|
15446
|
+
get size() {
|
|
15447
|
+
return this.drafts.size;
|
|
15448
|
+
}
|
|
15449
|
+
save(owner, draft) {
|
|
15450
|
+
if (composerDraftChars(draft) > MAX_COMPOSER_DRAFT_CHARS) {
|
|
15451
|
+
this.delete(owner);
|
|
15452
|
+
return "too-long";
|
|
15453
|
+
}
|
|
15454
|
+
if (isEmptyComposerDraft(draft)) return this.delete(owner) ? "deleted" : "unchanged";
|
|
15455
|
+
const key = keyOf(owner);
|
|
15456
|
+
const current = this.drafts.get(key);
|
|
15457
|
+
if (current !== void 0 && sameComposerDraft(current.draft, draft)) return "unchanged";
|
|
15458
|
+
this.drafts.set(key, { agent: owner.agent, sessionId: owner.sessionId, draft: copyOf(draft), updatedAt: this.now() });
|
|
15459
|
+
this.changed.add(key);
|
|
15460
|
+
for (const gone of trimOldest(this.drafts)) this.changed.add(gone);
|
|
15461
|
+
this.schedule();
|
|
15462
|
+
return "saved";
|
|
15463
|
+
}
|
|
15464
|
+
/** Borra el de esa conversacion: se mando, o se cerro su pestana. false si no habia. */
|
|
15465
|
+
delete(owner) {
|
|
15466
|
+
const key = keyOf(owner);
|
|
15467
|
+
if (!this.drafts.delete(key)) return false;
|
|
15468
|
+
this.changed.add(key);
|
|
15469
|
+
this.schedule();
|
|
15470
|
+
return true;
|
|
15471
|
+
}
|
|
15472
|
+
/**
|
|
15473
|
+
* La pestana cambio de conversacion —una CLI que la descubre, o que empieza
|
|
15474
|
+
* otra con `/clear`— y su borrador va con ella: es lo que se restaura con la
|
|
15475
|
+
* pestana, que desde ahora se guarda con el id nuevo.
|
|
15476
|
+
*/
|
|
15477
|
+
move(from, to) {
|
|
15478
|
+
const fromKey = keyOf(from);
|
|
15479
|
+
const stored = this.drafts.get(fromKey);
|
|
15480
|
+
if (stored === void 0 || fromKey === keyOf(to)) return;
|
|
15481
|
+
this.delete(from);
|
|
15482
|
+
this.save(to, stored.draft);
|
|
15483
|
+
}
|
|
15484
|
+
/**
|
|
15485
|
+
* Deja solo los de estas conversaciones: las pestanas del archivo del
|
|
15486
|
+
* arranque. Lo demas es de una pestana que ya no esta —se cerro con una build
|
|
15487
|
+
* anterior, o su carpeta se borro— y nadie lo va a pedir.
|
|
15488
|
+
*/
|
|
15489
|
+
retainOnly(owners) {
|
|
15490
|
+
const keep = new Set([...owners].map(keyOf));
|
|
15491
|
+
let removed = false;
|
|
15492
|
+
for (const key of [...this.drafts.keys()]) {
|
|
15493
|
+
if (keep.has(key)) continue;
|
|
15494
|
+
this.drafts.delete(key);
|
|
15495
|
+
this.changed.add(key);
|
|
15496
|
+
removed = true;
|
|
15497
|
+
}
|
|
15498
|
+
if (removed) this.schedule();
|
|
15499
|
+
}
|
|
15500
|
+
/** Escritura inmediata, para el apagado. */
|
|
15501
|
+
async flush() {
|
|
15502
|
+
if (this.writeTimer !== null) {
|
|
15503
|
+
clearTimeout(this.writeTimer);
|
|
15504
|
+
this.writeTimer = null;
|
|
15505
|
+
}
|
|
15506
|
+
await this.write();
|
|
15507
|
+
}
|
|
15508
|
+
schedule() {
|
|
15509
|
+
if (this.writeTimer !== null) return;
|
|
15510
|
+
this.writeTimer = setTimeout(() => {
|
|
15511
|
+
this.writeTimer = null;
|
|
15512
|
+
void this.write();
|
|
15513
|
+
}, WRITE_DEBOUNCE_MS3);
|
|
15514
|
+
this.writeTimer.unref();
|
|
15515
|
+
}
|
|
15516
|
+
write() {
|
|
15517
|
+
const run = async () => {
|
|
15518
|
+
if (this.changed.size > 0) await this.writeNow();
|
|
15519
|
+
};
|
|
15520
|
+
this.writing = this.writing.then(run, run);
|
|
15521
|
+
return this.writing;
|
|
15522
|
+
}
|
|
15523
|
+
async writeNow() {
|
|
15524
|
+
let onDisk = /* @__PURE__ */ new Map();
|
|
15525
|
+
try {
|
|
15526
|
+
onDisk = parseState3(await readFile15(this.statePath, "utf8"));
|
|
15527
|
+
} catch {
|
|
15528
|
+
}
|
|
15529
|
+
const changed = this.changed;
|
|
15530
|
+
this.changed = /* @__PURE__ */ new Set();
|
|
15531
|
+
const merged = onDisk;
|
|
15532
|
+
for (const key of changed) {
|
|
15533
|
+
const mine = this.drafts.get(key);
|
|
15534
|
+
if (mine === void 0) merged.delete(key);
|
|
15535
|
+
else merged.set(key, mine);
|
|
15536
|
+
}
|
|
15537
|
+
trimOldest(merged);
|
|
15538
|
+
this.drafts = new Map(merged);
|
|
15539
|
+
try {
|
|
15540
|
+
await mkdir12(path39.dirname(this.statePath), { recursive: true });
|
|
15541
|
+
const temporary = path39.join(path39.dirname(this.statePath), `composer-drafts.${process.pid}.tmp`);
|
|
15542
|
+
const payload = {
|
|
15543
|
+
version: STATE_VERSION3,
|
|
15544
|
+
drafts: [...merged.values()].map(({ agent, sessionId, updatedAt, draft }) => ({ agent, sessionId, updatedAt, draft }))
|
|
15545
|
+
};
|
|
15546
|
+
await writeFile7(temporary, JSON.stringify(payload, null, 2), { encoding: "utf8", mode: 384 });
|
|
15547
|
+
await rename6(temporary, this.statePath);
|
|
15548
|
+
} catch (error) {
|
|
15549
|
+
for (const key of changed) this.changed.add(key);
|
|
15550
|
+
console.warn("[drafts] couldn't save the message box drafts:", error);
|
|
15551
|
+
}
|
|
15552
|
+
}
|
|
15553
|
+
};
|
|
15554
|
+
|
|
14462
15555
|
// packages/server/src/shell-locator.ts
|
|
14463
15556
|
import { access as access2 } from "node:fs/promises";
|
|
14464
15557
|
import { constants as fsConstants2 } from "node:fs";
|
|
14465
|
-
import
|
|
15558
|
+
import path40 from "node:path";
|
|
14466
15559
|
async function isExecutable(candidate) {
|
|
14467
15560
|
try {
|
|
14468
15561
|
await access2(candidate, process.platform === "win32" ? fsConstants2.R_OK : fsConstants2.X_OK);
|
|
@@ -14474,7 +15567,7 @@ async function isExecutable(candidate) {
|
|
|
14474
15567
|
function windowsPowerShellPath() {
|
|
14475
15568
|
const systemRoot = process.env["SystemRoot"] ?? process.env["windir"];
|
|
14476
15569
|
if (systemRoot === void 0 || systemRoot.length === 0) return null;
|
|
14477
|
-
return
|
|
15570
|
+
return path40.join(systemRoot, "System32", "WindowsPowerShell", "v1.0", "powershell.exe");
|
|
14478
15571
|
}
|
|
14479
15572
|
async function locateWindowsShell() {
|
|
14480
15573
|
const pwsh = await findInPath("pwsh");
|
|
@@ -14494,7 +15587,7 @@ async function locateWindowsShell() {
|
|
|
14494
15587
|
async function locatePosixShell() {
|
|
14495
15588
|
const preferred = process.env["SHELL"];
|
|
14496
15589
|
if (preferred !== void 0 && preferred.length > 0 && await isExecutable(preferred)) {
|
|
14497
|
-
return { file: preferred, args: [], name:
|
|
15590
|
+
return { file: preferred, args: [], name: path40.basename(preferred) };
|
|
14498
15591
|
}
|
|
14499
15592
|
if (await isExecutable("/bin/bash")) return { file: "/bin/bash", args: [], name: "bash" };
|
|
14500
15593
|
if (await isExecutable("/bin/sh")) return { file: "/bin/sh", args: [], name: "sh" };
|
|
@@ -14505,13 +15598,13 @@ async function locateShell() {
|
|
|
14505
15598
|
}
|
|
14506
15599
|
|
|
14507
15600
|
// packages/server/src/session-watcher.ts
|
|
14508
|
-
import { stat as
|
|
15601
|
+
import { stat as stat21 } from "node:fs/promises";
|
|
14509
15602
|
import chokidar3 from "chokidar";
|
|
14510
15603
|
var INDEX_DEBOUNCE_MS = 750;
|
|
14511
15604
|
var ROOT_PROBE_MS = 5e3;
|
|
14512
15605
|
async function isDirectory3(target) {
|
|
14513
15606
|
try {
|
|
14514
|
-
return (await
|
|
15607
|
+
return (await stat21(target)).isDirectory();
|
|
14515
15608
|
} catch {
|
|
14516
15609
|
return false;
|
|
14517
15610
|
}
|
|
@@ -14612,9 +15705,9 @@ function watchSessions(index, hub, agents, options = {}) {
|
|
|
14612
15705
|
}
|
|
14613
15706
|
|
|
14614
15707
|
// packages/server/src/terminal-registry.ts
|
|
14615
|
-
import { randomUUID as
|
|
15708
|
+
import { randomUUID as randomUUID5 } from "node:crypto";
|
|
14616
15709
|
import { EventEmitter as EventEmitter5 } from "node:events";
|
|
14617
|
-
import { stat as
|
|
15710
|
+
import { stat as stat22 } from "node:fs/promises";
|
|
14618
15711
|
|
|
14619
15712
|
// packages/server/src/launch-hook-slot.ts
|
|
14620
15713
|
var LaunchHookSlot = class {
|
|
@@ -14876,10 +15969,10 @@ async function resolveLaunchPlan(launch) {
|
|
|
14876
15969
|
}
|
|
14877
15970
|
|
|
14878
15971
|
// packages/server/src/workspace-store.ts
|
|
14879
|
-
import { mkdir as
|
|
14880
|
-
import
|
|
14881
|
-
var
|
|
14882
|
-
var
|
|
15972
|
+
import { mkdir as mkdir13, readFile as readFile16, writeFile as writeFile8 } from "node:fs/promises";
|
|
15973
|
+
import path41 from "node:path";
|
|
15974
|
+
var STATE_VERSION4 = 1;
|
|
15975
|
+
var WRITE_DEBOUNCE_MS4 = 400;
|
|
14883
15976
|
var LEGACY_TAB_AGENT = "claude-code";
|
|
14884
15977
|
function placeByPosition(base, placed) {
|
|
14885
15978
|
const result = [...base];
|
|
@@ -14935,11 +16028,11 @@ function readTab(tab, missingAgent) {
|
|
|
14935
16028
|
if (agent === null) return null;
|
|
14936
16029
|
return { kind: "known", tab: { agent, cwd, sessionId, label: typeof label === "string" ? label : "" } };
|
|
14937
16030
|
}
|
|
14938
|
-
function
|
|
16031
|
+
function parseState4(raw) {
|
|
14939
16032
|
try {
|
|
14940
16033
|
const parsed = JSON.parse(raw);
|
|
14941
16034
|
if (!isRecord(parsed)) return null;
|
|
14942
|
-
if (parsed["version"] !==
|
|
16035
|
+
if (parsed["version"] !== STATE_VERSION4) return null;
|
|
14943
16036
|
const rawTabs = parsed["tabs"];
|
|
14944
16037
|
if (!Array.isArray(rawTabs)) return null;
|
|
14945
16038
|
const base = [];
|
|
@@ -14977,7 +16070,7 @@ function serializeState(state) {
|
|
|
14977
16070
|
otherTabs.push({ ...entry.tab, position });
|
|
14978
16071
|
}
|
|
14979
16072
|
}
|
|
14980
|
-
return otherTabs.length === 0 ? { version:
|
|
16073
|
+
return otherTabs.length === 0 ? { version: STATE_VERSION4, tabs } : { version: STATE_VERSION4, tabs, otherTabs };
|
|
14981
16074
|
}
|
|
14982
16075
|
var EMPTY_STATE = () => ({ tabs: [], foreignTabs: [] });
|
|
14983
16076
|
function persistableTabs(descriptors) {
|
|
@@ -14999,8 +16092,8 @@ var WorkspaceStore = class {
|
|
|
14999
16092
|
pending = null;
|
|
15000
16093
|
async load() {
|
|
15001
16094
|
try {
|
|
15002
|
-
const raw = await
|
|
15003
|
-
return
|
|
16095
|
+
const raw = await readFile16(workspaceStatePath(), "utf8");
|
|
16096
|
+
return parseState4(raw) ?? EMPTY_STATE();
|
|
15004
16097
|
} catch {
|
|
15005
16098
|
return EMPTY_STATE();
|
|
15006
16099
|
}
|
|
@@ -15014,7 +16107,7 @@ var WorkspaceStore = class {
|
|
|
15014
16107
|
const toWrite = this.pending;
|
|
15015
16108
|
this.pending = null;
|
|
15016
16109
|
if (toWrite !== null) void this.writeNow(toWrite);
|
|
15017
|
-
},
|
|
16110
|
+
}, WRITE_DEBOUNCE_MS4);
|
|
15018
16111
|
this.writeTimer.unref();
|
|
15019
16112
|
}
|
|
15020
16113
|
/** Escritura inmediata, para el apagado. */
|
|
@@ -15029,12 +16122,12 @@ var WorkspaceStore = class {
|
|
|
15029
16122
|
}
|
|
15030
16123
|
async writeNow(state) {
|
|
15031
16124
|
try {
|
|
15032
|
-
await
|
|
16125
|
+
await mkdir13(appConfigDir(), { recursive: true });
|
|
15033
16126
|
const target = workspaceStatePath();
|
|
15034
|
-
const temporary =
|
|
15035
|
-
await
|
|
15036
|
-
const { rename:
|
|
15037
|
-
await
|
|
16127
|
+
const temporary = path41.join(path41.dirname(target), `workspace.${process.pid}.tmp`);
|
|
16128
|
+
await writeFile8(temporary, JSON.stringify(serializeState(state), null, 2), "utf8");
|
|
16129
|
+
const { rename: rename7 } = await import("node:fs/promises");
|
|
16130
|
+
await rename7(temporary, target);
|
|
15038
16131
|
} catch (error) {
|
|
15039
16132
|
console.warn("[workspace] couldn't save the state:", error);
|
|
15040
16133
|
}
|
|
@@ -15151,14 +16244,14 @@ var TerminalRegistry = class extends EventEmitter5 {
|
|
|
15151
16244
|
}
|
|
15152
16245
|
await this.assertDirectory(options.cwd);
|
|
15153
16246
|
const resumed = kind === "agent" && options.resumeSessionId !== void 0;
|
|
15154
|
-
const terminalId =
|
|
16247
|
+
const terminalId = randomUUID5();
|
|
15155
16248
|
const descriptor = {
|
|
15156
16249
|
terminalId,
|
|
15157
16250
|
kind,
|
|
15158
16251
|
agent,
|
|
15159
16252
|
cwd: options.cwd,
|
|
15160
16253
|
// Provisional: `spawn` lo confirma con lo que diga el adaptador.
|
|
15161
|
-
sessionId: kind === "agent" ? options.resumeSessionId ??
|
|
16254
|
+
sessionId: kind === "agent" ? options.resumeSessionId ?? randomUUID5() : "",
|
|
15162
16255
|
label: options.label ?? "",
|
|
15163
16256
|
resumed,
|
|
15164
16257
|
createdAt: Date.now(),
|
|
@@ -15262,7 +16355,7 @@ var TerminalRegistry = class extends EventEmitter5 {
|
|
|
15262
16355
|
async assertDirectory(cwd) {
|
|
15263
16356
|
let info;
|
|
15264
16357
|
try {
|
|
15265
|
-
info = await
|
|
16358
|
+
info = await stat22(cwd);
|
|
15266
16359
|
} catch {
|
|
15267
16360
|
throw new TerminalOpenError("invalid-cwd", serverText("cwdMissing", { cwd }));
|
|
15268
16361
|
}
|
|
@@ -15290,7 +16383,7 @@ var TerminalRegistry = class extends EventEmitter5 {
|
|
|
15290
16383
|
let launch;
|
|
15291
16384
|
let adapter = null;
|
|
15292
16385
|
let launchedSessionId = sessionId;
|
|
15293
|
-
const launchToken =
|
|
16386
|
+
const launchToken = randomUUID5();
|
|
15294
16387
|
if (launcher.kind === "agent") {
|
|
15295
16388
|
adapter = launcher.agent.adapter;
|
|
15296
16389
|
const { location } = launcher.agent;
|
|
@@ -15526,7 +16619,7 @@ var TerminalRegistry = class extends EventEmitter5 {
|
|
|
15526
16619
|
console.warn(`[workspace] the tab of ${tab.cwd} wasn't restored: ${reason2}`);
|
|
15527
16620
|
continue;
|
|
15528
16621
|
}
|
|
15529
|
-
const terminalId =
|
|
16622
|
+
const terminalId = randomUUID5();
|
|
15530
16623
|
const descriptor = {
|
|
15531
16624
|
terminalId,
|
|
15532
16625
|
kind: "agent",
|
|
@@ -15688,20 +16781,20 @@ var TerminalRegistry = class extends EventEmitter5 {
|
|
|
15688
16781
|
import { WebSocketServer } from "ws";
|
|
15689
16782
|
|
|
15690
16783
|
// packages/server/src/directory-picker.ts
|
|
15691
|
-
import { randomUUID as
|
|
15692
|
-
import { mkdir as
|
|
16784
|
+
import { randomUUID as randomUUID6 } from "node:crypto";
|
|
16785
|
+
import { mkdir as mkdir14, readdir as readdir11, stat as stat23 } from "node:fs/promises";
|
|
15693
16786
|
import { homedir as homedir11 } from "node:os";
|
|
15694
|
-
import
|
|
16787
|
+
import path42 from "node:path";
|
|
15695
16788
|
var MAX_ENTRIES = 500;
|
|
15696
16789
|
var MAX_PICKERS = 8;
|
|
15697
16790
|
var SKIPPED = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "bin", "obj", "$RECYCLE.BIN"]);
|
|
15698
16791
|
var DirectoryPickerError = class extends ServerTextError {
|
|
15699
16792
|
};
|
|
15700
16793
|
function isInsideProtected(target, dirs) {
|
|
15701
|
-
const resolved =
|
|
16794
|
+
const resolved = path42.resolve(target);
|
|
15702
16795
|
return dirs.some((dir) => {
|
|
15703
|
-
const protectedDir =
|
|
15704
|
-
return resolved === protectedDir || resolved.startsWith(protectedDir +
|
|
16796
|
+
const protectedDir = path42.resolve(dir);
|
|
16797
|
+
return resolved === protectedDir || resolved.startsWith(protectedDir + path42.sep);
|
|
15705
16798
|
});
|
|
15706
16799
|
}
|
|
15707
16800
|
async function listRoots() {
|
|
@@ -15713,7 +16806,7 @@ async function listRoots() {
|
|
|
15713
16806
|
letters.map(async (letter) => {
|
|
15714
16807
|
const drive = `${letter}:\\`;
|
|
15715
16808
|
try {
|
|
15716
|
-
await
|
|
16809
|
+
await stat23(drive);
|
|
15717
16810
|
return drive;
|
|
15718
16811
|
} catch {
|
|
15719
16812
|
return null;
|
|
@@ -15740,7 +16833,7 @@ var DirectoryPickers = class {
|
|
|
15740
16833
|
if (this.pickers.size >= MAX_PICKERS) {
|
|
15741
16834
|
throw new DirectoryPickerError(serverText("pickerTooMany"));
|
|
15742
16835
|
}
|
|
15743
|
-
const picker = { id:
|
|
16836
|
+
const picker = { id: randomUUID6(), current: homedir11(), entries: /* @__PURE__ */ new Set() };
|
|
15744
16837
|
this.pickers.set(picker.id, picker);
|
|
15745
16838
|
return this.listing(picker);
|
|
15746
16839
|
}
|
|
@@ -15765,14 +16858,14 @@ var DirectoryPickers = class {
|
|
|
15765
16858
|
async enter(pickerId, name) {
|
|
15766
16859
|
const picker = this.require(pickerId);
|
|
15767
16860
|
if (name === "..") {
|
|
15768
|
-
const parent =
|
|
16861
|
+
const parent = path42.dirname(picker.current);
|
|
15769
16862
|
if (parent !== picker.current) picker.current = parent;
|
|
15770
16863
|
return this.listing(picker);
|
|
15771
16864
|
}
|
|
15772
16865
|
if (!picker.entries.has(name)) {
|
|
15773
16866
|
throw new DirectoryPickerError(serverText("pickerNotListed"));
|
|
15774
16867
|
}
|
|
15775
|
-
const target =
|
|
16868
|
+
const target = path42.join(picker.current, name);
|
|
15776
16869
|
if (isInsideProtected(target, this.protectedDirs)) {
|
|
15777
16870
|
throw new DirectoryPickerError(serverText("pickerCliFolder"));
|
|
15778
16871
|
}
|
|
@@ -15812,12 +16905,12 @@ var DirectoryPickers = class {
|
|
|
15812
16905
|
throw new DirectoryPickerError(serverText("pickerNameChars"));
|
|
15813
16906
|
}
|
|
15814
16907
|
}
|
|
15815
|
-
const target =
|
|
16908
|
+
const target = path42.join(picker.current, clean);
|
|
15816
16909
|
if (isInsideProtected(target, this.protectedDirs)) {
|
|
15817
16910
|
throw new DirectoryPickerError(serverText("pickerNoProjectsHere"));
|
|
15818
16911
|
}
|
|
15819
16912
|
try {
|
|
15820
|
-
await
|
|
16913
|
+
await mkdir14(target);
|
|
15821
16914
|
} catch (error) {
|
|
15822
16915
|
const code = error.code;
|
|
15823
16916
|
throw new DirectoryPickerError(
|
|
@@ -15849,7 +16942,7 @@ var DirectoryPickers = class {
|
|
|
15849
16942
|
let truncated = false;
|
|
15850
16943
|
try {
|
|
15851
16944
|
const found = await readdir11(picker.current, { withFileTypes: true });
|
|
15852
|
-
const dirs = found.filter((entry) => entry.isDirectory()).map((entry) => entry.name).filter((name) => !SKIPPED.has(name)).filter((name) => !isInsideProtected(
|
|
16945
|
+
const dirs = found.filter((entry) => entry.isDirectory()).map((entry) => entry.name).filter((name) => !SKIPPED.has(name)).filter((name) => !isInsideProtected(path42.join(picker.current, name), this.protectedDirs)).sort((a, b) => a.localeCompare(b));
|
|
15853
16946
|
truncated = dirs.length > MAX_ENTRIES;
|
|
15854
16947
|
names = dirs.slice(0, MAX_ENTRIES);
|
|
15855
16948
|
} catch {
|
|
@@ -15860,15 +16953,15 @@ var DirectoryPickers = class {
|
|
|
15860
16953
|
path: picker.current,
|
|
15861
16954
|
entries: names,
|
|
15862
16955
|
roots,
|
|
15863
|
-
canGoUp:
|
|
16956
|
+
canGoUp: path42.dirname(picker.current) !== picker.current,
|
|
15864
16957
|
truncated
|
|
15865
16958
|
};
|
|
15866
16959
|
}
|
|
15867
16960
|
};
|
|
15868
16961
|
|
|
15869
16962
|
// packages/server/src/file-browser.ts
|
|
15870
|
-
import { readFile as
|
|
15871
|
-
import
|
|
16963
|
+
import { readFile as readFile17, readdir as readdir12, stat as stat24 } from "node:fs/promises";
|
|
16964
|
+
import path43 from "node:path";
|
|
15872
16965
|
var MAX_ENTRIES2 = 1e3;
|
|
15873
16966
|
var SEARCH_MAX_RESULTS = 200;
|
|
15874
16967
|
var SEARCH_MAX_DIRECTORIES = 4e3;
|
|
@@ -15931,7 +17024,7 @@ function languageFor(fileName) {
|
|
|
15931
17024
|
const lower = fileName.toLowerCase();
|
|
15932
17025
|
const byName = LANGUAGE_BY_NAME[lower];
|
|
15933
17026
|
if (byName !== void 0) return byName;
|
|
15934
|
-
const extension =
|
|
17027
|
+
const extension = path43.extname(lower);
|
|
15935
17028
|
return LANGUAGE_BY_EXTENSION[extension] ?? null;
|
|
15936
17029
|
}
|
|
15937
17030
|
function looksBinary2(buffer) {
|
|
@@ -15963,7 +17056,7 @@ async function listDirectory(cwd, relativePath, options = {}) {
|
|
|
15963
17056
|
let sizeBytes = 0;
|
|
15964
17057
|
let modifiedAt = 0;
|
|
15965
17058
|
try {
|
|
15966
|
-
const info = await
|
|
17059
|
+
const info = await stat24(path43.join(absolute, candidate.name));
|
|
15967
17060
|
sizeBytes = candidate.isDirectory ? 0 : info.size;
|
|
15968
17061
|
modifiedAt = info.mtimeMs;
|
|
15969
17062
|
} catch {
|
|
@@ -15996,7 +17089,7 @@ async function readLevel(absolute) {
|
|
|
15996
17089
|
const always = ALWAYS_SKIP.has(dirent.name.toLowerCase());
|
|
15997
17090
|
if (dirent.isSymbolicLink()) {
|
|
15998
17091
|
try {
|
|
15999
|
-
const target = await
|
|
17092
|
+
const target = await stat24(path43.join(absolute, dirent.name));
|
|
16000
17093
|
candidates.push({
|
|
16001
17094
|
name: dirent.name,
|
|
16002
17095
|
isDirectory: target.isDirectory(),
|
|
@@ -16025,7 +17118,7 @@ async function markIgnored(repoRoot2, levels) {
|
|
|
16025
17118
|
for (const level of levels) {
|
|
16026
17119
|
for (const entry of level.entries) {
|
|
16027
17120
|
if (entry.hidden !== null) continue;
|
|
16028
|
-
keys.set(entry, toPosixPath(
|
|
17121
|
+
keys.set(entry, toPosixPath(path43.relative(repoRoot2, path43.join(level.absolute, entry.name))));
|
|
16029
17122
|
}
|
|
16030
17123
|
}
|
|
16031
17124
|
if (keys.size === 0) return;
|
|
@@ -16086,7 +17179,7 @@ async function searchFiles(cwd, query, options = {}) {
|
|
|
16086
17179
|
let sizeBytes = 0;
|
|
16087
17180
|
let modifiedAt = 0;
|
|
16088
17181
|
try {
|
|
16089
|
-
const info = await
|
|
17182
|
+
const info = await stat24(path43.join(level.absolute, candidate.name));
|
|
16090
17183
|
sizeBytes = candidate.isDirectory ? 0 : info.size;
|
|
16091
17184
|
modifiedAt = info.mtimeMs;
|
|
16092
17185
|
} catch {
|
|
@@ -16120,12 +17213,12 @@ async function searchFiles(cwd, query, options = {}) {
|
|
|
16120
17213
|
}
|
|
16121
17214
|
async function readPreview(cwd, relativePath) {
|
|
16122
17215
|
const absolute = await resolveInside(cwd, relativePath, { mustExist: true });
|
|
16123
|
-
const info = await
|
|
17216
|
+
const info = await stat24(absolute);
|
|
16124
17217
|
if (info.isDirectory()) {
|
|
16125
17218
|
throw new Error("It's a directory, not a file.");
|
|
16126
17219
|
}
|
|
16127
|
-
const content = await
|
|
16128
|
-
const fileName =
|
|
17220
|
+
const content = await readFile17(absolute);
|
|
17221
|
+
const fileName = path43.basename(absolute);
|
|
16129
17222
|
if (looksBinary2(content)) {
|
|
16130
17223
|
return {
|
|
16131
17224
|
path: relativePath,
|
|
@@ -16714,14 +17807,14 @@ var TerminalWriteQueue = class {
|
|
|
16714
17807
|
};
|
|
16715
17808
|
|
|
16716
17809
|
// packages/server/src/vault/service.ts
|
|
16717
|
-
import { mkdir as
|
|
17810
|
+
import { mkdir as mkdir17, stat as stat28 } from "node:fs/promises";
|
|
16718
17811
|
import { homedir as homedir12 } from "node:os";
|
|
16719
|
-
import
|
|
17812
|
+
import path47 from "node:path";
|
|
16720
17813
|
|
|
16721
17814
|
// packages/server/src/vault/move.ts
|
|
16722
17815
|
import { constants } from "node:fs";
|
|
16723
|
-
import { copyFile as copyFile2, lstat, mkdir as
|
|
16724
|
-
import
|
|
17816
|
+
import { copyFile as copyFile2, lstat, mkdir as mkdir15, readdir as readdir13, rm as rm5, stat as stat25, utimes, writeFile as writeFile9 } from "node:fs/promises";
|
|
17817
|
+
import path44 from "node:path";
|
|
16725
17818
|
var MOVED_ENTRIES = ["vault.json", "sessions", "memory"];
|
|
16726
17819
|
var OWN_TEMP_FILE = /\.\d+\.[0-9a-f]{12}\.tmp$/;
|
|
16727
17820
|
async function copyTree(source, target, result) {
|
|
@@ -16732,14 +17825,14 @@ async function copyTree(source, target, result) {
|
|
|
16732
17825
|
return;
|
|
16733
17826
|
}
|
|
16734
17827
|
if (info.isDirectory()) {
|
|
16735
|
-
await
|
|
17828
|
+
await mkdir15(target, { recursive: true });
|
|
16736
17829
|
for (const name of await readdir13(source)) {
|
|
16737
|
-
await copyTree(
|
|
17830
|
+
await copyTree(path44.join(source, name), path44.join(target, name), result);
|
|
16738
17831
|
}
|
|
16739
17832
|
return;
|
|
16740
17833
|
}
|
|
16741
17834
|
if (!info.isFile() || OWN_TEMP_FILE.test(source)) return;
|
|
16742
|
-
await
|
|
17835
|
+
await mkdir15(path44.dirname(target), { recursive: true });
|
|
16743
17836
|
try {
|
|
16744
17837
|
await copyFile2(source, target, constants.COPYFILE_EXCL);
|
|
16745
17838
|
} catch (error) {
|
|
@@ -16754,9 +17847,9 @@ async function copyTree(source, target, result) {
|
|
|
16754
17847
|
}
|
|
16755
17848
|
async function moveVault(from, to) {
|
|
16756
17849
|
const result = { copied: 0, skipped: 0 };
|
|
16757
|
-
await
|
|
17850
|
+
await mkdir15(to, { recursive: true });
|
|
16758
17851
|
for (const entry of MOVED_ENTRIES) {
|
|
16759
|
-
await copyTree(
|
|
17852
|
+
await copyTree(path44.join(from, entry), path44.join(to, entry), result);
|
|
16760
17853
|
}
|
|
16761
17854
|
return result;
|
|
16762
17855
|
}
|
|
@@ -16788,11 +17881,11 @@ function checkVaultTarget(current, target, options) {
|
|
|
16788
17881
|
return { kind: "ok" };
|
|
16789
17882
|
}
|
|
16790
17883
|
async function canWriteInto(dir) {
|
|
16791
|
-
const probe = temporaryPathFor(
|
|
17884
|
+
const probe = temporaryPathFor(path44.join(dir, ".escritura"));
|
|
16792
17885
|
try {
|
|
16793
|
-
await
|
|
16794
|
-
if (!(await
|
|
16795
|
-
await
|
|
17886
|
+
await mkdir15(dir, { recursive: true });
|
|
17887
|
+
if (!(await stat25(dir)).isDirectory()) return false;
|
|
17888
|
+
await writeFile9(probe, "");
|
|
16796
17889
|
return true;
|
|
16797
17890
|
} catch {
|
|
16798
17891
|
return false;
|
|
@@ -16802,12 +17895,12 @@ async function canWriteInto(dir) {
|
|
|
16802
17895
|
}
|
|
16803
17896
|
|
|
16804
17897
|
// packages/server/src/vault/serialize.ts
|
|
16805
|
-
import { createHash as
|
|
17898
|
+
import { createHash as createHash4 } from "node:crypto";
|
|
16806
17899
|
function imageKey(eventId, source, index) {
|
|
16807
17900
|
return `${eventId}|${source}|${index}`;
|
|
16808
17901
|
}
|
|
16809
17902
|
function assetName(bytes) {
|
|
16810
|
-
const hash =
|
|
17903
|
+
const hash = createHash4("sha256").update(bytes).digest("hex").slice(0, 32);
|
|
16811
17904
|
return `${hash}.${detectImageFormat(bytes)?.ext ?? "bin"}`;
|
|
16812
17905
|
}
|
|
16813
17906
|
function buildHeader(input) {
|
|
@@ -16894,12 +17987,12 @@ function serializeSession(input) {
|
|
|
16894
17987
|
}
|
|
16895
17988
|
|
|
16896
17989
|
// packages/server/src/vault/writer.ts
|
|
16897
|
-
import { readdir as readdir15, rm as rm6, stat as
|
|
16898
|
-
import
|
|
17990
|
+
import { readdir as readdir15, rm as rm6, stat as stat27 } from "node:fs/promises";
|
|
17991
|
+
import path46 from "node:path";
|
|
16899
17992
|
|
|
16900
17993
|
// packages/server/src/vault/memory-copy.ts
|
|
16901
|
-
import { mkdir as
|
|
16902
|
-
import
|
|
17994
|
+
import { mkdir as mkdir16, readdir as readdir14, readFile as readFile18, stat as stat26, utimes as utimes2 } from "node:fs/promises";
|
|
17995
|
+
import path45 from "node:path";
|
|
16903
17996
|
var MEMORY_FILE_MAX_BYTES = 1024 * 1024;
|
|
16904
17997
|
var MEMORY_MAX_FILES = 500;
|
|
16905
17998
|
var MEMORY_SOURCE_FILE = "source.json";
|
|
@@ -16924,7 +18017,7 @@ async function listMemoryNotes(cwd) {
|
|
|
16924
18017
|
let absolute;
|
|
16925
18018
|
try {
|
|
16926
18019
|
absolute = await resolveInside(cwd, `${MEMORY_DIR}/${name}`, { mustExist: true });
|
|
16927
|
-
const info = await
|
|
18020
|
+
const info = await stat26(absolute);
|
|
16928
18021
|
if (!info.isFile() || info.size > MEMORY_FILE_MAX_BYTES) continue;
|
|
16929
18022
|
notes.push({ name, absolute, size: info.size, mtimeMs: info.mtimeMs });
|
|
16930
18023
|
} catch {
|
|
@@ -16938,7 +18031,7 @@ async function measureProjectMemory(cwd) {
|
|
|
16938
18031
|
}
|
|
16939
18032
|
async function isUpToDate(target, note) {
|
|
16940
18033
|
try {
|
|
16941
|
-
const info = await
|
|
18034
|
+
const info = await stat26(target);
|
|
16942
18035
|
return info.isFile() && info.size === note.size && Math.abs(info.mtimeMs - note.mtimeMs) < SAME_MTIME_MS;
|
|
16943
18036
|
} catch {
|
|
16944
18037
|
return false;
|
|
@@ -16946,7 +18039,7 @@ async function isUpToDate(target, note) {
|
|
|
16946
18039
|
}
|
|
16947
18040
|
async function exists3(file) {
|
|
16948
18041
|
try {
|
|
16949
|
-
await
|
|
18042
|
+
await stat26(file);
|
|
16950
18043
|
return true;
|
|
16951
18044
|
} catch {
|
|
16952
18045
|
return false;
|
|
@@ -16962,14 +18055,14 @@ async function copyProjectMemory(dir, cwd, platform, deps = DEFAULT_WRITE_DEPS)
|
|
|
16962
18055
|
if (prepared) return;
|
|
16963
18056
|
prepared = true;
|
|
16964
18057
|
await ensureVaultMarker(dir, deps);
|
|
16965
|
-
await
|
|
18058
|
+
await mkdir16(target, { recursive: true });
|
|
16966
18059
|
};
|
|
16967
18060
|
for (const note of notes) {
|
|
16968
|
-
const destination =
|
|
18061
|
+
const destination = path45.join(target, note.name);
|
|
16969
18062
|
if (await isUpToDate(destination, note)) continue;
|
|
16970
18063
|
let content;
|
|
16971
18064
|
try {
|
|
16972
|
-
content = await
|
|
18065
|
+
content = await readFile18(note.absolute);
|
|
16973
18066
|
} catch {
|
|
16974
18067
|
continue;
|
|
16975
18068
|
}
|
|
@@ -16981,7 +18074,7 @@ async function copyProjectMemory(dir, cwd, platform, deps = DEFAULT_WRITE_DEPS)
|
|
|
16981
18074
|
result.files += 1;
|
|
16982
18075
|
result.bytes += content.length;
|
|
16983
18076
|
}
|
|
16984
|
-
const source =
|
|
18077
|
+
const source = path45.join(target, MEMORY_SOURCE_FILE);
|
|
16985
18078
|
if (result.files > 0 || !await exists3(source)) {
|
|
16986
18079
|
await prepare();
|
|
16987
18080
|
await writeFileAtomic(source, `${JSON.stringify({ cwd, copiedAt: deps.now() })}
|
|
@@ -17475,7 +18568,7 @@ var VaultWriter = class {
|
|
|
17475
18568
|
}
|
|
17476
18569
|
async assetOnDisk(dir, agent, sessionId, asset, bytes) {
|
|
17477
18570
|
try {
|
|
17478
|
-
const info = await
|
|
18571
|
+
const info = await stat27(assetFile(dir, agent, sessionId, asset));
|
|
17479
18572
|
return info.isFile() && info.size === bytes;
|
|
17480
18573
|
} catch {
|
|
17481
18574
|
return false;
|
|
@@ -17575,17 +18668,17 @@ var VaultWriter = class {
|
|
|
17575
18668
|
const folders = [dir];
|
|
17576
18669
|
const subfolders = async (parent) => {
|
|
17577
18670
|
try {
|
|
17578
|
-
return (await readdir15(parent, { withFileTypes: true })).filter((entry) => entry.isDirectory()).map((entry) =>
|
|
18671
|
+
return (await readdir15(parent, { withFileTypes: true })).filter((entry) => entry.isDirectory()).map((entry) => path46.join(parent, entry.name));
|
|
17579
18672
|
} catch {
|
|
17580
18673
|
return [];
|
|
17581
18674
|
}
|
|
17582
18675
|
};
|
|
17583
18676
|
for (const agentDir of await subfolders(sessionsRoot2(dir))) {
|
|
17584
|
-
if (!SESSION_AGENT_IDS.includes(
|
|
18677
|
+
if (!SESSION_AGENT_IDS.includes(path46.basename(agentDir))) continue;
|
|
17585
18678
|
folders.push(agentDir);
|
|
17586
18679
|
folders.push(...(await subfolders(agentDir)).filter((folder) => folder.endsWith(".assets")));
|
|
17587
18680
|
}
|
|
17588
|
-
folders.push(...await subfolders(
|
|
18681
|
+
folders.push(...await subfolders(path46.join(dir, "memory")));
|
|
17589
18682
|
for (const folder of folders) {
|
|
17590
18683
|
let names;
|
|
17591
18684
|
try {
|
|
@@ -17595,9 +18688,9 @@ var VaultWriter = class {
|
|
|
17595
18688
|
}
|
|
17596
18689
|
for (const name of names) {
|
|
17597
18690
|
if (!OWN_TEMP_FILE2.test(name)) continue;
|
|
17598
|
-
const file =
|
|
18691
|
+
const file = path46.join(folder, name);
|
|
17599
18692
|
try {
|
|
17600
|
-
const info = await
|
|
18693
|
+
const info = await stat27(file);
|
|
17601
18694
|
if (info.isFile() && now - info.mtimeMs > TEMP_MAX_AGE_MS) await rm6(file, { force: true });
|
|
17602
18695
|
} catch {
|
|
17603
18696
|
}
|
|
@@ -17785,14 +18878,14 @@ var VaultService = class {
|
|
|
17785
18878
|
async setDir(target) {
|
|
17786
18879
|
const { platform, agents, settings: settings2, catalog } = this.options;
|
|
17787
18880
|
const current = this.dir();
|
|
17788
|
-
const protectedDirs = [...agents.protectedDirs(),
|
|
18881
|
+
const protectedDirs = [...agents.protectedDirs(), path47.join(this.options.homeDir ?? homedir12(), ".gemini")];
|
|
17789
18882
|
const check = checkVaultTarget(current, target, { protectedDirs, platform });
|
|
17790
18883
|
if (check.kind === "refused") throw new VaultError(check.text);
|
|
17791
18884
|
if (check.kind === "same") return;
|
|
17792
18885
|
try {
|
|
17793
18886
|
await this.writer.exclusive("moving", async () => {
|
|
17794
18887
|
if (!await canWriteInto(target)) throw new VaultError(serverText("vaultCannotWrite"));
|
|
17795
|
-
const hadCopy = await
|
|
18888
|
+
const hadCopy = await stat28(vaultMarkerFile(current)).then(
|
|
17796
18889
|
() => true,
|
|
17797
18890
|
() => false
|
|
17798
18891
|
);
|
|
@@ -17826,7 +18919,7 @@ var VaultService = class {
|
|
|
17826
18919
|
/** Abre la carpeta de la copia con el explorador del sistema. */
|
|
17827
18920
|
async reveal() {
|
|
17828
18921
|
const dir = this.activeDir();
|
|
17829
|
-
const isFolder = await
|
|
18922
|
+
const isFolder = await stat28(dir).then(
|
|
17830
18923
|
(info) => info.isDirectory(),
|
|
17831
18924
|
() => false
|
|
17832
18925
|
);
|
|
@@ -17850,7 +18943,7 @@ var VaultService = class {
|
|
|
17850
18943
|
fromCopy: true
|
|
17851
18944
|
});
|
|
17852
18945
|
const file = sessionExportFile(dir, agent, sessionId);
|
|
17853
|
-
await
|
|
18946
|
+
await mkdir17(path47.dirname(file), { recursive: true });
|
|
17854
18947
|
await writeFileAtomic(file, text, this.writeDeps);
|
|
17855
18948
|
this.openWithSystem(file);
|
|
17856
18949
|
return file;
|
|
@@ -17937,7 +19030,7 @@ var VaultService = class {
|
|
|
17937
19030
|
const project = this.options.index.getProjects().find((candidate) => candidate.key === projectKey);
|
|
17938
19031
|
if (project === void 0) throw new VaultError(serverText("vaultProjectGone"));
|
|
17939
19032
|
const folder = projectExportDirFor(this.activeDir(), project, this.options.platform);
|
|
17940
|
-
await
|
|
19033
|
+
await mkdir17(folder, { recursive: true });
|
|
17941
19034
|
const entries = [];
|
|
17942
19035
|
const names = /* @__PURE__ */ new Set();
|
|
17943
19036
|
let skipped = 0;
|
|
@@ -17954,7 +19047,7 @@ var VaultService = class {
|
|
|
17954
19047
|
continue;
|
|
17955
19048
|
}
|
|
17956
19049
|
const fileName = this.exportFileName(summary, names);
|
|
17957
|
-
await writeFileAtomic(
|
|
19050
|
+
await writeFileAtomic(path47.join(folder, fileName), text, this.writeDeps);
|
|
17958
19051
|
entries.push({
|
|
17959
19052
|
updatedAt: summary.updatedAt,
|
|
17960
19053
|
agentLabel: this.labelOf(summary.agent),
|
|
@@ -17965,9 +19058,9 @@ var VaultService = class {
|
|
|
17965
19058
|
});
|
|
17966
19059
|
await nextTurn3();
|
|
17967
19060
|
}
|
|
17968
|
-
const name = project.cwd.length > 0 ?
|
|
19061
|
+
const name = project.cwd.length > 0 ? path47.basename(project.cwd) || project.cwd : project.fallbackName;
|
|
17969
19062
|
const index = renderProjectIndex({ name, cwd: project.cwd, exportedAt: this.now(), entries, skipped });
|
|
17970
|
-
await writeFileAtomic(
|
|
19063
|
+
await writeFileAtomic(path47.join(folder, "index.md"), index, this.writeDeps);
|
|
17971
19064
|
this.openWithSystem(folder);
|
|
17972
19065
|
return { sessions: entries.length, skipped, folder };
|
|
17973
19066
|
}
|
|
@@ -18123,12 +19216,14 @@ function attachTerminalSocket(options) {
|
|
|
18123
19216
|
index,
|
|
18124
19217
|
archived,
|
|
18125
19218
|
notes,
|
|
19219
|
+
drafts,
|
|
18126
19220
|
conversations,
|
|
18127
19221
|
repos,
|
|
18128
19222
|
memory,
|
|
18129
19223
|
agents,
|
|
18130
19224
|
defaultCwd,
|
|
18131
|
-
vault
|
|
19225
|
+
vault,
|
|
19226
|
+
remote
|
|
18132
19227
|
} = options;
|
|
18133
19228
|
const pasteStore = options.pasteStore ?? new PasteStore();
|
|
18134
19229
|
const writeQueue = new TerminalWriteQueue();
|
|
@@ -18143,6 +19238,14 @@ function attachTerminalSocket(options) {
|
|
|
18143
19238
|
if (socket.readyState === socket.OPEN) socket.send(payload);
|
|
18144
19239
|
}
|
|
18145
19240
|
};
|
|
19241
|
+
const remoteSockets = /* @__PURE__ */ new Map();
|
|
19242
|
+
let phonePairingOwner = null;
|
|
19243
|
+
const broadcastToHost = (message) => {
|
|
19244
|
+
const payload = encodeServerMessage(message);
|
|
19245
|
+
for (const socket of clients) {
|
|
19246
|
+
if (!remoteSockets.has(socket) && socket.readyState === socket.OPEN) socket.send(payload);
|
|
19247
|
+
}
|
|
19248
|
+
};
|
|
18146
19249
|
const broadcastExcept = (except, message) => {
|
|
18147
19250
|
const payload = encodeServerMessage(message);
|
|
18148
19251
|
for (const socket of clients) {
|
|
@@ -18193,7 +19296,25 @@ function attachTerminalSocket(options) {
|
|
|
18193
19296
|
}
|
|
18194
19297
|
return false;
|
|
18195
19298
|
};
|
|
18196
|
-
const
|
|
19299
|
+
const draftEntriesOf = (descriptors) => {
|
|
19300
|
+
const entries = [];
|
|
19301
|
+
for (const descriptor of descriptors) {
|
|
19302
|
+
const owner = draftOwnerOf(descriptor);
|
|
19303
|
+
const draft = owner === null ? null : drafts.get(owner);
|
|
19304
|
+
if (draft !== null) entries.push({ terminalId: descriptor.terminalId, draft });
|
|
19305
|
+
}
|
|
19306
|
+
return entries;
|
|
19307
|
+
};
|
|
19308
|
+
const draftTabs = new DraftTabs();
|
|
19309
|
+
draftTabs.update(registry.list());
|
|
19310
|
+
const onRegistryChanged = () => {
|
|
19311
|
+
const terminals = registry.list();
|
|
19312
|
+
broadcast({ type: "terminal.list", terminals, order: registry.getOrder() });
|
|
19313
|
+
const { appeared, moved } = draftTabs.update(terminals);
|
|
19314
|
+
for (const { from, to } of moved) drafts.move(from, to);
|
|
19315
|
+
const entries = draftEntriesOf(appeared);
|
|
19316
|
+
if (entries.length > 0) broadcast({ type: "composer.drafts", drafts: entries });
|
|
19317
|
+
};
|
|
18197
19318
|
const onRegistryExit = (terminalId, exitCode, signal) => broadcast({ type: "terminal.exit", terminalId, exitCode, signal });
|
|
18198
19319
|
const onRegistryActivity = (terminalId, activity, offlineReason) => broadcast(
|
|
18199
19320
|
offlineReason === null ? { type: "terminal.activity", terminalId, activity } : { type: "terminal.activity", terminalId, activity, offlineReason }
|
|
@@ -18221,7 +19342,9 @@ function attachTerminalSocket(options) {
|
|
|
18221
19342
|
};
|
|
18222
19343
|
const pieceWriter = (socket, terminalId) => (piece) => writeToTerminal(socket, terminalId, piece);
|
|
18223
19344
|
const dropTab = (terminalId) => {
|
|
19345
|
+
const draftOwner = draftOwnerOf(registry.get(terminalId));
|
|
18224
19346
|
if (!registry.close(terminalId)) return false;
|
|
19347
|
+
if (draftOwner !== null) drafts.delete(draftOwner);
|
|
18225
19348
|
conversations.drop(terminalId);
|
|
18226
19349
|
repos.drop(terminalId);
|
|
18227
19350
|
memory.drop(terminalId);
|
|
@@ -18293,6 +19416,23 @@ function attachTerminalSocket(options) {
|
|
|
18293
19416
|
memory.on("status", onMemoryStatus);
|
|
18294
19417
|
const stopAgentChanges = agents.subscribeChanges(() => broadcast(agentsMessage()));
|
|
18295
19418
|
const stopVaultStatus = vault.onStatus((status) => broadcast({ type: "vault.status", status }));
|
|
19419
|
+
const stopRemoteStatus = remote.onChange(
|
|
19420
|
+
() => broadcastToHost({ type: "remote.status", status: remote.status() })
|
|
19421
|
+
);
|
|
19422
|
+
const stopRemoteRevoked = remote.onRevoked((deviceId) => {
|
|
19423
|
+
for (const [socket, id] of remoteSockets) {
|
|
19424
|
+
if (deviceId === null || id === deviceId) socket.close(REMOTE_REVOKED_CLOSE_CODE, "revoked");
|
|
19425
|
+
}
|
|
19426
|
+
});
|
|
19427
|
+
const sendRemoteError = (socket, error) => {
|
|
19428
|
+
if (error instanceof ServerTextError) {
|
|
19429
|
+
sendError(socket, "remote-failed", error.text);
|
|
19430
|
+
return;
|
|
19431
|
+
}
|
|
19432
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
19433
|
+
console.warn("[remote] a request failed:", detail);
|
|
19434
|
+
sendError(socket, "remote-failed", serverText("remoteFailed"), detail);
|
|
19435
|
+
};
|
|
18296
19436
|
const sendVaultError = (socket, error) => {
|
|
18297
19437
|
if (error instanceof VaultError) {
|
|
18298
19438
|
sendError(socket, "vault-failed", error.text);
|
|
@@ -18310,20 +19450,25 @@ function attachTerminalSocket(options) {
|
|
|
18310
19450
|
return;
|
|
18311
19451
|
}
|
|
18312
19452
|
if (pathname !== WS_PATH) return;
|
|
18313
|
-
const
|
|
18314
|
-
if (
|
|
19453
|
+
const authorization = authorizeRequest(request, port, token, remote.verifier());
|
|
19454
|
+
if (!authorization.ok) {
|
|
18315
19455
|
socket.write("HTTP/1.1 403 Forbidden\r\nConnection: close\r\n\r\n");
|
|
18316
19456
|
socket.destroy();
|
|
18317
|
-
console.warn(`[security] upgrade rejected on ${WS_PATH}: ${
|
|
19457
|
+
console.warn(`[security] upgrade rejected on ${WS_PATH}: ${authorization.reason}`);
|
|
18318
19458
|
return;
|
|
18319
19459
|
}
|
|
18320
19460
|
wss.handleUpgrade(request, socket, head, (client) => {
|
|
18321
|
-
wss.emit("connection", client, request);
|
|
19461
|
+
wss.emit("connection", client, request, authorization.credential);
|
|
18322
19462
|
});
|
|
18323
19463
|
};
|
|
18324
19464
|
httpServer.on("upgrade", onUpgrade);
|
|
18325
|
-
wss.on("connection", (socket) => {
|
|
19465
|
+
wss.on("connection", (socket, _request, credential) => {
|
|
18326
19466
|
clients.add(socket);
|
|
19467
|
+
const deviceId = credential.kind === "device" ? credential.deviceId : null;
|
|
19468
|
+
if (deviceId !== null) {
|
|
19469
|
+
remoteSockets.set(socket, deviceId);
|
|
19470
|
+
remote.noteConnected(deviceId);
|
|
19471
|
+
}
|
|
18327
19472
|
const listener = (terminalId, chunk) => {
|
|
18328
19473
|
send(socket, { type: "terminal.output", terminalId, data: chunk });
|
|
18329
19474
|
};
|
|
@@ -18386,7 +19531,8 @@ function attachTerminalSocket(options) {
|
|
|
18386
19531
|
defaultAgent: agents.defaultAgent(),
|
|
18387
19532
|
platform: process.platform,
|
|
18388
19533
|
defaultCwd,
|
|
18389
|
-
shellName: shell?.name ?? null
|
|
19534
|
+
shellName: shell?.name ?? null,
|
|
19535
|
+
remoteClient: deviceId !== null
|
|
18390
19536
|
});
|
|
18391
19537
|
send(socket, terminalListMessage());
|
|
18392
19538
|
for (const entry of registry.activitySnapshot()) {
|
|
@@ -18395,7 +19541,9 @@ function attachTerminalSocket(options) {
|
|
|
18395
19541
|
send(socket, { type: "index.status", status: index.getStatus() });
|
|
18396
19542
|
send(socket, { type: "index.projects", projects: index.getProjects(), replace: true });
|
|
18397
19543
|
send(socket, { type: "notes.list", notes: notes.list() });
|
|
19544
|
+
send(socket, { type: "composer.drafts", drafts: draftEntriesOf(registry.list()) });
|
|
18398
19545
|
send(socket, { type: "vault.status", status: vault.status() });
|
|
19546
|
+
if (deviceId === null) send(socket, { type: "remote.status", status: remote.status() });
|
|
18399
19547
|
socket.on("message", (raw) => {
|
|
18400
19548
|
const text = Array.isArray(raw) ? Buffer.concat(raw).toString("utf8") : Buffer.from(raw).toString("utf8");
|
|
18401
19549
|
const message = parseClientMessage(text);
|
|
@@ -18405,9 +19553,16 @@ function attachTerminalSocket(options) {
|
|
|
18405
19553
|
return;
|
|
18406
19554
|
}
|
|
18407
19555
|
if (message.type !== "input") debugLog("socket", `received ${message.type}`);
|
|
19556
|
+
const refusal = remoteRefusal(message.type, deviceId !== null);
|
|
19557
|
+
if (refusal !== null) {
|
|
19558
|
+
sendError(socket, "remote-refused", refusal);
|
|
19559
|
+
return;
|
|
19560
|
+
}
|
|
18408
19561
|
switch (message.type) {
|
|
18409
19562
|
case "input":
|
|
18410
|
-
|
|
19563
|
+
if (!registry.write(message.terminalId, message.data) && registry.get(message.terminalId) === null) {
|
|
19564
|
+
sendError(socket, "unknown-terminal", serverText("terminalGone"));
|
|
19565
|
+
}
|
|
18411
19566
|
break;
|
|
18412
19567
|
/*
|
|
18413
19568
|
Un mensaje del cuadro de escritura.
|
|
@@ -18423,6 +19578,8 @@ function attachTerminalSocket(options) {
|
|
|
18423
19578
|
*/
|
|
18424
19579
|
case "agent.submit": {
|
|
18425
19580
|
const { terminalId, text: text2, images } = message;
|
|
19581
|
+
const submittedOwner = draftOwnerOf(registry.get(terminalId));
|
|
19582
|
+
if (submittedOwner !== null) drafts.delete(submittedOwner);
|
|
18426
19583
|
const files = message.files ?? [];
|
|
18427
19584
|
if (files.length > MAX_FILES_PER_SUBMIT) {
|
|
18428
19585
|
sendError(
|
|
@@ -18450,7 +19607,7 @@ function attachTerminalSocket(options) {
|
|
|
18450
19607
|
if (submitting !== null) {
|
|
18451
19608
|
const launchedAt = registry.launchedAtOf(terminalId);
|
|
18452
19609
|
const blind = isBlind();
|
|
18453
|
-
const
|
|
19610
|
+
const refusal2 = submitRefusal({
|
|
18454
19611
|
label: submitting.label,
|
|
18455
19612
|
approvesPendingOnCycle: guardsPending,
|
|
18456
19613
|
waitingFor: conversations.getWaitingFor(terminalId),
|
|
@@ -18459,8 +19616,8 @@ function attachTerminalSocket(options) {
|
|
|
18459
19616
|
blind,
|
|
18460
19617
|
openToolCall: blind && launchedAt !== null && conversations.hasOpenToolCall(terminalId, launchedAt)
|
|
18461
19618
|
});
|
|
18462
|
-
if (
|
|
18463
|
-
sendError(socket, "submit-failed",
|
|
19619
|
+
if (refusal2 !== null) {
|
|
19620
|
+
sendError(socket, "submit-failed", refusal2);
|
|
18464
19621
|
break;
|
|
18465
19622
|
}
|
|
18466
19623
|
}
|
|
@@ -19131,6 +20288,19 @@ function attachTerminalSocket(options) {
|
|
|
19131
20288
|
}
|
|
19132
20289
|
})();
|
|
19133
20290
|
break;
|
|
20291
|
+
/*
|
|
20292
|
+
El borrador del cuadro de una pestana, despues de una pausa del
|
|
20293
|
+
teclado (§6.29). No se contesta. Una consola, o una pestana cuya CLI
|
|
20294
|
+
todavia no dijo su conversacion, no tiene donde guardarlo: lo escrito
|
|
20295
|
+
sigue en el cuadro, y se guarda con la pausa siguiente a que aparezca.
|
|
20296
|
+
*/
|
|
20297
|
+
case "composer.draft": {
|
|
20298
|
+
const owner = draftOwnerOf(registry.get(message.terminalId));
|
|
20299
|
+
if (owner === null) break;
|
|
20300
|
+
const outcome = drafts.save(owner, message.draft);
|
|
20301
|
+
debugLog("drafts", `${message.terminalId.slice(0, 8)}: ${outcome}`);
|
|
20302
|
+
break;
|
|
20303
|
+
}
|
|
19134
20304
|
/*
|
|
19135
20305
|
Notas sueltas. No van dirigidas a ninguna pestana.
|
|
19136
20306
|
|
|
@@ -19415,9 +20585,70 @@ function attachTerminalSocket(options) {
|
|
|
19415
20585
|
case "search.cancel":
|
|
19416
20586
|
globalSearch.cancel(message.searchId);
|
|
19417
20587
|
break;
|
|
20588
|
+
/*
|
|
20589
|
+
El acceso remoto (hito 37). Hasta aca solo llega una ventana del
|
|
20590
|
+
anfitrion: a un equipo remoto lo freno `remoteRefusal`. El estado
|
|
20591
|
+
nuevo sale a las ventanas del anfitrion por `remote.onChange`; aca
|
|
20592
|
+
solo se contesta lo que falla, y el codigo, que va **solo** a quien lo
|
|
20593
|
+
pidio.
|
|
20594
|
+
*/
|
|
20595
|
+
case "remote.configure": {
|
|
20596
|
+
const patch = {};
|
|
20597
|
+
if (message.enabled !== void 0) patch.enabled = message.enabled;
|
|
20598
|
+
if (message.port !== void 0) patch.port = message.port;
|
|
20599
|
+
void remote.configure(patch).catch((error) => sendRemoteError(socket, error));
|
|
20600
|
+
break;
|
|
20601
|
+
}
|
|
20602
|
+
case "remote.pairing.start":
|
|
20603
|
+
try {
|
|
20604
|
+
send(socket, { type: "remote.pairing.code", pairing: remote.startPairing() });
|
|
20605
|
+
} catch (error) {
|
|
20606
|
+
sendRemoteError(socket, error);
|
|
20607
|
+
}
|
|
20608
|
+
break;
|
|
20609
|
+
case "remote.pairing.cancel":
|
|
20610
|
+
remote.cancelPairing();
|
|
20611
|
+
break;
|
|
20612
|
+
/*
|
|
20613
|
+
El telefono (hito 38): la respuesta lleva su llave privada dentro del
|
|
20614
|
+
texto del QR, asi que va solo a esta ventana, y si esta ventana se
|
|
20615
|
+
cierra sin cancelar, la llave se olvida igual (`teardown`).
|
|
20616
|
+
*/
|
|
20617
|
+
case "remote.phone.start":
|
|
20618
|
+
phonePairingOwner = socket;
|
|
20619
|
+
void remote.startPhonePairing().then((pairing) => send(socket, { type: "remote.phone.pairing", pairing })).catch((error) => sendRemoteError(socket, error));
|
|
20620
|
+
break;
|
|
20621
|
+
case "remote.phone.cancel":
|
|
20622
|
+
if (phonePairingOwner === socket) phonePairingOwner = null;
|
|
20623
|
+
remote.cancelPhonePairing();
|
|
20624
|
+
break;
|
|
20625
|
+
case "remote.device.rename":
|
|
20626
|
+
void remote.renameDevice(message.deviceId, message.label).then((renamed) => {
|
|
20627
|
+
if (!renamed) sendError(socket, "remote-failed", serverText("remoteDeviceGone"));
|
|
20628
|
+
}).catch((error) => sendRemoteError(socket, error));
|
|
20629
|
+
break;
|
|
20630
|
+
case "remote.device.revoke":
|
|
20631
|
+
void remote.revokeDevice(message.deviceId).then((revoked) => {
|
|
20632
|
+
if (!revoked) sendError(socket, "remote-failed", serverText("remoteDeviceGone"));
|
|
20633
|
+
}).catch((error) => sendRemoteError(socket, error));
|
|
20634
|
+
break;
|
|
20635
|
+
/*
|
|
20636
|
+
"Olvidar este equipo" del teléfono: se revoca el equipo con el que
|
|
20637
|
+
entró esta ventana, y ningún otro. Revocar cierra sus ventanas con
|
|
20638
|
+
4001, esta incluida: ése es el acuse. Una ventana del anfitrión no es
|
|
20639
|
+
un equipo, y no tiene nada que olvidar.
|
|
20640
|
+
*/
|
|
20641
|
+
case "remote.device.forgetSelf":
|
|
20642
|
+
if (deviceId === null) break;
|
|
20643
|
+
void remote.revokeDevice(deviceId).catch((error) => sendRemoteError(socket, error));
|
|
20644
|
+
break;
|
|
19418
20645
|
}
|
|
19419
20646
|
});
|
|
19420
20647
|
const teardown = () => {
|
|
20648
|
+
if (phonePairingOwner === socket) {
|
|
20649
|
+
phonePairingOwner = null;
|
|
20650
|
+
remote.cancelPhonePairing();
|
|
20651
|
+
}
|
|
19421
20652
|
globalSearch.cancel();
|
|
19422
20653
|
pickers.closeAll();
|
|
19423
20654
|
registry.detachAll(listener);
|
|
@@ -19428,6 +20659,7 @@ function attachTerminalSocket(options) {
|
|
|
19428
20659
|
for (const terminalId of memorySubscriptions) memory.unsubscribe(terminalId);
|
|
19429
20660
|
memorySubscriptions.clear();
|
|
19430
20661
|
clients.delete(socket);
|
|
20662
|
+
if (deviceId !== null && remoteSockets.delete(socket)) remote.noteDisconnected(deviceId);
|
|
19431
20663
|
};
|
|
19432
20664
|
socket.on("close", teardown);
|
|
19433
20665
|
socket.on("error", teardown);
|
|
@@ -19452,18 +20684,20 @@ function attachTerminalSocket(options) {
|
|
|
19452
20684
|
memory.off("status", onMemoryStatus);
|
|
19453
20685
|
stopAgentChanges();
|
|
19454
20686
|
stopVaultStatus();
|
|
20687
|
+
stopRemoteStatus();
|
|
20688
|
+
stopRemoteRevoked();
|
|
19455
20689
|
for (const client of wss.clients) client.terminate();
|
|
19456
20690
|
wss.close();
|
|
19457
20691
|
};
|
|
19458
20692
|
}
|
|
19459
20693
|
|
|
19460
20694
|
// packages/server/src/vault/catalog.ts
|
|
19461
|
-
import { open as open7, readdir as readdir16, stat as
|
|
19462
|
-
import
|
|
20695
|
+
import { open as open7, readdir as readdir16, stat as stat29 } from "node:fs/promises";
|
|
20696
|
+
import path48 from "node:path";
|
|
19463
20697
|
import { createInterface as createInterface2 } from "node:readline";
|
|
19464
20698
|
var HEADER_MAX_BYTES = 64 * 1024;
|
|
19465
20699
|
var LOAD_CONCURRENCY = 16;
|
|
19466
|
-
var
|
|
20700
|
+
var keyOf2 = (agent, sessionId) => `${agent}/${sessionId}`;
|
|
19467
20701
|
function isSessionAgentId2(value) {
|
|
19468
20702
|
return SESSION_AGENT_IDS.includes(value);
|
|
19469
20703
|
}
|
|
@@ -19479,7 +20713,7 @@ async function assetBytesOf(dir, agent, sessionId) {
|
|
|
19479
20713
|
for (const name of names) {
|
|
19480
20714
|
if (!VAULT_ASSET_NAME_PATTERN.test(name)) continue;
|
|
19481
20715
|
try {
|
|
19482
|
-
const info = await
|
|
20716
|
+
const info = await stat29(path48.join(folder, name));
|
|
19483
20717
|
if (info.isFile()) total += info.size;
|
|
19484
20718
|
} catch {
|
|
19485
20719
|
}
|
|
@@ -19490,7 +20724,7 @@ async function readEntry(dir, agent, sessionId) {
|
|
|
19490
20724
|
const file = sessionFile(dir, agent, sessionId);
|
|
19491
20725
|
let sizeBytes;
|
|
19492
20726
|
try {
|
|
19493
|
-
const info = await
|
|
20727
|
+
const info = await stat29(file);
|
|
19494
20728
|
if (!info.isFile()) return { kind: "absent" };
|
|
19495
20729
|
sizeBytes = info.size;
|
|
19496
20730
|
} catch {
|
|
@@ -19571,8 +20805,8 @@ var VaultCatalog = class {
|
|
|
19571
20805
|
const foreign = /* @__PURE__ */ new Set();
|
|
19572
20806
|
keys.forEach((key, position) => {
|
|
19573
20807
|
const read = reads[position];
|
|
19574
|
-
if (read?.kind === "listed") entries.set(
|
|
19575
|
-
else if (read?.kind === "foreign") foreign.add(
|
|
20808
|
+
if (read?.kind === "listed") entries.set(keyOf2(key.agent, key.sessionId), read.entry);
|
|
20809
|
+
else if (read?.kind === "foreign") foreign.add(keyOf2(key.agent, key.sessionId));
|
|
19576
20810
|
});
|
|
19577
20811
|
this.entries = entries;
|
|
19578
20812
|
this.foreign = foreign;
|
|
@@ -19586,7 +20820,7 @@ var VaultCatalog = class {
|
|
|
19586
20820
|
const generation = this.generation;
|
|
19587
20821
|
const read = await readEntry(dir, agent, sessionId);
|
|
19588
20822
|
if (generation !== this.generation) return;
|
|
19589
|
-
const key =
|
|
20823
|
+
const key = keyOf2(agent, sessionId);
|
|
19590
20824
|
const before = JSON.stringify(this.entries.get(key) ?? null);
|
|
19591
20825
|
const wasForeign = this.foreign.has(key);
|
|
19592
20826
|
this.entries.delete(key);
|
|
@@ -19616,11 +20850,11 @@ var VaultCatalog = class {
|
|
|
19616
20850
|
return this.summaryCache;
|
|
19617
20851
|
}
|
|
19618
20852
|
header(agent, sessionId) {
|
|
19619
|
-
return this.entries.get(
|
|
20853
|
+
return this.entries.get(keyOf2(agent, sessionId))?.header ?? null;
|
|
19620
20854
|
}
|
|
19621
20855
|
/** true si en disco hay una copia de esa sesion con un formato que esta app no conoce. */
|
|
19622
20856
|
hasForeignFormat(agent, sessionId) {
|
|
19623
|
-
return this.foreign.has(
|
|
20857
|
+
return this.foreign.has(keyOf2(agent, sessionId));
|
|
19624
20858
|
}
|
|
19625
20859
|
/**
|
|
19626
20860
|
* El cuerpo de una sesion listada, linea por linea: eventos y documentos.
|
|
@@ -19643,7 +20877,7 @@ var VaultCatalog = class {
|
|
|
19643
20877
|
*/
|
|
19644
20878
|
async *bodyLines(agent, sessionId) {
|
|
19645
20879
|
const dir = this.dir;
|
|
19646
|
-
if (dir === null || !this.entries.has(
|
|
20880
|
+
if (dir === null || !this.entries.has(keyOf2(agent, sessionId))) return;
|
|
19647
20881
|
const handle = await open7(sessionFile(dir, agent, sessionId), "r").catch(() => null);
|
|
19648
20882
|
if (handle === null) return;
|
|
19649
20883
|
const input = handle.createReadStream({ encoding: "utf8" });
|
|
@@ -19699,38 +20933,77 @@ var VaultCatalog = class {
|
|
|
19699
20933
|
};
|
|
19700
20934
|
|
|
19701
20935
|
// packages/server/src/index.ts
|
|
19702
|
-
var serverDir =
|
|
20936
|
+
var serverDir = path49.dirname(fileURLToPath2(import.meta.url));
|
|
19703
20937
|
var isPackaged = true;
|
|
19704
|
-
var webRoot = isPackaged ? serverDir :
|
|
19705
|
-
var webDist = isPackaged ?
|
|
19706
|
-
var repoRoot =
|
|
20938
|
+
var webRoot = isPackaged ? serverDir : path49.resolve(serverDir, "../../web");
|
|
20939
|
+
var webDist = isPackaged ? path49.join(serverDir, "web") : path49.join(webRoot, "dist");
|
|
20940
|
+
var repoRoot = path49.resolve(serverDir, "../../..");
|
|
19707
20941
|
var isProduction = isPackaged || process.argv.includes("--prod");
|
|
19708
20942
|
function resolveDefaultCwd() {
|
|
19709
20943
|
const override = process.env["AGENT_WORKBENCH_CWD"];
|
|
19710
|
-
if (override !== void 0 && override.length > 0) return
|
|
20944
|
+
if (override !== void 0 && override.length > 0) return path49.resolve(override);
|
|
19711
20945
|
if (isPackaged) return process.cwd();
|
|
19712
20946
|
return repoRoot;
|
|
19713
20947
|
}
|
|
19714
20948
|
var ORIGIN_REFUSED_TEXT = "Origin not allowed.\nOrigen no permitido.";
|
|
19715
20949
|
var TOKEN_MISSING_TEXT = "The session token is missing. Open the URL the server printed at startup.\nFalta el token de sesi\xF3n. Abre la URL que imprimi\xF3 el servidor al arrancar.";
|
|
19716
20950
|
var NOT_FOUND_TEXT = "Not found.\nNo encontrado.";
|
|
19717
|
-
|
|
20951
|
+
var PAIRING_HINT_TEXT = '\n\nFrom another computer: pair it first. On the computer that runs Agent Workbench, open "Remote access" and choose "Pair a device".\nDesde otro equipo: primero empar\xE9jalo. En el equipo donde corre Agent Workbench, abre "Acceso remoto" y elige "Emparejar un equipo".';
|
|
20952
|
+
var PAIRING_REFUSED_TEXT = `That pairing code isn't valid or has expired. Ask for a new one in "Remote access".
|
|
20953
|
+
Ese c\xF3digo para emparejar no es v\xE1lido o venci\xF3. Pide uno nuevo en "Acceso remoto".`;
|
|
20954
|
+
function createAuthMiddleware(port, token, remote) {
|
|
19718
20955
|
return (request, response, next) => {
|
|
19719
|
-
|
|
19720
|
-
|
|
20956
|
+
const authorization = authorizeRequest(request, port, token, remote.verifier());
|
|
20957
|
+
if (authorization.ok) {
|
|
20958
|
+
const { credential } = authorization;
|
|
20959
|
+
if (credential.kind === "session" && credential.source === "query") {
|
|
20960
|
+
response.setHeader("Set-Cookie", buildTokenCookie(token));
|
|
20961
|
+
}
|
|
20962
|
+
if (credential.kind === "device" && request.method === "GET" && request.path === "/") {
|
|
20963
|
+
response.setHeader("Set-Cookie", buildDeviceCookie(credential.credential));
|
|
20964
|
+
}
|
|
20965
|
+
next();
|
|
19721
20966
|
return;
|
|
19722
20967
|
}
|
|
19723
|
-
|
|
19724
|
-
|
|
19725
|
-
response.status(401).type("text/plain").send(TOKEN_MISSING_TEXT);
|
|
20968
|
+
if (authorization.reason !== "bad-token") {
|
|
20969
|
+
response.status(403).type("text/plain").send(ORIGIN_REFUSED_TEXT);
|
|
19726
20970
|
return;
|
|
19727
20971
|
}
|
|
19728
|
-
|
|
19729
|
-
|
|
20972
|
+
const code = request.method === "GET" && request.path === "/" ? readPairingCode(request) : null;
|
|
20973
|
+
if (code !== null && remote.verifier() !== null) {
|
|
20974
|
+
response.setHeader("Cache-Control", "no-store");
|
|
20975
|
+
void remote.redeemPairing(code, request.headers["user-agent"]).catch(() => null).then((redeemed) => {
|
|
20976
|
+
if (redeemed === null) {
|
|
20977
|
+
response.status(401).type("text/plain").send(PAIRING_REFUSED_TEXT);
|
|
20978
|
+
return;
|
|
20979
|
+
}
|
|
20980
|
+
response.setHeader("Set-Cookie", buildDeviceCookie(redeemed.credential));
|
|
20981
|
+
response.redirect(303, "/");
|
|
20982
|
+
});
|
|
20983
|
+
return;
|
|
19730
20984
|
}
|
|
19731
|
-
|
|
20985
|
+
response.status(401).type("text/plain").send(remote.verifier() === null ? TOKEN_MISSING_TEXT : TOKEN_MISSING_TEXT + PAIRING_HINT_TEXT);
|
|
19732
20986
|
};
|
|
19733
20987
|
}
|
|
20988
|
+
function isPortUnavailable(error) {
|
|
20989
|
+
const code = error?.code;
|
|
20990
|
+
return code === "EADDRINUSE" || code === "EACCES";
|
|
20991
|
+
}
|
|
20992
|
+
function listenOnLoopback(httpServer, port) {
|
|
20993
|
+
return new Promise((resolve, reject) => {
|
|
20994
|
+
const onError = (error) => {
|
|
20995
|
+
httpServer.off("listening", onListening);
|
|
20996
|
+
reject(error);
|
|
20997
|
+
};
|
|
20998
|
+
const onListening = () => {
|
|
20999
|
+
httpServer.off("error", onError);
|
|
21000
|
+
resolve();
|
|
21001
|
+
};
|
|
21002
|
+
httpServer.once("error", onError);
|
|
21003
|
+
httpServer.once("listening", onListening);
|
|
21004
|
+
httpServer.listen(port, LOOPBACK_HOST);
|
|
21005
|
+
});
|
|
21006
|
+
}
|
|
19734
21007
|
async function mountDevUi(app, httpServer) {
|
|
19735
21008
|
const { createServer: createViteServer } = await import("vite");
|
|
19736
21009
|
const vite = await createViteServer({
|
|
@@ -19743,7 +21016,7 @@ async function mountDevUi(app, httpServer) {
|
|
|
19743
21016
|
}
|
|
19744
21017
|
var LOOKS_LIKE_FILE = /\.[a-zA-Z0-9]{1,8}$/;
|
|
19745
21018
|
function mountBuiltUi(app) {
|
|
19746
|
-
const indexHtml =
|
|
21019
|
+
const indexHtml = path49.join(webDist, "index.html");
|
|
19747
21020
|
if (!existsSync4(indexHtml)) {
|
|
19748
21021
|
throw new Error(
|
|
19749
21022
|
`There's no compiled interface in ${webDist}.
|
|
@@ -19760,7 +21033,7 @@ Run "pnpm build" before "pnpm start", or use "pnpm dev".`
|
|
|
19760
21033
|
});
|
|
19761
21034
|
return () => Promise.resolve();
|
|
19762
21035
|
}
|
|
19763
|
-
function describeStartup(url, cwd, agents, shell) {
|
|
21036
|
+
function describeStartup(url, cwd, agents, shell, remoteLine) {
|
|
19764
21037
|
const agentList = agents.list();
|
|
19765
21038
|
const line = "-".repeat(64);
|
|
19766
21039
|
console.log(`
|
|
@@ -19782,6 +21055,9 @@ ${line}`);
|
|
|
19782
21055
|
}));
|
|
19783
21056
|
for (const agentLine of startupAgentLines(startupAgents)) console.log(agentLine);
|
|
19784
21057
|
console.log(` Console ${shell === null ? "not found" : shell.file}`);
|
|
21058
|
+
if (remoteLine !== null) console.log(remoteLine);
|
|
21059
|
+
const colored = process.stdout.isTTY === true && process.env["NO_COLOR"] === void 0;
|
|
21060
|
+
console.log(colored ? supportStartupLineColored() : supportStartupLine());
|
|
19785
21061
|
if (agentList.some((info) => info.environmentNotice === "child-session-marker")) {
|
|
19786
21062
|
console.log("");
|
|
19787
21063
|
console.log(" Note: this process inherited CLAUDE_CODE_CHILD_SESSION, which turns off");
|
|
@@ -19818,6 +21094,8 @@ async function main() {
|
|
|
19818
21094
|
);
|
|
19819
21095
|
const notes = new NotesStore();
|
|
19820
21096
|
await notes.load();
|
|
21097
|
+
const drafts = new DraftStore();
|
|
21098
|
+
await drafts.load();
|
|
19821
21099
|
const conversations = new ConversationHub(registry, agents);
|
|
19822
21100
|
const repos = new RepoHub(registry);
|
|
19823
21101
|
const memory = new MemoryHub(registry);
|
|
@@ -19825,16 +21103,31 @@ async function main() {
|
|
|
19825
21103
|
const app = express();
|
|
19826
21104
|
app.disable("x-powered-by");
|
|
19827
21105
|
const httpServer = createServer(app);
|
|
19828
|
-
|
|
19829
|
-
|
|
19830
|
-
|
|
19831
|
-
|
|
21106
|
+
const remoteSettings = settings2.get().remote;
|
|
21107
|
+
let listeningFixed = false;
|
|
21108
|
+
let fixedPortBusy = false;
|
|
21109
|
+
if (remoteSettings.enabled) {
|
|
21110
|
+
try {
|
|
21111
|
+
await listenOnLoopback(httpServer, remoteSettings.port);
|
|
21112
|
+
listeningFixed = true;
|
|
21113
|
+
} catch (error) {
|
|
21114
|
+
if (!isPortUnavailable(error)) throw error;
|
|
21115
|
+
fixedPortBusy = true;
|
|
21116
|
+
}
|
|
21117
|
+
}
|
|
21118
|
+
if (!listeningFixed) await listenOnLoopback(httpServer, 0);
|
|
19832
21119
|
const address = httpServer.address();
|
|
19833
21120
|
if (address === null || typeof address === "string") {
|
|
19834
21121
|
throw new Error("Couldn't determine the assigned port.");
|
|
19835
21122
|
}
|
|
19836
21123
|
const { port } = address;
|
|
19837
|
-
|
|
21124
|
+
const remote = new RemoteAccessService({
|
|
21125
|
+
settings: settings2,
|
|
21126
|
+
store: new RemoteDevicesStore(),
|
|
21127
|
+
listening: { port, fixed: listeningFixed, fixedPortBusy }
|
|
21128
|
+
});
|
|
21129
|
+
await remote.load();
|
|
21130
|
+
app.use(createAuthMiddleware(port, token, remote));
|
|
19838
21131
|
const closeUi = isProduction ? mountBuiltUi(app) : await mountDevUi(app, httpServer);
|
|
19839
21132
|
const detachSocket = attachTerminalSocket({
|
|
19840
21133
|
pasteStore,
|
|
@@ -19846,18 +21139,21 @@ async function main() {
|
|
|
19846
21139
|
index,
|
|
19847
21140
|
archived,
|
|
19848
21141
|
notes,
|
|
21142
|
+
drafts,
|
|
19849
21143
|
conversations,
|
|
19850
21144
|
repos,
|
|
19851
21145
|
memory,
|
|
19852
21146
|
agents,
|
|
19853
21147
|
defaultCwd,
|
|
19854
|
-
vault
|
|
21148
|
+
vault,
|
|
21149
|
+
remote
|
|
19855
21150
|
});
|
|
19856
21151
|
void pasteStore.purgeStale();
|
|
19857
21152
|
void index.start();
|
|
19858
21153
|
const stopWatching = watchSessions(index, conversations, agents);
|
|
19859
21154
|
if (process.env["AGENT_WORKBENCH_NO_RESTORE"] !== "1") {
|
|
19860
21155
|
void store.load().then((state) => {
|
|
21156
|
+
drafts.retainOnly(workspaceDraftOwners(state));
|
|
19861
21157
|
if (state.tabs.length === 0 && state.foreignTabs.length === 0) return;
|
|
19862
21158
|
if (agents.anyAvailable()) {
|
|
19863
21159
|
console.log(`Restoring ${state.tabs.length} tab(s) from the previous run...`);
|
|
@@ -19866,7 +21162,14 @@ async function main() {
|
|
|
19866
21162
|
});
|
|
19867
21163
|
}
|
|
19868
21164
|
const url = `http://${LOOPBACK_HOST}:${port}/?${TOKEN_QUERY_PARAM}=${token}`;
|
|
19869
|
-
|
|
21165
|
+
const remoteStatus = remote.status();
|
|
21166
|
+
describeStartup(
|
|
21167
|
+
url,
|
|
21168
|
+
defaultCwd,
|
|
21169
|
+
agents,
|
|
21170
|
+
shell,
|
|
21171
|
+
remoteAccessStartupLine(remoteStatus.state, remoteStatus.port, remote.pairedDevices)
|
|
21172
|
+
);
|
|
19870
21173
|
if (process.env["AGENT_WORKBENCH_NO_OPEN"] !== "1") openBrowser(url);
|
|
19871
21174
|
let shuttingDown = false;
|
|
19872
21175
|
const shutdown = () => {
|
|
@@ -19881,7 +21184,8 @@ async function main() {
|
|
|
19881
21184
|
repos.disposeAll();
|
|
19882
21185
|
void memory.disposeAll();
|
|
19883
21186
|
registry.disposeAll();
|
|
19884
|
-
|
|
21187
|
+
remote.dispose();
|
|
21188
|
+
void Promise.all([store.flush(), archived.flush(), notes.flush(), drafts.flush(), remote.flush(), agentsDisposed]).catch(() => void 0).then(() => closeUi()).catch(() => void 0).finally(() => {
|
|
19885
21189
|
httpServer.close(() => process.exit(0));
|
|
19886
21190
|
setTimeout(() => process.exit(0), 2e3).unref();
|
|
19887
21191
|
});
|