@gmickel/gno 1.40.0 → 1.41.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 (46) hide show
  1. package/README.md +1 -0
  2. package/assets/skill/SKILL.md +17 -0
  3. package/assets/skill/cli-reference.md +48 -0
  4. package/assets/skill/mcp-reference.md +24 -0
  5. package/browser-extension/artifacts/{gno-browser-clipper-v1.40.0.zip → gno-browser-clipper-v1.41.0.zip} +0 -0
  6. package/browser-extension/artifacts/gno-browser-clipper-v1.41.0.zip.sha256 +1 -0
  7. package/browser-extension/dist/manifest.json +1 -1
  8. package/package.json +1 -1
  9. package/spec/cli.md +146 -7
  10. package/spec/db/schema.sql +17 -0
  11. package/spec/mcp.md +194 -0
  12. package/spec/output-schemas/memory-recall.schema.json +159 -0
  13. package/spec/output-schemas/memory-remember.schema.json +164 -0
  14. package/spec/output-schemas/status.schema.json +269 -54
  15. package/src/cli/commands/memory.ts +491 -0
  16. package/src/cli/commands/status.ts +23 -4
  17. package/src/cli/options.ts +4 -0
  18. package/src/cli/program.ts +127 -0
  19. package/src/config/types.ts +7 -0
  20. package/src/core/audit-provenance.ts +91 -0
  21. package/src/core/audit-workspace.ts +17 -0
  22. package/src/core/memory-diagnostics.ts +144 -0
  23. package/src/core/memory-fence.ts +239 -0
  24. package/src/core/memory-recall.ts +269 -0
  25. package/src/core/memory-record.ts +435 -0
  26. package/src/core/memory-remember.ts +425 -0
  27. package/src/core/memory-types.ts +211 -0
  28. package/src/core/memory.ts +87 -0
  29. package/src/ingestion/sync.ts +17 -0
  30. package/src/mcp/http-egress.ts +2 -0
  31. package/src/mcp/tools/index.ts +43 -0
  32. package/src/mcp/tools/memory-recall.ts +122 -0
  33. package/src/mcp/tools/memory-remember.ts +177 -0
  34. package/src/mcp/tools/memory-shared.ts +80 -0
  35. package/src/pipeline/search.ts +2 -0
  36. package/src/pipeline/types.ts +8 -0
  37. package/src/sdk/client.ts +94 -1
  38. package/src/sdk/index.ts +13 -0
  39. package/src/sdk/types.ts +28 -0
  40. package/src/serve/routes/api.ts +167 -0
  41. package/src/serve/server.ts +26 -0
  42. package/src/store/migrations/027-memory-scopes.ts +37 -0
  43. package/src/store/migrations/index.ts +2 -0
  44. package/src/store/sqlite/adapter.ts +127 -3
  45. package/src/store/types.ts +54 -0
  46. package/browser-extension/artifacts/gno-browser-clipper-v1.40.0.zip.sha256 +0 -1
