@cavelang/cli 0.33.0 → 0.35.0

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 (68) hide show
  1. package/README.md +15 -3
  2. package/dist/internal/automate/main.d.ts.map +1 -1
  3. package/dist/internal/automate/main.js +9 -2
  4. package/dist/internal/automate/main.js.map +1 -1
  5. package/dist/internal/connect/declared.d.ts +225 -0
  6. package/dist/internal/connect/declared.d.ts.map +1 -0
  7. package/dist/internal/connect/declared.js +521 -0
  8. package/dist/internal/connect/declared.js.map +1 -0
  9. package/dist/internal/connect/index.d.ts +4 -2
  10. package/dist/internal/connect/index.d.ts.map +1 -1
  11. package/dist/internal/connect/index.js +3 -1
  12. package/dist/internal/connect/index.js.map +1 -1
  13. package/dist/internal/connect/main.d.ts.map +1 -1
  14. package/dist/internal/connect/main.js +356 -10
  15. package/dist/internal/connect/main.js.map +1 -1
  16. package/dist/internal/connect/run.d.ts +47 -1
  17. package/dist/internal/connect/run.d.ts.map +1 -1
  18. package/dist/internal/connect/run.js +81 -33
  19. package/dist/internal/connect/run.js.map +1 -1
  20. package/dist/internal/connect/source.d.ts +33 -2
  21. package/dist/internal/connect/source.d.ts.map +1 -1
  22. package/dist/internal/connect/source.js +168 -8
  23. package/dist/internal/connect/source.js.map +1 -1
  24. package/dist/internal/connect/template.d.ts +18 -0
  25. package/dist/internal/connect/template.d.ts.map +1 -1
  26. package/dist/internal/connect/template.js +28 -0
  27. package/dist/internal/connect/template.js.map +1 -1
  28. package/dist/internal/ingest/main.d.ts.map +1 -1
  29. package/dist/internal/ingest/main.js +19 -2
  30. package/dist/internal/ingest/main.js.map +1 -1
  31. package/dist/internal/mcp/main.d.ts +1 -1
  32. package/dist/internal/mcp/main.d.ts.map +1 -1
  33. package/dist/internal/mcp/main.js +15 -3
  34. package/dist/internal/mcp/main.js.map +1 -1
  35. package/dist/internal/rules/rule.d.ts.map +1 -1
  36. package/dist/internal/rules/rule.js +1 -22
  37. package/dist/internal/rules/rule.js.map +1 -1
  38. package/dist/internal/sync/sync.d.ts +2 -3
  39. package/dist/internal/sync/sync.d.ts.map +1 -1
  40. package/dist/internal/sync/sync.js +3 -15
  41. package/dist/internal/sync/sync.js.map +1 -1
  42. package/dist/internal/view/main.d.ts.map +1 -1
  43. package/dist/internal/view/main.js +4 -3
  44. package/dist/internal/view/main.js.map +1 -1
  45. package/dist/src/cli.d.ts +15 -1
  46. package/dist/src/cli.d.ts.map +1 -1
  47. package/dist/src/cli.js +227 -54
  48. package/dist/src/cli.js.map +1 -1
  49. package/dist/src/commands.d.ts +2 -2
  50. package/dist/src/commands.js +2 -2
  51. package/dist/src/commands.js.map +1 -1
  52. package/dist/src/dispatch.d.ts.map +1 -1
  53. package/dist/src/dispatch.js +7 -1
  54. package/dist/src/dispatch.js.map +1 -1
  55. package/dist/src/doctor.d.ts +1 -1
  56. package/dist/src/doctor.d.ts.map +1 -1
  57. package/dist/src/doctor.js +27 -2
  58. package/dist/src/doctor.js.map +1 -1
  59. package/dist/src/index.d.ts +1 -1
  60. package/dist/src/index.d.ts.map +1 -1
  61. package/dist/src/index.js +1 -1
  62. package/dist/src/index.js.map +1 -1
  63. package/package.json +19 -19
  64. package/src/cli.ts +258 -53
  65. package/src/commands.ts +2 -2
  66. package/src/dispatch.ts +7 -1
  67. package/src/doctor.ts +29 -3
  68. package/src/index.ts +1 -1
package/dist/src/cli.js CHANGED
@@ -40,7 +40,8 @@ import { parseArgs } from 'node:util';
40
40
  import { Version } from '@cavelang/core';
