@c3-oss/prosa 0.3.0 → 0.3.2

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/bin/prosa.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
 
@@ -1621,11 +1642,24 @@ async function openBundle(rootPath) {
1621
1642
  }
1622
1643
  return { path: resolved, db, manifest, paths };
1623
1644
  }
1645
+ async function openOrInitBundle(rootPath) {
1646
+ const resolved = path.resolve(rootPath);
1647
+ const paths = bundlePaths(resolved);
1648
+ const dirStat = await stat(resolved).catch(() => null);
1649
+ if (dirStat && !dirStat.isDirectory()) {
1650
+ throw new Error(`bundle path not found or not a directory: ${resolved}`);
1651
+ }
1652
+ if (!dirStat || !await exists(paths.manifest)) {
1653
+ return await initBundle(resolved);
1654
+ }
1655
+ return await openBundle(resolved);
1656
+ }
1624
1657
  function closeBundle(bundle) {
1625
1658
  closeDb(bundle.db);
1626
1659
  }
1627
1660
 
1628
1661
  // src/services/compile.ts
1662
+ init_errors();
1629
1663
  import os2 from "os";
1630
1664
  import path14 from "path";
1631
1665
 
@@ -1678,6 +1712,9 @@ function importBatchId(sourceTool, startedAtIso) {
1678
1712
  return tupleId(["import_batch", sourceTool, startedAtIso, String(Math.random())]);
1679
1713
  }
1680
1714
 
1715
+ // src/importers/claude/index.ts
1716
+ init_errors();
1717
+
1681
1718
  // src/core/ingest/batch.ts
1682
1719
  init_cas();
1683
1720
  init_db();
@@ -1967,7 +2004,7 @@ async function compileClaude(bundle, root, options = {}) {
1967
2004
  );
1968
2005
  await recordError(bundle, batch.batch_id, {
1969
2006
  kind: "claude_file_failed",
1970
- message: error instanceof Error ? error.message : String(error),
2007
+ message: getErrorMessage(error),
1971
2008
  payload: { path: file.filePath }
1972
2009
  });
1973
2010
  }
@@ -2882,6 +2919,7 @@ init_cas();
2882
2919
  init_db();
2883
2920
  import { readFile as readFile5 } from "fs/promises";
2884
2921
  import path7 from "path";
2922
+ init_errors();
2885
2923
 
2886
2924
  // src/importers/codex/discover.ts
2887
2925
  import { readdir as readdir2 } from "fs/promises";
@@ -2931,7 +2969,7 @@ async function compileCodex(bundle, root, options = {}) {
2931
2969
  );
2932
2970
  await recordError(bundle, batch.batch_id, {
2933
2971
  kind: "codex_file_failed",
2934
- message: error instanceof Error ? error.message : String(error),
2972
+ message: getErrorMessage(error),
2935
2973
  payload: { path: filePath }
2936
2974
  });
2937
2975
  }
@@ -4022,6 +4060,7 @@ init_cas();
4022
4060
  init_db();
4023
4061
  import path9 from "path";
4024
4062
  import Database2 from "better-sqlite3";
4063
+ init_errors();
4025
4064
 
4026
4065
  // src/importers/cursor/discover.ts
4027
4066
  import { readdir as readdir3 } from "fs/promises";
@@ -4086,7 +4125,7 @@ async function compileCursor(bundle, root, options = {}) {
4086
4125
  );
4087
4126
  await recordError(bundle, batch.batch_id, {
4088
4127
  kind: "cursor_store_failed",
4089
- message: error instanceof Error ? error.message : String(error),
4128
+ message: getErrorMessage(error),
4090
4129
  payload: { path: store.filePath }
4091
4130
  });
4092
4131
  }
@@ -4741,6 +4780,7 @@ init_cas();
4741
4780
  init_db();
4742
4781
  import { readFile as readFile7 } from "fs/promises";
4743
4782
  import path11 from "path";
4783
+ init_errors();
4744
4784
 
4745
4785
  // src/importers/gemini/discover.ts
4746
4786
  import { readFile as readFile6, readdir as readdir4 } from "fs/promises";
@@ -4812,7 +4852,7 @@ async function compileGemini(bundle, root, options = {}) {
4812
4852
  );
4813
4853
  await recordError(bundle, batch.batch_id, {
4814
4854
  kind: "gemini_file_failed",
4815
- message: error instanceof Error ? error.message : String(error),
4855
+ message: getErrorMessage(error),
4816
4856
  payload: { path: file.filePath }
4817
4857
  });
4818
4858
  }