@@ -0,0 +1,491 @@
1
+ /**
2
+ * gno remember / gno recall command implementations.
3
+ *
4
+ * Thin adapters over the transport-neutral memory service: they resolve CLI
5
+ * flags into service inputs, map `MemoryError` codes onto CLI exit codes, and
6
+ * format results. They never touch the store directly and never take the
7
+ * shared write lease (the service owns it).
8
+ *
9
+ * @module src/cli/commands/memory
10
+ */
11
+
12
+ import type { Collection } from "../../config/types";
13
+ import type { EmbeddingPort } from "../../llm/types";
14
+ import type { VectorIndexPort } from "../../store/vector/types";
15
+
16
+ import { getIndexDbPath } from "../../app/constants";
17
+ import {
18
+ type MemoryCandidate,
19
+ MemoryError,
20
+ type MemoryErrorCode,
21
+ type MemoryRecallReceipt,
22
+ MemoryService,
23
+ type RecallResult,
24
+ type RememberInput,
25
+ type RememberResult,
26
+ } from "../../core/memory";
27
+ import { writeLeasePath } from "../../core/write-lease";
28
+ import { LlmAdapter } from "../../llm/nodeLlamaCpp/adapter";
29
+ import { resolveModelUri } from "../../llm/registry";
30
+ import { createVectorIndexPort } from "../../store/vector";
31
+ import { CliError, type CliErrorCode } from "../errors";
32
+ import { initStore } from "./shared";
33
+
34
+ /** Environment overrides for the identity defaults. */
35
+ export const MEMORY_CALLER_ENV = "GNO_MEMORY_CALLER";
36
+ export const MEMORY_SESSION_ENV = "GNO_MEMORY_SESSION";
37
+
38
+ export interface MemoryIdentityCliOptions {
39
+ caller?: string;
40
+ session?: string;
41
+ }
42
+
43
+ export interface MemoryScopeCliOptions {
44
+ configPath?: string;
45
+ indexName?: string;
46
+ collection?: string;
47
+ scopes?: string[];
48
+ }
49
+
50
+ export interface RememberCliOptions
51
+ extends MemoryScopeCliOptions, MemoryIdentityCliOptions {
52
+ text: string;
53
+ /** `--decision add|supersede` (explicit form). */
54
+ decision?: string;
55
+ /** `--add` shorthand for `--decision add`. */
56
+ add?: boolean;
57
+ /** `--supersede <uri>` shorthand for `--decision supersede --predecessor <uri>`. */
58
+ supersede?: string;
59
+ predecessor?: string;
60
+ predecessorHash?: string;
61
+ /** Path to a recall receipt JSON (the recall `--json` output or its `receipt`). */
62
+ receipt?: string;
63
+ derivedFrom?: string[];
64
+ source?: string;
65
+ }
66
+
67
+ export interface RecallCliOptions
68
+ extends MemoryScopeCliOptions, MemoryIdentityCliOptions {
69
+ query: string;
70
+ maxFacts?: number;
71
+ maxTokens?: number;
72
+ }
73
+
74
+ export interface MemoryFormatOptions {
75
+ json?: boolean;
76
+ quiet?: boolean;
77
+ }
78
+
79
+ // ─────────────────────────────────────────────────────────────────────────────
80
+ // Identity + flag resolution
81
+ // ─────────────────────────────────────────────────────────────────────────────
82
+
83
+ /**
84
+ * Caller/session identity, defaulted from process context: env overrides
85
+ * first, then the invoking OS user and the parent process id (the shell or
86
+ * agent that ran `gno`). Flags override both.
87
+ */
88
+ export function resolveMemoryIdentity(
89
+ options: MemoryIdentityCliOptions,
90
+ env: NodeJS.ProcessEnv = process.env
91
+ ): { caller: string; session: string } {
92
+ const caller =
93
+ options.caller?.trim() ||
94
+ env[MEMORY_CALLER_ENV]?.trim() ||
95
+ `cli:${env.USER?.trim() || env.USERNAME?.trim() || "unknown"}`;
96
+ const session =
97
+ options.session?.trim() ||
98
+ env[MEMORY_SESSION_ENV]?.trim() ||
99
+ `ppid:${process.ppid}`;
100
+ return { caller, session };
101
+ }
102
+
103
+ function requireScopeFlag(scopes: string[] | undefined): string[] {
104
+ const cleaned = (scopes ?? []).map((s) => s.trim()).filter(Boolean);
105
+ if (cleaned.length === 0) {
106
+ throw new CliError(
107
+ "VALIDATION",
108
+ "--scope is required (repeatable): memory has no implicit global scope. Example: --scope project:gno"
109
+ );
110
+ }
111
+ return cleaned;
112
+ }
113
+
114
+ function resolveCollectionName(
115
+ collections: readonly Collection[],
116
+ requested: string | undefined
117
+ ): string {
118
+ const name = requested?.trim();
119
+ if (name) return name;
120
+ const managed = collections.filter((c) => c.memoryManaged === true);
121
+ if (managed.length === 1) return managed[0]!.name;
122
+ throw new CliError(
123
+ "VALIDATION",
124
+ managed.length === 0
125
+ ? "--collection is required and must name a memoryManaged collection (none is configured; set memoryManaged: true on a collection in the config)."
126
+ : `--collection is required: ${managed.length} memoryManaged collections are configured (${managed.map((c) => c.name).join(", ")}).`
127
+ );
128
+ }
129
+
130
+ function parsePositiveInt(flag: string, raw: unknown): number | undefined {
131
+ if (raw === undefined || raw === null || raw === "") return undefined;
132
+ const value = typeof raw === "number" ? raw : Number(raw);
133
+ if (!Number.isSafeInteger(value) || value < 1) {
134
+ throw new CliError("VALIDATION", `${flag} must be a positive integer.`, {
135
+ details: { memoryCode: "MEMORY_BUDGET_INVALID" },
136
+ });
137
+ }
138
+ return value;
139
+ }
140
+
141
+ interface ResolvedDecision {
142
+ decision?: "add" | "supersede";
143
+ predecessorUri?: string;
144
+ predecessorHash?: string;
145
+ }
146
+
147
+ function resolveDecision(options: RememberCliOptions): ResolvedDecision {
148
+ const explicit = options.decision?.trim();
149
+ if (explicit && explicit !== "add" && explicit !== "supersede") {
150
+ throw new CliError(
151
+ "VALIDATION",
152
+ "--decision must be add or supersede (omit it to get candidates only)."
153
+ );
154
+ }
155
+ const wantsAdd = Boolean(options.add) || explicit === "add";
156
+ const supersedeUri = options.supersede?.trim() || options.predecessor?.trim();
157
+ const wantsSupersede = Boolean(options.supersede) || explicit === "supersede";
158
+ if (wantsAdd && wantsSupersede) {
159
+ throw new CliError(
160
+ "VALIDATION",
161
+ "--add and --supersede are mutually exclusive; choose one decision."
162
+ );
163
+ }
164
+ if (wantsSupersede) {
165
+ if (!supersedeUri) {
166
+ throw new CliError(
167
+ "VALIDATION",
168
+ "--decision supersede requires --predecessor <gno://uri> (or use --supersede <gno://uri>)."
169
+ );
170
+ }
171
+ const predecessorHash = options.predecessorHash?.trim();
172
+ if (!predecessorHash) {
173
+ throw new CliError(
174
+ "VALIDATION",
175
+ "--supersede requires --predecessor-hash <hash> (the contentHash from recall)."
176
+ );
177
+ }
178
+ return {
179
+ decision: "supersede",
180
+ predecessorUri: supersedeUri,
181
+ predecessorHash,
182
+ };
183
+ }
184
+ if (options.predecessor || options.predecessorHash) {
185
+ throw new CliError(
186
+ "VALIDATION",
187
+ "--predecessor / --predecessor-hash only apply with --supersede or --decision supersede."
188
+ );
189
+ }
190
+ return wantsAdd ? { decision: "add" } : {};
191
+ }
192
+
193
+ async function readReceiptFile(
194
+ path: string | undefined
195
+ ): Promise<MemoryRecallReceipt | undefined> {
196
+ if (!path) return undefined;
197
+ let parsed: unknown;
198
+ try {
199
+ parsed = JSON.parse(await Bun.file(path).text());
200
+ } catch (error) {
201
+ throw new CliError(
202
+ "VALIDATION",
203
+ `--receipt must point to a JSON recall receipt: ${error instanceof Error ? error.message : String(error)}`
204
+ );
205
+ }
206
+ const candidate =
207
+ parsed && typeof parsed === "object" && "receipt" in parsed
208
+ ? (parsed as { receipt: unknown }).receipt
209
+ : parsed;
210
+ const receipt = candidate as Partial<MemoryRecallReceipt> | null;
211
+ if (
212
+ !receipt ||
213
+ typeof receipt !== "object" ||
214
+ !Array.isArray(receipt.spanHashes) ||
215
+ !Array.isArray(receipt.memoryIds) ||
216
+ typeof receipt.digest !== "string"
217
+ ) {
218
+ throw new CliError(
219
+ "VALIDATION",
220
+ "--receipt file is not a recall receipt (expected the recall --json output or its `receipt` object)."
221
+ );
222
+ }
223
+ return receipt as MemoryRecallReceipt;
224
+ }
225
+
226
+ // ─────────────────────────────────────────────────────────────────────────────
227
+ // Error mapping
228
+ // ─────────────────────────────────────────────────────────────────────────────
229
+
230
+ const MEMORY_ERROR_TO_CLI: Record<MemoryErrorCode, CliErrorCode> = {
231
+ MEMORY_TEXT_REQUIRED: "VALIDATION",
232
+ MEMORY_TEXT_TOO_LARGE: "VALIDATION",
233
+ MEMORY_QUERY_REQUIRED: "VALIDATION",
234
+ MEMORY_BUDGET_INVALID: "VALIDATION",
235
+ MEMORY_COLLECTION_REQUIRED: "VALIDATION",
236
+ MEMORY_COLLECTION_NOT_FOUND: "VALIDATION",
237
+ MEMORY_COLLECTION_UNMANAGED: "VALIDATION",
238
+ MEMORY_SCOPES_REQUIRED: "VALIDATION",
239
+ MEMORY_SCOPES_INVALID: "VALIDATION",
240
+ MEMORY_IDENTITY_REQUIRED: "VALIDATION",
241
+ MEMORY_DECISION_INVALID: "VALIDATION",
242
+ MEMORY_PREDECESSOR_REQUIRED: "VALIDATION",
243
+ MEMORY_PREDECESSOR_NOT_FOUND: "VALIDATION",
244
+ MEMORY_PREDECESSOR_HASH_MISMATCH: "VALIDATION",
245
+ MEMORY_FENCED_REPLAY: "VALIDATION",
246
+ MEMORY_FENCED_DERIVED: "VALIDATION",
247
+ // Concurrency outcomes: another writer won. Exit 4 like lease contention.
248
+ MEMORY_SUPERSEDE_CONFLICT: "BUSY",
249
+ MEMORY_WRITE_LEASE_BUSY: "BUSY",
250
+ MEMORY_SYNC_FAILED: "RUNTIME",
251
+ MEMORY_SUPERSEDE_PROJECTION_FAILED: "RUNTIME",
252
+ MEMORY_QUERY_FAILED: "RUNTIME",
253
+ };
254
+
255
+ /** Map a core `MemoryError` onto the CLI error model (code carried in details). */
256
+ export function toCliError(error: unknown): unknown {
257
+ if (!(error instanceof MemoryError)) return error;
258
+ return new CliError(MEMORY_ERROR_TO_CLI[error.code], error.message, {
259
+ details: { memoryCode: error.code },
260
+ });
261
+ }
262
+
263
+ // ─────────────────────────────────────────────────────────────────────────────
264
+ // Service construction
265
+ // ─────────────────────────────────────────────────────────────────────────────
266
+
267
+ interface MemoryRuntime {
268
+ service: MemoryService;
269
+ collectionName: string;
270
+ close: () => Promise<void>;
271
+ }
272
+
273
+ /**
274
+ * Open the store and build a MemoryService. Semantic matching is attached
275
+ * only when the configured embedding model is already cached (memory
276
+ * commands never download models); otherwise the service runs lexical-only
277
+ * and reports why in `matching` / `retrieval`.
278
+ */
279
+ async function openMemoryRuntime(
280
+ options: MemoryScopeCliOptions
281
+ ): Promise<MemoryRuntime> {
282
+ const storeInit = await initStore({
283
+ configPath: options.configPath,
284
+ indexName: options.indexName,
285
+ syncConfig: true,
286
+ });
287
+ if (!storeInit.ok) {
288
+ throw new CliError("VALIDATION", storeInit.error);
289
+ }
290
+ const { store, config, collections } = storeInit;
291
+ let embedPort: EmbeddingPort | null = null;
292
+ let vectorIndex: VectorIndexPort | null = null;
293
+ const close = async (): Promise<void> => {
294
+ await embedPort?.dispose();
295
+ await store.close();
296
+ };
297
+ try {
298
+ const collectionName = resolveCollectionName(
299
+ collections,
300
+ options.collection
301
+ );
302
+ const modelUri = resolveModelUri(
303
+ config,
304
+ "embed",
305
+ undefined,
306
+ collectionName
307
+ );
308
+ const embedResult = await new LlmAdapter(config).createEmbeddingPort(
309
+ modelUri,
310
+ { egressCollections: [collectionName] }
311
+ );
312
+ if (embedResult.ok) {
313
+ const initResult = await embedResult.value.init();
314
+ if (initResult.ok) {
315
+ embedPort = embedResult.value;
316
+ const vectorResult = await createVectorIndexPort(store.getRawDb(), {
317
+ model: modelUri,
318
+ dimensions: embedPort.dimensions(),
319
+ });
320
+ if (vectorResult.ok) vectorIndex = vectorResult.value;
321
+ } else {
322
+ await embedResult.value.dispose();
323
+ }
324
+ }
325
+ const service = new MemoryService({
326
+ store,
327
+ config,
328
+ collections,
329
+ lockPath: writeLeasePath(getIndexDbPath(options.indexName)),
330
+ embedPort,
331
+ vectorIndex,
332
+ });
333
+ return { service, collectionName, close };
334
+ } catch (error) {
335
+ await close();
336
+ throw error;
337
+ }
338
+ }
339
+
340
+ // ─────────────────────────────────────────────────────────────────────────────
341
+ // Commands
342
+ // ─────────────────────────────────────────────────────────────────────────────
343
+
344
+ export async function remember(
345
+ options: RememberCliOptions
346
+ ): Promise<RememberResult> {
347
+ const scopes = requireScopeFlag(options.scopes);
348
+ const identity = resolveMemoryIdentity(options);
349
+ const decision = resolveDecision(options);
350
+ const receipt = await readReceiptFile(options.receipt);
351
+ const runtime = await openMemoryRuntime(options);
352
+ try {
353
+ const input: RememberInput = {
354
+ ...identity,
355
+ text: options.text,
356
+ collection: runtime.collectionName,
357
+ scopes,
358
+ ...decision,
359
+ receipt,
360
+ derivedFrom: options.derivedFrom?.length
361
+ ? options.derivedFrom
362
+ : undefined,
363
+ source: options.source,
364
+ };
365
+ return await runtime.service.remember(input);
366
+ } catch (error) {
367
+ throw toCliError(error);
368
+ } finally {
369
+ await runtime.close();
370
+ }
371
+ }
372
+
373
+ export async function recall(options: RecallCliOptions): Promise<RecallResult> {
374
+ const scopes = requireScopeFlag(options.scopes);
375
+ const identity = resolveMemoryIdentity(options);
376
+ const maxFacts = parsePositiveInt("--max-facts", options.maxFacts);
377
+ const maxTokens = parsePositiveInt("--max-tokens", options.maxTokens);
378
+ const runtime = await openMemoryRuntime(options);
379
+ try {
380
+ return await runtime.service.recall({
381
+ ...identity,
382
+ query: options.query,
383
+ collection: runtime.collectionName,
384
+ scopes,
385
+ maxFacts,
386
+ maxTokens,
387
+ });
388
+ } catch (error) {
389
+ throw toCliError(error);
390
+ } finally {
391
+ await runtime.close();
392
+ }
393
+ }
394
+
395
+ // ─────────────────────────────────────────────────────────────────────────────
396
+ // Formatters
397
+ // ─────────────────────────────────────────────────────────────────────────────
398
+
399
+ function formatMatching(
400
+ matching: RememberResult["matching"] | RecallResult["retrieval"]
401
+ ): string {
402
+ const note = matching.semanticUnavailable
403
+ ? ` (${matching.semanticUnavailable})`
404
+ : "";
405
+ return `${matching.mode}${note}`;
406
+ }
407
+
408
+ function formatCandidate(candidate: MemoryCandidate): string[] {
409
+ return [
410
+ ` [${candidate.match} ${candidate.similarity.toFixed(2)}] ${candidate.uri}`,
411
+ ` hash: ${candidate.contentHash}`,
412
+ ` ${candidate.text}`,
413
+ ];
414
+ }
415
+
416
+ export function formatRememberResult(
417
+ result: RememberResult,
418
+ options: MemoryFormatOptions = {}
419
+ ): string {
420
+ if (options.json) return JSON.stringify(result, null, 2);
421
+ if (options.quiet) {
422
+ return result.outcome === "candidates"
423
+ ? result.candidates.map((c) => c.uri).join("\n")
424
+ : result.record.uri;
425
+ }
426
+ const lines: string[] = [];
427
+ if (result.outcome === "candidates") {
428
+ const count = result.candidates.length;
429
+ lines.push(
430
+ count === 0
431
+ ? "No write: no decision given and no candidates in scope. Re-run with --add to store it."
432
+ : `No write: ${count} candidate${count === 1 ? "" : "s"} in scope. Decide with --add (new fact) or --supersede <uri> --predecessor-hash <hash>.`
433
+ );
434
+ for (const candidate of result.candidates) {
435
+ lines.push(...formatCandidate(candidate));
436
+ }
437
+ lines.push(`Matching: ${formatMatching(result.matching)}`);
438
+ return lines.join("\n");
439
+ }
440
+ const { record } = result;
441
+ lines.push(
442
+ result.outcome === "existing"
443
+ ? "Already remembered (exact duplicate, nothing written)."
444
+ : result.outcome === "superseded"
445
+ ? "Remembered fact (supersedes predecessor)."
446
+ : "Remembered fact."
447
+ );
448
+ lines.push(`URI: ${record.uri}`);
449
+ lines.push(`Record: ${record.recordId}`);
450
+ lines.push(`Hash: ${record.contentHash}`);
451
+ lines.push(`Scopes: ${record.scopes.join(", ")}`);
452
+ if (record.supersedes.length > 0) {
453
+ lines.push(`Supersedes: ${record.supersedes.join(", ")}`);
454
+ }
455
+ if (result.outcome !== "existing") {
456
+ lines.push(`Path: ${result.absPath}`);
457
+ lines.push(`Sync: ${result.sync.status}`);
458
+ }
459
+ lines.push(`Matching: ${formatMatching(result.matching)}`);
460
+ return lines.join("\n");
461
+ }
462
+
463
+ export function formatRecallResult(
464
+ result: RecallResult,
465
+ options: MemoryFormatOptions = {}
466
+ ): string {
467
+ if (options.json) return JSON.stringify(result, null, 2);
468
+ if (options.quiet) {
469
+ return result.facts.length === 0
470
+ ? (result.hint ?? "")
471
+ : result.facts.map((fact) => fact.uri).join("\n");
472
+ }
473
+ const lines: string[] = [];
474
+ if (result.facts.length === 0 && result.hint) {
475
+ lines.push(result.hint);
476
+ }
477
+ for (const [index, fact] of result.facts.entries()) {
478
+ lines.push(`${index + 1}. ${fact.uri}`);
479
+ lines.push(` ${fact.text}`);
480
+ lines.push(
481
+ ` scopes: ${fact.scopes.join(", ")} | hash: ${fact.contentHash} | by ${fact.caller}/${fact.session} at ${fact.createdAt}`
482
+ );
483
+ }
484
+ const { budget } = result;
485
+ lines.push(
486
+ `Budget: ${result.facts.length}/${budget.maxFacts} facts, ${budget.usedTokens}/${budget.maxTokens} tokens, ${budget.omitted} omitted`
487
+ );
488
+ lines.push(`Retrieval: ${formatMatching(result.retrieval)}`);
489
+ lines.push(`Receipt: ${result.receipt.digest}`);
490
+ return lines.join("\n");
491
+ }
@@ -7,6 +7,7 @@
7
7
 
