@c3-oss/prosa 0.2.0 → 0.3.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/cli/main.js CHANGED
@@ -12,8 +12,8 @@ var __export = (target, all) => {
12
12
 
13
13
  // src/core/db.ts
14
14
  import Database from "better-sqlite3";
15
- function openDb(path23) {
16
- const db = new Database(path23);
15
+ function openDb(path20) {
16
+ const db = new Database(path20);
17
17
  db.pragma("journal_mode = WAL");
18
18
  db.pragma("foreign_keys = ON");
19
19
  db.pragma("synchronous = NORMAL");
@@ -48,6 +48,15 @@ var init_db = __esm({
48
48
  }
49
49
  });
50
50
 
51
+ // src/core/errors.ts
52
+ var getErrorMessage;
53
+ var init_errors = __esm({
54
+ "src/core/errors.ts"() {
55
+ "use strict";
56
+ getErrorMessage = (err) => err instanceof Error ? err.message : String(err);
57
+ }
58
+ });
59
+
51
60
  // src/core/cas/compress.ts
52
61
  import { compress as zstdCompress, decompress as zstdDecompress } from "zstd-napi";
53
62
  function compressBytes(input) {
@@ -393,7 +402,7 @@ function rebuildFts5Index(bundle) {
393
402
  status: "failed",
394
403
  sourceDocCount: countSearchDocs(bundle),
395
404
  indexedDocCount: countFts5Docs(bundle),
396
- errorMessage: error instanceof Error ? error.message : String(error)
405
+ errorMessage: getErrorMessage(error)
397
406
  });
398
407
  throw error;
399
408
  }
@@ -466,7 +475,7 @@ async function rebuildTantivyIndex(bundle) {
466
475
  status: "failed",
467
476
  sourceDocCount: countSearchDocs(bundle),
468
477
  indexedDocCount: 0,
469
- errorMessage: error instanceof Error ? error.message : String(error)
478
+ errorMessage: getErrorMessage(error)
470
479
  });
471
480
  throw error;
472
481
  }