41
41
  import { parseDocument, Token } from '@cavelang/parser';
42
42
  import { Registry, standardRegistry } from '@cavelang/canonical';
43
- import { Sensitivity, backup as backupStore, defaultDbPath, open, restoreBackup, verifyBackup } from '@cavelang/store';
43
+ import { backup as backupStore, defaultDbPath, kindOf, LocateError, openAt, restoreBackup, Sensitivity, verifyBackup } from '@cavelang/store';
44
+ import { Declared, assemble as assembleSources } from '@cavelang/cli/connect';
44
45
  import { defaultLimit as defaultQueryLimit, maxLimit as maxQueryLimit, page as caveQueryPage } from '@cavelang/query';
45
46
  import { check as caveCheck, defaultMinScore, defaultStaleDays, gatedIngest, judgePrompt, parseJudgeReply, generateClient, suggestAliases, writeSuggestions } from '@cavelang/cli/shape';
46
47
  import { declareRules, defaultMaxPasses, defaultMinConf, derive, listRules, retractRule } from '@cavelang/cli/rules';
@@ -115,9 +116,11 @@ Usage:
115
116
  cave help [command] this text, or one command's options and examples
116
117
 
117
118
  Every command answers --help. --db defaults to $CAVE_DB, or cave.db in
118
- the current directory. The spec lives in the .claude/skills/ directory at
119
+ the current directory, and names a SQLite store or a CAVE text file: text
120
+ replays into an in-memory store for commands that only read (writes refuse
121
+ it), and a read never creates a missing database. The spec lives in the .claude/skills/ directory at
119
122
  the repository root (section index in README.md).`;
120
- const dbHelp = `--db <path> database file (default: $CAVE_DB, or cave.db)`;
123
+ const dbHelp = `--db <path> store: a SQLite file, or CAVE text read into memory (default: $CAVE_DB, or cave.db)`;
121
124
  /** Per-command help, printed for \`cave <command> --help\` and \`cave help <command>\`. */
122
125
  export const commandHelp = {
123
126
  doctor: `cave doctor — diagnose runtime, installation, and store health
@@ -216,7 +219,7 @@ Examples:
216
219
  query: `cave query — run a CAVE-Q pattern against a store
217
220
 
218
221
  Usage:
219
- cave query [--db <path>] <pattern> [WHERE <filter>] [--limit <n>] [--cursor <token>] [--json] [--all] [--aliases] [--as-of <t>] [--at <t>] [--resolve] [--no-prelude]
222
+ cave query [--db <path>] <pattern> [WHERE <filter>] [--limit <n>] [--cursor <token>] [--json] [--all] [--aliases] [--as-of <t>] [--at <t>] [--resolve] [--sources] [--no-prelude]
220
223
 
221
224
  Options:
222
225
  ${dbHelp}
@@ -242,6 +245,12 @@ Options:
242
245
  collapse to the row the resolution policy picks
243
246
  (precedence class, reliability-weighted confidence, tx);
244
247
  incompatible with --all
248
+ --sources overlay the store's declared sources (spec §23.4) inside a
249
+ rolled-back transaction — federation-lite: external data is
250
+ consulted at query time, nothing persists. The answer is
251
+ whole (no --cursor, --limit is not a page size); a text
252
+ store followed its local sources on open, so only URL
253
+ sources are loaded here
245
254
  --no-prelude open the store without the standard verb registry
246
255
 
247
256
  Patterns are claim triples with ?variables and optional metadata filters
@@ -252,6 +261,7 @@ with the answer; fully bound patterns print the raw line, comment included.
252
261
 
253
262
  Examples:
254
263
  cave query '?x USES jwt'
264
+ cave query --db notes.cave '?x USES jwt'
255
265
  cave query --db k.db '?x HAS bug: ?bug #security'
256
266
  cave query --db k.db '?cause CAUSE app/crash' 'WHERE conf >= 0.5'
257
267
  cave query --db k.db 'terrier EXTENDS+ animal'
@@ -260,6 +270,7 @@ Examples:
260
270
  cave query --db k.db 'revenue IS' --at 2026-07
261
271
  cave query --db k.db 'alice WORKS-AT ?org' --at 2021
262
272
  cave query --db k.db 'service HAS owner: ?who' --resolve
273
+ cave query --db k.db '?who WORKS-AT acme' --sources
263
274
  cave query --db k.db '?x ?verb ?y @production' --json`,
