formative-memory 0.1.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/dist/cli.js ADDED
@@ -0,0 +1,520 @@
1
+ #!/usr/bin/env node
2
+ import { n as MemorySourceGuard, r as TemporalStateGuard, t as MemoryDatabase } from "./db-D2pzT6fw.js";
3
+ import { join } from "node:path";
4
+ import { existsSync, readFileSync } from "node:fs";
5
+ //#region src/cli.ts
6
+ /**
7
+ * Associative Memory CLI
8
+ *
9
+ * Diagnostic tool for inspecting and managing the memory database.
10
+ * Operates directly on SQLite — does not require the OpenClaw runtime.
11
+ *
12
+ * Usage: memory <command> <memory-dir> [options]
13
+ *
14
+ * Commands:
15
+ * stats — Overview of memory database
16
+ * list — List memories (filterable)
17
+ * inspect — Detailed view of a single memory
18
+ * search — Search memories by content
19
+ * export — Export database to JSON
20
+ */
21
+ function fail(message) {
22
+ throw new CliError(message);
23
+ }
24
+ var CliError = class extends Error {
25
+ constructor(message) {
26
+ super(message);
27
+ this.name = "CliError";
28
+ }
29
+ };
30
+ function parsePositiveInt(value, name) {
31
+ const n = Number(value);
32
+ if (!Number.isInteger(n) || n < 1) fail(`Invalid value for ${name}: '${value}'. Expected a positive integer.`);
33
+ return n;
34
+ }
35
+ function parseNonNegativeNumber(value, name) {
36
+ const n = Number(value);
37
+ if (!Number.isFinite(n) || n < 0) fail(`Invalid value for ${name}: '${value}'. Expected a non-negative number.`);
38
+ return n;
39
+ }
40
+ function validateFormat(value) {
41
+ if (value === "json" || value === "text") return value;
42
+ fail(`Invalid format: '${value}'. Expected 'json' or 'text'.`);
43
+ }
44
+ function main() {
45
+ const args = process.argv.slice(2);
46
+ let format = "json";
47
+ const filteredArgs = [];
48
+ for (let i = 0; i < args.length; i++) if (args[i] === "--format" && args[i + 1]) format = validateFormat(args[++i]);
49
+ else if (args[i] === "--text") format = "text";
50
+ else filteredArgs.push(args[i]);
51
+ const [command, memoryDir, ...rest] = filteredArgs;
52
+ if (!command || command === "--help" || command === "-h") {
53
+ printUsage();
54
+ process.exit(0);
55
+ }
56
+ if (!memoryDir) fail("Memory directory path required. Usage: memory <command> <memory-dir> [options]");
57
+ const dbPath = join(memoryDir, "associations.db");
58
+ if (command !== "import" && !existsSync(dbPath)) fail(`Database not found at ${dbPath}`);
59
+ const db = new MemoryDatabase(dbPath);
60
+ try {
61
+ const ctx = {
62
+ db,
63
+ format
64
+ };
65
+ switch (command) {
66
+ case "stats":
67
+ cmdStats(ctx);
68
+ break;
69
+ case "list":
70
+ cmdList(ctx, rest);
71
+ break;
72
+ case "inspect":
73
+ cmdInspect(ctx, rest);
74
+ break;
75
+ case "search":
76
+ cmdSearch(ctx, rest);
77
+ break;
78
+ case "export":
79
+ cmdExport(ctx);
80
+ break;
81
+ case "history":
82
+ cmdHistory(ctx, rest);
83
+ break;
84
+ case "graph":
85
+ cmdGraph(ctx);
86
+ break;
87
+ case "import":
88
+ cmdImport(ctx, rest);
89
+ break;
90
+ default: fail(`Unknown command: ${command}`);
91
+ }
92
+ } finally {
93
+ db.close();
94
+ }
95
+ }
96
+ try {
97
+ main();
98
+ } catch (err) {
99
+ if (err instanceof CliError) console.error(`Error: ${err.message}`);
100
+ else console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
101
+ process.exit(1);
102
+ }
103
+ function cmdStats(ctx) {
104
+ const { db } = ctx;
105
+ const stats = db.stats();
106
+ const lastConsolidation = db.getState("last_consolidation_at");
107
+ output(ctx, {
108
+ ...stats,
109
+ lastConsolidation
110
+ }, () => {
111
+ console.log(`Memories: ${stats.total} (${stats.working} working, ${stats.consolidated} consolidated)`);
112
+ console.log(`Associations: ${stats.associations}`);
113
+ console.log(`Last consolidation: ${lastConsolidation ?? "never"}`);
114
+ });
115
+ }
116
+ function cmdList(ctx, args) {
117
+ const { db } = ctx;
118
+ let type = null;
119
+ let state = null;
120
+ let minStrength = 0;
121
+ let limit = 50;
122
+ for (let i = 0; i < args.length; i++) {
123
+ const arg = args[i];
124
+ if (arg === "--type") type = args[++i] ?? fail("Missing value for --type");
125
+ else if (arg === "--state") state = args[++i] ?? fail("Missing value for --state");
126
+ else if (arg === "--min-strength") minStrength = parseNonNegativeNumber(args[++i] ?? "", "--min-strength");
127
+ else if (arg === "--limit") limit = parsePositiveInt(args[++i] ?? "", "--limit");
128
+ else if (arg.startsWith("--")) fail(`Unknown option for list: ${arg}`);
129
+ }
130
+ let memories = db.getAllMemories();
131
+ if (type) memories = memories.filter((m) => m.type === type);
132
+ if (state) memories = memories.filter((m) => m.temporal_state === state);
133
+ if (minStrength > 0) memories = memories.filter((m) => m.strength >= minStrength);
134
+ memories = memories.slice(0, limit);
135
+ const result = memories.map((m) => ({
136
+ id: m.id,
137
+ id_short: m.id.slice(0, 8),
138
+ type: m.type,
139
+ content: m.content.length > 100 ? m.content.slice(0, 97) + "..." : m.content,
140
+ strength: Math.round(m.strength * 1e3) / 1e3,
141
+ temporal_state: m.temporal_state,
142
+ consolidated: m.consolidated === 1,
143
+ source: m.source,
144
+ created_at: m.created_at
145
+ }));
146
+ output(ctx, result, () => {
147
+ if (result.length === 0) {
148
+ console.log("No memories found.");
149
+ return;
150
+ }
151
+ for (const m of result) {
152
+ const flag = m.consolidated ? "C" : "W";
153
+ console.log(`[${m.id_short}] [${flag}] str=${m.strength} type=${m.type} ${m.content}`);
154
+ }
155
+ console.log(`\n${result.length} memories shown.`);
156
+ });
157
+ }
158
+ /** Resolve a memory by exact ID or unique prefix. Fails on ambiguous or missing. */
159
+ function resolveMemory(db, idOrPrefix) {
160
+ const exact = db.getMemory(idOrPrefix);
161
+ if (exact) return exact;
162
+ const matches = db.getAllMemories().filter((m) => m.id.startsWith(idOrPrefix));
163
+ if (matches.length === 0) fail(`Memory not found: ${idOrPrefix}`);
164
+ if (matches.length > 1) fail(`Ambiguous prefix '${idOrPrefix}'. Matches: ${matches.slice(0, 5).map((m) => m.id.slice(0, 12)).join(", ")}${matches.length > 5 ? `, ... (${matches.length} total)` : ""}`);
165
+ return matches[0];
166
+ }
167
+ function cmdInspect(ctx, args) {
168
+ const { db } = ctx;
169
+ const id = args[0];
170
+ if (!id) fail("Memory ID required for inspect");
171
+ const row = resolveMemory(db, id);
172
+ const associations = db.getAssociations(row.id).map((a) => ({
173
+ neighbor: a.memory_a === row.id ? a.memory_b : a.memory_a,
174
+ weight: Math.round(a.weight * 1e3) / 1e3
175
+ }));
176
+ const attributions = db.getAttributionsByMemory(row.id).map((a) => ({
177
+ message_id: a.message_id,
178
+ evidence: a.evidence,
179
+ confidence: a.confidence,
180
+ turn_id: a.turn_id,
181
+ reinforcement_applied: a.reinforcement_applied === 1
182
+ }));
183
+ const exposures = db.getExposuresByMemory(row.id).map((e) => ({
184
+ turn_id: e.turn_id,
185
+ mode: e.mode,
186
+ score: e.score,
187
+ retrieval_mode: e.retrieval_mode
188
+ }));
189
+ const alias = db.getAlias(row.id);
190
+ const canonical = alias ? db.resolveAlias(row.id) : null;
191
+ const result = {
192
+ id: row.id,
193
+ id_short: row.id.slice(0, 8),
194
+ type: row.type,
195
+ content: row.content,
196
+ strength: row.strength,
197
+ temporal_state: row.temporal_state,
198
+ temporal_anchor: row.temporal_anchor,
199
+ consolidated: row.consolidated === 1,
200
+ source: row.source,
201
+ created_at: row.created_at,
202
+ alias: alias ? {
203
+ points_to: alias,
204
+ canonical
205
+ } : null,
206
+ associations,
207
+ attributions,
208
+ exposures
209
+ };
210
+ output(ctx, result, () => {
211
+ console.log(`ID: ${result.id}`);
212
+ console.log(`Type: ${result.type}`);
213
+ console.log(`Strength: ${result.strength}`);
214
+ console.log(`State: ${result.temporal_state}${result.temporal_anchor ? ` (anchor: ${result.temporal_anchor})` : ""}`);
215
+ console.log(`Source: ${result.source} | ${result.consolidated ? "Consolidated" : "Working"}`);
216
+ console.log(`Created: ${result.created_at}`);
217
+ if (result.alias) console.log(`Alias: → ${result.alias.canonical}`);
218
+ console.log(`\nContent:\n${result.content}`);
219
+ if (associations.length > 0) {
220
+ console.log(`\nAssociations (${associations.length}):`);
221
+ for (const a of associations) console.log(` ${a.neighbor.slice(0, 8)} weight=${a.weight}`);
222
+ }
223
+ if (attributions.length > 0) {
224
+ console.log(`\nAttributions (${attributions.length}):`);
225
+ for (const a of attributions) console.log(` ${a.evidence} conf=${a.confidence} turn=${a.turn_id.slice(0, 20)} reinforced=${a.reinforcement_applied}`);
226
+ }
227
+ if (exposures.length > 0) {
228
+ console.log(`\nExposures (${exposures.length}):`);
229
+ for (const e of exposures) console.log(` ${e.mode} score=${e.score} mode=${e.retrieval_mode}`);
230
+ }
231
+ });
232
+ }
233
+ function cmdSearch(ctx, args) {
234
+ const { db } = ctx;
235
+ const query = args.join(" ");
236
+ if (!query) {
237
+ console.error("Error: search query required");
238
+ process.exit(1);
239
+ }
240
+ const result = db.searchFts(query, 20).map((r) => {
241
+ const mem = db.getMemory(r.id);
242
+ return {
243
+ id: r.id,
244
+ id_short: r.id.slice(0, 8),
245
+ type: mem?.type ?? "unknown",
246
+ content: mem?.content ?? "(not found)",
247
+ strength: mem?.strength ?? 0,
248
+ fts_rank: r.rank
249
+ };
250
+ });
251
+ output(ctx, result, () => {
252
+ if (result.length === 0) {
253
+ console.log("No results found.");
254
+ return;
255
+ }
256
+ for (const r of result) {
257
+ const content = r.content.length > 80 ? r.content.slice(0, 77) + "..." : r.content;
258
+ console.log(`[${r.id_short}] str=${r.strength} rank=${Math.round(r.fts_rank * 100) / 100} ${content}`);
259
+ }
260
+ });
261
+ }
262
+ function cmdExport(ctx) {
263
+ const { db } = ctx;
264
+ const memories = db.getAllMemories().map((m) => ({
265
+ id: m.id,
266
+ type: m.type,
267
+ content: m.content,
268
+ strength: m.strength,
269
+ temporal_state: m.temporal_state,
270
+ temporal_anchor: m.temporal_anchor,
271
+ consolidated: m.consolidated === 1,
272
+ source: m.source,
273
+ created_at: m.created_at
274
+ }));
275
+ const associations = db.getAllAssociations().map((a) => ({
276
+ memory_a: a.memory_a,
277
+ memory_b: a.memory_b,
278
+ weight: a.weight,
279
+ created_at: a.created_at,
280
+ last_updated_at: a.last_updated_at
281
+ }));
282
+ const attributions = db.getAllAttributions();
283
+ const exposures = db.getAllExposures();
284
+ const aliases = db.getAllAliases();
285
+ const state = db.getAllState();
286
+ const exportData = {
287
+ version: 2,
288
+ exported_at: (/* @__PURE__ */ new Date()).toISOString(),
289
+ memories,
290
+ associations,
291
+ attributions,
292
+ exposures,
293
+ aliases,
294
+ state
295
+ };
296
+ console.log(JSON.stringify(exportData, null, 2));
297
+ }
298
+ function cmdHistory(ctx, args) {
299
+ const { db } = ctx;
300
+ const id = args[0];
301
+ if (!id) fail("Memory ID required for history");
302
+ const row = resolveMemory(db, id);
303
+ const canonical = db.resolveAlias(row.id);
304
+ const isAliased = canonical !== row.id;
305
+ const predecessors = db.getAliasedIdsPointingTo(row.id);
306
+ const attributions = db.getAttributionsByMemory(row.id);
307
+ const exposures = db.getExposuresByMemory(row.id);
308
+ const result = {
309
+ id: row.id,
310
+ id_short: row.id.slice(0, 8),
311
+ source: row.source,
312
+ created_at: row.created_at,
313
+ current_strength: row.strength,
314
+ consolidated: row.consolidated === 1,
315
+ alias: isAliased ? {
316
+ canonical,
317
+ note: "This memory was merged into another"
318
+ } : null,
319
+ predecessors: predecessors.length > 0 ? predecessors : null,
320
+ attribution_count: attributions.length,
321
+ exposure_count: exposures.length,
322
+ timeline: [
323
+ {
324
+ event: "created",
325
+ at: row.created_at,
326
+ detail: `source=${row.source}, type=${row.type}`
327
+ },
328
+ ...attributions.map((a) => ({
329
+ event: "attributed",
330
+ at: a.created_at,
331
+ detail: `${a.evidence} conf=${a.confidence} msg=${a.message_id.slice(0, 20)}`
332
+ })),
333
+ ...exposures.map((e) => ({
334
+ event: "exposed",
335
+ at: e.created_at,
336
+ detail: `${e.mode} turn=${e.turn_id.slice(0, 20)}`
337
+ }))
338
+ ].sort((a, b) => a.at.localeCompare(b.at))
339
+ };
340
+ output(ctx, result, () => {
341
+ console.log(`History for ${result.id_short} (${row.type})`);
342
+ console.log(`Source: ${result.source} | Created: ${result.created_at}`);
343
+ console.log(`Current strength: ${result.current_strength}`);
344
+ if (result.alias) console.log(`Merged into: ${result.alias.canonical.slice(0, 8)}`);
345
+ if (result.predecessors) console.log(`Predecessors: ${result.predecessors.map((p) => p.slice(0, 8)).join(", ")}`);
346
+ console.log(`\nTimeline (${result.timeline.length} events):`);
347
+ for (const ev of result.timeline) console.log(` ${ev.at} ${ev.event}: ${ev.detail}`);
348
+ });
349
+ }
350
+ function cmdGraph(ctx) {
351
+ const { db } = ctx;
352
+ const memories = db.getAllMemories();
353
+ const edges = [];
354
+ const seen = /* @__PURE__ */ new Set();
355
+ for (const mem of memories) for (const assoc of db.getAssociations(mem.id)) {
356
+ const key = `${assoc.memory_a}:${assoc.memory_b}`;
357
+ if (!seen.has(key)) {
358
+ seen.add(key);
359
+ edges.push({
360
+ a: assoc.memory_a,
361
+ b: assoc.memory_b,
362
+ weight: assoc.weight
363
+ });
364
+ }
365
+ }
366
+ if (ctx.format === "text") {
367
+ console.log("graph associations {");
368
+ console.log(" node [shape=box, style=rounded];");
369
+ for (const mem of memories) {
370
+ const label = mem.content.length > 30 ? mem.content.slice(0, 27) + "..." : mem.content;
371
+ const flag = mem.consolidated === 1 ? "C" : "W";
372
+ console.log(` "${mem.id.slice(0, 8)}" [label="${mem.id.slice(0, 8)}\\n${flag} str=${mem.strength.toFixed(2)}\\n${escapeDot(label)}"];`);
373
+ }
374
+ for (const e of edges) console.log(` "${e.a.slice(0, 8)}" -- "${e.b.slice(0, 8)}" [label="${e.weight.toFixed(2)}"];`);
375
+ console.log("}");
376
+ } else {
377
+ const result = {
378
+ nodes: memories.map((m) => ({
379
+ id: m.id,
380
+ id_short: m.id.slice(0, 8),
381
+ type: m.type,
382
+ strength: m.strength,
383
+ consolidated: m.consolidated === 1
384
+ })),
385
+ edges: edges.map((e) => ({
386
+ a: e.a.slice(0, 8),
387
+ b: e.b.slice(0, 8),
388
+ weight: Math.round(e.weight * 1e3) / 1e3
389
+ }))
390
+ };
391
+ console.log(JSON.stringify(result, null, 2));
392
+ }
393
+ }
394
+ function cmdImport(ctx, args) {
395
+ const { db } = ctx;
396
+ const filePath = args[0];
397
+ if (!filePath) fail("Import file path required");
398
+ if (!existsSync(filePath)) fail(`File not found: ${filePath}`);
399
+ let data;
400
+ try {
401
+ data = JSON.parse(readFileSync(filePath, "utf8"));
402
+ } catch (err) {
403
+ fail(`Failed to parse import file: ${err instanceof Error ? err.message : String(err)}`);
404
+ }
405
+ if (typeof data !== "object" || data === null) fail("Import file must contain a JSON object");
406
+ const version = data.version;
407
+ if (version !== 1 && version !== 2) fail(`Unsupported export version: ${version}. Expected 1 or 2.`);
408
+ const counts = {
409
+ memories: 0,
410
+ memoriesSkipped: 0,
411
+ associations: 0,
412
+ attributions: 0,
413
+ exposures: 0,
414
+ aliases: 0,
415
+ state: 0
416
+ };
417
+ db.transaction(() => {
418
+ const memories = data.memories;
419
+ if (memories) for (const m of memories) {
420
+ if (db.getMemory(m.id)) {
421
+ counts.memoriesSkipped++;
422
+ continue;
423
+ }
424
+ db.insertMemory({
425
+ id: m.id,
426
+ type: m.type,
427
+ content: m.content,
428
+ strength: m.strength,
429
+ temporal_state: TemporalStateGuard.is(m.temporal_state) ? m.temporal_state : "none",
430
+ temporal_anchor: m.temporal_anchor ?? null,
431
+ consolidated: m.consolidated ?? false,
432
+ source: MemorySourceGuard.is(m.source) ? m.source : "agent_tool",
433
+ created_at: m.created_at
434
+ });
435
+ db.insertFts(m.id, m.content, m.type);
436
+ counts.memories++;
437
+ }
438
+ const associations = data.associations;
439
+ if (associations) for (const a of associations) {
440
+ const memA = a.memory_a ?? a.a;
441
+ const memB = a.memory_b ?? a.b;
442
+ const createdAt = a.created_at ?? (/* @__PURE__ */ new Date()).toISOString();
443
+ db.upsertAssociation(memA, memB, a.weight, createdAt);
444
+ counts.associations++;
445
+ }
446
+ const attributions = data.attributions;
447
+ if (attributions) for (const a of attributions) {
448
+ db.insertAttributionRaw({
449
+ message_id: a.message_id,
450
+ memory_id: a.memory_id,
451
+ evidence: a.evidence,
452
+ confidence: a.confidence,
453
+ turn_id: a.turn_id,
454
+ created_at: a.created_at,
455
+ updated_at: a.updated_at ?? null,
456
+ reinforcement_applied: a.reinforcement_applied ?? 0
457
+ });
458
+ counts.attributions++;
459
+ }
460
+ const exposures = data.exposures;
461
+ if (exposures) for (const e of exposures) {
462
+ db.insertExposureRaw(e);
463
+ counts.exposures++;
464
+ }
465
+ const aliases = data.aliases;
466
+ if (aliases) for (const a of aliases) {
467
+ db.insertAlias(a.old_id, a.new_id, a.reason, a.created_at);
468
+ counts.aliases++;
469
+ }
470
+ const stateEntries = data.state;
471
+ if (stateEntries) for (const s of stateEntries) {
472
+ db.setState(s.key, s.value);
473
+ counts.state++;
474
+ }
475
+ });
476
+ output(ctx, counts, () => {
477
+ console.log(`Imported: ${counts.memories} memories, ${counts.associations} associations`);
478
+ if (counts.attributions > 0) console.log(` ${counts.attributions} attributions`);
479
+ if (counts.exposures > 0) console.log(` ${counts.exposures} exposures`);
480
+ if (counts.aliases > 0) console.log(` ${counts.aliases} aliases`);
481
+ if (counts.state > 0) console.log(` ${counts.state} state entries`);
482
+ if (counts.memoriesSkipped > 0) console.log(`Skipped: ${counts.memoriesSkipped} memories (already exist)`);
483
+ });
484
+ }
485
+ function escapeDot(s) {
486
+ return s.replace(/\\/g, "\\\\").replace(/\n/g, "\\n").replace(/\r/g, "").replace(/"/g, "\\\"");
487
+ }
488
+ function output(ctx, data, textFn) {
489
+ if (ctx.format === "text") textFn();
490
+ else console.log(JSON.stringify(data, null, 2));
491
+ }
492
+ function printUsage() {
493
+ console.log(`
494
+ Associative Memory CLI
495
+
496
+ Usage: memory <command> <memory-dir> [options]
497
+
498
+ Commands:
499
+ stats Overview of memory database
500
+ list List memories
501
+ inspect <id> Detailed view of a single memory
502
+ search <query> Search memories by content
503
+ history <id> Timeline of a memory's lifecycle
504
+ graph Association graph (DOT format in text mode)
505
+ export Export database to JSON
506
+ import <file> Import memories from JSON export file
507
+
508
+ Options:
509
+ --format json|text Output format (default: json)
510
+ --text Shorthand for --format text
511
+
512
+ List filters:
513
+ --type <type> Filter by memory type
514
+ --state <state> Filter by temporal state
515
+ --min-strength <n> Minimum strength threshold
516
+ --limit <n> Max results (default: 50)
517
+ `.trim());
518
+ }
519
+ //#endregion
520
+ export {};