@@ -5498,6 +5538,7 @@ function flushPending4(bundle, pending) {
5498
5538
  import { mkdir as mkdir3, rm, writeFile as writeFile4 } from "fs/promises";
5499
5539
  import path12 from "path";
5500
5540
  import { DuckDBConnection } from "@duckdb/node-api";
5541
+ init_errors();
5501
5542
  var PARQUET_TABLES = [
5502
5543
  "objects",
5503
5544
  "source_files",
@@ -5595,7 +5636,7 @@ async function attachSqlite(connection, dbPath) {
5595
5636
  await connection.run(`ATTACH ${sqlString(dbPath)} AS prosa (TYPE sqlite)`);
5596
5637
  } catch (error) {
5597
5638
  throw new Error(
5598
- `DuckDB could not attach prosa.sqlite via the sqlite extension: ${error instanceof Error ? error.message : String(error)}`
5639
+ `DuckDB could not attach prosa.sqlite via the sqlite extension: ${getErrorMessage(error)}`
5599
5640
  );
5600
5641
  }
5601
5642
  }
@@ -5626,7 +5667,7 @@ function sqlString(value) {
5626
5667
  return `'${value.replace(/'/g, "''")}'`;
5627
5668
  }
5628
5669
  function isMissingParquetError(error) {
5629
- const message = error instanceof Error ? error.message : String(error);
5670
+ const message = getErrorMessage(error);
5630
5671
  return /No files found|does not exist|not found/i.test(message) && /\.parquet/i.test(message);
5631
5672
  }
5632
5673
 
@@ -5723,7 +5764,7 @@ async function runCompileImports(options) {
5723
5764
  tantivy = { indexedDocCount: status.indexed_doc_count };
5724
5765
  options.onTantivyComplete?.(tantivy);
5725
5766
  } catch (error) {
5726
- tantivyError = error instanceof Error ? error.message : String(error);
5767
+ tantivyError = getErrorMessage(error);
5727
5768
  logger?.error({ err: error }, "tantivy rebuild failed; SQLite data is intact");
5728
5769
  }
5729
5770
  }
