@cavelang/cli 0.32.3 → 0.34.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 (58) hide show
  1. package/README.md +16 -3
  2. package/dist/internal/automate/main.d.ts.map +1 -1
  3. package/dist/internal/automate/main.js +7 -2
  4. package/dist/internal/automate/main.js.map +1 -1
  5. package/dist/internal/connect/main.d.ts.map +1 -1
  6. package/dist/internal/connect/main.js +16 -10
  7. package/dist/internal/connect/main.js.map +1 -1
  8. package/dist/internal/connect/template.d.ts +9 -2
  9. package/dist/internal/connect/template.d.ts.map +1 -1
  10. package/dist/internal/connect/template.js +36 -5
  11. package/dist/internal/connect/template.js.map +1 -1
  12. package/dist/internal/ingest/main.d.ts.map +1 -1
  13. package/dist/internal/ingest/main.js +18 -2
  14. package/dist/internal/ingest/main.js.map +1 -1
  15. package/dist/internal/loop/demo.d.ts.map +1 -1
  16. package/dist/internal/loop/demo.js +2 -1
  17. package/dist/internal/loop/demo.js.map +1 -1
  18. package/dist/internal/mcp/guide.js +3 -3
  19. package/dist/internal/mcp/guide.js.map +1 -1
  20. package/dist/internal/mcp/main.d.ts +1 -1
  21. package/dist/internal/mcp/main.d.ts.map +1 -1
  22. package/dist/internal/mcp/main.js +13 -3
  23. package/dist/internal/mcp/main.js.map +1 -1
  24. package/dist/internal/mcp/tools.d.ts.map +1 -1
  25. package/dist/internal/mcp/tools.js +39 -10
  26. package/dist/internal/mcp/tools.js.map +1 -1
  27. package/dist/internal/sync/sync.d.ts +2 -3
  28. package/dist/internal/sync/sync.d.ts.map +1 -1
  29. package/dist/internal/sync/sync.js +3 -15
  30. package/dist/internal/sync/sync.js.map +1 -1
  31. package/dist/internal/view/main.js +3 -3
  32. package/dist/internal/view/main.js.map +1 -1
  33. package/dist/internal/view/report.d.ts.map +1 -1
  34. package/dist/internal/view/report.js +6 -1
  35. package/dist/internal/view/report.js.map +1 -1
  36. package/dist/src/cli.d.ts +14 -5
  37. package/dist/src/cli.d.ts.map +1 -1
  38. package/dist/src/cli.js +224 -34
  39. package/dist/src/cli.js.map +1 -1
  40. package/dist/src/commands.d.ts +3 -0
  41. package/dist/src/commands.d.ts.map +1 -1
  42. package/dist/src/commands.js +1 -0
  43. package/dist/src/commands.js.map +1 -1
  44. package/dist/src/dispatch.d.ts.map +1 -1
  45. package/dist/src/dispatch.js +3 -1
  46. package/dist/src/dispatch.js.map +1 -1
  47. package/dist/src/doctor.js +1 -1
  48. package/dist/src/doctor.js.map +1 -1
  49. package/dist/src/index.d.ts +2 -2
  50. package/dist/src/index.d.ts.map +1 -1
  51. package/dist/src/index.js +2 -2
  52. package/dist/src/index.js.map +1 -1
  53. package/package.json +19 -19
  54. package/src/cli.ts +232 -37
  55. package/src/commands.ts +1 -0
  56. package/src/dispatch.ts +3 -1
  57. package/src/doctor.ts +1 -1
  58. package/src/index.ts +2 -2
package/src/cli.ts CHANGED
@@ -9,12 +9,13 @@
9
9
  * - `cave highlight [file]` — ANSI syntax colors (async)
10
10
  * - `cave add [--db <path>] [file…]` — ingest into a store (`--strict`, `--check`)
11
11
  * - `cave query [--db <path>] '<pattern>'` — CAVE-Q (`--json`, `--all`, `--aliases`, `--as-of`, `--resolve`)
12
+ * - `cave search [--db <path>] <terms..>` — FTS5 full-text search over claims and comments (`--raw`, `--limit`, `--json`)
12
13
  * - `cave resolve [--db <path>]` — contested facts under the §26 policy (`--aliases`, `--policy`, `--json`)
13
14
  * - `cave derive [--db <path>] [rules.cave…]` — fire rules (`--dry-run`, `--full`, `--list`, `--retract`)
14
15
  * - `cave act [--db <path>] <name> [param=value…]` — execute an action (spec §25; `--declare`, `--list`, `--retract`)
15
16
  * - `cave automate [--db <path>]` — the event-driven loop (spec §29; async)
16
17
  * - `cave check [--db <path>]` — knowledge health report (`--stale`, `--json`)
