@wrongstack/core 0.77.0 → 0.84.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.
Files changed (80) hide show
  1. package/dist/{agent-bridge-EWdqs8v6.d.ts → agent-bridge-C9P_HPez.d.ts} +2 -2
  2. package/dist/{agent-subagent-runner-D8qW8OSC.d.ts → agent-subagent-runner-2Aq0jOSj.d.ts} +107 -102
  3. package/dist/{compactor-D_ExJajC.d.ts → compactor-CJq7LQev.d.ts} +3 -3
  4. package/dist/{config-Dy0CK_o6.d.ts → config-_DZ7dN-T.d.ts} +77 -75
  5. package/dist/{context-y87Jc5ei.d.ts → context-ToHAp4-U.d.ts} +119 -90
  6. package/dist/coordination/index.d.ts +16 -16
  7. package/dist/coordination/index.js +318 -37
  8. package/dist/coordination/index.js.map +1 -1
  9. package/dist/defaults/index.d.ts +32 -32
  10. package/dist/defaults/index.js +433 -81
  11. package/dist/defaults/index.js.map +1 -1
  12. package/dist/{director-state-BmYi3DGA.d.ts → director-state-CgIc30qi.d.ts} +19 -19
  13. package/dist/{events-CYaoLN5_.d.ts → events-DnRqXaZ3.d.ts} +43 -42
  14. package/dist/execution/index.d.ts +53 -53
  15. package/dist/execution/index.js +72 -29
  16. package/dist/execution/index.js.map +1 -1
  17. package/dist/extension/index.d.ts +9 -9
  18. package/dist/extension/index.js +8 -1
  19. package/dist/extension/index.js.map +1 -1
  20. package/dist/{goal-store-C7jcumEh.d.ts → goal-store-DvWLNu52.d.ts} +4 -4
  21. package/dist/{index-DIxjTOga.d.ts → index-BNOLadHw.d.ts} +28 -28
  22. package/dist/{index-Dsda0uCn.d.ts → index-N0_c4bHQ.d.ts} +45 -45
  23. package/dist/index.d.ts +167 -167
  24. package/dist/index.js +617 -155
  25. package/dist/index.js.map +1 -1
  26. package/dist/infrastructure/index.d.ts +9 -9
  27. package/dist/infrastructure/index.js +13 -5
  28. package/dist/infrastructure/index.js.map +1 -1
  29. package/dist/kernel/index.d.ts +14 -14
  30. package/dist/kernel/index.js +7 -0
  31. package/dist/kernel/index.js.map +1 -1
  32. package/dist/logger-B72yyPc6.d.ts +12 -0
  33. package/dist/{logger-BppKxDqZ.d.ts → logger-C_27pj9i.d.ts} +6 -7
  34. package/dist/{mcp-servers-T0O6UN_w.d.ts → mcp-servers-Dck3T85_.d.ts} +20 -20
  35. package/dist/{mode-BO4SEUIv.d.ts → mode-CHo2XtHs.d.ts} +4 -4
  36. package/dist/models/index.d.ts +10 -10
  37. package/dist/models/index.js +8 -2
  38. package/dist/models/index.js.map +1 -1
  39. package/dist/{models-registry-BcYJDKLm.d.ts → models-registry-Be3osGt5.d.ts} +28 -28
  40. package/dist/{models-registry-Cuq1C8V9.d.ts → models-registry-Boz639EI.d.ts} +12 -12
  41. package/dist/{multi-agent-coordinator-DpbG3wiy.d.ts → multi-agent-coordinator-DllpCVkF.d.ts} +12 -12
  42. package/dist/{null-fleet-bus-u5ys3lW_.d.ts → null-fleet-bus-BY0AN-sr.d.ts} +121 -121
  43. package/dist/observability/index.d.ts +41 -41
  44. package/dist/observability/index.js.map +1 -1
  45. package/dist/{observability-BhnVLBLS.d.ts → observability-CoSNZdhX.d.ts} +4 -4
  46. package/dist/{parallel-eternal-engine-Dn0P8Pbj.d.ts → parallel-eternal-engine-D402RASp.d.ts} +49 -49
  47. package/dist/{path-resolver-B32v2JIq.d.ts → path-resolver-UPFTsDyD.d.ts} +6 -6
  48. package/dist/{permission-V5BLOrY6.d.ts → permission-14CChMmO.d.ts} +10 -8
  49. package/dist/{permission-policy-CBVx-d-8.d.ts → permission-policy-gW5htOo1.d.ts} +7 -7
  50. package/dist/{plan-templates-BcUwLlMQ.d.ts → plan-templates-DRvPgkfZ.d.ts} +65 -32
  51. package/dist/{provider-runner-CSi_7l0h.d.ts → provider-runner-COAJM8tC.d.ts} +6 -6
  52. package/dist/{retry-policy-CG3qvH_e.d.ts → retry-policy-DSu6O6rD.d.ts} +4 -4
  53. package/dist/sdd/index.d.ts +47 -47
  54. package/dist/sdd/index.js +47 -22
  55. package/dist/sdd/index.js.map +1 -1
  56. package/dist/{secret-scrubber-7rSC_emZ.d.ts → secret-scrubber-yGBFQYju.d.ts} +10 -2
  57. package/dist/security/index.d.ts +7 -7
  58. package/dist/security/index.js +15 -8
  59. package/dist/security/index.js.map +1 -1
  60. package/dist/{selector-RvBR_YRW.d.ts → selector-11-fm95U.d.ts} +2 -2
  61. package/dist/{session-event-bridge-CDHxcmQU.d.ts → session-event-bridge-D0u-x576.d.ts} +7 -7
  62. package/dist/{session-reader-BIpwM60D.d.ts → session-reader-BQU-toaN.d.ts} +23 -23
  63. package/dist/{skill-CxuWrsKK.d.ts → skill-BJeF2DwY.d.ts} +1 -1
  64. package/dist/skills/index.d.ts +9 -9
  65. package/dist/skills/index.js +15 -3
  66. package/dist/skills/index.js.map +1 -1
  67. package/dist/storage/index.d.ts +15 -15
  68. package/dist/storage/index.js +378 -76
  69. package/dist/storage/index.js.map +1 -1
  70. package/dist/{system-prompt-CA11g6Jo.d.ts → system-prompt-C0rLCeyn.d.ts} +16 -11
  71. package/dist/{task-graph-D1YQbpxF.d.ts → task-graph-CikNdRTG.d.ts} +22 -22
  72. package/dist/types/index.d.ts +26 -26
  73. package/dist/types/index.js +53 -17
  74. package/dist/types/index.js.map +1 -1
  75. package/dist/utils/index.d.ts +57 -45
  76. package/dist/utils/index.js +66 -12
  77. package/dist/utils/index.js.map +1 -1
  78. package/dist/{wstack-paths-D7evAFWM.d.ts → wstack-paths-BQMvEllz.d.ts} +2 -2
  79. package/package.json +1 -1
  80. package/dist/logger-DDd5C--Z.d.ts +0 -12