@@ -5759,9 +5800,9 @@ import pretty from "pino-pretty";
5759
5800
  function createCliLogger(options) {
5760
5801
  const loggerOptions = {
5761
5802
  base: void 0,
5762
- level: options.verbose === true ? "debug" : "info"
5803
+ level: options.verbose ? "debug" : "info"
5763
5804
  };
5764
- if (options.jsonLogs === true) {
5805
+ if (options.jsonLogs) {
5765
5806
  return pino(loggerOptions, pino.destination({ dest: 2, sync: true }));
5766
5807
  }
5767
5808
  return pino(
@@ -5797,7 +5838,7 @@ function compileAllCommand() {
5797
5838
  await runCompiles({
5798
5839
  providers: COMPILE_PROVIDERS,
5799
5840
  storePath: defaultBundlePath(),
5800
- deferIndex: options.deferIndex === true,
5841
+ deferIndex: options.deferIndex ?? false,
5801
5842
  logOptions: options
5802
5843
  });
5803
5844
  });
@@ -5812,7 +5853,7 @@ function providerCompileCommand(provider) {
5812
5853
  await runCompiles({
5813
5854
  providers: [provider],
5814
5855
  storePath: options.store,
5815
- deferIndex: options.deferIndex === true,
5856
+ deferIndex: options.deferIndex ?? false,
5816
5857
  sessionsPath: options.sessionsPath,
5817
5858
  logOptions: command.optsWithGlobals()
5818
5859
  });
@@ -5870,7 +5911,7 @@ function printCounts(summary) {
5870
5911
 
5871
5912
  // src/cli/commands/export.ts
5872
5913
  import { writeFile as writeFile6 } from "fs/promises";
5873
- import path15 from "path";
5914
+ import path16 from "path";
5874
5915
  import { Command as Command2 } from "commander";
5875
5916
 
5876
5917
  // src/services/export/markdown.ts
@@ -5984,30 +6025,38 @@ function renderToolCall(c) {
5984
6025
  return lines.join("\n");
5985
6026
  }
5986
6027
 
6028
+ // src/cli/bundle.ts
6029
+ import path15 from "path";
6030
+ async function withBundle(storePath, fn) {
6031
+ const bundle = await openBundle(path15.resolve(storePath));
6032
+ try {
6033
+ return await fn(bundle);
6034
+ } finally {
6035
+ closeBundle(bundle);
6036
+ }
6037
+ }
6038
+
5987
6039
  // src/cli/commands/export.ts
5988
6040
  function exportCommand() {
5989
6041
  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) => {
5990
6042
  if (options.format !== "markdown") {
5991
6043
  throw new Error(`unsupported format: ${options.format} (try --format markdown)`);
5992
6044
  }
5993
- const bundle = await openBundle(path15.resolve(options.store));
5994
- try {
6045
+ await withBundle(options.store, async (bundle) => {
5995
6046
  const markdown = await exportSessionMarkdown(bundle, sessionId2);
5996
6047
  if (options.out) {
5997
- await writeFile6(path15.resolve(options.out), markdown, "utf8");
5998
- process.stdout.write(`wrote ${path15.resolve(options.out)}
6048
+ await writeFile6(path16.resolve(options.out), markdown, "utf8");
6049
+ process.stdout.write(`wrote ${path16.resolve(options.out)}
5999
6050
  `);
6000
6051
  } else {
6001
6052
  process.stdout.write(markdown);
6002
6053
  }
6003
- } finally {
6004
- closeBundle(bundle);
6005
- }
6054
+ });
6006
6055
  });
6007
6056
  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) => {
6008
6057
  const result = await exportBundleParquet({
6009
- bundlePath: path15.resolve(options.store),
6010
- outDir: options.out ? path15.resolve(options.out) : void 0
6058
+ bundlePath: path16.resolve(options.store),
6059
+ outDir: options.out ? path16.resolve(options.out) : void 0
6011
6060
  });
6012
6061
  process.stdout.write(`wrote parquet export to ${result.outDir}
6013
6062
  `);
@@ -6018,12 +6067,13 @@ function exportCommand() {
6018
6067
  }
6019
6068
 
6020
6069
  // src/cli/commands/index.ts
6021
- import path16 from "path";
6022
6070
  import { Command as Command3 } from "commander";
6023
6071
  init_indexing();
6024
6072
 
6025
6073
  // src/cli/output.ts
6026
6074
  var OUTPUT_FORMATS = ["interactive", "table", "json", "csv"];
6075
+ var COL_SEPARATOR = " ";
6076
+ var RULE_CHAR = "-";
6027
6077
  function parseOutputFormat(value, fallback) {
6028
6078
  if (value === void 0) return fallback;
6029
6079
  if (OUTPUT_FORMATS.includes(value)) return value;
@@ -6051,11 +6101,12 @@ function printJson(rows, opts) {
6051
6101
  `);
6052
6102
  }
6053
6103
  function printCsv(rows, opts) {
6054
- const cols = opts.columns;
6055
- process.stdout.write(`${cols.map(csvField).join(",")}
6104
+ const columns = opts.columns;
6105
+ process.stdout.write(`${columns.map(csvField).join(",")}
6056
6106
  `);
6057
6107
  for (const row of rows) {
6058
- const line = cols.map((c) => csvField(formatCell(row[c]))).join(",");
6108
+ const record = row;
6109
+ const line = columns.map((column) => csvField(formatCell(record[column]))).join(",");
6059
6110
  process.stdout.write(`${line}
6060
6111
  `);
6061
6112
  }
@@ -6065,23 +6116,24 @@ function csvField(value) {
6065
6116
  return value;
6066
6117
  }
6067
6118
  function printTable(rows, opts) {
6068
- const cols = opts.columns;
6069
- const widths = cols.map((c) => c.length);
6070
- const cells = rows.map(
6071
- (row) => cols.map((col, i) => {
6072
- const text = formatCell(row[col]);
6073
- const w = widths[i] ?? 0;
6074
- if (text.length > w) widths[i] = text.length;
6119
+ const columns = opts.columns;
6120
+ const widths = columns.map((column) => column.length);
6121
+ const cells = rows.map((row) => {
6122
+ const record = row;
6123
+ return columns.map((column, index) => {
6124
+ const text = formatCell(record[column]);
6125
+ const width = widths[index] ?? 0;
6126
+ if (text.length > width) widths[index] = text.length;
6075
6127
  return text;
6076
- })
6077
- );
6078
- const header = cols.map((c, i) => c.padEnd(widths[i] ?? 0)).join(" ");
6079
- const sep = cols.map((_, i) => "-".repeat(widths[i] ?? 0)).join(" ");
6128
+ });
6129
+ });
6130
+ const header = columns.map((column, index) => column.padEnd(widths[index] ?? 0)).join(COL_SEPARATOR);
6131
+ const rule = columns.map((_, index) => RULE_CHAR.repeat(widths[index] ?? 0)).join(COL_SEPARATOR);
6080
6132
  process.stdout.write(`${header}
6081
- ${sep}
6133
+ ${rule}
6082
6134
  `);
6083
6135
  for (const cellRow of cells) {
6084
- const line = cellRow.map((c, i) => c.padEnd(widths[i] ?? 0)).join(" ");
6136
+ const line = cellRow.map((cell, index) => cell.padEnd(widths[index] ?? 0)).join(COL_SEPARATOR);
6085
6137
  process.stdout.write(`${line}
6086
6138
  `);
6087
6139
  }
@@ -6096,27 +6148,18 @@ function formatCell(value) {
6096
6148
  // src/cli/commands/index.ts
6097
6149
  function indexCommand() {
6098
6150
  const fts5 = new Command3("fts5").description("Rebuild the SQLite FTS5 index from search_docs.").option("--store <path>", "bundle directory", defaultBundlePath()).action(async (options) => {
6099
- const bundle = await openBundle(path16.resolve(options.store));
6100
- try {
6101
- const status2 = rebuildFts5Index(bundle);
6102
- printIndexStatus(status2);
6103
- } finally {
6104
- closeBundle(bundle);
6105
- }
6151
+ await withBundle(options.store, (bundle) => {
6152
+ printIndexStatus(rebuildFts5Index(bundle));
6153
+ });
6106
6154
  });
6107
6155
  const tantivy = new Command3("tantivy").description("Rebuild the Tantivy sidecar index from search_docs.").option("--store <path>", "bundle directory", defaultBundlePath()).action(async (options) => {
6108
- const bundle = await openBundle(path16.resolve(options.store));
6109
- try {
6110
- const status2 = await rebuildTantivyIndex(bundle);
6111
- printIndexStatus(status2);
6112
- } finally {
6113
- closeBundle(bundle);
6114
- }
6156
+ await withBundle(options.store, async (bundle) => {
6157
+ printIndexStatus(await rebuildTantivyIndex(bundle));
6158
+ });
6115
6159
  });
6116
6160
  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) => {
6117
6161
  const format = parseOutputFormat(options.outputFormat, "table");
6118
- const bundle = await openBundle(path16.resolve(options.store));
6119
- try {
6162
+ await withBundle(options.store, (bundle) => {
6120
6163
  const rows = getSearchIndexStatuses(bundle);
6121
6164
  printRows(rows, {
6122
6165
  format,
@@ -6129,9 +6172,7 @@ function indexCommand() {
6129
6172
  "error_message"
6130
6173
  ]
6131
6174
  });
6132
- } finally {
6133
- closeBundle(bundle);
6134
- }
6175
+ });
6135
6176
  });
6136
6177
  return new Command3("index").description("Build or inspect derived search indexes.").addCommand(fts5).addCommand(tantivy).addCommand(status);
6137
6178
  }
@@ -6178,6 +6219,7 @@ import path18 from "path";
6178
6219
  import { Command as Command5 } from "commander";
6179
6220
 
6180
6221
  // src/mcp/server.ts
6222
+ init_errors();
6181
6223
  import { randomUUID } from "crypto";
6182
6224
  import http from "http";
6183
6225
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
@@ -6237,12 +6279,20 @@ Use this workflow:
6237
6279
 
6238
6280
  // src/mcp/tools.ts
6239
6281
  import { z } from "zod";
6282
+
6283
+ // src/core/domain/types.ts
6284
+ var SOURCE_TOOLS = ["cursor", "codex", "claude", "gemini"];
6285
+
6286
+ // src/mcp/tools.ts
6287
+ init_errors();
6288
+ init_limits();
6240
6289
  init_indexing();
6241
6290
  init_search();
6242
6291
  init_sessions();
6243
6292
  function registerProsaTools(server, bundle, options = {}) {
6244
6293
  const searchEngine = options.searchEngine ?? "fts5";
6245
6294
  const storePath = options.storePath ?? bundle.path;
6295
+ const ensureStore = options.ensureStore ?? false;
6246
6296
  registerProsaPrompts(server);
6247
6297
  server.registerTool(
6248
6298
  "compile",
@@ -6250,12 +6300,12 @@ function registerProsaTools(server, bundle, options = {}) {
6250
6300
  title: "Compile sessions",
6251
6301
  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.",
6252
6302
  inputSchema: {
6253
- source: z.enum(["cursor", "codex", "claude", "gemini"]).optional(),
6303
+ source: z.enum(SOURCE_TOOLS).optional(),
6254
6304
  sessions_path: z.string().min(1).optional()
6255
6305
  },
6256
6306
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true }
6257
6307
  },
6258
- async ({ source, sessions_path }) => {
6308
+ async ({ source, sessions_path }) => withToolBundle(bundle, storePath, ensureStore, async (activeBundle) => {
6259
6309
  if (sessions_path && !source) {
6260
6310
  return {
6261
6311
  content: [
@@ -6269,7 +6319,7 @@ function registerProsaTools(server, bundle, options = {}) {
6269
6319
  }
6270
6320
  try {
6271
6321
  const result = await runCompileImports({
6272
- bundle,
6322
+ bundle: activeBundle,
6273
6323
  providers: source ? [getCompileProvider(source)] : COMPILE_PROVIDERS,
6274
6324
  deferIndex: false,
6275
6325
  sessionsPath: sessions_path
@@ -6306,11 +6356,11 @@ function registerProsaTools(server, bundle, options = {}) {
6306
6356
  };
6307
6357
  } catch (error) {
6308
6358
  return {
6309
- content: [{ type: "text", text: error instanceof Error ? error.message : String(error) }],
6359
+ content: [{ type: "text", text: getErrorMessage(error) }],
6310
6360
  isError: true
6311
6361
  };
6312
6362
  }
6313
- }
6363
+ })
6314
6364
  );
6315
6365
  server.registerTool(
6316
6366
  "list_sessions",
@@ -6318,15 +6368,15 @@ function registerProsaTools(server, bundle, options = {}) {
6318
6368
  title: "List sessions",
6319
6369
  description: "List recent sessions when you need candidates by source/date before deeper inspection. Next step: call get_session for relevant session_id values.",
6320
6370
  inputSchema: {
6321
- source: z.enum(["cursor", "codex", "claude", "gemini"]).optional(),
6371
+ source: z.enum(SOURCE_TOOLS).optional(),
6322
6372
  since: z.string().optional().describe("ISO timestamp lower bound (inclusive)"),
6323
6373
  until: z.string().optional().describe("ISO timestamp upper bound (exclusive)"),
6324
6374
  limit: z.number().int().min(1).max(500).optional().default(50)
6325
6375
  },
6326
6376
  annotations: { readOnlyHint: true, idempotentHint: true }
6327
6377
  },
6328
- async (input) => {
6329
- const rows = listSessions(bundle, {
6378
+ async (input) => withToolBundle(bundle, storePath, ensureStore, (activeBundle) => {
6379
+ const rows = listSessions(activeBundle, {
6330
6380
  sourceTool: input.source,
6331
6381
  sinceIso: input.since,
6332
6382
  untilIso: input.until,
@@ -6335,7 +6385,7 @@ function registerProsaTools(server, bundle, options = {}) {
6335
6385
  return {
6336
6386
  content: [{ type: "text", text: JSON.stringify(rows, null, 2) }]
6337
6387
  };
6338
- }
6388
+ })
6339
6389
  );
6340
6390
  server.registerTool(
6341
6391
  "get_session",
@@ -6347,8 +6397,8 @@ function registerProsaTools(server, bundle, options = {}) {
6347
6397
  },
6348
6398
  annotations: { readOnlyHint: true, idempotentHint: true }
6349
6399
  },
6350
- async ({ session_id }) => {
6351
- const detail = getSession(bundle, session_id);
6400
+ async ({ session_id }) => withToolBundle(bundle, storePath, ensureStore, (activeBundle) => {
6401
+ const detail = getSession(activeBundle, session_id);
6352
6402
  if (!detail) {
6353
6403
  return {
6354
6404
  content: [{ type: "text", text: `session not found: ${session_id}` }],
@@ -6358,7 +6408,7 @@ function registerProsaTools(server, bundle, options = {}) {
6358
6408
  return {
6359
6409
  content: [{ type: "text", text: JSON.stringify(detail, null, 2) }]
6360
6410
  };
6361
- }
6411
+ })
6362
6412
  );
6363
6413
  server.registerTool(
6364
6414
  "search_sessions",
@@ -6372,8 +6422,13 @@ function registerProsaTools(server, bundle, options = {}) {
6372
6422
  },
6373
6423
  annotations: { readOnlyHint: true, idempotentHint: true }
6374
6424
  },
6375
- async ({ query, limit, raw }) => {
6376
- const hits = searchFullText(bundle, { query, limit: limit ?? 50, raw, engine: searchEngine });
6425
+ async ({ query, limit, raw }) => withToolBundle(bundle, storePath, ensureStore, (activeBundle) => {
6426
+ const hits = searchFullText(activeBundle, {
6427
+ query,
6428
+ limit: limit ?? 50,
6429
+ raw,
6430
+ engine: searchEngine
6431
+ });
6377
6432
  return {
6378
6433
  content: [
6379
6434
  {
@@ -6386,7 +6441,7 @@ function registerProsaTools(server, bundle, options = {}) {
6386
6441
  }
6387
6442
  ]
6388
6443
  };
6389
- }
6444
+ })
6390
6445
  );
6391
6446
  server.registerTool(
6392
6447
  "export_session_markdown",
@@ -6398,17 +6453,17 @@ function registerProsaTools(server, bundle, options = {}) {
6398
6453
  },
6399
6454
  annotations: { readOnlyHint: true, idempotentHint: true }
6400
6455
  },
6401
- async ({ session_id }) => {
6456
+ async ({ session_id }) => withToolBundle(bundle, storePath, ensureStore, async (activeBundle) => {
6402
6457
  try {
6403
- const md = await exportSessionMarkdown(bundle, session_id);
6458
+ const md = await exportSessionMarkdown(activeBundle, session_id);
6404
6459
  return { content: [{ type: "text", text: md }] };
6405
6460
  } catch (error) {
6406
6461
  return {
6407
- content: [{ type: "text", text: error instanceof Error ? error.message : String(error) }],
6462
+ content: [{ type: "text", text: getErrorMessage(error) }],
6408
6463
  isError: true
6409
6464
  };
6410
6465
  }
6411
- }
6466
+ })
6412
6467
  );
6413
6468
  server.registerTool(
6414
6469
  "list_tool_calls",
@@ -6435,7 +6490,7 @@ function registerProsaTools(server, bundle, options = {}) {
6435
6490
  },
6436
6491
  annotations: { readOnlyHint: true, idempotentHint: true }
6437
6492
  },
6438
- async ({ tool_name, canonical_type, session_id, errors_only, limit }) => {
6493
+ async ({ tool_name, canonical_type, session_id, errors_only, limit }) => withToolBundle(bundle, storePath, ensureStore, (activeBundle) => {
6439
6494
  const conds = [];
6440
6495
  const params = [];
6441
6496
  if (tool_name) {
@@ -6463,13 +6518,13 @@ function registerProsaTools(server, bundle, options = {}) {
6463
6518
  LEFT JOIN tool_results tr ON tr.tool_call_id = tc.tool_call_id
6464
6519
  ${where}
6465
6520
  ORDER BY tc.timestamp_start DESC
6466
- LIMIT ${Math.max(1, Math.min(500, limit ?? 100))}
6521
+ LIMIT ${clampLimit(limit, { max: 500, fallback: 100 })}
6467
6522
  `;
6468
- const rows = bundle.db.prepare(sql).all(...params);
6523
+ const rows = activeBundle.db.prepare(sql).all(...params);
6469
6524
  return {
6470
6525
  content: [{ type: "text", text: JSON.stringify(rows, null, 2) }]
6471
6526
  };
6472
- }
6527
+ })
6473
6528
  );
6474
6529
  server.registerTool(
6475
6530
  "find_touched_files",
@@ -6482,7 +6537,7 @@ function registerProsaTools(server, bundle, options = {}) {
6482
6537
  },
6483
6538
  annotations: { readOnlyHint: true, idempotentHint: true }
6484
6539
  },
6485
- async ({ path_substring, limit }) => {
6540
+ async ({ path_substring, limit }) => withToolBundle(bundle, storePath, ensureStore, (activeBundle) => {
6486
6541
  const sql = `
6487
6542
  SELECT tc.session_id, tc.tool_name, tc.canonical_tool_type, tc.path,
6488
6543
  tc.timestamp_start, tc.command
@@ -6494,14 +6549,14 @@ function registerProsaTools(server, bundle, options = {}) {
6494
6549
  FROM artifacts a
6495
6550
  WHERE a.path IS NOT NULL AND a.path LIKE ?
6496
6551
  ORDER BY timestamp_start DESC
6497
- LIMIT ${Math.max(1, Math.min(500, limit ?? 100))}
6552
+ LIMIT ${clampLimit(limit, { max: 500, fallback: 100 })}
6498
6553
  `;
6499
6554
  const like = `%${path_substring}%`;
6500
- const rows = bundle.db.prepare(sql).all(like, like);
6555
+ const rows = activeBundle.db.prepare(sql).all(like, like);
6501
6556
  return {
6502
6557
  content: [{ type: "text", text: JSON.stringify(rows, null, 2) }]
6503
6558
  };
6504
- }
6559
+ })
6505
6560
  );
6506
6561
  server.registerTool(
6507
6562
  "get_artifact",
@@ -6513,8 +6568,8 @@ function registerProsaTools(server, bundle, options = {}) {
6513
6568
  },
6514
6569
  annotations: { readOnlyHint: true, idempotentHint: true }
6515
6570
  },
6516
- async ({ artifact_id }) => {
6517
- const row = bundle.db.prepare(`SELECT text_object_id, object_id, mime_type FROM artifacts WHERE artifact_id = ?`).get(artifact_id);
6571
+ async ({ artifact_id }) => withToolBundle(bundle, storePath, ensureStore, async (activeBundle) => {
6572
+ const row = activeBundle.db.prepare(`SELECT text_object_id, object_id, mime_type FROM artifacts WHERE artifact_id = ?`).get(artifact_id);
6518
6573
  if (!row) {
6519
6574
  return {
6520
6575
  content: [{ type: "text", text: `artifact not found: ${artifact_id}` }],
@@ -6527,12 +6582,12 @@ function registerProsaTools(server, bundle, options = {}) {
6527
6582
  }
6528
6583
  try {
6529
6584
  const { getText: getText2 } = await Promise.resolve().then(() => (init_cas(), cas_exports));
6530
- const text = await getText2(bundle, objectId);
6585
+ const text = await getText2(activeBundle, objectId);
6531
6586
  return { content: [{ type: "text", text }] };
6532
6587
  } catch {
6533
6588
  return { content: [{ type: "text", text: `[binary artifact: ${objectId}]` }] };
6534
6589
  }
6535
- }
6590
+ })
6536
6591
  );
6537
6592
  server.registerTool(
6538
6593
  "index_status",
@@ -6542,14 +6597,25 @@ function registerProsaTools(server, bundle, options = {}) {
6542
6597
  inputSchema: {},
6543
6598
  annotations: { readOnlyHint: true, idempotentHint: true }
6544
6599
  },
6545
- async () => {
6546
- const rows = getSearchIndexStatuses(bundle);
6600
+ async () => withToolBundle(bundle, storePath, ensureStore, (activeBundle) => {
6601
+ const rows = getSearchIndexStatuses(activeBundle);
6547
6602
  return {
6548
6603
  content: [{ type: "text", text: JSON.stringify(rows, null, 2) }]
6549
6604
  };
6550
- }
6605
+ })
6551
6606
  );
6552
6607
  }
6608
+ async function withToolBundle(fallbackBundle, storePath, ensureStore, fn) {
6609
+ if (!ensureStore) {
6610
+ return await fn(fallbackBundle);
6611
+ }
6612
+ const bundle = await openOrInitBundle(storePath);
6613
+ try {
6614
+ return await fn(bundle);
6615
+ } finally {
6616
+ closeBundle(bundle);
6617
+ }
6618
+ }
6553
6619
  function registerProsaPrompts(server) {
6554
6620
  server.registerPrompt(
6555
6621
  "investigate_prior_work",
@@ -6582,14 +6648,14 @@ function registerProsaPrompts(server) {
6582
6648
  path: z.string().min(1).describe("File path, directory, or distinctive path suffix")
6583
6649
  }
6584
6650
  },
6585
- ({ path: path23 }) => ({
6651
+ ({ path: path20 }) => ({
6586
6652
  description: "Find sessions that touched a path and summarize the evidence.",
6587
6653
  messages: [
6588
6654
  {
6589
6655
  role: "user",
6590
6656
  content: {
6591
6657
  type: "text",
6592
- text: FIND_FILE_HISTORY_PROMPT.replace("{{path}}", path23)
6658
+ text: FIND_FILE_HISTORY_PROMPT.replace("{{path}}", path20)
6593
6659
  }
6594
6660
  }
6595
6661
  ]
@@ -6722,7 +6788,7 @@ function createMcpServer(bundle, searchEngine, storePath) {
6722
6788
  },
6723
6789
  { instructions: PROSA_MCP_INSTRUCTIONS }
6724
6790
  );
6725
- registerProsaTools(server, bundle, { searchEngine, storePath });
6791
+ registerProsaTools(server, bundle, { ensureStore: true, searchEngine, storePath });
6726
6792
  return server;
6727
6793
  }
6728
6794
  async function readBody(req) {
@@ -6753,18 +6819,33 @@ function writeError(res, error) {
6753
6819
  res.end(
6754
6820
  JSON.stringify({
6755
6821
  jsonrpc: "2.0",
6756
- error: { code: -32603, message: error instanceof Error ? error.message : String(error) },
6822
+ error: { code: -32603, message: getErrorMessage(error) },
6757
6823
  id: null
6758
6824
  })
6759
6825
  );
6760
6826
  }
6761
6827
 
6828
+ // src/cli/parsers.ts
6829
+ function parseSearchEngine(value) {
6830
+ if (value === "fts5" || value === "tantivy") return value;
6831
+ throw new Error(`invalid search engine: ${value} (expected fts5 or tantivy)`);
6832
+ }
6833
+ function parseMcpTransport(value) {
6834
+ if (value === "stdio" || value === "http") return value;
6835
+ throw new Error(`invalid transport: ${value} (expected stdio or http)`);
6836
+ }
6837
+ function parseSourceTool(value) {
6838
+ if (value === void 0) return void 0;
6839
+ if (SOURCE_TOOLS.includes(value)) return value;
6840
+ throw new Error(`invalid source tool: ${value} (expected one of ${SOURCE_TOOLS.join(", ")})`);
6841
+ }
6842
+
6762
6843
  // src/cli/commands/mcp.ts
6763
6844
  function mcpCommand() {
6764
6845
  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(
6765
6846
  async (options) => {
6766
6847
  const storePath = path18.resolve(options.store);
6767
- const bundle = await openBundle(storePath);
6848
+ const bundle = await openOrInitBundle(storePath);
6768
6849
  try {
6769
6850
  const transport = parseMcpTransport(options.transport);
6770
6851
  const searchEngine = parseSearchEngine(options.searchEngine);
@@ -6796,14 +6877,6 @@ function mcpCommand() {
6796
6877
  );
6797
6878
  return new Command5("mcp").description("MCP server commands.").addCommand(serve);
6798
6879
  }
6799
- function parseMcpTransport(value) {
6800
- if (value === "stdio" || value === "http") return value;
6801
- throw new Error(`invalid --transport: ${value} (expected stdio or http)`);
6802
- }
6803
- function parseSearchEngine(value) {
6804
- if (value === "fts5" || value === "tantivy") return value;
6805
- throw new Error(`invalid --search-engine: ${value} (expected fts5 or tantivy)`);
6806
- }
6807
6880
  function registerShutdown(closeServer, bundle) {
6808
6881
  const shutdown = async () => {
6809
6882
  await closeServer();
@@ -6825,7 +6898,7 @@ function queryCommand() {
6825
6898
  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(
6826
6899
  async (sql, options) => {
6827
6900
  const format = parseOutputFormat(options.outputFormat, "table");
6828
- const parquetDir = options.parquetDir ? path19.resolve(options.parquetDir) : await defaultParquetDir(path19.resolve(options.store));
6901
+ const parquetDir = options.parquetDir ? path19.resolve(options.parquetDir) : await withBundle(options.store, (bundle) => bundle.paths.parquet);
6829
6902
  const result = await queryDuckDbParquet({ parquetDir, sql });
6830
6903
  printRows(result.rows, {
6831
6904
  format,
@@ -6836,26 +6909,16 @@ function queryCommand() {
6836
6909
  );
6837
6910
  return new Command6("query").description("Run derived analytical queries.").addCommand(duckdb);
6838
6911
  }
6839
- async function defaultParquetDir(storePath) {
6840
- const bundle = await openBundle(storePath);
6841
- try {
6842
- return bundle.paths.parquet;
6843
- } finally {
6844
- closeBundle(bundle);
6845
- }
6846
- }
6847
6912
 
6848
6913
  // src/cli/commands/search.ts
6849
- import path20 from "path";
6850
6914
  import { Command as Command7 } from "commander";
6851
6915
  init_search();
6852
6916
  function searchCommand() {
6853
6917
  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(
6854
6918
  async (query, options) => {
6855
- const engine = parseSearchEngine2(options.engine);
6919
+ const engine = parseSearchEngine(options.engine);
6856
6920
  const format = parseOutputFormat(options.outputFormat, "table");
6857
- const bundle = await openBundle(path20.resolve(options.store));
6858
- try {
6921
+ await withBundle(options.store, (bundle) => {
6859
6922
  const hits = searchFullText(bundle, {
6860
6923
  query,
6861
6924
  limit: Number.parseInt(options.limit, 10),
@@ -6866,29 +6929,21 @@ function searchCommand() {
6866
6929
  columns: ["timestamp", "role", "tool_name", "session_id", "snippet"],
6867
6930
  meta: { query, engine, count: hits.length }
6868
6931
  });
6869
- } finally {
6870
- closeBundle(bundle);
6871
- }
6932
+ });
6872
6933
  }
6873
6934
  );
6874
6935
  }
6875
- function parseSearchEngine2(value) {
6876
- if (value === "fts5" || value === "tantivy") return value;
6877
- throw new Error(`invalid --engine: ${value} (expected fts5 or tantivy)`);
6878
- }
6879
6936
 
6880
6937
  // src/cli/commands/sessions.ts
6881
- import path21 from "path";
6882
6938
  import { Command as Command8 } from "commander";
6883
6939
  init_sessions();
6884
6940
  function sessionsCommand() {
6885
6941
  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(
6886
6942
  async (options) => {
6887
6943
  const format = parseOutputFormat(options.outputFormat, "table");
6888
- const bundle = await openBundle(path21.resolve(options.store));
6889
- try {
6944
+ await withBundle(options.store, (bundle) => {
6890
6945
  const rows = listSessions(bundle, {
6891
- sourceTool: options.source,
6946
+ sourceTool: parseSourceTool(options.source),
6892
6947
  sinceIso: options.since,
6893
6948
  untilIso: options.until,
6894
6949
  limit: Number.parseInt(options.limit, 10)
@@ -6906,26 +6961,21 @@ function sessionsCommand() {
6906
6961
  "title"
6907
6962
  ]
6908
6963
  });
6909
- } finally {
6910
- closeBundle(bundle);
6911
- }
6964
+ });
6912
6965
  }
6913
6966
  );
6914
6967
  command.addCommand(
6915
6968
  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(
6916
6969
  async (options) => {
6917
- const bundle = await openBundle(path21.resolve(options.store));
6918
- try {
6970
+ await withBundle(options.store, (bundle) => {
6919
6971
  const count = countSessions(bundle, {
6920
- sourceTool: options.source,
6972
+ sourceTool: parseSourceTool(options.source),
6921
6973
  sinceIso: options.since,
6922
6974
  untilIso: options.until
6923
6975
  });
6924
6976
  process.stdout.write(`${count}
6925
6977
  `);
6926
- } finally {
6927
- closeBundle(bundle);
6928
- }
6978
+ });
6929
6979
  }
6930
6980
  )
6931
6981
  );
@@ -6933,7 +6983,6 @@ function sessionsCommand() {
6933
6983
  }
6934
6984
 
6935
6985
  // src/cli/commands/tui.ts
6936
- import path22 from "path";
6937
6986
  import { Command as Command9 } from "commander";
6938
6987
  function tuiCommand() {
6939
6988
  return new Command9("tui").description("Open the interactive Ink-based explorer.").option("--store <path>", "bundle directory", defaultBundlePath()).action(async (options) => {
@@ -6942,14 +6991,11 @@ function tuiCommand() {
6942
6991
  import("react"),
6943
6992
  Promise.resolve().then(() => (init_App(), App_exports))
6944
6993
  ]);
6945
- const bundle = await openBundle(path22.resolve(options.store));
6946
- try {
6994
+ await withBundle(options.store, async (bundle) => {
6947
6995
  console.clear();
6948
6996
  const app = render(React.createElement(App2, { bundle }));
6949
6997
  await app.waitUntilExit();
6950
- } finally {
6951
- closeBundle(bundle);
6952
- }
6998
+ });
6953
6999
  });
6954
7000
  }
6955
7001