agent-workbench 0.3.0 → 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 +188 -12
- package/dist/server.js +1952 -599
- 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
|
}
|
|
@@ -10617,195 +10722,1104 @@ function openBrowser(url) {
|
|
|
10617
10722
|
}
|
|
10618
10723
|
}
|
|
10619
10724
|
|
|
10620
|
-
// packages/server/src/
|
|
10621
|
-
import {
|
|
10622
|
-
|
|
10623
|
-
|
|
10624
|
-
|
|
10625
|
-
|
|
10626
|
-
|
|
10627
|
-
|
|
10628
|
-
}
|
|
10629
|
-
|
|
10630
|
-
const bufferA = Buffer.from(a, "utf8");
|
|
10631
|
-
const bufferB = Buffer.from(b, "utf8");
|
|
10632
|
-
if (bufferA.length !== bufferB.length) return false;
|
|
10633
|
-
return timingSafeEqual(bufferA, bufferB);
|
|
10725
|
+
// packages/server/src/pty-helper.ts
|
|
10726
|
+
import { chmodSync, statSync } from "node:fs";
|
|
10727
|
+
import { createRequire } from "node:module";
|
|
10728
|
+
import path29 from "node:path";
|
|
10729
|
+
function helperCandidates(packageDir, platform, arch) {
|
|
10730
|
+
return [
|
|
10731
|
+
path29.join(packageDir, "build", "Release", "spawn-helper"),
|
|
10732
|
+
path29.join(packageDir, "build", "Debug", "spawn-helper"),
|
|
10733
|
+
path29.join(packageDir, "prebuilds", `${platform}-${arch}`, "spawn-helper")
|
|
10734
|
+
];
|
|
10634
10735
|
}
|
|
10635
|
-
function
|
|
10736
|
+
function locateNodePty() {
|
|
10636
10737
|
try {
|
|
10637
|
-
|
|
10738
|
+
const require2 = createRequire(import.meta.url);
|
|
10739
|
+
return path29.resolve(path29.dirname(require2.resolve("node-pty")), "..");
|
|
10638
10740
|
} catch {
|
|
10639
10741
|
return null;
|
|
10640
10742
|
}
|
|
10641
10743
|
}
|
|
10642
|
-
function
|
|
10643
|
-
const
|
|
10644
|
-
if (
|
|
10645
|
-
|
|
10646
|
-
|
|
10647
|
-
|
|
10648
|
-
|
|
10649
|
-
|
|
10650
|
-
|
|
10744
|
+
function ensurePtyHelperExecutable(options = {}) {
|
|
10745
|
+
const platform = options.platform ?? process.platform;
|
|
10746
|
+
if (platform === "win32") return [];
|
|
10747
|
+
const packageDir = options.packageDir === void 0 ? locateNodePty() : options.packageDir;
|
|
10748
|
+
if (packageDir === null) return [];
|
|
10749
|
+
const warn = options.warn ?? ((message) => console.warn(message));
|
|
10750
|
+
const fixed = [];
|
|
10751
|
+
for (const file of helperCandidates(packageDir, platform, options.arch ?? process.arch)) {
|
|
10752
|
+
let mode;
|
|
10753
|
+
try {
|
|
10754
|
+
const stats = statSync(file);
|
|
10755
|
+
if (!stats.isFile()) continue;
|
|
10756
|
+
mode = stats.mode;
|
|
10757
|
+
} catch {
|
|
10758
|
+
continue;
|
|
10759
|
+
}
|
|
10760
|
+
if ((mode & 73) === 73) continue;
|
|
10761
|
+
try {
|
|
10762
|
+
chmodSync(file, mode & 511 | 493);
|
|
10763
|
+
fixed.push(file);
|
|
10764
|
+
} catch {
|
|
10765
|
+
warn(
|
|
10766
|
+
`[pty] ${file} is not executable and couldn't be fixed: no tab will be able to start a CLI. Run: chmod +x "${file}"`
|
|
10767
|
+
);
|
|
10768
|
+
}
|
|
10651
10769
|
}
|
|
10652
|
-
return
|
|
10770
|
+
return fixed;
|
|
10653
10771
|
}
|
|
10654
|
-
|
|
10655
|
-
|
|
10656
|
-
|
|
10657
|
-
|
|
10658
|
-
|
|
10772
|
+
|
|
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();
|
|
10828
|
+
}
|
|
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) {
|
|
10848
|
+
const bufferA = Buffer.from(a, "utf8");
|
|
10849
|
+
const bufferB = Buffer.from(b, "utf8");
|
|
10850
|
+
if (bufferA.length !== bufferB.length) return false;
|
|
10851
|
+
return timingSafeEqual(bufferA, bufferB);
|
|
10852
|
+
}
|
|
10853
|
+
var PairingDesk = class {
|
|
10854
|
+
constructor(deps) {
|
|
10855
|
+
this.deps = deps;
|
|
10659
10856
|
}
|
|
10660
|
-
|
|
10661
|
-
|
|
10662
|
-
|
|
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 };
|
|
10663
10867
|
}
|
|
10664
|
-
|
|
10665
|
-
|
|
10666
|
-
return { source: "cookie" };
|
|
10868
|
+
cancel() {
|
|
10869
|
+
this.current = null;
|
|
10667
10870
|
}
|
|
10668
|
-
|
|
10669
|
-
|
|
10670
|
-
|
|
10671
|
-
|
|
10871
|
+
isActive() {
|
|
10872
|
+
if (this.current !== null && this.deps.now() >= this.current.expiresAt) this.current = null;
|
|
10873
|
+
return this.current !== null;
|
|
10874
|
+
}
|
|
10875
|
+
/** Cuándo vence el vigente, o null. */
|
|
10876
|
+
expiresAt() {
|
|
10877
|
+
return this.isActive() ? this.current?.expiresAt ?? null : null;
|
|
10878
|
+
}
|
|
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");
|
|
10672
10896
|
}
|
|
10673
|
-
|
|
10674
|
-
|
|
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 };
|
|
10675
10971
|
}
|
|
10676
|
-
function
|
|
10677
|
-
const
|
|
10678
|
-
if (
|
|
10679
|
-
|
|
10680
|
-
const hostname = separator === -1 ? host : host.slice(0, separator);
|
|
10681
|
-
const hostPort = separator === -1 ? "" : host.slice(separator + 1);
|
|
10682
|
-
if (!ALLOWED_HOSTNAMES.has(hostname)) return false;
|
|
10683
|
-
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) ?? [];
|
|
10684
10976
|
}
|
|
10685
|
-
function
|
|
10686
|
-
|
|
10687
|
-
|
|
10688
|
-
let parsed;
|
|
10689
|
-
try {
|
|
10690
|
-
parsed = new URL(origin);
|
|
10691
|
-
} catch {
|
|
10692
|
-
return false;
|
|
10693
|
-
}
|
|
10694
|
-
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return false;
|
|
10695
|
-
if (!ALLOWED_HOSTNAMES.has(parsed.hostname)) return false;
|
|
10696
|
-
return parsed.port === String(port);
|
|
10977
|
+
function serializeRemoteDevices(devices) {
|
|
10978
|
+
return `${JSON.stringify({ version: REMOTE_DEVICES_VERSION, devices }, null, 2)}
|
|
10979
|
+
`;
|
|
10697
10980
|
}
|
|
10698
|
-
function
|
|
10699
|
-
if (!
|
|
10700
|
-
if (
|
|
10701
|
-
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");
|
|
10702
11035
|
return null;
|
|
10703
11036
|
}
|
|
10704
11037
|
|
|
10705
|
-
// packages/server/src/
|
|
10706
|
-
|
|
10707
|
-
|
|
10708
|
-
|
|
10709
|
-
|
|
10710
|
-
|
|
10711
|
-
|
|
10712
|
-
|
|
10713
|
-
|
|
10714
|
-
return output.length > 0 ? serverText("raw", { text: output }) : serverText("gitCommandFailed", { command });
|
|
10715
|
-
}
|
|
10716
|
-
var MAX_CHANGES = 2e3;
|
|
10717
|
-
var MAX_DIFF_LINES = 4e3;
|
|
10718
|
-
var MAX_DIFF_BYTES = 512 * 1024;
|
|
10719
|
-
var GIT_TIMEOUT_MS = 15e3;
|
|
10720
|
-
var GIT_MAX_BUFFER = 32 * 1024 * 1024;
|
|
10721
|
-
function isMissingBinary(error) {
|
|
10722
|
-
return typeof error === "object" && error !== null && error.code === "ENOENT";
|
|
10723
|
-
}
|
|
10724
|
-
var GitMissingError = class extends Error {
|
|
10725
|
-
constructor() {
|
|
10726
|
-
super("git wasn't found in the PATH.");
|
|
10727
|
-
this.name = "GitMissingError";
|
|
10728
|
-
}
|
|
10729
|
-
};
|
|
10730
|
-
function runGit(cwd, args) {
|
|
10731
|
-
return new Promise((resolve, reject) => {
|
|
10732
|
-
execFile3(
|
|
10733
|
-
"git",
|
|
10734
|
-
[
|
|
10735
|
-
// Sin esto git escapa los no-ASCII en octal y un archivo con acento
|
|
10736
|
-
// vuelve ilegible.
|
|
10737
|
-
"-c",
|
|
10738
|
-
"core.quotepath=false",
|
|
10739
|
-
// Esta app corre `git status` sola, cada pocos segundos, mientras el
|
|
10740
|
-
// usuario trabaja. Sin esta bandera `status` refresca y **escribe** el
|
|
10741
|
-
// indice, y eso trae dos problemas: toma `index.lock`, con lo que puede
|
|
10742
|
-
// hacer fallar un `git add` que el usuario esta escribiendo en la
|
|
10743
|
-
// terminal, y modifica `.git/index`, que es justo lo que observa
|
|
10744
|
-
// nuestro watcher — o sea que cada lectura se dispara a si misma.
|
|
10745
|
-
"--no-optional-locks",
|
|
10746
|
-
...args
|
|
10747
|
-
],
|
|
10748
|
-
{
|
|
10749
|
-
cwd,
|
|
10750
|
-
timeout: GIT_TIMEOUT_MS,
|
|
10751
|
-
maxBuffer: GIT_MAX_BUFFER,
|
|
10752
|
-
windowsHide: true,
|
|
10753
|
-
encoding: "utf8"
|
|
10754
|
-
},
|
|
10755
|
-
(error, stdout, stderr) => {
|
|
10756
|
-
if (error !== null && isMissingBinary(error)) {
|
|
10757
|
-
reject(new GitMissingError());
|
|
10758
|
-
return;
|
|
10759
|
-
}
|
|
10760
|
-
const code = error !== null && typeof error.code === "number" ? error.code : error !== null ? 1 : 0;
|
|
10761
|
-
resolve({ stdout, stderr, code });
|
|
10762
|
-
}
|
|
10763
|
-
);
|
|
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
|
+
});
|
|
10764
11047
|
});
|
|
10765
11048
|
}
|
|
10766
|
-
function
|
|
10767
|
-
|
|
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())] };
|
|
10768
11057
|
}
|
|
10769
|
-
|
|
10770
|
-
|
|
10771
|
-
|
|
10772
|
-
|
|
10773
|
-
|
|
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 });
|
|
10774
11101
|
}
|
|
10775
|
-
|
|
10776
|
-
|
|
10777
|
-
|
|
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();
|
|
10778
11106
|
}
|
|
10779
|
-
|
|
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();
|
|
10780
11125
|
return {
|
|
10781
|
-
|
|
10782
|
-
|
|
10783
|
-
|
|
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
|
+
}))
|
|
10784
11137
|
};
|
|
10785
11138
|
}
|
|
10786
|
-
|
|
10787
|
-
|
|
10788
|
-
|
|
10789
|
-
|
|
10790
|
-
|
|
10791
|
-
|
|
10792
|
-
|
|
10793
|
-
|
|
10794
|
-
|
|
10795
|
-
|
|
10796
|
-
|
|
10797
|
-
|
|
10798
|
-
|
|
10799
|
-
|
|
10800
|
-
|
|
10801
|
-
|
|
10802
|
-
|
|
10803
|
-
|
|
10804
|
-
|
|
10805
|
-
|
|
10806
|
-
|
|
10807
|
-
|
|
10808
|
-
|
|
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;
|
|
11149
|
+
}
|
|
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":
|
|
10809
11823
|
return "deleted";
|
|
10810
11824
|
case "R":
|
|
10811
11825
|
return "renamed";
|
|
@@ -10900,7 +11914,7 @@ function parseWorktrees(raw, currentRoot) {
|
|
|
10900
11914
|
worktrees.push({
|
|
10901
11915
|
path: current.path,
|
|
10902
11916
|
branch: current.branch,
|
|
10903
|
-
isCurrent:
|
|
11917
|
+
isCurrent: path32.resolve(current.path) === path32.resolve(currentRoot)
|
|
10904
11918
|
});
|
|
10905
11919
|
current = null;
|
|
10906
11920
|
};
|
|
@@ -11043,7 +12057,7 @@ function looksBinary(buffer) {
|
|
|
11043
12057
|
async function readUntrackedAsDiff(absolutePath, relativePath) {
|
|
11044
12058
|
let content;
|
|
11045
12059
|
try {
|
|
11046
|
-
content = await
|
|
12060
|
+
content = await readFile10(absolutePath);
|
|
11047
12061
|
} catch {
|
|
11048
12062
|
return {
|
|
11049
12063
|
path: relativePath,
|
|
@@ -11091,7 +12105,7 @@ async function readUntrackedAsDiff(absolutePath, relativePath) {
|
|
|
11091
12105
|
async function readDiff(options) {
|
|
11092
12106
|
const { repoRoot: repoRoot2, relativePath, staged, untracked } = options;
|
|
11093
12107
|
if (untracked && !staged) {
|
|
11094
|
-
return readUntrackedAsDiff(
|
|
12108
|
+
return readUntrackedAsDiff(path32.join(repoRoot2, relativePath), relativePath);
|
|
11095
12109
|
}
|
|
11096
12110
|
const args = [
|
|
11097
12111
|
"diff",
|
|
@@ -11130,7 +12144,7 @@ async function filterIgnored(repoRoot2, relativePaths) {
|
|
|
11130
12144
|
const ignored = /* @__PURE__ */ new Set();
|
|
11131
12145
|
if (relativePaths.length === 0) return ignored;
|
|
11132
12146
|
const result = await new Promise((resolve) => {
|
|
11133
|
-
const child =
|
|
12147
|
+
const child = execFile4(
|
|
11134
12148
|
"git",
|
|
11135
12149
|
["-c", "core.quotepath=false", "check-ignore", "--stdin", "-z"],
|
|
11136
12150
|
{ cwd: repoRoot2, timeout: GIT_TIMEOUT_MS, maxBuffer: GIT_MAX_BUFFER, windowsHide: true },
|
|
@@ -11311,24 +12325,24 @@ var RepoHub = class extends EventEmitter2 {
|
|
|
11311
12325
|
// packages/server/src/memory-hub.ts
|
|
11312
12326
|
import { EventEmitter as EventEmitter3 } from "node:events";
|
|
11313
12327
|
import { homedir as homedir10 } from "node:os";
|
|
11314
|
-
import
|
|
12328
|
+
import path34 from "node:path";
|
|
11315
12329
|
import chokidar2 from "chokidar";
|
|
11316
12330
|
|
|
11317
12331
|
// packages/server/src/memory-bridge.ts
|
|
11318
|
-
import { execFile as
|
|
11319
|
-
import { randomBytes as
|
|
12332
|
+
import { execFile as execFile5 } from "node:child_process";
|
|
12333
|
+
import { randomBytes as randomBytes4 } from "node:crypto";
|
|
11320
12334
|
import {
|
|
11321
|
-
mkdir as
|
|
12335
|
+
mkdir as mkdir7,
|
|
11322
12336
|
open as open6,
|
|
11323
12337
|
readdir as readdir9,
|
|
11324
|
-
readFile as
|
|
12338
|
+
readFile as readFile11,
|
|
11325
12339
|
realpath as realpath2,
|
|
11326
|
-
rename as
|
|
11327
|
-
stat as
|
|
12340
|
+
rename as rename4,
|
|
12341
|
+
stat as stat17,
|
|
11328
12342
|
unlink as unlink3
|
|
11329
12343
|
} from "node:fs/promises";
|
|
11330
12344
|
import { homedir as homedir9 } from "node:os";
|
|
11331
|
-
import
|
|
12345
|
+
import path33 from "node:path";
|
|
11332
12346
|
var MemoryBridgeError = class extends ServerTextError {
|
|
11333
12347
|
};
|
|
11334
12348
|
var MAX_LISTED_NOTES = 500;
|
|
@@ -11346,9 +12360,9 @@ var GITIGNORE_MEMORY_LINE = `${MEMORY_DIR}/`;
|
|
|
11346
12360
|
var BOM2 = Buffer.from([239, 187, 191]);
|
|
11347
12361
|
async function inspectMemory(cwd, home = homedir9()) {
|
|
11348
12362
|
await assertMemoryDirInside(cwd);
|
|
11349
|
-
const folder =
|
|
12363
|
+
const folder = path33.join(cwd, MEMORY_DIR);
|
|
11350
12364
|
const folderExists = await isDirectory2(folder);
|
|
11351
|
-
const indexExists = folderExists && await isFile(
|
|
12365
|
+
const indexExists = folderExists && await isFile(path33.join(folder, MEMORY_INDEX_FILE));
|
|
11352
12366
|
const noteNames = folderExists ? await listNoteFiles(folder) : [];
|
|
11353
12367
|
const notes = await describeNotes(folder, noteNames);
|
|
11354
12368
|
const files = await Promise.all(
|
|
@@ -11380,7 +12394,7 @@ async function installMemory(cwd, options, home = homedir9()) {
|
|
|
11380
12394
|
}
|
|
11381
12395
|
async function importNativeMemory(cwd, home = homedir9()) {
|
|
11382
12396
|
await assertMemoryDirInside(cwd);
|
|
11383
|
-
if (!await isDirectory2(
|
|
12397
|
+
if (!await isDirectory2(path33.join(cwd, MEMORY_DIR))) {
|
|
11384
12398
|
throw new MemoryBridgeError(serverText("memoryNotInstalled"));
|
|
11385
12399
|
}
|
|
11386
12400
|
const plan = new PlanBuilder(cwd);
|
|
@@ -11398,9 +12412,9 @@ async function readMemoryNote(cwd, name) {
|
|
|
11398
12412
|
}
|
|
11399
12413
|
const absolute = await resolveInside(cwd, `${MEMORY_DIR}/${name}`);
|
|
11400
12414
|
try {
|
|
11401
|
-
const info = await
|
|
12415
|
+
const info = await stat17(absolute);
|
|
11402
12416
|
if (!info.isFile()) return null;
|
|
11403
|
-
const content = await
|
|
12417
|
+
const content = await readFile11(absolute);
|
|
11404
12418
|
return {
|
|
11405
12419
|
name,
|
|
11406
12420
|
text: content.subarray(0, MAX_NOTE_BYTES).toString("utf8"),
|
|
@@ -11415,7 +12429,7 @@ function isNoteName(name) {
|
|
|
11415
12429
|
if (name.length === 0 || name.length > 255) return false;
|
|
11416
12430
|
if (name.includes("\0")) return false;
|
|
11417
12431
|
if (name === "." || name === "..") return false;
|
|
11418
|
-
if (
|
|
12432
|
+
if (path33.basename(name) !== name) return false;
|
|
11419
12433
|
if (/[\\/:*?"<>|\u0000-\u001f]/.test(name)) return false;
|
|
11420
12434
|
return name.toLowerCase().endsWith(".md");
|
|
11421
12435
|
}
|
|
@@ -11452,7 +12466,7 @@ var PlanBuilder = class {
|
|
|
11452
12466
|
/** Nombres de nota que habria en la carpeta, en minusculas. */
|
|
11453
12467
|
async noteNames() {
|
|
11454
12468
|
const names = /* @__PURE__ */ new Map();
|
|
11455
|
-
for (const name of await listNoteFiles(
|
|
12469
|
+
for (const name of await listNoteFiles(path33.join(this.cwd, MEMORY_DIR))) {
|
|
11456
12470
|
names.set(name.toLowerCase(), name);
|
|
11457
12471
|
}
|
|
11458
12472
|
for (const rel of this.files.keys()) {
|
|
@@ -11495,7 +12509,7 @@ var PlanBuilder = class {
|
|
|
11495
12509
|
continue;
|
|
11496
12510
|
}
|
|
11497
12511
|
const target = await realpath2(absolute);
|
|
11498
|
-
const info = await
|
|
12512
|
+
const info = await stat17(target);
|
|
11499
12513
|
if (info.nlink > 1) {
|
|
11500
12514
|
throw new MemoryBridgeError(serverText("memoryHardLinks", { file: file.rel }));
|
|
11501
12515
|
}
|
|
@@ -11514,7 +12528,7 @@ var PlanBuilder = class {
|
|
|
11514
12528
|
);
|
|
11515
12529
|
if (pending.length === 0) return;
|
|
11516
12530
|
if (pending.some((file) => file.rel.startsWith(`${MEMORY_DIR}/`))) {
|
|
11517
|
-
await
|
|
12531
|
+
await mkdir7(path33.join(this.cwd, MEMORY_DIR), { recursive: true });
|
|
11518
12532
|
}
|
|
11519
12533
|
for (const file of pending) {
|
|
11520
12534
|
if (file.target === null) throw new Error(`Unvalidated plan: ${file.rel}`);
|
|
@@ -11625,7 +12639,7 @@ async function planNativeImport(plan, cwd, home) {
|
|
|
11625
12639
|
const local = await plan.noteNames();
|
|
11626
12640
|
for (const name of await listNoteFiles(folder)) {
|
|
11627
12641
|
const key = name.toLowerCase();
|
|
11628
|
-
const content = await readIfFile(
|
|
12642
|
+
const content = await readIfFile(path33.join(folder, name), name);
|
|
11629
12643
|
if (content === null) continue;
|
|
11630
12644
|
const already = chosen.get(key);
|
|
11631
12645
|
if (already !== void 0) {
|
|
@@ -11639,7 +12653,7 @@ async function planNativeImport(plan, cwd, home) {
|
|
|
11639
12653
|
if (existing !== null && !existing.equals(content)) skipped.push(name);
|
|
11640
12654
|
continue;
|
|
11641
12655
|
}
|
|
11642
|
-
plan.put(`${MEMORY_DIR}/${name}`, null, content, "copy", "",
|
|
12656
|
+
plan.put(`${MEMORY_DIR}/${name}`, null, content, "copy", "", path33.join(folder, name));
|
|
11643
12657
|
copied.push(name);
|
|
11644
12658
|
}
|
|
11645
12659
|
}
|
|
@@ -11649,7 +12663,7 @@ async function planNativeImport(plan, cwd, home) {
|
|
|
11649
12663
|
const known = new Set(linkTargets(index.toString("utf8")));
|
|
11650
12664
|
const additions = [];
|
|
11651
12665
|
for (const folder of folders) {
|
|
11652
|
-
const nativeIndex = await readIfFile(
|
|
12666
|
+
const nativeIndex = await readIfFile(path33.join(folder, MEMORY_INDEX_FILE), MEMORY_INDEX_FILE);
|
|
11653
12667
|
if (nativeIndex === null) continue;
|
|
11654
12668
|
for (const line of nativeIndex.toString("utf8").replace(/^\uFEFF/, "").split(/\r?\n/)) {
|
|
11655
12669
|
const target = linkTargets(line)[0];
|
|
@@ -11874,7 +12888,7 @@ async function describeNotes(folder, names) {
|
|
|
11874
12888
|
const stated = [];
|
|
11875
12889
|
for (const name of names) {
|
|
11876
12890
|
try {
|
|
11877
|
-
const info = await
|
|
12891
|
+
const info = await stat17(path33.join(folder, name));
|
|
11878
12892
|
if (info.isFile()) stated.push({ name, sizeBytes: info.size, modifiedAt: info.mtimeMs });
|
|
11879
12893
|
} catch {
|
|
11880
12894
|
}
|
|
@@ -11882,7 +12896,7 @@ async function describeNotes(folder, names) {
|
|
|
11882
12896
|
stated.sort((a, b) => b.modifiedAt - a.modifiedAt || a.name.localeCompare(b.name));
|
|
11883
12897
|
const notes = [];
|
|
11884
12898
|
for (const item of stated.slice(0, MAX_LISTED_NOTES)) {
|
|
11885
|
-
const head = await readHead(
|
|
12899
|
+
const head = await readHead(path33.join(folder, item.name));
|
|
11886
12900
|
notes.push({ ...item, ...describeNoteText(head, item.name) });
|
|
11887
12901
|
}
|
|
11888
12902
|
return notes;
|
|
@@ -11940,28 +12954,28 @@ async function countNativeMemory(cwd, home, sharedNames) {
|
|
|
11940
12954
|
return { available: names.size, pending };
|
|
11941
12955
|
}
|
|
11942
12956
|
async function nativeMemoryFolders(cwd, home) {
|
|
11943
|
-
const root =
|
|
12957
|
+
const root = path33.join(home, ".claude", "projects");
|
|
11944
12958
|
const slug = projectSlugFor(cwd).toLowerCase();
|
|
11945
12959
|
try {
|
|
11946
12960
|
const entries = await readdir9(root, { withFileTypes: true });
|
|
11947
|
-
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"));
|
|
11948
12962
|
} catch {
|
|
11949
12963
|
return [];
|
|
11950
12964
|
}
|
|
11951
12965
|
}
|
|
11952
12966
|
function buildGlobalFragments(home) {
|
|
11953
12967
|
const windows = process.platform === "win32";
|
|
11954
|
-
const globalFile =
|
|
12968
|
+
const globalFile = path33.join(home, ...GLOBAL_MEMORY_RELATIVE.split("/"));
|
|
11955
12969
|
const hardLink = (link) => windows ? `mklink /H "${link}" "${globalFile}"` : `ln "${globalFile}" "${link}"`;
|
|
11956
12970
|
const hardLinkNote = windows ? serverText("memoryNoteHardLinkCmd", { path: GLOBAL_MEMORY_RELATIVE }) : serverText("memoryNoteHardLink", { path: GLOBAL_MEMORY_RELATIVE });
|
|
11957
|
-
const codexTarget =
|
|
11958
|
-
const geminiTarget =
|
|
12971
|
+
const codexTarget = path33.join(home, ".codex", "AGENTS.md");
|
|
12972
|
+
const geminiTarget = path33.join(home, ".gemini", "GEMINI.md");
|
|
11959
12973
|
const homeSlashes = home.replace(/\\/g, "/").replace(/\/+$/, "");
|
|
11960
12974
|
return [
|
|
11961
12975
|
{
|
|
11962
12976
|
agent: "claude-code",
|
|
11963
12977
|
label: MEMORY_AGENT_LABELS["claude-code"],
|
|
11964
|
-
target:
|
|
12978
|
+
target: path33.join(home, ".claude", "CLAUDE.md"),
|
|
11965
12979
|
kind: "line",
|
|
11966
12980
|
text: `@~/${GLOBAL_MEMORY_RELATIVE}`,
|
|
11967
12981
|
note: serverText("memoryNoteClaude")
|
|
@@ -11985,7 +12999,7 @@ function buildGlobalFragments(home) {
|
|
|
11985
12999
|
{
|
|
11986
13000
|
agent: "opencode",
|
|
11987
13001
|
label: MEMORY_AGENT_LABELS.opencode,
|
|
11988
|
-
target:
|
|
13002
|
+
target: path33.join(home, ".config", "opencode", "opencode.json"),
|
|
11989
13003
|
kind: "json",
|
|
11990
13004
|
text: `"instructions": ["${homeSlashes}/${GLOBAL_MEMORY_RELATIVE}"]`,
|
|
11991
13005
|
note: serverText("memoryNoteOpenCode")
|
|
@@ -11994,7 +13008,7 @@ function buildGlobalFragments(home) {
|
|
|
11994
13008
|
}
|
|
11995
13009
|
function runGit2(cwd, args) {
|
|
11996
13010
|
return new Promise((resolve) => {
|
|
11997
|
-
|
|
13011
|
+
execFile5(
|
|
11998
13012
|
"git",
|
|
11999
13013
|
["-c", "core.quotepath=false", "--no-optional-locks", ...args],
|
|
12000
13014
|
{ cwd, timeout: GIT_TIMEOUT_MS2, windowsHide: true, encoding: "utf8", maxBuffer: 1024 * 1024 },
|
|
@@ -12033,17 +13047,17 @@ async function readGit(cwd) {
|
|
|
12033
13047
|
if (!toplevel || !gitDir || !commonDir) return fail(serverText("gitUnexpected", { command: "rev-parse" }));
|
|
12034
13048
|
const ignored = await checkIgnored(cwd, INDEX_REL);
|
|
12035
13049
|
if (typeof ignored !== "boolean") return fail(ignored);
|
|
12036
|
-
const absoluteGitDir =
|
|
12037
|
-
const absoluteCommonDir =
|
|
13050
|
+
const absoluteGitDir = path33.resolve(cwd, gitDir);
|
|
13051
|
+
const absoluteCommonDir = path33.resolve(cwd, commonDir);
|
|
12038
13052
|
let worktree = null;
|
|
12039
13053
|
let main2 = null;
|
|
12040
13054
|
if (!await samePath(absoluteGitDir, absoluteCommonDir)) {
|
|
12041
|
-
if (
|
|
12042
|
-
const mainRoot =
|
|
12043
|
-
const inner =
|
|
12044
|
-
const sameFolder = inner.length === 0 || inner.startsWith("..") ||
|
|
12045
|
-
const mainPath = sameFolder ? mainRoot :
|
|
12046
|
-
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("/");
|
|
12047
13061
|
main2 = { root: mainRoot, memoryRel };
|
|
12048
13062
|
worktree = {
|
|
12049
13063
|
mainPath,
|
|
@@ -12068,7 +13082,7 @@ async function samePath(a, b) {
|
|
|
12068
13082
|
try {
|
|
12069
13083
|
return await realpath2(value);
|
|
12070
13084
|
} catch {
|
|
12071
|
-
return
|
|
13085
|
+
return path33.resolve(value);
|
|
12072
13086
|
}
|
|
12073
13087
|
};
|
|
12074
13088
|
const [left, right] = await Promise.all([resolve(a), resolve(b)]);
|
|
@@ -12110,9 +13124,9 @@ async function listNoteFiles(folder) {
|
|
|
12110
13124
|
}
|
|
12111
13125
|
async function readIfFile(absolute, label) {
|
|
12112
13126
|
try {
|
|
12113
|
-
const info = await
|
|
13127
|
+
const info = await stat17(absolute);
|
|
12114
13128
|
if (!info.isFile()) throw new MemoryBridgeError(serverText("memoryNotAFile", { label }));
|
|
12115
|
-
return await
|
|
13129
|
+
return await readFile11(absolute);
|
|
12116
13130
|
} catch (error) {
|
|
12117
13131
|
if (isNotFound(error)) return null;
|
|
12118
13132
|
throw error;
|
|
@@ -12134,14 +13148,14 @@ async function readHead(absolute) {
|
|
|
12134
13148
|
}
|
|
12135
13149
|
async function isFile(absolute) {
|
|
12136
13150
|
try {
|
|
12137
|
-
return (await
|
|
13151
|
+
return (await stat17(absolute)).isFile();
|
|
12138
13152
|
} catch {
|
|
12139
13153
|
return false;
|
|
12140
13154
|
}
|
|
12141
13155
|
}
|
|
12142
13156
|
async function isDirectory2(absolute) {
|
|
12143
13157
|
try {
|
|
12144
|
-
return (await
|
|
13158
|
+
return (await stat17(absolute)).isDirectory();
|
|
12145
13159
|
} catch {
|
|
12146
13160
|
return false;
|
|
12147
13161
|
}
|
|
@@ -12159,7 +13173,7 @@ async function writeAtomic(target, content, mustNotExist) {
|
|
|
12159
13173
|
await handle.close();
|
|
12160
13174
|
}
|
|
12161
13175
|
if (mustNotExist && await exists2(target)) {
|
|
12162
|
-
throw new MemoryBridgeError(serverText("memoryAppeared", { file:
|
|
13176
|
+
throw new MemoryBridgeError(serverText("memoryAppeared", { file: path33.basename(target) }));
|
|
12163
13177
|
}
|
|
12164
13178
|
await renameWithRetry(temporary, target);
|
|
12165
13179
|
} catch (error) {
|
|
@@ -12169,10 +13183,10 @@ async function writeAtomic(target, content, mustNotExist) {
|
|
|
12169
13183
|
}
|
|
12170
13184
|
async function createTemporary(target) {
|
|
12171
13185
|
for (let attempt = 1; ; attempt += 1) {
|
|
12172
|
-
const suffix =
|
|
12173
|
-
const temporary =
|
|
12174
|
-
|
|
12175
|
-
`.${
|
|
13186
|
+
const suffix = randomBytes4(6).toString("hex");
|
|
13187
|
+
const temporary = path33.join(
|
|
13188
|
+
path33.dirname(target),
|
|
13189
|
+
`.${path33.basename(target)}.${process.pid}.${suffix}.tmp`
|
|
12176
13190
|
);
|
|
12177
13191
|
try {
|
|
12178
13192
|
return { temporary, handle: await open6(temporary, "wx") };
|
|
@@ -12184,7 +13198,7 @@ async function createTemporary(target) {
|
|
|
12184
13198
|
}
|
|
12185
13199
|
async function exists2(absolute) {
|
|
12186
13200
|
try {
|
|
12187
|
-
await
|
|
13201
|
+
await stat17(absolute);
|
|
12188
13202
|
return true;
|
|
12189
13203
|
} catch {
|
|
12190
13204
|
return false;
|
|
@@ -12193,7 +13207,7 @@ async function exists2(absolute) {
|
|
|
12193
13207
|
async function renameWithRetry(from, to) {
|
|
12194
13208
|
for (let attempt = 1; ; attempt += 1) {
|
|
12195
13209
|
try {
|
|
12196
|
-
await
|
|
13210
|
+
await rename4(from, to);
|
|
12197
13211
|
return;
|
|
12198
13212
|
} catch (error) {
|
|
12199
13213
|
const code = error.code;
|
|
@@ -12410,13 +13424,13 @@ var MemoryHub = class extends EventEmitter3 {
|
|
|
12410
13424
|
}
|
|
12411
13425
|
};
|
|
12412
13426
|
function keyFor(cwd) {
|
|
12413
|
-
const resolved =
|
|
13427
|
+
const resolved = path34.resolve(cwd);
|
|
12414
13428
|
return process.platform === "win32" ? resolved.toLowerCase() : resolved;
|
|
12415
13429
|
}
|
|
12416
13430
|
function isRelevantPath(cwd, candidate) {
|
|
12417
|
-
const relative =
|
|
13431
|
+
const relative = path34.relative(cwd, candidate);
|
|
12418
13432
|
if (relative.length === 0) return true;
|
|
12419
|
-
if (relative.startsWith("..") ||
|
|
13433
|
+
if (relative.startsWith("..") || path34.isAbsolute(relative)) return false;
|
|
12420
13434
|
const same = (a, b) => process.platform === "win32" ? a.toLowerCase() === b.toLowerCase() : a === b;
|
|
12421
13435
|
const parts = relative.split(/[\\/]/);
|
|
12422
13436
|
const [first = "", second = "", third = ""] = parts;
|
|
@@ -12433,10 +13447,10 @@ function isRelevantPath(cwd, candidate) {
|
|
|
12433
13447
|
}
|
|
12434
13448
|
|
|
12435
13449
|
// packages/server/src/paste-store.ts
|
|
12436
|
-
import { createHash, randomUUID as
|
|
12437
|
-
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";
|
|
12438
13452
|
import { tmpdir as tmpdir4 } from "node:os";
|
|
12439
|
-
import
|
|
13453
|
+
import path35 from "node:path";
|
|
12440
13454
|
|
|
12441
13455
|
// packages/server/src/vault/markdown.ts
|
|
12442
13456
|
var pad2 = (value) => String(value).padStart(2, "0");
|
|
@@ -12951,8 +13965,8 @@ function buildContinuationMessage(input) {
|
|
|
12951
13965
|
}
|
|
12952
13966
|
return sanitizeForPaste(lines.join("\n"));
|
|
12953
13967
|
}
|
|
12954
|
-
function transcriptReferenceFor(
|
|
12955
|
-
return style === "at-quoted" ? fileReference(
|
|
13968
|
+
function transcriptReferenceFor(path50, style) {
|
|
13969
|
+
return style === "at-quoted" ? fileReference(path50, "at-quoted") : `"${path50}"`;
|
|
12956
13970
|
}
|
|
12957
13971
|
function planTranscript(input) {
|
|
12958
13972
|
const turns = splitTurns(input.events);
|
|
@@ -13127,7 +14141,7 @@ var PasteStore = class {
|
|
|
13127
14141
|
root;
|
|
13128
14142
|
counter = 0;
|
|
13129
14143
|
constructor(root) {
|
|
13130
|
-
this.root = root ??
|
|
14144
|
+
this.root = root ?? path35.join(tmpdir4(), "agent-workbench", "pasted");
|
|
13131
14145
|
}
|
|
13132
14146
|
/**
|
|
13133
14147
|
* Guarda una imagen y devuelve su ruta.
|
|
@@ -13154,12 +14168,12 @@ var PasteStore = class {
|
|
|
13154
14168
|
if (signature === null) {
|
|
13155
14169
|
throw new PasteImageError(serverText("imageUnknownFormat"));
|
|
13156
14170
|
}
|
|
13157
|
-
const directory =
|
|
13158
|
-
await
|
|
14171
|
+
const directory = path35.join(this.root, safeSegment(terminalId));
|
|
14172
|
+
await mkdir8(directory, { recursive: true, mode: DIRECTORY_MODE });
|
|
13159
14173
|
this.counter += 1;
|
|
13160
|
-
const name = `pegada-${this.counter}-${
|
|
13161
|
-
const file =
|
|
13162
|
-
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 });
|
|
13163
14177
|
return { path: file, bytes: bytes.length };
|
|
13164
14178
|
}
|
|
13165
14179
|
/**
|
|
@@ -13176,12 +14190,12 @@ var PasteStore = class {
|
|
|
13176
14190
|
if (bytes.length > MAX_TEXT_BYTES) {
|
|
13177
14191
|
throw new PasteTextError(`The text is ${bytes.length} bytes; the maximum is ${MAX_TEXT_BYTES}.`);
|
|
13178
14192
|
}
|
|
13179
|
-
const directory =
|
|
13180
|
-
await
|
|
14193
|
+
const directory = path35.join(this.root, safeSegment(terminalId));
|
|
14194
|
+
await mkdir8(directory, { recursive: true, mode: DIRECTORY_MODE });
|
|
13181
14195
|
this.counter += 1;
|
|
13182
|
-
const name = `${baseName}-${this.counter}-${
|
|
13183
|
-
const file =
|
|
13184
|
-
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 });
|
|
13185
14199
|
return { path: file, bytes: bytes.length };
|
|
13186
14200
|
}
|
|
13187
14201
|
/**
|
|
@@ -13210,17 +14224,17 @@ var PasteStore = class {
|
|
|
13210
14224
|
})
|
|
13211
14225
|
);
|
|
13212
14226
|
}
|
|
13213
|
-
const directory =
|
|
13214
|
-
await
|
|
14227
|
+
const directory = path35.join(this.root, safeSegment(terminalId));
|
|
14228
|
+
await mkdir8(directory, { recursive: true, mode: DIRECTORY_MODE });
|
|
13215
14229
|
this.counter += 1;
|
|
13216
|
-
const name = `adjunto-${this.counter}-${
|
|
13217
|
-
const file =
|
|
13218
|
-
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 });
|
|
13219
14233
|
return { path: file, bytes: bytes.length, name };
|
|
13220
14234
|
}
|
|
13221
14235
|
/** Borra lo de una pestana. Se llama al cerrarla. */
|
|
13222
14236
|
async clearTerminal(terminalId) {
|
|
13223
|
-
await
|
|
14237
|
+
await rm3(path35.join(this.root, safeSegment(terminalId)), {
|
|
13224
14238
|
recursive: true,
|
|
13225
14239
|
force: true
|
|
13226
14240
|
}).catch(() => {
|
|
@@ -13242,11 +14256,11 @@ var PasteStore = class {
|
|
|
13242
14256
|
}
|
|
13243
14257
|
await Promise.all(
|
|
13244
14258
|
entries.map(async (entry) => {
|
|
13245
|
-
const full =
|
|
14259
|
+
const full = path35.join(this.root, entry);
|
|
13246
14260
|
try {
|
|
13247
|
-
const info = await
|
|
14261
|
+
const info = await stat18(full);
|
|
13248
14262
|
if (now - info.mtimeMs > STALE_AFTER_MS) {
|
|
13249
|
-
await
|
|
14263
|
+
await rm3(full, { recursive: true, force: true });
|
|
13250
14264
|
}
|
|
13251
14265
|
} catch {
|
|
13252
14266
|
}
|
|
@@ -13256,13 +14270,13 @@ var PasteStore = class {
|
|
|
13256
14270
|
};
|
|
13257
14271
|
function safeSegment(value) {
|
|
13258
14272
|
const clean = value.replace(/[^a-zA-Z0-9_-]/g, "");
|
|
13259
|
-
return clean.length > 0 ? clean :
|
|
14273
|
+
return clean.length > 0 ? clean : createHash3("sha1").update(value).digest("hex").slice(0, 16);
|
|
13260
14274
|
}
|
|
13261
14275
|
|
|
13262
14276
|
// packages/server/src/session-index.ts
|
|
13263
14277
|
import { EventEmitter as EventEmitter4 } from "node:events";
|
|
13264
|
-
import { mkdir as
|
|
13265
|
-
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";
|
|
13266
14280
|
var CACHE_VERSION = 4;
|
|
13267
14281
|
var vaultPairKey = (agent, sessionId) => `${agent}/${sessionId}`;
|
|
13268
14282
|
function vaultOnlySessions(natives, vault, listState, ready) {
|
|
@@ -13311,7 +14325,7 @@ function cacheKey(agent, ref) {
|
|
|
13311
14325
|
}
|
|
13312
14326
|
async function pathExists3(candidate) {
|
|
13313
14327
|
try {
|
|
13314
|
-
await
|
|
14328
|
+
await stat19(candidate);
|
|
13315
14329
|
return true;
|
|
13316
14330
|
} catch {
|
|
13317
14331
|
return false;
|
|
@@ -13361,7 +14375,7 @@ function migrateEntryV3(filePath, value) {
|
|
|
13361
14375
|
return {
|
|
13362
14376
|
agent: "claude-code",
|
|
13363
14377
|
ref: filePath,
|
|
13364
|
-
group:
|
|
14378
|
+
group: path36.basename(path36.dirname(filePath)),
|
|
13365
14379
|
mtimeMs,
|
|
13366
14380
|
sizeBytes,
|
|
13367
14381
|
cwd,
|
|
@@ -13626,7 +14640,7 @@ var SessionIndex = class extends EventEmitter4 {
|
|
|
13626
14640
|
}
|
|
13627
14641
|
async loadCache() {
|
|
13628
14642
|
try {
|
|
13629
|
-
const raw = await
|
|
14643
|
+
const raw = await readFile12(sessionIndexCachePath(), "utf8");
|
|
13630
14644
|
const parsed = JSON.parse(raw);
|
|
13631
14645
|
const migrated = migrateIndexCache(parsed);
|
|
13632
14646
|
if (migrated === null) return;
|
|
@@ -13639,8 +14653,8 @@ var SessionIndex = class extends EventEmitter4 {
|
|
|
13639
14653
|
if (!this.cacheDirty) return;
|
|
13640
14654
|
this.cacheDirty = false;
|
|
13641
14655
|
try {
|
|
13642
|
-
await
|
|
13643
|
-
await
|
|
14656
|
+
await mkdir9(appConfigDir(), { recursive: true });
|
|
14657
|
+
await writeFile5(sessionIndexCachePath(), JSON.stringify(this.cache), "utf8");
|
|
13644
14658
|
} catch (error) {
|
|
13645
14659
|
console.warn("[index] couldn't save the cache:", error);
|
|
13646
14660
|
}
|
|
@@ -13816,200 +14830,43 @@ var SessionIndex = class extends EventEmitter4 {
|
|
|
13816
14830
|
const history = this.agents.adapter(agent).history;
|
|
13817
14831
|
let refs;
|
|
13818
14832
|
try {
|
|
13819
|
-
refs = await history.changedRefs(filePath);
|
|
13820
|
-
} catch {
|
|
13821
|
-
refs = null;
|
|
13822
|
-
}
|
|
13823
|
-
if (refs === null || refs.length === 0) return;
|
|
13824
|
-
let changed = false;
|
|
13825
|
-
for (const ref of refs) {
|
|
13826
|
-
const key = cacheKey(agent, ref);
|
|
13827
|
-
const cachedBefore = this.cache.entries[key];
|
|
13828
|
-
if (cachedBefore !== void 0) {
|
|
13829
|
-
delete this.cache.entries[key];
|
|
13830
|
-
this.cacheDirty = true;
|
|
13831
|
-
}
|
|
13832
|
-
let indexed;
|
|
13833
|
-
try {
|
|
13834
|
-
const item = await history.item(ref);
|
|
13835
|
-
indexed = item === null ? null : await this.readOrThrow(agent, history, item);
|
|
13836
|
-
} catch {
|
|
13837
|
-
if (cachedBefore !== void 0 && !(key in this.cache.entries)) this.cache.entries[key] = cachedBefore;
|
|
13838
|
-
continue;
|
|
13839
|
-
}
|
|
13840
|
-
if (indexed === null) {
|
|
13841
|
-
if (this.sessions.delete(key)) changed = true;
|
|
13842
|
-
continue;
|
|
13843
|
-
}
|
|
13844
|
-
if (indexed.cwd !== null) this.cwdExists.set(indexed.cwd, await pathExists3(indexed.cwd));
|
|
13845
|
-
this.sessions.set(key, indexed);
|
|
13846
|
-
changed = true;
|
|
13847
|
-
}
|
|
13848
|
-
if (changed) this.emitProjects();
|
|
13849
|
-
await this.saveCache();
|
|
13850
|
-
}
|
|
13851
|
-
};
|
|
13852
|
-
|
|
13853
|
-
// packages/server/src/settings-store.ts
|
|
13854
|
-
import { mkdir as mkdir9, readFile as readFile12 } from "node:fs/promises";
|
|
13855
|
-
import path35 from "node:path";
|
|
13856
|
-
|
|
13857
|
-
// packages/server/src/vault/write.ts
|
|
13858
|
-
import { randomBytes as randomBytes4 } from "node:crypto";
|
|
13859
|
-
import { mkdir as mkdir8, rename as rename4, rm as rm3, stat as stat18, writeFile as writeFile5 } from "node:fs/promises";
|
|
13860
|
-
|
|
13861
|
-
// packages/server/src/vault/paths.ts
|
|
13862
|
-
import { createHash as createHash2 } from "node:crypto";
|
|
13863
|
-
import path34 from "node:path";
|
|
13864
|
-
function defaultVaultDir() {
|
|
13865
|
-
return path34.join(appConfigDir(), "vault");
|
|
13866
|
-
}
|
|
13867
|
-
function vaultMarkerFile(dir) {
|
|
13868
|
-
return path34.join(dir, "vault.json");
|
|
13869
|
-
}
|
|
13870
|
-
function sessionsRoot2(dir) {
|
|
13871
|
-
return path34.join(dir, "sessions");
|
|
13872
|
-
}
|
|
13873
|
-
function isSafeSessionId(sessionId) {
|
|
13874
|
-
return isVaultSessionId(sessionId);
|
|
13875
|
-
}
|
|
13876
|
-
function isSessionAgentId(value) {
|
|
13877
|
-
return SESSION_AGENT_IDS.includes(value);
|
|
13878
|
-
}
|
|
13879
|
-
function assertSessionKey(agent, sessionId) {
|
|
13880
|
-
if (!isSessionAgentId(agent)) throw new Error(`Unknown source for the local copy: ${JSON.stringify(agent)}`);
|
|
13881
|
-
if (!isSafeSessionId(sessionId)) throw new Error(`Session id that isn't copied: ${JSON.stringify(sessionId)}`);
|
|
13882
|
-
}
|
|
13883
|
-
function agentSessionsDir(dir, agent) {
|
|
13884
|
-
if (!isSessionAgentId(agent)) throw new Error(`Unknown source for the local copy: ${JSON.stringify(agent)}`);
|
|
13885
|
-
return path34.join(sessionsRoot2(dir), agent);
|
|
13886
|
-
}
|
|
13887
|
-
function sessionFile(dir, agent, sessionId) {
|
|
13888
|
-
assertSessionKey(agent, sessionId);
|
|
13889
|
-
return path34.join(sessionsRoot2(dir), agent, `${sessionId}.jsonl`);
|
|
13890
|
-
}
|
|
13891
|
-
function assetsDir(dir, agent, sessionId) {
|
|
13892
|
-
assertSessionKey(agent, sessionId);
|
|
13893
|
-
return path34.join(sessionsRoot2(dir), agent, `${sessionId}.assets`);
|
|
13894
|
-
}
|
|
13895
|
-
function assetFile(dir, agent, sessionId, asset) {
|
|
13896
|
-
if (!VAULT_ASSET_NAME_PATTERN.test(asset)) throw new Error(`Invalid asset name: ${JSON.stringify(asset)}`);
|
|
13897
|
-
return path34.join(assetsDir(dir, agent, sessionId), asset);
|
|
13898
|
-
}
|
|
13899
|
-
function projectFolderName(cwd, platform) {
|
|
13900
|
-
const hash = createHash2("sha256").update(normalizeCwdKey(cwd, platform)).digest("hex").slice(0, 8);
|
|
13901
|
-
const base = (platform === "win32" ? path34.win32 : path34.posix).basename(cwd);
|
|
13902
|
-
const name = base.length === 0 ? "proyecto" : sanitizeFileName(base, 60);
|
|
13903
|
-
return `${name}-${hash}`;
|
|
13904
|
-
}
|
|
13905
|
-
function memoryDir(dir, cwd, platform) {
|
|
13906
|
-
return path34.join(dir, "memory", projectFolderName(cwd, platform));
|
|
13907
|
-
}
|
|
13908
|
-
function projectExportDir(dir, cwd, platform) {
|
|
13909
|
-
return path34.join(dir, "export", projectFolderName(cwd, platform));
|
|
13910
|
-
}
|
|
13911
|
-
function projectExportDirFor(dir, project, platform) {
|
|
13912
|
-
if (project.cwd.length > 0) return projectExportDir(dir, project.cwd, platform);
|
|
13913
|
-
const hash = createHash2("sha256").update(project.key).digest("hex").slice(0, 8);
|
|
13914
|
-
const name = project.fallbackName.length > 0 ? sanitizeFileName(project.fallbackName, 60) : "proyecto";
|
|
13915
|
-
return path34.join(dir, "export", `${name}-${hash}`);
|
|
13916
|
-
}
|
|
13917
|
-
function sessionExportFile(dir, agent, sessionId) {
|
|
13918
|
-
assertSessionKey(agent, sessionId);
|
|
13919
|
-
return path34.join(dir, "export", "sesiones", `${agent}-${sessionId}.md`);
|
|
13920
|
-
}
|
|
13921
|
-
var RESERVED_WINDOWS_NAME = /^(con|prn|aux|nul|com[1-9]|lpt[1-9])$/i;
|
|
13922
|
-
function sanitizeFileName(text, max = 80) {
|
|
13923
|
-
const limit = Math.max(1, Math.floor(max));
|
|
13924
|
-
const kept = text.normalize("NFC").replace(/[^\p{L}\p{M}\p{N} ._-]/gu, "_");
|
|
13925
|
-
const trimEnds = (value) => value.replace(/^ +/u, "").replace(/[. ]+$/u, "");
|
|
13926
|
-
let name = trimEnds(Array.from(trimEnds(kept)).slice(0, limit).join(""));
|
|
13927
|
-
if (name.length === 0) return "_";
|
|
13928
|
-
const stem = (name.split(".")[0] ?? "").replace(/ +$/u, "");
|
|
13929
|
-
if (RESERVED_WINDOWS_NAME.test(stem)) {
|
|
13930
|
-
name = `_${Array.from(name).slice(0, limit - 1).join("")}`;
|
|
13931
|
-
name = name.replace(/[. ]+$/u, "");
|
|
13932
|
-
}
|
|
13933
|
-
return name;
|
|
13934
|
-
}
|
|
13935
|
-
|
|
13936
|
-
// packages/server/src/vault/write.ts
|
|
13937
|
-
var RENAME_RETRIES = 3;
|
|
13938
|
-
var RENAME_RETRY_MS = 100;
|
|
13939
|
-
var DEFAULT_WRITE_DEPS = {
|
|
13940
|
-
rename: rename4,
|
|
13941
|
-
platform: process.platform,
|
|
13942
|
-
delay: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
|
|
13943
|
-
now: () => Date.now()
|
|
13944
|
-
};
|
|
13945
|
-
function temporaryPathFor(target) {
|
|
13946
|
-
return `${target}.${process.pid}.${randomBytes4(6).toString("hex")}.tmp`;
|
|
13947
|
-
}
|
|
13948
|
-
function isRetryableRename(error, platform) {
|
|
13949
|
-
if (platform !== "win32") return false;
|
|
13950
|
-
const code = error?.code;
|
|
13951
|
-
return code === "EPERM" || code === "EBUSY";
|
|
13952
|
-
}
|
|
13953
|
-
async function writeFileAtomic(target, data, deps = DEFAULT_WRITE_DEPS) {
|
|
13954
|
-
const temporary = temporaryPathFor(target);
|
|
13955
|
-
try {
|
|
13956
|
-
await writeFile5(temporary, data);
|
|
13957
|
-
for (let attempt = 0; ; attempt += 1) {
|
|
14833
|
+
refs = await history.changedRefs(filePath);
|
|
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;
|
|
13958
14847
|
try {
|
|
13959
|
-
await
|
|
13960
|
-
|
|
13961
|
-
} catch
|
|
13962
|
-
if (
|
|
13963
|
-
|
|
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;
|
|
13964
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;
|
|
13965
14861
|
}
|
|
13966
|
-
|
|
13967
|
-
await
|
|
13968
|
-
throw error;
|
|
13969
|
-
}
|
|
13970
|
-
}
|
|
13971
|
-
async function ensureVaultMarker(dir, deps = DEFAULT_WRITE_DEPS) {
|
|
13972
|
-
const marker = vaultMarkerFile(dir);
|
|
13973
|
-
try {
|
|
13974
|
-
await stat18(marker);
|
|
13975
|
-
return;
|
|
13976
|
-
} catch {
|
|
13977
|
-
}
|
|
13978
|
-
await mkdir8(dir, { recursive: true });
|
|
13979
|
-
await writeFileAtomic(marker, `${JSON.stringify({ format: VAULT_FORMAT, createdAt: deps.now() })}
|
|
13980
|
-
`, deps);
|
|
13981
|
-
}
|
|
13982
|
-
async function assetPresent(file, size) {
|
|
13983
|
-
try {
|
|
13984
|
-
const info = await stat18(file);
|
|
13985
|
-
return info.isFile() && info.size === size;
|
|
13986
|
-
} catch {
|
|
13987
|
-
return false;
|
|
13988
|
-
}
|
|
13989
|
-
}
|
|
13990
|
-
async function writeSessionFile(dir, serialized, deps = DEFAULT_WRITE_DEPS) {
|
|
13991
|
-
const { agent, sessionId } = serialized.header;
|
|
13992
|
-
const file = sessionFile(dir, agent, sessionId);
|
|
13993
|
-
await ensureVaultMarker(dir, deps);
|
|
13994
|
-
await mkdir8(agentSessionsDir(dir, agent), { recursive: true });
|
|
13995
|
-
let assetsWritten = 0;
|
|
13996
|
-
let assetBytesWritten = 0;
|
|
13997
|
-
if (serialized.assets.size > 0) {
|
|
13998
|
-
await mkdir8(assetsDir(dir, agent, sessionId), { recursive: true });
|
|
13999
|
-
for (const [name, bytes] of serialized.assets) {
|
|
14000
|
-
const target = assetFile(dir, agent, sessionId, name);
|
|
14001
|
-
if (await assetPresent(target, bytes.length)) continue;
|
|
14002
|
-
await writeFileAtomic(target, bytes, deps);
|
|
14003
|
-
assetsWritten += 1;
|
|
14004
|
-
assetBytesWritten += bytes.length;
|
|
14005
|
-
}
|
|
14862
|
+
if (changed) this.emitProjects();
|
|
14863
|
+
await this.saveCache();
|
|
14006
14864
|
}
|
|
14007
|
-
|
|
14008
|
-
await writeFileAtomic(file, text, deps);
|
|
14009
|
-
return { file, bytes: text.length, assetsWritten, assetBytesWritten };
|
|
14010
|
-
}
|
|
14865
|
+
};
|
|
14011
14866
|
|
|
14012
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";
|
|
14013
14870
|
var SETTINGS_VERSION = 1;
|
|
14014
14871
|
var VAULT_TOOL_RESULT_DEFAULT_CHARS = 64e3;
|
|
14015
14872
|
var VAULT_TOOL_RESULT_MIN_CHARS = 4e3;
|
|
@@ -14017,13 +14874,14 @@ var VAULT_TOOL_RESULT_MAX_CHARS = 1e6;
|
|
|
14017
14874
|
function defaultAppSettings() {
|
|
14018
14875
|
return {
|
|
14019
14876
|
version: SETTINGS_VERSION,
|
|
14020
|
-
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 }
|
|
14021
14879
|
};
|
|
14022
14880
|
}
|
|
14023
14881
|
function isAbsoluteDir(dir, platform) {
|
|
14024
14882
|
if (dir.length === 0 || dir.includes("\0")) return false;
|
|
14025
14883
|
if (platform === "win32") return /^[A-Za-z]:[\\/]/.test(dir) || /^\\\\[^\\/]+[\\/][^\\/]+/.test(dir);
|
|
14026
|
-
return
|
|
14884
|
+
return path37.posix.isAbsolute(dir);
|
|
14027
14885
|
}
|
|
14028
14886
|
function clampToolResultMaxChars(value) {
|
|
14029
14887
|
if (typeof value !== "number" || !Number.isFinite(value)) return VAULT_TOOL_RESULT_DEFAULT_CHARS;
|
|
@@ -14033,6 +14891,11 @@ function parseAppSettings(value, platform, warn = () => void 0) {
|
|
|
14033
14891
|
const record = asRecord(value);
|
|
14034
14892
|
if (record === null || record["version"] !== SETTINGS_VERSION) return null;
|
|
14035
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
|
+
}
|
|
14036
14899
|
const vault = asRecord(record["vault"]);
|
|
14037
14900
|
if (vault === null) return settings2;
|
|
14038
14901
|
settings2.vault.enabled = asBoolean(vault["enabled"]) ?? false;
|
|
@@ -14046,7 +14909,11 @@ function parseAppSettings(value, platform, warn = () => void 0) {
|
|
|
14046
14909
|
return settings2;
|
|
14047
14910
|
}
|
|
14048
14911
|
function frozen(settings2) {
|
|
14049
|
-
return Object.freeze({
|
|
14912
|
+
return Object.freeze({
|
|
14913
|
+
...settings2,
|
|
14914
|
+
vault: Object.freeze({ ...settings2.vault }),
|
|
14915
|
+
remote: Object.freeze({ ...settings2.remote })
|
|
14916
|
+
});
|
|
14050
14917
|
}
|
|
14051
14918
|
var SettingsStore = class {
|
|
14052
14919
|
/** La ruta es inyectable para las pruebas; por defecto, la de la carpeta de configuracion. */
|
|
@@ -14065,7 +14932,7 @@ var SettingsStore = class {
|
|
|
14065
14932
|
async load() {
|
|
14066
14933
|
let raw;
|
|
14067
14934
|
try {
|
|
14068
|
-
raw = await
|
|
14935
|
+
raw = await readFile13(this.filePath, "utf8");
|
|
14069
14936
|
} catch {
|
|
14070
14937
|
this.current = frozen(defaultAppSettings());
|
|
14071
14938
|
return;
|
|
@@ -14084,17 +14951,49 @@ var SettingsStore = class {
|
|
|
14084
14951
|
}
|
|
14085
14952
|
this.current = frozen(settings2 ?? defaultAppSettings());
|
|
14086
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
|
+
}
|
|
14087
14980
|
/** Lo ultimo que quedo escrito. Congelado: nadie de afuera lo cambia sin pasar por `update`. */
|
|
14088
14981
|
get() {
|
|
14089
14982
|
return this.current;
|
|
14090
14983
|
}
|
|
14091
14984
|
/**
|
|
14092
|
-
* Aplica un cambio y lo escribe. Lanza si la carpeta no es absoluta
|
|
14093
|
-
* 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.
|
|
14094
14988
|
*/
|
|
14095
14989
|
update(patch) {
|
|
14096
14990
|
const run = async () => {
|
|
14097
|
-
|
|
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
|
+
};
|
|
14098
14997
|
const vault = patch.vault ?? {};
|
|
14099
14998
|
if (vault.enabled !== void 0) next.vault.enabled = vault.enabled;
|
|
14100
14999
|
if (vault.dir !== void 0) {
|
|
@@ -14106,7 +15005,13 @@ var SettingsStore = class {
|
|
|
14106
15005
|
if (vault.toolResultMaxChars !== void 0) {
|
|
14107
15006
|
next.vault.toolResultMaxChars = clampToolResultMaxChars(vault.toolResultMaxChars);
|
|
14108
15007
|
}
|
|
14109
|
-
|
|
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 });
|
|
14110
15015
|
await writeFileAtomic(this.filePath, `${JSON.stringify(next, null, 2)}
|
|
14111
15016
|
`);
|
|
14112
15017
|
this.current = frozen(next);
|
|
@@ -14133,6 +15038,19 @@ function statusLineStartupText(state) {
|
|
|
14133
15038
|
return "couldn't read its settings.json";
|
|
14134
15039
|
}
|
|
14135
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
|
+
}
|
|
14136
15054
|
var AVAILABLE_AGENTS_LABEL = "Available CLIs";
|
|
14137
15055
|
var HISTORY_LABEL = "History";
|
|
14138
15056
|
function missingText(text) {
|
|
@@ -14171,11 +15089,18 @@ function startupAgentLines(agents) {
|
|
|
14171
15089
|
}
|
|
14172
15090
|
return lines;
|
|
14173
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
|
+
}
|
|
14174
15099
|
|
|
14175
15100
|
// packages/server/src/notes-store.ts
|
|
14176
|
-
import { randomUUID as
|
|
14177
|
-
import { mkdir as
|
|
14178
|
-
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";
|
|
14179
15104
|
var STATE_VERSION2 = 1;
|
|
14180
15105
|
var WRITE_DEBOUNCE_MS2 = 500;
|
|
14181
15106
|
var NotesError = class extends ServerTextError {
|
|
@@ -14218,7 +15143,7 @@ var NotesStore = class {
|
|
|
14218
15143
|
}
|
|
14219
15144
|
async load() {
|
|
14220
15145
|
try {
|
|
14221
|
-
const raw = await
|
|
15146
|
+
const raw = await readFile14(this.statePath, "utf8");
|
|
14222
15147
|
this.notes = parseState2(raw) ?? [];
|
|
14223
15148
|
} catch {
|
|
14224
15149
|
this.notes = [];
|
|
@@ -14302,11 +15227,11 @@ var NotesStore = class {
|
|
|
14302
15227
|
throw new NotesError(serverText("imageUnknownFormat"));
|
|
14303
15228
|
}
|
|
14304
15229
|
const image = {
|
|
14305
|
-
imageId:
|
|
15230
|
+
imageId: randomUUID4(),
|
|
14306
15231
|
mediaType: format.mediaType,
|
|
14307
15232
|
bytes: bytes.length
|
|
14308
15233
|
};
|
|
14309
|
-
await
|
|
15234
|
+
await mkdir11(this.imagesDir, { recursive: true });
|
|
14310
15235
|
await writeFile6(this.imagePath(image), bytes);
|
|
14311
15236
|
note.images.push(image);
|
|
14312
15237
|
note.updatedAt = now;
|
|
@@ -14335,7 +15260,7 @@ var NotesStore = class {
|
|
|
14335
15260
|
const image = this.findImage(imageId);
|
|
14336
15261
|
if (image === null) return null;
|
|
14337
15262
|
try {
|
|
14338
|
-
const bytes = await
|
|
15263
|
+
const bytes = await readFile14(this.imagePath(image));
|
|
14339
15264
|
return { mediaType: image.mediaType, data: bytes.toString("base64") };
|
|
14340
15265
|
} catch {
|
|
14341
15266
|
return null;
|
|
@@ -14355,7 +15280,7 @@ var NotesStore = class {
|
|
|
14355
15280
|
const images = [];
|
|
14356
15281
|
for (const image of note.images) {
|
|
14357
15282
|
try {
|
|
14358
|
-
const bytes = await
|
|
15283
|
+
const bytes = await readFile14(this.imagePath(image));
|
|
14359
15284
|
images.push({ mediaType: image.mediaType, data: bytes.toString("base64") });
|
|
14360
15285
|
} catch {
|
|
14361
15286
|
}
|
|
@@ -14379,7 +15304,7 @@ var NotesStore = class {
|
|
|
14379
15304
|
}
|
|
14380
15305
|
imagePath(image) {
|
|
14381
15306
|
const safe = image.imageId.replace(/[^a-zA-Z0-9-]/g, "");
|
|
14382
|
-
return
|
|
15307
|
+
return path38.join(this.imagesDir, `${safe}.${extensionFor(image.mediaType)}`);
|
|
14383
15308
|
}
|
|
14384
15309
|
async unlinkImage(image) {
|
|
14385
15310
|
await rm4(this.imagePath(image), { force: true }).catch(() => {
|
|
@@ -14397,8 +15322,8 @@ var NotesStore = class {
|
|
|
14397
15322
|
async writeNow() {
|
|
14398
15323
|
this.dirty = false;
|
|
14399
15324
|
try {
|
|
14400
|
-
await
|
|
14401
|
-
const temporary =
|
|
15325
|
+
await mkdir11(path38.dirname(this.statePath), { recursive: true });
|
|
15326
|
+
const temporary = path38.join(path38.dirname(this.statePath), `notes.${process.pid}.tmp`);
|
|
14402
15327
|
await writeFile6(
|
|
14403
15328
|
temporary,
|
|
14404
15329
|
JSON.stringify({ version: STATE_VERSION2, notes: this.notes }, null, 2),
|
|
@@ -14411,10 +15336,226 @@ var NotesStore = class {
|
|
|
14411
15336
|
}
|
|
14412
15337
|
};
|
|
14413
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
|
+
|
|
14414
15555
|
// packages/server/src/shell-locator.ts
|
|
14415
15556
|
import { access as access2 } from "node:fs/promises";
|
|
14416
15557
|
import { constants as fsConstants2 } from "node:fs";
|
|
14417
|
-
import
|
|
15558
|
+
import path40 from "node:path";
|
|
14418
15559
|
async function isExecutable(candidate) {
|
|
14419
15560
|
try {
|
|
14420
15561
|
await access2(candidate, process.platform === "win32" ? fsConstants2.R_OK : fsConstants2.X_OK);
|
|
@@ -14426,7 +15567,7 @@ async function isExecutable(candidate) {
|
|
|
14426
15567
|
function windowsPowerShellPath() {
|
|
14427
15568
|
const systemRoot = process.env["SystemRoot"] ?? process.env["windir"];
|
|
14428
15569
|
if (systemRoot === void 0 || systemRoot.length === 0) return null;
|
|
14429
|
-
return
|
|
15570
|
+
return path40.join(systemRoot, "System32", "WindowsPowerShell", "v1.0", "powershell.exe");
|
|
14430
15571
|
}
|
|
14431
15572
|
async function locateWindowsShell() {
|
|
14432
15573
|
const pwsh = await findInPath("pwsh");
|
|
@@ -14446,7 +15587,7 @@ async function locateWindowsShell() {
|
|
|
14446
15587
|
async function locatePosixShell() {
|
|
14447
15588
|
const preferred = process.env["SHELL"];
|
|
14448
15589
|
if (preferred !== void 0 && preferred.length > 0 && await isExecutable(preferred)) {
|
|
14449
|
-
return { file: preferred, args: [], name:
|
|
15590
|
+
return { file: preferred, args: [], name: path40.basename(preferred) };
|
|
14450
15591
|
}
|
|
14451
15592
|
if (await isExecutable("/bin/bash")) return { file: "/bin/bash", args: [], name: "bash" };
|
|
14452
15593
|
if (await isExecutable("/bin/sh")) return { file: "/bin/sh", args: [], name: "sh" };
|
|
@@ -14457,13 +15598,13 @@ async function locateShell() {
|
|
|
14457
15598
|
}
|
|
14458
15599
|
|
|
14459
15600
|
// packages/server/src/session-watcher.ts
|
|
14460
|
-
import { stat as
|
|
15601
|
+
import { stat as stat21 } from "node:fs/promises";
|
|
14461
15602
|
import chokidar3 from "chokidar";
|
|
14462
15603
|
var INDEX_DEBOUNCE_MS = 750;
|
|
14463
15604
|
var ROOT_PROBE_MS = 5e3;
|
|
14464
15605
|
async function isDirectory3(target) {
|
|
14465
15606
|
try {
|
|
14466
|
-
return (await
|
|
15607
|
+
return (await stat21(target)).isDirectory();
|
|
14467
15608
|
} catch {
|
|
14468
15609
|
return false;
|
|
14469
15610
|
}
|
|
@@ -14564,9 +15705,9 @@ function watchSessions(index, hub, agents, options = {}) {
|
|
|
14564
15705
|
}
|
|
14565
15706
|
|
|
14566
15707
|
// packages/server/src/terminal-registry.ts
|
|
14567
|
-
import { randomUUID as
|
|
15708
|
+
import { randomUUID as randomUUID5 } from "node:crypto";
|
|
14568
15709
|
import { EventEmitter as EventEmitter5 } from "node:events";
|
|
14569
|
-
import { stat as
|
|
15710
|
+
import { stat as stat22 } from "node:fs/promises";
|
|
14570
15711
|
|
|
14571
15712
|
// packages/server/src/launch-hook-slot.ts
|
|
14572
15713
|
var LaunchHookSlot = class {
|
|
@@ -14828,10 +15969,10 @@ async function resolveLaunchPlan(launch) {
|
|
|
14828
15969
|
}
|
|
14829
15970
|
|
|
14830
15971
|
// packages/server/src/workspace-store.ts
|
|
14831
|
-
import { mkdir as
|
|
14832
|
-
import
|
|
14833
|
-
var
|
|
14834
|
-
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;
|
|
14835
15976
|
var LEGACY_TAB_AGENT = "claude-code";
|
|
14836
15977
|
function placeByPosition(base, placed) {
|
|
14837
15978
|
const result = [...base];
|
|
@@ -14887,11 +16028,11 @@ function readTab(tab, missingAgent) {
|
|
|
14887
16028
|
if (agent === null) return null;
|
|
14888
16029
|
return { kind: "known", tab: { agent, cwd, sessionId, label: typeof label === "string" ? label : "" } };
|
|
14889
16030
|
}
|
|
14890
|
-
function
|
|
16031
|
+
function parseState4(raw) {
|
|
14891
16032
|
try {
|
|
14892
16033
|
const parsed = JSON.parse(raw);
|
|
14893
16034
|
if (!isRecord(parsed)) return null;
|
|
14894
|
-
if (parsed["version"] !==
|
|
16035
|
+
if (parsed["version"] !== STATE_VERSION4) return null;
|
|
14895
16036
|
const rawTabs = parsed["tabs"];
|
|
14896
16037
|
if (!Array.isArray(rawTabs)) return null;
|
|
14897
16038
|
const base = [];
|
|
@@ -14929,7 +16070,7 @@ function serializeState(state) {
|
|
|
14929
16070
|
otherTabs.push({ ...entry.tab, position });
|
|
14930
16071
|
}
|
|
14931
16072
|
}
|
|
14932
|
-
return otherTabs.length === 0 ? { version:
|
|
16073
|
+
return otherTabs.length === 0 ? { version: STATE_VERSION4, tabs } : { version: STATE_VERSION4, tabs, otherTabs };
|
|
14933
16074
|
}
|
|
14934
16075
|
var EMPTY_STATE = () => ({ tabs: [], foreignTabs: [] });
|
|
14935
16076
|
function persistableTabs(descriptors) {
|
|
@@ -14951,8 +16092,8 @@ var WorkspaceStore = class {
|
|
|
14951
16092
|
pending = null;
|
|
14952
16093
|
async load() {
|
|
14953
16094
|
try {
|
|
14954
|
-
const raw = await
|
|
14955
|
-
return
|
|
16095
|
+
const raw = await readFile16(workspaceStatePath(), "utf8");
|
|
16096
|
+
return parseState4(raw) ?? EMPTY_STATE();
|
|
14956
16097
|
} catch {
|
|
14957
16098
|
return EMPTY_STATE();
|
|
14958
16099
|
}
|
|
@@ -14966,7 +16107,7 @@ var WorkspaceStore = class {
|
|
|
14966
16107
|
const toWrite = this.pending;
|
|
14967
16108
|
this.pending = null;
|
|
14968
16109
|
if (toWrite !== null) void this.writeNow(toWrite);
|
|
14969
|
-
},
|
|
16110
|
+
}, WRITE_DEBOUNCE_MS4);
|
|
14970
16111
|
this.writeTimer.unref();
|
|
14971
16112
|
}
|
|
14972
16113
|
/** Escritura inmediata, para el apagado. */
|
|
@@ -14981,12 +16122,12 @@ var WorkspaceStore = class {
|
|
|
14981
16122
|
}
|
|
14982
16123
|
async writeNow(state) {
|
|
14983
16124
|
try {
|
|
14984
|
-
await
|
|
16125
|
+
await mkdir13(appConfigDir(), { recursive: true });
|
|
14985
16126
|
const target = workspaceStatePath();
|
|
14986
|
-
const temporary =
|
|
14987
|
-
await
|
|
14988
|
-
const { rename:
|
|
14989
|
-
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);
|
|
14990
16131
|
} catch (error) {
|
|
14991
16132
|
console.warn("[workspace] couldn't save the state:", error);
|
|
14992
16133
|
}
|
|
@@ -15103,14 +16244,14 @@ var TerminalRegistry = class extends EventEmitter5 {
|
|
|
15103
16244
|
}
|
|
15104
16245
|
await this.assertDirectory(options.cwd);
|
|
15105
16246
|
const resumed = kind === "agent" && options.resumeSessionId !== void 0;
|
|
15106
|
-
const terminalId =
|
|
16247
|
+
const terminalId = randomUUID5();
|
|
15107
16248
|
const descriptor = {
|
|
15108
16249
|
terminalId,
|
|
15109
16250
|
kind,
|
|
15110
16251
|
agent,
|
|
15111
16252
|
cwd: options.cwd,
|
|
15112
16253
|
// Provisional: `spawn` lo confirma con lo que diga el adaptador.
|
|
15113
|
-
sessionId: kind === "agent" ? options.resumeSessionId ??
|
|
16254
|
+
sessionId: kind === "agent" ? options.resumeSessionId ?? randomUUID5() : "",
|
|
15114
16255
|
label: options.label ?? "",
|
|
15115
16256
|
resumed,
|
|
15116
16257
|
createdAt: Date.now(),
|
|
@@ -15214,7 +16355,7 @@ var TerminalRegistry = class extends EventEmitter5 {
|
|
|
15214
16355
|
async assertDirectory(cwd) {
|
|
15215
16356
|
let info;
|
|
15216
16357
|
try {
|
|
15217
|
-
info = await
|
|
16358
|
+
info = await stat22(cwd);
|
|
15218
16359
|
} catch {
|
|
15219
16360
|
throw new TerminalOpenError("invalid-cwd", serverText("cwdMissing", { cwd }));
|
|
15220
16361
|
}
|
|
@@ -15242,7 +16383,7 @@ var TerminalRegistry = class extends EventEmitter5 {
|
|
|
15242
16383
|
let launch;
|
|
15243
16384
|
let adapter = null;
|
|
15244
16385
|
let launchedSessionId = sessionId;
|
|
15245
|
-
const launchToken =
|
|
16386
|
+
const launchToken = randomUUID5();
|
|
15246
16387
|
if (launcher.kind === "agent") {
|
|
15247
16388
|
adapter = launcher.agent.adapter;
|
|
15248
16389
|
const { location } = launcher.agent;
|
|
@@ -15478,7 +16619,7 @@ var TerminalRegistry = class extends EventEmitter5 {
|
|
|
15478
16619
|
console.warn(`[workspace] the tab of ${tab.cwd} wasn't restored: ${reason2}`);
|
|
15479
16620
|
continue;
|
|
15480
16621
|
}
|
|
15481
|
-
const terminalId =
|
|
16622
|
+
const terminalId = randomUUID5();
|
|
15482
16623
|
const descriptor = {
|
|
15483
16624
|
terminalId,
|
|
15484
16625
|
kind: "agent",
|
|
@@ -15640,20 +16781,20 @@ var TerminalRegistry = class extends EventEmitter5 {
|
|
|
15640
16781
|
import { WebSocketServer } from "ws";
|
|
15641
16782
|
|
|
15642
16783
|
// packages/server/src/directory-picker.ts
|
|
15643
|
-
import { randomUUID as
|
|
15644
|
-
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";
|
|
15645
16786
|
import { homedir as homedir11 } from "node:os";
|
|
15646
|
-
import
|
|
16787
|
+
import path42 from "node:path";
|
|
15647
16788
|
var MAX_ENTRIES = 500;
|
|
15648
16789
|
var MAX_PICKERS = 8;
|
|
15649
16790
|
var SKIPPED = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "bin", "obj", "$RECYCLE.BIN"]);
|
|
15650
16791
|
var DirectoryPickerError = class extends ServerTextError {
|
|
15651
16792
|
};
|
|
15652
16793
|
function isInsideProtected(target, dirs) {
|
|
15653
|
-
const resolved =
|
|
16794
|
+
const resolved = path42.resolve(target);
|
|
15654
16795
|
return dirs.some((dir) => {
|
|
15655
|
-
const protectedDir =
|
|
15656
|
-
return resolved === protectedDir || resolved.startsWith(protectedDir +
|
|
16796
|
+
const protectedDir = path42.resolve(dir);
|
|
16797
|
+
return resolved === protectedDir || resolved.startsWith(protectedDir + path42.sep);
|
|
15657
16798
|
});
|
|
15658
16799
|
}
|
|
15659
16800
|
async function listRoots() {
|
|
@@ -15665,7 +16806,7 @@ async function listRoots() {
|
|
|
15665
16806
|
letters.map(async (letter) => {
|
|
15666
16807
|
const drive = `${letter}:\\`;
|
|
15667
16808
|
try {
|
|
15668
|
-
await
|
|
16809
|
+
await stat23(drive);
|
|
15669
16810
|
return drive;
|
|
15670
16811
|
} catch {
|
|
15671
16812
|
return null;
|
|
@@ -15692,7 +16833,7 @@ var DirectoryPickers = class {
|
|
|
15692
16833
|
if (this.pickers.size >= MAX_PICKERS) {
|
|
15693
16834
|
throw new DirectoryPickerError(serverText("pickerTooMany"));
|
|
15694
16835
|
}
|
|
15695
|
-
const picker = { id:
|
|
16836
|
+
const picker = { id: randomUUID6(), current: homedir11(), entries: /* @__PURE__ */ new Set() };
|
|
15696
16837
|
this.pickers.set(picker.id, picker);
|
|
15697
16838
|
return this.listing(picker);
|
|
15698
16839
|
}
|
|
@@ -15717,14 +16858,14 @@ var DirectoryPickers = class {
|
|
|
15717
16858
|
async enter(pickerId, name) {
|
|
15718
16859
|
const picker = this.require(pickerId);
|
|
15719
16860
|
if (name === "..") {
|
|
15720
|
-
const parent =
|
|
16861
|
+
const parent = path42.dirname(picker.current);
|
|
15721
16862
|
if (parent !== picker.current) picker.current = parent;
|
|
15722
16863
|
return this.listing(picker);
|
|
15723
16864
|
}
|
|
15724
16865
|
if (!picker.entries.has(name)) {
|
|
15725
16866
|
throw new DirectoryPickerError(serverText("pickerNotListed"));
|
|
15726
16867
|
}
|
|
15727
|
-
const target =
|
|
16868
|
+
const target = path42.join(picker.current, name);
|
|
15728
16869
|
if (isInsideProtected(target, this.protectedDirs)) {
|
|
15729
16870
|
throw new DirectoryPickerError(serverText("pickerCliFolder"));
|
|
15730
16871
|
}
|
|
@@ -15764,12 +16905,12 @@ var DirectoryPickers = class {
|
|
|
15764
16905
|
throw new DirectoryPickerError(serverText("pickerNameChars"));
|
|
15765
16906
|
}
|
|
15766
16907
|
}
|
|
15767
|
-
const target =
|
|
16908
|
+
const target = path42.join(picker.current, clean);
|
|
15768
16909
|
if (isInsideProtected(target, this.protectedDirs)) {
|
|
15769
16910
|
throw new DirectoryPickerError(serverText("pickerNoProjectsHere"));
|
|
15770
16911
|
}
|
|
15771
16912
|
try {
|
|
15772
|
-
await
|
|
16913
|
+
await mkdir14(target);
|
|
15773
16914
|
} catch (error) {
|
|
15774
16915
|
const code = error.code;
|
|
15775
16916
|
throw new DirectoryPickerError(
|
|
@@ -15801,7 +16942,7 @@ var DirectoryPickers = class {
|
|
|
15801
16942
|
let truncated = false;
|
|
15802
16943
|
try {
|
|
15803
16944
|
const found = await readdir11(picker.current, { withFileTypes: true });
|
|
15804
|
-
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));
|
|
15805
16946
|
truncated = dirs.length > MAX_ENTRIES;
|
|
15806
16947
|
names = dirs.slice(0, MAX_ENTRIES);
|
|
15807
16948
|
} catch {
|
|
@@ -15812,15 +16953,15 @@ var DirectoryPickers = class {
|
|
|
15812
16953
|
path: picker.current,
|
|
15813
16954
|
entries: names,
|
|
15814
16955
|
roots,
|
|
15815
|
-
canGoUp:
|
|
16956
|
+
canGoUp: path42.dirname(picker.current) !== picker.current,
|
|
15816
16957
|
truncated
|
|
15817
16958
|
};
|
|
15818
16959
|
}
|
|
15819
16960
|
};
|
|
15820
16961
|
|
|
15821
16962
|
// packages/server/src/file-browser.ts
|
|
15822
|
-
import { readFile as
|
|
15823
|
-
import
|
|
16963
|
+
import { readFile as readFile17, readdir as readdir12, stat as stat24 } from "node:fs/promises";
|
|
16964
|
+
import path43 from "node:path";
|
|
15824
16965
|
var MAX_ENTRIES2 = 1e3;
|
|
15825
16966
|
var SEARCH_MAX_RESULTS = 200;
|
|
15826
16967
|
var SEARCH_MAX_DIRECTORIES = 4e3;
|
|
@@ -15883,7 +17024,7 @@ function languageFor(fileName) {
|
|
|
15883
17024
|
const lower = fileName.toLowerCase();
|
|
15884
17025
|
const byName = LANGUAGE_BY_NAME[lower];
|
|
15885
17026
|
if (byName !== void 0) return byName;
|
|
15886
|
-
const extension =
|
|
17027
|
+
const extension = path43.extname(lower);
|
|
15887
17028
|
return LANGUAGE_BY_EXTENSION[extension] ?? null;
|
|
15888
17029
|
}
|
|
15889
17030
|
function looksBinary2(buffer) {
|
|
@@ -15915,7 +17056,7 @@ async function listDirectory(cwd, relativePath, options = {}) {
|
|
|
15915
17056
|
let sizeBytes = 0;
|
|
15916
17057
|
let modifiedAt = 0;
|
|
15917
17058
|
try {
|
|
15918
|
-
const info = await
|
|
17059
|
+
const info = await stat24(path43.join(absolute, candidate.name));
|
|
15919
17060
|
sizeBytes = candidate.isDirectory ? 0 : info.size;
|
|
15920
17061
|
modifiedAt = info.mtimeMs;
|
|
15921
17062
|
} catch {
|
|
@@ -15948,7 +17089,7 @@ async function readLevel(absolute) {
|
|
|
15948
17089
|
const always = ALWAYS_SKIP.has(dirent.name.toLowerCase());
|
|
15949
17090
|
if (dirent.isSymbolicLink()) {
|
|
15950
17091
|
try {
|
|
15951
|
-
const target = await
|
|
17092
|
+
const target = await stat24(path43.join(absolute, dirent.name));
|
|
15952
17093
|
candidates.push({
|
|
15953
17094
|
name: dirent.name,
|
|
15954
17095
|
isDirectory: target.isDirectory(),
|
|
@@ -15977,7 +17118,7 @@ async function markIgnored(repoRoot2, levels) {
|
|
|
15977
17118
|
for (const level of levels) {
|
|
15978
17119
|
for (const entry of level.entries) {
|
|
15979
17120
|
if (entry.hidden !== null) continue;
|
|
15980
|
-
keys.set(entry, toPosixPath(
|
|
17121
|
+
keys.set(entry, toPosixPath(path43.relative(repoRoot2, path43.join(level.absolute, entry.name))));
|
|
15981
17122
|
}
|
|
15982
17123
|
}
|
|
15983
17124
|
if (keys.size === 0) return;
|
|
@@ -16038,7 +17179,7 @@ async function searchFiles(cwd, query, options = {}) {
|
|
|
16038
17179
|
let sizeBytes = 0;
|
|
16039
17180
|
let modifiedAt = 0;
|
|
16040
17181
|
try {
|
|
16041
|
-
const info = await
|
|
17182
|
+
const info = await stat24(path43.join(level.absolute, candidate.name));
|
|
16042
17183
|
sizeBytes = candidate.isDirectory ? 0 : info.size;
|
|
16043
17184
|
modifiedAt = info.mtimeMs;
|
|
16044
17185
|
} catch {
|
|
@@ -16072,12 +17213,12 @@ async function searchFiles(cwd, query, options = {}) {
|
|
|
16072
17213
|
}
|
|
16073
17214
|
async function readPreview(cwd, relativePath) {
|
|
16074
17215
|
const absolute = await resolveInside(cwd, relativePath, { mustExist: true });
|
|
16075
|
-
const info = await
|
|
17216
|
+
const info = await stat24(absolute);
|
|
16076
17217
|
if (info.isDirectory()) {
|
|
16077
17218
|
throw new Error("It's a directory, not a file.");
|
|
16078
17219
|
}
|
|
16079
|
-
const content = await
|
|
16080
|
-
const fileName =
|
|
17220
|
+
const content = await readFile17(absolute);
|
|
17221
|
+
const fileName = path43.basename(absolute);
|
|
16081
17222
|
if (looksBinary2(content)) {
|
|
16082
17223
|
return {
|
|
16083
17224
|
path: relativePath,
|
|
@@ -16666,14 +17807,14 @@ var TerminalWriteQueue = class {
|
|
|
16666
17807
|
};
|
|
16667
17808
|
|
|
16668
17809
|
// packages/server/src/vault/service.ts
|
|
16669
|
-
import { mkdir as
|
|
17810
|
+
import { mkdir as mkdir17, stat as stat28 } from "node:fs/promises";
|
|
16670
17811
|
import { homedir as homedir12 } from "node:os";
|
|
16671
|
-
import
|
|
17812
|
+
import path47 from "node:path";
|
|
16672
17813
|
|
|
16673
17814
|
// packages/server/src/vault/move.ts
|
|
16674
17815
|
import { constants } from "node:fs";
|
|
16675
|
-
import { copyFile as copyFile2, lstat, mkdir as
|
|
16676
|
-
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";
|
|
16677
17818
|
var MOVED_ENTRIES = ["vault.json", "sessions", "memory"];
|
|
16678
17819
|
var OWN_TEMP_FILE = /\.\d+\.[0-9a-f]{12}\.tmp$/;
|
|
16679
17820
|
async function copyTree(source, target, result) {
|
|
@@ -16684,14 +17825,14 @@ async function copyTree(source, target, result) {
|
|
|
16684
17825
|
return;
|
|
16685
17826
|
}
|
|
16686
17827
|
if (info.isDirectory()) {
|
|
16687
|
-
await
|
|
17828
|
+
await mkdir15(target, { recursive: true });
|
|
16688
17829
|
for (const name of await readdir13(source)) {
|
|
16689
|
-
await copyTree(
|
|
17830
|
+
await copyTree(path44.join(source, name), path44.join(target, name), result);
|
|
16690
17831
|
}
|
|
16691
17832
|
return;
|
|
16692
17833
|
}
|
|
16693
17834
|
if (!info.isFile() || OWN_TEMP_FILE.test(source)) return;
|
|
16694
|
-
await
|
|
17835
|
+
await mkdir15(path44.dirname(target), { recursive: true });
|
|
16695
17836
|
try {
|
|
16696
17837
|
await copyFile2(source, target, constants.COPYFILE_EXCL);
|
|
16697
17838
|
} catch (error) {
|
|
@@ -16706,9 +17847,9 @@ async function copyTree(source, target, result) {
|
|
|
16706
17847
|
}
|
|
16707
17848
|
async function moveVault(from, to) {
|
|
16708
17849
|
const result = { copied: 0, skipped: 0 };
|
|
16709
|
-
await
|
|
17850
|
+
await mkdir15(to, { recursive: true });
|
|
16710
17851
|
for (const entry of MOVED_ENTRIES) {
|
|
16711
|
-
await copyTree(
|
|
17852
|
+
await copyTree(path44.join(from, entry), path44.join(to, entry), result);
|
|
16712
17853
|
}
|
|
16713
17854
|
return result;
|
|
16714
17855
|
}
|
|
@@ -16740,11 +17881,11 @@ function checkVaultTarget(current, target, options) {
|
|
|
16740
17881
|
return { kind: "ok" };
|
|
16741
17882
|
}
|
|
16742
17883
|
async function canWriteInto(dir) {
|
|
16743
|
-
const probe = temporaryPathFor(
|
|
17884
|
+
const probe = temporaryPathFor(path44.join(dir, ".escritura"));
|
|
16744
17885
|
try {
|
|
16745
|
-
await
|
|
16746
|
-
if (!(await
|
|
16747
|
-
await
|
|
17886
|
+
await mkdir15(dir, { recursive: true });
|
|
17887
|
+
if (!(await stat25(dir)).isDirectory()) return false;
|
|
17888
|
+
await writeFile9(probe, "");
|
|
16748
17889
|
return true;
|
|
16749
17890
|
} catch {
|
|
16750
17891
|
return false;
|
|
@@ -16754,12 +17895,12 @@ async function canWriteInto(dir) {
|
|
|
16754
17895
|
}
|
|
16755
17896
|
|
|
16756
17897
|
// packages/server/src/vault/serialize.ts
|
|
16757
|
-
import { createHash as
|
|
17898
|
+
import { createHash as createHash4 } from "node:crypto";
|
|
16758
17899
|
function imageKey(eventId, source, index) {
|
|
16759
17900
|
return `${eventId}|${source}|${index}`;
|
|
16760
17901
|
}
|
|
16761
17902
|
function assetName(bytes) {
|
|
16762
|
-
const hash =
|
|
17903
|
+
const hash = createHash4("sha256").update(bytes).digest("hex").slice(0, 32);
|
|
16763
17904
|
return `${hash}.${detectImageFormat(bytes)?.ext ?? "bin"}`;
|
|
16764
17905
|
}
|
|
16765
17906
|
function buildHeader(input) {
|
|
@@ -16846,12 +17987,12 @@ function serializeSession(input) {
|
|
|
16846
17987
|
}
|
|
16847
17988
|
|
|
16848
17989
|
// packages/server/src/vault/writer.ts
|
|
16849
|
-
import { readdir as readdir15, rm as rm6, stat as
|
|
16850
|
-
import
|
|
17990
|
+
import { readdir as readdir15, rm as rm6, stat as stat27 } from "node:fs/promises";
|
|
17991
|
+
import path46 from "node:path";
|
|
16851
17992
|
|
|
16852
17993
|
// packages/server/src/vault/memory-copy.ts
|
|
16853
|
-
import { mkdir as
|
|
16854
|
-
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";
|
|
16855
17996
|
var MEMORY_FILE_MAX_BYTES = 1024 * 1024;
|
|
16856
17997
|
var MEMORY_MAX_FILES = 500;
|
|
16857
17998
|
var MEMORY_SOURCE_FILE = "source.json";
|
|
@@ -16876,7 +18017,7 @@ async function listMemoryNotes(cwd) {
|
|
|
16876
18017
|
let absolute;
|
|
16877
18018
|
try {
|
|
16878
18019
|
absolute = await resolveInside(cwd, `${MEMORY_DIR}/${name}`, { mustExist: true });
|
|
16879
|
-
const info = await
|
|
18020
|
+
const info = await stat26(absolute);
|
|
16880
18021
|
if (!info.isFile() || info.size > MEMORY_FILE_MAX_BYTES) continue;
|
|
16881
18022
|
notes.push({ name, absolute, size: info.size, mtimeMs: info.mtimeMs });
|
|
16882
18023
|
} catch {
|
|
@@ -16890,7 +18031,7 @@ async function measureProjectMemory(cwd) {
|
|
|
16890
18031
|
}
|
|
16891
18032
|
async function isUpToDate(target, note) {
|
|
16892
18033
|
try {
|
|
16893
|
-
const info = await
|
|
18034
|
+
const info = await stat26(target);
|
|
16894
18035
|
return info.isFile() && info.size === note.size && Math.abs(info.mtimeMs - note.mtimeMs) < SAME_MTIME_MS;
|
|
16895
18036
|
} catch {
|
|
16896
18037
|
return false;
|
|
@@ -16898,7 +18039,7 @@ async function isUpToDate(target, note) {
|
|
|
16898
18039
|
}
|
|
16899
18040
|
async function exists3(file) {
|
|
16900
18041
|
try {
|
|
16901
|
-
await
|
|
18042
|
+
await stat26(file);
|
|
16902
18043
|
return true;
|
|
16903
18044
|
} catch {
|
|
16904
18045
|
return false;
|
|
@@ -16914,14 +18055,14 @@ async function copyProjectMemory(dir, cwd, platform, deps = DEFAULT_WRITE_DEPS)
|
|
|
16914
18055
|
if (prepared) return;
|
|
16915
18056
|
prepared = true;
|
|
16916
18057
|
await ensureVaultMarker(dir, deps);
|
|
16917
|
-
await
|
|
18058
|
+
await mkdir16(target, { recursive: true });
|
|
16918
18059
|
};
|
|
16919
18060
|
for (const note of notes) {
|
|
16920
|
-
const destination =
|
|
18061
|
+
const destination = path45.join(target, note.name);
|
|
16921
18062
|
if (await isUpToDate(destination, note)) continue;
|
|
16922
18063
|
let content;
|
|
16923
18064
|
try {
|
|
16924
|
-
content = await
|
|
18065
|
+
content = await readFile18(note.absolute);
|
|
16925
18066
|
} catch {
|
|
16926
18067
|
continue;
|
|
16927
18068
|
}
|
|
@@ -16933,7 +18074,7 @@ async function copyProjectMemory(dir, cwd, platform, deps = DEFAULT_WRITE_DEPS)
|
|
|
16933
18074
|
result.files += 1;
|
|
16934
18075
|
result.bytes += content.length;
|
|
16935
18076
|
}
|
|
16936
|
-
const source =
|
|
18077
|
+
const source = path45.join(target, MEMORY_SOURCE_FILE);
|
|
16937
18078
|
if (result.files > 0 || !await exists3(source)) {
|
|
16938
18079
|
await prepare();
|
|
16939
18080
|
await writeFileAtomic(source, `${JSON.stringify({ cwd, copiedAt: deps.now() })}
|
|
@@ -17427,7 +18568,7 @@ var VaultWriter = class {
|
|
|
17427
18568
|
}
|
|
17428
18569
|
async assetOnDisk(dir, agent, sessionId, asset, bytes) {
|
|
17429
18570
|
try {
|
|
17430
|
-
const info = await
|
|
18571
|
+
const info = await stat27(assetFile(dir, agent, sessionId, asset));
|
|
17431
18572
|
return info.isFile() && info.size === bytes;
|
|
17432
18573
|
} catch {
|
|
17433
18574
|
return false;
|
|
@@ -17527,17 +18668,17 @@ var VaultWriter = class {
|
|
|
17527
18668
|
const folders = [dir];
|
|
17528
18669
|
const subfolders = async (parent) => {
|
|
17529
18670
|
try {
|
|
17530
|
-
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));
|
|
17531
18672
|
} catch {
|
|
17532
18673
|
return [];
|
|
17533
18674
|
}
|
|
17534
18675
|
};
|
|
17535
18676
|
for (const agentDir of await subfolders(sessionsRoot2(dir))) {
|
|
17536
|
-
if (!SESSION_AGENT_IDS.includes(
|
|
18677
|
+
if (!SESSION_AGENT_IDS.includes(path46.basename(agentDir))) continue;
|
|
17537
18678
|
folders.push(agentDir);
|
|
17538
18679
|
folders.push(...(await subfolders(agentDir)).filter((folder) => folder.endsWith(".assets")));
|
|
17539
18680
|
}
|
|
17540
|
-
folders.push(...await subfolders(
|
|
18681
|
+
folders.push(...await subfolders(path46.join(dir, "memory")));
|
|
17541
18682
|
for (const folder of folders) {
|
|
17542
18683
|
let names;
|
|
17543
18684
|
try {
|
|
@@ -17547,9 +18688,9 @@ var VaultWriter = class {
|
|
|
17547
18688
|
}
|
|
17548
18689
|
for (const name of names) {
|
|
17549
18690
|
if (!OWN_TEMP_FILE2.test(name)) continue;
|
|
17550
|
-
const file =
|
|
18691
|
+
const file = path46.join(folder, name);
|
|
17551
18692
|
try {
|
|
17552
|
-
const info = await
|
|
18693
|
+
const info = await stat27(file);
|
|
17553
18694
|
if (info.isFile() && now - info.mtimeMs > TEMP_MAX_AGE_MS) await rm6(file, { force: true });
|
|
17554
18695
|
} catch {
|
|
17555
18696
|
}
|
|
@@ -17737,14 +18878,14 @@ var VaultService = class {
|
|
|
17737
18878
|
async setDir(target) {
|
|
17738
18879
|
const { platform, agents, settings: settings2, catalog } = this.options;
|
|
17739
18880
|
const current = this.dir();
|
|
17740
|
-
const protectedDirs = [...agents.protectedDirs(),
|
|
18881
|
+
const protectedDirs = [...agents.protectedDirs(), path47.join(this.options.homeDir ?? homedir12(), ".gemini")];
|
|
17741
18882
|
const check = checkVaultTarget(current, target, { protectedDirs, platform });
|
|
17742
18883
|
if (check.kind === "refused") throw new VaultError(check.text);
|
|
17743
18884
|
if (check.kind === "same") return;
|
|
17744
18885
|
try {
|
|
17745
18886
|
await this.writer.exclusive("moving", async () => {
|
|
17746
18887
|
if (!await canWriteInto(target)) throw new VaultError(serverText("vaultCannotWrite"));
|
|
17747
|
-
const hadCopy = await
|
|
18888
|
+
const hadCopy = await stat28(vaultMarkerFile(current)).then(
|
|
17748
18889
|
() => true,
|
|
17749
18890
|
() => false
|
|
17750
18891
|
);
|
|
@@ -17778,7 +18919,7 @@ var VaultService = class {
|
|
|
17778
18919
|
/** Abre la carpeta de la copia con el explorador del sistema. */
|
|
17779
18920
|
async reveal() {
|
|
17780
18921
|
const dir = this.activeDir();
|
|
17781
|
-
const isFolder = await
|
|
18922
|
+
const isFolder = await stat28(dir).then(
|
|
17782
18923
|
(info) => info.isDirectory(),
|
|
17783
18924
|
() => false
|
|
17784
18925
|
);
|
|
@@ -17802,7 +18943,7 @@ var VaultService = class {
|
|
|
17802
18943
|
fromCopy: true
|
|
17803
18944
|
});
|
|
17804
18945
|
const file = sessionExportFile(dir, agent, sessionId);
|
|
17805
|
-
await
|
|
18946
|
+
await mkdir17(path47.dirname(file), { recursive: true });
|
|
17806
18947
|
await writeFileAtomic(file, text, this.writeDeps);
|
|
17807
18948
|
this.openWithSystem(file);
|
|
17808
18949
|
return file;
|
|
@@ -17889,7 +19030,7 @@ var VaultService = class {
|
|
|
17889
19030
|
const project = this.options.index.getProjects().find((candidate) => candidate.key === projectKey);
|
|
17890
19031
|
if (project === void 0) throw new VaultError(serverText("vaultProjectGone"));
|
|
17891
19032
|
const folder = projectExportDirFor(this.activeDir(), project, this.options.platform);
|
|
17892
|
-
await
|
|
19033
|
+
await mkdir17(folder, { recursive: true });
|
|
17893
19034
|
const entries = [];
|
|
17894
19035
|
const names = /* @__PURE__ */ new Set();
|
|
17895
19036
|
let skipped = 0;
|
|
@@ -17906,7 +19047,7 @@ var VaultService = class {
|
|
|
17906
19047
|
continue;
|
|
17907
19048
|
}
|
|
17908
19049
|
const fileName = this.exportFileName(summary, names);
|
|
17909
|
-
await writeFileAtomic(
|
|
19050
|
+
await writeFileAtomic(path47.join(folder, fileName), text, this.writeDeps);
|
|
17910
19051
|
entries.push({
|
|
17911
19052
|
updatedAt: summary.updatedAt,
|
|
17912
19053
|
agentLabel: this.labelOf(summary.agent),
|
|
@@ -17917,9 +19058,9 @@ var VaultService = class {
|
|
|
17917
19058
|
});
|
|
17918
19059
|
await nextTurn3();
|
|
17919
19060
|
}
|
|
17920
|
-
const name = project.cwd.length > 0 ?
|
|
19061
|
+
const name = project.cwd.length > 0 ? path47.basename(project.cwd) || project.cwd : project.fallbackName;
|
|
17921
19062
|
const index = renderProjectIndex({ name, cwd: project.cwd, exportedAt: this.now(), entries, skipped });
|
|
17922
|
-
await writeFileAtomic(
|
|
19063
|
+
await writeFileAtomic(path47.join(folder, "index.md"), index, this.writeDeps);
|
|
17923
19064
|
this.openWithSystem(folder);
|
|
17924
19065
|
return { sessions: entries.length, skipped, folder };
|
|
17925
19066
|
}
|
|
@@ -18075,12 +19216,14 @@ function attachTerminalSocket(options) {
|
|
|
18075
19216
|
index,
|
|
18076
19217
|
archived,
|
|
18077
19218
|
notes,
|
|
19219
|
+
drafts,
|
|
18078
19220
|
conversations,
|
|
18079
19221
|
repos,
|
|
18080
19222
|
memory,
|
|
18081
19223
|
agents,
|
|
18082
19224
|
defaultCwd,
|
|
18083
|
-
vault
|
|
19225
|
+
vault,
|
|
19226
|
+
remote
|
|
18084
19227
|
} = options;
|
|
18085
19228
|
const pasteStore = options.pasteStore ?? new PasteStore();
|
|
18086
19229
|
const writeQueue = new TerminalWriteQueue();
|
|
@@ -18095,6 +19238,14 @@ function attachTerminalSocket(options) {
|
|
|
18095
19238
|
if (socket.readyState === socket.OPEN) socket.send(payload);
|
|
18096
19239
|
}
|
|
18097
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
|
+
};
|
|
18098
19249
|
const broadcastExcept = (except, message) => {
|
|
18099
19250
|
const payload = encodeServerMessage(message);
|
|
18100
19251
|
for (const socket of clients) {
|
|
@@ -18145,7 +19296,25 @@ function attachTerminalSocket(options) {
|
|
|
18145
19296
|
}
|
|
18146
19297
|
return false;
|
|
18147
19298
|
};
|
|
18148
|
-
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
|
+
};
|
|
18149
19318
|
const onRegistryExit = (terminalId, exitCode, signal) => broadcast({ type: "terminal.exit", terminalId, exitCode, signal });
|
|
18150
19319
|
const onRegistryActivity = (terminalId, activity, offlineReason) => broadcast(
|
|
18151
19320
|
offlineReason === null ? { type: "terminal.activity", terminalId, activity } : { type: "terminal.activity", terminalId, activity, offlineReason }
|
|
@@ -18173,7 +19342,9 @@ function attachTerminalSocket(options) {
|
|
|
18173
19342
|
};
|
|
18174
19343
|
const pieceWriter = (socket, terminalId) => (piece) => writeToTerminal(socket, terminalId, piece);
|
|
18175
19344
|
const dropTab = (terminalId) => {
|
|
19345
|
+
const draftOwner = draftOwnerOf(registry.get(terminalId));
|
|
18176
19346
|
if (!registry.close(terminalId)) return false;
|
|
19347
|
+
if (draftOwner !== null) drafts.delete(draftOwner);
|
|
18177
19348
|
conversations.drop(terminalId);
|
|
18178
19349
|
repos.drop(terminalId);
|
|
18179
19350
|
memory.drop(terminalId);
|
|
@@ -18245,6 +19416,23 @@ function attachTerminalSocket(options) {
|
|
|
18245
19416
|
memory.on("status", onMemoryStatus);
|
|
18246
19417
|
const stopAgentChanges = agents.subscribeChanges(() => broadcast(agentsMessage()));
|
|
18247
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
|
+
};
|
|
18248
19436
|
const sendVaultError = (socket, error) => {
|
|
18249
19437
|
if (error instanceof VaultError) {
|
|
18250
19438
|
sendError(socket, "vault-failed", error.text);
|
|
@@ -18262,20 +19450,25 @@ function attachTerminalSocket(options) {
|
|
|
18262
19450
|
return;
|
|
18263
19451
|
}
|
|
18264
19452
|
if (pathname !== WS_PATH) return;
|
|
18265
|
-
const
|
|
18266
|
-
if (
|
|
19453
|
+
const authorization = authorizeRequest(request, port, token, remote.verifier());
|
|
19454
|
+
if (!authorization.ok) {
|
|
18267
19455
|
socket.write("HTTP/1.1 403 Forbidden\r\nConnection: close\r\n\r\n");
|
|
18268
19456
|
socket.destroy();
|
|
18269
|
-
console.warn(`[security] upgrade rejected on ${WS_PATH}: ${
|
|
19457
|
+
console.warn(`[security] upgrade rejected on ${WS_PATH}: ${authorization.reason}`);
|
|
18270
19458
|
return;
|
|
18271
19459
|
}
|
|
18272
19460
|
wss.handleUpgrade(request, socket, head, (client) => {
|
|
18273
|
-
wss.emit("connection", client, request);
|
|
19461
|
+
wss.emit("connection", client, request, authorization.credential);
|
|
18274
19462
|
});
|
|
18275
19463
|
};
|
|
18276
19464
|
httpServer.on("upgrade", onUpgrade);
|
|
18277
|
-
wss.on("connection", (socket) => {
|
|
19465
|
+
wss.on("connection", (socket, _request, credential) => {
|
|
18278
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
|
+
}
|
|
18279
19472
|
const listener = (terminalId, chunk) => {
|
|
18280
19473
|
send(socket, { type: "terminal.output", terminalId, data: chunk });
|
|
18281
19474
|
};
|
|
@@ -18338,7 +19531,8 @@ function attachTerminalSocket(options) {
|
|
|
18338
19531
|
defaultAgent: agents.defaultAgent(),
|
|
18339
19532
|
platform: process.platform,
|
|
18340
19533
|
defaultCwd,
|
|
18341
|
-
shellName: shell?.name ?? null
|
|
19534
|
+
shellName: shell?.name ?? null,
|
|
19535
|
+
remoteClient: deviceId !== null
|
|
18342
19536
|
});
|
|
18343
19537
|
send(socket, terminalListMessage());
|
|
18344
19538
|
for (const entry of registry.activitySnapshot()) {
|
|
@@ -18347,7 +19541,9 @@ function attachTerminalSocket(options) {
|
|
|
18347
19541
|
send(socket, { type: "index.status", status: index.getStatus() });
|
|
18348
19542
|
send(socket, { type: "index.projects", projects: index.getProjects(), replace: true });
|
|
18349
19543
|
send(socket, { type: "notes.list", notes: notes.list() });
|
|
19544
|
+
send(socket, { type: "composer.drafts", drafts: draftEntriesOf(registry.list()) });
|
|
18350
19545
|
send(socket, { type: "vault.status", status: vault.status() });
|
|
19546
|
+
if (deviceId === null) send(socket, { type: "remote.status", status: remote.status() });
|
|
18351
19547
|
socket.on("message", (raw) => {
|
|
18352
19548
|
const text = Array.isArray(raw) ? Buffer.concat(raw).toString("utf8") : Buffer.from(raw).toString("utf8");
|
|
18353
19549
|
const message = parseClientMessage(text);
|
|
@@ -18357,9 +19553,16 @@ function attachTerminalSocket(options) {
|
|
|
18357
19553
|
return;
|
|
18358
19554
|
}
|
|
18359
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
|
+
}
|
|
18360
19561
|
switch (message.type) {
|
|
18361
19562
|
case "input":
|
|
18362
|
-
|
|
19563
|
+
if (!registry.write(message.terminalId, message.data) && registry.get(message.terminalId) === null) {
|
|
19564
|
+
sendError(socket, "unknown-terminal", serverText("terminalGone"));
|
|
19565
|
+
}
|
|
18363
19566
|
break;
|
|
18364
19567
|
/*
|
|
18365
19568
|
Un mensaje del cuadro de escritura.
|
|
@@ -18375,6 +19578,8 @@ function attachTerminalSocket(options) {
|
|
|
18375
19578
|
*/
|
|
18376
19579
|
case "agent.submit": {
|
|
18377
19580
|
const { terminalId, text: text2, images } = message;
|
|
19581
|
+
const submittedOwner = draftOwnerOf(registry.get(terminalId));
|
|
19582
|
+
if (submittedOwner !== null) drafts.delete(submittedOwner);
|
|
18378
19583
|
const files = message.files ?? [];
|
|
18379
19584
|
if (files.length > MAX_FILES_PER_SUBMIT) {
|
|
18380
19585
|
sendError(
|
|
@@ -18402,7 +19607,7 @@ function attachTerminalSocket(options) {
|
|
|
18402
19607
|
if (submitting !== null) {
|
|
18403
19608
|
const launchedAt = registry.launchedAtOf(terminalId);
|
|
18404
19609
|
const blind = isBlind();
|
|
18405
|
-
const
|
|
19610
|
+
const refusal2 = submitRefusal({
|
|
18406
19611
|
label: submitting.label,
|
|
18407
19612
|
approvesPendingOnCycle: guardsPending,
|
|
18408
19613
|
waitingFor: conversations.getWaitingFor(terminalId),
|
|
@@ -18411,8 +19616,8 @@ function attachTerminalSocket(options) {
|
|
|
18411
19616
|
blind,
|
|
18412
19617
|
openToolCall: blind && launchedAt !== null && conversations.hasOpenToolCall(terminalId, launchedAt)
|
|
18413
19618
|
});
|
|
18414
|
-
if (
|
|
18415
|
-
sendError(socket, "submit-failed",
|
|
19619
|
+
if (refusal2 !== null) {
|
|
19620
|
+
sendError(socket, "submit-failed", refusal2);
|
|
18416
19621
|
break;
|
|
18417
19622
|
}
|
|
18418
19623
|
}
|
|
@@ -19083,6 +20288,19 @@ function attachTerminalSocket(options) {
|
|
|
19083
20288
|
}
|
|
19084
20289
|
})();
|
|
19085
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
|
+
}
|
|
19086
20304
|
/*
|
|
19087
20305
|
Notas sueltas. No van dirigidas a ninguna pestana.
|
|
19088
20306
|
|
|
@@ -19367,9 +20585,70 @@ function attachTerminalSocket(options) {
|
|
|
19367
20585
|
case "search.cancel":
|
|
19368
20586
|
globalSearch.cancel(message.searchId);
|
|
19369
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;
|
|
19370
20645
|
}
|
|
19371
20646
|
});
|
|
19372
20647
|
const teardown = () => {
|
|
20648
|
+
if (phonePairingOwner === socket) {
|
|
20649
|
+
phonePairingOwner = null;
|
|
20650
|
+
remote.cancelPhonePairing();
|
|
20651
|
+
}
|
|
19373
20652
|
globalSearch.cancel();
|
|
19374
20653
|
pickers.closeAll();
|
|
19375
20654
|
registry.detachAll(listener);
|
|
@@ -19380,6 +20659,7 @@ function attachTerminalSocket(options) {
|
|
|
19380
20659
|
for (const terminalId of memorySubscriptions) memory.unsubscribe(terminalId);
|
|
19381
20660
|
memorySubscriptions.clear();
|
|
19382
20661
|
clients.delete(socket);
|
|
20662
|
+
if (deviceId !== null && remoteSockets.delete(socket)) remote.noteDisconnected(deviceId);
|
|
19383
20663
|
};
|
|
19384
20664
|
socket.on("close", teardown);
|
|
19385
20665
|
socket.on("error", teardown);
|
|
@@ -19404,18 +20684,20 @@ function attachTerminalSocket(options) {
|
|
|
19404
20684
|
memory.off("status", onMemoryStatus);
|
|
19405
20685
|
stopAgentChanges();
|
|
19406
20686
|
stopVaultStatus();
|
|
20687
|
+
stopRemoteStatus();
|
|
20688
|
+
stopRemoteRevoked();
|
|
19407
20689
|
for (const client of wss.clients) client.terminate();
|
|
19408
20690
|
wss.close();
|
|
19409
20691
|
};
|
|
19410
20692
|
}
|
|
19411
20693
|
|
|
19412
20694
|
// packages/server/src/vault/catalog.ts
|
|
19413
|
-
import { open as open7, readdir as readdir16, stat as
|
|
19414
|
-
import
|
|
20695
|
+
import { open as open7, readdir as readdir16, stat as stat29 } from "node:fs/promises";
|
|
20696
|
+
import path48 from "node:path";
|
|
19415
20697
|
import { createInterface as createInterface2 } from "node:readline";
|
|
19416
20698
|
var HEADER_MAX_BYTES = 64 * 1024;
|
|
19417
20699
|
var LOAD_CONCURRENCY = 16;
|
|
19418
|
-
var
|
|
20700
|
+
var keyOf2 = (agent, sessionId) => `${agent}/${sessionId}`;
|
|
19419
20701
|
function isSessionAgentId2(value) {
|
|
19420
20702
|
return SESSION_AGENT_IDS.includes(value);
|
|
19421
20703
|
}
|
|
@@ -19431,7 +20713,7 @@ async function assetBytesOf(dir, agent, sessionId) {
|
|
|
19431
20713
|
for (const name of names) {
|
|
19432
20714
|
if (!VAULT_ASSET_NAME_PATTERN.test(name)) continue;
|
|
19433
20715
|
try {
|
|
19434
|
-
const info = await
|
|
20716
|
+
const info = await stat29(path48.join(folder, name));
|
|
19435
20717
|
if (info.isFile()) total += info.size;
|
|
19436
20718
|
} catch {
|
|
19437
20719
|
}
|
|
@@ -19442,7 +20724,7 @@ async function readEntry(dir, agent, sessionId) {
|
|
|
19442
20724
|
const file = sessionFile(dir, agent, sessionId);
|
|
19443
20725
|
let sizeBytes;
|
|
19444
20726
|
try {
|
|
19445
|
-
const info = await
|
|
20727
|
+
const info = await stat29(file);
|
|
19446
20728
|
if (!info.isFile()) return { kind: "absent" };
|
|
19447
20729
|
sizeBytes = info.size;
|
|
19448
20730
|
} catch {
|
|
@@ -19523,8 +20805,8 @@ var VaultCatalog = class {
|
|
|
19523
20805
|
const foreign = /* @__PURE__ */ new Set();
|
|
19524
20806
|
keys.forEach((key, position) => {
|
|
19525
20807
|
const read = reads[position];
|
|
19526
|
-
if (read?.kind === "listed") entries.set(
|
|
19527
|
-
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));
|
|
19528
20810
|
});
|
|
19529
20811
|
this.entries = entries;
|
|
19530
20812
|
this.foreign = foreign;
|
|
@@ -19538,7 +20820,7 @@ var VaultCatalog = class {
|
|
|
19538
20820
|
const generation = this.generation;
|
|
19539
20821
|
const read = await readEntry(dir, agent, sessionId);
|
|
19540
20822
|
if (generation !== this.generation) return;
|
|
19541
|
-
const key =
|
|
20823
|
+
const key = keyOf2(agent, sessionId);
|
|
19542
20824
|
const before = JSON.stringify(this.entries.get(key) ?? null);
|
|
19543
20825
|
const wasForeign = this.foreign.has(key);
|
|
19544
20826
|
this.entries.delete(key);
|
|
@@ -19568,11 +20850,11 @@ var VaultCatalog = class {
|
|
|
19568
20850
|
return this.summaryCache;
|
|
19569
20851
|
}
|
|
19570
20852
|
header(agent, sessionId) {
|
|
19571
|
-
return this.entries.get(
|
|
20853
|
+
return this.entries.get(keyOf2(agent, sessionId))?.header ?? null;
|
|
19572
20854
|
}
|
|
19573
20855
|
/** true si en disco hay una copia de esa sesion con un formato que esta app no conoce. */
|
|
19574
20856
|
hasForeignFormat(agent, sessionId) {
|
|
19575
|
-
return this.foreign.has(
|
|
20857
|
+
return this.foreign.has(keyOf2(agent, sessionId));
|
|
19576
20858
|
}
|
|
19577
20859
|
/**
|
|
19578
20860
|
* El cuerpo de una sesion listada, linea por linea: eventos y documentos.
|
|
@@ -19595,7 +20877,7 @@ var VaultCatalog = class {
|
|
|
19595
20877
|
*/
|
|
19596
20878
|
async *bodyLines(agent, sessionId) {
|
|
19597
20879
|
const dir = this.dir;
|
|
19598
|
-
if (dir === null || !this.entries.has(
|
|
20880
|
+
if (dir === null || !this.entries.has(keyOf2(agent, sessionId))) return;
|
|
19599
20881
|
const handle = await open7(sessionFile(dir, agent, sessionId), "r").catch(() => null);
|
|
19600
20882
|
if (handle === null) return;
|
|
19601
20883
|
const input = handle.createReadStream({ encoding: "utf8" });
|
|
@@ -19651,38 +20933,77 @@ var VaultCatalog = class {
|
|
|
19651
20933
|
};
|
|
19652
20934
|
|
|
19653
20935
|
// packages/server/src/index.ts
|
|
19654
|
-
var serverDir =
|
|
20936
|
+
var serverDir = path49.dirname(fileURLToPath2(import.meta.url));
|
|
19655
20937
|
var isPackaged = true;
|
|
19656
|
-
var webRoot = isPackaged ? serverDir :
|
|
19657
|
-
var webDist = isPackaged ?
|
|
19658
|
-
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, "../../..");
|
|
19659
20941
|
var isProduction = isPackaged || process.argv.includes("--prod");
|
|
19660
20942
|
function resolveDefaultCwd() {
|
|
19661
20943
|
const override = process.env["AGENT_WORKBENCH_CWD"];
|
|
19662
|
-
if (override !== void 0 && override.length > 0) return
|
|
20944
|
+
if (override !== void 0 && override.length > 0) return path49.resolve(override);
|
|
19663
20945
|
if (isPackaged) return process.cwd();
|
|
19664
20946
|
return repoRoot;
|
|
19665
20947
|
}
|
|
19666
20948
|
var ORIGIN_REFUSED_TEXT = "Origin not allowed.\nOrigen no permitido.";
|
|
19667
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.";
|
|
19668
20950
|
var NOT_FOUND_TEXT = "Not found.\nNo encontrado.";
|
|
19669
|
-
|
|
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) {
|
|
19670
20955
|
return (request, response, next) => {
|
|
19671
|
-
|
|
19672
|
-
|
|
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();
|
|
19673
20966
|
return;
|
|
19674
20967
|
}
|
|
19675
|
-
|
|
19676
|
-
|
|
19677
|
-
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);
|
|
19678
20970
|
return;
|
|
19679
20971
|
}
|
|
19680
|
-
|
|
19681
|
-
|
|
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;
|
|
19682
20984
|
}
|
|
19683
|
-
|
|
20985
|
+
response.status(401).type("text/plain").send(remote.verifier() === null ? TOKEN_MISSING_TEXT : TOKEN_MISSING_TEXT + PAIRING_HINT_TEXT);
|
|
19684
20986
|
};
|
|
19685
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
|
+
}
|
|
19686
21007
|
async function mountDevUi(app, httpServer) {
|
|
19687
21008
|
const { createServer: createViteServer } = await import("vite");
|
|
19688
21009
|
const vite = await createViteServer({
|
|
@@ -19695,7 +21016,7 @@ async function mountDevUi(app, httpServer) {
|
|
|
19695
21016
|
}
|
|
19696
21017
|
var LOOKS_LIKE_FILE = /\.[a-zA-Z0-9]{1,8}$/;
|
|
19697
21018
|
function mountBuiltUi(app) {
|
|
19698
|
-
const indexHtml =
|
|
21019
|
+
const indexHtml = path49.join(webDist, "index.html");
|
|
19699
21020
|
if (!existsSync4(indexHtml)) {
|
|
19700
21021
|
throw new Error(
|
|
19701
21022
|
`There's no compiled interface in ${webDist}.
|
|
@@ -19712,7 +21033,7 @@ Run "pnpm build" before "pnpm start", or use "pnpm dev".`
|
|
|
19712
21033
|
});
|
|
19713
21034
|
return () => Promise.resolve();
|
|
19714
21035
|
}
|
|
19715
|
-
function describeStartup(url, cwd, agents, shell) {
|
|
21036
|
+
function describeStartup(url, cwd, agents, shell, remoteLine) {
|
|
19716
21037
|
const agentList = agents.list();
|
|
19717
21038
|
const line = "-".repeat(64);
|
|
19718
21039
|
console.log(`
|
|
@@ -19734,6 +21055,9 @@ ${line}`);
|
|
|
19734
21055
|
}));
|
|
19735
21056
|
for (const agentLine of startupAgentLines(startupAgents)) console.log(agentLine);
|
|
19736
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());
|
|
19737
21061
|
if (agentList.some((info) => info.environmentNotice === "child-session-marker")) {
|
|
19738
21062
|
console.log("");
|
|
19739
21063
|
console.log(" Note: this process inherited CLAUDE_CODE_CHILD_SESSION, which turns off");
|
|
@@ -19751,6 +21075,7 @@ async function main() {
|
|
|
19751
21075
|
const movedFrom = await migrateLegacyConfigDir();
|
|
19752
21076
|
if (movedFrom !== null) console.log(`Configuration moved from ${movedFrom} to ${appConfigDir()}`);
|
|
19753
21077
|
await agents.prepareAll();
|
|
21078
|
+
ensurePtyHelperExecutable();
|
|
19754
21079
|
const store = new WorkspaceStore();
|
|
19755
21080
|
const archived = new ArchivedSessions();
|
|
19756
21081
|
await archived.load();
|
|
@@ -19769,6 +21094,8 @@ async function main() {
|
|
|
19769
21094
|
);
|
|
19770
21095
|
const notes = new NotesStore();
|
|
19771
21096
|
await notes.load();
|
|
21097
|
+
const drafts = new DraftStore();
|
|
21098
|
+
await drafts.load();
|
|
19772
21099
|
const conversations = new ConversationHub(registry, agents);
|
|
19773
21100
|
const repos = new RepoHub(registry);
|
|
19774
21101
|
const memory = new MemoryHub(registry);
|
|
@@ -19776,16 +21103,31 @@ async function main() {
|
|
|
19776
21103
|
const app = express();
|
|
19777
21104
|
app.disable("x-powered-by");
|
|
19778
21105
|
const httpServer = createServer(app);
|
|
19779
|
-
|
|
19780
|
-
|
|
19781
|
-
|
|
19782
|
-
|
|
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);
|
|
19783
21119
|
const address = httpServer.address();
|
|
19784
21120
|
if (address === null || typeof address === "string") {
|
|
19785
21121
|
throw new Error("Couldn't determine the assigned port.");
|
|
19786
21122
|
}
|
|
19787
21123
|
const { port } = address;
|
|
19788
|
-
|
|
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));
|
|
19789
21131
|
const closeUi = isProduction ? mountBuiltUi(app) : await mountDevUi(app, httpServer);
|
|
19790
21132
|
const detachSocket = attachTerminalSocket({
|
|
19791
21133
|
pasteStore,
|
|
@@ -19797,18 +21139,21 @@ async function main() {
|
|
|
19797
21139
|
index,
|
|
19798
21140
|
archived,
|
|
19799
21141
|
notes,
|
|
21142
|
+
drafts,
|
|
19800
21143
|
conversations,
|
|
19801
21144
|
repos,
|
|
19802
21145
|
memory,
|
|
19803
21146
|
agents,
|
|
19804
21147
|
defaultCwd,
|
|
19805
|
-
vault
|
|
21148
|
+
vault,
|
|
21149
|
+
remote
|
|
19806
21150
|
});
|
|
19807
21151
|
void pasteStore.purgeStale();
|
|
19808
21152
|
void index.start();
|
|
19809
21153
|
const stopWatching = watchSessions(index, conversations, agents);
|
|
19810
21154
|
if (process.env["AGENT_WORKBENCH_NO_RESTORE"] !== "1") {
|
|
19811
21155
|
void store.load().then((state) => {
|
|
21156
|
+
drafts.retainOnly(workspaceDraftOwners(state));
|
|
19812
21157
|
if (state.tabs.length === 0 && state.foreignTabs.length === 0) return;
|
|
19813
21158
|
if (agents.anyAvailable()) {
|
|
19814
21159
|
console.log(`Restoring ${state.tabs.length} tab(s) from the previous run...`);
|
|
@@ -19817,7 +21162,14 @@ async function main() {
|
|
|
19817
21162
|
});
|
|
19818
21163
|
}
|
|
19819
21164
|
const url = `http://${LOOPBACK_HOST}:${port}/?${TOKEN_QUERY_PARAM}=${token}`;
|
|
19820
|
-
|
|
21165
|
+
const remoteStatus = remote.status();
|
|
21166
|
+
describeStartup(
|
|
21167
|
+
url,
|
|
21168
|
+
defaultCwd,
|
|
21169
|
+
agents,
|
|
21170
|
+
shell,
|
|
21171
|
+
remoteAccessStartupLine(remoteStatus.state, remoteStatus.port, remote.pairedDevices)
|
|
21172
|
+
);
|
|
19821
21173
|
if (process.env["AGENT_WORKBENCH_NO_OPEN"] !== "1") openBrowser(url);
|
|
19822
21174
|
let shuttingDown = false;
|
|
19823
21175
|
const shutdown = () => {
|
|
@@ -19832,7 +21184,8 @@ async function main() {
|
|
|
19832
21184
|
repos.disposeAll();
|
|
19833
21185
|
void memory.disposeAll();
|
|
19834
21186
|
registry.disposeAll();
|
|
19835
|
-
|
|
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(() => {
|
|
19836
21189
|
httpServer.close(() => process.exit(0));
|
|
19837
21190
|
setTimeout(() => process.exit(0), 2e3).unref();
|
|
19838
21191
|
});
|