@cavelang/cli 0.15.0 → 0.20.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.
package/src/cli.ts CHANGED
@@ -8,11 +8,14 @@
8
8
  * - `cave parse [file]` — lint / dump the AST (`--json`)
9
9
  * - `cave highlight [file]` — ANSI syntax colors (async, routed in `main.ts`)
10
10
  * - `cave add [--db <path>] [file…]` — ingest into a store (`--strict`, `--check`)
11
- * - `cave query [--db <path>] '<pattern>'` — CAVE-Q (`--json`, `--all`, `--aliases`, `--as-of`)
11
+ * - `cave query [--db <path>] '<pattern>'` — CAVE-Q (`--json`, `--all`, `--aliases`, `--as-of`, `--resolve`)
12
+ * - `cave resolve [--db <path>]` — contested facts under the §26 policy (`--aliases`, `--policy`, `--json`)
12
13
  * - `cave derive [--db <path>] [rules.cave…]` — fire rules (`--dry-run`, `--full`, `--list`, `--retract`)
13
14
  * - `cave act [--db <path>] <name> [param=value…]` — execute an action (spec §25; `--declare`, `--list`, `--retract`)
14
15
  * - `cave check [--db <path>]` — knowledge health report (`--stale`, `--json`)
15
- * - `cave export [--db <path>]` — canonical text out (`--current`)
16
+ * - `cave suggest-alias [--db <path>]` — same-entity candidates (spec §27; async, routed in `main.ts`)
17
+ * - `cave sync [--db <path>] <source>` — merge another store by row identity (spec §28; `--dry-run`, `--as`, `--into`)
18
+ * - `cave export [--db <path>]` — canonical text out (`--current`, `--tx`)
16
19
  * - `cave connect [--db <path>] <source>` — deterministic structured ingestion (async, routed in `main.ts`)
17
20
  * - `cave eval <suite...>` — golden-fixture extraction/query/reconstruction evals (async, routed in `main.ts`)
18
21
  * - `cave reconstruct [--db <path>] <seed...>` — §18 reconstruction from seed cues (async, routed in `main.ts`)
@@ -31,7 +34,10 @@ import { parseDocument } from '@cavelang/parser'
31
34
  import { Registry, standardRegistry } from '@cavelang/canonical'
32
35
  import { defaultDbPath, open } from '@cavelang/store'
33
36
  import { query as caveQuery } from '@cavelang/query'
34
- import { check as caveCheck, defaultStaleDays, gatedIngest } from '@cavelang/shape'
37
+ import {
38
+ check as caveCheck, defaultMinScore, defaultStaleDays, gatedIngest, judgePrompt, parseJudgeReply,
39
+ suggestAliases, writeSuggestions
40
+ } from '@cavelang/shape'
35
41
  import type { Report, Violation } from '@cavelang/shape'
36
42
  import { declareRules, defaultMaxPasses, defaultMinConf, derive, listRules, retractRule } from '@cavelang/rules'
37
43
  import type { Declaration, DeriveReport } from '@cavelang/rules'
@@ -40,7 +46,9 @@ import type { ActReport } from '@cavelang/act'
40
46
  import {
41
47
  Demo, heuristicPolicy, llmPolicy, reconstruct, reconstructAsync, shellComplete, sqliteStore
42
48
  } from '@cavelang/loop'
43
- import { emitClaim } from '@cavelang/canonical'
49
+ import { labelOf, sanitizeLabel, syncFile, syncText } from '@cavelang/sync'
50
+ import type { SyncReport } from '@cavelang/sync'
51
+ import { emitClaim, txOfLine } from '@cavelang/canonical'
44
52
 
