@gmickel/gno 1.19.0 → 1.21.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 (85) hide show
  1. package/README.md +28 -8
  2. package/assets/skill/SKILL.md +73 -27
  3. package/assets/skill/mcp-reference.md +7 -2
  4. package/assets/skill/recipes/citation-and-provenance.md +32 -9
  5. package/package.json +1 -1
  6. package/spec/cli.md +142 -17
  7. package/spec/db/schema.sql +170 -0
  8. package/spec/evals-agentic.md +87 -5
  9. package/spec/mcp.md +75 -3
  10. package/spec/output-schemas/ask.schema.json +198 -0
  11. package/spec/output-schemas/capsule-reverified-event.schema.json +47 -0
  12. package/spec/output-schemas/changes.schema.json +280 -0
  13. package/spec/output-schemas/claim-verification.schema.json +291 -0
  14. package/spec/output-schemas/context-capsule-v1.schema.json +36 -1
  15. package/spec/output-schemas/document-diff.schema.json +185 -0
  16. package/spec/output-schemas/impact.schema.json +122 -0
  17. package/spec/output-schemas/saved-capsule-list.schema.json +16 -0
  18. package/spec/output-schemas/saved-capsule-registration.schema.json +172 -0
  19. package/spec/output-schemas/saved-capsule-reverification.schema.json +59 -0
  20. package/spec/output-schemas/saved-capsule-unwatch.schema.json +16 -0
  21. package/spec/output-schemas/saved-capsule-watch.schema.json +17 -0
  22. package/src/app/context-runtime-contract.ts +10 -5
  23. package/src/app/context-runtime-input.ts +29 -1
  24. package/src/app/context-runtime-types.ts +4 -0
  25. package/src/app/context-runtime.ts +5 -1
  26. package/src/app/context-surface.ts +4 -0
  27. package/src/app/verified-ask.ts +291 -0
  28. package/src/cli/commands/ask-format.ts +255 -0
  29. package/src/cli/commands/ask.ts +40 -149
  30. package/src/cli/commands/changes.ts +160 -0
  31. package/src/cli/commands/context-saved.ts +189 -0
  32. package/src/cli/options.ts +8 -0
  33. package/src/cli/program.ts +227 -1
  34. package/src/core/capsule-registry.ts +279 -0
  35. package/src/core/capsule-reverification-scheduler.ts +218 -0
  36. package/src/core/capsule-reverification.ts +289 -0
  37. package/src/core/change-diff.ts +182 -0
  38. package/src/core/change-journal.ts +228 -0
  39. package/src/core/context-budget.ts +6 -0
  40. package/src/core/context-capsule-retrieval-schema.ts +4 -0
  41. package/src/core/context-capsule-schema.ts +17 -0
  42. package/src/core/context-capsule-validation.ts +3 -2
  43. package/src/core/context-capsule.ts +18 -0
  44. package/src/core/context-compiler.ts +33 -21
  45. package/src/core/context-evidence.ts +6 -0
  46. package/src/core/knowledge-delta.ts +395 -0
  47. package/src/core/knowledge-impact.ts +202 -0
  48. package/src/core/retrieval-trace-evidence-origin.ts +3 -0
  49. package/src/core/retrieval-trace-session.ts +15 -2
  50. package/src/ingestion/sync.ts +214 -165
  51. package/src/llm/errors.ts +10 -1
  52. package/src/llm/httpGeneration.ts +11 -1
  53. package/src/llm/nodeLlamaCpp/generation.ts +54 -10
  54. package/src/llm/types.ts +6 -0
  55. package/src/mcp/tools/ask.ts +228 -0
  56. package/src/mcp/tools/changes.ts +80 -0
  57. package/src/mcp/tools/context.ts +28 -7
  58. package/src/mcp/tools/index.ts +38 -0
  59. package/src/pipeline/claim-verification-schema.ts +235 -0
  60. package/src/pipeline/claim-verification.ts +487 -0
  61. package/src/pipeline/claim-verifier.ts +474 -0
  62. package/src/pipeline/types.ts +25 -0
  63. package/src/sdk/client.ts +77 -2
  64. package/src/sdk/index.ts +7 -0
  65. package/src/sdk/types.ts +22 -0
  66. package/src/serve/doc-events.ts +12 -1
  67. package/src/serve/public/components/AskVerificationPanel.tsx +189 -0
  68. package/src/serve/public/globals.built.css +1 -1
  69. package/src/serve/public/pages/Ask.tsx +42 -4
  70. package/src/serve/resident-runtime.ts +22 -0
  71. package/src/serve/routes/api.ts +162 -3
  72. package/src/serve/routes/changes.ts +102 -0
  73. package/src/serve/server.ts +34 -0
  74. package/src/serve/watch-service.ts +9 -0
  75. package/src/store/index.ts +21 -0
  76. package/src/store/migrations/015-document-change-journal.ts +85 -0
  77. package/src/store/migrations/016-saved-capsules.ts +131 -0
  78. package/src/store/migrations/017-document-change-retention-counters.ts +33 -0
  79. package/src/store/migrations/018-saved-capsule-registration-epoch.ts +24 -0
  80. package/src/store/migrations/019-saved-capsule-registration-generation.ts +53 -0
  81. package/src/store/migrations/index.ts +10 -0
  82. package/src/store/sqlite/adapter.ts +291 -7
  83. package/src/store/sqlite/capsule-registry-store.ts +534 -0
  84. package/src/store/sqlite/change-journal-store.ts +473 -0
  85. package/src/store/types.ts +262 -0