8
8
  import type { ContentTypeBoostStatus } from "../../config/content-types";
9
9
  import type { ActivationStatus } from "../../core/activation-status";
10
+ import type { MemoryStatus } from "../../core/memory-diagnostics";
10
11
  import type { IndexStatus } from "../../store/types";
11
12
 
12
13
  import { getIndexDbPath, getModelsCachePath } from "../../app/constants";
@@ -18,6 +19,10 @@ import {
18
19
  } from "../../config";
19
20
  import { isConnectorActivationComplete } from "../../core/activation-connector-health";
20
21
  import { buildActivationStatus } from "../../core/activation-status";
22
+ import {
23
+ buildMemoryStatus,
24
+ formatMemoryStatusLines,
25
+ } from "../../core/memory-diagnostics";
21
26
  import { ModelCache } from "../../llm/cache";
22
27
  import { getActivePreset, resolveModelUri } from "../../llm/registry";
23
28
  import { getConnectorVerificationTargets } from "../../serve/connectors";
@@ -47,6 +52,7 @@ export type StatusResult =
47
52
  status: IndexStatus;
48
53
  activation: ActivationStatus;
49
54
  contentTypeBoost: ContentTypeBoostStatus;
55
+ memory: MemoryStatus;
50
56
  }
51
57
  | { success: false; error: string };