@@ -514,6 +523,7 @@ var init_indexing = __esm({
514
523
  "src/services/indexing.ts"() {
515
524
  "use strict";
516
525
  init_db();
526
+ init_errors();
517
527
  FTS5_TRIGGER_SQL = `
518
528
  CREATE TRIGGER IF NOT EXISTS search_docs_ai AFTER INSERT ON search_docs BEGIN
519
529
  INSERT INTO search_docs_fts(rowid, text, role, tool_name, field_kind)
@@ -535,6 +545,16 @@ END;
535
545
  }
536
546
  });
537
547
 
548
+ // src/core/limits.ts
549
+ function clampLimit(value, opts) {
550
+ return Math.max(opts.min ?? 1, Math.min(opts.max, value ?? opts.fallback));
551
+ }
552
+ var init_limits = __esm({
553
+ "src/core/limits.ts"() {
554
+ "use strict";
555
+ }
556
+ });
557
+
538
558
  // src/services/search.ts
539
559
  import { existsSync } from "fs";
540
560
  import { createRequire } from "module";
@@ -545,7 +565,7 @@ function searchFullText(bundle, options) {
545
565
  if (options.engine === "tantivy") {
546
566
  return searchTantivy(bundle, options);
547
567
  }
548
- const limit = Math.max(1, Math.min(500, options.limit ?? 50));
568
+ const limit = clampLimit(options.limit, { max: 500, fallback: 50 });
549
569
  const sql = `
550
570
  SELECT d.doc_id,
551
571
  d.entity_type,
@@ -576,7 +596,7 @@ function searchTantivy(bundle, options) {
576
596
  `tantivy index is ${status?.status ?? "missing"}; run \`prosa index tantivy\` first`
577
597
  );
578
598
  }
579
- const limit = Math.max(1, Math.min(500, options.limit ?? 50));
599
+ const limit = clampLimit(options.limit, { max: 500, fallback: 50 });
580
600
  const queryText = options.query.trim();
581
601
  if (!queryText) return [];
582
602
  const tantivy = requireTantivy();
@@ -610,9 +630,7 @@ function requireTantivy() {
610
630
  try {
611
631
  return require2("@oxdev03/node-tantivy-binding");
612
632
  } catch (error) {
613
- throw new Error(
614
- `tantivy engine is unavailable: ${error instanceof Error ? error.message : String(error)}`
615
- );
633
+ throw new Error(`tantivy engine is unavailable: ${getErrorMessage(error)}`);
616
634
  }
617
635
  }
618
636
  function getStoredText(doc, field) {
@@ -641,6 +659,8 @@ var require2;
641
659
  var init_search = __esm({
642
660
  "src/services/search.ts"() {
643
661
  "use strict";
662
+ init_errors();
663
+ init_limits();
644
664
  init_indexing();
645
665
  require2 = createRequire(import.meta.url);
646
666
  }
@@ -669,7 +689,7 @@ function sessionFilterWhere(filters) {
669
689
  }
670
690
  function listSessions(bundle, filters = {}) {
671
691
  const { where, params } = sessionFilterWhere(filters);
672
- const limit = Math.max(1, Math.min(1e3, filters.limit ?? 50));
692
+ const limit = clampLimit(filters.limit, { max: 1e3, fallback: 50 });
673
693
  const sql = `
674
694
  SELECT s.session_id,
675
695
  s.source_tool,
@@ -743,6 +763,7 @@ function getSession(bundle, sessionId2) {
743
763
  var init_sessions = __esm({
744
764
  "src/services/sessions.ts"() {
745
765
  "use strict";
766
+ init_limits();
746
767
  }
747
768
  });
748
769
 
@@ -1120,8 +1141,6 @@ var PROSA_PARSER_VERSION = "0.1.0";
1120
1141
  var PROSA_SCHEMA_VERSION = 2;
1121
1142
 
1122
1143
  // src/cli/commands/compile.ts
1123
- import os2 from "os";
1124
- import path14 from "path";
1125
1144
  import { Command } from "commander";
1126
1145
 
1127
1146
  // src/core/bundle.ts
@@ -1627,6 +1646,11 @@ function closeBundle(bundle) {
1627
1646
  closeDb(bundle.db);
1628
1647
  }
1629
1648
 
1649
+ // src/services/compile.ts
1650
+ init_errors();
1651
+ import os2 from "os";
1652
+ import path14 from "path";
1653
+
1630
1654
  // src/importers/claude/index.ts
1631
1655
  init_cas();
1632
1656
  init_db();
@@ -1676,6 +1700,9 @@ function importBatchId(sourceTool, startedAtIso) {
1676
1700
  return tupleId(["import_batch", sourceTool, startedAtIso, String(Math.random())]);
1677
1701
  }
1678
1702
 
1703
+ // src/importers/claude/index.ts
1704
+ init_errors();
1705
+
1679
1706
  // src/core/ingest/batch.ts
1680
1707
  init_cas();
1681
1708
  init_db();
@@ -1965,7 +1992,7 @@ async function compileClaude(bundle, root, options = {}) {
1965
1992
  );
1966
1993
  await recordError(bundle, batch.batch_id, {
1967
1994
  kind: "claude_file_failed",
1968
- message: error instanceof Error ? error.message : String(error),
1995
+ message: getErrorMessage(error),
1969
1996
  payload: { path: file.filePath }
1970
1997
  });
1971
1998
  }
@@ -2880,6 +2907,7 @@ init_cas();
2880
2907
  init_db();
2881
2908
  import { readFile as readFile5 } from "fs/promises";
2882
2909
  import path7 from "path";
2910
+ init_errors();
2883
2911
 
2884
2912
  // src/importers/codex/discover.ts
2885
2913
  import { readdir as readdir2 } from "fs/promises";
@@ -2929,7 +2957,7 @@ async function compileCodex(bundle, root, options = {}) {
2929
2957
  );
2930
2958
  await recordError(bundle, batch.batch_id, {
2931
2959
  kind: "codex_file_failed",
2932
- message: error instanceof Error ? error.message : String(error),
2960
+ message: getErrorMessage(error),
2933
2961
  payload: { path: filePath }
2934
2962
  });
2935
2963
  }
@@ -4020,6 +4048,7 @@ init_cas();
4020
4048
  init_db();
4021
4049
  import path9 from "path";
4022
4050
  import Database2 from "better-sqlite3";
4051
+ init_errors();
4023
4052
 
4024
4053
  // src/importers/cursor/discover.ts
4025
4054
  import { readdir as readdir3 } from "fs/promises";
@@ -4084,7 +4113,7 @@ async function compileCursor(bundle, root, options = {}) {
4084
4113
  );
4085
4114
  await recordError(bundle, batch.batch_id, {
4086
4115
  kind: "cursor_store_failed",
4087
- message: error instanceof Error ? error.message : String(error),
4116
+ message: getErrorMessage(error),
4088
4117
  payload: { path: store.filePath }
4089
4118
  });
4090
4119
  }
@@ -4739,6 +4768,7 @@ init_cas();
4739
4768
  init_db();
4740
4769
  import { readFile as readFile7 } from "fs/promises";
4741
4770
  import path11 from "path";
4771
+ init_errors();
4742
4772
 
4743
4773
  // src/importers/gemini/discover.ts
4744
4774
  import { readFile as readFile6, readdir as readdir4 } from "fs/promises";
@@ -4810,7 +4840,7 @@ async function compileGemini(bundle, root, options = {}) {
4810
4840
  );
4811
4841
  await recordError(bundle, batch.batch_id, {
4812
4842
  kind: "gemini_file_failed",
4813
- message: error instanceof Error ? error.message : String(error),
4843
+ message: getErrorMessage(error),
4814
4844
  payload: { path: file.filePath }
4815
4845
  });
4816
4846
  }
@@ -5496,6 +5526,7 @@ function flushPending4(bundle, pending) {
5496
5526
  import { mkdir as mkdir3, rm, writeFile as writeFile4 } from "fs/promises";
5497
5527
  import path12 from "path";
5498
5528
  import { DuckDBConnection } from "@duckdb/node-api";
5529
+ init_errors();
5499
5530
  var PARQUET_TABLES = [
5500
5531
  "objects",
5501
5532
  "source_files",
@@ -5593,7 +5624,7 @@ async function attachSqlite(connection, dbPath) {
5593
5624
  await connection.run(`ATTACH ${sqlString(dbPath)} AS prosa (TYPE sqlite)`);
5594
5625
  } catch (error) {
5595
5626
  throw new Error(
5596
- `DuckDB could not attach prosa.sqlite via the sqlite extension: ${error instanceof Error ? error.message : String(error)}`
5627
+ `DuckDB could not attach prosa.sqlite via the sqlite extension: ${getErrorMessage(error)}`
5597
5628
  );
5598
5629
  }
5599
5630
  }
@@ -5624,39 +5655,13 @@ function sqlString(value) {
5624
5655
  return `'${value.replace(/'/g, "''")}'`;
5625
5656
  }
5626
5657
  function isMissingParquetError(error) {
5627
- const message = error instanceof Error ? error.message : String(error);
5658
+ const message = getErrorMessage(error);
5628
5659
  return /No files found|does not exist|not found/i.test(message) && /\.parquet/i.test(message);
5629
5660
  }
5630
5661
 
5631
- // src/cli/commands/compile.ts
5662
+ // src/services/compile.ts
5632
5663
  init_indexing();
5633
-
5634
- // src/cli/logger.ts
5635
- import pino from "pino";
5636
- import pretty from "pino-pretty";
5637
- function createCliLogger(options) {
5638
- const loggerOptions = {
5639
- base: void 0,
5640
- level: options.verbose === true ? "debug" : "info"
5641
- };
5642
- if (options.jsonLogs === true) {
5643
- return pino(loggerOptions, pino.destination({ dest: 2, sync: true }));
5644
- }
5645
- return pino(
5646
- loggerOptions,
5647
- pretty({
5648
- colorize: process.stderr.isTTY,
5649
- destination: 2,
5650
- ignore: "pid,hostname",
5651
- singleLine: true,
5652
- sync: true,
5653
- translateTime: "SYS:yyyy-mm-dd HH:MM:ss.l"
5654
- })
5655
- );
5656
- }
5657
-
5658
- // src/cli/commands/compile.ts
5659
- var PROVIDERS = [
5664
+ var COMPILE_PROVIDERS = [
5660
5665
  {
5661
5666
  name: "codex",
5662
5667
  description: "Import Codex CLI session histories into the bundle.",
@@ -5686,13 +5691,129 @@ var PROVIDERS = [
5686
5691
  compile: compileCursor
5687
5692
  }
5688
5693
  ];
5694
+ function getCompileProvider(source) {
5695
+ const provider = COMPILE_PROVIDERS.find((p) => p.name === source);
5696
+ if (!provider) {
5697
+ throw new Error(`unknown compile source: ${source}`);
5698
+ }
5699
+ return provider;
5700
+ }
5701
+ function resolveCompilePath(p) {
5702
+ if (p === "~") return os2.homedir();
5703
+ if (p.startsWith("~/")) return path14.join(os2.homedir(), p.slice(2));
5704
+ return path14.resolve(p);
5705
+ }
5706
+ async function runCompileImports(options) {
5707
+ const { bundle, providers, deferIndex, logger } = options;
5708
+ let importedAny = false;
5709
+ const summaries = [];
5710
+ let tantivy = null;
5711
+ let tantivyError = null;
5712
+ try {
5713
+ if (deferIndex) {
5714
+ logger?.info("disabling FTS5 triggers for deferred indexing");
5715
+ disableFts5Triggers(bundle);
5716
+ }
5717
+ for (const provider of providers) {
5718
+ const sourcePath = resolveCompilePath(options.sessionsPath ?? provider.defaultSessionsPath());
5719
+ const providerLogger = logger?.child({
5720
+ source_tool: provider.name,
5721
+ source_path: sourcePath
5722
+ });
5723
+ providerLogger?.info("starting compile");
5724
+ const r = await provider.compile(bundle, sourcePath, { logger: providerLogger });
5725
+ importedAny ||= r.counts.source_files_imported > 0;
5726
+ providerLogger?.info(
5727
+ {
5728
+ batch_id: r.batch.batch_id,
5729
+ counts: r.counts
5730
+ },
5731
+ "compile finished"
5732
+ );
5733
+ const summary = {
5734
+ source: provider.name,
5735
+ sourcePath,
5736
+ batchId: r.batch.batch_id,
5737
+ batch: r.batch,
5738
+ counts: r.counts
5739
+ };
5740
+ summaries.push(summary);
5741
+ options.onProviderComplete?.(summary);
5742
+ }
5743
+ logger?.info({ changed: importedAny, fts5_deferred: deferIndex }, "marking indexes");
5744
+ markIndexesAfterImport(bundle, {
5745
+ changed: importedAny,
5746
+ fts5Deferred: deferIndex
5747
+ });
5748
+ if (importedAny) {
5749
+ try {
5750
+ logger?.info("rebuilding tantivy index");
5751
+ const status = await rebuildTantivyIndex(bundle);
5752
+ tantivy = { indexedDocCount: status.indexed_doc_count };
5753
+ options.onTantivyComplete?.(tantivy);
5754
+ } catch (error) {
5755
+ tantivyError = getErrorMessage(error);
5756
+ logger?.error({ err: error }, "tantivy rebuild failed; SQLite data is intact");
5757
+ }
5758
+ }
5759
+ } finally {
5760
+ if (deferIndex) {
5761
+ logger?.info("re-enabling FTS5 triggers");
5762
+ enableFts5Triggers(bundle);
5763
+ }
5764
+ }
5765
+ return {
5766
+ providers: summaries,
5767
+ importedAny,
5768
+ tantivy,
5769
+ tantivyError
5770
+ };
5771
+ }
5772
+ async function exportCompileParquet(options) {
5773
+ const storePath = resolveCompilePath(options.storePath);
5774
+ options.logger?.info({ store_path: storePath }, "exporting parquet");
5775
+ const result = await exportBundleParquet({ bundlePath: storePath });
5776
+ return {
5777
+ outDir: result.outDir,
5778
+ manifestPath: result.manifestPath,
5779
+ tableCount: Object.keys(result.files).length,
5780
+ files: result.files,
5781
+ counts: result.counts
5782
+ };
5783
+ }
5784
+
5785
+ // src/cli/logger.ts
5786
+ import pino from "pino";
5787
+ import pretty from "pino-pretty";
5788
+ function createCliLogger(options) {
5789
+ const loggerOptions = {
5790
+ base: void 0,
5791
+ level: options.verbose ? "debug" : "info"
5792
+ };
5793
+ if (options.jsonLogs) {
5794
+ return pino(loggerOptions, pino.destination({ dest: 2, sync: true }));
5795
+ }
5796
+ return pino(
5797
+ loggerOptions,
5798
+ pretty({
5799
+ colorize: process.stderr.isTTY,
5800
+ destination: 2,
5801
+ ignore: "pid,hostname",
5802
+ singleLine: true,
5803
+ sync: true,
5804
+ translateTime: "SYS:yyyy-mm-dd HH:MM:ss.l"
5805
+ })
5806
+ );
5807
+ }
5808
+
5809
+ // src/cli/commands/compile.ts
5689
5810
  function compileCommand() {
5690
5811
  const command = addCompileLogOptions(
5691
5812
  new Command("compile").description(
5692
5813
  "Import session histories from one agent CLI into the bundle."
5693
5814
  )
5694
5815
  );
5695
- for (const provider of PROVIDERS) {
5816
+ for (const provider of COMPILE_PROVIDERS) {
5696
5817
  command.addCommand(providerCompileCommand(provider));
5697
5818
  }
5698
5819
  command.action(() => {
@@ -5703,9 +5824,9 @@ function compileCommand() {
5703
5824
  function compileAllCommand() {
5704
5825
  return addCompileLogOptions(new Command("compile-all")).description("Import all agent CLI session histories using default source paths.").option("--defer-index", "skip immediate FTS5 updates; run `prosa index fts5` later").action(async (options) => {
5705
5826
  await runCompiles({
5706
- providers: PROVIDERS,
5827
+ providers: COMPILE_PROVIDERS,
5707
5828
  storePath: defaultBundlePath(),
5708
- deferIndex: options.deferIndex === true,
5829
+ deferIndex: options.deferIndex ?? false,
5709
5830
  logOptions: options
5710
5831
  });
5711
5832
  });
@@ -5720,7 +5841,7 @@ function providerCompileCommand(provider) {
5720
5841
  await runCompiles({
5721
5842
  providers: [provider],
5722
5843
  storePath: options.store,
5723
- deferIndex: options.deferIndex === true,
5844
+ deferIndex: options.deferIndex ?? false,
5724
5845
  sessionsPath: options.sessionsPath,
5725
5846
  logOptions: command.optsWithGlobals()
5726
5847
  });
@@ -5732,76 +5853,42 @@ function addCompileLogOptions(command) {
5732
5853
  }
5733
5854
  async function runCompiles(options) {
5734
5855
  const logger = createCliLogger(options.logOptions);
5735
- const storePath = resolvePath(options.storePath);
5856
+ const storePath = resolveCompilePath(options.storePath);
5736
5857
  logger.info({ store_path: storePath }, "opening bundle");
5737
5858
  const bundle = await openBundle(storePath);
5738
5859
  let importedAny = false;
5739
5860
  try {
5740
- if (options.deferIndex) {
5741
- logger.info("disabling FTS5 triggers for deferred indexing");
5742
- disableFts5Triggers(bundle);
5743
- }
5744
- for (const provider of options.providers) {
5745
- const sourcePath = resolvePath(options.sessionsPath ?? provider.defaultSessionsPath());
5746
- const providerLogger = logger.child({
5747
- source_tool: provider.name,
5748
- source_path: sourcePath
5749
- });
5750
- providerLogger.info("starting compile");
5751
- const r = await provider.compile(bundle, sourcePath, { logger: providerLogger });
5752
- importedAny ||= r.counts.source_files_imported > 0;
5753
- providerLogger.info(
5754
- {
5755
- batch_id: r.batch.batch_id,
5756
- counts: r.counts
5757
- },
5758
- "compile finished"
5759
- );
5760
- printCounts(provider.name, r.batch.batch_id, r.counts);
5761
- }
5762
- logger.info({ changed: importedAny, fts5_deferred: options.deferIndex }, "marking indexes");
5763
- markIndexesAfterImport(bundle, {
5764
- changed: importedAny,
5765
- fts5Deferred: options.deferIndex
5766
- });
5767
- if (importedAny) {
5768
- try {
5769
- logger.info("rebuilding tantivy index");
5770
- const status = await rebuildTantivyIndex(bundle);
5771
- process.stdout.write(`tantivy: indexed ${status.indexed_doc_count} docs
5861
+ const result = await runCompileImports({
5862
+ bundle,
5863
+ providers: options.providers,
5864
+ deferIndex: options.deferIndex,
5865
+ sessionsPath: options.sessionsPath,
5866
+ logger,
5867
+ onProviderComplete: printCounts,
5868
+ onTantivyComplete: (status) => {
5869
+ process.stdout.write(`tantivy: indexed ${status.indexedDocCount} docs
5772
5870
  `);
5773
- } catch (error) {
5774
- logger.error({ err: error }, "tantivy rebuild failed; SQLite data is intact");
5775
5871
  }
5776
- }
5872
+ });
5873
+ importedAny = result.importedAny;
5777
5874
  } finally {
5778
- if (options.deferIndex) {
5779
- logger.info("re-enabling FTS5 triggers");
5780
- enableFts5Triggers(bundle);
5781
- }
5782
5875
  closeBundle(bundle);
5783
5876
  logger.info({ store_path: storePath }, "bundle closed");
5784
5877
  }
5785
5878
  if (importedAny) {
5786
5879
  try {
5787
- logger.info({ store_path: storePath }, "exporting parquet");
5788
- const result = await exportBundleParquet({ bundlePath: storePath });
5789
- const tableCount = Object.keys(result.files).length;
5790
- process.stdout.write(`parquet: wrote ${tableCount} tables to ${result.outDir}
5880
+ const result = await exportCompileParquet({ storePath, logger });
5881
+ process.stdout.write(`parquet: wrote ${result.tableCount} tables to ${result.outDir}
5791
5882
  `);
5792
5883
  } catch (error) {
5793
5884
  logger.error({ err: error }, "parquet export failed; SQLite data is intact");
5794
5885
  }
5795
5886
  }
5796
5887
  }
5797
- function resolvePath(p) {
5798
- if (p === "~") return os2.homedir();
5799
- if (p.startsWith("~/")) return path14.join(os2.homedir(), p.slice(2));
5800
- return path14.resolve(p);
5801
- }
5802
- function printCounts(label, batchId, c) {
5888
+ function printCounts(summary) {
5889
+ const c = summary.counts;
5803
5890
  process.stdout.write(
5804
- `${label} import: batch=${batchId}
5891
+ `${summary.source} import: batch=${summary.batchId}
5805
5892
  source_files seen=${c.source_files_seen} imported=${c.source_files_imported} skipped=${c.source_files_skipped}
5806
5893
  sessions=${c.sessions} turns=${c.turns} messages=${c.messages} blocks=${c.content_blocks}
5807
5894
  events=${c.events} tool_calls=${c.tool_calls} tool_results=${c.tool_results}
@@ -5812,7 +5899,7 @@ function printCounts(label, batchId, c) {
5812
5899
 
5813
5900
  // src/cli/commands/export.ts
5814
5901
  import { writeFile as writeFile6 } from "fs/promises";
5815
- import path15 from "path";
5902
+ import path16 from "path";
5816
5903
  import { Command as Command2 } from "commander";
5817
5904
 
5818
5905
  // src/services/export/markdown.ts
@@ -5926,30 +6013,38 @@ function renderToolCall(c) {
5926
6013
  return lines.join("\n");
5927
6014
  }
5928
6015
 
6016
+ // src/cli/bundle.ts
6017
+ import path15 from "path";
6018
+ async function withBundle(storePath, fn) {
6019
+ const bundle = await openBundle(path15.resolve(storePath));
6020
+ try {
6021
+ return await fn(bundle);
6022
+ } finally {
6023
+ closeBundle(bundle);
6024
+ }
6025
+ }
6026
+
5929
6027
  // src/cli/commands/export.ts
5930
6028
  function exportCommand() {
5931
6029
  const session = new Command2("session").description("Export a single session to a human-readable format.").argument("<session-id>", "prosa session_id").requiredOption("--format <fmt>", 'currently only "markdown" is supported').option("--out <path>", "write to file instead of stdout").option("--store <path>", "bundle directory", defaultBundlePath()).action(async (sessionId2, options) => {
5932
6030
  if (options.format !== "markdown") {
5933
6031
  throw new Error(`unsupported format: ${options.format} (try --format markdown)`);
5934
6032
  }
5935
- const bundle = await openBundle(path15.resolve(options.store));
5936
- try {
6033
+ await withBundle(options.store, async (bundle) => {
5937
6034
  const markdown = await exportSessionMarkdown(bundle, sessionId2);
5938
6035
  if (options.out) {
5939
- await writeFile6(path15.resolve(options.out), markdown, "utf8");
5940
- process.stdout.write(`wrote ${path15.resolve(options.out)}
6036
+ await writeFile6(path16.resolve(options.out), markdown, "utf8");
6037
+ process.stdout.write(`wrote ${path16.resolve(options.out)}
5941
6038
  `);
5942
6039
  } else {
5943
6040
  process.stdout.write(markdown);
5944
6041
  }
5945
- } finally {
5946
- closeBundle(bundle);
5947
- }
6042
+ });
5948
6043
  });
5949
6044
  const parquet = new Command2("parquet").description("Export canonical tables to derived Parquet files for analytics.").option("--store <path>", "bundle directory", defaultBundlePath()).option("--out <path>", "output directory (default: <store>/parquet)").action(async (options) => {
5950
6045
  const result = await exportBundleParquet({
5951
- bundlePath: path15.resolve(options.store),
5952
- outDir: options.out ? path15.resolve(options.out) : void 0
6046
+ bundlePath: path16.resolve(options.store),
6047
+ outDir: options.out ? path16.resolve(options.out) : void 0
5953
6048
  });
5954
6049
  process.stdout.write(`wrote parquet export to ${result.outDir}
5955
6050
  `);
@@ -5960,12 +6055,13 @@ function exportCommand() {
5960
6055
  }
5961
6056
 
5962
6057
  // src/cli/commands/index.ts
5963
- import path16 from "path";
5964
6058
  import { Command as Command3 } from "commander";
5965
6059
  init_indexing();
5966
6060
 
5967
6061
  // src/cli/output.ts
5968
6062
  var OUTPUT_FORMATS = ["interactive", "table", "json", "csv"];
6063
+ var COL_SEPARATOR = " ";
6064
+ var RULE_CHAR = "-";
5969
6065
  function parseOutputFormat(value, fallback) {
5970
6066
  if (value === void 0) return fallback;
5971
6067
  if (OUTPUT_FORMATS.includes(value)) return value;
@@ -5993,11 +6089,12 @@ function printJson(rows, opts) {
5993
6089
  `);
5994
6090
  }
5995
6091
  function printCsv(rows, opts) {
5996
- const cols = opts.columns;
5997
- process.stdout.write(`${cols.map(csvField).join(",")}
6092
+ const columns = opts.columns;
6093
+ process.stdout.write(`${columns.map(csvField).join(",")}
5998
6094
  `);
5999
6095
  for (const row of rows) {
6000
- const line = cols.map((c) => csvField(formatCell(row[c]))).join(",");
6096
+ const record = row;
6097
+ const line = columns.map((column) => csvField(formatCell(record[column]))).join(",");
6001
6098
  process.stdout.write(`${line}
6002
6099
  `);
6003
6100
  }
@@ -6007,23 +6104,24 @@ function csvField(value) {
6007
6104
  return value;
6008
6105
  }
6009
6106
  function printTable(rows, opts) {
6010
- const cols = opts.columns;
6011
- const widths = cols.map((c) => c.length);
6012
- const cells = rows.map(
6013
- (row) => cols.map((col, i) => {
6014
- const text = formatCell(row[col]);
6015
- const w = widths[i] ?? 0;
6016
- if (text.length > w) widths[i] = text.length;
6107
+ const columns = opts.columns;
6108
+ const widths = columns.map((column) => column.length);
6109
+ const cells = rows.map((row) => {
6110
+ const record = row;
6111
+ return columns.map((column, index) => {
6112
+ const text = formatCell(record[column]);
6113
+ const width = widths[index] ?? 0;
6114
+ if (text.length > width) widths[index] = text.length;
6017
6115
  return text;
6018
- })
6019
- );
6020
- const header = cols.map((c, i) => c.padEnd(widths[i] ?? 0)).join(" ");
6021
- const sep = cols.map((_, i) => "-".repeat(widths[i] ?? 0)).join(" ");
6116
+ });
6117
+ });
6118
+ const header = columns.map((column, index) => column.padEnd(widths[index] ?? 0)).join(COL_SEPARATOR);
6119
+ const rule = columns.map((_, index) => RULE_CHAR.repeat(widths[index] ?? 0)).join(COL_SEPARATOR);
6022
6120
  process.stdout.write(`${header}
6023
- ${sep}
6121
+ ${rule}
6024
6122
  `);
6025
6123
  for (const cellRow of cells) {
6026
- const line = cellRow.map((c, i) => c.padEnd(widths[i] ?? 0)).join(" ");
6124
+ const line = cellRow.map((cell, index) => cell.padEnd(widths[index] ?? 0)).join(COL_SEPARATOR);
6027
6125
  process.stdout.write(`${line}
6028
6126
  `);
6029
6127
  }
@@ -6038,27 +6136,18 @@ function formatCell(value) {
6038
6136
  // src/cli/commands/index.ts
6039
6137
  function indexCommand() {
6040
6138
  const fts5 = new Command3("fts5").description("Rebuild the SQLite FTS5 index from search_docs.").option("--store <path>", "bundle directory", defaultBundlePath()).action(async (options) => {
6041
- const bundle = await openBundle(path16.resolve(options.store));
6042
- try {
6043
- const status2 = rebuildFts5Index(bundle);
6044
- printIndexStatus(status2);
6045
- } finally {
6046
- closeBundle(bundle);
6047
- }
6139
+ await withBundle(options.store, (bundle) => {
6140
+ printIndexStatus(rebuildFts5Index(bundle));
6141
+ });
6048
6142
  });
6049
6143
  const tantivy = new Command3("tantivy").description("Rebuild the Tantivy sidecar index from search_docs.").option("--store <path>", "bundle directory", defaultBundlePath()).action(async (options) => {
6050
- const bundle = await openBundle(path16.resolve(options.store));
6051
- try {
6052
- const status2 = await rebuildTantivyIndex(bundle);
6053
- printIndexStatus(status2);
6054
- } finally {
6055
- closeBundle(bundle);
6056
- }
6144
+ await withBundle(options.store, async (bundle) => {
6145
+ printIndexStatus(await rebuildTantivyIndex(bundle));
6146
+ });
6057
6147
  });
6058
6148
  const status = new Command3("status").description("Show derived search index status.").option("--store <path>", "bundle directory", defaultBundlePath()).option("--output-format <fmt>", "interactive|table|json|csv", "table").action(async (options) => {
6059
6149
  const format = parseOutputFormat(options.outputFormat, "table");
6060
- const bundle = await openBundle(path16.resolve(options.store));
6061
- try {
6150
+ await withBundle(options.store, (bundle) => {
6062
6151
  const rows = getSearchIndexStatuses(bundle);
6063
6152
  printRows(rows, {
6064
6153
  format,
@@ -6071,9 +6160,7 @@ function indexCommand() {
6071
6160
  "error_message"
6072
6161
  ]
6073
6162
  });
6074
- } finally {
6075
- closeBundle(bundle);
6076
- }
6163
+ });
6077
6164
  });
6078
6165
  return new Command3("index").description("Build or inspect derived search indexes.").addCommand(fts5).addCommand(tantivy).addCommand(status);
6079
6166
  }
@@ -6120,6 +6207,7 @@ import path18 from "path";
6120
6207
  import { Command as Command5 } from "commander";
6121
6208
 
6122
6209
  // src/mcp/server.ts
6210
+ init_errors();
6123
6211
  import { randomUUID } from "crypto";
6124
6212
  import http from "http";
6125
6213
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
@@ -6128,10 +6216,13 @@ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/
6128
6216
 
6129
6217
  // src/mcp/guidance.ts
6130
6218
  var PROSA_MCP_INSTRUCTIONS = `
6131
- prosa is a read-only memory over local agent session histories. Use it to find prior work,
6132
- commands, decisions, file touches, and full transcripts before answering from memory.
6219
+ prosa is a local memory over local agent session histories. Use it to import recent sessions,
6220
+ find prior work, commands, decisions, file touches, and full transcripts before answering from
6221
+ memory.
6133
6222
 
6134
6223
  Recommended workflow:
6224
+ - Use compile to refresh the bundle when recent local sessions may not be indexed yet. With no
6225
+ input it imports all supported providers from default paths.
6135
6226
  - For open-ended questions, start with search_sessions using 2-5 concrete terms.
6136
6227
  - For questions about a file or path, start with find_touched_files, then inspect the returned sessions.
6137
6228
  - After search results, call get_session for the most relevant session_ids before drawing conclusions.
@@ -6176,19 +6267,95 @@ Use this workflow:
6176
6267
 
6177
6268
  // src/mcp/tools.ts
6178
6269
  import { z } from "zod";
6270
+
6271
+ // src/core/domain/types.ts
6272
+ var SOURCE_TOOLS = ["cursor", "codex", "claude", "gemini"];
6273
+
6274
+ // src/mcp/tools.ts
6275
+ init_errors();
6276
+ init_limits();
6179
6277
  init_indexing();
6180
6278
  init_search();
6181
6279
  init_sessions();
6182
6280
  function registerProsaTools(server, bundle, options = {}) {
6183
6281
  const searchEngine = options.searchEngine ?? "fts5";
6282
+ const storePath = options.storePath ?? bundle.path;
6184
6283
  registerProsaPrompts(server);
6284
+ server.registerTool(
6285
+ "compile",
6286
+ {
6287
+ title: "Compile sessions",
6288
+ description: "Import local agent session histories into the active prosa bundle. With no input, compiles all providers from default paths. With source, compiles that provider; sessions_path may override that provider path.",
6289
+ inputSchema: {
6290
+ source: z.enum(SOURCE_TOOLS).optional(),
6291
+ sessions_path: z.string().min(1).optional()
6292
+ },
6293
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true }
6294
+ },
6295
+ async ({ source, sessions_path }) => {
6296
+ if (sessions_path && !source) {
6297
+ return {
6298
+ content: [
6299
+ {
6300
+ type: "text",
6301
+ text: "sessions_path requires source because providers use incompatible source layouts"
6302
+ }
6303
+ ],
6304
+ isError: true
6305
+ };
6306
+ }
6307
+ try {
6308
+ const result = await runCompileImports({
6309
+ bundle,
6310
+ providers: source ? [getCompileProvider(source)] : COMPILE_PROVIDERS,
6311
+ deferIndex: false,
6312
+ sessionsPath: sessions_path
6313
+ });
6314
+ const parquet = result.importedAny ? await exportCompileParquet({ storePath }) : null;
6315
+ return {
6316
+ content: [
6317
+ {
6318
+ type: "text",
6319
+ text: JSON.stringify(
6320
+ {
6321
+ providers: result.providers.map((provider) => ({
6322
+ source: provider.source,
6323
+ source_path: provider.sourcePath,
6324
+ batch_id: provider.batchId,
6325
+ counts: provider.counts
6326
+ })),
6327
+ imported_any: result.importedAny,
6328
+ tantivy: result.tantivy ? { indexed_doc_count: result.tantivy.indexedDocCount } : null,
6329
+ tantivy_error: result.tantivyError,
6330
+ parquet: parquet ? {
6331
+ out_dir: parquet.outDir,
6332
+ manifest_path: parquet.manifestPath,
6333
+ table_count: parquet.tableCount,
6334
+ files: parquet.files,
6335
+ counts: parquet.counts
6336
+ } : null
6337
+ },
6338
+ null,
6339
+ 2
6340
+ )
6341
+ }
6342
+ ]
6343
+ };
6344
+ } catch (error) {
6345
+ return {
6346
+ content: [{ type: "text", text: getErrorMessage(error) }],
6347
+ isError: true
6348
+ };
6349
+ }
6350
+ }
6351
+ );
6185
6352
  server.registerTool(
6186
6353
  "list_sessions",
6187
6354
  {
6188
6355
  title: "List sessions",
6189
6356
  description: "List recent sessions when you need candidates by source/date before deeper inspection. Next step: call get_session for relevant session_id values.",
6190
6357
  inputSchema: {
6191
- source: z.enum(["cursor", "codex", "claude", "gemini"]).optional(),
6358
+ source: z.enum(SOURCE_TOOLS).optional(),
6192
6359
  since: z.string().optional().describe("ISO timestamp lower bound (inclusive)"),
6193
6360
  until: z.string().optional().describe("ISO timestamp upper bound (exclusive)"),
6194
6361
  limit: z.number().int().min(1).max(500).optional().default(50)
@@ -6274,7 +6441,7 @@ function registerProsaTools(server, bundle, options = {}) {
6274
6441
  return { content: [{ type: "text", text: md }] };
6275
6442
  } catch (error) {
6276
6443
  return {
6277
- content: [{ type: "text", text: error instanceof Error ? error.message : String(error) }],
6444
+ content: [{ type: "text", text: getErrorMessage(error) }],
6278
6445
  isError: true
6279
6446
  };
6280
6447
  }
@@ -6333,7 +6500,7 @@ function registerProsaTools(server, bundle, options = {}) {
6333
6500
  LEFT JOIN tool_results tr ON tr.tool_call_id = tc.tool_call_id
6334
6501
  ${where}
6335
6502
  ORDER BY tc.timestamp_start DESC
6336
- LIMIT ${Math.max(1, Math.min(500, limit ?? 100))}
6503
+ LIMIT ${clampLimit(limit, { max: 500, fallback: 100 })}
6337
6504
  `;
6338
6505
  const rows = bundle.db.prepare(sql).all(...params);
6339
6506
  return {
@@ -6364,7 +6531,7 @@ function registerProsaTools(server, bundle, options = {}) {
6364
6531
  FROM artifacts a
6365
6532
  WHERE a.path IS NOT NULL AND a.path LIKE ?
6366
6533
  ORDER BY timestamp_start DESC
6367
- LIMIT ${Math.max(1, Math.min(500, limit ?? 100))}
6534
+ LIMIT ${clampLimit(limit, { max: 500, fallback: 100 })}
6368
6535
  `;
6369
6536
  const like = `%${path_substring}%`;
6370
6537
  const rows = bundle.db.prepare(sql).all(like, like);
@@ -6452,14 +6619,14 @@ function registerProsaPrompts(server) {
6452
6619
  path: z.string().min(1).describe("File path, directory, or distinctive path suffix")
6453
6620
  }
6454
6621
  },
6455
- ({ path: path23 }) => ({
6622
+ ({ path: path20 }) => ({
6456
6623
  description: "Find sessions that touched a path and summarize the evidence.",
6457
6624
  messages: [
6458
6625
  {
6459
6626
  role: "user",
6460
6627
  content: {
6461
6628
  type: "text",
6462
- text: FIND_FILE_HISTORY_PROMPT.replace("{{path}}", path23)
6629
+ text: FIND_FILE_HISTORY_PROMPT.replace("{{path}}", path20)
6463
6630
  }
6464
6631
  }
6465
6632
  ]
@@ -6494,7 +6661,7 @@ function registerProsaPrompts(server) {
6494
6661
 
6495
6662
  // src/mcp/server.ts
6496
6663
  async function listenMcpStdioServer(bundle, options = {}) {
6497
- const server = createMcpServer(bundle, options.searchEngine ?? "fts5");
6664
+ const server = createMcpServer(bundle, options.searchEngine ?? "fts5", options.storePath);
6498
6665
  const transport = new StdioServerTransport();
6499
6666
  await server.connect(transport);
6500
6667
  return {
@@ -6508,10 +6675,13 @@ async function listenMcpServer(bundle, options) {
6508
6675
  const mcpPath = options.path ?? "/mcp";
6509
6676
  const sessions = /* @__PURE__ */ new Map();
6510
6677
  const searchEngine = options.searchEngine ?? "fts5";
6678
+ const storePath = options.storePath ?? bundle.path;
6511
6679
  const httpServer = http.createServer((req, res) => {
6512
- handleRequest(req, res, mcpPath, sessions, bundle, searchEngine).catch((error) => {
6513
- writeError(res, error);
6514
- });
6680
+ handleRequest(req, res, mcpPath, sessions, bundle, searchEngine, storePath).catch(
6681
+ (error) => {
6682
+ writeError(res, error);
6683
+ }
6684
+ );
6515
6685
  });
6516
6686
  await new Promise((resolve, reject) => {
6517
6687
  httpServer.once("error", reject);
@@ -6534,7 +6704,7 @@ async function listenMcpServer(bundle, options) {
6534
6704
  }
6535
6705
  };
6536
6706
  }
6537
- async function handleRequest(req, res, mcpPath, sessions, bundle, searchEngine) {
6707
+ async function handleRequest(req, res, mcpPath, sessions, bundle, searchEngine, storePath) {
6538
6708
  if (!req.url || !req.url.startsWith(mcpPath)) {
6539
6709
  res.writeHead(404).end();
6540
6710
  return;
@@ -6556,14 +6726,14 @@ async function handleRequest(req, res, mcpPath, sessions, bundle, searchEngine)
6556
6726
  res.writeHead(404).end();
6557
6727
  return;
6558
6728
  }
6559
- entry = await openSession(bundle, sessions, searchEngine);
6729
+ entry = await openSession(bundle, sessions, searchEngine, storePath);
6560
6730
  }
6561
6731
  const bodyText = await readBody(req);
6562
6732
  const body = bodyText.length > 0 ? safeJsonParse(bodyText) : void 0;
6563
6733
  await entry.transport.handleRequest(req, res, body);
6564
6734
  }
6565
- async function openSession(bundle, store, searchEngine) {
6566
- const server = createMcpServer(bundle, searchEngine);
6735
+ async function openSession(bundle, store, searchEngine, storePath) {
6736
+ const server = createMcpServer(bundle, searchEngine, storePath);
6567
6737
  const transport = new StreamableHTTPServerTransport({
6568
6738
  sessionIdGenerator: () => randomUUID(),
6569
6739
  onsessioninitialized: (id) => {
@@ -6581,7 +6751,7 @@ async function openSession(bundle, store, searchEngine) {
6581
6751
  await server.connect(transport);
6582
6752
  return { server, transport };
6583
6753
  }
6584
- function createMcpServer(bundle, searchEngine) {
6754
+ function createMcpServer(bundle, searchEngine, storePath) {
6585
6755
  const server = new McpServer(
6586
6756
  {
6587
6757
  name: "prosa",
@@ -6589,7 +6759,7 @@ function createMcpServer(bundle, searchEngine) {
6589
6759
  },
6590
6760
  { instructions: PROSA_MCP_INSTRUCTIONS }
6591
6761
  );
6592
- registerProsaTools(server, bundle, { searchEngine });
6762
+ registerProsaTools(server, bundle, { searchEngine, storePath });
6593
6763
  return server;
6594
6764
  }
6595
6765
  async function readBody(req) {
@@ -6620,17 +6790,33 @@ function writeError(res, error) {
6620
6790
  res.end(
6621
6791
  JSON.stringify({
6622
6792
  jsonrpc: "2.0",
6623
- error: { code: -32603, message: error instanceof Error ? error.message : String(error) },
6793
+ error: { code: -32603, message: getErrorMessage(error) },
6624
6794
  id: null
6625
6795
  })
6626
6796
  );
6627
6797
  }
6628
6798
 
6799
+ // src/cli/parsers.ts
6800
+ function parseSearchEngine(value) {
6801
+ if (value === "fts5" || value === "tantivy") return value;
6802
+ throw new Error(`invalid search engine: ${value} (expected fts5 or tantivy)`);
6803
+ }
6804
+ function parseMcpTransport(value) {
6805
+ if (value === "stdio" || value === "http") return value;
6806
+ throw new Error(`invalid transport: ${value} (expected stdio or http)`);
6807
+ }
6808
+ function parseSourceTool(value) {
6809
+ if (value === void 0) return void 0;
6810
+ if (SOURCE_TOOLS.includes(value)) return value;
6811
+ throw new Error(`invalid source tool: ${value} (expected one of ${SOURCE_TOOLS.join(", ")})`);
6812
+ }
6813
+
6629
6814
  // src/cli/commands/mcp.ts
6630
6815
  function mcpCommand() {
6631
6816
  const serve = new Command5("serve").description("Start a local MCP server over the prosa bundle.").option("--store <path>", "bundle directory", defaultBundlePath()).option("--transport <transport>", "MCP transport: stdio|http", "stdio").option("--host <host>", "bind host", "127.0.0.1").option("--port <port>", "bind port", "7331").option("--path <path>", "HTTP path", "/mcp").option("--search-engine <engine>", "search engine: fts5|tantivy", "fts5").action(
6632
6817
  async (options) => {
6633
- const bundle = await openBundle(path18.resolve(options.store));
6818
+ const storePath = path18.resolve(options.store);
6819
+ const bundle = await openBundle(storePath);
6634
6820
  try {
6635
6821
  const transport = parseMcpTransport(options.transport);
6636
6822
  const searchEngine = parseSearchEngine(options.searchEngine);
@@ -6643,7 +6829,8 @@ function mcpCommand() {
6643
6829
  host: options.host,
6644
6830
  port,
6645
6831
  path: options.path,
6646
- searchEngine
6832
+ searchEngine,
6833
+ storePath
6647
6834
  });
6648
6835
  process.stdout.write(`prosa mcp server listening at ${server2.url}
6649
6836
  `);
@@ -6651,7 +6838,7 @@ function mcpCommand() {
6651
6838
  registerShutdown(server2.close, bundle);
6652
6839
  return;
6653
6840
  }
6654
- const server = await listenMcpStdioServer(bundle, { searchEngine });
6841
+ const server = await listenMcpStdioServer(bundle, { searchEngine, storePath });
6655
6842
  registerShutdown(server.close, bundle);
6656
6843
  } catch (error) {
6657
6844
  closeBundle(bundle);
@@ -6661,14 +6848,6 @@ function mcpCommand() {
6661
6848
  );
6662
6849
  return new Command5("mcp").description("MCP server commands.").addCommand(serve);
6663
6850
  }
6664
- function parseMcpTransport(value) {
6665
- if (value === "stdio" || value === "http") return value;
6666
- throw new Error(`invalid --transport: ${value} (expected stdio or http)`);
6667
- }
6668
- function parseSearchEngine(value) {
6669
- if (value === "fts5" || value === "tantivy") return value;
6670
- throw new Error(`invalid --search-engine: ${value} (expected fts5 or tantivy)`);
6671
- }
6672
6851
  function registerShutdown(closeServer, bundle) {
6673
6852
  const shutdown = async () => {
6674
6853
  await closeServer();
@@ -6690,7 +6869,7 @@ function queryCommand() {
6690
6869
  const duckdb = new Command6("duckdb").description("Run a DuckDB SQL query over exported Parquet tables.").argument("<sql>", "DuckDB SQL query").option("--store <path>", "bundle directory", defaultBundlePath()).option("--parquet-dir <path>", "Parquet directory (default: <store>/parquet)").option("--output-format <fmt>", "interactive|table|json|csv", "table").action(
6691
6870
  async (sql, options) => {
6692
6871
  const format = parseOutputFormat(options.outputFormat, "table");
6693
- const parquetDir = options.parquetDir ? path19.resolve(options.parquetDir) : await defaultParquetDir(path19.resolve(options.store));
6872
+ const parquetDir = options.parquetDir ? path19.resolve(options.parquetDir) : await withBundle(options.store, (bundle) => bundle.paths.parquet);
6694
6873
  const result = await queryDuckDbParquet({ parquetDir, sql });
6695
6874
  printRows(result.rows, {
6696
6875
  format,
@@ -6701,26 +6880,16 @@ function queryCommand() {
6701
6880
  );
6702
6881
  return new Command6("query").description("Run derived analytical queries.").addCommand(duckdb);
6703
6882
  }
6704
- async function defaultParquetDir(storePath) {
6705
- const bundle = await openBundle(storePath);
6706
- try {
6707
- return bundle.paths.parquet;
6708
- } finally {
6709
- closeBundle(bundle);
6710
- }
6711
- }
6712
6883
 
6713
6884
  // src/cli/commands/search.ts
6714
- import path20 from "path";
6715
6885
  import { Command as Command7 } from "commander";
6716
6886
  init_search();
6717
6887
  function searchCommand() {
6718
6888
  return new Command7("search").description("Full-text search across messages, tool calls and tool outputs.").argument("<query>", "FTS5 query string (supports MATCH syntax)").option("--store <path>", "bundle directory", defaultBundlePath()).option("--limit <n>", "maximum hits", "50").option("--engine <engine>", "search engine: fts5|tantivy", "fts5").option("--output-format <fmt>", "interactive|table|json|csv", "table").action(
6719
6889
  async (query, options) => {
6720
- const engine = parseSearchEngine2(options.engine);
6890
+ const engine = parseSearchEngine(options.engine);
6721
6891
  const format = parseOutputFormat(options.outputFormat, "table");
6722
- const bundle = await openBundle(path20.resolve(options.store));
6723
- try {
6892
+ await withBundle(options.store, (bundle) => {
6724
6893
  const hits = searchFullText(bundle, {
6725
6894
  query,
6726
6895
  limit: Number.parseInt(options.limit, 10),
@@ -6731,29 +6900,21 @@ function searchCommand() {
6731
6900
  columns: ["timestamp", "role", "tool_name", "session_id", "snippet"],
6732
6901
  meta: { query, engine, count: hits.length }
6733
6902
  });
6734
- } finally {
6735
- closeBundle(bundle);
6736
- }
6903
+ });
6737
6904
  }
6738
6905
  );
6739
6906
  }
6740
- function parseSearchEngine2(value) {
6741
- if (value === "fts5" || value === "tantivy") return value;
6742
- throw new Error(`invalid --engine: ${value} (expected fts5 or tantivy)`);
6743
- }
6744
6907
 
6745
6908
  // src/cli/commands/sessions.ts
6746
- import path21 from "path";
6747
6909
  import { Command as Command8 } from "commander";
6748
6910
  init_sessions();
6749
6911
  function sessionsCommand() {
6750
6912
  const command = new Command8("sessions").description("List sessions in the bundle, with filters.").enablePositionalOptions().option("--store <path>", "bundle directory", defaultBundlePath()).option("--source <tool>", "filter by source tool: cursor|codex|claude|gemini").option("--since <iso>", "sessions starting on/after this ISO timestamp").option("--until <iso>", "sessions starting before this ISO timestamp").option("--limit <n>", "maximum rows", "50").option("--output-format <fmt>", "interactive|table|json|csv", "table").action(
6751
6913
  async (options) => {
6752
6914
  const format = parseOutputFormat(options.outputFormat, "table");
6753
- const bundle = await openBundle(path21.resolve(options.store));
6754
- try {
6915
+ await withBundle(options.store, (bundle) => {
6755
6916
  const rows = listSessions(bundle, {
6756
- sourceTool: options.source,
6917
+ sourceTool: parseSourceTool(options.source),
6757
6918
  sinceIso: options.since,
6758
6919
  untilIso: options.until,
6759
6920
  limit: Number.parseInt(options.limit, 10)
@@ -6771,26 +6932,21 @@ function sessionsCommand() {
6771
6932
  "title"
6772
6933
  ]
6773
6934
  });
6774
- } finally {
6775
- closeBundle(bundle);
6776
- }
6935
+ });
6777
6936
  }
6778
6937
  );
6779
6938
  command.addCommand(
6780
6939
  new Command8("count").description("Count sessions in the bundle, with filters.").option("--store <path>", "bundle directory", defaultBundlePath()).option("--source <tool>", "filter by source tool: cursor|codex|claude|gemini").option("--since <iso>", "sessions starting on/after this ISO timestamp").option("--until <iso>", "sessions starting before this ISO timestamp").action(
6781
6940
  async (options) => {
6782
- const bundle = await openBundle(path21.resolve(options.store));
6783
- try {
6941
+ await withBundle(options.store, (bundle) => {
6784
6942
  const count = countSessions(bundle, {
6785
- sourceTool: options.source,
6943
+ sourceTool: parseSourceTool(options.source),
6786
6944
  sinceIso: options.since,
6787
6945
  untilIso: options.until
6788
6946
  });
6789
6947
  process.stdout.write(`${count}
6790
6948
  `);
6791
- } finally {
6792
- closeBundle(bundle);
6793
- }
6949
+ });
6794
6950
  }
6795
6951
  )
6796
6952
  );
@@ -6798,7 +6954,6 @@ function sessionsCommand() {
6798
6954
  }
6799
6955
 
6800
6956
  // src/cli/commands/tui.ts
6801
- import path22 from "path";
6802
6957
  import { Command as Command9 } from "commander";
6803
6958
  function tuiCommand() {
6804
6959
  return new Command9("tui").description("Open the interactive Ink-based explorer.").option("--store <path>", "bundle directory", defaultBundlePath()).action(async (options) => {
@@ -6807,14 +6962,11 @@ function tuiCommand() {
6807
6962
  import("react"),
6808
6963
  Promise.resolve().then(() => (init_App(), App_exports))
6809
6964
  ]);
6810
- const bundle = await openBundle(path22.resolve(options.store));
6811
- try {
6965
+ await withBundle(options.store, async (bundle) => {
6812
6966
  console.clear();
6813
6967
  const app = render(React.createElement(App2, { bundle }));
6814
6968
  await app.waitUntilExit();
6815
- } finally {
6816
- closeBundle(bundle);
6817
- }
6969
+ });
6818
6970
  });
6819
6971
  }
6820
6972