@sayknow-cli/coding-agent 0.5.2 → 0.5.8
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/CHANGELOG.md +66 -16
- package/dist/types/cli/update-cli.d.ts +8 -1
- package/dist/types/config/atomic-yaml-patch.d.ts +20 -1
- package/dist/types/config/keybindings.d.ts +10 -0
- package/dist/types/config/model-registry.d.ts +1 -1
- package/dist/types/config/models-config-schema.d.ts +4 -0
- package/dist/types/config/settings-schema.d.ts +73 -0
- package/dist/types/dap/client.d.ts +1 -0
- package/dist/types/lsp/client.d.ts +5 -0
- package/dist/types/modes/components/welcome.d.ts +1 -0
- package/dist/types/modes/controllers/selector-controller.d.ts +3 -1
- package/dist/types/modes/interactive-mode.d.ts +13 -1
- package/dist/types/modes/types.d.ts +7 -1
- package/dist/types/runtime-mcp/transports/stdio.d.ts +2 -0
- package/dist/types/session/agent-session.d.ts +8 -0
- package/dist/types/session/internal/managed-session-scope.d.ts +3 -1
- package/dist/types/session/internal/managed-session-storage.d.ts +2 -0
- package/dist/types/session/session-manager.d.ts +9 -0
- package/dist/types/session/session-storage.d.ts +17 -0
- package/dist/types/session-import/claude.d.ts +26 -0
- package/dist/types/session-import/codex.d.ts +22 -0
- package/dist/types/session-import/command.d.ts +20 -0
- package/dist/types/session-import/detect.d.ts +22 -0
- package/dist/types/session-import/index.d.ts +7 -0
- package/dist/types/session-import/redact.d.ts +23 -0
- package/dist/types/session-import/service.d.ts +28 -0
- package/dist/types/session-import/types.d.ts +125 -0
- package/dist/types/skc-runtime/launch-worktree.d.ts +17 -1
- package/dist/types/slash-commands/builtin-registry.d.ts +4 -0
- package/dist/types/slash-commands/types.d.ts +5 -0
- package/dist/types/system-prompt.d.ts +5 -0
- package/dist/types/tools/ask.d.ts +5 -0
- package/dist/types/utils/mupdf-wasm.d.ts +1 -0
- package/package.json +13 -7
- package/scripts/compile-args.ts +8 -4
- package/scripts/generate-sdk-operation-inventory.ts +5 -6
- package/src/cli/config-cli.ts +23 -8
- package/src/cli/update-cli.ts +265 -21
- package/src/config/atomic-yaml-patch.ts +167 -30
- package/src/config/keybindings.ts +10 -0
- package/src/config/model-profiles.ts +84 -16
- package/src/config/model-registry.ts +60 -38
- package/src/config/models-config-schema.ts +1 -1
- package/src/config/settings-schema.ts +79 -0
- package/src/config/settings.ts +13 -2
- package/src/dap/client.ts +78 -30
- package/src/internal-urls/docs-index.generated.ts +5 -4
- package/src/lsp/client.ts +17 -29
- package/src/modes/action-registry.ts +2 -0
- package/src/modes/components/welcome.ts +5 -0
- package/src/modes/controllers/goal-mode-controller.ts +7 -2
- package/src/modes/controllers/input-controller.ts +71 -6
- package/src/modes/controllers/plan-mode-controller.ts +8 -1
- package/src/modes/controllers/runtime-mcp-command-controller.ts +14 -0
- package/src/modes/controllers/selector-controller.ts +47 -19
- package/src/modes/interactive-mode.ts +54 -4
- package/src/modes/types.ts +5 -1
- package/src/prompts/system/system-prompt.md +8 -0
- package/src/runtime-mcp/transports/stdio.ts +96 -33
- package/src/sdk/broker/lifecycle.ts +6 -5
- package/src/sdk/protocol/operation-inventory.generated.json +33 -0
- package/src/sdk/session.ts +27 -18
- package/src/session/agent-session.ts +155 -15
- package/src/session/internal/managed-session-scope.ts +41 -6
- package/src/session/internal/managed-session-storage.ts +14 -0
- package/src/session/session-manager.ts +113 -11
- package/src/session/session-storage.ts +70 -0
- package/src/session-import/claude.ts +382 -0
- package/src/session-import/codex.ts +458 -0
- package/src/session-import/command.ts +61 -0
- package/src/session-import/detect.ts +133 -0
- package/src/session-import/index.ts +27 -0
- package/src/session-import/redact.ts +125 -0
- package/src/session-import/service.ts +749 -0
- package/src/session-import/types.ts +163 -0
- package/src/skc-runtime/launch-worktree.ts +277 -4
- package/src/slash-commands/acp-builtins.ts +5 -1
- package/src/slash-commands/builtin-registry.ts +100 -1
- package/src/slash-commands/types.ts +5 -0
- package/src/system-prompt.ts +7 -0
- package/src/tools/ask.ts +5 -0
- package/src/tools/fetch.ts +7 -2
- package/src/utils/markit.ts +24 -2
- package/src/utils/mupdf-wasm.ts +26 -0
- package/src/export/html/template.css +0 -1060
- package/src/export/html/template.html +0 -47
- package/src/export/html/template.js +0 -2348
- package/vendor/insane-search/engine/tests/test_hardening.py +0 -57
- package/vendor/insane-search/engine/tests/test_smoke.py +0 -152
- package/vendor/insane-search/engine/tests/test_u1.py +0 -200
- package/vendor/insane-search/engine/tests/test_u4.py +0 -131
- package/vendor/insane-search/engine/tests/test_u5.py +0 -163
- package/vendor/insane-search/engine/tests/test_u7.py +0 -124
- package/vendor/insane-search/engine/tests/test_u8.py +0 -216
|
@@ -4350,6 +4350,7 @@ export class SessionManager {
|
|
|
4350
4350
|
#sessionName: string | undefined;
|
|
4351
4351
|
#titleSource: "auto" | "user" | undefined;
|
|
4352
4352
|
#sessionFile: string | undefined;
|
|
4353
|
+
#transcriptStatFailureReported = false;
|
|
4353
4354
|
#flushed: boolean = false;
|
|
4354
4355
|
#needsFullRewriteOnNextPersist: boolean = false;
|
|
4355
4356
|
#ensuredOnDisk: boolean = false;
|
|
@@ -6573,6 +6574,28 @@ export class SessionManager {
|
|
|
6573
6574
|
getSessionFile(): string | undefined {
|
|
6574
6575
|
return this.#sessionFile;
|
|
6575
6576
|
}
|
|
6577
|
+
/**
|
|
6578
|
+
* Managed on-disk transcript size in bytes. Returns undefined when the
|
|
6579
|
+
* destination is explicit, the file does not exist yet, or its identity
|
|
6580
|
+
* cannot be verified.
|
|
6581
|
+
*/
|
|
6582
|
+
getTranscriptFileBytes(): number | undefined {
|
|
6583
|
+
if (this.destination.kind !== "managed" || !this.#sessionFile) return undefined;
|
|
6584
|
+
try {
|
|
6585
|
+
const size = this.#managedTranscriptStore().sizeSync(path.basename(this.#sessionFile));
|
|
6586
|
+
this.#transcriptStatFailureReported = false;
|
|
6587
|
+
return size ?? undefined;
|
|
6588
|
+
} catch (error) {
|
|
6589
|
+
if (!this.#transcriptStatFailureReported) {
|
|
6590
|
+
this.#transcriptStatFailureReported = true;
|
|
6591
|
+
logger.warn("Managed transcript size sampling failed", {
|
|
6592
|
+
sessionFile: this.#sessionFile,
|
|
6593
|
+
error: toError(error).message,
|
|
6594
|
+
});
|
|
6595
|
+
}
|
|
6596
|
+
return undefined;
|
|
6597
|
+
}
|
|
6598
|
+
}
|
|
6576
6599
|
|
|
6577
6600
|
acquireMemoryGuardParticipantIngressLease(): MemoryGuardParticipantIngressLease {
|
|
6578
6601
|
if (this.#memoryGuardParticipantIngressToken)
|
|
@@ -7093,7 +7116,17 @@ export class SessionManager {
|
|
|
7093
7116
|
new Map(),
|
|
7094
7117
|
);
|
|
7095
7118
|
const persistedEntry = prepareEntryForPersistenceSync(materializedEntry, this.#blobStore);
|
|
7096
|
-
|
|
7119
|
+
try {
|
|
7120
|
+
this.#appendManagedRecordSync(persistedEntry);
|
|
7121
|
+
} catch (appendError) {
|
|
7122
|
+
// A managed append-only transcript can hit the per-file limit
|
|
7123
|
+
// even after compaction reduced the live entries. Only the
|
|
7124
|
+
// authority-bound append error may authorize a full rewrite.
|
|
7125
|
+
if (!(appendError instanceof Error) || appendError.message !== "content_too_large") {
|
|
7126
|
+
throw appendError;
|
|
7127
|
+
}
|
|
7128
|
+
this.#rewriteFileSync();
|
|
7129
|
+
}
|
|
7097
7130
|
return;
|
|
7098
7131
|
}
|
|
7099
7132
|
const writer = this.#ensurePersistWriter();
|
|
@@ -7119,16 +7152,29 @@ export class SessionManager {
|
|
|
7119
7152
|
}
|
|
7120
7153
|
|
|
7121
7154
|
#appendEntry(entry: SessionEntry): void {
|
|
7155
|
+
if (this.#persistError) throw this.#persistError;
|
|
7122
7156
|
this.#assertRecoveryHydrationWritable();
|
|
7123
7157
|
const normalizedEntry = normalizeSessionEntryForStorage(entry);
|
|
7124
7158
|
const residentEntry = prepareEntryForResidentSync(normalizedEntry, this.#residentBlobStores()) as SessionEntry;
|
|
7159
|
+
const previousLeafId = this.#leafId;
|
|
7125
7160
|
this.#fileEntries.push(residentEntry);
|
|
7126
7161
|
this.#byId.set(residentEntry.id, residentEntry);
|
|
7127
7162
|
this.#leafId = residentEntry.id;
|
|
7128
7163
|
this.#bumpEntryRevision();
|
|
7129
7164
|
this.#leafRevision++;
|
|
7130
7165
|
if (entry.type === "label") this.#labelRevision++;
|
|
7131
|
-
|
|
7166
|
+
try {
|
|
7167
|
+
this._persist(residentEntry);
|
|
7168
|
+
} catch (error) {
|
|
7169
|
+
this.#fileEntries.pop();
|
|
7170
|
+
this.#byId.delete(residentEntry.id);
|
|
7171
|
+
this.#leafId = previousLeafId;
|
|
7172
|
+
this.#entryRevision--;
|
|
7173
|
+
this.#leafRevision--;
|
|
7174
|
+
if (entry.type === "label") this.#labelRevision--;
|
|
7175
|
+
this.#resetMaterializedCaches();
|
|
7176
|
+
throw error;
|
|
7177
|
+
}
|
|
7132
7178
|
// Same validated, overflow-guarded aggregation as the resume path (#buildIndex): a
|
|
7133
7179
|
// malformed task-result or assistant usage shape reaching the append path must not
|
|
7134
7180
|
// crash or poison getUsageStatistics() either.
|
|
@@ -9395,7 +9441,14 @@ export class SessionManager {
|
|
|
9395
9441
|
*/
|
|
9396
9442
|
static inventorySessionsStrict(
|
|
9397
9443
|
cwd: string,
|
|
9398
|
-
options?: {
|
|
9444
|
+
options?: {
|
|
9445
|
+
sessionDir?: string;
|
|
9446
|
+
storage?: SessionStorage;
|
|
9447
|
+
destination?: SessionDestination;
|
|
9448
|
+
maxCandidates?: number;
|
|
9449
|
+
maxTotalBytes?: number;
|
|
9450
|
+
filterOtherWorkspaces?: boolean;
|
|
9451
|
+
},
|
|
9399
9452
|
): StrictInventoryResult {
|
|
9400
9453
|
const storage = options?.storage ?? new FileSessionStorage();
|
|
9401
9454
|
const destination = options?.destination;
|
|
@@ -9419,20 +9472,27 @@ export class SessionManager {
|
|
|
9419
9472
|
});
|
|
9420
9473
|
if (resolved.kind === "error")
|
|
9421
9474
|
return { kind: "failure", failures: [{ kind: "root", message: resolved.message, path: dir }] };
|
|
9422
|
-
const listing = listManagedCandidates(resolved.scope);
|
|
9475
|
+
const listing = listManagedCandidates(resolved.scope, { maxCandidates: options?.maxCandidates });
|
|
9423
9476
|
if (listing.kind === "error")
|
|
9424
9477
|
return { kind: "failure", failures: [{ kind: "scan", message: listing.message, path: dir }] };
|
|
9425
9478
|
const failures: StrictInventoryFailure[] = [];
|
|
9426
9479
|
const candidates: StrictInventoryCandidate[] = [];
|
|
9480
|
+
let remainingBytes = options?.maxTotalBytes;
|
|
9427
9481
|
for (const managedCandidate of listing.owned) {
|
|
9428
9482
|
const candidate = inventoryReadCandidate(
|
|
9429
9483
|
storage,
|
|
9430
9484
|
managedCandidate.path,
|
|
9431
9485
|
cwd,
|
|
9432
9486
|
path.dirname(managedCandidate.path),
|
|
9487
|
+
remainingBytes,
|
|
9488
|
+
options?.filterOtherWorkspaces,
|
|
9433
9489
|
);
|
|
9434
9490
|
if ("failures" in candidate) failures.push(...candidate.failures);
|
|
9435
|
-
else
|
|
9491
|
+
else {
|
|
9492
|
+
if (remainingBytes !== undefined) remainingBytes -= candidate.bytes;
|
|
9493
|
+
if ("filtered" in candidate) continue;
|
|
9494
|
+
candidates.push(candidate.candidate);
|
|
9495
|
+
}
|
|
9436
9496
|
}
|
|
9437
9497
|
assertManagedDirectoryRoot(destination.securityContext.rootAuthority);
|
|
9438
9498
|
store.assertBound();
|
|
@@ -9450,11 +9510,27 @@ export class SessionManager {
|
|
|
9450
9510
|
failures: [{ kind: "scan", message: "Strict scoped session scan is unavailable", path: dir }],
|
|
9451
9511
|
};
|
|
9452
9512
|
}
|
|
9453
|
-
const strictScan = storage.listFilesStrictSync.bind(storage);
|
|
9454
9513
|
|
|
9455
9514
|
let files: string[];
|
|
9456
9515
|
try {
|
|
9457
|
-
|
|
9516
|
+
if (options?.maxCandidates !== undefined) {
|
|
9517
|
+
if (!storage.listFilesStrictBoundedSync) {
|
|
9518
|
+
return {
|
|
9519
|
+
kind: "failure",
|
|
9520
|
+
failures: [{ kind: "scan", message: "Bounded strict session scan is unavailable", path: dir }],
|
|
9521
|
+
};
|
|
9522
|
+
}
|
|
9523
|
+
const bounded = storage.listFilesStrictBoundedSync(dir, "*.jsonl", options.maxCandidates);
|
|
9524
|
+
if (bounded.truncated) {
|
|
9525
|
+
return {
|
|
9526
|
+
kind: "failure",
|
|
9527
|
+
failures: [{ kind: "scan", message: "Strict session candidate limit exceeded", path: dir }],
|
|
9528
|
+
};
|
|
9529
|
+
}
|
|
9530
|
+
files = bounded.files;
|
|
9531
|
+
} else {
|
|
9532
|
+
files = storage.listFilesStrictSync(dir, "*.jsonl");
|
|
9533
|
+
}
|
|
9458
9534
|
} catch (err) {
|
|
9459
9535
|
const code = (err as NodeJS.ErrnoException)?.code;
|
|
9460
9536
|
// Only a confirmed ENOENT proves the scoped root is genuinely absent,
|
|
@@ -9487,12 +9563,22 @@ export class SessionManager {
|
|
|
9487
9563
|
|
|
9488
9564
|
const failures: StrictInventoryFailure[] = [];
|
|
9489
9565
|
const candidates: StrictInventoryCandidate[] = [];
|
|
9566
|
+
let remainingBytes = options?.maxTotalBytes;
|
|
9490
9567
|
for (const file of files) {
|
|
9491
|
-
const candidate = inventoryReadCandidate(
|
|
9568
|
+
const candidate = inventoryReadCandidate(
|
|
9569
|
+
storage,
|
|
9570
|
+
file,
|
|
9571
|
+
cwd,
|
|
9572
|
+
dir,
|
|
9573
|
+
remainingBytes,
|
|
9574
|
+
options?.filterOtherWorkspaces,
|
|
9575
|
+
);
|
|
9492
9576
|
if ("failures" in candidate) {
|
|
9493
9577
|
failures.push(...candidate.failures);
|
|
9494
9578
|
continue;
|
|
9495
9579
|
}
|
|
9580
|
+
if (remainingBytes !== undefined) remainingBytes -= candidate.bytes;
|
|
9581
|
+
if ("filtered" in candidate) continue;
|
|
9496
9582
|
candidates.push(candidate.candidate);
|
|
9497
9583
|
}
|
|
9498
9584
|
if (failures.length > 0) {
|
|
@@ -9521,7 +9607,12 @@ function inventoryReadCandidate(
|
|
|
9521
9607
|
file: string,
|
|
9522
9608
|
expectedCwd: string,
|
|
9523
9609
|
sessionDir: string,
|
|
9524
|
-
|
|
9610
|
+
maxBytes?: number,
|
|
9611
|
+
filterOtherWorkspaces = false,
|
|
9612
|
+
):
|
|
9613
|
+
| { candidate: StrictInventoryCandidate; bytes: number }
|
|
9614
|
+
| { filtered: true; bytes: number }
|
|
9615
|
+
| { failures: StrictInventoryFailure[] } {
|
|
9525
9616
|
const failures: StrictInventoryFailure[] = [];
|
|
9526
9617
|
const resolvedFile = path.resolve(file);
|
|
9527
9618
|
if (!pathIsWithin(path.resolve(sessionDir), resolvedFile)) {
|
|
@@ -9534,7 +9625,14 @@ function inventoryReadCandidate(
|
|
|
9534
9625
|
}
|
|
9535
9626
|
let snapshot: SessionStorageSnapshot;
|
|
9536
9627
|
try {
|
|
9537
|
-
|
|
9628
|
+
if (maxBytes !== undefined) {
|
|
9629
|
+
if (maxBytes < 0 || !storage.readSnapshotBoundedSync) {
|
|
9630
|
+
return { failures: [{ kind: "read", message: "Bounded exact session read is unavailable", path: file }] };
|
|
9631
|
+
}
|
|
9632
|
+
snapshot = storage.readSnapshotBoundedSync(file, maxBytes);
|
|
9633
|
+
} else {
|
|
9634
|
+
snapshot = storage.readSnapshotSync(file);
|
|
9635
|
+
}
|
|
9538
9636
|
} catch (err) {
|
|
9539
9637
|
const code = (err as NodeJS.ErrnoException)?.code;
|
|
9540
9638
|
if (code === "ELOOP" || code === "SYMLINK") {
|
|
@@ -9576,8 +9674,12 @@ function inventoryReadCandidate(
|
|
|
9576
9674
|
}
|
|
9577
9675
|
const canonicalHeaderCwd = resolveEquivalentPath(header.cwd);
|
|
9578
9676
|
if (canonicalHeaderCwd !== resolveEquivalentPath(expectedCwd)) {
|
|
9677
|
+
if (filterOtherWorkspaces) return { filtered: true, bytes: snapshot.bytes.byteLength };
|
|
9579
9678
|
failures.push({ kind: "cwd", message: "Candidate cwd does not match the scoped workspace", path: file });
|
|
9580
9679
|
return { failures };
|
|
9581
9680
|
}
|
|
9582
|
-
return {
|
|
9681
|
+
return {
|
|
9682
|
+
candidate: { path: resolvedFile, id: header.id, cwd: header.cwd, identity: snapshot.stat },
|
|
9683
|
+
bytes: snapshot.bytes.byteLength,
|
|
9684
|
+
};
|
|
9583
9685
|
}
|
|
@@ -165,6 +165,8 @@ export interface SessionStorage {
|
|
|
165
165
|
readBytesSync?(path: string): Uint8Array;
|
|
166
166
|
/** Exact bytes and descriptor-bound identity captured from one opened regular file. */
|
|
167
167
|
readSnapshotSync?(path: string): SessionStorageSnapshot;
|
|
168
|
+
/** Exact descriptor-bound bytes, refusing growth or allocation beyond maxBytes. */
|
|
169
|
+
readSnapshotBoundedSync?(path: string, maxBytes: number): SessionStorageSnapshot;
|
|
168
170
|
statSync(path: string): SessionStorageStat;
|
|
169
171
|
listFilesSync(dir: string, pattern: string): string[];
|
|
170
172
|
/** List matching files with mtimes without issuing one JavaScript stat call per path. */
|
|
@@ -174,6 +176,8 @@ export interface SessionStorage {
|
|
|
174
176
|
* authorization inventory; the forgiving {@link listFilesSync} stays display-only.
|
|
175
177
|
*/
|
|
176
178
|
listFilesStrictSync?(dir: string, pattern: string): string[];
|
|
179
|
+
/** Strict bounded scan; `truncated` means completeness was not granted. */
|
|
180
|
+
listFilesStrictBoundedSync?(dir: string, pattern: string, maxFiles: number): { files: string[]; truncated: boolean };
|
|
177
181
|
|
|
178
182
|
exists(path: string): Promise<boolean>;
|
|
179
183
|
readText(path: string): Promise<string>;
|
|
@@ -696,6 +700,39 @@ export class FileSessionStorage implements SessionStorage {
|
|
|
696
700
|
}
|
|
697
701
|
}
|
|
698
702
|
|
|
703
|
+
readSnapshotBoundedSync(fpath: string, maxBytes: number): SessionStorageSnapshot {
|
|
704
|
+
const flags = fs.constants.O_RDONLY | fs.constants.O_NONBLOCK | (fs.constants.O_NOFOLLOW ?? 0);
|
|
705
|
+
const fd = fs.openSync(fpath, flags);
|
|
706
|
+
try {
|
|
707
|
+
const initial = statFromNode(fs.fstatSync(fd, { bigint: true }));
|
|
708
|
+
if (!initial.isFile) throw new Error(`Not a regular file: ${fpath}`);
|
|
709
|
+
if (initial.size > maxBytes) throw new Error("Snapshot exceeds the strict byte limit.");
|
|
710
|
+
const chunks: Buffer[] = [];
|
|
711
|
+
let total = 0;
|
|
712
|
+
while (total <= maxBytes) {
|
|
713
|
+
const chunk = Buffer.allocUnsafe(Math.min(1024 * 1024, maxBytes - total + 1));
|
|
714
|
+
const bytesRead = fs.readSync(fd, chunk, 0, chunk.byteLength, null);
|
|
715
|
+
if (bytesRead === 0) break;
|
|
716
|
+
total += bytesRead;
|
|
717
|
+
if (total > maxBytes) throw new Error("Snapshot exceeds the strict byte limit.");
|
|
718
|
+
chunks.push(chunk.subarray(0, bytesRead));
|
|
719
|
+
}
|
|
720
|
+
const terminal = statFromNode(fs.fstatSync(fd, { bigint: true }));
|
|
721
|
+
if (
|
|
722
|
+
initial.dev !== terminal.dev ||
|
|
723
|
+
initial.ino !== terminal.ino ||
|
|
724
|
+
initial.size !== terminal.size ||
|
|
725
|
+
initial.mtimeNs !== terminal.mtimeNs ||
|
|
726
|
+
total !== terminal.size
|
|
727
|
+
) {
|
|
728
|
+
throw new Error("Snapshot changed during the bounded read.");
|
|
729
|
+
}
|
|
730
|
+
return { bytes: Buffer.concat(chunks, total), stat: terminal };
|
|
731
|
+
} finally {
|
|
732
|
+
fs.closeSync(fd);
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
|
|
699
736
|
statSync(path: string): SessionStorageStat {
|
|
700
737
|
return statFromNode(fs.statSync(path, { bigint: true }));
|
|
701
738
|
}
|
|
@@ -729,6 +766,26 @@ export class FileSessionStorage implements SessionStorage {
|
|
|
729
766
|
return Array.from(new Bun.Glob(pattern).scanSync(dir)).map(name => path.join(dir, name));
|
|
730
767
|
}
|
|
731
768
|
|
|
769
|
+
listFilesStrictBoundedSync(dir: string, pattern: string, maxFiles: number): { files: string[]; truncated: boolean } {
|
|
770
|
+
const handle = fs.opendirSync(dir);
|
|
771
|
+
const files: string[] = [];
|
|
772
|
+
let entries = 0;
|
|
773
|
+
try {
|
|
774
|
+
for (;;) {
|
|
775
|
+
const entry = handle.readSync();
|
|
776
|
+
if (!entry) break;
|
|
777
|
+
entries++;
|
|
778
|
+
if (entries > maxFiles * 4 + 128) return { files, truncated: true };
|
|
779
|
+
if (!matchesPattern(entry.name, pattern)) continue;
|
|
780
|
+
files.push(path.join(dir, entry.name));
|
|
781
|
+
if (files.length > maxFiles) return { files, truncated: true };
|
|
782
|
+
}
|
|
783
|
+
return { files, truncated: false };
|
|
784
|
+
} finally {
|
|
785
|
+
handle.closeSync();
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
|
|
732
789
|
async exists(path: string): Promise<boolean> {
|
|
733
790
|
try {
|
|
734
791
|
await fs.promises.access(path);
|
|
@@ -1469,6 +1526,12 @@ export class MemorySessionStorage implements SessionStorage {
|
|
|
1469
1526
|
return { bytes: Buffer.from(entry.content), stat: this.#statFor(entry) };
|
|
1470
1527
|
}
|
|
1471
1528
|
|
|
1529
|
+
readSnapshotBoundedSync(path: string, maxBytes: number): SessionStorageSnapshot {
|
|
1530
|
+
const snapshot = this.readSnapshotSync(path);
|
|
1531
|
+
if (snapshot.bytes.byteLength > maxBytes) throw new Error("Snapshot exceeds the strict byte limit.");
|
|
1532
|
+
return snapshot;
|
|
1533
|
+
}
|
|
1534
|
+
|
|
1472
1535
|
statSync(path: string): SessionStorageStat {
|
|
1473
1536
|
const entry = this.#files.get(path);
|
|
1474
1537
|
if (!entry) throw new Error(`File not found: ${path}`);
|
|
@@ -1498,6 +1561,13 @@ export class MemorySessionStorage implements SessionStorage {
|
|
|
1498
1561
|
return this.listFilesSync(dir, pattern);
|
|
1499
1562
|
}
|
|
1500
1563
|
|
|
1564
|
+
listFilesStrictBoundedSync(dir: string, pattern: string, maxFiles: number): { files: string[]; truncated: boolean } {
|
|
1565
|
+
const files = this.listFilesSync(dir, pattern);
|
|
1566
|
+
return files.length > maxFiles
|
|
1567
|
+
? { files: files.slice(0, maxFiles + 1), truncated: true }
|
|
1568
|
+
: { files, truncated: false };
|
|
1569
|
+
}
|
|
1570
|
+
|
|
1501
1571
|
exists(path: string): Promise<boolean> {
|
|
1502
1572
|
return Promise.resolve(this.existsSync(path));
|
|
1503
1573
|
}
|