52
58
 
@@ -75,7 +81,8 @@ function isStatusHealthy(
75
81
  function formatTerminal(
76
82
  indexStatus: IndexStatus,
77
83
  activation: ActivationStatus,
78
- contentTypeBoost: ContentTypeBoostStatus
84
+ contentTypeBoost: ContentTypeBoostStatus,
85
+ memory: MemoryStatus
79
86
  ): string {
80
87
  const lines: string[] = [];
81
88
 
@@ -146,6 +153,7 @@ function formatTerminal(
146
153
  if (projectionLine) {
147
154
  lines.push(projectionLine);
148
155
  }
156
+ lines.push(...formatMemoryStatusLines(memory));
149
157
 
150
158
  return lines.join("\n");
151
159
  }
@@ -156,7 +164,8 @@ function formatTerminal(
156
164
  function formatMarkdown(
157
165
  indexStatus: IndexStatus,
158
166
  activation: ActivationStatus,
159
- contentTypeBoost: ContentTypeBoostStatus
167
+ contentTypeBoost: ContentTypeBoostStatus,
168
+ memory: MemoryStatus
160
169
  ): string {
161
170
  const lines: string[] = [];
162
171
 
@@ -216,6 +225,12 @@ function formatMarkdown(
216
225
  if (projectionLine) {
217
226
  lines.push(`- **${projectionLine}**`);
218
227
  }
228
+ lines.push("");
229
+ lines.push("## Memory");
230
+ lines.push("");
231
+ for (const line of formatMemoryStatusLines(memory)) {
232
+ lines.push(`- ${line.trim()}`);
233
+ }
219
234
 
220
235
  return lines.join("\n");
221
236
  }
@@ -285,6 +300,7 @@ export async function status(
285
300
  status: statusResult.value,
286
301
  activation,
287
302
  contentTypeBoost: buildContentTypeBoostStatus(config.contentTypes ?? []),
303
+ memory: await buildMemoryStatus(store, config.collections),
288
304
  };
289
305
  } finally {
290
306
  await store.close();
@@ -327,6 +343,7 @@ export function formatStatus(
327
343
  healthy: isStatusHealthy(s, result.activation),
328
344
  contentTypeBoost: result.contentTypeBoost,
329
345
  activation: result.activation,
346
+ memory: result.memory,
330
347
  },
331
348
  null,
332
349
  2
@@ -337,13 +354,15 @@ export function formatStatus(
337
354
  return formatMarkdown(
338
355
  result.status,
339
356
  result.activation,
340
- result.contentTypeBoost
357
+ result.contentTypeBoost,
358
+ result.memory
341
359
  );
342
360
  }
343
361
 
344
362
  return formatTerminal(
345
363
  result.status,
346
364
  result.activation,
347
- result.contentTypeBoost
365
+ result.contentTypeBoost,
366
+ result.memory
348
367
  );
349
368
  }
@@ -87,6 +87,8 @@ export const CMD = {
87
87
  diff: "diff",
88
88
  impact: "impact",
89
89
  capture: "capture",
90
+ remember: "remember",
91
+ recall: "recall",
90
92
  } as const;
91
93
 
92
94
  export type CommandId = (typeof CMD)[keyof typeof CMD];
@@ -122,6 +124,8 @@ const FORMAT_SUPPORT: Record<CommandId, OutputFormat[]> = {
122
124
  [CMD.diff]: ["terminal", "json"],
123
125
  [CMD.impact]: ["terminal", "json"],
124
126
  [CMD.capture]: ["terminal", "json"],
127
+ [CMD.remember]: ["terminal", "json"],
128
+ [CMD.recall]: ["terminal", "json"],
125
129
  };
126
130
 
127
131
  // ─────────────────────────────────────────────────────────────────────────────