17
- * - `cave backup [--db <path>] --out <path>` — exact verified SQLite snapshot
18
+ * - `cave backup [--db <path>] --out <path>` — exact verified store snapshot
18
19
  * - `cave restore <snapshot> --db <path>` — verify and atomically restore a snapshot
19
20
  * - `cave generate [--db <path>]` — versioned TypeScript client from EXPECTS declarations
20
21
  * - `cave suggest-alias [--db <path>]` — same-entity candidates (spec §27; async)
@@ -38,11 +39,11 @@ import { readFileSync, readSync, statSync, writeFileSync } from 'node:fs'
38
39
  import { resolve } from 'node:path'
39
40
  import { parseArgs } from 'node:util'
40
41
  import { Version } from '@cavelang/core'
41
- import { parseDocument } from '@cavelang/parser'
42
+ import { parseDocument, Token } from '@cavelang/parser'
42
43
  import { Registry, standardRegistry } from '@cavelang/canonical'
43
- import { Sensitivity, backup as backupStore, defaultDbPath, open, restoreBackup, verifyBackup } from '@cavelang/store'
44
- import type { Store } from '@cavelang/store'
45
- import { defaultLimit as defaultQueryLimit, page as caveQueryPage } from '@cavelang/query'
44
+ import { LocateError, Sensitivity, backup as backupStore, defaultDbPath, openAt, restoreBackup, verifyBackup } from '@cavelang/store'
45
+ import type { OpenIntent, Store } from '@cavelang/store'
46
+ import { defaultLimit as defaultQueryLimit, maxLimit as maxQueryLimit, page as caveQueryPage } from '@cavelang/query'
46
47
  import {
47
48
  check as caveCheck, defaultMinScore, defaultStaleDays, gatedIngest, judgePrompt, parseJudgeReply,
48
49
  generateClient, suggestAliases, writeSuggestions
@@ -115,13 +116,14 @@ Usage:
115
116
  cave add [--db <path>] [file...] ingest into a store [--strict] [--check] [--no-prelude] [--no-src]
116
117
  cave import [--db <path>] [file...] restore/merge from CAVE text (add without @src: stamping)
117
118
  cave query [--db <path>] <pattern> run a bounded CAVE-Q page [--limit <n>] [--cursor <token>] [--json]
119
+ cave search [--db <path>] <terms..> full-text search over claims and comments (FTS5) [--raw] [--limit <n>] [--json]
118
120
  cave resolve [--db <path>] contested facts + winners (spec §26) [--aliases] [--policy] [--json]
119
121
  cave derive [--db <path>] [rules.cave..] declare + fire rules (spec §24) [--dry-run] [--full] [--list] [--retract <rule>]
120
122
  cave act [--db <path>] <name> [p=v...] execute an action (spec §25) [--dry-run] [--no-check] [--hooks <file>]
121
123
  cave act --declare [file...] declare actions from a CAVE document; --list / --retract <name> manage them
122
124
  cave automate [--db <path>] event-driven loop (spec §29): new claims fire rules/actions/hooks/agent prompts [--once]
123
125
  cave check [--db <path>] knowledge health report (spec §20) [--stale <days>] [--json]
124
- cave backup [--db <path>] --out <file> exact verified SQLite snapshot [--force]
126
+ cave backup [--db <path>] --out <file> exact verified store snapshot [--force]
125
127
  cave restore <snapshot> --db <path> verify and atomically restore an exact snapshot [--force]
126
128
  cave generate [--db <path>] generate a typed client from EXPECTS [--out <file>] [--version <n>]
127
129
  cave suggest-alias [--db <path>] propose same-entity ALIAS candidates (spec §27) [--min <s>] [--agent] [--write]
@@ -140,10 +142,12 @@ Usage:
140
142
  cave help [command] this text, or one command's options and examples
141
143
 
142
144
  Every command answers --help. --db defaults to $CAVE_DB, or cave.db in
143
- the current directory. The spec lives in the .claude/skills/ directory at
145
+ the current directory, and names a SQLite store or a CAVE text file: text
146
+ replays into an in-memory store for commands that only read (writes refuse
147
+ it), and a read never creates a missing database. The spec lives in the .claude/skills/ directory at
144
148
  the repository root (section index in README.md).`
145
149
 
146
- const dbHelp = `--db <path> database file (default: $CAVE_DB, or cave.db)`
150
+ const dbHelp = `--db <path> store: a SQLite file, or CAVE text read into memory (default: $CAVE_DB, or cave.db)`
147
151
 
148
152
  /** Per-command help, printed for \`cave <command> --help\` and \`cave help <command>\`. */
149
153
  export const commandHelp: Record<string, string> = {
@@ -278,10 +282,13 @@ Options:
278
282
 
279
283
  Patterns are claim triples with ?variables and optional metadata filters
280
284
  (spec §12). A second positional starting with WHERE filters on conf,
281
- value or tx.
285
+ value or tx. A binding line carries the matched claim's comment
286
+ (?x = value ; comment), so evidence written next to a claim travels
287
+ with the answer; fully bound patterns print the raw line, comment included.
282
288
 
283
289
  Examples:
284
290
  cave query '?x USES jwt'
291
+ cave query --db notes.cave '?x USES jwt'
285
292
  cave query --db k.db '?x HAS bug: ?bug #security'
286
293
  cave query --db k.db '?cause CAUSE app/crash' 'WHERE conf >= 0.5'
287
294
  cave query --db k.db 'terrier EXTENDS+ animal'
@@ -292,6 +299,39 @@ Examples:
292
299
  cave query --db k.db 'service HAS owner: ?who' --resolve
293
300
  cave query --db k.db '?x ?verb ?y @production' --json`,
294
301
 
302
+ search: `cave search — full-text search over claims and comments (spec §13.2)
303
+
304
+ Usage:
305
+ cave search [--db <path>] <terms...> [--raw] [--limit <n>] [--json] [--no-prelude]
306
+
307
+ Options:
308
+ ${dbHelp}
309
+ --raw treat the terms as FTS5 MATCH syntax: AND / OR / NOT,
310
+ NEAR(a b), prefix* and column filters such as
311
+ comment:heap or subject:auth
312
+ --limit <n> matches to print (default ${defaultQueryLimit}, maximum ${maxQueryLimit})
313
+ --json emit a versioned cave.search/v1 JSON object whose matches
314
+ are cave.claim/v1 records
315
+ --no-prelude open the store without the standard verb registry
316
+
317
+ Searches the full-text index the store maintains over every claim:
318
+ subject, verb, object, attribute name, value text, comment, and the raw
319
+ line as written (so tags, contexts, and inverse spellings match too).
320
+ Terms are one literal phrase by default — safe for hyphenated names such
321
+ as token-expiry, which raw FTS5 syntax would read as a column filter.
322
+ Tokens split on punctuation, so heap-dump also matches "heap dump" and
323
+ auth/middleware matches auth middleware. Results are newest first, one
324
+ raw line each, comments included; retracted and superseded rows stay
325
+ searchable (spec §9.6), so pair a hit with cave query or cave export
326
+ --current when currency matters.
327
+
328
+ Examples:
329
+ cave search 'heap dump'
330
+ cave search --db k.db token-expiry
331
+ cave search --db k.db --raw 'comment:heap AND subject:auth'
332
+ cave search --db k.db --raw 'jwt OR oauth*' --limit 20
333
+ cave search --db k.db 'rotated weekly' --json`,
334
+
295
335
  resolve: `cave resolve — contested facts and their winners (spec §26)
296
336
 
297
337
  Usage:
@@ -434,7 +474,7 @@ Examples:
434
474
  cave check --db k.db --stale 30
435
475
  cave check --db k.db --json | jq '.violations'`,
436
476
 
437
- backup: `cave backup — exact, verified SQLite snapshot (spec §13.2.2)
477
+ backup: `cave backup — exact, verified store snapshot (spec §13.2.2)
438
478
 
439
479
  Usage:
440
480
  cave backup [--db <path>] --out <file> [--force]
@@ -456,7 +496,7 @@ Examples:
456
496
  cave backup --db k.db --out backups/k.db
457
497
  cave backup --verify backups/k.db --sha256 <hex>`,
458
498
 
459
- restore: `cave restore — verify and atomically restore an exact SQLite snapshot (spec §13.2.2)
499
+ restore: `cave restore — verify and atomically restore an exact store snapshot (spec §13.2.2)
460
500
 
461
501
  Usage:
462
502
  cave restore <snapshot> --db <path> [--force] [--sha256 <hex>]
@@ -712,7 +752,19 @@ topic → fix over a small in-memory store.`,
712
752
  version: `cave version — print the cave version
713
753
 
714
754
  Usage:
715
- cave version`
755
+ cave version`,
756
+
757
+ help: `cave help — the overview, or one command's options and examples
758
+
759
+ Usage:
760
+ cave help
761
+ cave help <command>
762
+
763
+ Every command answers --help with the same text as cave help <command>.
764
+
765
+ Examples:
766
+ cave help query
767
+ cave help serve`
716
768
  }
717
769
 
718
770
  const readStdin = (): string => {
@@ -733,6 +785,21 @@ const readStdin = (): string => {
733
785
  }
734
786
  }
735
787
 
788
+ /**
789
+ * Opens the store `--db` names (spec §13.7): a SQLite file, or a CAVE text
790
+ * file replayed into memory. `read` never creates, migrates, or changes a
791
+ * file — a missing path is an error, not an empty database; `scratch` is a
792
+ * dry run that appends inside a rolled-back transaction, so it may write
793
+ * but creates and migrates nothing; `write` creates a missing SQLite store,
794
+ * migrates an older one, and refuses text files with the materialization
795
+ * hint.
796
+ */
797
+ const openDb = (values: { db?: string, 'no-prelude'?: boolean }, intent: OpenIntent): Store =>
798
+ openAt(values.db ?? defaultDbPath(), {
799
+ intent,
800
+ ...values['no-prelude'] === true ? { registry: Registry.empty } : {}
801
+ })
802
+
736
803
  const readInput = (files: readonly string[]): string =>
737
804
  files.length === 0 || (files.length === 1 && files[0] === '-') ?
738
805
  readStdin() :
@@ -816,7 +883,7 @@ const ingestCommand = (name: 'add' | 'import') => (argv: readonly string[]): Out
816
883
  allowPositionals: true
817
884
  })
818
885
  const input = readInput(positionals)
819
- const store = open(values.db ?? defaultDbPath(), values['no-prelude'] === true ? { registry: Registry.empty } : {})
886
+ const store = openDb(values, 'write')
820
887
  try {
821
888
  const options = {
822
889
  strict: values.strict === true,
@@ -883,7 +950,7 @@ export const queryCommand = (argv: readonly string[]): Output => {
883
950
  return fail('cave query: a pattern is required (spec §12.1)\n')
884
951
  }
885
952
  const pattern = positionals.join('\n')
886
- const store = open(values.db ?? defaultDbPath(), values['no-prelude'] === true ? { registry: Registry.empty } : {})
953
+ const store = openDb(values, 'read')
887
954
  try {
888
955
  const limit = values.limit === undefined ? defaultQueryLimit : Number(values.limit)
889
956
  const result = caveQueryPage(store, pattern, {
@@ -908,7 +975,14 @@ export const queryCommand = (argv: readonly string[]): Output => {
908
975
  .join(' ')
909
976
  // A fully bound pattern has no bindings to print; transitive matches
910
977
  // additionally carry no row — confirm with the pattern itself.
911
- const line = bindings !== '' ? bindings : match.claim?.claim.raw ?? pattern.split('\n')[0]!
978
+ // Bindings carry the matched claim's comment so the evidence written
979
+ // next to a claim travels with the answer; raw lines already do. A
980
+ // multi-line comment (§6.4) opens above the binding line, as it does
981
+ // above a claim.
982
+ const comment = match.claim?.claim.comment
983
+ const line = bindings !== '' ?
984
+ comment === undefined ? bindings : Token.joinComment(`${bindings} `, comment) :
985
+ match.claim?.claim.raw ?? pattern.split('\n')[0]!
912
986
  // An interpolated trajectory shows its value at the anchor
913
987
  // (spec §32.4) — bindings already carry it in the value slot.
914
988
  return match.at !== undefined && bindings === '' ?
@@ -923,6 +997,66 @@ export const queryCommand = (argv: readonly string[]): Output => {
923
997
  }
924
998
  }
925
999
 
1000
+ export const searchFormat = 'cave.search' as const
1001
+ export const searchVersion = 1 as const
1002
+
1003
+ /**
1004
+ * `cave search` — the store's FTS5 index (spec §13.2) from the shell:
1005
+ * one literal phrase by default, raw MATCH syntax on request, newest
1006
+ * matches first. Lines print as written, so comments come along.
1007
+ */
1008
+ export const searchCommand = (argv: readonly string[]): Output => {
1009
+ const { values, positionals } = parseArgs({
1010
+ args: [...argv],
1011
+ options: {
1012
+ db: { type: 'string' },
1013
+ raw: { type: 'boolean' },
1014
+ limit: { type: 'string' },
1015
+ json: { type: 'boolean' },
1016
+ 'no-prelude': { type: 'boolean' }
1017
+ },
1018
+ allowPositionals: true
1019
+ })
1020
+ const query = positionals.join(' ').trim()
1021
+ if (query === '') {
1022
+ return fail('cave search: search terms are required\n')
1023
+ }
1024
+ const limit = values.limit === undefined ? defaultQueryLimit : Number(values.limit)
1025
+ if (!Number.isInteger(limit) || limit < 1 || limit > maxQueryLimit) {
1026
+ return fail(`cave search: --limit must be an integer from 1 to ${maxQueryLimit}\n`)
1027
+ }
1028
+ const store = openDb(values, 'read')
1029
+ try {
1030
+ const raw = values.raw === true
1031
+ const found = store.search(query, { raw, limit: limit + 1 })
1032
+ const matches = found.slice(0, limit)
1033
+ const truncated = found.length > limit
1034
+ if (values.json === true) {
1035
+ return ok(`${JSON.stringify({
1036
+ format: searchFormat,
1037
+ version: searchVersion,
1038
+ query,
1039
+ raw,
1040
+ limit,
1041
+ truncated,
1042
+ matches: matches.map(row => store.recordOf(row))
1043
+ }, undefined, 2)}\n`)
1044
+ }
1045
+ if (matches.length === 0) {
1046
+ return ok('no matches\n')
1047
+ }
1048
+ const lines = matches.map(row => row.raw_line)
1049
+ if (truncated) {
1050
+ lines.push(`more matches beyond ${limit}; raise --limit`)
1051
+ }
1052
+ return ok(`${lines.join('\n')}\n`)
1053
+ } catch (error) {
1054
+ return fail(`${error instanceof Error ? error.message : String(error)}\n`)
1055
+ } finally {
1056
+ store.close()
1057
+ }
1058
+ }
1059
+
926
1060
  /**
927
1061
  * `cave resolve` — the human view of contradiction resolution (spec §26.4):
928
1062
  * contested facts with their candidates ranked as the policy sees them,
@@ -941,7 +1075,7 @@ export const resolveCommand = (argv: readonly string[]): Output => {
941
1075
  allowPositionals: false
942
1076
  })
943
1077
  const percent = (value: number): string => `${Math.round(value * 1000) / 10}%`
944
- const store = open(values.db ?? defaultDbPath(), values['no-prelude'] === true ? { registry: Registry.empty } : {})
1078
+ const store = openDb(values, 'read')
945
1079
  try {
946
1080
  if (values.policy === true) {
947
1081
  const policy = store.resolutionPolicy()
@@ -1004,7 +1138,12 @@ export const deriveCommand = (argv: readonly string[]): Output => {
1004
1138
  if (maxPasses !== undefined && (!Number.isInteger(maxPasses) || maxPasses < 1)) {
1005
1139
  return fail(`cave derive: --max-passes expects a positive integer, got ${JSON.stringify(values['max-passes'])}\n`)
1006
1140
  }
1007
- const store = open(values.db ?? defaultDbPath(), values['no-prelude'] === true ? { registry: Registry.empty } : {})
1141
+ // The intent follows the branch order below: listing reads, retracting
1142
+ // writes, a dry run appends inside a rolled-back transaction.
1143
+ const store = openDb(values,
1144
+ values.list === true ? 'read' :
1145
+ values.retract !== undefined ? 'write' :
1146
+ values['dry-run'] === true ? 'scratch' : 'write')
1008
1147
  try {
1009
1148
  if (values.list === true) {
1010
1149
  const rules = listRules(store)
@@ -1158,7 +1297,14 @@ export const actCommand = (argv: readonly string[]): Output => {
1158
1297
  },
1159
1298
  allowPositionals: true
1160
1299
  })
1161
- const store = open(values.db ?? defaultDbPath(), values['no-prelude'] === true ? { registry: Registry.empty } : {})
1300
+ // The intent follows the branch order below: declaring and retracting
1301
+ // write, listing reads, a dry-run execution appends inside a rolled-back
1302
+ // transaction.
1303
+ const store = openDb(values,
1304
+ values.declare === true ? 'write' :
1305
+ values.list === true ? 'read' :
1306
+ values.retract !== undefined ? 'write' :
1307
+ values['dry-run'] === true ? 'scratch' : 'write')
1162
1308
  try {
1163
1309
  if (values.declare === true) {
1164
1310
  const declaration = declareActions(store, readInput(positionals))
@@ -1279,7 +1425,7 @@ export const checkCommand = (argv: readonly string[]): Output => {
1279
1425
  if (!Number.isFinite(staleDays) || staleDays < 0) {
1280
1426
  return fail(`cave check: --stale expects a non-negative number of days, got ${JSON.stringify(values.stale)}\n`)
1281
1427
  }
1282
- const store = open(values.db ?? defaultDbPath(), values['no-prelude'] === true ? { registry: Registry.empty } : {})
1428
+ const store = openDb(values, 'read')
1283
1429
  try {
1284
1430
  const report = caveCheck(store, { staleDays })
1285
1431
  const out = values.json === true ?
@@ -1299,7 +1445,7 @@ const snapshotLine = (
1299
1445
  `${action} exact backup (${snapshot.rows} row(s), schema v${snapshot.schemaVersion}, ` +
1300
1446
  `${snapshot.bytes} bytes, sha256:${snapshot.sha256}) at ${snapshot.path}\n`
1301
1447
 
1302
- /** `cave backup` — create or verify an exact SQLite snapshot (§13.2.2). */
1448
+ /** `cave backup` — create or verify an exact store snapshot (§13.2.2). */
1303
1449
  export const backupCommand = (argv: readonly string[]): Output => {
1304
1450
  const { values } = parseArgs({
1305
1451
  args: [...argv],
@@ -1328,7 +1474,15 @@ export const backupCommand = (argv: readonly string[]): Output => {
1328
1474
  if (values.out === undefined) {
1329
1475
  return fail('cave backup: --out <file> is required\n')
1330
1476
  }
1331
- const store = open(values.db ?? defaultDbPath())
1477
+ const dbPath = values.db ?? defaultDbPath()
1478
+ // A text store is replayed into memory, so the backup API cannot see that
1479
+ // the destination aliases the source: refuse before the file could be
1480
+ // replaced by a SQLite snapshot of itself (a SQLite source is covered by
1481
+ // the API's own check).
1482
+ if (sameFile(dbPath, values.out)) {
1483
+ return fail(`cave backup: --out '${values.out}' is the source database — refusing to overwrite it\n`)
1484
+ }
1485
+ const store = openAt(dbPath, { intent: 'read' })
1332
1486
  try {
1333
1487
  return ok(snapshotLine('created', backupStore(store, values.out, { force: values.force === true })))
1334
1488
  } finally {
@@ -1411,7 +1565,7 @@ export const suggestAliasCommand = async (
1411
1565
  if (timeoutSeconds !== undefined && (!Number.isFinite(timeoutSeconds) || timeoutSeconds <= 0)) {
1412
1566
  return fail(`cave suggest-alias: --timeout must be a positive number of seconds, got '${values.timeout}'\n`)
1413
1567
  }
1414
- const store = open(values.db ?? defaultDbPath(), values['no-prelude'] === true ? { registry: Registry.empty } : {})
1568
+ const store = openDb(values, values.write === true ? 'write' : 'read')
1415
1569
  try {
1416
1570
  let suggestions = suggestAliases(store, { minScore, ...limit === undefined ? {} : { limit } })
1417
1571
  if (values.agent !== undefined && suggestions.length > 0) {
@@ -1468,7 +1622,7 @@ export const syncCommand = (argv: readonly string[]): Output => {
1468
1622
  return fail('cave sync: exactly one source is required — a CAVE store file, ;@-annotated text, or - for stdin (spec §28.5)\n')
1469
1623
  }
1470
1624
  const dbPath = values.db ?? defaultDbPath()
1471
- const store = open(dbPath, values['no-prelude'] === true ? { registry: Registry.empty } : {})
1625
+ const store = openDb(values, values['dry-run'] === true ? 'scratch' : 'write')
1472
1626
  try {
1473
1627
  const options = {
1474
1628
  into: values.into === undefined ? labelOf(dbPath) : sanitizeLabel(values.into),
@@ -1529,7 +1683,10 @@ export const reportCommand = (argv: readonly string[]): Output => {
1529
1683
  if (maximum === undefined) {
1530
1684
  return fail(`cave report: --max-sensitivity expects ${Sensitivity.levels.join(', ')}, got ${JSON.stringify(values['max-sensitivity'])}\n`)
1531
1685
  }
1532
- const store = open(values.db ?? defaultDbPath(), values['no-prelude'] === true ? { registry: Registry.empty } : {})
1686
+ if (values.out !== undefined && sameFile(values.db ?? defaultDbPath(), values.out)) {
1687
+ return fail(`cave report: --out '${values.out}' is the source database — refusing to overwrite it\n`)
1688
+ }
1689
+ const store = openDb(values, 'read')
1533
1690
  try {
1534
1691
  const rendered = caveReport(store, template, {
1535
1692
  aliases: values.aliases === true,
@@ -1597,7 +1754,7 @@ export const exportCommand = (argv: readonly string[]): Output => {
1597
1754
  if (values.out !== undefined && sameFile(db, values.out)) {
1598
1755
  return fail(`cave export: --out '${values.out}' is the source database — refusing to overwrite it\n`)
1599
1756
  }
1600
- const store = open(db, values['no-prelude'] === true ? { registry: Registry.empty } : {})
1757
+ const store = openDb(values, 'read')
1601
1758
  try {
1602
1759
  const text = store.exportText({ current: values.current === true, tx: values.tx === true, maxSensitivity: maximum })
1603
1760
  if (values.out === undefined) {
@@ -1636,7 +1793,7 @@ export const generateCommand = (argv: readonly string[]): Output => {
1636
1793
  if (values.out !== undefined && sameFile(db, values.out)) {
1637
1794
  return fail(`cave generate: --out '${values.out}' is the source database — refusing to overwrite it\n`)
1638
1795
  }
1639
- const store = open(db, values['no-prelude'] === true ? { registry: Registry.empty } : {})
1796
+ const store = openDb(values, 'read')
1640
1797
  try {
1641
1798
  const generated = generateClient(store, version === undefined ? {} : { version })
1642
1799
  if (!generated.ok) {
@@ -1701,7 +1858,7 @@ export const reconstructCommand = async (
1701
1858
  if (timeoutSeconds !== undefined && (!Number.isFinite(timeoutSeconds) || timeoutSeconds <= 0)) {
1702
1859
  return fail(`cave reconstruct: --timeout must be a positive number of seconds, got '${values.timeout}'\n`)
1703
1860
  }
1704
- const store = open(values.db ?? defaultDbPath(), values['no-prelude'] === true ? { registry: Registry.empty } : {})
1861
+ const store = openDb(values, 'read')
1705
1862
  try {
1706
1863
  const graph = sqliteStore(store)
1707
1864
  const result = values.agent === undefined ?
@@ -1717,9 +1874,11 @@ export const reconstructCommand = async (
1717
1874
  ),
1718
1875
  positionals
1719
1876
  )
1877
+ // The trace is a documentary comment block: the blank line after it
1878
+ // keeps it off the first claim when the output is added back (§6.4).
1720
1879
  const lines = [
1721
- ...values.trace === true ?
1722
- result.trace.map(step => `; ${step.step}. ${step.cue.entity} @ ${step.cue.score.toFixed(2)} +${step.collected} claim(s)`) :
1880
+ ...values.trace === true && result.trace.length > 0 ?
1881
+ [...result.trace.map(step => `; ${step.step}. ${step.cue.entity} @ ${step.cue.score.toFixed(2)} +${step.collected} claim(s)`), ''] :
1723
1882
  [],
1724
1883
  ...result.claims.map(claim => emitClaim(claim))
1725
1884
  ]
@@ -1732,15 +1891,36 @@ export const reconstructCommand = async (
1732
1891
  }
1733
1892
  }
1734
1893
 
1735
- export const demoCommand = (): Output =>
1736
- ok(`${Demo.run().lines.join('\n')}\n`)
1894
+ /**
1895
+ * Positional arguments of a command that declares no options: an unknown
1896
+ * option throws, which the dispatcher reports as an ordinary problem
1897
+ * (status 1).
1898
+ */
1899
+ const positionalsOf = (argv: readonly string[]): readonly string[] =>
1900
+ parseArgs({ args: [...argv], options: {}, allowPositionals: true }).positionals
1901
+
1902
+ /** The usage error (status 2) for more positionals than a command takes. */
1903
+ const surplusPositionals = (command: string, positionals: readonly string[], max: number): Output | undefined =>
1904
+ positionals.length > max ? fail(`cave ${command}: unexpected positional arguments\n`, 2) : undefined
1737
1905
 
1738
- export const versionCommand = (): Output =>
1739
- ok(`${Version.current()}\n`)
1906
+ export const demoCommand = (argv: readonly string[] = []): Output => {
1907
+ const problem = surplusPositionals('demo', positionalsOf(argv), 0)
1908
+ if (problem !== undefined) return problem
1909
+ return ok(`${Demo.run().lines.join('\n')}\n`)
1910
+ }
1911
+
1912
+ export const versionCommand = (argv: readonly string[] = []): Output => {
1913
+ const problem = surplusPositionals('version', positionalsOf(argv), 0)
1914
+ if (problem !== undefined) return problem
1915
+ return ok(`${Version.current()}\n`)
1916
+ }
1740
1917
 
1741
1918
  /** `cave help [command]` — the overview, or one command's help text. */
1742
1919
  export const helpCommand = (argv: readonly string[]): Output => {
1743
- const [topic] = argv
1920
+ const positionals = positionalsOf(argv)
1921
+ const problem = surplusPositionals('help', positionals, 1)
1922
+ if (problem !== undefined) return problem
1923
+ const [topic] = positionals
1744
1924
  if (topic === undefined) {
1745
1925
  return ok(`${usage}\n`)
1746
1926
  }
@@ -1763,6 +1943,19 @@ export const cave = (argv: readonly string[]): Output => {
1763
1943
  (rest.includes('--help') || rest.includes('-h'))) {
1764
1944
  return ok(`${commandHelp[canonical]}\n`)
1765
1945
  }
1946
+ try {
1947
+ return run(canonical, rest)
1948
+ } catch (error) {
1949
+ // A --db path that is not what the command needs (spec §13.7) is a
1950
+ // usage failure, reported the way dispatch reports thrown errors.
1951
+ if (error instanceof LocateError) {
1952
+ return fail(`cave ${canonical}: ${error.message}\n`)
1953
+ }
1954
+ throw error
1955
+ }
1956
+ }
1957
+
1958
+ const run = (canonical: undefined | string, rest: readonly string[]): Output => {
1766
1959
  switch (canonical) {
1767
1960
  case 'parse':
1768
1961
  return parseCommand(rest)
@@ -1772,6 +1965,8 @@ export const cave = (argv: readonly string[]): Output => {
1772
1965
  return importCommand(rest)
1773
1966
  case 'query':
1774
1967
  return queryCommand(rest)
1968
+ case 'search':
1969
+ return searchCommand(rest)
1775
1970
  case 'resolve':
1776
1971
  return resolveCommand(rest)
1777
1972
  case 'derive':
@@ -1795,11 +1990,11 @@ export const cave = (argv: readonly string[]): Output => {
1795
1990
  case 'doctor':
1796
1991
  return doctorCommand(rest)
1797
1992
  case 'demo':
1798
- return demoCommand()
1993
+ return demoCommand(rest)
1799
1994
  case 'version':
1800
1995
  case '--version':
1801
1996
  case '-v':
1802
- return versionCommand()
1997
+ return versionCommand(rest)
1803
1998
  case 'help':
1804
1999
  return helpCommand(rest)
1805
2000
  case undefined:
@@ -1807,6 +2002,6 @@ export const cave = (argv: readonly string[]): Output => {
1807
2002
  case '-h':
1808
2003
  return ok(`${usage}\n`)
1809
2004
  default:
1810
- return fail(`cave: unknown command ${JSON.stringify(command)}\n\n${usage}\n`, 2)
2005
+ return fail(`cave: unknown command ${JSON.stringify(canonical)}\n\n${usage}\n`, 2)
1811
2006
  }
1812
2007
  }
package/src/commands.ts CHANGED
@@ -19,6 +19,7 @@ export const commandRegistry = [
19
19
  { name: 'add', importantOptions: ['--strict', '--check', '--no-prelude', '--no-src'] },
20
20
  { name: 'import', importantOptions: ['--strict', '--no-prelude'] },
21
21
  { name: 'query', importantOptions: ['--json', '--limit', '--cursor', '--all', '--aliases', '--as-of', '--at', '--resolve', '--no-prelude'] },
22
+ { name: 'search', importantOptions: ['--raw', '--limit', '--json', '--no-prelude'] },
22
23
  { name: 'resolve', importantOptions: ['--aliases', '--policy', '--json', '--no-prelude'] },
23
24
  { name: 'derive', importantOptions: ['--dry-run', '--full', '--aliases', '--min-conf', '--max-passes', '--list', '--retract', '--json', '--no-prelude'] },
24
25
  { name: 'act', importantOptions: ['--declare', '--list', '--retract', '--dry-run', '--no-check', '--aliases', '--hooks', '--json', '--no-prelude'] },
package/src/dispatch.ts CHANGED
@@ -22,8 +22,10 @@ const processRuntime = (): CommandRuntime => ({
22
22
 
23
23
  const delegatedHelp = new Set<string>(delegatedCommandNames)
24
24
 
25
+ // Exactly `help <delegated command>` becomes `<command> --help`; anything
26
+ // longer stays with `cave help`, which rejects the surplus as a usage error.
25
27
  const normalizedArgs = (raw: readonly string[]): readonly string[] =>
26
- raw[0] === 'help' && raw[1] !== undefined && delegatedHelp.has(raw[1]) ? [raw[1], '--help'] : raw
28
+ raw.length === 2 && raw[0] === 'help' && delegatedHelp.has(raw[1]!) ? [raw[1]!, '--help'] : raw
27
29
 
28
30
  const write = (runtime: CommandRuntime, output: Output): number => {
29
31
  if (output.out !== '') runtime.stdout.write(output.out)
package/src/doctor.ts CHANGED
@@ -249,7 +249,7 @@ const inspectDatabase = (path: string, source: DatabaseConfiguration['source']):
249
249
  configuration,
250
250
  checks: [check('store.database', 'warn',
251
251
  `Database schema ${version} needs migration to schema ${Schema.currentVersion}`,
252
- 'Back up the database, then open it with a normal cave command to migrate it.')]
252
+ 'Close every user and copy the database file as a rollback point, then open it with a writing command such as cave add to migrate it; reads never migrate.')]
253
253
  }
254
254
  }
255
255
 
package/src/index.ts CHANGED
@@ -1,10 +1,10 @@
1
1
  /**
2
- * `@cavelang/cli` — the `cave` command (parse / add / query / resolve / derive / act / check / suggest-alias / sync / export / report / reconstruct / demo).
2
+ * `@cavelang/cli` — the `cave` command (parse / add / query / search / resolve / derive / act / check / suggest-alias / sync / export / report / reconstruct / demo).
3
3
  * Programmatic access to the same implementations lives here; `dispatch`
4
4
  * and `runCli` expose the shared awaited command lifecycle.
5
5
  */
6
6
 
7
- export { actCommand, addCommand, backupCommand, cave, checkCommand, commandHelp, demoCommand, deriveCommand, exportCommand, generateCommand, helpCommand, highlightCommand, importCommand, parseCommand, queryCommand, reconstructCommand, reportCommand, resolveCommand, restoreCommand, suggestAliasCommand, syncCommand, usage, versionCommand } from './cli.ts'
7
+ export { actCommand, addCommand, backupCommand, cave, checkCommand, commandHelp, demoCommand, deriveCommand, exportCommand, generateCommand, helpCommand, highlightCommand, importCommand, parseCommand, queryCommand, reconstructCommand, reportCommand, resolveCommand, restoreCommand, searchCommand, suggestAliasCommand, syncCommand, usage, versionCommand } from './cli.ts'
8
8
  export { dispatch, runCli } from './dispatch.ts'
9
9
  export type { CommandRuntime } from './dispatch.ts'
10
10
  export { commandRegistry } from './commands.ts'