@@ -1,6 +1,6 @@
1
1
  import { randomBytes, randomUUID, createHash } from 'crypto';
2
2
  import * as fsp from 'fs/promises';
3
- import * as path14 from 'path';
3
+ import * as path2 from 'path';
4
4
  import * as os from 'os';
5
5
 
6
6
  var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
@@ -10,9 +10,9 @@ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require
10
10
  throw Error('Dynamic require of "' + x + '" is not supported');
11
11
  });
12
12
  async function atomicWrite(targetPath, content, opts = {}) {
13
- const dir = path14.dirname(targetPath);
13
+ const dir = path2.dirname(targetPath);
14
14
  await fsp.mkdir(dir, { recursive: true });
15
- const tmp = path14.join(dir, `.${path14.basename(targetPath)}.${randomBytes(6).toString("hex")}.tmp`);
15
+ const tmp = path2.join(dir, `.${path2.basename(targetPath)}.${randomBytes(6).toString("hex")}.tmp`);
16
16
  try {
17
17
  if (typeof content === "string") {
18
18
  await fsp.writeFile(tmp, content, { flag: "wx", encoding: opts.encoding ?? "utf8" });
@@ -75,6 +75,12 @@ async function renameWithRetry(from, to) {
75
75
  }
76
76
 
77
77
  // src/utils/message-invariants.ts
78
+ function expectDefined(value) {
79
+ if (value === null || value === void 0) {
80
+ throw new Error("Expected value to be defined");
81
+ }
82
+ return value;
83
+ }
78
84
  function repairToolUseAdjacency(messages) {
79
85
  const removedToolUses = [];
80
86
  const removedToolResults = [];
@@ -82,7 +88,7 @@ function repairToolUseAdjacency(messages) {
82
88
  let changed = false;
83
89
  const out = [];
84
90
  for (let i = 0; i < messages.length; i++) {
85
- const original = messages[i];
91
+ const original = expectDefined(messages[i]);
86
92
  let msg = original;
87
93
  if (hasToolUse(msg)) {
88
94
  const nextIds = toolResultIds(messages[i + 1]);
@@ -167,6 +173,12 @@ function isEmptyMessage(msg) {
167
173
  }
168
174
 
169
175
  // src/storage/session-store.ts
176
+ function expectDefined2(value) {
177
+ if (value === null || value === void 0) {
178
+ throw new Error("Expected value to be defined");
179
+ }
180
+ return value;
181
+ }
170
182
  function sanitizeModel(model) {
171
183
  return model.replace(/[^a-zA-Z0-9_-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "").slice(0, 40);
172
184
  }
@@ -177,7 +189,7 @@ function generateSessionId(startedAt, model) {
177
189
  const modelPart = model ? `_${sanitizeModel(model)}` : "";
178
190
  return `${date}/${time}Z${modelPart}_${suffix}`;
179
191
  }
180
- var DefaultSessionStore = class {
192
+ var DefaultSessionStore = class _DefaultSessionStore {
181
193
  dir;
182
194
  events;
183
195
  secretScrubber;
@@ -186,9 +198,13 @@ var DefaultSessionStore = class {
186
198
  this.events = opts.events;
187
199
  this.secretScrubber = opts.secretScrubber;
188
200
  }
201
+ /** Absolute path to the session index file. */
202
+ get indexFile() {
203
+ return path2.join(this.dir, "_index.jsonl");
204
+ }
189
205
  /** Join session ID to its absolute path within the store directory. */
190
206
  sessionPath(id, ext) {
191
- return path14.join(this.dir, `${id}${ext}`);
207
+ return path2.join(this.dir, `${id}${ext}`);
192
208
  }
193
209
  /**
194
210
  * Ensure the directory implied by the session ID exists. When the ID
@@ -196,7 +212,7 @@ var DefaultSessionStore = class {
196
212
  * subdirectory so sessions group naturally by day.
197
213
  */
198
214
  async ensureShardDir(id) {
199
- const dirPath = path14.dirname(path14.join(this.dir, id));
215
+ const dirPath = path2.dirname(path2.join(this.dir, id));
200
216
  await ensureDir(dirPath);
201
217
  return dirPath;
202
218
  }
@@ -204,7 +220,7 @@ var DefaultSessionStore = class {
204
220
  const startedAt = (/* @__PURE__ */ new Date()).toISOString();
205
221
  const id = meta.id && meta.id.length > 0 ? meta.id : generateSessionId(startedAt, meta.model ?? meta.provider);
206
222
  const shardDir = await this.ensureShardDir(id);
207
- const file = path14.join(shardDir, `${path14.basename(id)}.jsonl`);
223
+ const file = path2.join(shardDir, `${path2.basename(id)}.jsonl`);
208
224
  let handle;
209
225
  try {
210
226
  handle = await fsp.open(file, "a", 384);
@@ -218,7 +234,8 @@ var DefaultSessionStore = class {
218
234
  return new FileSessionWriter(id, handle, startedAt, meta, this.events, {
219
235
  dir: shardDir,
220
236
  filePath: file,
221
- secretScrubber: this.secretScrubber
237
+ secretScrubber: this.secretScrubber,
238
+ onClose: (s) => this.appendToIndex(s)
222
239
  });
223
240
  } catch (err) {
224
241
  await handle.close().catch(() => {
@@ -249,7 +266,7 @@ var DefaultSessionStore = class {
249
266
  provider: data.metadata.provider
250
267
  },
251
268
  this.events,
252
- { resumed: true, dir: this.dir, filePath: file, secretScrubber: this.secretScrubber }
269
+ { resumed: true, dir: this.dir, filePath: file, secretScrubber: this.secretScrubber, onClose: (s) => this.appendToIndex(s) }
253
270
  );
254
271
  return { writer, data };
255
272
  } catch (err) {
@@ -279,6 +296,15 @@ var DefaultSessionStore = class {
279
296
  async list(limit = 20) {
280
297
  try {
281
298
  await ensureDir(this.dir);
299
+ const indexed = await this.readIndex();
300
+ if (indexed.length > 0) {
301
+ indexed.sort((a, b) => {
302
+ if (a.startedAt < b.startedAt) return 1;
303
+ if (a.startedAt > b.startedAt) return -1;
304
+ return a.id.localeCompare(b.id);
305
+ });
306
+ return indexed.slice(0, limit);
307
+ }
282
308
  const ids = await this.collectSessionIds(this.dir);
283
309
  const sessions = await Promise.all(ids.map((id) => this.summaryFor(id).catch(() => null)));
284
310
  const out = sessions.filter((s) => s !== null);
@@ -292,16 +318,121 @@ var DefaultSessionStore = class {
292
318
  return [];
293
319
  }
294
320
  }
295
- /** Recursively collect all session IDs from shard subdirectories. */
296
- async collectSessionIds(dir) {
321
+ // ── Session index (_index.jsonl) ─────────────────────────────────────────
322
+ //
323
+ // One JSON line per closed session, appended atomically on close().
324
+ // When a session is deleted, a tombstone {action:"delete",id:"..."} is
325
+ // appended. On read, tombstones filter out matching session entries.
326
+ // This keeps listing O(lines-in-index) instead of O(files-on-disk).
327
+ //
328
+ // The index auto-compacts every N appends to prevent unbounded growth
329
+ // from tombstones and duplicate entries (resume cycles).
330
+ indexAppendCount = 0;
331
+ static COMPACT_EVERY = 30;
332
+ /** Append a session summary to the index. */
333
+ async appendToIndex(summary) {
334
+ try {
335
+ await ensureDir(this.dir);
336
+ const line = JSON.stringify(summary) + "\n";
337
+ await fsp.appendFile(this.indexFile, line, "utf8");
338
+ this.indexAppendCount++;
339
+ if (this.indexAppendCount >= _DefaultSessionStore.COMPACT_EVERY) {
340
+ await this.compactIndex();
341
+ this.indexAppendCount = 0;
342
+ }
343
+ } catch {
344
+ }
345
+ }
346
+ /** Append a tombstone entry for a deleted session. */
347
+ async writeTombstone(id) {
348
+ try {
349
+ await ensureDir(this.dir);
350
+ const line = JSON.stringify({ action: "delete", id }) + "\n";
351
+ await fsp.appendFile(this.indexFile, line, "utf8");
352
+ this.indexAppendCount++;
353
+ } catch {
354
+ }
355
+ }
356
+ /**
357
+ * Compact the index: read all entries, drop tombstones, deduplicate
358
+ * (keep latest per session), and rewrite. Atomic via temp+rename.
359
+ */
360
+ async compactIndex() {
361
+ const entries = await this.readIndex();
362
+ if (entries.length === 0) return;
363
+ const tmp = `${this.indexFile}.compact.tmp`;
364
+ const lines = entries.map((s) => JSON.stringify(s)).join("\n") + "\n";
365
+ await fsp.writeFile(tmp, lines, "utf8");
366
+ await fsp.rename(tmp, this.indexFile);
367
+ }
368
+ /**
369
+ * Read the index file and return deduplicated session summaries.
370
+ * Entries with a matching tombstone are filtered out.
371
+ * Returns empty array when the index doesn't exist or is corrupt.
372
+ */
373
+ async readIndex() {
374
+ let raw;
375
+ try {
376
+ raw = await fsp.readFile(this.indexFile, "utf8");
377
+ } catch {
378
+ return [];
379
+ }
380
+ const deleted = /* @__PURE__ */ new Set();
381
+ const seen = /* @__PURE__ */ new Map();
382
+ for (const line of raw.split("\n")) {
383
+ if (!line.trim()) continue;
384
+ try {
385
+ const entry = JSON.parse(line);
386
+ if (entry.action === "delete" && entry.id) {
387
+ deleted.add(entry.id);
388
+ seen.delete(entry.id);
389
+ continue;
390
+ }
391
+ if (entry.id && !deleted.has(entry.id)) {
392
+ seen.set(entry.id, entry);
393
+ }
394
+ } catch {
395
+ }
396
+ }
397
+ return Array.from(seen.values());
398
+ }
399
+ /**
400
+ * Rebuild the index from disk by scanning all sessions and writing a
401
+ * fresh _index.jsonl. Useful after manual cleanup or index corruption.
402
+ */
403
+ async rebuildIndex() {
404
+ const ids = await this.collectSessionIds(this.dir);
405
+ const summaries = await Promise.all(ids.map((id) => this.summaryFor(id).catch(() => null)));
406
+ const valid = summaries.filter((s) => s !== null);
407
+ const tmp = `${this.indexFile}.tmp`;
408
+ const lines = valid.map((s) => JSON.stringify(s)).join("\n") + "\n";
409
+ await fsp.writeFile(tmp, lines, "utf8");
410
+ await fsp.rename(tmp, this.indexFile);
411
+ return valid.length;
412
+ }
413
+ /** Recursively collect session IDs from date-shard subdirectories.
414
+ * IDs include the date-prefix path (e.g. "2026-06-06/17-46-57Z_…").
415
+ * Skips `.jsonl`/`.summary.json` root files, dot-files, and
416
+ * sub-directories that belong to fleet/subagent sessions. */
417
+ async collectSessionIds(dir, prefix = "", depth = 0) {
297
418
  const ids = [];
298
- const entries = await fsp.readdir(dir, { withFileTypes: true });
419
+ let entries;
420
+ try {
421
+ entries = await fsp.readdir(dir, { withFileTypes: true });
422
+ } catch {
423
+ return ids;
424
+ }
299
425
  for (const entry of entries) {
300
- const full = path14.join(dir, entry.name);
426
+ if (entry.name.startsWith(".") && entry.name !== ".wrongstack") continue;
427
+ if (entry.name === "shared" || entry.name === "subagents" || entry.name === "attachments")
428
+ continue;
301
429
  if (entry.isDirectory()) {
302
- ids.push(...await this.collectSessionIds(full));
430
+ const childPrefix = depth === 0 ? entry.name : `${prefix}/${entry.name}`;
431
+ ids.push(...await this.collectSessionIds(path2.join(dir, entry.name), childPrefix, depth + 1));
303
432
  } else if (entry.isFile() && entry.name.endsWith(".jsonl")) {
304
- ids.push(entry.name.replace(/\.jsonl$/, ""));
433
+ if (entry.name === "_index.jsonl") continue;
434
+ const base = entry.name.replace(/\.jsonl$/, "");
435
+ ids.push(prefix ? `${prefix}/${base}` : base);
305
436
  }
306
437
  }
307
438
  return ids;
@@ -324,9 +455,70 @@ var DefaultSessionStore = class {
324
455
  return summary;
325
456
  }
326
457
  }
327
- async delete(id) {
328
- await fsp.unlink(this.sessionPath(id, ".jsonl"));
458
+ /**
459
+ * Delete a session and all associated files: JSONL, summary, plan/todos
460
+ * sidecars, and the session directory (fleet.json, shared/, subagents/).
461
+ */
462
+ async deleteSession(id) {
463
+ await fsp.unlink(this.sessionPath(id, ".jsonl")).catch(() => void 0);
329
464
  await fsp.unlink(this.sessionPath(id, ".summary.json")).catch(() => void 0);
465
+ const shardDir = path2.dirname(path2.join(this.dir, id));
466
+ const base = path2.basename(id);
467
+ for (const ext of [".plan.json", ".todos.json"]) {
468
+ await fsp.unlink(path2.join(shardDir, `${base}${ext}`)).catch(() => void 0);
469
+ }
470
+ const sessDir = path2.join(shardDir, base);
471
+ await fsp.rm(sessDir, { recursive: true, force: true }).catch(() => void 0);
472
+ await this.writeTombstone(id);
473
+ }
474
+ async delete(id) {
475
+ await this.deleteSession(id);
476
+ }
477
+ async prune(maxAgeDays = 30) {
478
+ const cutoff = Date.now() - maxAgeDays * 864e5;
479
+ let deleted = 0;
480
+ let activeSessionId = null;
481
+ try {
482
+ const raw = await fsp.readFile(path2.join(this.dir, "active.json"), "utf8");
483
+ const active = JSON.parse(raw);
484
+ activeSessionId = active.sessionId ?? null;
485
+ } catch {
486
+ }
487
+ const entries = await fsp.readdir(this.dir, { withFileTypes: true }).catch(() => []);
488
+ for (const entry of entries) {
489
+ if (!entry.isDirectory()) continue;
490
+ const dateDir = path2.join(this.dir, entry.name);
491
+ const files = await fsp.readdir(dateDir, { withFileTypes: true }).catch(() => []);
492
+ for (const file of files) {
493
+ if (!file.isFile() || !file.name.endsWith(".jsonl")) continue;
494
+ const jsonlPath = path2.join(dateDir, file.name);
495
+ try {
496
+ const stat5 = await fsp.stat(jsonlPath);
497
+ if (stat5.mtimeMs >= cutoff) continue;
498
+ } catch {
499
+ continue;
500
+ }
501
+ const id = `${entry.name}/${file.name.replace(/\.jsonl$/, "")}`;
502
+ if (activeSessionId && id === activeSessionId) continue;
503
+ await this.deleteSession(id);
504
+ deleted++;
505
+ }
506
+ }
507
+ if (deleted > 0) {
508
+ await this.compactIndex().catch(() => void 0);
509
+ }
510
+ for (const entry of entries) {
511
+ if (!entry.isDirectory()) continue;
512
+ const dateDir = path2.join(this.dir, entry.name);
513
+ try {
514
+ const remaining = await fsp.readdir(dateDir);
515
+ if (remaining.length === 0) {
516
+ await fsp.rmdir(dateDir).catch(() => void 0);
517
+ }
518
+ } catch {
519
+ }
520
+ }
521
+ return deleted;
330
522
  }
331
523
  async clearHistory(id) {
332
524
  await this.ensureShardDir(id);
@@ -348,13 +540,42 @@ var DefaultSessionStore = class {
348
540
  const data = await this.load(id);
349
541
  const firstUser = data.events.find((e) => e.type === "user_input");
350
542
  const title = firstUser && firstUser.type === "user_input" ? userInputTitle(firstUser.content) : "(empty session)";
543
+ let iterationCount = 0;
544
+ let toolCallCount = 0;
545
+ let toolErrorCount = 0;
546
+ let fileChangeCount = 0;
547
+ const toolBreakdown = {};
548
+ let outcome = void 0;
549
+ const lastEvent = data.events[data.events.length - 1];
550
+ for (const e of data.events) {
551
+ if (e.type === "in_flight_start") iterationCount++;
552
+ else if (e.type === "tool_call_start") {
553
+ toolCallCount++;
554
+ toolBreakdown[e.name] = (toolBreakdown[e.name] ?? 0) + 1;
555
+ } else if (e.type === "tool_result" && e.isError) toolErrorCount++;
556
+ else if (e.type === "file_snapshot") fileChangeCount += e.files.length;
557
+ }
558
+ if (lastEvent?.type === "session_end") {
559
+ outcome = "completed";
560
+ } else if (lastEvent?.type === "in_flight_start") {
561
+ outcome = "aborted";
562
+ } else if (data.events.some((e) => e.type === "error")) {
563
+ outcome = "error";
564
+ }
351
565
  return {
352
566
  id,
353
567
  title,
354
568
  startedAt: data.metadata.startedAt,
569
+ endedAt: data.metadata.endedAt,
355
570
  model: data.metadata.model ?? "unknown",
356
571
  provider: data.metadata.provider ?? "unknown",
357
- tokenTotal: data.usage.input + data.usage.output
572
+ tokenTotal: data.usage.input + data.usage.output,
573
+ iterationCount: iterationCount > 0 ? iterationCount : void 0,
574
+ toolCallCount: toolCallCount > 0 ? toolCallCount : void 0,
575
+ toolErrorCount: toolErrorCount > 0 ? toolErrorCount : void 0,
576
+ fileChangeCount: fileChangeCount > 0 ? fileChangeCount : void 0,
577
+ toolBreakdown: Object.keys(toolBreakdown).length > 0 ? toolBreakdown : {},
578
+ outcome
358
579
  };
359
580
  } catch {
360
581
  return {
@@ -453,9 +674,10 @@ var FileSessionWriter = class {
453
674
  this.meta = meta;
454
675
  this.events = events;
455
676
  this.resumed = opts.resumed ?? false;
456
- this.manifestFile = opts.dir ? path14.join(opts.dir, `${id}.summary.json`) : "";
677
+ this.manifestFile = opts.dir ? path2.join(opts.dir, `${path2.basename(id)}.summary.json`) : "";
457
678
  this.filePath = opts.filePath ?? "";
458
679
  this.secretScrubber = opts.secretScrubber;
680
+ this.onCloseCb = opts.onClose;
459
681
  this.summary = {
460
682
  id,
461
683
  title: "(empty session)",
@@ -485,6 +707,15 @@ var FileSessionWriter = class {
485
707
  appendFailCount = 0;
486
708
  lastAppendWarnAt = 0;
487
709
  secretScrubber;
710
+ onCloseCb;
711
+ // ── Enriched summary tracking ──────────────────────────────────────────
712
+ iterationCount = 0;
713
+ toolCallCount = 0;
714
+ toolErrorCount = 0;
715
+ toolBreakdown = {};
716
+ fileChangeCount = 0;
717
+ compactionCount = 0;
718
+ outcome = void 0;
488
719
  /**
489
720
  * Scrub secrets out of conversation-turn events before they are observed
490
721
  * for the summary, written to the JSONL log, or surfaced on resume. Only
@@ -562,8 +793,22 @@ var FileSessionWriter = class {
562
793
  observeForSummary(event) {
563
794
  if (event.type === "tool_use") {
564
795
  this.openToolUses.add(event.id);
796
+ } else if (event.type === "tool_call_start") {
797
+ this.toolCallCount++;
798
+ this.toolBreakdown[event.name] = (this.toolBreakdown[event.name] ?? 0) + 1;
565
799
  } else if (event.type === "tool_result") {
566
800
  this.openToolUses.delete(event.id);
801
+ if (event.isError) {
802
+ this.toolErrorCount++;
803
+ this.outcome = "error";
804
+ }
805
+ } else if (event.type === "file_snapshot") {
806
+ this.fileChangeCount += event.files.length;
807
+ } else if (event.type === "compaction") {
808
+ this.compactionCount++;
809
+ }
810
+ if (event.type === "error" || event.type === "provider_error") {
811
+ this.outcome = "error";
567
812
  }
568
813
  if (event.type === "user_input" && this.summary.title === "(empty session)") {
569
814
  this.summary = { ...this.summary, title: userInputTitle(event.content) };
@@ -574,18 +819,35 @@ var FileSessionWriter = class {
574
819
  } else if (event.type === "session_end") {
575
820
  const total = event.usage.input + event.usage.output;
576
821
  if (total > 0) this.summary = { ...this.summary, tokenTotal: total };
822
+ } else if (event.type === "in_flight_start") {
823
+ this.iterationCount++;
577
824
  }
578
825
  }
579
826
  async close() {
580
827
  if (this.closing) return;
581
828
  this.closing = true;
582
829
  this.closed = true;
830
+ this.summary = {
831
+ ...this.summary,
832
+ endedAt: (/* @__PURE__ */ new Date()).toISOString(),
833
+ iterationCount: this.iterationCount,
834
+ toolCallCount: this.toolCallCount,
835
+ toolErrorCount: this.toolErrorCount,
836
+ fileChangeCount: this.fileChangeCount,
837
+ compactionCount: this.compactionCount > 0 ? this.compactionCount : void 0,
838
+ toolBreakdown: { ...this.toolBreakdown },
839
+ outcome: this.outcome ?? "completed"
840
+ };
583
841
  if (this.manifestFile) {
584
842
  try {
585
843
  await atomicWrite(this.manifestFile, JSON.stringify(this.summary), { mode: 384 });
586
844
  } catch {
587
845
  }
588
846
  }
847
+ try {
848
+ await this.onCloseCb?.(this.summary);
849
+ } catch {
850
+ }
589
851
  try {
590
852
  await this.handle.close();
591
853
  } catch {
@@ -627,7 +889,7 @@ var FileSessionWriter = class {
627
889
  let targetCheckpointLine = -1;
628
890
  let afterTarget = false;
629
891
  for (let i = 0; i < lines.length; i++) {
630
- const line = lines[i];
892
+ const line = expectDefined2(lines[i]);
631
893
  if (!line.trim()) continue;
632
894
  let event;
633
895
  try {
@@ -732,7 +994,7 @@ function userInputTitle(content) {
732
994
  var QueueStore = class {
733
995
  file;
734
996
  constructor(opts) {
735
- this.file = path14.join(opts.dir, "queue.json");
997
+ this.file = path2.join(opts.dir, "queue.json");
736
998
  }
737
999
  async write(items) {
738
1000
  if (items.length === 0) {
@@ -799,7 +1061,7 @@ var DefaultAttachmentStore = class {
799
1061
  let data = input.data;
800
1062
  if (this.spoolDir && bytes >= this.spoolThreshold) {
801
1063
  await fsp.mkdir(this.spoolDir, { recursive: true });
802
- spooledPath = path14.join(this.spoolDir, `${id}.bin`);
1064
+ spooledPath = path2.join(this.spoolDir, `${id}.bin`);
803
1065
  await atomicWrite(spooledPath, input.data, {
804
1066
  encoding: input.kind === "image" ? "base64" : "utf8"
805
1067
  });
@@ -984,7 +1246,7 @@ ${body.trim()}`);
984
1246
  async remember(text, scope = "project-memory") {
985
1247
  return this.runSerialized(scope, async () => {
986
1248
  const file = this.files[scope];
987
- await ensureDir(path14.dirname(file));
1249
+ await ensureDir(path2.dirname(file));
988
1250
  let existing = "";
989
1251
  try {
990
1252
  existing = await fsp.readFile(file, "utf8");
@@ -1623,7 +1885,7 @@ var RecoveryLock = class {
1623
1885
  sessionStore;
1624
1886
  probe;
1625
1887
  constructor(opts) {
1626
- this.file = path14.join(opts.dir, LOCK_FILE);
1888
+ this.file = path2.join(opts.dir, LOCK_FILE);
1627
1889
  this.pid = opts.pid ?? process.pid;
1628
1890
  this.hostname = opts.hostname ?? os.hostname();
1629
1891
  this.maxAgeMs = opts.maxAgeMs ?? DEFAULT_MAX_AGE_MS;
@@ -1681,7 +1943,7 @@ var RecoveryLock = class {
1681
1943
  * null return before calling this.
1682
1944
  */
1683
1945
  async write(sessionId) {
1684
- await ensureDir(path14.dirname(this.file));
1946
+ await ensureDir(path2.dirname(this.file));
1685
1947
  const lock = {
1686
1948
  v: 1,
1687
1949
  sessionId,
@@ -1785,6 +2047,12 @@ function compileUserRegex(pattern, flags) {
1785
2047
  }
1786
2048
 
1787
2049
  // src/storage/session-reader.ts
2050
+ function expectDefined3(value) {
2051
+ if (value === null || value === void 0) {
2052
+ throw new Error("Expected value to be defined");
2053
+ }
2054
+ return value;
2055
+ }
1788
2056
  var DefaultSessionReader = class {
1789
2057
  store;
1790
2058
  constructor(opts) {
@@ -1846,7 +2114,7 @@ var DefaultSessionReader = class {
1846
2114
  continue;
1847
2115
  }
1848
2116
  for (let i = 0; i < data.events.length; i++) {
1849
- const ev = data.events[i];
2117
+ const ev = expectDefined3(data.events[i]);
1850
2118
  if (allowedTypes && !allowedTypes.has(ev.type)) continue;
1851
2119
  const text = eventText(ev);
1852
2120
  if (text === null) continue;
@@ -2059,6 +2327,12 @@ function renderPlainText(meta, events) {
2059
2327
  }
2060
2328
  return lines.join("\n");
2061
2329
  }
2330
+ function expectDefined4(value) {
2331
+ if (value === null || value === void 0) {
2332
+ throw new Error("Expected value to be defined");
2333
+ }
2334
+ return value;
2335
+ }
2062
2336
  var FILE_VERSION = 1;
2063
2337
  var MAX_TEXT_LENGTH = 2e3;
2064
2338
  var MAX_ANNOTATIONS = 1e3;
@@ -2150,7 +2424,7 @@ var AnnotationsStore = class {
2150
2424
  return;
2151
2425
  }
2152
2426
  const next = {
2153
- ...all[idx],
2427
+ ...expectDefined4(all[idx]),
2154
2428
  resolved: true,
2155
2429
  resolvedAt: (/* @__PURE__ */ new Date()).toISOString(),
2156
2430
  resolvedBy: input.resolvedBy
@@ -2166,7 +2440,7 @@ var AnnotationsStore = class {
2166
2440
  if (!sessionId || sessionId.includes("/") || sessionId.includes("\\") || sessionId.includes("..")) {
2167
2441
  throw new Error(`Invalid sessionId: ${sessionId}`);
2168
2442
  }
2169
- return path14.join(this.dir, `${sessionId}.annotations.json`);
2443
+ return path2.join(this.dir, `${sessionId}.annotations.json`);
2170
2444
  }
2171
2445
  async readFile(sessionId) {
2172
2446
  const fp = this.filePath(sessionId);
@@ -2323,7 +2597,7 @@ var ReplayLogStore = class {
2323
2597
  out.push({
2324
2598
  sessionId,
2325
2599
  entryCount: all.length,
2326
- path: path14.join(this.dir, name)
2600
+ path: path2.join(this.dir, name)
2327
2601
  });
2328
2602
  }
2329
2603
  return out.sort((a, b) => a.sessionId.localeCompare(b.sessionId));
@@ -2333,7 +2607,7 @@ var ReplayLogStore = class {
2333
2607
  if (!sessionId || sessionId.includes("/") || sessionId.includes("\\") || sessionId.includes("..")) {
2334
2608
  throw new Error(`Invalid sessionId: ${sessionId}`);
2335
2609
  }
2336
- return path14.join(this.dir, `${sessionId}.replay.jsonl`);
2610
+ return path2.join(this.dir, `${sessionId}.replay.jsonl`);
2337
2611
  }
2338
2612
  async readAll(sessionId) {
2339
2613
  const fp = this.filePath(sessionId);
@@ -2383,6 +2657,12 @@ var ReplayLogStore = class {
2383
2657
  return next;
2384
2658
  }
2385
2659
  };
2660
+ function expectDefined5(value) {
2661
+ if (value === null || value === void 0) {
2662
+ throw new Error("Expected value to be defined");
2663
+ }
2664
+ return value;
2665
+ }
2386
2666
  var SessionRecovery = class {
2387
2667
  constructor(dir) {
2388
2668
  this.dir = dir;
@@ -2423,7 +2703,7 @@ var SessionRecovery = class {
2423
2703
  const lines = raw.split("\n").filter((l) => l.trim());
2424
2704
  for (let i = lines.length - 1; i >= 0; i--) {
2425
2705
  try {
2426
- const ev = JSON.parse(lines[i]);
2706
+ const ev = JSON.parse(expectDefined5(lines[i]));
2427
2707
  if (ev.type === "in_flight_start") {
2428
2708
  return {
2429
2709
  sessionId,
@@ -2475,13 +2755,13 @@ var SessionRecovery = class {
2475
2755
  let lastCheckpoint = null;
2476
2756
  let lastCheckpointIdx = -1;
2477
2757
  for (let i = 0; i < events.length; i++) {
2478
- if (events[i].type === "checkpoint") {
2479
- lastCheckpoint = events[i];
2758
+ if (events[i]?.type === "checkpoint") {
2759
+ lastCheckpoint = expectDefined5(events[i]);
2480
2760
  lastCheckpointIdx = i;
2481
2761
  }
2482
2762
  }
2483
2763
  const pendingEvents = lastCheckpointIdx >= 0 ? events.slice(lastCheckpointIdx + 1) : events;
2484
- const lastEv = events[events.length - 1];
2764
+ const lastEv = expectDefined5(events[events.length - 1]);
2485
2765
  const inFlightStart = lastEv.type === "in_flight_start" ? lastEv : null;
2486
2766
  const context = inFlightStart && inFlightStart.type === "in_flight_start" ? inFlightStart.context : null;
2487
2767
  return {
@@ -2521,9 +2801,15 @@ var SessionRecovery = class {
2521
2801
  if (!sessionId || sessionId.includes("/") || sessionId.includes("\\") || sessionId.includes("..")) {
2522
2802
  throw new Error(`Invalid sessionId: ${sessionId}`);
2523
2803
  }
2524
- return path14.join(this.dir, `${sessionId}.jsonl`);
2804
+ return path2.join(this.dir, `${sessionId}.jsonl`);
2525
2805
  }
2526
2806
  };
2807
+ function expectDefined6(value) {
2808
+ if (value === null || value === void 0) {
2809
+ throw new Error("Expected value to be defined");
2810
+ }
2811
+ return value;
2812
+ }
2527
2813
  var GENESIS_PREV = "0".repeat(64);
2528
2814
  var DEFAULT_FSYNC_EVERY = 100;
2529
2815
  var ToolAuditLog = class {
@@ -2590,7 +2876,7 @@ var ToolAuditLog = class {
2590
2876
  async verify(sessionId) {
2591
2877
  const entries = await this.readAll(sessionId);
2592
2878
  if (entries.length === 0) return { ok: true, entries: 0 };
2593
- if (entries[0].prevHash !== GENESIS_PREV) {
2879
+ if (entries[0]?.prevHash !== GENESIS_PREV) {
2594
2880
  return {
2595
2881
  ok: false,
2596
2882
  brokenAt: 0,
@@ -2599,7 +2885,7 @@ var ToolAuditLog = class {
2599
2885
  }
2600
2886
  let prevHash = GENESIS_PREV;
2601
2887
  for (let i = 0; i < entries.length; i++) {
2602
- const e = entries[i];
2888
+ const e = expectDefined6(entries[i]);
2603
2889
  if (e.prevHash !== prevHash) {
2604
2890
  return {
2605
2891
  ok: false,
@@ -2639,7 +2925,7 @@ var ToolAuditLog = class {
2639
2925
  if (!sessionId || sessionId.includes("/") || sessionId.includes("\\") || sessionId.includes("..")) {
2640
2926
  throw new Error(`Invalid sessionId: ${sessionId}`);
2641
2927
  }
2642
- return path14.join(this.dir, `${sessionId}.audit.jsonl`);
2928
+ return path2.join(this.dir, `${sessionId}.audit.jsonl`);
2643
2929
  }
2644
2930
  async readAll(sessionId) {
2645
2931
  const fp = this.filePath(sessionId);
@@ -2807,11 +3093,20 @@ var SessionAnalyzer = class {
2807
3093
  }
2808
3094
  calcDuration(events) {
2809
3095
  if (events.length < 2) return 0;
2810
- const first = new Date(events[0].ts).getTime();
2811
- const last = new Date(events[events.length - 1].ts).getTime();
3096
+ const firstEvent = events[0];
3097
+ const lastEvent = events[events.length - 1];
3098
+ if (!firstEvent || !lastEvent) return 0;
3099
+ const first = new Date(firstEvent.ts).getTime();
3100
+ const last = new Date(lastEvent.ts).getTime();
2812
3101
  return last - first;
2813
3102
  }
2814
3103
  };
3104
+ function expectDefined7(value) {
3105
+ if (value === null || value === void 0) {
3106
+ throw new Error("Expected value to be defined");
3107
+ }
3108
+ return value;
3109
+ }
2815
3110
  var DefaultSessionRewinder = class {
2816
3111
  constructor(sessionsDir, projectRoot) {
2817
3112
  this.sessionsDir = sessionsDir;
@@ -2820,7 +3115,7 @@ var DefaultSessionRewinder = class {
2820
3115
  sessionsDir;
2821
3116
  projectRoot;
2822
3117
  async listCheckpoints(sessionId) {
2823
- const file = path14.join(this.sessionsDir, `${sessionId}.jsonl`);
3118
+ const file = path2.join(this.sessionsDir, `${sessionId}.jsonl`);
2824
3119
  const raw = await fsp.readFile(file, "utf8");
2825
3120
  const events = parseEvents(raw);
2826
3121
  const fileCountMap = /* @__PURE__ */ new Map();
@@ -2845,12 +3140,12 @@ var DefaultSessionRewinder = class {
2845
3140
  return checkpoints;
2846
3141
  }
2847
3142
  async rewindToCheckpoint(sessionId, checkpointIndex) {
2848
- const file = path14.join(this.sessionsDir, `${sessionId}.jsonl`);
3143
+ const file = path2.join(this.sessionsDir, `${sessionId}.jsonl`);
2849
3144
  const raw = await fsp.readFile(file, "utf8");
2850
3145
  const events = parseEvents(raw);
2851
3146
  let targetIdx = -1;
2852
3147
  for (let i = 0; i < events.length; i++) {
2853
- const event = events[i];
3148
+ const event = expectDefined7(events[i]);
2854
3149
  if (event.type === "checkpoint") {
2855
3150
  const checkpointEvent = event;
2856
3151
  if (checkpointEvent.promptIndex === checkpointIndex) {
@@ -2864,7 +3159,7 @@ var DefaultSessionRewinder = class {
2864
3159
  }
2865
3160
  const snapshotsToRevert = [];
2866
3161
  for (let i = targetIdx + 1; i < events.length; i++) {
2867
- const event = events[i];
3162
+ const event = expectDefined7(events[i]);
2868
3163
  if (event.type === "checkpoint") {
2869
3164
  break;
2870
3165
  }
@@ -2880,7 +3175,7 @@ var DefaultSessionRewinder = class {
2880
3175
  return { ...result, toPromptIndex: checkpointIndex, removedEvents };
2881
3176
  }
2882
3177
  async rewindLastN(sessionId, n) {
2883
- const file = path14.join(this.sessionsDir, `${sessionId}.jsonl`);
3178
+ const file = path2.join(this.sessionsDir, `${sessionId}.jsonl`);
2884
3179
  const raw = await fsp.readFile(file, "utf8");
2885
3180
  const events = parseEvents(raw);
2886
3181
  const checkpoints = [];
@@ -2909,7 +3204,7 @@ var DefaultSessionRewinder = class {
2909
3204
  return { ...result, toPromptIndex: targetIndex, removedEvents: snapshotsToRevert.length };
2910
3205
  }
2911
3206
  async rewindToStart(sessionId) {
2912
- const file = path14.join(this.sessionsDir, `${sessionId}.jsonl`);
3207
+ const file = path2.join(this.sessionsDir, `${sessionId}.jsonl`);
2913
3208
  const raw = await fsp.readFile(file, "utf8");
2914
3209
  const events = parseEvents(raw);
2915
3210
  const allSnapshots = [];
@@ -2945,10 +3240,10 @@ async function revertSnapshots(snapshots, projectRoot) {
2945
3240
  for (const snapshot of snapshots) {
2946
3241
  for (const file of snapshot.files) {
2947
3242
  try {
2948
- const absPath = path14.resolve(file.path);
2949
- const root = path14.resolve(projectRoot);
2950
- const rel = path14.relative(root, absPath);
2951
- if (rel.startsWith("..") || path14.isAbsolute(rel)) {
3243
+ const absPath = path2.resolve(file.path);
3244
+ const root = path2.resolve(projectRoot);
3245
+ const rel = path2.relative(root, absPath);
3246
+ if (rel.startsWith("..") || path2.isAbsolute(rel)) {
2952
3247
  errors.push(`${file.path}: path resolves outside project root \u2014 skipping`);
2953
3248
  continue;
2954
3249
  }
@@ -3507,8 +3802,8 @@ var FsError = class extends WrongStackError {
3507
3802
  // src/storage/goal-store.ts
3508
3803
  var MAX_JOURNAL_ENTRIES = 500;
3509
3804
  function goalFilePath(projectRoot) {
3510
- const hash = createHash("sha256").update(path14.resolve(projectRoot)).digest("hex").slice(0, 12);
3511
- return path14.join(os.homedir(), ".wrongstack", "projects", hash, "goal.json");
3805
+ const hash = createHash("sha256").update(path2.resolve(projectRoot)).digest("hex").slice(0, 12);
3806
+ return path2.join(os.homedir(), ".wrongstack", "projects", hash, "goal.json");
3512
3807
  }
3513
3808
  async function loadGoal(filePath) {
3514
3809
  let raw;
@@ -3623,7 +3918,7 @@ var DefaultPromptStore = class {
3623
3918
  if (!file.endsWith(".json")) continue;
3624
3919
  try {
3625
3920
  const raw = JSON.parse(
3626
- await fsp.readFile(path14.join(this.dir, file), "utf8")
3921
+ await fsp.readFile(path2.join(this.dir, file), "utf8")
3627
3922
  );
3628
3923
  entries.push(raw.entry);
3629
3924
  } catch {
@@ -3636,7 +3931,7 @@ var DefaultPromptStore = class {
3636
3931
  );
3637
3932
  }
3638
3933
  async get(id) {
3639
- const file = path14.join(this.dir, `${id}.json`);
3934
+ const file = path2.join(this.dir, `${id}.json`);
3640
3935
  try {
3641
3936
  const raw = JSON.parse(await fsp.readFile(file, "utf8"));
3642
3937
  return raw.entry;
@@ -3646,12 +3941,12 @@ var DefaultPromptStore = class {
3646
3941
  }
3647
3942
  async save(entry) {
3648
3943
  await ensureDir(this.dir);
3649
- const file = path14.join(this.dir, `${entry.id}.json`);
3944
+ const file = path2.join(this.dir, `${entry.id}.json`);
3650
3945
  const raw = { version: 1, entry };
3651
3946
  await atomicWrite(file, JSON.stringify(raw, null, 2));
3652
3947
  }
3653
3948
  async delete(id) {
3654
- const file = path14.join(this.dir, `${id}.json`);
3949
+ const file = path2.join(this.dir, `${id}.json`);
3655
3950
  try {
3656
3951
  await fsp.unlink(file);
3657
3952
  return true;
@@ -3679,13 +3974,19 @@ var DefaultPromptStore = class {
3679
3974
  };
3680
3975
  }
3681
3976
  };
3977
+ function expectDefined8(value) {
3978
+ if (value === null || value === void 0) {
3979
+ throw new Error("Expected value to be defined");
3980
+ }
3981
+ return value;
3982
+ }
3682
3983
  var ALL_SYNC_CATEGORIES = ["settings", "skills", "prompts", "memory", "history"];
3683
3984
  var CloudSync = class {
3684
3985
  constructor(paths, getConfig, setConfig) {
3685
3986
  this.paths = paths;
3686
3987
  this.getConfig = getConfig;
3687
3988
  this.setConfig = setConfig;
3688
- this.statePath = path14.join(paths.globalRoot, "sync-state.json");
3989
+ this.statePath = path2.join(paths.globalRoot, "sync-state.json");
3689
3990
  }
3690
3991
  paths;
3691
3992
  getConfig;
@@ -3721,8 +4022,8 @@ var CloudSync = class {
3721
4022
  const cfg = this.getConfig();
3722
4023
  if (!cfg?.enabled) return { ok: false, action: "push", categories: [], message: "Not enabled." };
3723
4024
  const parts = cfg.repo.split("/");
3724
- const owner = parts[0];
3725
- const repoName = parts[1];
4025
+ const owner = expectDefined8(parts[0]);
4026
+ const repoName = expectDefined8(parts[1]);
3726
4027
  const branch = "main";
3727
4028
  const baseTreeSha = this.state?.sha;
3728
4029
  const { treeEntries, rev } = await this.buildLocalTree(cfg.categories);
@@ -3774,8 +4075,8 @@ var CloudSync = class {
3774
4075
  const cfg = this.getConfig();
3775
4076
  if (!cfg?.enabled) return { ok: false, action: "pull", categories: [], message: "Not enabled." };
3776
4077
  const pullParts = cfg.repo.split("/");
3777
- const owner = pullParts[0];
3778
- const repoName = pullParts[1];
4078
+ const owner = expectDefined8(pullParts[0]);
4079
+ const repoName = expectDefined8(pullParts[1]);
3779
4080
  const branchData = await this.getRef(token, owner, repoName, "main");
3780
4081
  const currentSha = branchData.object.sha;
3781
4082
  const commitData = await this.getCommit(token, owner, repoName, currentSha);
@@ -3792,7 +4093,7 @@ var CloudSync = class {
3792
4093
  const rel = segments.slice(2).join("/");
3793
4094
  const destPath = resolvePulledCategoryPath(cat, localPath, rel, entry.path);
3794
4095
  const blobData = await this.getBlob(token, owner, repoName, entry.sha);
3795
- await fsp.mkdir(path14.dirname(destPath), { recursive: true });
4096
+ await fsp.mkdir(path2.dirname(destPath), { recursive: true });
3796
4097
  await fsp.writeFile(destPath, Buffer.from(blobData, "base64"));
3797
4098
  }
3798
4099
  const localRev = await this.hashLocalCategories(cfg.categories);
@@ -3830,7 +4131,7 @@ var CloudSync = class {
3830
4131
  // ── GitHub API helpers ──────────────────────────────────────────────
3831
4132
  async githubFetch(token, owner, repo, method, pathSegment, body) {
3832
4133
  const url = `https://api.github.com/repos/${owner}/${repo}${pathSegment}`;
3833
- const res = await fetch(url, {
4134
+ const init = {
3834
4135
  signal: AbortSignal.timeout(15e3),
3835
4136
  method,
3836
4137
  headers: {
@@ -3838,9 +4139,10 @@ var CloudSync = class {
3838
4139
  Accept: "application/vnd.github+json",
3839
4140
  "X-GitHub-Api-Version": "2022-11-28",
3840
4141
  "Content-Type": "application/json"
3841
- },
3842
- body: body !== void 0 ? JSON.stringify(body) : void 0
3843
- });
4142
+ }
4143
+ };
4144
+ if (body !== void 0) init.body = JSON.stringify(body);
4145
+ const res = await fetch(url, init);
3844
4146
  if (!res.ok) {
3845
4147
  const errText = await res.text();
3846
4148
  throw new Error(`GitHub API ${method} ${pathSegment} failed (${res.status}): ${errText}`);
@@ -3898,7 +4200,7 @@ var CloudSync = class {
3898
4200
  const files = await this.walkDir(localPath, localPath);
3899
4201
  for (const file of files) {
3900
4202
  const content = await fsp.readFile(file, "utf8");
3901
- const rel = path14.relative(localPath, file).replace(/\\/g, "/");
4203
+ const rel = path2.relative(localPath, file).replace(/\\/g, "/");
3902
4204
  entries.push({ path: `data/${cat}/${rel}`, content, mode: "100644" });
3903
4205
  hashes.push(content);
3904
4206
  }
@@ -3955,7 +4257,7 @@ var CloudSync = class {
3955
4257
  const results = [];
3956
4258
  const entries = await fsp.readdir(dir, { withFileTypes: true });
3957
4259
  for (const entry of entries) {
3958
- const full = path14.join(dir, entry.name);
4260
+ const full = path2.join(dir, entry.name);
3959
4261
  if (entry.isDirectory()) {
3960
4262
  results.push(...await this.walkDir(full, base));
3961
4263
  } else {
@@ -3972,15 +4274,15 @@ function resolvePulledCategoryPath(cat, localPath, rel, remotePath) {
3972
4274
  return localPath;
3973
4275
  }
3974
4276
  if (!rel) return localPath;
3975
- const normalizedRel = path14.normalize(rel);
3976
- const traversesUp = normalizedRel === ".." || normalizedRel.startsWith(`..${path14.sep}`);
3977
- if (path14.isAbsolute(normalizedRel) || traversesUp) {
4277
+ const normalizedRel = path2.normalize(rel);
4278
+ const traversesUp = normalizedRel === ".." || normalizedRel.startsWith(`..${path2.sep}`);
4279
+ if (path2.isAbsolute(normalizedRel) || traversesUp) {
3978
4280
  throw new Error(`Refusing CloudSync path traversal: ${remotePath}`);
3979
4281
  }
3980
- const dest = path14.resolve(localPath, normalizedRel);
3981
- const root = path14.resolve(localPath);
3982
- const relative3 = path14.relative(root, dest);
3983
- if (relative3.startsWith("..") || path14.isAbsolute(relative3)) {
4282
+ const dest = path2.resolve(localPath, normalizedRel);
4283
+ const root = path2.resolve(localPath);
4284
+ const relative3 = path2.relative(root, dest);
4285
+ if (relative3.startsWith("..") || path2.isAbsolute(relative3)) {
3984
4286
  throw new Error(`Refusing CloudSync path outside category root: ${remotePath}`);
3985
4287
  }
3986
4288
  return dest;