@@ -14,6 +14,7 @@ import type {
14
14
  } from "../../llm/types";
15
15
  import type { AskOptions, AskResult, Citation } from "../../pipeline/types";
16
16
 
17
+ import { buildVerifiedAsk } from "../../app/verified-ask";
17
18
  import {
18
19
  finishRetrievalTraceAfterError,
19
20
  retrievalTraceFilters,
@@ -39,6 +40,8 @@ import {
39
40
  } from "../progress";
40
41
  import { initStore } from "./shared";
41
42
 
43
+ export { formatAsk } from "./ask-format";
44
+
42
45
  // ─────────────────────────────────────────────────────────────────────────────
43
46
  // Types
44
47
  // ─────────────────────────────────────────────────────────────────────────────
@@ -83,9 +86,11 @@ export async function ask(
83
86
  options: AskCommandOptions = {}
84
87
  ): Promise<AskCommandResult> {
85
88
  const limit = options.limit ?? 5;
89
+ const globals = getGlobals();
86
90
 
87
91
  const initResult = await initStore({
88
92
  configPath: options.configPath,
93
+ indexName: globals.index,
89
94
  collection: options.collection,
90
95
  syncConfig: false,
91
96
  });
@@ -103,7 +108,9 @@ export async function ask(
103
108
  let traceSession: RetrievalTraceSession | undefined;
104
109
 
105
110
  try {
106
- const answerRequested = Boolean(options.answer && !options.noAnswer);
111
+ const verificationRequested = options.verify === true;
112
+ const answerRequested =
113
+ verificationRequested || Boolean(options.answer && !options.noAnswer);
107
114
  const embedUri = resolveModelUri(
108
115
  config,
109
116
  "embed",
@@ -111,7 +118,7 @@ export async function ask(
111
118
  options.collection
112
119
  );
113
120
  const expandUri =
114
- !options.noExpand && !options.queryModes?.length
121
+ !verificationRequested && !options.noExpand && !options.queryModes?.length
115
122
  ? resolveModelUri(
116
123
  config,
117
124
  "expand",
@@ -136,6 +143,7 @@ export async function ask(
136
143
  query,
137
144
  filters: retrievalTraceFilters({ ...options, limit }),
138
145
  pipeline: "ask",
146
+ indexName: globals.index,
139
147
  modelUris: [embedUri, expandUri, answerUri, rerankUri].filter(
140
148
  (value): value is string => Boolean(value)
141
149
  ),
@@ -147,7 +155,6 @@ export async function ask(
147
155
  const llm = new LlmAdapter(config);
148
156
 
149
157
  // Resolve download policy from env/flags
150
- const globals = getGlobals();
151
158
  const policy = resolveDownloadPolicy(process.env, {
152
159
  offline: globals.offline,
153
160
  });
@@ -254,6 +261,34 @@ export async function ask(
254
261
  };
255
262
  }
256
263
 
264
+ if (verificationRequested && answerPort) {
265
+ const verified = await buildVerifiedAsk(
266
+ query,
267
+ { ...options, limit },
268
+ {
269
+ store,
270
+ config,
271
+ indexName: globals.index,
272
+ vectorIndex,
273
+ embedPort,
274
+ rerankPort,
275
+ genPort: answerPort,
276
+ traceSession,
277
+ }
278
+ );
279
+ const finalized = await traceSession?.finish(
280
+ answerTraceTerminalStatus(verified.citations)
281
+ );
282
+ if (finalized && !finalized.ok) {
283
+ return { success: false, error: finalized.error.message };
284
+ }
285
+ return {
286
+ success: true,
287
+ data: verified,
288
+ metadata: traceSession?.metadata(),
289
+ };
290
+ }
291
+
257
292
  // Run hybrid search
258
293
  const searchResult = await searchHybrid(deps, query, {
259
294
  limit,
@@ -267,6 +302,8 @@ export async function ask(
267
302
  tagsAll: options.tagsAll,
268
303
  tagsAny: options.tagsAny,
269
304
  exclude: options.exclude,
305
+ minScore: options.minScore,
306
+ graph: options.graph,
270
307
  queryModes: options.queryModes,
271
308
  noExpand: options.noExpand,
272
309
  noRerank: options.noRerank,
@@ -394,149 +431,3 @@ export async function ask(
394
431
  await store.close();
395
432
  }
396
433
  }
397
-
398
- // ─────────────────────────────────────────────────────────────────────────────
399
- // Formatters
400
- // ─────────────────────────────────────────────────────────────────────────────
401
-
402
- interface FormatOptions {
403
- showSources?: boolean;
404
- }
405
-
406
- // oxlint-disable-next-line max-lines-per-function -- terminal formatting with conditional sections
407
- function formatTerminal(data: AskResult, opts: FormatOptions = {}): string {
408
- const lines: string[] = [];
409
- const hasAnswer = Boolean(data.answer);
410
-
411
- // Show answer if present
412
- if (data.answer) {
413
- lines.push("Answer:");
414
- lines.push(data.answer);
415
- lines.push("");
416
- }
417
-
418
- // Show cited sources (only sources actually referenced in answer)
419
- if (data.citations && data.citations.length > 0) {
420
- lines.push("Cited Sources:");
421
- for (let i = 0; i < data.citations.length; i++) {
422
- const c = data.citations[i];
423
- if (c) {
424
- lines.push(` [${i + 1}] ${c.uri}`);
425
- }
426
- }
427
- lines.push("");
428
- }
429
-
430
- // Show all retrieved sources if:
431
- // - No answer was generated (retrieval-only mode)
432
- // - User explicitly requested with --show-sources
433
- const showAllSources = !hasAnswer || opts.showSources;
434
- if (showAllSources && data.results.length > 0) {
435
- lines.push(hasAnswer ? "All Retrieved Sources:" : "Sources:");
436
- for (const r of data.results) {
437
- lines.push(` [${r.docid}] ${r.uri}`);
438
- if (r.title) {
439
- lines.push(` ${r.title}`);
440
- }
441
- }
442
- } else if (hasAnswer && data.results.length > 0) {
443
- // Hint about --show-sources when we have more sources
444
- const citedCount = data.citations?.length ?? 0;
445
- if (data.results.length > citedCount) {
446
- lines.push(
447
- `(${data.results.length} sources retrieved, use --show-sources to list all)`
448
- );
449
- }
450
- }
451
-
452
- if (!data.answer && data.results.length === 0) {
453
- lines.push("No relevant sources found.");
454
- }
455
-
456
- return lines.join("\n");
457
- }
458
-
459
- function formatMarkdown(data: AskResult, opts: FormatOptions = {}): string {
460
- const lines: string[] = [];
461
- const hasAnswer = Boolean(data.answer);
462
-
463
- lines.push(`# Question: ${data.query}`);
464
- lines.push("");
465
-
466
- if (data.answer) {
467
- lines.push("## Answer");
468
- lines.push("");
469
- lines.push(data.answer);
470
- lines.push("");
471
- }
472
-
473
- // Show cited sources (only sources actually referenced in answer)
474
- if (data.citations && data.citations.length > 0) {
475
- lines.push("## Cited Sources");
476
- lines.push("");
477
- for (let i = 0; i < data.citations.length; i++) {
478
- const c = data.citations[i];
479
- if (c) {
480
- lines.push(`**[${i + 1}]** \`${c.uri}\``);
481
- }
482
- }
483
- lines.push("");
484
- }
485
-
486
- // Show all retrieved sources if no answer or --show-sources
487
- const showAllSources = !hasAnswer || opts.showSources;
488
- if (showAllSources) {
489
- lines.push(hasAnswer ? "## All Retrieved Sources" : "## Sources");
490
- lines.push("");
491
-
492
- for (let i = 0; i < data.results.length; i++) {
493
- const r = data.results[i];
494
- if (!r) {
495
- continue;
496
- }
497
- lines.push(`${i + 1}. **${r.title || r.source.relPath}**`);
498
- lines.push(` - URI: \`${r.uri}\``);
499
- lines.push(` - Score: ${r.score.toFixed(2)}`);
500
- }
501
-
502
- if (data.results.length === 0) {
503
- lines.push("*No relevant sources found.*");
504
- }
505
- }
506
-
507
- lines.push("");
508
- lines.push("---");
509
- lines.push(
510
- `*Mode: ${data.mode} | Expanded: ${data.meta.expanded} | Reranked: ${data.meta.reranked}*`
511
- );
512
-
513
- return lines.join("\n");
514
- }
515
-
516
- /**
517
- * Format ask result for output.
518
- */
519
- export function formatAsk(
520
- result: AskCommandResult,
521
- options: AskCommandOptions
522
- ): string {
523
- if (!result.success) {
524
- return options.json
525
- ? JSON.stringify({
526
- error: { code: "ASK_FAILED", message: result.error },
527
- })
528
- : `Error: ${result.error}`;
529
- }
530
-
531
- const formatOpts: FormatOptions = { showSources: options.showSources };
532
-
533
- if (options.json) {
534
- return JSON.stringify(result.data, null, 2);
535
- }
536
-
537
- if (options.md) {
538
- return formatMarkdown(result.data, formatOpts);
539
- }
540
-
541
- return formatTerminal(result.data, formatOpts);
542
- }
@@ -0,0 +1,160 @@
1
+ /** CLI adapters and readable formatters for knowledge delta read services. */
2
+
3
+ import type {
4
+ KnowledgeChangesResult,
5
+ KnowledgeDeltaServiceResult,
6
+ KnowledgeDiffResult,
7
+ KnowledgeImpactInput,
8
+ KnowledgeImpactResult,
9
+ ListKnowledgeChangesInput,
10
+ } from "../../core/knowledge-delta";
11
+ import type { StorePort } from "../../store/types";
12
+
13
+ import {
14
+ analyzeKnowledgeImpact,
15
+ getKnowledgeDiff,
16
+ listKnowledgeChanges,
17
+ } from "../../core/knowledge-delta";
18
+ import { initStore } from "./shared";
19
+
20
+ export interface KnowledgeDeltaCliContext {
21
+ configPath?: string;
22
+ indexName?: string;
23
+ }
24
+
25
+ export const changesRead = (
26
+ store: StorePort,
27
+ input: ListKnowledgeChangesInput = {}
28
+ ): Promise<KnowledgeDeltaServiceResult<KnowledgeChangesResult>> =>
29
+ listKnowledgeChanges(store, input);
30
+
31
+ export const diffRead = (
32
+ store: StorePort,
33
+ ref: string,
34
+ changeId?: string
35
+ ): Promise<KnowledgeDeltaServiceResult<KnowledgeDiffResult>> =>
36
+ getKnowledgeDiff(store, ref, changeId);
37
+
38
+ export const impactRead = (
39
+ store: StorePort,
40
+ ref: string,
41
+ input: KnowledgeImpactInput = {}
42
+ ): Promise<KnowledgeDeltaServiceResult<KnowledgeImpactResult>> =>
43
+ analyzeKnowledgeImpact(store, ref, input);
44
+
45
+ const withStore = async <T>(
46
+ context: KnowledgeDeltaCliContext,
47
+ operation: (store: StorePort) => Promise<KnowledgeDeltaServiceResult<T>>
48
+ ): Promise<KnowledgeDeltaServiceResult<T>> => {
49
+ const initialized = await initStore({
50
+ configPath: context.configPath,
51
+ indexName: context.indexName,
52
+ syncConfig: false,
53
+ allowEmptyCollections: true,
54
+ });
55
+ if (!initialized.ok) {
56
+ return { success: false, error: initialized.error };
57
+ }
58
+ try {
59
+ return await operation(initialized.store);
60
+ } finally {
61
+ await initialized.store.close();
62
+ }
63
+ };
64
+
65
+ export const changes = (
66
+ input: ListKnowledgeChangesInput,
67
+ context: KnowledgeDeltaCliContext = {}
68
+ ): Promise<KnowledgeDeltaServiceResult<KnowledgeChangesResult>> =>
69
+ withStore(context, (store) => changesRead(store, input));
70
+
71
+ export const diff = (
72
+ ref: string,
73
+ changeId: string | undefined,
74
+ context: KnowledgeDeltaCliContext = {}
75
+ ): Promise<KnowledgeDeltaServiceResult<KnowledgeDiffResult>> =>
76
+ withStore(context, (store) => diffRead(store, ref, changeId));
77
+
78
+ export const impact = (
79
+ ref: string,
80
+ input: KnowledgeImpactInput,
81
+ context: KnowledgeDeltaCliContext = {}
82
+ ): Promise<KnowledgeDeltaServiceResult<KnowledgeImpactResult>> =>
83
+ withStore(context, (store) => impactRead(store, ref, input));
84
+
85
+ const json = (value: unknown): string => JSON.stringify(value, null, 2);
86
+
87
+ export function formatChanges(
88
+ result: KnowledgeChangesResult,
89
+ format: "terminal" | "json"
90
+ ): string {
91
+ if (format === "json") return json(result);
92
+ const lines = [`${result.changes.length} retained changes`];
93
+ for (const change of result.changes) {
94
+ const uri = change.current?.uri ?? change.previous?.uri ?? "(unknown)";
95
+ lines.push(
96
+ `${change.observedAt} ${change.kind.padEnd(10)} ${uri} ${change.id}`
97
+ );
98
+ }
99
+ if (result.page.cursorExpired) {
100
+ lines.push(`Cursor expired; earliest: ${result.page.earliestCursor}`);
101
+ } else if (result.page.nextCursor) {
102
+ lines.push(`Next cursor: ${result.page.nextCursor}`);
103
+ }
104
+ for (const warning of result.warnings) {
105
+ lines.push(`Warning: ${warning}`);
106
+ }
107
+ return lines.join("\n");
108
+ }
109
+
110
+ export function formatDiff(
111
+ result: KnowledgeDiffResult,
112
+ format: "terminal" | "json"
113
+ ): string {
114
+ if (format === "json") return json(result);
115
+ const lines = [
116
+ `Structural diff for ${result.document.uri}`,
117
+ `Status: ${result.status}`,
118
+ `History: ${result.history.status}${result.history.reason ? ` (${result.history.reason})` : ""}`,
119
+ "Content: not retained (metadata-only journal)",
120
+ ];
121
+ if (result.change) {
122
+ const delta = result.change.structureDelta;
123
+ lines.push(
124
+ `Change: ${result.change.kind} at ${result.change.observedAt} (${result.change.id})`,
125
+ `Headings: +${delta.headings.added.length} -${delta.headings.removed.length}`,
126
+ `Links: +${delta.links.added.length} -${delta.links.removed.length}`,
127
+ `Typed edges: +${delta.typedEdges.added.length} -${delta.typedEdges.removed.length}`,
128
+ `Dates: +${delta.dates.added.length} -${delta.dates.removed.length} ~${delta.dates.changed.length}`
129
+ );
130
+ }
131
+ for (const warning of result.warnings) {
132
+ lines.push(`Warning: ${warning}`);
133
+ }
134
+ return lines.join("\n");
135
+ }
136
+
137
+ export function formatImpact(
138
+ result: KnowledgeImpactResult,
139
+ format: "terminal" | "json"
140
+ ): string {
141
+ if (format === "json") return json(result);
142
+ const lines = [
143
+ `${result.impacted.length} documents depend on ${result.root.uri}`,
144
+ ];
145
+ for (const item of result.impacted) {
146
+ const path = item.evidencePath
147
+ .map(
148
+ (step) => `${step.source.uri} -[${step.edgeType}]-> ${step.target.uri}`
149
+ )
150
+ .join(" -> ");
151
+ lines.push(`depth ${item.depth} ${item.document.uri}`, ` ${path}`);
152
+ }
153
+ if (result.meta.truncated) {
154
+ lines.push("Traversal truncated by configured caps");
155
+ }
156
+ for (const warning of result.meta.warnings) {
157
+ lines.push(`Warning: ${warning}`);
158
+ }
159
+ return lines.join("\n");
160
+ }
@@ -0,0 +1,189 @@
1
+ /** CLI lifecycle for explicitly saved Context Capsules. */
2
+
3
+ import type { Config } from "../../config/types";
4
+ import type { SqliteAdapter } from "../../store/sqlite/adapter";
5
+ import type { SavedCapsuleRegistrationRecord } from "../../store/types";
6
+
7
+ import { DEFAULT_INDEX_NAME } from "../../app/constants";
8
+ import { canonicalizeIndexName } from "../../app/index-name";
9
+ import {
10
+ canonicalSavedCapsuleRegistryJson,
11
+ listSavedCapsules,
12
+ loadSavedCapsuleFile,
13
+ registerSavedCapsule,
14
+ unregisterSavedCapsule,
15
+ } from "../../core/capsule-registry";
16
+ import { reverifySavedCapsuleManually } from "../../core/capsule-reverification";
17
+ import { CliError } from "../errors";
18
+ import { contextCliError } from "./context-build";
19
+ import { initStore } from "./shared";
20
+
21
+ export interface SavedCapsuleCommandOptions {
22
+ configPath?: string;
23
+ indexName?: string;
24
+ format: "terminal" | "json";
25
+ }
26
+
27
+ export interface WatchSavedCapsuleCommandOptions extends SavedCapsuleCommandOptions {
28
+ explicitIndexName?: string;
29
+ question?: string;
30
+ label?: string;
31
+ notify?: boolean;
32
+ }
33
+
34
+ export interface ReverifySavedCapsuleCommandResult {
35
+ output: string;
36
+ operationStatus: "completed" | "failed";
37
+ errorMessage: string | null;
38
+ }
39
+
40
+ const withStore = async <T>(
41
+ options: SavedCapsuleCommandOptions,
42
+ operation: (input: {
43
+ store: SqliteAdapter;
44
+ config: Config;
45
+ indexName: string;
46
+ }) => Promise<T>
47
+ ): Promise<T> => {
48
+ const indexName = canonicalizeIndexName(
49
+ options.indexName ?? DEFAULT_INDEX_NAME
50
+ );
51
+ const initialized = await initStore({
52
+ configPath: options.configPath,
53
+ indexName,
54
+ syncConfig: true,
55
+ allowEmptyCollections: true,
56
+ });
57
+ if (!initialized.ok) throw new CliError("RUNTIME", initialized.error);
58
+ try {
59
+ return await operation({
60
+ store: initialized.store,
61
+ config: initialized.config,
62
+ indexName,
63
+ });
64
+ } finally {
65
+ await initialized.store.close();
66
+ }
67
+ };
68
+
69
+ const formatRegistration = (
70
+ registration: SavedCapsuleRegistrationRecord
71
+ ): string =>
72
+ [
73
+ `${registration.registrationId} ${registration.label ?? registration.capsuleId}`,
74
+ ` file: ${registration.filePath}`,
75
+ ` index: ${registration.indexName}`,
76
+ ` evidence: ${registration.evidence.length}`,
77
+ ` notify: ${registration.notificationPreference}`,
78
+ ` state: ${registration.verification?.affectedQuestionState ?? "not_verified"}`,
79
+ ].join("\n");
80
+
81
+ export const watchSavedCapsule = async (
82
+ filePath: string,
83
+ options: WatchSavedCapsuleCommandOptions
84
+ ): Promise<string> => {
85
+ try {
86
+ const loaded = await loadSavedCapsuleFile(filePath);
87
+ const explicit = options.explicitIndexName
88
+ ? canonicalizeIndexName(options.explicitIndexName)
89
+ : undefined;
90
+ if (explicit && explicit !== loaded.capsule.scope.indexName) {
91
+ throw Object.assign(
92
+ new Error(
93
+ `Context Capsule index ${loaded.capsule.scope.indexName} does not match --index ${explicit}`
94
+ ),
95
+ { code: "invalid_filter" }
96
+ );
97
+ }
98
+ return await withStore(
99
+ { ...options, indexName: loaded.capsule.scope.indexName },
100
+ async ({ store, indexName }) => {
101
+ const registration = await registerSavedCapsule(store, indexName, {
102
+ filePath,
103
+ question: options.question,
104
+ label: options.label,
105
+ notificationPreference: options.notify ? "local" : "none",
106
+ });
107
+ return options.format === "json"
108
+ ? canonicalSavedCapsuleRegistryJson(registration)
109
+ : formatRegistration(registration);
110
+ }
111
+ );
112
+ } catch (error) {
113
+ throw contextCliError(error);
114
+ }
115
+ };
116
+
117
+ export const listWatchedCapsules = async (
118
+ options: SavedCapsuleCommandOptions
119
+ ): Promise<string> =>
120
+ withStore(options, async ({ store }) => {
121
+ const registrations = await listSavedCapsules(store);
122
+ if (options.format === "json") {
123
+ return canonicalSavedCapsuleRegistryJson({
124
+ schemaVersion: "1.0",
125
+ registrations,
126
+ });
127
+ }
128
+ return registrations.length === 0
129
+ ? "No saved Context Capsules are watched."
130
+ : registrations.map(formatRegistration).join("\n\n");
131
+ }).catch((error) => {
132
+ throw contextCliError(error);
133
+ });
134
+
135
+ export const unwatchSavedCapsule = async (
136
+ registrationId: string,
137
+ options: SavedCapsuleCommandOptions
138
+ ): Promise<string> =>
139
+ withStore(options, async ({ store }) => {
140
+ await unregisterSavedCapsule(store, registrationId);
141
+ return options.format === "json"
142
+ ? canonicalSavedCapsuleRegistryJson({
143
+ schemaVersion: "1.0",
144
+ registrationId,
145
+ removed: true,
146
+ })
147
+ : `Stopped watching ${registrationId}.`;
148
+ }).catch((error) => {
149
+ throw contextCliError(error);
150
+ });
151
+
152
+ export const reverifyWatchedCapsule = async (
153
+ registrationId: string,
154
+ options: SavedCapsuleCommandOptions
155
+ ): Promise<ReverifySavedCapsuleCommandResult> =>
156
+ withStore(options, async ({ store, config, indexName }) => {
157
+ const outcome = await reverifySavedCapsuleManually(registrationId, {
158
+ store,
159
+ config,
160
+ indexName,
161
+ });
162
+ const errorMessage =
163
+ outcome.verification.operationStatus === "failed"
164
+ ? `${outcome.verification.errorCode ?? "verification_failed"}: ${
165
+ outcome.verification.errorMessage ??
166
+ "Saved Context Capsule verification failed"
167
+ }`
168
+ : null;
169
+ const output =
170
+ options.format === "json"
171
+ ? canonicalSavedCapsuleRegistryJson({
172
+ schemaVersion: "1.0",
173
+ registration: outcome.registration,
174
+ verification: outcome.verification,
175
+ receipt: outcome.receipt,
176
+ })
177
+ : [
178
+ formatRegistration(outcome.registration),
179
+ ` operation: ${outcome.verification.operationStatus}`,
180
+ ...(errorMessage ? [` error: ${errorMessage}`] : []),
181
+ ].join("\n");
182
+ return {
183
+ output,
184
+ operationStatus: outcome.verification.operationStatus,
185
+ errorMessage,
186
+ };
187
+ }).catch((error) => {
188
+ throw contextCliError(error);
189
+ });
@@ -35,6 +35,7 @@ export const CMD = {
35
35
  contextCheck: "context.check",
36
36
  contextBuild: "context.build",
37
37
  contextVerify: "context.verify",
38
+ contextSaved: "context.saved",
38
39
  modelsList: "models.list",
39
40
  tagsList: "tags.list",
40
41
  linksList: "links.list",
@@ -42,6 +43,9 @@ export const CMD = {
42
43
  similar: "similar",
43
44
  graph: "graph",
44
45
  graphQuery: "graph.query",
46
+ changes: "changes",
47
+ diff: "diff",
48
+ impact: "impact",
45
49
  capture: "capture",
46
50
  } as const;
47
51
 
@@ -64,6 +68,7 @@ const FORMAT_SUPPORT: Record<CommandId, OutputFormat[]> = {
64
68
  [CMD.contextCheck]: ["terminal", "json", "md"],
65
69
  [CMD.contextBuild]: ["terminal", "json", "md"],
66
70
  [CMD.contextVerify]: ["terminal", "json", "md"],
71
+ [CMD.contextSaved]: ["terminal", "json"],
67
72
  [CMD.modelsList]: ["terminal", "json"],
68
73
  [CMD.tagsList]: ["terminal", "json", "md"],
69
74
  [CMD.linksList]: ["terminal", "json", "md"],
@@ -72,6 +77,9 @@ const FORMAT_SUPPORT: Record<CommandId, OutputFormat[]> = {
72
77
  // graph uses custom --dot/--mermaid flags (not OutputFormat) and writes via terminal output
73
78
  [CMD.graph]: ["json", "terminal"],
74
79
  [CMD.graphQuery]: ["terminal", "json"],
80
+ [CMD.changes]: ["terminal", "json"],
81
+ [CMD.diff]: ["terminal", "json"],
82
+ [CMD.impact]: ["terminal", "json"],
75
83
  [CMD.capture]: ["terminal", "json"],
76
84
  };
77
85