45
53
  export type Output = {
46
54
  readonly code: number
@@ -61,12 +69,15 @@ Usage:
61
69
  cave highlight [file...] print CAVE text with ANSI syntax colors
62
70
  cave add [--db <path>] [file...] ingest into a store [--strict] [--check] [--no-prelude] [--no-src]
63
71
  cave import [--db <path>] [file...] restore/merge from CAVE text (add without @src: stamping)
64
- cave query [--db <path>] <pattern> run a CAVE-Q pattern [--json] [--all] [--aliases] [--as-of <t>] [--no-prelude]
72
+ cave query [--db <path>] <pattern> run a CAVE-Q pattern [--json] [--all] [--aliases] [--as-of <t>] [--resolve] [--no-prelude]
73
+ cave resolve [--db <path>] contested facts + winners (spec §26) [--aliases] [--policy] [--json]
65
74
  cave derive [--db <path>] [rules.cave..] declare + fire rules (spec §24) [--dry-run] [--full] [--list] [--retract <rule>]
66
75
  cave act [--db <path>] <name> [p=v...] execute an action (spec §25) [--dry-run] [--no-check] [--hooks <file>]
67
76
  cave act --declare [file...] declare actions from a CAVE document; --list / --retract <name> manage them
68
77
  cave check [--db <path>] knowledge health report (spec §20) [--stale <days>] [--json]
69
- cave export [--db <path>] [--out <file>] emit canonical CAVE text [--current] [--no-prelude]
78
+ cave suggest-alias [--db <path>] propose same-entity ALIAS candidates (spec §27) [--min <s>] [--agent] [--write]
79
+ cave sync [--db <path>] <source> merge another store by row identity (spec §28) [--dry-run] [--as] [--into]
80
+ cave export [--db <path>] [--out <file>] emit canonical CAVE text [--current] [--tx] [--no-prelude]
70
81
  cave mcp [--db <path>] serve the engine as an MCP server on stdio [--no-prelude]
71
82
  cave ingest [--db <path>] <globs/urls..> LLM-driven ingestion of files and web pages
72
83
  cave eval <suite..> --agent '<command>' golden-fixture extraction/query/reconstruction evals (items 9, 10)
@@ -153,7 +164,10 @@ Canonical CAVE text is the interchange format (spec §2.2): importing a
153
164
  file written by \`cave export\` replays its claims append-only, preserving
154
165
  the belief series, qualifier edges and in-band declarations. Unlike
155
166
  \`cave add\`, import never stamps actor provenance — replayed claims must
156
- keep the claim keys they were exported with (spec §9.5).
167
+ keep the claim keys they were exported with (spec §9.5). Import always
168
+ mints fresh transaction ids — ;@ annotations in a \`cave export --tx\`
169
+ file read as ordinary comments; to merge under the recorded row
170
+ identity, use \`cave sync\` (spec §28.4).
157
171
 
158
172
  Examples:
159
173
  cave export --db old.db --out backup.cave
@@ -162,7 +176,7 @@ Examples:
162
176
  query: `cave query — run a CAVE-Q pattern against a store
163
177
 
164
178
  Usage:
165
- cave query [--db <path>] <pattern> [WHERE <filter>] [--json] [--all] [--aliases] [--as-of <t>] [--no-prelude]
179
+ cave query [--db <path>] <pattern> [WHERE <filter>] [--json] [--all] [--aliases] [--as-of <t>] [--resolve] [--no-prelude]
166
180
 
167
181
  Options:
168
182
  ${dbHelp}
@@ -172,6 +186,11 @@ Options:
172
186
  --as-of <t> resolve beliefs as of a past moment (spec §12.3): a date
173
187
  (whole day included), a timestamp (whole second), or a
174
188
  transaction id — rows recorded later are invisible
189
+ --resolve match resolved winners only (spec §26): contested facts —
190
+ same fact from several sources, or opposite polarity —
191
+ collapse to the row the resolution policy picks
192
+ (precedence class, reliability-weighted confidence, tx);
193
+ incompatible with --all
175
194
  --no-prelude open the store without the standard verb registry
176
195
 
177
196
  Patterns are claim triples with ?variables and optional metadata filters
@@ -185,8 +204,40 @@ Examples:
185
204
  cave query --db k.db 'terrier EXTENDS+ animal'
186
205
  cave query --db k.db '?x USES postgres' --aliases
187
206
  cave query --db k.db 'server IS compromised' --as-of 2026-01-15
207
+ cave query --db k.db 'service HAS owner: ?who' --resolve
188
208
  cave query --db k.db '?x ?verb ?y @production' --json`,
189
209
 
210
+ resolve: `cave resolve — contested facts and their winners (spec §26)
211
+
212
+ Usage:
213
+ cave resolve [--db <path>] [--aliases] [--policy] [--json] [--no-prelude]
214
+
215
+ Options:
216
+ ${dbHelp}
217
+ --aliases widen resolution groups through current ALIAS claims
218
+ (spec §13.6) — aliased names contest one fact
219
+ --policy print the effective resolution policy instead: the
220
+ built-in precedence ladder merged with in-band
221
+ source/<name> HAS precedence: / HAS reliability: claims
222
+ --json emit the report as JSON
223
+ --no-prelude open the store without the standard verb registry
224
+
225
+ Lists every fact more than one belief series currently speaks about —
226
+ same fact from different sources (§9.5 actor stamps, content sources)
227
+ or opposite polarity — with the candidates ranked as the resolution
228
+ policy sees them: precedence class first, reliability-weighted
229
+ confidence second, latest transaction last. The winner is what
230
+ \`cave query --resolve\` matches; the rest are invisible there. Declare
231
+ policy overrides in-band, e.g.:
232
+
233
+ source/scanner-a HAS reliability: 60%
234
+ source/ingest HAS precedence: 1
235
+
236
+ Examples:
237
+ cave resolve --db k.db
238
+ cave resolve --db k.db --aliases
239
+ cave resolve --db k.db --policy`,
240
+
190
241
  derive: `cave derive — declare and fire rules (spec §24)
191
242
 
192
243
  Usage:
@@ -296,21 +347,115 @@ Examples:
296
347
  cave check --db k.db --stale 30
297
348
  cave check --db k.db --json | jq '.violations'`,
298
349
 
350
+ 'suggest-alias': `cave suggest-alias — propose same-entity ALIAS candidates for review (spec §27)
351
+
352
+ Usage:
353
+ cave suggest-alias [--db <path>] [options]
354
+
355
+ Options:
356
+ ${dbHelp}
357
+ --min <s> minimum evidence score, 0..1 or N% (default 60%)
358
+ --limit <n> at most n suggestions, strongest first
359
+ --agent <template> LLM judge filtering the candidates — the cave
360
+ ingest/eval shell contract: the prompt (each pair
361
+ with both sides' current claims) is piped to stdin
362
+ and {prompt-file} is substituted; stdout replies
363
+ with a JSON array of confirmed suggestion numbers
364
+ --timeout <seconds> judge timeout (default 120)
365
+ --write append the suggestions, stamped @src:suggest/alias,
366
+ instead of only printing them
367
+ --json print suggestions with scores and signals as JSON
368
+ --no-prelude open the store without the standard verb registry
369
+
370
+ Candidates come from string similarity (case/separator drift, segment
371
+ containment, prefixes, typos) and shared rare attribute values; shared
372
+ relations strengthen a candidate but never create one. Suggested claims
373
+ are questions, not merges: confidence 30–50% puts them in cave check's
374
+ review band, and a pair with any recorded ALIAS history — merged,
375
+ rejected or unmerged — is never suggested again.
376
+
377
+ Output is CAVE text: review it, edit it, pipe it into cave add — or
378
+ --write it and review afterwards. A written suggestion is a positive
379
+ claim, so the opt-in §13.6 closure links it until reviewed:
380
+
381
+ confirm echo 'dupe ALIAS canonical' | cave add --db k.db
382
+ reject echo 'dupe ALIAS canonical @src:suggest/alias @ 0%' | cave add --db k.db
383
+
384
+ (rejection retracts the suggestion's own series — contexts are part of
385
+ claim identity, §9.2, so a plain @ 0% append would start a new series
386
+ and leave the suggested link standing).
387
+
388
+ Examples:
389
+ cave suggest-alias --db k.db
390
+ cave suggest-alias --db k.db --min 80% --limit 10
391
+ cave suggest-alias --db k.db | cave add --db k.db
392
+ cave suggest-alias --db k.db --agent 'claude -p' --write`,
393
+
394
+ sync: `cave sync — merge another store into this one by row identity (spec §28)
395
+
396
+ Usage:
397
+ cave sync [--db <path>] <source> [--as <label>] [--into <label>]
398
+ [--dry-run] [--no-record] [--json] [--no-prelude]
399
+
400
+ Options:
401
+ ${dbHelp}
402
+ --as <label> origin label in the merge record (default: the source
403
+ file's basename stem; stdin defaults to "stdin")
404
+ --into <label> target label in the merge record (default: the database
405
+ file's basename stem)
406
+ --dry-run compute the full report inside a rolled-back transaction
407
+ --no-record do not append the store/<from> SYNCED-INTO store/<into>
408
+ merge record
409
+ --json emit the report as JSON
410
+ --no-prelude open the store without the standard verb registry
411
+
412
+ The source is a CAVE store file (recognized by the SQLite header),
413
+ canonical text with ;@ transaction annotations (cave export --tx), or -
414
+ for annotated text on stdin. Rows merge by identity: a row's UUIDv7 is
415
+ its id everywhere, so present rows skip, re-runs merge nothing, and two
416
+ stores syncing each other converge. Contradictions coexist (spec §9.4)
417
+ and resolve at read time (cave resolve, --resolve); everything appended
418
+ after a merge outsorts everything merged, whatever the origin machine's
419
+ clock read (spec §28.2). An effective merge appends one record claim,
420
+ stamped @src:sync — the sync log is its belief series.
421
+
422
+ Branching (spec §28.6): commit the full annotated export — the text is
423
+ the store — and rebuild working stores from it. A checkout is plumbing
424
+ (--no-record); landing reviewed text into a live store is a real merge
425
+ event (let it record). Never hand-merge exports: sync both sides into
426
+ a fresh store and re-export — the union — which git can run for you as
427
+ a merge driver on *.cave files.
428
+
429
+ Examples:
430
+ cave sync --db main.db laptop.db
431
+ cave sync --db main.db laptop.db --dry-run --json
432
+ cave export --db laptop.db --tx | cave sync --db main.db -
433
+ cave sync --db a.db b.db && cave sync --db b.db a.db # converge both
434
+ cave sync --db work.db knowledge.cave --no-record # checkout (§28.6)
435
+ cave sync --db main.db knowledge.cave --as reorg-auth # land the branch`,
436
+
299
437
  export: `cave export — emit canonical CAVE text from a store
300
438
 
301
439
  Usage:
302
- cave export [--db <path>] [--out <file>] [--current] [--no-prelude]
440
+ cave export [--db <path>] [--out <file>] [--current] [--tx] [--no-prelude]
303
441
 
304
442
  Options:
305
443
  ${dbHelp}
306
444
  --out <file> write to a file instead of stdout
307
445
  --current current beliefs only (skip superseded rows)
446
+ --tx precede every claim line with its ;@ transaction
447
+ annotation (spec §28.4) — the text carries row identity,
448
+ so cave sync replays it idempotently; other readers see
449
+ ordinary comments
308
450
  --no-prelude open the store without the standard verb registry
309
451
 
310
452
  Examples:
311
453
  cave export --db k.db
312
454
  cave export --db k.db --out backup.cave
313
- cave export --db k.db --current --out snapshot.cave`,
455
+ cave export --db k.db --current --out snapshot.cave
456
+ cave export --db k.db --tx | cave sync --db other.db -
457
+ cave export --db work.db --tx --out knowledge.cave # the committed,
458
+ # reviewable store text (spec §28.6)`,
314
459
 
315
460
  reconstruct: `cave reconstruct — active memory reconstruction from seed cues (spec §18)
316
461
 
@@ -479,6 +624,7 @@ export const queryCommand = (argv: readonly string[]): Output => {
479
624
  all: { type: 'boolean' },
480
625
  aliases: { type: 'boolean' },
481
626
  'as-of': { type: 'string' },
627
+ resolve: { type: 'boolean' },
482
628
  'no-prelude': { type: 'boolean' }
483
629
  },
484
630
  allowPositionals: true
@@ -492,6 +638,7 @@ export const queryCommand = (argv: readonly string[]): Output => {
492
638
  const matches = caveQuery(store, pattern, {
493
639
  all: values.all === true,
494
640
  aliases: values.aliases === true,
641
+ resolve: values.resolve === true,
495
642
  ...values['as-of'] === undefined ? {} : { asOf: values['as-of'] }
496
643
  })
497
644
  if (values.json === true) {
@@ -516,6 +663,60 @@ export const queryCommand = (argv: readonly string[]): Output => {
516
663
  }
517
664
  }
518
665
 
666
+ /**
667
+ * `cave resolve` — the human view of contradiction resolution (spec §26.4):
668
+ * contested facts with their candidates ranked as the policy sees them,
669
+ * or the effective policy itself under `--policy`.
670
+ */
671
+ export const resolveCommand = (argv: readonly string[]): Output => {
672
+ const { values } = parseArgs({
673
+ args: [...argv],
674
+ options: {
675
+ db: { type: 'string' },
676
+ aliases: { type: 'boolean' },
677
+ policy: { type: 'boolean' },
678
+ json: { type: 'boolean' },
679
+ 'no-prelude': { type: 'boolean' }
680
+ },
681
+ allowPositionals: false
682
+ })
683
+ const percent = (value: number): string => `${Math.round(value * 1000) / 10}%`
684
+ const store = open(values.db ?? defaultDbPath(), values['no-prelude'] === true ? { registry: Registry.empty } : {})
685
+ try {
686
+ if (values.policy === true) {
687
+ const policy = store.resolutionPolicy()
688
+ if (values.json === true) {
689
+ return ok(`${JSON.stringify(policy, undefined, 2)}\n`)
690
+ }
691
+ const width = Math.max(...policy.map(entry => entry.prefix.length + 7))
692
+ const lines = policy.map(entry => {
693
+ const subject = entry.prefix === '' ? 'source' : `source/${entry.prefix}`
694
+ const dimensions = [
695
+ ...entry.precedence === undefined ? [] : [`precedence ${entry.precedence}`],
696
+ ...entry.reliability === undefined ? [] : [`reliability ${percent(entry.reliability)}`]
697
+ ]
698
+ return `${subject.padEnd(width)} ${dimensions.join(', ')}`
699
+ })
700
+ return ok(`${lines.join('\n')}\n`)
701
+ }
702
+ const contested = store.contested({ aliases: values.aliases === true })
703
+ if (values.json === true) {
704
+ return ok(`${JSON.stringify(contested, undefined, 2)}\n`)
705
+ }
706
+ if (contested.length === 0) {
707
+ return ok('no contested facts\n')
708
+ }
709
+ const lines = contested.flatMap(group =>
710
+ group.rows.map((row, at) =>
711
+ `${at === 0 ? '' : ' over '}${row.raw_line} ; class ${row.res_class}, effective ${percent(row.res_conf)}`))
712
+ return ok(`${lines.join('\n')}\n`)
713
+ } catch (error) {
714
+ return fail(`${error instanceof Error ? error.message : String(error)}\n`)
715
+ } finally {
716
+ store.close()
717
+ }
718
+ }
719
+
519
720
  export const deriveCommand = (argv: readonly string[]): Output => {
520
721
  const { values, positionals } = parseArgs({
521
722
  args: [...argv],
@@ -831,12 +1032,141 @@ export const checkCommand = (argv: readonly string[]): Output => {
831
1032
  }
832
1033
  }
833
1034
 
1035
+ /**
1036
+ * `cave suggest-alias` — alias discovery (spec §27): same-entity
1037
+ * candidates as suggested `ALIAS` claims for review. Async (the optional
1038
+ * judge runs a shell agent), so `main.ts` routes it separately, like
1039
+ * `reconstruct`.
1040
+ */
1041
+ export const suggestAliasCommand = async (argv: readonly string[]): Promise<Output> => {
1042
+ try {
1043
+ const { values } = parseArgs({
1044
+ args: [...argv],
1045
+ options: {
1046
+ db: { type: 'string' },
1047
+ min: { type: 'string' },
1048
+ limit: { type: 'string' },
1049
+ agent: { type: 'string' },
1050
+ timeout: { type: 'string' },
1051
+ write: { type: 'boolean' },
1052
+ json: { type: 'boolean' },
1053
+ 'no-prelude': { type: 'boolean' },
1054
+ help: { type: 'boolean', short: 'h' }
1055
+ },
1056
+ allowPositionals: false
1057
+ })
1058
+ if (values.help === true) {
1059
+ return ok(`${commandHelp['suggest-alias']}\n`)
1060
+ }
1061
+ const minScore = values.min === undefined ?
1062
+ defaultMinScore :
1063
+ values.min.endsWith('%') ? Number(values.min.slice(0, -1)) / 100 : Number(values.min)
1064
+ if (!Number.isFinite(minScore) || minScore < 0 || minScore > 1) {
1065
+ return fail(`cave suggest-alias: --min expects a score in 0..1 or N%, got ${JSON.stringify(values.min)}\n`)
1066
+ }
1067
+ const limit = values.limit === undefined ? undefined : Number(values.limit)
1068
+ if (limit !== undefined && (!Number.isInteger(limit) || limit < 1)) {
1069
+ return fail(`cave suggest-alias: --limit expects a positive integer, got ${JSON.stringify(values.limit)}\n`)
1070
+ }
1071
+ const timeoutSeconds = values.timeout === undefined ? undefined : Number(values.timeout)
1072
+ if (timeoutSeconds !== undefined && (!Number.isFinite(timeoutSeconds) || timeoutSeconds <= 0)) {
1073
+ return fail(`cave suggest-alias: --timeout must be a positive number of seconds, got '${values.timeout}'\n`)
1074
+ }
1075
+ const store = open(values.db ?? defaultDbPath(), values['no-prelude'] === true ? { registry: Registry.empty } : {})
1076
+ try {
1077
+ let suggestions = suggestAliases(store, { minScore, ...limit === undefined ? {} : { limit } })
1078
+ if (values.agent !== undefined && suggestions.length > 0) {
1079
+ const reply = await shellComplete(values.agent, timeoutSeconds === undefined ? {} : { timeoutSeconds })(
1080
+ judgePrompt(store, suggestions)
1081
+ )
1082
+ suggestions = parseJudgeReply(reply, suggestions.length).map(index => suggestions[index]!)
1083
+ }
1084
+ if (values.json === true) {
1085
+ return ok(`${JSON.stringify(suggestions, undefined, 2)}\n`)
1086
+ }
1087
+ if (suggestions.length === 0) {
1088
+ return ok('no alias suggestions\n')
1089
+ }
1090
+ const lines = suggestions.map(suggestion => suggestion.line)
1091
+ if (values.write === true) {
1092
+ const { appended } = writeSuggestions(store, suggestions)
1093
+ return ok(`${lines.join('\n')}\nappended ${appended} suggested alias claim(s)\n`)
1094
+ }
1095
+ return ok(`${lines.join('\n')}\n`)
1096
+ } finally {
1097
+ store.close()
1098
+ }
1099
+ } catch (error) {
1100
+ return fail(`${error instanceof Error ? error.message : String(error)}\n`)
1101
+ }
1102
+ }
1103
+
1104
+ /**
1105
+ * `cave sync` — store merge (spec §28): another store's rows arrive by
1106
+ * identity — a store file merges through SQL, `;@`-annotated canonical
1107
+ * text (`cave export --tx`, or `-` for stdin) replays under its recorded
1108
+ * ids. Idempotent: re-runs merge nothing and append no record.
1109
+ */
1110
+ export const syncCommand = (argv: readonly string[]): Output => {
1111
+ const { values, positionals } = parseArgs({
1112
+ args: [...argv],
1113
+ options: {
1114
+ db: { type: 'string' },
1115
+ as: { type: 'string' },
1116
+ into: { type: 'string' },
1117
+ 'dry-run': { type: 'boolean' },
1118
+ 'no-record': { type: 'boolean' },
1119
+ json: { type: 'boolean' },
1120
+ 'no-prelude': { type: 'boolean' }
1121
+ },
1122
+ allowPositionals: true
1123
+ })
1124
+ const [source, ...extra] = positionals
1125
+ if (source === undefined || extra.length > 0) {
1126
+ return fail('cave sync: exactly one source is required — a CAVE store file, ;@-annotated text, or - for stdin (spec §28.5)\n')
1127
+ }
1128
+ const dbPath = values.db ?? defaultDbPath()
1129
+ const store = open(dbPath, values['no-prelude'] === true ? { registry: Registry.empty } : {})
1130
+ try {
1131
+ const options = {
1132
+ into: values.into === undefined ? labelOf(dbPath) : sanitizeLabel(values.into),
1133
+ record: values['no-record'] !== true,
1134
+ dryRun: values['dry-run'] === true,
1135
+ ...values.as === undefined ? {} : { from: sanitizeLabel(values.as) }
1136
+ }
1137
+ const report: SyncReport = source === '-' ?
1138
+ syncText(store, readFileSync(0, 'utf8'), { from: 'stdin', ...options }) :
1139
+ syncFile(store, source, options)
1140
+ if (values.json === true) {
1141
+ return { code: report.problems.length > 0 ? 1 : 0, out: `${JSON.stringify(report, undefined, 2)}\n`, err: '' }
1142
+ }
1143
+ if (report.problems.length > 0) {
1144
+ const detail = report.problems.map(problem => ` line ${problem.line}: ${problem.message}`).join('\n')
1145
+ return fail(`cave sync: ${source}: ${report.problems.length} problem(s), nothing merged\n${detail}\n`)
1146
+ }
1147
+ const lines = [
1148
+ `merged ${report.merged} claim(s), ${report.edges} edge(s)` +
1149
+ (report.skipped > 0 ? `, ${report.skipped} already present` : '') +
1150
+ (report.dryRun ? ' (dry run)' : '')
1151
+ ]
1152
+ if (report.record !== undefined) {
1153
+ lines.push(`record: ${report.record}`)
1154
+ }
1155
+ return ok(`${lines.join('\n')}\n`)
1156
+ } catch (error) {
1157
+ return fail(`${error instanceof Error ? error.message : String(error)}\n`)
1158
+ } finally {
1159
+ store.close()
1160
+ }
1161
+ }
1162
+
834
1163
  export const exportCommand = (argv: readonly string[]): Output => {
835
1164
  const { values } = parseArgs({
836
1165
  args: [...argv],
837
1166
  options: {
838
1167
  db: { type: 'string' },
839
1168
  current: { type: 'boolean' },
1169
+ tx: { type: 'boolean' },
840
1170
  out: { type: 'string' },
841
1171
  'no-prelude': { type: 'boolean' }
842
1172
  },
@@ -844,12 +1174,12 @@ export const exportCommand = (argv: readonly string[]): Output => {
844
1174
  })
845
1175
  const store = open(values.db ?? defaultDbPath(), values['no-prelude'] === true ? { registry: Registry.empty } : {})
846
1176
  try {
847
- const text = store.exportText({ current: values.current === true })
1177
+ const text = store.exportText({ current: values.current === true, tx: values.tx === true })
848
1178
  if (values.out === undefined) {
849
1179
  return ok(text)
850
1180
  }
851
1181
  writeFileSync(values.out, text)
852
- const claims = text === '' ? 0 : text.trimEnd().split('\n').length
1182
+ const claims = text === '' ? 0 : text.trimEnd().split('\n').filter(line => txOfLine(line) === undefined).length
853
1183
  return ok(`exported ${claims} claim(s) to ${values.out}\n`)
854
1184
  } finally {
855
1185
  store.close()
@@ -969,12 +1299,16 @@ export const cave = (argv: readonly string[]): Output => {
969
1299
  return importCommand(rest)
970
1300
  case 'query':
971
1301
  return queryCommand(rest)
1302
+ case 'resolve':
1303
+ return resolveCommand(rest)
972
1304
  case 'derive':
973
1305
  return deriveCommand(rest)
974
1306
  case 'act':
975
1307
  return actCommand(rest)
976
1308
  case 'check':
977
1309
  return checkCommand(rest)
1310
+ case 'sync':
1311
+ return syncCommand(rest)
978
1312
  case 'export':
979
1313
  return exportCommand(rest)
980
1314
  case 'demo':
package/src/index.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  /**
2
- * `@cavelang/cli` — the `cave` command (parse / add / query / derive / act / check / export / reconstruct / demo).
2
+ * `@cavelang/cli` — the `cave` command (parse / add / query / resolve / derive / act / check / suggest-alias / sync / export / reconstruct / demo).
3
3
  * Programmatic access to the same implementations lives here.
4
4
  */
5
5
 
6
- export { actCommand, addCommand, cave, checkCommand, commandHelp, demoCommand, deriveCommand, exportCommand, helpCommand, highlightCommand, importCommand, parseCommand, queryCommand, reconstructCommand, usage, versionCommand } from './cli.ts'
6
+ export { actCommand, addCommand, cave, checkCommand, commandHelp, demoCommand, deriveCommand, exportCommand, helpCommand, highlightCommand, importCommand, parseCommand, queryCommand, reconstructCommand, resolveCommand, suggestAliasCommand, syncCommand, usage, versionCommand } from './cli.ts'
7
7
  export type { Output } from './cli.ts'
package/src/main.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  /** `cave` binary entry point — see `cli.ts` for the command implementations. */
3
3
 
4
- import { cave, highlightCommand, reconstructCommand } from './cli.ts'
4
+ import { cave, highlightCommand, reconstructCommand, suggestAliasCommand } from './cli.ts'
5
5
 
6
6
  const write = (code: number, out: string, err: string): void => {
7
7
  if (out !== '') {
@@ -38,6 +38,10 @@ if (argv[0] === 'mcp') {
38
38
  // Async: the LLM policy runs a shell agent once per step.
39
39
  const { code, out, err } = await reconstructCommand(argv.slice(1))
40
40
  write(code, out, err)
41
+ } else if (argv[0] === 'suggest-alias') {
42
+ // Async: the optional judge runs a shell agent.
43
+ const { code, out, err } = await suggestAliasCommand(argv.slice(1))
44
+ write(code, out, err)
41
45
  } else if (argv[0] === 'highlight' && !argv.includes('--help') && !argv.includes('-h')) {
42
46
  // Async (grammar WASM loads on first use); --help stays on the sync path.
43
47
  const { code, out, err } = await highlightCommand(argv.slice(1))