@wrongstack/core 0.291.0 → 0.291.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/boot.d.ts.map +1 -1
- package/dist/chronicle/index.js +20 -1
- package/dist/chronicle/index.js.map +2 -2
- package/dist/chronicle/tool-adapter.d.ts.map +1 -1
- package/dist/coordination/director.d.ts +18 -0
- package/dist/coordination/director.d.ts.map +1 -1
- package/dist/coordination/fleet-spawn.d.ts.map +1 -1
- package/dist/coordination/index.js +37 -15
- package/dist/coordination/index.js.map +2 -2
- package/dist/core/agent-response.d.ts.map +1 -1
- package/dist/defaults/index.js +660 -391
- package/dist/defaults/index.js.map +4 -4
- package/dist/execution/index.js +61 -12
- package/dist/execution/index.js.map +3 -3
- package/dist/execution/tool-executor.d.ts.map +1 -1
- package/dist/hq/index.js +17 -0
- package/dist/hq/index.js.map +2 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1304 -882
- package/dist/index.js.map +4 -4
- package/dist/infrastructure/index.js +15 -10
- package/dist/infrastructure/index.js.map +2 -2
- package/dist/kernel/events/tool-events.d.ts +24 -0
- package/dist/kernel/events/tool-events.d.ts.map +1 -1
- package/dist/plugins/auto-review-plugin.d.ts +10 -0
- package/dist/plugins/auto-review-plugin.d.ts.map +1 -1
- package/dist/security/index.js +148 -0
- package/dist/security/index.js.map +3 -3
- package/dist/security/permission-policy.d.ts +8 -1
- package/dist/security/permission-policy.d.ts.map +1 -1
- package/dist/storage/config-loader.d.ts +8 -0
- package/dist/storage/config-loader.d.ts.map +1 -1
- package/dist/storage/index.js +457 -379
- package/dist/storage/index.js.map +4 -4
- package/dist/storage/provider-config-watcher.d.ts +6 -0
- package/dist/storage/provider-config-watcher.d.ts.map +1 -1
- package/dist/types/permission.d.ts +38 -0
- package/dist/types/permission.d.ts.map +1 -1
- package/dist/utils/config-backup.d.ts +20 -0
- package/dist/utils/config-backup.d.ts.map +1 -0
- package/dist/utils/index.d.ts +2 -1
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +172 -106
- package/dist/utils/index.js.map +4 -4
- package/dist/utils/message-invariants.d.ts +12 -9
- package/dist/utils/message-invariants.d.ts.map +1 -1
- package/dist/utils/term.d.ts +6 -0
- package/dist/utils/term.d.ts.map +1 -1
- package/dist/utils/wstack-paths.d.ts +10 -0
- package/dist/utils/wstack-paths.d.ts.map +1 -1
- package/package.json +2 -2
package/dist/storage/index.js
CHANGED
|
@@ -2,14 +2,14 @@
|
|
|
2
2
|
import { createHash as createHash4 } from "node:crypto";
|
|
3
3
|
import { createReadStream } from "node:fs";
|
|
4
4
|
import * as fsp4 from "node:fs/promises";
|
|
5
|
-
import * as
|
|
5
|
+
import * as path8 from "node:path";
|
|
6
6
|
import { createInterface } from "node:readline";
|
|
7
7
|
|
|
8
8
|
// src/utils/atomic-write.ts
|
|
9
9
|
import { randomBytes as randomBytes2 } from "node:crypto";
|
|
10
|
-
import * as
|
|
10
|
+
import * as fs3 from "node:fs/promises";
|
|
11
11
|
import { watch as watchDir } from "node:fs";
|
|
12
|
-
import * as
|
|
12
|
+
import * as path4 from "node:path";
|
|
13
13
|
|
|
14
14
|
// src/utils/child-env.ts
|
|
15
15
|
var ALLOWED_KEYS = /* @__PURE__ */ new Set([
|
|
@@ -186,6 +186,37 @@ var color = {
|
|
|
186
186
|
bgGreen: wrap("42", "49")
|
|
187
187
|
};
|
|
188
188
|
|
|
189
|
+
// src/utils/config-backup.ts
|
|
190
|
+
import * as fs from "node:fs/promises";
|
|
191
|
+
import * as path from "node:path";
|
|
192
|
+
function configHistoryDir(globalRoot) {
|
|
193
|
+
return path.join(globalRoot, "config-history");
|
|
194
|
+
}
|
|
195
|
+
function configSlug(absolutePath, globalRoot) {
|
|
196
|
+
const rel = path.relative(globalRoot, absolutePath);
|
|
197
|
+
const normalized = rel.replace(/\\/g, "/").replace(/\.json$/i, "");
|
|
198
|
+
return normalized.replace(/\//g, "-");
|
|
199
|
+
}
|
|
200
|
+
async function backupConfigFile(filePath, paths) {
|
|
201
|
+
let currentContent;
|
|
202
|
+
try {
|
|
203
|
+
currentContent = await fs.readFile(filePath, "utf8");
|
|
204
|
+
if (!currentContent.trim()) return;
|
|
205
|
+
} catch {
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
const now = /* @__PURE__ */ new Date();
|
|
209
|
+
const ts = now.toISOString().replace(/[:.]/g, "-").replace(/Z$/, "");
|
|
210
|
+
const slug = configSlug(filePath, paths.globalRoot);
|
|
211
|
+
const backupDir = configHistoryDir(paths.globalRoot);
|
|
212
|
+
const backupFile = path.join(backupDir, `${slug}-${ts}.json`);
|
|
213
|
+
try {
|
|
214
|
+
await fs.mkdir(backupDir, { recursive: true });
|
|
215
|
+
await fs.writeFile(backupFile, currentContent, { mode: 384, encoding: "utf8" });
|
|
216
|
+
} catch {
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
189
220
|
// src/utils/deep-merge.ts
|
|
190
221
|
var FORBIDDEN_PROTO_KEYS = /* @__PURE__ */ new Set([
|
|
191
222
|
"__proto__",
|
|
@@ -261,16 +292,6 @@ function expectDefined(value, label) {
|
|
|
261
292
|
}
|
|
262
293
|
|
|
263
294
|
// src/utils/message-invariants.ts
|
|
264
|
-
function hasMeaningfulContent(content) {
|
|
265
|
-
if (typeof content === "string") return content.trim().length > 0;
|
|
266
|
-
return content.some((block) => {
|
|
267
|
-
if (block.type === "text") return block.text.trim().length > 0;
|
|
268
|
-
if (block.type === "thinking") {
|
|
269
|
-
return block.thinking.trim().length > 0 || Boolean(block.signature);
|
|
270
|
-
}
|
|
271
|
-
return true;
|
|
272
|
-
});
|
|
273
|
-
}
|
|
274
295
|
function repairToolUseAdjacency(messages) {
|
|
275
296
|
const removedToolUses = [];
|
|
276
297
|
const removedToolResults = [];
|
|
@@ -357,6 +378,21 @@ function mapContent(msg, fn) {
|
|
|
357
378
|
}
|
|
358
379
|
return { ...msg, content: next };
|
|
359
380
|
}
|
|
381
|
+
function hasMeaningfulContent(content) {
|
|
382
|
+
if (typeof content === "string") return content.trim().length > 0;
|
|
383
|
+
for (const block of content) {
|
|
384
|
+
if (block.type === "text") {
|
|
385
|
+
if (block.text.trim().length > 0) return true;
|
|
386
|
+
continue;
|
|
387
|
+
}
|
|
388
|
+
if (block.type === "thinking") {
|
|
389
|
+
if (block.thinking.trim().length > 0 || block.signature) return true;
|
|
390
|
+
continue;
|
|
391
|
+
}
|
|
392
|
+
return true;
|
|
393
|
+
}
|
|
394
|
+
return false;
|
|
395
|
+
}
|
|
360
396
|
function isEmptyMessage(msg) {
|
|
361
397
|
return !hasMeaningfulContent(msg.content);
|
|
362
398
|
}
|
|
@@ -413,14 +449,14 @@ function safeParse(input, maxBytes = 5e6) {
|
|
|
413
449
|
}
|
|
414
450
|
|
|
415
451
|
// src/utils/session-scoped-path.ts
|
|
416
|
-
import * as
|
|
452
|
+
import * as path2 from "node:path";
|
|
417
453
|
function sessionScopedPath(dir, sessionId, suffix) {
|
|
418
454
|
if (!sessionId || sessionId.includes("\\") || sessionId.includes("..")) {
|
|
419
455
|
throw invalid(sessionId);
|
|
420
456
|
}
|
|
421
|
-
const resolved =
|
|
422
|
-
const rel =
|
|
423
|
-
if (rel.startsWith("..") ||
|
|
457
|
+
const resolved = path2.resolve(dir, `${sessionId}${suffix}`);
|
|
458
|
+
const rel = path2.relative(path2.resolve(dir), resolved);
|
|
459
|
+
if (rel.startsWith("..") || path2.isAbsolute(rel)) {
|
|
424
460
|
throw invalid(sessionId);
|
|
425
461
|
}
|
|
426
462
|
return resolved;
|
|
@@ -469,23 +505,23 @@ function ulid(seedTime = Date.now()) {
|
|
|
469
505
|
|
|
470
506
|
// src/utils/wstack-paths.ts
|
|
471
507
|
import { createHash } from "node:crypto";
|
|
472
|
-
import * as
|
|
508
|
+
import * as fs2 from "node:fs";
|
|
473
509
|
import * as os from "node:os";
|
|
474
|
-
import * as
|
|
510
|
+
import * as path3 from "node:path";
|
|
475
511
|
function canonicalProjectRoot(absRoot) {
|
|
476
|
-
const checkoutRoot =
|
|
477
|
-
const dotGit =
|
|
512
|
+
const checkoutRoot = path3.resolve(absRoot);
|
|
513
|
+
const dotGit = path3.join(checkoutRoot, ".git");
|
|
478
514
|
try {
|
|
479
|
-
if (!
|
|
480
|
-
const gitDirLine =
|
|
515
|
+
if (!fs2.statSync(dotGit).isFile()) return checkoutRoot;
|
|
516
|
+
const gitDirLine = fs2.readFileSync(dotGit, "utf8").trim();
|
|
481
517
|
const match = /^gitdir:\s*(.+)$/i.exec(gitDirLine);
|
|
482
518
|
if (!match?.[1]) return checkoutRoot;
|
|
483
|
-
const gitDir =
|
|
484
|
-
const commonDirFile =
|
|
485
|
-
if (!
|
|
486
|
-
const commonDir =
|
|
487
|
-
if (
|
|
488
|
-
return
|
|
519
|
+
const gitDir = path3.resolve(checkoutRoot, match[1].trim());
|
|
520
|
+
const commonDirFile = path3.join(gitDir, "commondir");
|
|
521
|
+
if (!fs2.statSync(commonDirFile).isFile()) return checkoutRoot;
|
|
522
|
+
const commonDir = path3.resolve(gitDir, fs2.readFileSync(commonDirFile, "utf8").trim());
|
|
523
|
+
if (path3.basename(commonDir).toLowerCase() !== ".git") return checkoutRoot;
|
|
524
|
+
return path3.dirname(commonDir);
|
|
489
525
|
} catch {
|
|
490
526
|
return checkoutRoot;
|
|
491
527
|
}
|
|
@@ -495,7 +531,7 @@ function projectHash(absRoot) {
|
|
|
495
531
|
}
|
|
496
532
|
function projectSlug(absRoot) {
|
|
497
533
|
const identityRoot = canonicalProjectRoot(absRoot);
|
|
498
|
-
const base = slugify2(
|
|
534
|
+
const base = slugify2(path3.basename(identityRoot));
|
|
499
535
|
const hash = createHash("sha256").update(identityRoot).digest("hex").slice(0, 6);
|
|
500
536
|
return `${base}-${hash}`;
|
|
501
537
|
}
|
|
@@ -504,66 +540,83 @@ function slugify2(name) {
|
|
|
504
540
|
}
|
|
505
541
|
function wstackGlobalRoot() {
|
|
506
542
|
const fromEnv = process.env["WRONGSTACK_HOME"];
|
|
507
|
-
if (fromEnv && fromEnv.trim().length > 0) return
|
|
508
|
-
return
|
|
543
|
+
if (fromEnv && fromEnv.trim().length > 0) return path3.resolve(fromEnv);
|
|
544
|
+
return path3.join(os.homedir(), ".wrongstack");
|
|
509
545
|
}
|
|
510
546
|
function resolveWstackPaths(opts) {
|
|
511
|
-
const globalRoot = opts.globalRoot ?? (opts.userHome ?
|
|
547
|
+
const globalRoot = opts.globalRoot ?? (opts.userHome ? path3.join(opts.userHome, ".wrongstack") : wstackGlobalRoot());
|
|
512
548
|
const homeDir = opts.userHome ?? os.homedir();
|
|
513
549
|
const hash = projectHash(opts.projectRoot);
|
|
514
550
|
const slug = projectSlug(opts.projectRoot);
|
|
515
|
-
const projectDir =
|
|
551
|
+
const projectDir = path3.join(globalRoot, "projects", slug);
|
|
516
552
|
return {
|
|
517
553
|
globalRoot,
|
|
518
554
|
projectRoot: opts.projectRoot,
|
|
519
555
|
homeDir,
|
|
520
556
|
configDir: globalRoot,
|
|
521
|
-
globalConfig:
|
|
522
|
-
profilesDir:
|
|
557
|
+
globalConfig: path3.join(globalRoot, "config.json"),
|
|
558
|
+
profilesDir: path3.join(globalRoot, "profiles"),
|
|
523
559
|
profileConfig: (name) => {
|
|
524
560
|
const safe = name.replace(/[/\\:]/g, "_").replace(/\.\./g, "_");
|
|
525
|
-
return
|
|
561
|
+
return path3.join(globalRoot, "profiles", safe || "default", "config.json");
|
|
562
|
+
},
|
|
563
|
+
profileStatuslineConfig: (name) => {
|
|
564
|
+
const safe = name.replace(/[/\\:]/g, "_").replace(/\.\./g, "_");
|
|
565
|
+
return path3.join(globalRoot, "profiles", safe || "default", "statusline.json");
|
|
566
|
+
},
|
|
567
|
+
profileModeConfig: (name) => {
|
|
568
|
+
const safe = name.replace(/[/\\:]/g, "_").replace(/\.\./g, "_");
|
|
569
|
+
return path3.join(globalRoot, "profiles", safe || "default", "mode.json");
|
|
570
|
+
},
|
|
571
|
+
profileProviderStatus: (name) => {
|
|
572
|
+
const safe = name.replace(/[/\\:]/g, "_").replace(/\.\./g, "_");
|
|
573
|
+
return path3.join(globalRoot, "profiles", safe || "default", "provider-status.json");
|
|
574
|
+
},
|
|
575
|
+
profileUpdateCache: (name) => {
|
|
576
|
+
const safe = name.replace(/[/\\:]/g, "_").replace(/\.\./g, "_");
|
|
577
|
+
return path3.join(globalRoot, "profiles", safe || "default", "update-cache.json");
|
|
526
578
|
},
|
|
527
|
-
secretsKey:
|
|
528
|
-
globalMemory:
|
|
529
|
-
globalSkills:
|
|
530
|
-
globalClaudeSkills:
|
|
531
|
-
globalDesignKits:
|
|
532
|
-
globalPrompts:
|
|
533
|
-
globalInstructions:
|
|
534
|
-
promptUsage:
|
|
535
|
-
cacheDir:
|
|
536
|
-
modelsCache:
|
|
537
|
-
modelsOverlayCache:
|
|
538
|
-
historyFile:
|
|
539
|
-
logFile:
|
|
579
|
+
secretsKey: path3.join(globalRoot, ".key"),
|
|
580
|
+
globalMemory: path3.join(globalRoot, "memory.md"),
|
|
581
|
+
globalSkills: path3.join(globalRoot, "skills"),
|
|
582
|
+
globalClaudeSkills: path3.join(homeDir, ".claude", "skills"),
|
|
583
|
+
globalDesignKits: path3.join(globalRoot, "design-kits"),
|
|
584
|
+
globalPrompts: path3.join(globalRoot, "prompts"),
|
|
585
|
+
globalInstructions: path3.join(globalRoot, "instructions"),
|
|
586
|
+
promptUsage: path3.join(globalRoot, "prompt-usage.json"),
|
|
587
|
+
cacheDir: path3.join(globalRoot, "cache"),
|
|
588
|
+
modelsCache: path3.join(globalRoot, "cache", "models.dev.json"),
|
|
589
|
+
modelsOverlayCache: path3.join(globalRoot, "cache", "models-overlay.json"),
|
|
590
|
+
historyFile: path3.join(globalRoot, "history"),
|
|
591
|
+
logFile: path3.join(globalRoot, "logs", "wrongstack.log"),
|
|
540
592
|
projectDir,
|
|
541
|
-
projectCodebaseIndex:
|
|
542
|
-
projectMemory:
|
|
543
|
-
projectSessions:
|
|
544
|
-
projectTrust:
|
|
545
|
-
projectMeta:
|
|
546
|
-
projectLocalConfig:
|
|
547
|
-
inProjectConfig:
|
|
548
|
-
inProjectAgentsFile:
|
|
549
|
-
inProjectSkills:
|
|
550
|
-
inProjectClaudeSkills:
|
|
551
|
-
inProjectPrompts:
|
|
552
|
-
inProjectInstructions:
|
|
553
|
-
inProjectDesignKits:
|
|
554
|
-
inProjectWorktrees:
|
|
593
|
+
projectCodebaseIndex: path3.join(projectDir, "codebase-index"),
|
|
594
|
+
projectMemory: path3.join(projectDir, "memory.md"),
|
|
595
|
+
projectSessions: path3.join(projectDir, "sessions"),
|
|
596
|
+
projectTrust: path3.join(projectDir, "trust.json"),
|
|
597
|
+
projectMeta: path3.join(projectDir, "meta.json"),
|
|
598
|
+
projectLocalConfig: path3.join(projectDir, "config.local.json"),
|
|
599
|
+
inProjectConfig: path3.join(opts.projectRoot, ".wrongstack", "config.json"),
|
|
600
|
+
inProjectAgentsFile: path3.join(opts.projectRoot, ".wrongstack", "AGENTS.md"),
|
|
601
|
+
inProjectSkills: path3.join(opts.projectRoot, ".wrongstack", "skills"),
|
|
602
|
+
inProjectClaudeSkills: path3.join(opts.projectRoot, ".claude", "skills"),
|
|
603
|
+
inProjectPrompts: path3.join(opts.projectRoot, ".wrongstack", "prompts"),
|
|
604
|
+
inProjectInstructions: path3.join(opts.projectRoot, ".wrongstack", "instructions"),
|
|
605
|
+
inProjectDesignKits: path3.join(opts.projectRoot, ".wrongstack", "design-kits"),
|
|
606
|
+
inProjectWorktrees: path3.join(opts.projectRoot, ".wrongstack", "worktrees"),
|
|
555
607
|
projectHash: hash,
|
|
556
608
|
projectSlug: slug,
|
|
557
|
-
projectGoal:
|
|
558
|
-
projectInputHistory:
|
|
559
|
-
projectSpecs:
|
|
560
|
-
projectTaskGraphs:
|
|
561
|
-
projectSddSession:
|
|
562
|
-
projectPlan:
|
|
563
|
-
projectAutophase:
|
|
564
|
-
projectSddBoards:
|
|
565
|
-
syncConfig:
|
|
566
|
-
|
|
609
|
+
projectGoal: path3.join(projectDir, "goal.json"),
|
|
610
|
+
projectInputHistory: path3.join(projectDir, "input-history.json"),
|
|
611
|
+
projectSpecs: path3.join(projectDir, "specs"),
|
|
612
|
+
projectTaskGraphs: path3.join(projectDir, "task-graphs"),
|
|
613
|
+
projectSddSession: path3.join(projectDir, "sdd-session.json"),
|
|
614
|
+
projectPlan: path3.join(projectDir, "plan.json"),
|
|
615
|
+
projectAutophase: path3.join(projectDir, "autophase"),
|
|
616
|
+
projectSddBoards: path3.join(projectDir, "sdd-boards"),
|
|
617
|
+
syncConfig: path3.join(globalRoot, "sync.json"),
|
|
618
|
+
configHistoryDir: path3.join(globalRoot, "config-history"),
|
|
619
|
+
projectStatus: (projectHash2) => path3.join(globalRoot, "projects", projectHash2, "status.json")
|
|
567
620
|
};
|
|
568
621
|
}
|
|
569
622
|
|
|
@@ -701,17 +754,17 @@ var FsError = class extends WrongStackError {
|
|
|
701
754
|
|
|
702
755
|
// src/utils/atomic-write.ts
|
|
703
756
|
async function atomicWrite(targetPath, content, opts = {}) {
|
|
704
|
-
const dir =
|
|
705
|
-
await
|
|
706
|
-
const tmp =
|
|
757
|
+
const dir = path4.dirname(targetPath);
|
|
758
|
+
await fs3.mkdir(dir, { recursive: true });
|
|
759
|
+
const tmp = path4.join(dir, `.${path4.basename(targetPath)}.${randomBytes2(6).toString("hex")}.tmp`);
|
|
707
760
|
try {
|
|
708
761
|
if (typeof content === "string") {
|
|
709
|
-
await
|
|
762
|
+
await fs3.writeFile(tmp, content, { flag: "wx", encoding: opts.encoding ?? "utf8" });
|
|
710
763
|
} else {
|
|
711
|
-
await
|
|
764
|
+
await fs3.writeFile(tmp, content, { flag: "wx" });
|
|
712
765
|
}
|
|
713
766
|
try {
|
|
714
|
-
const fh = await
|
|
767
|
+
const fh = await fs3.open(tmp, "r+");
|
|
715
768
|
try {
|
|
716
769
|
await fh.sync();
|
|
717
770
|
} finally {
|
|
@@ -721,63 +774,63 @@ async function atomicWrite(targetPath, content, opts = {}) {
|
|
|
721
774
|
}
|
|
722
775
|
let mode;
|
|
723
776
|
try {
|
|
724
|
-
const stat13 = await
|
|
777
|
+
const stat13 = await fs3.stat(targetPath);
|
|
725
778
|
mode = stat13.mode & 511;
|
|
726
779
|
} catch {
|
|
727
780
|
mode = opts.mode;
|
|
728
781
|
}
|
|
729
782
|
if (mode !== void 0) {
|
|
730
|
-
await
|
|
783
|
+
await fs3.chmod(tmp, mode);
|
|
731
784
|
}
|
|
732
785
|
await renameWithRetry(tmp, targetPath);
|
|
733
786
|
if (mode !== void 0 && process.platform === "win32") {
|
|
734
787
|
try {
|
|
735
|
-
await
|
|
788
|
+
await fs3.chmod(targetPath, mode);
|
|
736
789
|
} catch {
|
|
737
790
|
}
|
|
738
791
|
}
|
|
739
792
|
} catch (err) {
|
|
740
793
|
try {
|
|
741
|
-
await
|
|
794
|
+
await fs3.unlink(tmp);
|
|
742
795
|
} catch {
|
|
743
796
|
}
|
|
744
797
|
throw err;
|
|
745
798
|
}
|
|
746
799
|
}
|
|
747
800
|
async function ensureDir(dir) {
|
|
748
|
-
await
|
|
801
|
+
await fs3.mkdir(dir, { recursive: true });
|
|
749
802
|
}
|
|
750
803
|
async function withFileLock(targetPath, fn, opts = {}) {
|
|
751
|
-
const dir =
|
|
752
|
-
await
|
|
753
|
-
const lockPath =
|
|
804
|
+
const dir = path4.dirname(targetPath);
|
|
805
|
+
await fs3.mkdir(dir, { recursive: true });
|
|
806
|
+
const lockPath = path4.join(dir, `.${path4.basename(targetPath)}.lock`);
|
|
754
807
|
const timeoutMs = opts.timeoutMs ?? 15e3;
|
|
755
808
|
const staleMs = opts.staleMs ?? 3e4;
|
|
756
809
|
const started = Date.now();
|
|
757
810
|
let handle;
|
|
758
811
|
for (; ; ) {
|
|
759
812
|
try {
|
|
760
|
-
handle = await
|
|
813
|
+
handle = await fs3.open(lockPath, "wx");
|
|
761
814
|
await handle.writeFile(`${process.pid}:${Date.now()}`);
|
|
762
815
|
break;
|
|
763
816
|
} catch (err) {
|
|
764
817
|
if (handle) {
|
|
765
818
|
await handle.close().catch(() => {
|
|
766
819
|
});
|
|
767
|
-
await
|
|
820
|
+
await fs3.unlink(lockPath).catch(() => {
|
|
768
821
|
});
|
|
769
822
|
handle = void 0;
|
|
770
823
|
}
|
|
771
824
|
const code = err.code;
|
|
772
825
|
if (code === "ENOENT") {
|
|
773
|
-
await
|
|
826
|
+
await fs3.mkdir(dir, { recursive: true });
|
|
774
827
|
continue;
|
|
775
828
|
}
|
|
776
829
|
if (code !== "EEXIST" && code !== "EPERM") throw err;
|
|
777
830
|
try {
|
|
778
|
-
const stat13 = await
|
|
831
|
+
const stat13 = await fs3.stat(lockPath);
|
|
779
832
|
if (Date.now() - stat13.mtimeMs > staleMs) {
|
|
780
|
-
await
|
|
833
|
+
await fs3.unlink(lockPath);
|
|
781
834
|
continue;
|
|
782
835
|
}
|
|
783
836
|
} catch {
|
|
@@ -803,14 +856,14 @@ async function withFileLock(targetPath, fn, opts = {}) {
|
|
|
803
856
|
} catch {
|
|
804
857
|
}
|
|
805
858
|
try {
|
|
806
|
-
await
|
|
859
|
+
await fs3.unlink(lockPath);
|
|
807
860
|
} catch {
|
|
808
861
|
}
|
|
809
862
|
}
|
|
810
863
|
}
|
|
811
864
|
async function waitForLockRelease(lockPath, remainingMs) {
|
|
812
|
-
const parentDir =
|
|
813
|
-
const lockName =
|
|
865
|
+
const parentDir = path4.dirname(lockPath);
|
|
866
|
+
const lockName = path4.basename(lockPath);
|
|
814
867
|
const intervalMs = Math.min(remainingMs, 100);
|
|
815
868
|
return new Promise((resolve11) => {
|
|
816
869
|
let settled = false;
|
|
@@ -838,7 +891,7 @@ async function waitForLockRelease(lockPath, remainingMs) {
|
|
|
838
891
|
}
|
|
839
892
|
return;
|
|
840
893
|
}
|
|
841
|
-
|
|
894
|
+
fs3.access(lockPath).then(
|
|
842
895
|
() => {
|
|
843
896
|
},
|
|
844
897
|
() => {
|
|
@@ -855,14 +908,14 @@ async function waitForLockRelease(lockPath, remainingMs) {
|
|
|
855
908
|
var TRANSIENT_RENAME_CODES = /* @__PURE__ */ new Set(["EPERM", "EBUSY", "EACCES", "ENOTEMPTY"]);
|
|
856
909
|
async function renameWithRetry(from, to) {
|
|
857
910
|
if (process.platform !== "win32") {
|
|
858
|
-
await
|
|
911
|
+
await fs3.rename(from, to);
|
|
859
912
|
return;
|
|
860
913
|
}
|
|
861
914
|
const delays = [10, 25, 60, 120, 250];
|
|
862
915
|
let lastErr;
|
|
863
916
|
for (let i = 0; i <= delays.length; i++) {
|
|
864
917
|
try {
|
|
865
|
-
await
|
|
918
|
+
await fs3.rename(from, to);
|
|
866
919
|
return;
|
|
867
920
|
} catch (err) {
|
|
868
921
|
lastErr = err;
|
|
@@ -879,7 +932,7 @@ async function renameWithRetry(from, to) {
|
|
|
879
932
|
// src/storage/file-session-writer.ts
|
|
880
933
|
import { closeSync, fsyncSync, openSync, writeSync } from "node:fs";
|
|
881
934
|
import * as fsp from "node:fs/promises";
|
|
882
|
-
import * as
|
|
935
|
+
import * as path5 from "node:path";
|
|
883
936
|
|
|
884
937
|
// src/storage/session-helpers.ts
|
|
885
938
|
function userInputTitle(content) {
|
|
@@ -896,7 +949,7 @@ var FileSessionWriter = class _FileSessionWriter {
|
|
|
896
949
|
this.meta = meta;
|
|
897
950
|
this.events = events;
|
|
898
951
|
this.resumed = opts.resumed ?? false;
|
|
899
|
-
this.manifestFile = opts.dir ?
|
|
952
|
+
this.manifestFile = opts.dir ? path5.join(opts.dir, `${path5.basename(id)}.summary.json`) : "";
|
|
900
953
|
this.filePath = opts.filePath ?? "";
|
|
901
954
|
this.secretScrubber = opts.secretScrubber;
|
|
902
955
|
this.checkpointCas = opts.checkpointCas;
|
|
@@ -1678,7 +1731,7 @@ var FileSessionWriter = class _FileSessionWriter {
|
|
|
1678
1731
|
import { spawn } from "node:child_process";
|
|
1679
1732
|
import { createHash as createHash2, randomUUID } from "node:crypto";
|
|
1680
1733
|
import * as fsp2 from "node:fs/promises";
|
|
1681
|
-
import * as
|
|
1734
|
+
import * as path6 from "node:path";
|
|
1682
1735
|
|
|
1683
1736
|
// src/storage/storage-concurrency.ts
|
|
1684
1737
|
async function mapWithConcurrency(items, concurrency, mapper) {
|
|
@@ -1709,29 +1762,29 @@ function sha256(content) {
|
|
|
1709
1762
|
return createHash2("sha256").update(content).digest("hex");
|
|
1710
1763
|
}
|
|
1711
1764
|
function isInside(root, target) {
|
|
1712
|
-
const
|
|
1713
|
-
return
|
|
1765
|
+
const relative7 = path6.relative(root, target);
|
|
1766
|
+
return relative7 === "" || !relative7.startsWith("..") && !path6.isAbsolute(relative7);
|
|
1714
1767
|
}
|
|
1715
1768
|
function normalizeRelative(input) {
|
|
1716
|
-
if (!input ||
|
|
1769
|
+
if (!input || path6.isAbsolute(input)) return null;
|
|
1717
1770
|
const normalized = input.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
1718
|
-
const resolved =
|
|
1771
|
+
const resolved = path6.posix.normalize(normalized);
|
|
1719
1772
|
if (!resolved || resolved === "." || resolved === ".." || resolved.startsWith("../")) return null;
|
|
1720
1773
|
return resolved;
|
|
1721
1774
|
}
|
|
1722
1775
|
function parseNulPaths(output) {
|
|
1723
1776
|
return output.split("\0").map(normalizeRelative).filter((value) => value !== null);
|
|
1724
1777
|
}
|
|
1725
|
-
function isWrongStackWorktreePath(
|
|
1726
|
-
return
|
|
1778
|
+
function isWrongStackWorktreePath(relative7) {
|
|
1779
|
+
return relative7 === ".wrongstack/worktrees" || relative7.startsWith(".wrongstack/worktrees/");
|
|
1727
1780
|
}
|
|
1728
1781
|
var SessionCheckpointCas = class {
|
|
1729
1782
|
rootDir;
|
|
1730
1783
|
projectRoot;
|
|
1731
1784
|
runGit;
|
|
1732
1785
|
constructor(opts) {
|
|
1733
|
-
this.rootDir =
|
|
1734
|
-
this.projectRoot =
|
|
1786
|
+
this.rootDir = path6.resolve(opts.rootDir);
|
|
1787
|
+
this.projectRoot = path6.resolve(opts.projectRoot);
|
|
1735
1788
|
this.runGit = opts.runGit ?? defaultRunGit;
|
|
1736
1789
|
}
|
|
1737
1790
|
async capture(_sessionId, _promptIndex) {
|
|
@@ -1749,46 +1802,46 @@ var SessionCheckpointCas = class {
|
|
|
1749
1802
|
}
|
|
1750
1803
|
const relativePaths = [
|
|
1751
1804
|
.../* @__PURE__ */ new Set([...parseNulPaths(tracked.stdout), ...parseNulPaths(untracked.stdout)])
|
|
1752
|
-
].filter((
|
|
1805
|
+
].filter((relative7) => !isWrongStackWorktreePath(relative7)).sort();
|
|
1753
1806
|
const unresolved = [];
|
|
1754
1807
|
let scheduledBytes = 0;
|
|
1755
1808
|
const captured = await mapWithConcurrency(
|
|
1756
1809
|
relativePaths,
|
|
1757
1810
|
CAPTURE_CONCURRENCY,
|
|
1758
|
-
async (
|
|
1759
|
-
const absolute =
|
|
1811
|
+
async (relative7) => {
|
|
1812
|
+
const absolute = path6.resolve(this.projectRoot, ...relative7.split("/"));
|
|
1760
1813
|
if (!isInside(this.projectRoot, absolute)) {
|
|
1761
|
-
unresolved.push({ path:
|
|
1814
|
+
unresolved.push({ path: relative7, reason: "path escapes project root" });
|
|
1762
1815
|
return null;
|
|
1763
1816
|
}
|
|
1764
1817
|
try {
|
|
1765
1818
|
const stat13 = await fsp2.lstat(absolute);
|
|
1766
1819
|
if (stat13.isSymbolicLink()) {
|
|
1767
1820
|
const linkTarget = await fsp2.readlink(absolute);
|
|
1768
|
-
const resolvedLink =
|
|
1769
|
-
if (
|
|
1821
|
+
const resolvedLink = path6.resolve(path6.dirname(absolute), linkTarget);
|
|
1822
|
+
if (path6.isAbsolute(linkTarget) || !isInside(this.projectRoot, resolvedLink)) {
|
|
1770
1823
|
unresolved.push({
|
|
1771
|
-
path:
|
|
1824
|
+
path: relative7,
|
|
1772
1825
|
reason: "symlink target escapes project root"
|
|
1773
1826
|
});
|
|
1774
1827
|
return null;
|
|
1775
1828
|
}
|
|
1776
|
-
return { path:
|
|
1829
|
+
return { path: relative7, state: "symlink", linkTarget };
|
|
1777
1830
|
}
|
|
1778
1831
|
if (!stat13.isFile()) {
|
|
1779
|
-
unresolved.push({ path:
|
|
1832
|
+
unresolved.push({ path: relative7, reason: "changed path is not a regular file" });
|
|
1780
1833
|
return null;
|
|
1781
1834
|
}
|
|
1782
1835
|
if (stat13.size > MAX_BLOB_BYTES) {
|
|
1783
1836
|
unresolved.push({
|
|
1784
|
-
path:
|
|
1837
|
+
path: relative7,
|
|
1785
1838
|
reason: `file exceeds ${MAX_BLOB_BYTES}-byte checkpoint blob limit`
|
|
1786
1839
|
});
|
|
1787
1840
|
return null;
|
|
1788
1841
|
}
|
|
1789
1842
|
if (scheduledBytes + stat13.size > MAX_CHECKPOINT_BYTES) {
|
|
1790
1843
|
unresolved.push({
|
|
1791
|
-
path:
|
|
1844
|
+
path: relative7,
|
|
1792
1845
|
reason: `checkpoint exceeds ${MAX_CHECKPOINT_BYTES}-byte aggregate blob limit`
|
|
1793
1846
|
});
|
|
1794
1847
|
return null;
|
|
@@ -1797,12 +1850,12 @@ var SessionCheckpointCas = class {
|
|
|
1797
1850
|
const content = await fsp2.readFile(absolute);
|
|
1798
1851
|
const blobHash = sha256(content);
|
|
1799
1852
|
await this.putBlob(blobHash, content);
|
|
1800
|
-
return { path:
|
|
1853
|
+
return { path: relative7, state: "file", blobHash, mode: stat13.mode & 511 };
|
|
1801
1854
|
} catch (err) {
|
|
1802
1855
|
if (err.code === "ENOENT") {
|
|
1803
|
-
return { path:
|
|
1856
|
+
return { path: relative7, state: "absent" };
|
|
1804
1857
|
}
|
|
1805
|
-
unresolved.push({ path:
|
|
1858
|
+
unresolved.push({ path: relative7, reason: toErrorMessage(err) });
|
|
1806
1859
|
return null;
|
|
1807
1860
|
}
|
|
1808
1861
|
}
|
|
@@ -1828,7 +1881,7 @@ var SessionCheckpointCas = class {
|
|
|
1828
1881
|
};
|
|
1829
1882
|
}
|
|
1830
1883
|
async materialize(checkpoint, targetRoot) {
|
|
1831
|
-
const target =
|
|
1884
|
+
const target = path6.resolve(targetRoot);
|
|
1832
1885
|
if (target === this.projectRoot) {
|
|
1833
1886
|
throw new Error("Refusing to materialize a workspace checkpoint over the parent project root");
|
|
1834
1887
|
}
|
|
@@ -1868,10 +1921,10 @@ var SessionCheckpointCas = class {
|
|
|
1868
1921
|
try {
|
|
1869
1922
|
const output = await this.safeOutputPath(target, realTarget, entry.path);
|
|
1870
1923
|
if (entry.state === "symlink") {
|
|
1871
|
-
if (
|
|
1924
|
+
if (path6.isAbsolute(entry.linkTarget)) {
|
|
1872
1925
|
throw new Error("absolute symlink target refused");
|
|
1873
1926
|
}
|
|
1874
|
-
const resolvedLink =
|
|
1927
|
+
const resolvedLink = path6.resolve(path6.dirname(output), entry.linkTarget);
|
|
1875
1928
|
if (!isInside(target, resolvedLink)) throw new Error("symlink target escapes checkpoint root");
|
|
1876
1929
|
}
|
|
1877
1930
|
prepared.push({
|
|
@@ -1899,7 +1952,7 @@ var SessionCheckpointCas = class {
|
|
|
1899
1952
|
await fsp2.unlink(output).catch((err) => {
|
|
1900
1953
|
if (err.code !== "ENOENT") throw err;
|
|
1901
1954
|
});
|
|
1902
|
-
await fsp2.mkdir(
|
|
1955
|
+
await fsp2.mkdir(path6.dirname(output), { recursive: true });
|
|
1903
1956
|
await fsp2.symlink(entry.linkTarget, output);
|
|
1904
1957
|
writtenFiles.push(entry.path);
|
|
1905
1958
|
continue;
|
|
@@ -1916,15 +1969,15 @@ var SessionCheckpointCas = class {
|
|
|
1916
1969
|
}
|
|
1917
1970
|
objectPath(hash) {
|
|
1918
1971
|
if (!HASH_RE.test(hash)) throw new Error(`Invalid CAS object hash: ${hash}`);
|
|
1919
|
-
return
|
|
1972
|
+
return path6.join(this.rootDir, "objects", hash.slice(0, 2), hash.slice(2));
|
|
1920
1973
|
}
|
|
1921
1974
|
manifestPath(hash) {
|
|
1922
1975
|
if (!HASH_RE.test(hash)) throw new Error(`Invalid checkpoint manifest hash: ${hash}`);
|
|
1923
|
-
return
|
|
1976
|
+
return path6.join(this.rootDir, "manifests", `${hash}.json`);
|
|
1924
1977
|
}
|
|
1925
1978
|
async putBlob(hash, content) {
|
|
1926
1979
|
const target = this.objectPath(hash);
|
|
1927
|
-
await fsp2.mkdir(
|
|
1980
|
+
await fsp2.mkdir(path6.dirname(target), { recursive: true });
|
|
1928
1981
|
try {
|
|
1929
1982
|
const existing = await fsp2.readFile(target);
|
|
1930
1983
|
if (sha256(existing) !== hash) throw new Error(`Corrupt CAS object collision: ${hash}`);
|
|
@@ -1932,9 +1985,9 @@ var SessionCheckpointCas = class {
|
|
|
1932
1985
|
} catch (err) {
|
|
1933
1986
|
if (err.code !== "ENOENT") throw err;
|
|
1934
1987
|
}
|
|
1935
|
-
const temp =
|
|
1936
|
-
|
|
1937
|
-
`.${
|
|
1988
|
+
const temp = path6.join(
|
|
1989
|
+
path6.dirname(target),
|
|
1990
|
+
`.${path6.basename(target)}.${process.pid}.${randomUUID()}.tmp`
|
|
1938
1991
|
);
|
|
1939
1992
|
let handle;
|
|
1940
1993
|
try {
|
|
@@ -1992,10 +2045,10 @@ var SessionCheckpointCas = class {
|
|
|
1992
2045
|
}
|
|
1993
2046
|
return parsed;
|
|
1994
2047
|
}
|
|
1995
|
-
async safeOutputPath(target, realTarget,
|
|
1996
|
-
const normalized = normalizeRelative(
|
|
2048
|
+
async safeOutputPath(target, realTarget, relative7) {
|
|
2049
|
+
const normalized = normalizeRelative(relative7);
|
|
1997
2050
|
if (!normalized) throw new Error("invalid relative path");
|
|
1998
|
-
const output =
|
|
2051
|
+
const output = path6.resolve(target, ...normalized.split("/"));
|
|
1999
2052
|
if (!isInside(target, output)) throw new Error("path escapes checkpoint target");
|
|
2000
2053
|
let probe = output;
|
|
2001
2054
|
for (; ; ) {
|
|
@@ -2005,7 +2058,7 @@ var SessionCheckpointCas = class {
|
|
|
2005
2058
|
return output;
|
|
2006
2059
|
} catch (err) {
|
|
2007
2060
|
if (err.code !== "ENOENT") throw err;
|
|
2008
|
-
const parent =
|
|
2061
|
+
const parent = path6.dirname(probe);
|
|
2009
2062
|
if (parent === probe) throw err;
|
|
2010
2063
|
probe = parent;
|
|
2011
2064
|
}
|
|
@@ -2074,13 +2127,13 @@ function generateSessionId(startedAt, _model) {
|
|
|
2074
2127
|
// src/storage/session-resume-validation.ts
|
|
2075
2128
|
import { createHash as createHash3 } from "node:crypto";
|
|
2076
2129
|
import * as fsp3 from "node:fs/promises";
|
|
2077
|
-
import * as
|
|
2130
|
+
import * as path7 from "node:path";
|
|
2078
2131
|
var MAX_REVALIDATE_BYTES = 5 * 1024 * 1024;
|
|
2079
2132
|
var VALIDATION_CONCURRENCY = 8;
|
|
2080
2133
|
var NOTICE_PATH_LIMIT = 20;
|
|
2081
2134
|
function isInside2(root, target) {
|
|
2082
|
-
const
|
|
2083
|
-
return
|
|
2135
|
+
const relative7 = path7.relative(root, target);
|
|
2136
|
+
return relative7 === "" || !relative7.startsWith("..") && !path7.isAbsolute(relative7);
|
|
2084
2137
|
}
|
|
2085
2138
|
function errno(err) {
|
|
2086
2139
|
return err && typeof err === "object" && "code" in err ? String(err.code) : void 0;
|
|
@@ -2091,7 +2144,7 @@ function latestObservations(events, projectRoot) {
|
|
|
2091
2144
|
if (event.type !== "file_observation" || typeof event.path !== "string" || event.path.length === 0 || typeof event.hash !== "string" || !/^[a-f\d]{64}$/i.test(event.hash)) {
|
|
2092
2145
|
continue;
|
|
2093
2146
|
}
|
|
2094
|
-
const normalized =
|
|
2147
|
+
const normalized = path7.resolve(projectRoot, event.path);
|
|
2095
2148
|
latest.set(normalized, {
|
|
2096
2149
|
path: normalized,
|
|
2097
2150
|
hash: event.hash.toLowerCase(),
|
|
@@ -2149,7 +2202,7 @@ async function validateOne(observation, lexicalRoot, realRoot) {
|
|
|
2149
2202
|
}
|
|
2150
2203
|
}
|
|
2151
2204
|
async function validateResumeFileObservations(events, projectRoot) {
|
|
2152
|
-
const lexicalRoot =
|
|
2205
|
+
const lexicalRoot = path7.resolve(projectRoot);
|
|
2153
2206
|
const realRoot = await fsp3.realpath(lexicalRoot).catch(() => lexicalRoot);
|
|
2154
2207
|
const observations = latestObservations(events, lexicalRoot);
|
|
2155
2208
|
const results = await mapWithConcurrency(
|
|
@@ -2165,10 +2218,10 @@ async function validateResumeFileObservations(events, projectRoot) {
|
|
|
2165
2218
|
}
|
|
2166
2219
|
function formatResumeValidationNotice(validation, projectRoot) {
|
|
2167
2220
|
if (validation.staleFiles.length === 0) return null;
|
|
2168
|
-
const root =
|
|
2221
|
+
const root = path7.resolve(projectRoot);
|
|
2169
2222
|
const shown = validation.staleFiles.slice(0, NOTICE_PATH_LIMIT).map((entry) => {
|
|
2170
|
-
const
|
|
2171
|
-
const display = isInside2(root, entry.path) ?
|
|
2223
|
+
const relative7 = path7.relative(root, entry.path);
|
|
2224
|
+
const display = isInside2(root, entry.path) ? relative7 || "." : entry.path;
|
|
2172
2225
|
return `- ${JSON.stringify(display)} [${entry.status}]`;
|
|
2173
2226
|
});
|
|
2174
2227
|
const omitted = validation.staleFiles.length - shown.length;
|
|
@@ -2301,9 +2354,9 @@ var DefaultSessionStore = class _DefaultSessionStore {
|
|
|
2301
2354
|
static LIST_SCAN_CONCURRENCY = 32;
|
|
2302
2355
|
constructor(opts) {
|
|
2303
2356
|
this.dir = opts.dir;
|
|
2304
|
-
this.projectRoot = opts.projectRoot ?
|
|
2357
|
+
this.projectRoot = opts.projectRoot ? path8.resolve(opts.projectRoot) : void 0;
|
|
2305
2358
|
this.checkpointCas = this.projectRoot ? new SessionCheckpointCas({
|
|
2306
|
-
rootDir:
|
|
2359
|
+
rootDir: path8.join(this.dir, "_cas"),
|
|
2307
2360
|
projectRoot: this.projectRoot
|
|
2308
2361
|
}) : void 0;
|
|
2309
2362
|
this.events = opts.events;
|
|
@@ -2369,17 +2422,17 @@ var DefaultSessionStore = class _DefaultSessionStore {
|
|
|
2369
2422
|
}
|
|
2370
2423
|
/** Absolute path to the session index file. */
|
|
2371
2424
|
get indexFile() {
|
|
2372
|
-
return
|
|
2425
|
+
return path8.join(this.dir, "_index.jsonl");
|
|
2373
2426
|
}
|
|
2374
2427
|
/** Join session ID to its absolute path within the store directory. */
|
|
2375
2428
|
sessionPath(id, ext) {
|
|
2376
2429
|
return sessionScopedPath(this.dir, id, ext);
|
|
2377
2430
|
}
|
|
2378
2431
|
shardManifestPath(shardKey) {
|
|
2379
|
-
return shardKey ?
|
|
2432
|
+
return shardKey ? path8.join(this.dir, shardKey, "_manifest.json") : path8.join(this.dir, "_manifest.json");
|
|
2380
2433
|
}
|
|
2381
2434
|
shardKeyForSessionId(id) {
|
|
2382
|
-
const dirName =
|
|
2435
|
+
const dirName = path8.dirname(id);
|
|
2383
2436
|
return dirName === "." ? "" : dirName;
|
|
2384
2437
|
}
|
|
2385
2438
|
invalidateShardManifestBySessionId(id) {
|
|
@@ -2391,7 +2444,7 @@ var DefaultSessionStore = class _DefaultSessionStore {
|
|
|
2391
2444
|
* subdirectory so sessions group naturally by day.
|
|
2392
2445
|
*/
|
|
2393
2446
|
async ensureShardDir(id) {
|
|
2394
|
-
const dirPath =
|
|
2447
|
+
const dirPath = path8.dirname(sessionScopedPath(this.dir, id, ""));
|
|
2395
2448
|
await ensureDir(dirPath);
|
|
2396
2449
|
return dirPath;
|
|
2397
2450
|
}
|
|
@@ -2555,7 +2608,7 @@ var DefaultSessionStore = class _DefaultSessionStore {
|
|
|
2555
2608
|
// Shard directory (sessions/<date>/) — must match create() so the
|
|
2556
2609
|
// .summary.json sidecar lands next to the JSONL instead of the
|
|
2557
2610
|
// sessions root (where summaryFor() would never find it).
|
|
2558
|
-
dir:
|
|
2611
|
+
dir: path8.dirname(file),
|
|
2559
2612
|
filePath: file,
|
|
2560
2613
|
secretScrubber: this.secretScrubber,
|
|
2561
2614
|
checkpointCas: this.checkpointCas,
|
|
@@ -3176,7 +3229,7 @@ var DefaultSessionStore = class _DefaultSessionStore {
|
|
|
3176
3229
|
return entry;
|
|
3177
3230
|
}
|
|
3178
3231
|
async collectSessionFilesInShard(shardKey) {
|
|
3179
|
-
const dir = shardKey ?
|
|
3232
|
+
const dir = shardKey ? path8.join(this.dir, shardKey) : this.dir;
|
|
3180
3233
|
const entries = await this.collectSessionFiles(dir, shardKey);
|
|
3181
3234
|
return shardKey ? entries.filter((entry) => entry.id.startsWith(`${shardKey}/`)) : entries.filter((entry) => !entry.id.includes("/"));
|
|
3182
3235
|
}
|
|
@@ -3199,13 +3252,13 @@ var DefaultSessionStore = class _DefaultSessionStore {
|
|
|
3199
3252
|
if (entry.name === "_index.jsonl") continue;
|
|
3200
3253
|
const base = entry.name.replace(/\.jsonl$/, "");
|
|
3201
3254
|
const id = prefix ? `${prefix}/${base}` : base;
|
|
3202
|
-
files.push({ id, filePath:
|
|
3255
|
+
files.push({ id, filePath: path8.join(dir, entry.name) });
|
|
3203
3256
|
}
|
|
3204
3257
|
}
|
|
3205
3258
|
const childFileArrays = await Promise.all(
|
|
3206
3259
|
dirEntries.map((entry) => {
|
|
3207
3260
|
const childPrefix = depth === 0 ? entry.name : `${prefix}/${entry.name}`;
|
|
3208
|
-
return this.collectSessionFiles(
|
|
3261
|
+
return this.collectSessionFiles(path8.join(dir, entry.name), childPrefix, depth + 1);
|
|
3209
3262
|
})
|
|
3210
3263
|
);
|
|
3211
3264
|
return [...childFileArrays.flat(), ...files];
|
|
@@ -3238,7 +3291,7 @@ var DefaultSessionStore = class _DefaultSessionStore {
|
|
|
3238
3291
|
const childIdArrays = await Promise.all(
|
|
3239
3292
|
dirEntries.map((entry) => {
|
|
3240
3293
|
const childPrefix = depth === 0 ? entry.name : `${prefix}/${entry.name}`;
|
|
3241
|
-
return this.collectSessionIds(
|
|
3294
|
+
return this.collectSessionIds(path8.join(dir, entry.name), childPrefix, depth + 1);
|
|
3242
3295
|
})
|
|
3243
3296
|
);
|
|
3244
3297
|
return [...childIdArrays.flat(), ...fileIds];
|
|
@@ -3353,9 +3406,9 @@ var DefaultSessionStore = class _DefaultSessionStore {
|
|
|
3353
3406
|
async deleteSession(id) {
|
|
3354
3407
|
const jsonlPath = this.sessionPath(id, ".jsonl");
|
|
3355
3408
|
const summaryPath = this.sessionPath(id, ".summary.json");
|
|
3356
|
-
const shardDir =
|
|
3357
|
-
const base =
|
|
3358
|
-
const sessDir =
|
|
3409
|
+
const shardDir = path8.dirname(jsonlPath);
|
|
3410
|
+
const base = path8.basename(id);
|
|
3411
|
+
const sessDir = path8.join(shardDir, base);
|
|
3359
3412
|
const deletions = [
|
|
3360
3413
|
fsp4.unlink(jsonlPath),
|
|
3361
3414
|
fsp4.unlink(summaryPath),
|
|
@@ -3400,7 +3453,7 @@ var DefaultSessionStore = class _DefaultSessionStore {
|
|
|
3400
3453
|
*/
|
|
3401
3454
|
async readActiveSessionId() {
|
|
3402
3455
|
try {
|
|
3403
|
-
const raw = await fsp4.readFile(
|
|
3456
|
+
const raw = await fsp4.readFile(path8.join(this.dir, "active.json"), "utf8");
|
|
3404
3457
|
const active = JSON.parse(raw);
|
|
3405
3458
|
return active.sessionId ?? null;
|
|
3406
3459
|
} catch {
|
|
@@ -3463,7 +3516,7 @@ var DefaultSessionStore = class _DefaultSessionStore {
|
|
|
3463
3516
|
const activeSessionId = await this.readActiveSessionId();
|
|
3464
3517
|
const isPrunableJsonl = (name) => name.endsWith(".jsonl") && name !== "_index.jsonl" && name !== "_mailbox.jsonl" && !name.endsWith(".replay.jsonl") && !name.endsWith(".audit.jsonl");
|
|
3465
3518
|
const pruneFile = async (dir, name, prefix) => {
|
|
3466
|
-
const jsonlPath =
|
|
3519
|
+
const jsonlPath = path8.join(dir, name);
|
|
3467
3520
|
try {
|
|
3468
3521
|
const stat13 = await fsp4.stat(jsonlPath);
|
|
3469
3522
|
if (stat13.mtimeMs >= cutoff) return;
|
|
@@ -3483,7 +3536,7 @@ var DefaultSessionStore = class _DefaultSessionStore {
|
|
|
3483
3536
|
continue;
|
|
3484
3537
|
}
|
|
3485
3538
|
if (!entry.isDirectory()) continue;
|
|
3486
|
-
const dateDir =
|
|
3539
|
+
const dateDir = path8.join(this.dir, entry.name);
|
|
3487
3540
|
const files = await fsp4.readdir(dateDir, { withFileTypes: true }).catch(() => []);
|
|
3488
3541
|
for (const file of files) {
|
|
3489
3542
|
if (!file.isFile() || !isPrunableJsonl(file.name)) continue;
|
|
@@ -3495,7 +3548,7 @@ var DefaultSessionStore = class _DefaultSessionStore {
|
|
|
3495
3548
|
}
|
|
3496
3549
|
for (const entry of entries) {
|
|
3497
3550
|
if (!entry.isDirectory()) continue;
|
|
3498
|
-
const dateDir =
|
|
3551
|
+
const dateDir = path8.join(this.dir, entry.name);
|
|
3499
3552
|
try {
|
|
3500
3553
|
const remaining = await fsp4.readdir(dateDir);
|
|
3501
3554
|
if (remaining.length === 0) {
|
|
@@ -3620,7 +3673,7 @@ var DefaultSessionStore = class _DefaultSessionStore {
|
|
|
3620
3673
|
|
|
3621
3674
|
// src/storage/queue-store.ts
|
|
3622
3675
|
import * as fsp5 from "node:fs/promises";
|
|
3623
|
-
import * as
|
|
3676
|
+
import * as path9 from "node:path";
|
|
3624
3677
|
var QueueStore = class {
|
|
3625
3678
|
file;
|
|
3626
3679
|
// Use `| undefined` (not `?`) so exactOptionalPropertyTypes doesn't
|
|
@@ -3629,7 +3682,7 @@ var QueueStore = class {
|
|
|
3629
3682
|
traceId;
|
|
3630
3683
|
logger;
|
|
3631
3684
|
constructor(opts) {
|
|
3632
|
-
this.file =
|
|
3685
|
+
this.file = path9.join(opts.dir, "queue.json");
|
|
3633
3686
|
this.events = opts.events;
|
|
3634
3687
|
this.traceId = opts.traceId;
|
|
3635
3688
|
this.logger = opts.logger;
|
|
@@ -3787,7 +3840,7 @@ function isPersistedQueueItem(v) {
|
|
|
3787
3840
|
// src/storage/attachment-store.ts
|
|
3788
3841
|
import { randomBytes as randomBytes3 } from "node:crypto";
|
|
3789
3842
|
import * as fsp6 from "node:fs/promises";
|
|
3790
|
-
import * as
|
|
3843
|
+
import * as path10 from "node:path";
|
|
3791
3844
|
var DEFAULT_SPOOL_THRESHOLD = 256 * 1024;
|
|
3792
3845
|
var PLACEHOLDER_RE = /\[(pasted|image|file) #(\d+)[^\]]*\]|\[file:([^\]]+)\]/g;
|
|
3793
3846
|
var DefaultAttachmentStore = class {
|
|
@@ -3808,7 +3861,7 @@ var DefaultAttachmentStore = class {
|
|
|
3808
3861
|
let data = input.data;
|
|
3809
3862
|
if (this.spoolDir && bytes >= this.spoolThreshold) {
|
|
3810
3863
|
await fsp6.mkdir(this.spoolDir, { recursive: true });
|
|
3811
|
-
spooledPath =
|
|
3864
|
+
spooledPath = path10.join(this.spoolDir, `${id}.bin`);
|
|
3812
3865
|
await atomicWrite(spooledPath, input.data, {
|
|
3813
3866
|
encoding: input.kind === "image" ? "base64" : "utf8"
|
|
3814
3867
|
});
|
|
@@ -3928,8 +3981,8 @@ function mergeAdjacentText(blocks) {
|
|
|
3928
3981
|
|
|
3929
3982
|
// src/storage/memory-backend.ts
|
|
3930
3983
|
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
3931
|
-
import * as
|
|
3932
|
-
import * as
|
|
3984
|
+
import * as fs4 from "node:fs/promises";
|
|
3985
|
+
import * as path11 from "node:path";
|
|
3933
3986
|
|
|
3934
3987
|
// src/types/memory.ts
|
|
3935
3988
|
var MEMORY_TYPE_LABELS = {
|
|
@@ -4096,7 +4149,7 @@ var FileMemoryBackend = class {
|
|
|
4096
4149
|
}
|
|
4097
4150
|
async getMtime(file) {
|
|
4098
4151
|
try {
|
|
4099
|
-
const stat13 = await
|
|
4152
|
+
const stat13 = await fs4.stat(file);
|
|
4100
4153
|
return stat13.mtimeMs;
|
|
4101
4154
|
} catch {
|
|
4102
4155
|
return 0;
|
|
@@ -4143,10 +4196,10 @@ var FileMemoryBackend = class {
|
|
|
4143
4196
|
}
|
|
4144
4197
|
async remember(scope, entry, filePath) {
|
|
4145
4198
|
const file = this.resolveFile(filePath, scope);
|
|
4146
|
-
await ensureDir(
|
|
4199
|
+
await ensureDir(path11.dirname(file));
|
|
4147
4200
|
let existing = "";
|
|
4148
4201
|
try {
|
|
4149
|
-
existing = await
|
|
4202
|
+
existing = await fs4.readFile(file, "utf8");
|
|
4150
4203
|
} catch {
|
|
4151
4204
|
}
|
|
4152
4205
|
const id = `mem_${Date.now()}_${randomUUID2().slice(0, 8)}`;
|
|
@@ -4164,7 +4217,7 @@ ${line}`;
|
|
|
4164
4217
|
return withFileLock(file, async () => {
|
|
4165
4218
|
let existing;
|
|
4166
4219
|
try {
|
|
4167
|
-
existing = await
|
|
4220
|
+
existing = await fs4.readFile(file, "utf8");
|
|
4168
4221
|
} catch {
|
|
4169
4222
|
return 0;
|
|
4170
4223
|
}
|
|
@@ -4201,7 +4254,7 @@ ${line}`;
|
|
|
4201
4254
|
async readAll(scope, filePath) {
|
|
4202
4255
|
const file = this.resolveFile(filePath, scope);
|
|
4203
4256
|
try {
|
|
4204
|
-
return await
|
|
4257
|
+
return await fs4.readFile(file, "utf8");
|
|
4205
4258
|
} catch {
|
|
4206
4259
|
return "";
|
|
4207
4260
|
}
|
|
@@ -4225,7 +4278,7 @@ ${line}`;
|
|
|
4225
4278
|
const file = this.resolveFile(filePath, scope);
|
|
4226
4279
|
let existing;
|
|
4227
4280
|
try {
|
|
4228
|
-
existing = await
|
|
4281
|
+
existing = await fs4.readFile(file, "utf8");
|
|
4229
4282
|
} catch {
|
|
4230
4283
|
return 0;
|
|
4231
4284
|
}
|
|
@@ -4245,7 +4298,7 @@ ${line}`;
|
|
|
4245
4298
|
const next = lines.join("\n");
|
|
4246
4299
|
const backup = `${file}.bak.${Date.now()}`;
|
|
4247
4300
|
try {
|
|
4248
|
-
await
|
|
4301
|
+
await fs4.copyFile(file, backup);
|
|
4249
4302
|
await pruneConsolidateBackups(file);
|
|
4250
4303
|
} catch {
|
|
4251
4304
|
}
|
|
@@ -4259,14 +4312,14 @@ ${line}`;
|
|
|
4259
4312
|
}
|
|
4260
4313
|
};
|
|
4261
4314
|
async function pruneConsolidateBackups(file) {
|
|
4262
|
-
const dir =
|
|
4263
|
-
const base =
|
|
4315
|
+
const dir = path11.dirname(file);
|
|
4316
|
+
const base = path11.basename(file);
|
|
4264
4317
|
const prefix = `${base}.bak.`;
|
|
4265
|
-
const backups = (await
|
|
4318
|
+
const backups = (await fs4.readdir(dir)).filter((name) => name.startsWith(prefix)).sort().reverse();
|
|
4266
4319
|
await Promise.all(
|
|
4267
4320
|
backups.slice(MAX_MEMORY_CONSOLIDATE_BACKUPS).map(async (name) => {
|
|
4268
4321
|
try {
|
|
4269
|
-
await
|
|
4322
|
+
await fs4.unlink(path11.join(dir, name));
|
|
4270
4323
|
} catch {
|
|
4271
4324
|
}
|
|
4272
4325
|
})
|
|
@@ -4282,7 +4335,7 @@ function parseEntries(raw, scope = "project-memory") {
|
|
|
4282
4335
|
}
|
|
4283
4336
|
|
|
4284
4337
|
// src/storage/memory-graph-backend.ts
|
|
4285
|
-
import * as
|
|
4338
|
+
import * as fs5 from "node:fs/promises";
|
|
4286
4339
|
var GraphMemoryBackend = class _GraphMemoryBackend {
|
|
4287
4340
|
kind = "graph";
|
|
4288
4341
|
file;
|
|
@@ -4450,7 +4503,7 @@ var GraphMemoryBackend = class _GraphMemoryBackend {
|
|
|
4450
4503
|
this.loadedScope = scope;
|
|
4451
4504
|
this.loaded = true;
|
|
4452
4505
|
try {
|
|
4453
|
-
await
|
|
4506
|
+
await fs5.unlink(this.graphFile);
|
|
4454
4507
|
} catch {
|
|
4455
4508
|
}
|
|
4456
4509
|
}
|
|
@@ -4490,7 +4543,7 @@ var GraphMemoryBackend = class _GraphMemoryBackend {
|
|
|
4490
4543
|
async loadGraph(scope) {
|
|
4491
4544
|
if (this.loaded && this.loadedScope === scope) return;
|
|
4492
4545
|
try {
|
|
4493
|
-
const raw = await
|
|
4546
|
+
const raw = await fs5.readFile(this.graphFile, "utf8");
|
|
4494
4547
|
const data = JSON.parse(raw);
|
|
4495
4548
|
this.nodes = new Map(data.nodes);
|
|
4496
4549
|
this.edges = data.edges;
|
|
@@ -4512,10 +4565,10 @@ var GraphMemoryBackend = class _GraphMemoryBackend {
|
|
|
4512
4565
|
edges: this.edges
|
|
4513
4566
|
};
|
|
4514
4567
|
const dir = this.graphFile.substring(0, this.graphFile.lastIndexOf("/"));
|
|
4515
|
-
await
|
|
4568
|
+
await fs5.mkdir(dir, { recursive: true });
|
|
4516
4569
|
const tmp = `${this.graphFile}.tmp`;
|
|
4517
|
-
await
|
|
4518
|
-
await
|
|
4570
|
+
await fs5.writeFile(tmp, JSON.stringify(data));
|
|
4571
|
+
await fs5.rename(tmp, this.graphFile);
|
|
4519
4572
|
} catch {
|
|
4520
4573
|
}
|
|
4521
4574
|
}
|
|
@@ -4634,7 +4687,7 @@ function simpleHash(s) {
|
|
|
4634
4687
|
|
|
4635
4688
|
// src/utils/instruction-file.ts
|
|
4636
4689
|
import { readFileSync as readFileSync2, statSync as statSync2 } from "node:fs";
|
|
4637
|
-
import * as
|
|
4690
|
+
import * as path12 from "node:path";
|
|
4638
4691
|
import { fileURLToPath } from "node:url";
|
|
4639
4692
|
var textCache = /* @__PURE__ */ new Map();
|
|
4640
4693
|
var rootCandidates;
|
|
@@ -4644,7 +4697,7 @@ function readBundledInstructionText(relativePath) {
|
|
|
4644
4697
|
let resolved = "";
|
|
4645
4698
|
for (const root of instructionRootCandidates()) {
|
|
4646
4699
|
try {
|
|
4647
|
-
resolved = readFileSync2(
|
|
4700
|
+
resolved = readFileSync2(path12.join(root, relativePath), "utf8").trimEnd();
|
|
4648
4701
|
break;
|
|
4649
4702
|
} catch {
|
|
4650
4703
|
}
|
|
@@ -4660,11 +4713,11 @@ function renderInstructionTemplate(template, values) {
|
|
|
4660
4713
|
}
|
|
4661
4714
|
function instructionRootCandidates() {
|
|
4662
4715
|
if (rootCandidates !== void 0) return rootCandidates;
|
|
4663
|
-
const here =
|
|
4716
|
+
const here = path12.dirname(fileURLToPath(import.meta.url));
|
|
4664
4717
|
const candidates = [
|
|
4665
|
-
|
|
4666
|
-
|
|
4667
|
-
|
|
4718
|
+
path12.resolve(here, "../../instructions"),
|
|
4719
|
+
path12.resolve(here, "../instructions"),
|
|
4720
|
+
path12.resolve(here, "instructions")
|
|
4668
4721
|
];
|
|
4669
4722
|
rootCandidates = candidates.sort((a, b) => Number(!isDirectory(a)) - Number(!isDirectory(b)));
|
|
4670
4723
|
return rootCandidates;
|
|
@@ -4912,8 +4965,8 @@ function deepFreeze(obj) {
|
|
|
4912
4965
|
|
|
4913
4966
|
// src/storage/provider-config-watcher.ts
|
|
4914
4967
|
import * as syncFs from "node:fs";
|
|
4915
|
-
import * as
|
|
4916
|
-
import * as
|
|
4968
|
+
import * as fs6 from "node:fs/promises";
|
|
4969
|
+
import * as path13 from "node:path";
|
|
4917
4970
|
|
|
4918
4971
|
// src/security/config-secrets.ts
|
|
4919
4972
|
function decryptConfigSecrets(cfg, vault, opts) {
|
|
@@ -4959,7 +5012,7 @@ function isSecretField(name) {
|
|
|
4959
5012
|
async function readProviderSnapshot(configPath, vault, warn) {
|
|
4960
5013
|
let raw;
|
|
4961
5014
|
try {
|
|
4962
|
-
raw = await
|
|
5015
|
+
raw = await fs6.readFile(configPath, "utf8");
|
|
4963
5016
|
} catch (err) {
|
|
4964
5017
|
if (err.code !== "ENOENT") {
|
|
4965
5018
|
warn?.(`Could not read ${configPath}: ${err.message}`);
|
|
@@ -4975,7 +5028,8 @@ async function readProviderSnapshot(configPath, vault, warn) {
|
|
|
4975
5028
|
}
|
|
4976
5029
|
const decrypted = decryptConfigSecrets(parsed, vault, warn ? { warn } : {});
|
|
4977
5030
|
const snapshot = {
|
|
4978
|
-
providers: decrypted.providers ?? {}
|
|
5031
|
+
providers: decrypted.providers ?? {},
|
|
5032
|
+
snapshotHasProviders: decrypted.providers !== void 0
|
|
4979
5033
|
};
|
|
4980
5034
|
if (typeof decrypted.apiKey === "string") snapshot.apiKey = decrypted.apiKey;
|
|
4981
5035
|
if (typeof decrypted.baseUrl === "string") snapshot.baseUrl = decrypted.baseUrl;
|
|
@@ -5010,7 +5064,7 @@ function serializeSnapshot(s) {
|
|
|
5010
5064
|
function watchProviderConfig(configPath, vault, onChange, opts = {}) {
|
|
5011
5065
|
const debounceMs = opts.debounceMs ?? 200;
|
|
5012
5066
|
const warn = opts.warn;
|
|
5013
|
-
const base =
|
|
5067
|
+
const base = path13.basename(configPath);
|
|
5014
5068
|
let timer;
|
|
5015
5069
|
let closed = false;
|
|
5016
5070
|
let lastSerialized;
|
|
@@ -5021,7 +5075,7 @@ function watchProviderConfig(configPath, vault, onChange, opts = {}) {
|
|
|
5021
5075
|
});
|
|
5022
5076
|
let watcher;
|
|
5023
5077
|
try {
|
|
5024
|
-
watcher = syncFs.watch(
|
|
5078
|
+
watcher = syncFs.watch(path13.dirname(configPath), { recursive: false });
|
|
5025
5079
|
} catch (err) {
|
|
5026
5080
|
warn?.(`Provider config watcher could not start: ${err.message}`);
|
|
5027
5081
|
return { close: () => {
|
|
@@ -5064,8 +5118,8 @@ function watchProviderConfig(configPath, vault, onChange, opts = {}) {
|
|
|
5064
5118
|
}
|
|
5065
5119
|
|
|
5066
5120
|
// src/storage/config-loader.ts
|
|
5067
|
-
import * as
|
|
5068
|
-
import * as
|
|
5121
|
+
import * as fs7 from "node:fs/promises";
|
|
5122
|
+
import * as path14 from "node:path";
|
|
5069
5123
|
|
|
5070
5124
|
// src/types/config.ts
|
|
5071
5125
|
var DEFAULT_TUI_THINKING_WORD = "thinking";
|
|
@@ -5589,8 +5643,8 @@ function stripUnsafeInProjectFields(inProject, sourcePath, warn = (msg) => conso
|
|
|
5589
5643
|
return out;
|
|
5590
5644
|
}
|
|
5591
5645
|
function samePath(a, b) {
|
|
5592
|
-
let ra =
|
|
5593
|
-
let rb =
|
|
5646
|
+
let ra = path14.resolve(a);
|
|
5647
|
+
let rb = path14.resolve(b);
|
|
5594
5648
|
if (process.platform === "win32" || process.platform === "darwin") {
|
|
5595
5649
|
ra = ra.toLowerCase();
|
|
5596
5650
|
rb = rb.toLowerCase();
|
|
@@ -5612,7 +5666,7 @@ function deepMerge2(base, patch) {
|
|
|
5612
5666
|
opts
|
|
5613
5667
|
);
|
|
5614
5668
|
}
|
|
5615
|
-
var DefaultConfigLoader = class {
|
|
5669
|
+
var DefaultConfigLoader = class _DefaultConfigLoader {
|
|
5616
5670
|
paths;
|
|
5617
5671
|
strict;
|
|
5618
5672
|
vault;
|
|
@@ -5719,6 +5773,11 @@ var DefaultConfigLoader = class {
|
|
|
5719
5773
|
}
|
|
5720
5774
|
return Object.freeze(cfg);
|
|
5721
5775
|
}
|
|
5776
|
+
/** Check whether a config object contains only the two bootstrap keys. */
|
|
5777
|
+
static isBootstrapOnly(config) {
|
|
5778
|
+
const BOOTSTRAP_KEYS = /* @__PURE__ */ new Set(["version", "activeProfile"]);
|
|
5779
|
+
return Object.keys(config).every((k) => BOOTSTRAP_KEYS.has(k));
|
|
5780
|
+
}
|
|
5722
5781
|
async ensureGlobalDefaults() {
|
|
5723
5782
|
const fp = this.paths.globalConfig;
|
|
5724
5783
|
const t0 = Date.now();
|
|
@@ -5727,7 +5786,7 @@ var DefaultConfigLoader = class {
|
|
|
5727
5786
|
let parsed;
|
|
5728
5787
|
let fileExisted = true;
|
|
5729
5788
|
try {
|
|
5730
|
-
const raw = await
|
|
5789
|
+
const raw = await fs7.readFile(fp, "utf8");
|
|
5731
5790
|
const result = safeParse(raw);
|
|
5732
5791
|
if (!result.ok || !isPlainRecord(result.value)) {
|
|
5733
5792
|
return;
|
|
@@ -5756,9 +5815,12 @@ var DefaultConfigLoader = class {
|
|
|
5756
5815
|
fileExisted = false;
|
|
5757
5816
|
parsed = {};
|
|
5758
5817
|
}
|
|
5818
|
+
const profileName = parsed.activeProfile ?? "default";
|
|
5819
|
+
const profileFp = this.paths.profileConfig(profileName);
|
|
5820
|
+
await this.ensureProfileConfig(profileFp, parsed, fileExisted);
|
|
5759
5821
|
const bootstrap = {
|
|
5760
5822
|
version: 1,
|
|
5761
|
-
activeProfile:
|
|
5823
|
+
activeProfile: profileName
|
|
5762
5824
|
};
|
|
5763
5825
|
let needsBootstrapWrite = false;
|
|
5764
5826
|
if (parsed.version !== 1) {
|
|
@@ -5767,7 +5829,11 @@ var DefaultConfigLoader = class {
|
|
|
5767
5829
|
if (parsed.activeProfile === void 0) {
|
|
5768
5830
|
needsBootstrapWrite = true;
|
|
5769
5831
|
}
|
|
5832
|
+
if (!_DefaultConfigLoader.isBootstrapOnly(parsed)) {
|
|
5833
|
+
needsBootstrapWrite = true;
|
|
5834
|
+
}
|
|
5770
5835
|
if (needsBootstrapWrite) {
|
|
5836
|
+
await backupConfigFile(fp, this.paths);
|
|
5771
5837
|
await atomicWrite(fp, JSON.stringify(bootstrap, null, 2), { mode: 384 });
|
|
5772
5838
|
this.events?.emit("storage.write", {
|
|
5773
5839
|
sessionId: "~config~",
|
|
@@ -5779,9 +5845,6 @@ var DefaultConfigLoader = class {
|
|
|
5779
5845
|
...this.traceId !== void 0 ? { traceId: this.traceId } : {}
|
|
5780
5846
|
});
|
|
5781
5847
|
}
|
|
5782
|
-
const profileName = bootstrap.activeProfile ?? "default";
|
|
5783
|
-
const profileFp = this.paths.profileConfig(profileName);
|
|
5784
|
-
await this.ensureProfileConfig(profileFp, parsed, fileExisted);
|
|
5785
5848
|
});
|
|
5786
5849
|
} catch (err) {
|
|
5787
5850
|
this.events?.emit("storage.error", {
|
|
@@ -5807,77 +5870,92 @@ var DefaultConfigLoader = class {
|
|
|
5807
5870
|
* On first boot: migrate content from the old flat global config, or seed
|
|
5808
5871
|
* with behavior defaults. Subsequent boots: fill any missing keys from
|
|
5809
5872
|
* BEHAVIOR_DEFAULTS (keeping user settings intact).
|
|
5873
|
+
*
|
|
5874
|
+
* CRITICAL SAFETY NET: when the profile already exists but the old global
|
|
5875
|
+
* config has extra non-bootstrap keys (because WebUI/CLI persistence functions
|
|
5876
|
+
* mistakenly wrote settings to the root config), those keys are merged into
|
|
5877
|
+
* the profile so they are NOT silently destroyed by the subsequent bootstrap
|
|
5878
|
+
* trim. This prevents the "config keeps getting emptied" bug.
|
|
5810
5879
|
*/
|
|
5811
5880
|
async ensureProfileConfig(profileFp, oldGlobalParsed, oldFileExisted) {
|
|
5812
5881
|
const t0 = Date.now();
|
|
5882
|
+
let parsed;
|
|
5883
|
+
let existed = true;
|
|
5813
5884
|
try {
|
|
5814
|
-
await
|
|
5815
|
-
|
|
5816
|
-
|
|
5817
|
-
|
|
5818
|
-
|
|
5819
|
-
|
|
5820
|
-
|
|
5821
|
-
|
|
5822
|
-
|
|
5823
|
-
|
|
5824
|
-
|
|
5825
|
-
|
|
5826
|
-
|
|
5827
|
-
|
|
5885
|
+
const raw = await fs7.readFile(profileFp, "utf8");
|
|
5886
|
+
const result = safeParse(raw);
|
|
5887
|
+
if (!result.ok || !isPlainRecord(result.value)) {
|
|
5888
|
+
this.logWarn("Profile config parse failed \u2014 falling back to defaults", {
|
|
5889
|
+
event: "config.profile_parse_failed",
|
|
5890
|
+
path: profileFp
|
|
5891
|
+
});
|
|
5892
|
+
parsed = {};
|
|
5893
|
+
} else {
|
|
5894
|
+
parsed = result.value;
|
|
5895
|
+
}
|
|
5896
|
+
} catch (err) {
|
|
5897
|
+
if (err.code !== "ENOENT") {
|
|
5898
|
+
this.logWarn("Profile config read failed", {
|
|
5899
|
+
event: "config.profile_read_failed",
|
|
5900
|
+
path: profileFp,
|
|
5901
|
+
message: toErrorMessage(err)
|
|
5902
|
+
});
|
|
5903
|
+
return;
|
|
5904
|
+
}
|
|
5905
|
+
existed = false;
|
|
5906
|
+
parsed = {};
|
|
5907
|
+
}
|
|
5908
|
+
const profileHasContent = existed && Object.keys(parsed).some((k) => k !== "version" && k !== "activeProfile");
|
|
5909
|
+
let seed;
|
|
5910
|
+
if (!profileHasContent && oldFileExisted && Object.keys(oldGlobalParsed).length >= 1) {
|
|
5911
|
+
seed = { ...oldGlobalParsed };
|
|
5912
|
+
delete seed["activeProfile"];
|
|
5913
|
+
const filled = fillMissingDefaults(seed, BEHAVIOR_DEFAULTS);
|
|
5914
|
+
seed = filled.value;
|
|
5915
|
+
} else {
|
|
5916
|
+
if (existed && !_DefaultConfigLoader.isBootstrapOnly(oldGlobalParsed)) {
|
|
5917
|
+
const BOOTSTRAP_KEYS = /* @__PURE__ */ new Set(["version", "activeProfile"]);
|
|
5918
|
+
let merged = false;
|
|
5919
|
+
for (const [k, v] of Object.entries(oldGlobalParsed)) {
|
|
5920
|
+
if (!BOOTSTRAP_KEYS.has(k) && !(k in parsed)) {
|
|
5921
|
+
parsed[k] = v;
|
|
5922
|
+
merged = true;
|
|
5828
5923
|
}
|
|
5829
|
-
}
|
|
5830
|
-
|
|
5831
|
-
|
|
5832
|
-
|
|
5833
|
-
|
|
5834
|
-
|
|
5924
|
+
}
|
|
5925
|
+
if (merged) {
|
|
5926
|
+
const filled2 = fillMissingDefaults(parsed, BEHAVIOR_DEFAULTS);
|
|
5927
|
+
if (filled2.changed) {
|
|
5928
|
+
seed = filled2.value;
|
|
5929
|
+
await backupConfigFile(profileFp, this.paths);
|
|
5930
|
+
await atomicWrite(profileFp, JSON.stringify(seed, null, 2), { mode: 384 });
|
|
5931
|
+
this.events?.emit("storage.write", {
|
|
5932
|
+
sessionId: "~config~",
|
|
5933
|
+
store: "config",
|
|
5934
|
+
filePath: profileFp,
|
|
5935
|
+
operation: "ensure_profile_defaults",
|
|
5936
|
+
outcome: "success",
|
|
5937
|
+
durationMs: Date.now() - t0,
|
|
5938
|
+
...this.traceId !== void 0 ? { traceId: this.traceId } : {}
|
|
5835
5939
|
});
|
|
5836
5940
|
return;
|
|
5837
5941
|
}
|
|
5838
|
-
existed = false;
|
|
5839
|
-
parsed = {};
|
|
5840
5942
|
}
|
|
5841
|
-
|
|
5842
|
-
|
|
5843
|
-
|
|
5844
|
-
|
|
5845
|
-
const filled = fillMissingDefaults(seed, BEHAVIOR_DEFAULTS);
|
|
5846
|
-
seed = filled.value;
|
|
5847
|
-
} else {
|
|
5848
|
-
const filled = fillMissingDefaults(parsed, BEHAVIOR_DEFAULTS);
|
|
5849
|
-
if (!filled.changed) return;
|
|
5850
|
-
seed = filled.value;
|
|
5851
|
-
}
|
|
5852
|
-
await atomicWrite(profileFp, JSON.stringify(seed, null, 2), { mode: 384 });
|
|
5853
|
-
this.events?.emit("storage.write", {
|
|
5854
|
-
sessionId: "~config~",
|
|
5855
|
-
store: "config",
|
|
5856
|
-
filePath: profileFp,
|
|
5857
|
-
operation: "ensure_profile_defaults",
|
|
5858
|
-
outcome: "success",
|
|
5859
|
-
durationMs: Date.now() - t0,
|
|
5860
|
-
...this.traceId !== void 0 ? { traceId: this.traceId } : {}
|
|
5861
|
-
});
|
|
5862
|
-
});
|
|
5863
|
-
} catch (err) {
|
|
5864
|
-
this.events?.emit("storage.error", {
|
|
5865
|
-
sessionId: "~config~",
|
|
5866
|
-
store: "config",
|
|
5867
|
-
filePath: profileFp,
|
|
5868
|
-
operation: "ensure_profile_defaults",
|
|
5869
|
-
outcome: "failure",
|
|
5870
|
-
error: storageErrorString(err),
|
|
5871
|
-
recoverable: false,
|
|
5872
|
-
durationMs: Date.now() - t0,
|
|
5873
|
-
...this.traceId !== void 0 ? { traceId: this.traceId } : {}
|
|
5874
|
-
});
|
|
5875
|
-
this.logWarn("Profile config defaults write failed", {
|
|
5876
|
-
event: "config.profile_write_failed",
|
|
5877
|
-
path: profileFp,
|
|
5878
|
-
message: toErrorMessage(err)
|
|
5879
|
-
});
|
|
5943
|
+
}
|
|
5944
|
+
const filled = fillMissingDefaults(parsed, BEHAVIOR_DEFAULTS);
|
|
5945
|
+
if (!filled.changed) return;
|
|
5946
|
+
seed = filled.value;
|
|
5880
5947
|
}
|
|
5948
|
+
await backupConfigFile(profileFp, this.paths);
|
|
5949
|
+
await atomicWrite(profileFp, JSON.stringify(seed, null, 2), { mode: 384 });
|
|
5950
|
+
this.events?.emit("storage.write", {
|
|
5951
|
+
sessionId: "~config~",
|
|
5952
|
+
store: "config",
|
|
5953
|
+
filePath: profileFp,
|
|
5954
|
+
operation: "ensure_profile_defaults",
|
|
5955
|
+
outcome: "success",
|
|
5956
|
+
durationMs: Date.now() - t0,
|
|
5957
|
+
...this.traceId !== void 0 ? { traceId: this.traceId } : {}
|
|
5958
|
+
});
|
|
5881
5959
|
}
|
|
5882
5960
|
/**
|
|
5883
5961
|
* Persist a sync config to ~/.wrongstack/sync.json, with the token encrypted
|
|
@@ -5927,7 +6005,7 @@ var DefaultConfigLoader = class {
|
|
|
5927
6005
|
const fp = this.paths.syncConfig;
|
|
5928
6006
|
const t0 = Date.now();
|
|
5929
6007
|
try {
|
|
5930
|
-
const raw = await
|
|
6008
|
+
const raw = await fs7.readFile(fp, "utf8");
|
|
5931
6009
|
const parsed = safeParse(raw);
|
|
5932
6010
|
if (!parsed.ok || !parsed.value) {
|
|
5933
6011
|
this.events?.emit("storage.read", {
|
|
@@ -5989,7 +6067,7 @@ var DefaultConfigLoader = class {
|
|
|
5989
6067
|
const t0 = Date.now();
|
|
5990
6068
|
let mtimeMs = null;
|
|
5991
6069
|
try {
|
|
5992
|
-
const stat13 = await
|
|
6070
|
+
const stat13 = await fs7.stat(file);
|
|
5993
6071
|
mtimeMs = stat13.mtimeMs;
|
|
5994
6072
|
const cached = this.jsonCache.get(file);
|
|
5995
6073
|
if (cached && cached.mtimeMs === mtimeMs) {
|
|
@@ -6019,7 +6097,7 @@ var DefaultConfigLoader = class {
|
|
|
6019
6097
|
}
|
|
6020
6098
|
let raw;
|
|
6021
6099
|
try {
|
|
6022
|
-
raw = await
|
|
6100
|
+
raw = await fs7.readFile(file, "utf8");
|
|
6023
6101
|
} catch (err) {
|
|
6024
6102
|
if (err.code !== "ENOENT") {
|
|
6025
6103
|
this.events?.emit("storage.read", {
|
|
@@ -6182,7 +6260,7 @@ var DEFAULT_CONFIG_MIGRATIONS = [];
|
|
|
6182
6260
|
// src/storage/recovery-lock.ts
|
|
6183
6261
|
import * as fsp7 from "node:fs/promises";
|
|
6184
6262
|
import * as os2 from "node:os";
|
|
6185
|
-
import * as
|
|
6263
|
+
import * as path15 from "node:path";
|
|
6186
6264
|
var LOCK_FILE = "active.json";
|
|
6187
6265
|
var DEFAULT_MAX_AGE_MS = 24 * 60 * 60 * 1e3;
|
|
6188
6266
|
var RecoveryLock = class {
|
|
@@ -6193,7 +6271,7 @@ var RecoveryLock = class {
|
|
|
6193
6271
|
sessionStore;
|
|
6194
6272
|
probe;
|
|
6195
6273
|
constructor(opts) {
|
|
6196
|
-
this.file =
|
|
6274
|
+
this.file = path15.join(opts.dir, LOCK_FILE);
|
|
6197
6275
|
this.pid = opts.pid ?? process.pid;
|
|
6198
6276
|
this.hostname = opts.hostname ?? os2.hostname();
|
|
6199
6277
|
this.maxAgeMs = opts.maxAgeMs ?? DEFAULT_MAX_AGE_MS;
|
|
@@ -6254,7 +6332,7 @@ var RecoveryLock = class {
|
|
|
6254
6332
|
* null return before calling this.
|
|
6255
6333
|
*/
|
|
6256
6334
|
async write(sessionId) {
|
|
6257
|
-
await ensureDir(
|
|
6335
|
+
await ensureDir(path15.dirname(this.file));
|
|
6258
6336
|
const lock = {
|
|
6259
6337
|
v: 1,
|
|
6260
6338
|
sessionId,
|
|
@@ -6322,7 +6400,7 @@ function defaultIsPidAlive(pid) {
|
|
|
6322
6400
|
}
|
|
6323
6401
|
|
|
6324
6402
|
// src/storage/session-reader.ts
|
|
6325
|
-
import * as
|
|
6403
|
+
import * as fs8 from "node:fs/promises";
|
|
6326
6404
|
var DefaultSessionReader = class _DefaultSessionReader {
|
|
6327
6405
|
store;
|
|
6328
6406
|
eventCache = /* @__PURE__ */ new Map();
|
|
@@ -6340,7 +6418,7 @@ var DefaultSessionReader = class _DefaultSessionReader {
|
|
|
6340
6418
|
const sessionPath = sessionScopedPath(rootDir, sessionId, ".jsonl");
|
|
6341
6419
|
let mtimeMs = null;
|
|
6342
6420
|
try {
|
|
6343
|
-
const stat13 = await
|
|
6421
|
+
const stat13 = await fs8.stat(sessionPath);
|
|
6344
6422
|
mtimeMs = stat13.mtimeMs;
|
|
6345
6423
|
} catch {
|
|
6346
6424
|
this.eventCache.delete(sessionId);
|
|
@@ -6704,7 +6782,7 @@ function renderPlainText(meta, events) {
|
|
|
6704
6782
|
|
|
6705
6783
|
// src/storage/annotations-store.ts
|
|
6706
6784
|
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
6707
|
-
import * as
|
|
6785
|
+
import * as fs9 from "node:fs/promises";
|
|
6708
6786
|
var FILE_VERSION = 1;
|
|
6709
6787
|
var MAX_TEXT_LENGTH = 2e3;
|
|
6710
6788
|
var MAX_ANNOTATIONS = 1e3;
|
|
@@ -6931,7 +7009,7 @@ var AnnotationsStore = class {
|
|
|
6931
7009
|
const fp = this.filePath(sessionId);
|
|
6932
7010
|
let raw;
|
|
6933
7011
|
try {
|
|
6934
|
-
raw = await
|
|
7012
|
+
raw = await fs9.readFile(fp, "utf8");
|
|
6935
7013
|
} catch (err) {
|
|
6936
7014
|
if (err.code === "ENOENT") return null;
|
|
6937
7015
|
throw err;
|
|
@@ -6968,8 +7046,8 @@ var AnnotationsStore = class {
|
|
|
6968
7046
|
};
|
|
6969
7047
|
|
|
6970
7048
|
// src/storage/replay-log-store.ts
|
|
6971
|
-
import * as
|
|
6972
|
-
import * as
|
|
7049
|
+
import * as fs10 from "node:fs/promises";
|
|
7050
|
+
import * as path16 from "node:path";
|
|
6973
7051
|
|
|
6974
7052
|
// src/replay/hash.ts
|
|
6975
7053
|
import { createHash as createHash5 } from "node:crypto";
|
|
@@ -7058,12 +7136,12 @@ var ReplayLogStore = class {
|
|
|
7058
7136
|
const line = JSON.stringify(entry) + "\n";
|
|
7059
7137
|
let offset2 = 0;
|
|
7060
7138
|
try {
|
|
7061
|
-
const stat13 = await
|
|
7139
|
+
const stat13 = await fs10.stat(fp);
|
|
7062
7140
|
offset2 = stat13.size;
|
|
7063
7141
|
} catch (err) {
|
|
7064
7142
|
if (err.code !== "ENOENT") throw err;
|
|
7065
7143
|
}
|
|
7066
|
-
await
|
|
7144
|
+
await fs10.appendFile(fp, line, "utf8");
|
|
7067
7145
|
cache.set(hash, { entry, offset: offset2, length: Buffer.byteLength(line, "utf8") });
|
|
7068
7146
|
this.diskCount.set(input.sessionId, currentCount + 1);
|
|
7069
7147
|
this.events?.emit("storage.write", {
|
|
@@ -7191,7 +7269,7 @@ var ReplayLogStore = class {
|
|
|
7191
7269
|
const scan = async (dir, prefix, depth) => {
|
|
7192
7270
|
let entries;
|
|
7193
7271
|
try {
|
|
7194
|
-
entries = await
|
|
7272
|
+
entries = await fs10.readdir(dir, { withFileTypes: true });
|
|
7195
7273
|
} catch (err) {
|
|
7196
7274
|
if (depth === 0 && err.code !== "ENOENT") {
|
|
7197
7275
|
console.warn(JSON.stringify({
|
|
@@ -7207,13 +7285,13 @@ var ReplayLogStore = class {
|
|
|
7207
7285
|
for (const entry of entries) {
|
|
7208
7286
|
if (entry.name.startsWith(".")) continue;
|
|
7209
7287
|
if (entry.isDirectory()) {
|
|
7210
|
-
if (depth === 0) await scan(
|
|
7288
|
+
if (depth === 0) await scan(path16.join(dir, entry.name), entry.name, depth + 1);
|
|
7211
7289
|
continue;
|
|
7212
7290
|
}
|
|
7213
7291
|
if (!entry.isFile() || !entry.name.endsWith(".replay.jsonl")) continue;
|
|
7214
7292
|
const base = entry.name.slice(0, -".replay.jsonl".length);
|
|
7215
7293
|
const sessionId = prefix ? `${prefix}/${base}` : base;
|
|
7216
|
-
const fp =
|
|
7294
|
+
const fp = path16.join(dir, entry.name);
|
|
7217
7295
|
out.push({
|
|
7218
7296
|
sessionId,
|
|
7219
7297
|
entryCount: await this.countEntries(fp),
|
|
@@ -7229,7 +7307,7 @@ var ReplayLogStore = class {
|
|
|
7229
7307
|
return sessionScopedPath(this.dir, sessionId, ".replay.jsonl");
|
|
7230
7308
|
}
|
|
7231
7309
|
async countEntries(filePath) {
|
|
7232
|
-
const handle = await
|
|
7310
|
+
const handle = await fs10.open(filePath, "r");
|
|
7233
7311
|
const buffer = Buffer.allocUnsafe(64 * 1024);
|
|
7234
7312
|
let count = 0;
|
|
7235
7313
|
let hasNonWhitespace2 = false;
|
|
@@ -7270,7 +7348,7 @@ var ReplayLogStore = class {
|
|
|
7270
7348
|
async readAll(sessionId) {
|
|
7271
7349
|
const fp = this.filePath(sessionId);
|
|
7272
7350
|
try {
|
|
7273
|
-
const raw = await
|
|
7351
|
+
const raw = await fs10.readFile(fp, "utf8");
|
|
7274
7352
|
const out = [];
|
|
7275
7353
|
for (const line of raw.split("\n")) {
|
|
7276
7354
|
if (!line.trim()) continue;
|
|
@@ -7306,7 +7384,7 @@ var ReplayLogStore = class {
|
|
|
7306
7384
|
const fp = this.filePath(sessionId);
|
|
7307
7385
|
cache = /* @__PURE__ */ new Map();
|
|
7308
7386
|
try {
|
|
7309
|
-
const handle = await
|
|
7387
|
+
const handle = await fs10.open(fp, "r");
|
|
7310
7388
|
const CHUNK = 64 * 1024;
|
|
7311
7389
|
const buffer = Buffer.alloc(CHUNK);
|
|
7312
7390
|
let leftover = "";
|
|
@@ -7357,7 +7435,7 @@ var ReplayLogStore = class {
|
|
|
7357
7435
|
async hydrateEntry(sessionId, hash, location) {
|
|
7358
7436
|
if (location.entry) return location.entry;
|
|
7359
7437
|
const fp = this.filePath(sessionId);
|
|
7360
|
-
const handle = await
|
|
7438
|
+
const handle = await fs10.open(fp, "r");
|
|
7361
7439
|
try {
|
|
7362
7440
|
const buffer = Buffer.alloc(location.length);
|
|
7363
7441
|
const { bytesRead } = await handle.read(buffer, 0, location.length, location.offset);
|
|
@@ -7384,8 +7462,8 @@ var ReplayLogStore = class {
|
|
|
7384
7462
|
};
|
|
7385
7463
|
|
|
7386
7464
|
// src/storage/session-recovery.ts
|
|
7387
|
-
import * as
|
|
7388
|
-
import * as
|
|
7465
|
+
import * as fs11 from "node:fs/promises";
|
|
7466
|
+
import * as path17 from "node:path";
|
|
7389
7467
|
var SessionRecovery = class {
|
|
7390
7468
|
constructor(dir) {
|
|
7391
7469
|
this.dir = dir;
|
|
@@ -7411,7 +7489,7 @@ var SessionRecovery = class {
|
|
|
7411
7489
|
const fp = this.filePath(sessionId);
|
|
7412
7490
|
let stat13;
|
|
7413
7491
|
try {
|
|
7414
|
-
stat13 = await
|
|
7492
|
+
stat13 = await fs11.stat(fp);
|
|
7415
7493
|
} catch (err) {
|
|
7416
7494
|
if (err.code === "ENOENT") return null;
|
|
7417
7495
|
return null;
|
|
@@ -7445,7 +7523,7 @@ var SessionRecovery = class {
|
|
|
7445
7523
|
const fp = this.filePath(sessionId);
|
|
7446
7524
|
let raw;
|
|
7447
7525
|
try {
|
|
7448
|
-
raw = await
|
|
7526
|
+
raw = await fs11.readFile(fp, "utf8");
|
|
7449
7527
|
} catch (err) {
|
|
7450
7528
|
if (err.code === "ENOENT") return null;
|
|
7451
7529
|
return null;
|
|
@@ -7490,7 +7568,7 @@ var SessionRecovery = class {
|
|
|
7490
7568
|
const collect = async (dir, prefix, depth) => {
|
|
7491
7569
|
let entries;
|
|
7492
7570
|
try {
|
|
7493
|
-
entries = await
|
|
7571
|
+
entries = await fs11.readdir(dir, { withFileTypes: true });
|
|
7494
7572
|
} catch {
|
|
7495
7573
|
return;
|
|
7496
7574
|
}
|
|
@@ -7500,7 +7578,7 @@ var SessionRecovery = class {
|
|
|
7500
7578
|
continue;
|
|
7501
7579
|
if (entry.isDirectory()) {
|
|
7502
7580
|
if (depth === 0) {
|
|
7503
|
-
await collect(
|
|
7581
|
+
await collect(path17.join(dir, entry.name), entry.name, depth + 1);
|
|
7504
7582
|
}
|
|
7505
7583
|
continue;
|
|
7506
7584
|
}
|
|
@@ -7541,7 +7619,7 @@ function hasNonWhitespace(line) {
|
|
|
7541
7619
|
}
|
|
7542
7620
|
async function scanLatestLifecycleBoundary(filePath, size) {
|
|
7543
7621
|
const CHUNK_SIZE = 64 * 1024;
|
|
7544
|
-
const handle = await
|
|
7622
|
+
const handle = await fs11.open(filePath, "r");
|
|
7545
7623
|
let position = size;
|
|
7546
7624
|
let laterLineFragment = Buffer.alloc(0);
|
|
7547
7625
|
let latestBoundary = null;
|
|
@@ -7580,7 +7658,7 @@ async function scanLatestLifecycleBoundary(filePath, size) {
|
|
|
7580
7658
|
|
|
7581
7659
|
// src/storage/tool-audit-log.ts
|
|
7582
7660
|
import { createHash as createHash6, randomUUID as randomUUID4 } from "node:crypto";
|
|
7583
|
-
import * as
|
|
7661
|
+
import * as fs12 from "node:fs/promises";
|
|
7584
7662
|
var GENESIS_PREV = "0".repeat(64);
|
|
7585
7663
|
var DEFAULT_FSYNC_EVERY = 100;
|
|
7586
7664
|
var ToolAuditLog = class {
|
|
@@ -7650,9 +7728,9 @@ var ToolAuditLog = class {
|
|
|
7650
7728
|
isError: input.isError,
|
|
7651
7729
|
index
|
|
7652
7730
|
};
|
|
7653
|
-
await
|
|
7731
|
+
await fs12.appendFile(fp, JSON.stringify(entry) + "\n", "utf8");
|
|
7654
7732
|
try {
|
|
7655
|
-
const st = await
|
|
7733
|
+
const st = await fs12.stat(fp);
|
|
7656
7734
|
this.tailStat.set(input.sessionId, { mtimeMs: st.mtimeMs, size: st.size });
|
|
7657
7735
|
} catch {
|
|
7658
7736
|
}
|
|
@@ -7699,7 +7777,7 @@ var ToolAuditLog = class {
|
|
|
7699
7777
|
const cachedStat = this.tailStat.get(sessionId);
|
|
7700
7778
|
if (cachedHash !== void 0 && cachedIndex !== void 0 && cachedStat) {
|
|
7701
7779
|
try {
|
|
7702
|
-
const st = await
|
|
7780
|
+
const st = await fs12.stat(fp);
|
|
7703
7781
|
if (st.mtimeMs === cachedStat.mtimeMs && st.size === cachedStat.size) {
|
|
7704
7782
|
return { prevHash: cachedHash, nextIndex: cachedIndex };
|
|
7705
7783
|
}
|
|
@@ -7720,7 +7798,7 @@ var ToolAuditLog = class {
|
|
|
7720
7798
|
this.tailHash.set(sessionId, prevHash);
|
|
7721
7799
|
this.tailIndex.set(sessionId, nextIndex);
|
|
7722
7800
|
try {
|
|
7723
|
-
const st = await
|
|
7801
|
+
const st = await fs12.stat(fp);
|
|
7724
7802
|
this.tailStat.set(sessionId, { mtimeMs: st.mtimeMs, size: st.size });
|
|
7725
7803
|
} catch {
|
|
7726
7804
|
}
|
|
@@ -7841,7 +7919,7 @@ var ToolAuditLog = class {
|
|
|
7841
7919
|
async readAll(sessionId) {
|
|
7842
7920
|
const fp = this.filePath(sessionId);
|
|
7843
7921
|
try {
|
|
7844
|
-
const raw = await
|
|
7922
|
+
const raw = await fs12.readFile(fp, "utf8");
|
|
7845
7923
|
const out = [];
|
|
7846
7924
|
for (const line of raw.split("\n")) {
|
|
7847
7925
|
if (!line.trim()) continue;
|
|
@@ -7879,7 +7957,7 @@ var ToolAuditLog = class {
|
|
|
7879
7957
|
}
|
|
7880
7958
|
async sync(sessionId, fp) {
|
|
7881
7959
|
try {
|
|
7882
|
-
const fh = await
|
|
7960
|
+
const fh = await fs12.open(fp, "r+");
|
|
7883
7961
|
try {
|
|
7884
7962
|
await fh.sync();
|
|
7885
7963
|
} finally {
|
|
@@ -8018,8 +8096,8 @@ var SessionAnalyzer = class {
|
|
|
8018
8096
|
|
|
8019
8097
|
// src/session-registry.ts
|
|
8020
8098
|
import { randomUUID as randomUUID5 } from "node:crypto";
|
|
8021
|
-
import * as
|
|
8022
|
-
import * as
|
|
8099
|
+
import * as fs13 from "node:fs/promises";
|
|
8100
|
+
import * as path18 from "node:path";
|
|
8023
8101
|
var REGISTRY_FILE = "session-registry.json";
|
|
8024
8102
|
var HEARTBEAT_INTERVAL_MS = 5e3;
|
|
8025
8103
|
var STALE_TIMEOUT_MS = 3e4;
|
|
@@ -8058,7 +8136,7 @@ var SessionRegistry = class {
|
|
|
8058
8136
|
*/
|
|
8059
8137
|
lastEntry = null;
|
|
8060
8138
|
constructor(globalRoot) {
|
|
8061
|
-
this.filePath =
|
|
8139
|
+
this.filePath = path18.join(globalRoot, REGISTRY_FILE);
|
|
8062
8140
|
}
|
|
8063
8141
|
// ── Public API ──────────────────────────────────────────────────────────
|
|
8064
8142
|
/**
|
|
@@ -8214,7 +8292,7 @@ var SessionRegistry = class {
|
|
|
8214
8292
|
}
|
|
8215
8293
|
async readAndPrune() {
|
|
8216
8294
|
try {
|
|
8217
|
-
const raw = await
|
|
8295
|
+
const raw = await fs13.readFile(this.filePath, "utf8");
|
|
8218
8296
|
const registry = parseRegistry(raw);
|
|
8219
8297
|
const now = Date.now();
|
|
8220
8298
|
let pruned = false;
|
|
@@ -8267,11 +8345,11 @@ var SessionRegistry = class {
|
|
|
8267
8345
|
const retryDelayMs = 20;
|
|
8268
8346
|
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
8269
8347
|
try {
|
|
8270
|
-
await
|
|
8271
|
-
let lockHandle = await
|
|
8348
|
+
await fs13.mkdir(path18.dirname(this.filePath), { recursive: true });
|
|
8349
|
+
let lockHandle = await fs13.open(lockPath, "wx").catch(() => null);
|
|
8272
8350
|
if (!lockHandle) {
|
|
8273
8351
|
if (await this.breakStaleLock(lockPath)) {
|
|
8274
|
-
lockHandle = await
|
|
8352
|
+
lockHandle = await fs13.open(lockPath, "wx").catch(() => null);
|
|
8275
8353
|
}
|
|
8276
8354
|
if (!lockHandle) {
|
|
8277
8355
|
await new Promise((r) => setTimeout(r, retryDelayMs * (attempt + 1)));
|
|
@@ -8280,14 +8358,14 @@ var SessionRegistry = class {
|
|
|
8280
8358
|
}
|
|
8281
8359
|
try {
|
|
8282
8360
|
await lockHandle.writeFile(String(process.pid)).catch(() => void 0);
|
|
8283
|
-
const raw = await
|
|
8361
|
+
const raw = await fs13.readFile(this.filePath, "utf8").catch(() => "{}");
|
|
8284
8362
|
const registry = parseRegistry(raw);
|
|
8285
8363
|
fn(registry);
|
|
8286
8364
|
await this.writeAtomicLocked(registry);
|
|
8287
8365
|
return;
|
|
8288
8366
|
} finally {
|
|
8289
8367
|
await lockHandle.close();
|
|
8290
|
-
await
|
|
8368
|
+
await fs13.unlink(lockPath).catch(() => void 0);
|
|
8291
8369
|
}
|
|
8292
8370
|
} catch {
|
|
8293
8371
|
return;
|
|
@@ -8304,16 +8382,16 @@ var SessionRegistry = class {
|
|
|
8304
8382
|
async breakStaleLock(lockPath) {
|
|
8305
8383
|
try {
|
|
8306
8384
|
const [stat13, content] = await Promise.all([
|
|
8307
|
-
|
|
8385
|
+
fs13.stat(lockPath),
|
|
8308
8386
|
/* v8 ignore start -- best-effort lock-content read; .catch only fires if the lock vanished */
|
|
8309
|
-
|
|
8387
|
+
fs13.readFile(lockPath, "utf8").catch(() => "")
|
|
8310
8388
|
/* v8 ignore stop */
|
|
8311
8389
|
]);
|
|
8312
8390
|
const ageMs = Date.now() - stat13.mtimeMs;
|
|
8313
8391
|
const ownerPid = Number.parseInt(content.trim(), 10);
|
|
8314
8392
|
const ownerDead = Number.isInteger(ownerPid) && ownerPid > 0 && ownerPid !== process.pid && !pidAlive(ownerPid);
|
|
8315
8393
|
if (ownerDead || ageMs > STALE_LOCK_MS) {
|
|
8316
|
-
await
|
|
8394
|
+
await fs13.unlink(lockPath).catch(() => void 0);
|
|
8317
8395
|
return true;
|
|
8318
8396
|
}
|
|
8319
8397
|
return false;
|
|
@@ -8331,41 +8409,41 @@ var SessionRegistry = class {
|
|
|
8331
8409
|
await this.writeAtomicFile(registry);
|
|
8332
8410
|
}
|
|
8333
8411
|
async writeAtomicFile(registry) {
|
|
8334
|
-
const tmp =
|
|
8335
|
-
|
|
8336
|
-
`.${
|
|
8412
|
+
const tmp = path18.join(
|
|
8413
|
+
path18.dirname(this.filePath),
|
|
8414
|
+
`.${path18.basename(this.filePath)}.${randomUUID5().slice(0, 8)}.tmp`
|
|
8337
8415
|
);
|
|
8338
8416
|
try {
|
|
8339
|
-
const handle = await
|
|
8417
|
+
const handle = await fs13.open(tmp, "w");
|
|
8340
8418
|
try {
|
|
8341
8419
|
await handle.writeFile(JSON.stringify(registry, null, 2), "utf8");
|
|
8342
8420
|
await handle.sync().catch(() => void 0);
|
|
8343
8421
|
} finally {
|
|
8344
8422
|
await handle.close();
|
|
8345
8423
|
}
|
|
8346
|
-
await
|
|
8424
|
+
await fs13.rename(tmp, this.filePath);
|
|
8347
8425
|
} catch (err) {
|
|
8348
|
-
await
|
|
8426
|
+
await fs13.unlink(tmp).catch(() => void 0);
|
|
8349
8427
|
throw err;
|
|
8350
8428
|
}
|
|
8351
8429
|
}
|
|
8352
8430
|
async pruneStaleTempFiles() {
|
|
8353
8431
|
try {
|
|
8354
|
-
const dir =
|
|
8355
|
-
const base =
|
|
8432
|
+
const dir = path18.dirname(this.filePath);
|
|
8433
|
+
const base = path18.basename(this.filePath);
|
|
8356
8434
|
const now = Date.now();
|
|
8357
8435
|
const stale = [];
|
|
8358
|
-
for (const name of await
|
|
8436
|
+
for (const name of await fs13.readdir(dir)) {
|
|
8359
8437
|
const isTemp = (name.startsWith(`${base}.`) || name.startsWith(`.${base}.`)) && name.endsWith(".tmp");
|
|
8360
8438
|
if (!isTemp) continue;
|
|
8361
|
-
const stat13 = await
|
|
8439
|
+
const stat13 = await fs13.stat(path18.join(dir, name)).catch(() => null);
|
|
8362
8440
|
if (!stat13) continue;
|
|
8363
8441
|
if (now - stat13.mtimeMs > STALE_TMP_MS) stale.push({ name, mtimeMs: stat13.mtimeMs });
|
|
8364
8442
|
}
|
|
8365
8443
|
stale.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
8366
8444
|
await Promise.all(
|
|
8367
8445
|
stale.slice(MAX_STALE_TMP_FILES).map(async ({ name }) => {
|
|
8368
|
-
await
|
|
8446
|
+
await fs13.unlink(path18.join(dir, name)).catch(() => void 0);
|
|
8369
8447
|
})
|
|
8370
8448
|
);
|
|
8371
8449
|
} catch {
|
|
@@ -9100,8 +9178,8 @@ var AgentStatusTracker = class {
|
|
|
9100
9178
|
};
|
|
9101
9179
|
|
|
9102
9180
|
// src/fleet-notifier.ts
|
|
9103
|
-
import * as
|
|
9104
|
-
import * as
|
|
9181
|
+
import * as fs14 from "node:fs/promises";
|
|
9182
|
+
import * as path19 from "node:path";
|
|
9105
9183
|
var INSTANCES_FILE = "webui-instances.json";
|
|
9106
9184
|
var DISCOVERY_TTL_MS = 2500;
|
|
9107
9185
|
var COALESCE_MS = 50;
|
|
@@ -9116,7 +9194,7 @@ function pidAlive2(pid) {
|
|
|
9116
9194
|
}
|
|
9117
9195
|
}
|
|
9118
9196
|
function normRoot(root) {
|
|
9119
|
-
const resolved =
|
|
9197
|
+
const resolved = path19.resolve(root);
|
|
9120
9198
|
return process.platform === "win32" ? resolved.toLowerCase() : resolved;
|
|
9121
9199
|
}
|
|
9122
9200
|
var FleetNotifier = class {
|
|
@@ -9163,7 +9241,7 @@ var FleetNotifier = class {
|
|
|
9163
9241
|
}
|
|
9164
9242
|
async discover() {
|
|
9165
9243
|
try {
|
|
9166
|
-
const raw = await
|
|
9244
|
+
const raw = await fs14.readFile(path19.join(this.baseDir, INSTANCES_FILE), "utf8");
|
|
9167
9245
|
const data = JSON.parse(raw);
|
|
9168
9246
|
const list = Array.isArray(data?.instances) ? data.instances : [];
|
|
9169
9247
|
return list.filter((i) => i && typeof i.httpPort === "number").filter((i) => i.pid !== this.selfPid).filter((i) => normRoot(i.projectRoot) === this.projectRoot).filter((i) => pidAlive2(i.pid)).map((i) => {
|
|
@@ -9184,7 +9262,7 @@ async function defaultPost(url) {
|
|
|
9184
9262
|
|
|
9185
9263
|
// src/storage/session-rewinder.ts
|
|
9186
9264
|
import * as fsp8 from "node:fs/promises";
|
|
9187
|
-
import * as
|
|
9265
|
+
import * as path20 from "node:path";
|
|
9188
9266
|
var DefaultSessionRewinder = class {
|
|
9189
9267
|
constructor(sessionsDir, projectRoot) {
|
|
9190
9268
|
this.sessionsDir = sessionsDir;
|
|
@@ -9322,10 +9400,10 @@ async function revertSnapshots(snapshots, projectRoot) {
|
|
|
9322
9400
|
for (const snapshot of [...snapshots].reverse()) {
|
|
9323
9401
|
for (const file of [...snapshot.files].reverse()) {
|
|
9324
9402
|
try {
|
|
9325
|
-
const absPath =
|
|
9326
|
-
const root =
|
|
9327
|
-
const rel =
|
|
9328
|
-
if (rel.startsWith("..") ||
|
|
9403
|
+
const absPath = path20.resolve(file.path);
|
|
9404
|
+
const root = path20.resolve(projectRoot);
|
|
9405
|
+
const rel = path20.relative(root, absPath);
|
|
9406
|
+
if (rel.startsWith("..") || path20.isAbsolute(rel)) {
|
|
9329
9407
|
errors.push(`${file.path}: path resolves outside project root \u2014 skipping`);
|
|
9330
9408
|
continue;
|
|
9331
9409
|
}
|
|
@@ -10850,8 +10928,8 @@ function normalizeTitle(value) {
|
|
|
10850
10928
|
|
|
10851
10929
|
// src/storage/prompt-store.ts
|
|
10852
10930
|
import { createHash as createHash7 } from "node:crypto";
|
|
10853
|
-
import * as
|
|
10854
|
-
import * as
|
|
10931
|
+
import * as fs15 from "node:fs/promises";
|
|
10932
|
+
import * as path21 from "node:path";
|
|
10855
10933
|
var SCHEMA_VERSION = 2;
|
|
10856
10934
|
function promptChecksum(content) {
|
|
10857
10935
|
return createHash7("sha256").update(content, "utf8").digest("hex");
|
|
@@ -10915,12 +10993,12 @@ var DefaultPromptStore = class {
|
|
|
10915
10993
|
await ensureDir(this.dir);
|
|
10916
10994
|
const entries = [];
|
|
10917
10995
|
try {
|
|
10918
|
-
const files = await
|
|
10996
|
+
const files = await fs15.readdir(this.dir);
|
|
10919
10997
|
for (const file of files) {
|
|
10920
10998
|
if (!file.endsWith(".json")) continue;
|
|
10921
10999
|
try {
|
|
10922
11000
|
const raw = JSON.parse(
|
|
10923
|
-
await
|
|
11001
|
+
await fs15.readFile(path21.join(this.dir, file), "utf8")
|
|
10924
11002
|
);
|
|
10925
11003
|
const migrated = migratePromptEntry(raw.entry);
|
|
10926
11004
|
if (migrated) entries.push(migrated);
|
|
@@ -10934,9 +11012,9 @@ var DefaultPromptStore = class {
|
|
|
10934
11012
|
);
|
|
10935
11013
|
}
|
|
10936
11014
|
async get(id) {
|
|
10937
|
-
const file =
|
|
11015
|
+
const file = path21.join(this.dir, `${id}.json`);
|
|
10938
11016
|
try {
|
|
10939
|
-
const raw = JSON.parse(await
|
|
11017
|
+
const raw = JSON.parse(await fs15.readFile(file, "utf8"));
|
|
10940
11018
|
return migratePromptEntry(raw.entry);
|
|
10941
11019
|
} catch {
|
|
10942
11020
|
return null;
|
|
@@ -10944,14 +11022,14 @@ var DefaultPromptStore = class {
|
|
|
10944
11022
|
}
|
|
10945
11023
|
async save(entry) {
|
|
10946
11024
|
await ensureDir(this.dir);
|
|
10947
|
-
const file =
|
|
11025
|
+
const file = path21.join(this.dir, `${entry.id}.json`);
|
|
10948
11026
|
const raw = { version: SCHEMA_VERSION, entry };
|
|
10949
11027
|
await atomicWrite(file, JSON.stringify(raw, null, 2));
|
|
10950
11028
|
}
|
|
10951
11029
|
async delete(id) {
|
|
10952
|
-
const file =
|
|
11030
|
+
const file = path21.join(this.dir, `${id}.json`);
|
|
10953
11031
|
try {
|
|
10954
|
-
await
|
|
11032
|
+
await fs15.unlink(file);
|
|
10955
11033
|
return true;
|
|
10956
11034
|
} catch {
|
|
10957
11035
|
return false;
|
|
@@ -10990,8 +11068,8 @@ var DefaultPromptStore = class {
|
|
|
10990
11068
|
};
|
|
10991
11069
|
|
|
10992
11070
|
// src/storage/prompt-usage-store.ts
|
|
10993
|
-
import * as
|
|
10994
|
-
import * as
|
|
11071
|
+
import * as fs16 from "node:fs/promises";
|
|
11072
|
+
import * as path22 from "node:path";
|
|
10995
11073
|
var PromptUsageStore = class {
|
|
10996
11074
|
constructor(file) {
|
|
10997
11075
|
this.file = file;
|
|
@@ -10999,7 +11077,7 @@ var PromptUsageStore = class {
|
|
|
10999
11077
|
file;
|
|
11000
11078
|
async load() {
|
|
11001
11079
|
try {
|
|
11002
|
-
const raw = JSON.parse(await
|
|
11080
|
+
const raw = JSON.parse(await fs16.readFile(this.file, "utf8"));
|
|
11003
11081
|
if (raw && typeof raw === "object" && raw.usage && typeof raw.usage === "object") {
|
|
11004
11082
|
return raw.usage;
|
|
11005
11083
|
}
|
|
@@ -11012,7 +11090,7 @@ var PromptUsageStore = class {
|
|
|
11012
11090
|
const prev = usage[slug];
|
|
11013
11091
|
const next = { count: (prev?.count ?? 0) + 1, lastUsedAt: at };
|
|
11014
11092
|
usage[slug] = next;
|
|
11015
|
-
await ensureDir(
|
|
11093
|
+
await ensureDir(path22.dirname(this.file));
|
|
11016
11094
|
await atomicWrite(this.file, JSON.stringify({ version: 1, usage }, null, 2));
|
|
11017
11095
|
return next;
|
|
11018
11096
|
}
|
|
@@ -11036,8 +11114,8 @@ var PromptUsageStore = class {
|
|
|
11036
11114
|
};
|
|
11037
11115
|
|
|
11038
11116
|
// src/storage/input-history-store.ts
|
|
11039
|
-
import * as
|
|
11040
|
-
import * as
|
|
11117
|
+
import * as fs17 from "node:fs/promises";
|
|
11118
|
+
import * as path23 from "node:path";
|
|
11041
11119
|
var INPUT_HISTORY_DEFAULT_MAX = 100;
|
|
11042
11120
|
var InputHistoryStore = class {
|
|
11043
11121
|
/**
|
|
@@ -11059,7 +11137,7 @@ var InputHistoryStore = class {
|
|
|
11059
11137
|
*/
|
|
11060
11138
|
async load() {
|
|
11061
11139
|
try {
|
|
11062
|
-
const raw = JSON.parse(await
|
|
11140
|
+
const raw = JSON.parse(await fs17.readFile(this.file, "utf8"));
|
|
11063
11141
|
if (raw && typeof raw === "object" && Array.isArray(raw.entries) && raw.entries.every((e) => typeof e === "string")) {
|
|
11064
11142
|
return raw.entries.slice(0, this.maxEntries);
|
|
11065
11143
|
}
|
|
@@ -11076,7 +11154,7 @@ var InputHistoryStore = class {
|
|
|
11076
11154
|
*/
|
|
11077
11155
|
async save(entries) {
|
|
11078
11156
|
const cleaned = this.scrubAndFilter(entries);
|
|
11079
|
-
await ensureDir(
|
|
11157
|
+
await ensureDir(path23.dirname(this.file));
|
|
11080
11158
|
const payload = {
|
|
11081
11159
|
version: 1,
|
|
11082
11160
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -11086,7 +11164,7 @@ var InputHistoryStore = class {
|
|
|
11086
11164
|
}
|
|
11087
11165
|
/** Truncate the file to an empty entry list (used by /clear). */
|
|
11088
11166
|
async clear() {
|
|
11089
|
-
await ensureDir(
|
|
11167
|
+
await ensureDir(path23.dirname(this.file));
|
|
11090
11168
|
const payload = { version: 1, updatedAt: (/* @__PURE__ */ new Date()).toISOString(), entries: [] };
|
|
11091
11169
|
await atomicWrite(this.file, JSON.stringify(payload, null, 2));
|
|
11092
11170
|
}
|
|
@@ -11116,8 +11194,8 @@ var InputHistoryStore = class {
|
|
|
11116
11194
|
};
|
|
11117
11195
|
|
|
11118
11196
|
// src/storage/cloud-sync.ts
|
|
11119
|
-
import * as
|
|
11120
|
-
import * as
|
|
11197
|
+
import * as fs18 from "node:fs/promises";
|
|
11198
|
+
import * as path24 from "node:path";
|
|
11121
11199
|
import { createHash as createHash8 } from "node:crypto";
|
|
11122
11200
|
var ALL_SYNC_CATEGORIES = ["settings", "skills", "prompts", "memory", "history"];
|
|
11123
11201
|
var CloudSync = class {
|
|
@@ -11125,7 +11203,7 @@ var CloudSync = class {
|
|
|
11125
11203
|
this.paths = paths;
|
|
11126
11204
|
this.getConfig = getConfig;
|
|
11127
11205
|
this.setConfig = setConfig;
|
|
11128
|
-
this.statePath =
|
|
11206
|
+
this.statePath = path24.join(paths.globalRoot, "sync-state.json");
|
|
11129
11207
|
}
|
|
11130
11208
|
paths;
|
|
11131
11209
|
getConfig;
|
|
@@ -11200,7 +11278,7 @@ var CloudSync = class {
|
|
|
11200
11278
|
lastSyncedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
11201
11279
|
localRev: rev
|
|
11202
11280
|
};
|
|
11203
|
-
await
|
|
11281
|
+
await fs18.writeFile(this.statePath, JSON.stringify(syncState, null, 2), "utf8");
|
|
11204
11282
|
this.state = syncState;
|
|
11205
11283
|
return {
|
|
11206
11284
|
ok: true,
|
|
@@ -11232,8 +11310,8 @@ var CloudSync = class {
|
|
|
11232
11310
|
const rel = segments.slice(2).join("/");
|
|
11233
11311
|
const destPath = resolvePulledCategoryPath(cat, localPath, rel, entry.path);
|
|
11234
11312
|
const blobData = await this.getBlob(token, owner, repoName, entry.sha);
|
|
11235
|
-
await
|
|
11236
|
-
await
|
|
11313
|
+
await fs18.mkdir(path24.dirname(destPath), { recursive: true });
|
|
11314
|
+
await fs18.writeFile(destPath, Buffer.from(blobData, "base64"));
|
|
11237
11315
|
}
|
|
11238
11316
|
const localRev = await this.hashLocalCategories(cfg.categories);
|
|
11239
11317
|
const syncState = {
|
|
@@ -11242,7 +11320,7 @@ var CloudSync = class {
|
|
|
11242
11320
|
lastSyncedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
11243
11321
|
localRev
|
|
11244
11322
|
};
|
|
11245
|
-
await
|
|
11323
|
+
await fs18.writeFile(this.statePath, JSON.stringify(syncState, null, 2), "utf8");
|
|
11246
11324
|
this.state = syncState;
|
|
11247
11325
|
return {
|
|
11248
11326
|
ok: true,
|
|
@@ -11261,7 +11339,7 @@ var CloudSync = class {
|
|
|
11261
11339
|
}
|
|
11262
11340
|
async loadState() {
|
|
11263
11341
|
try {
|
|
11264
|
-
const raw = await
|
|
11342
|
+
const raw = await fs18.readFile(this.statePath, "utf8");
|
|
11265
11343
|
this.state = JSON.parse(raw);
|
|
11266
11344
|
} catch {
|
|
11267
11345
|
this.state = null;
|
|
@@ -11339,17 +11417,17 @@ var CloudSync = class {
|
|
|
11339
11417
|
const localPath = this.categoryToPath(cat);
|
|
11340
11418
|
if (!localPath) continue;
|
|
11341
11419
|
try {
|
|
11342
|
-
const stat13 = await
|
|
11420
|
+
const stat13 = await fs18.stat(localPath);
|
|
11343
11421
|
if (stat13.isDirectory()) {
|
|
11344
11422
|
const files = await this.walkDir(localPath, localPath);
|
|
11345
11423
|
for (const file of files) {
|
|
11346
|
-
const content = await
|
|
11347
|
-
const rel =
|
|
11424
|
+
const content = await fs18.readFile(file, "utf8");
|
|
11425
|
+
const rel = path24.relative(localPath, file).replace(/\\/g, "/");
|
|
11348
11426
|
entries.push({ path: `data/${cat}/${rel}`, content, mode: "100644" });
|
|
11349
11427
|
hashes.push(content);
|
|
11350
11428
|
}
|
|
11351
11429
|
} else {
|
|
11352
|
-
const content = await
|
|
11430
|
+
const content = await fs18.readFile(localPath, "utf8");
|
|
11353
11431
|
entries.push({ path: `data/${cat}`, content, mode: "100644" });
|
|
11354
11432
|
hashes.push(content);
|
|
11355
11433
|
}
|
|
@@ -11365,15 +11443,15 @@ var CloudSync = class {
|
|
|
11365
11443
|
const localPath = this.categoryToPath(cat);
|
|
11366
11444
|
if (!localPath) continue;
|
|
11367
11445
|
try {
|
|
11368
|
-
const stat13 = await
|
|
11446
|
+
const stat13 = await fs18.stat(localPath);
|
|
11369
11447
|
if (stat13.isDirectory()) {
|
|
11370
11448
|
const files = await this.walkDir(localPath, localPath);
|
|
11371
11449
|
for (const file of files) {
|
|
11372
|
-
const content = await
|
|
11450
|
+
const content = await fs18.readFile(file);
|
|
11373
11451
|
hashes.push(content.toString("base64") + file);
|
|
11374
11452
|
}
|
|
11375
11453
|
} else {
|
|
11376
|
-
const content = await
|
|
11454
|
+
const content = await fs18.readFile(localPath);
|
|
11377
11455
|
hashes.push(content.toString("base64") + localPath);
|
|
11378
11456
|
}
|
|
11379
11457
|
} catch {
|
|
@@ -11400,9 +11478,9 @@ var CloudSync = class {
|
|
|
11400
11478
|
}
|
|
11401
11479
|
async walkDir(dir, base) {
|
|
11402
11480
|
const results = [];
|
|
11403
|
-
const entries = await
|
|
11481
|
+
const entries = await fs18.readdir(dir, { withFileTypes: true });
|
|
11404
11482
|
for (const entry of entries) {
|
|
11405
|
-
const full =
|
|
11483
|
+
const full = path24.join(dir, entry.name);
|
|
11406
11484
|
if (entry.isDirectory()) {
|
|
11407
11485
|
results.push(...await this.walkDir(full, base));
|
|
11408
11486
|
} else {
|
|
@@ -11424,9 +11502,9 @@ function resolvePulledCategoryPath(cat, localPath, rel, remotePath) {
|
|
|
11424
11502
|
return localPath;
|
|
11425
11503
|
}
|
|
11426
11504
|
if (!rel) return localPath;
|
|
11427
|
-
const normalizedRel =
|
|
11428
|
-
const traversesUp = normalizedRel === ".." || normalizedRel.startsWith(`..${
|
|
11429
|
-
if (
|
|
11505
|
+
const normalizedRel = path24.normalize(rel);
|
|
11506
|
+
const traversesUp = normalizedRel === ".." || normalizedRel.startsWith(`..${path24.sep}`);
|
|
11507
|
+
if (path24.isAbsolute(normalizedRel) || traversesUp) {
|
|
11430
11508
|
throw new FsError({
|
|
11431
11509
|
message: `Refusing CloudSync path traversal: ${remotePath}`,
|
|
11432
11510
|
code: ERROR_CODES.FS_DELETE_FAILED,
|
|
@@ -11434,10 +11512,10 @@ function resolvePulledCategoryPath(cat, localPath, rel, remotePath) {
|
|
|
11434
11512
|
context: { reason: "path_traversal", normalizedRel }
|
|
11435
11513
|
});
|
|
11436
11514
|
}
|
|
11437
|
-
const dest =
|
|
11438
|
-
const root =
|
|
11439
|
-
const
|
|
11440
|
-
if (
|
|
11515
|
+
const dest = path24.resolve(localPath, normalizedRel);
|
|
11516
|
+
const root = path24.resolve(localPath);
|
|
11517
|
+
const relative7 = path24.relative(root, dest);
|
|
11518
|
+
if (relative7.startsWith("..") || path24.isAbsolute(relative7)) {
|
|
11441
11519
|
throw new FsError({
|
|
11442
11520
|
message: `Refusing CloudSync path outside category root: ${remotePath}`,
|
|
11443
11521
|
code: ERROR_CODES.FS_DELETE_FAILED,
|