264
275
  search: `cave search — full-text search over claims and comments (spec §13.2)
265
276
 
@@ -733,6 +744,54 @@ const readStdin = () => {
733
744
  }
734
745
  }
735
746
  };
747
+ /**
748
+ * Opens the store `--db` names (spec §13.7): a SQLite file, or a CAVE text
749
+ * file replayed into memory. `read` never creates, migrates, or changes a
750
+ * file — a missing path is an error, not an empty database; `scratch` is a
751
+ * dry run that appends inside a rolled-back transaction, so it may write
752
+ * but creates and migrates nothing; `write` creates a missing SQLite store,
753
+ * migrates an older one, and refuses text files with the materialization
754
+ * hint.
755
+ */
756
+ const openDb = (values, intent) => openAt(values.db ?? defaultDbPath(), {
757
+ intent,
758
+ // A text store follows its declared sources on open (spec §23.4).
759
+ assemble: assembleSources,
760
+ ...values['no-prelude'] === true ? { registry: Registry.empty } : {}
761
+ });
762
+ const sourcesRollback = Symbol('cave query --sources rollback');
763
+ /**
764
+ * Runs `body` with the store's declared sources overlaid (spec §23.4): the
765
+ * sources, loaded beforehand (URLs included), append inside a transaction
766
+ * that always rolls back, so external data is consulted at query time and
767
+ * nothing persists, digests included.
768
+ */
769
+ const withSources = (store, discovered, body) => {
770
+ let result;
771
+ try {
772
+ store.transaction(() => {
773
+ // Discovered on a snapshot: if a writer changed the declarations
774
+ // since, the sequence is stale and the caller rediscovers.
775
+ if (!Declared.sameDeclarations(discovered.baseline, Declared.declarationState(store))) {
776
+ throw sourcesRollback;
777
+ }
778
+ for (const source of discovered.sequence) {
779
+ const report = Declared.run(store, source, { force: true });
780
+ if (report.failures.length > 0) {
781
+ throw new LocateError(`source/${source.declared.name}: ${report.failures.map(failure => `${failure.record}: ${failure.problems.join('; ')}`).join('; ')}`);
782
+ }
783
+ }
784
+ result = { value: body() };
785
+ throw sourcesRollback;
786
+ });
787
+ }
788
+ catch (error) {
789
+ if (error !== sourcesRollback) {
790
+ throw error;
791
+ }
792
+ }
793
+ return result;
794
+ };
736
795
  const readInput = (files) => files.length === 0 || (files.length === 1 && files[0] === '-') ?
737
796
  readStdin() :
738
797
  files.map(file => readFileSync(file, 'utf8')).join('\n');
@@ -809,7 +868,7 @@ const ingestCommand = (name) => (argv) => {
809
868
  allowPositionals: true
810
869
  });
811
870
  const input = readInput(positionals);
812
- const store = open(values.db ?? defaultDbPath(), values['no-prelude'] === true ? { registry: Registry.empty } : {});
871
+ const store = openDb(values, 'write');
813
872
  try {
814
873
  const options = {
815
874
  strict: values.strict === true,
@@ -854,39 +913,125 @@ export const addCommand = ingestCommand('add');
854
913
  * identity.
855
914
  */
856
915
  export const importCommand = ingestCommand('import');
857
- export const queryCommand = (argv) => {
858
- const { values, positionals } = parseArgs({
859
- args: [...argv],
860
- options: {
861
- db: { type: 'string' },
862
- json: { type: 'boolean' },
863
- all: { type: 'boolean' },
864
- aliases: { type: 'boolean' },
865
- 'as-of': { type: 'string' },
866
- at: { type: 'string' },
867
- resolve: { type: 'boolean' },
868
- limit: { type: 'string' },
869
- cursor: { type: 'string' },
870
- 'no-prelude': { type: 'boolean' }
871
- },
872
- allowPositionals: true
873
- });
916
+ const queryOptions = {
917
+ db: { type: 'string' },
918
+ json: { type: 'boolean' },
919
+ all: { type: 'boolean' },
920
+ aliases: { type: 'boolean' },
921
+ 'as-of': { type: 'string' },
922
+ at: { type: 'string' },
923
+ resolve: { type: 'boolean' },
924
+ sources: { type: 'boolean' },
925
+ limit: { type: 'string' },
926
+ cursor: { type: 'string' },
927
+ 'no-prelude': { type: 'boolean' }
928
+ };
929
+ const parseQuery = (argv) => {
930
+ const { values, positionals } = parseArgs({ args: [...argv], options: queryOptions, allowPositionals: true });
874
931
  if (positionals.length === 0) {
875
- return fail('cave query: a pattern is required (spec §12.1)\n');
932
+ return { failure: fail('cave query: a pattern is required (spec §12.1)\n') };
876
933
  }
877
- const pattern = positionals.join('\n');
878
- const store = open(values.db ?? defaultDbPath(), values['no-prelude'] === true ? { registry: Registry.empty } : {});
934
+ if (values.sources === true && values.cursor !== undefined) {
935
+ return { failure: fail('cave query: --cursor does not apply to --sources — the overlay exists for one invocation and is answered whole\n') };
936
+ }
937
+ return { values, pattern: positionals.join('\n') };
938
+ };
939
+ /**
940
+ * `cave query --sources` — the store with its declared sources overlaid
941
+ * (spec §23.4). Asynchronous because the sources load first, URLs
942
+ * included, before the overlay appends inside a transaction that rolls
943
+ * back; the dispatcher routes `--sources` here. A text store followed its
944
+ * local sources on open, so only what assembly could not follow — URL
945
+ * sources and what they declare — is loaded. The overlay exists for this
946
+ * invocation alone, so the answer is whole: pages are followed inside the
947
+ * transaction and no cursor is handed out.
948
+ */
949
+ export const querySourcesCommand = async (argv, runtime = {}) => {
950
+ const parsed = parseQuery(argv);
951
+ if ('failure' in parsed) {
952
+ return parsed.failure;
953
+ }
954
+ const { values, pattern } = parsed;
955
+ if (values.sources !== true) {
956
+ return queryCommand(argv);
957
+ }
958
+ const root = values.db ?? defaultDbPath();
959
+ // The overlay appends inside a transaction it rolls back: writable, but
960
+ // never creating or migrating (spec §13.7).
961
+ const store = openDb(values, 'scratch');
879
962
  try {
880
- const limit = values.limit === undefined ? defaultQueryLimit : Number(values.limit);
881
- const result = caveQueryPage(store, pattern, {
882
- all: values.all === true,
883
- aliases: values.aliases === true,
884
- resolve: values.resolve === true,
885
- limit,
886
- ...values.cursor === undefined ? {} : { cursor: values.cursor },
887
- ...values['as-of'] === undefined ? {} : { asOf: values['as-of'] },
888
- ...values.at === undefined ? {} : { at: values.at }
889
- });
963
+ for (let attempt = 0; attempt < Declared.overlayAttempts; attempt += 1) {
964
+ const discovered = await Declared.discovery(store, root, {
965
+ // The overlay applies every source it loads (force), so discovery
966
+ // simulates that; a text store skips what assembly already followed.
967
+ force: true,
968
+ ...kindOf(root) === 'text' ? { skipFollowed: true } : {},
969
+ ...runtime.fetchImpl === undefined ? {} : { fetchImpl: runtime.fetchImpl }
970
+ });
971
+ const page = withSources(store, discovered, () => wholeQuery(store, values, pattern));
972
+ if (page !== undefined) {
973
+ return renderQueryPage(store, values, pattern, () => page.value);
974
+ }
975
+ }
976
+ throw Declared.staleOverlay();
977
+ }
978
+ catch (error) {
979
+ return fail(`${error instanceof Error ? error.message : String(error)}\n`);
980
+ }
981
+ finally {
982
+ store.close();
983
+ }
984
+ };
985
+ export const queryCommand = (argv) => {
986
+ const parsed = parseQuery(argv);
987
+ if ('failure' in parsed) {
988
+ return parsed.failure;
989
+ }
990
+ const { values, pattern } = parsed;
991
+ if (values.sources === true) {
992
+ // Loading sources cannot be synchronous: the in-process entry for the
993
+ // overlay is `querySourcesCommand`, which the dispatcher uses.
994
+ return fail('cave query: --sources loads the declared sources first and runs asynchronously — use querySourcesCommand\n');
995
+ }
996
+ const store = openDb(values, 'read');
997
+ try {
998
+ return renderQueryPage(store, values, pattern, () => queryPage(store, values, pattern));
999
+ }
1000
+ catch (error) {
1001
+ return fail(`${error instanceof Error ? error.message : String(error)}\n`);
1002
+ }
1003
+ finally {
1004
+ store.close();
1005
+ }
1006
+ };
1007
+ /** Every match of the overlay: the pages are followed here, inside the transaction that holds it. */
1008
+ const wholeQuery = (store, values, pattern) => {
1009
+ const matches = [];
1010
+ let first;
1011
+ let cursor;
1012
+ for (;;) {
1013
+ const page = queryPage(store, { ...values, limit: '1000', ...cursor === undefined ? {} : { cursor } }, pattern);
1014
+ first ??= page;
1015
+ matches.push(...page.matches);
1016
+ if (page.next === undefined) {
1017
+ const { next: _next, ...rest } = first;
1018
+ return { ...rest, matches };
1019
+ }
1020
+ cursor = page.next;
1021
+ }
1022
+ };
1023
+ const queryPage = (store, values, pattern) => caveQueryPage(store, pattern, {
1024
+ all: values.all === true,
1025
+ aliases: values.aliases === true,
1026
+ resolve: values.resolve === true,
1027
+ limit: values.limit === undefined ? defaultQueryLimit : Number(values.limit),
1028
+ ...values.cursor === undefined ? {} : { cursor: values.cursor },
1029
+ ...values['as-of'] === undefined ? {} : { asOf: values['as-of'] },
1030
+ ...values.at === undefined ? {} : { at: values.at }
1031
+ });
1032
+ const renderQueryPage = (store, values, pattern, page) => {
1033
+ {
1034
+ const result = page();
890
1035
  if (values.json === true) {
891
1036
  return ok(`${JSON.stringify(result, undefined, 2)}\n`);
892
1037
  }
@@ -916,12 +1061,6 @@ export const queryCommand = (argv) => {
916
1061
  });
917
1062
  return ok(`${lines.join('\n')}\n${result.next === undefined ? '' : `next: ${result.next}\n`}`);
918
1063
  }
919
- catch (error) {
920
- return fail(`${error instanceof Error ? error.message : String(error)}\n`);
921
- }
922
- finally {
923
- store.close();
924
- }
925
1064
  };
926
1065
  export const searchFormat = 'cave.search';
927
1066
  export const searchVersion = 1;
@@ -950,7 +1089,7 @@ export const searchCommand = (argv) => {
950
1089
  if (!Number.isInteger(limit) || limit < 1 || limit > maxQueryLimit) {
951
1090
  return fail(`cave search: --limit must be an integer from 1 to ${maxQueryLimit}\n`);
952
1091
  }
953
- const store = open(values.db ?? defaultDbPath(), values['no-prelude'] === true ? { registry: Registry.empty } : {});
1092
+ const store = openDb(values, 'read');
954
1093
  try {
955
1094
  const raw = values.raw === true;
956
1095
  const found = store.search(query, { raw, limit: limit + 1 });
@@ -1001,7 +1140,7 @@ export const resolveCommand = (argv) => {
1001
1140
  allowPositionals: false
1002
1141
  });
1003
1142
  const percent = (value) => `${Math.round(value * 1000) / 10}%`;
1004
- const store = open(values.db ?? defaultDbPath(), values['no-prelude'] === true ? { registry: Registry.empty } : {});
1143
+ const store = openDb(values, 'read');
1005
1144
  try {
1006
1145
  if (values.policy === true) {
1007
1146
  const policy = store.resolutionPolicy();
@@ -1063,7 +1202,11 @@ export const deriveCommand = (argv) => {
1063
1202
  if (maxPasses !== undefined && (!Number.isInteger(maxPasses) || maxPasses < 1)) {
1064
1203
  return fail(`cave derive: --max-passes expects a positive integer, got ${JSON.stringify(values['max-passes'])}\n`);
1065
1204
  }
1066
- const store = open(values.db ?? defaultDbPath(), values['no-prelude'] === true ? { registry: Registry.empty } : {});
1205
+ // The intent follows the branch order below: listing reads, retracting
1206
+ // writes, a dry run appends inside a rolled-back transaction.
1207
+ const store = openDb(values, values.list === true ? 'read' :
1208
+ values.retract !== undefined ? 'write' :
1209
+ values['dry-run'] === true ? 'scratch' : 'write');
1067
1210
  try {
1068
1211
  if (values.list === true) {
1069
1212
  const rules = listRules(store);
@@ -1217,7 +1360,13 @@ export const actCommand = (argv) => {
1217
1360
  },
1218
1361
  allowPositionals: true
1219
1362
  });
1220
- const store = open(values.db ?? defaultDbPath(), values['no-prelude'] === true ? { registry: Registry.empty } : {});
1363
+ // The intent follows the branch order below: declaring and retracting
1364
+ // write, listing reads, a dry-run execution appends inside a rolled-back
1365
+ // transaction.
1366
+ const store = openDb(values, values.declare === true ? 'write' :
1367
+ values.list === true ? 'read' :
1368
+ values.retract !== undefined ? 'write' :
1369
+ values['dry-run'] === true ? 'scratch' : 'write');
1221
1370
  try {
1222
1371
  if (values.declare === true) {
1223
1372
  const declaration = declareActions(store, readInput(positionals));
@@ -1335,7 +1484,7 @@ export const checkCommand = (argv) => {
1335
1484
  if (!Number.isFinite(staleDays) || staleDays < 0) {
1336
1485
  return fail(`cave check: --stale expects a non-negative number of days, got ${JSON.stringify(values.stale)}\n`);
1337
1486
  }
1338
- const store = open(values.db ?? defaultDbPath(), values['no-prelude'] === true ? { registry: Registry.empty } : {});
1487
+ const store = openDb(values, 'read');
1339
1488
  try {
1340
1489
  const report = caveCheck(store, { staleDays });
1341
1490
  const out = values.json === true ?
@@ -1379,7 +1528,15 @@ export const backupCommand = (argv) => {
1379
1528
  if (values.out === undefined) {
1380
1529
  return fail('cave backup: --out <file> is required\n');
1381
1530
  }
1382
- const store = open(values.db ?? defaultDbPath());
1531
+ const dbPath = values.db ?? defaultDbPath();
1532
+ // A text store is replayed into memory, so the backup API cannot see that
1533
+ // the destination aliases the source: refuse before the file could be
1534
+ // replaced by a SQLite snapshot of itself (a SQLite source is covered by
1535
+ // the API's own check).
1536
+ if (sameFile(dbPath, values.out)) {
1537
+ return fail(`cave backup: --out '${values.out}' is the source database — refusing to overwrite it\n`);
1538
+ }
1539
+ const store = openAt(dbPath, { intent: 'read', assemble: assembleSources });
1383
1540
  try {
1384
1541
  return ok(snapshotLine('created', backupStore(store, values.out, { force: values.force === true })));
1385
1542
  }
@@ -1460,7 +1617,7 @@ export const suggestAliasCommand = async (argv, runtime = {}) => {
1460
1617
  if (timeoutSeconds !== undefined && (!Number.isFinite(timeoutSeconds) || timeoutSeconds <= 0)) {
1461
1618
  return fail(`cave suggest-alias: --timeout must be a positive number of seconds, got '${values.timeout}'\n`);
1462
1619
  }
1463
- const store = open(values.db ?? defaultDbPath(), values['no-prelude'] === true ? { registry: Registry.empty } : {});
1620
+ const store = openDb(values, values.write === true ? 'write' : 'read');
1464
1621
  try {
1465
1622
  let suggestions = suggestAliases(store, { minScore, ...limit === undefined ? {} : { limit } });
1466
1623
  if (values.agent !== undefined && suggestions.length > 0) {
@@ -1516,7 +1673,7 @@ export const syncCommand = (argv) => {
1516
1673
  return fail('cave sync: exactly one source is required — a CAVE store file, ;@-annotated text, or - for stdin (spec §28.5)\n');
1517
1674
  }
1518
1675
  const dbPath = values.db ?? defaultDbPath();
1519
- const store = open(dbPath, values['no-prelude'] === true ? { registry: Registry.empty } : {});
1676
+ const store = openDb(values, values['dry-run'] === true ? 'scratch' : 'write');
1520
1677
  try {
1521
1678
  const options = {
1522
1679
  into: values.into === undefined ? labelOf(dbPath) : sanitizeLabel(values.into),
@@ -1578,7 +1735,10 @@ export const reportCommand = (argv) => {
1578
1735
  if (maximum === undefined) {
1579
1736
  return fail(`cave report: --max-sensitivity expects ${Sensitivity.levels.join(', ')}, got ${JSON.stringify(values['max-sensitivity'])}\n`);
1580
1737
  }
1581
- const store = open(values.db ?? defaultDbPath(), values['no-prelude'] === true ? { registry: Registry.empty } : {});
1738
+ if (values.out !== undefined && sameFile(values.db ?? defaultDbPath(), values.out)) {
1739
+ return fail(`cave report: --out '${values.out}' is the source database — refusing to overwrite it\n`);
1740
+ }
1741
+ const store = openDb(values, 'read');
1582
1742
  try {
1583
1743
  const rendered = caveReport(store, template, {
1584
1744
  aliases: values.aliases === true,
@@ -1647,7 +1807,7 @@ export const exportCommand = (argv) => {
1647
1807
  if (values.out !== undefined && sameFile(db, values.out)) {
1648
1808
  return fail(`cave export: --out '${values.out}' is the source database — refusing to overwrite it\n`);
1649
1809
  }
1650
- const store = open(db, values['no-prelude'] === true ? { registry: Registry.empty } : {});
1810
+ const store = openDb(values, 'read');
1651
1811
  try {
1652
1812
  const text = store.exportText({ current: values.current === true, tx: values.tx === true, maxSensitivity: maximum });
1653
1813
  if (values.out === undefined) {
@@ -1687,7 +1847,7 @@ export const generateCommand = (argv) => {
1687
1847
  if (values.out !== undefined && sameFile(db, values.out)) {
1688
1848
  return fail(`cave generate: --out '${values.out}' is the source database — refusing to overwrite it\n`);
1689
1849
  }
1690
- const store = open(db, values['no-prelude'] === true ? { registry: Registry.empty } : {});
1850
+ const store = openDb(values, 'read');
1691
1851
  try {
1692
1852
  const generated = generateClient(store, version === undefined ? {} : { version });
1693
1853
  if (!generated.ok) {
@@ -1750,7 +1910,7 @@ export const reconstructCommand = async (argv, runtime = {}) => {
1750
1910
  if (timeoutSeconds !== undefined && (!Number.isFinite(timeoutSeconds) || timeoutSeconds <= 0)) {
1751
1911
  return fail(`cave reconstruct: --timeout must be a positive number of seconds, got '${values.timeout}'\n`);
1752
1912
  }
1753
- const store = open(values.db ?? defaultDbPath(), values['no-prelude'] === true ? { registry: Registry.empty } : {});
1913
+ const store = openDb(values, 'read');
1754
1914
  try {
1755
1915
  const graph = sqliteStore(store);
1756
1916
  const result = values.agent === undefined ?
@@ -1825,6 +1985,19 @@ export const cave = (argv) => {
1825
1985
  (rest.includes('--help') || rest.includes('-h'))) {
1826
1986
  return ok(`${commandHelp[canonical]}\n`);
1827
1987
  }
1988
+ try {
1989
+ return run(canonical, rest);
1990
+ }
1991
+ catch (error) {
1992
+ // A --db path that is not what the command needs (spec §13.7) is a
1993
+ // usage failure, reported the way dispatch reports thrown errors.
1994
+ if (error instanceof LocateError) {
1995
+ return fail(`cave ${canonical}: ${error.message}\n`);
1996
+ }
1997
+ throw error;
1998
+ }
1999
+ };
2000
+ const run = (canonical, rest) => {
1828
2001
  switch (canonical) {
1829
2002
  case 'parse':
1830
2003
  return parseCommand(rest);
@@ -1871,7 +2044,7 @@ export const cave = (argv) => {
1871
2044
  case '-h':
1872
2045
  return ok(`${usage}\n`);
1873
2046
  default:
1874
- return fail(`cave: unknown command ${JSON.stringify(command)}\n\n${usage}\n`, 2);
2047
+ return fail(`cave: unknown command ${JSON.stringify(canonical)}\n\n${usage}\n`, 2);
1875
2048
  }
1876
2049
  };
1877
2050
  //# sourceMappingURL=cli.js.map