@jbarket/loomai 0.5.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 (176) hide show
  1. package/CHANGELOG.md +475 -0
  2. package/LICENSE +661 -0
  3. package/README.md +702 -0
  4. package/assets/demo.cast +396 -0
  5. package/assets/skill/SKILL.md +192 -0
  6. package/dist/backends/embedding-runtime.d.ts +39 -0
  7. package/dist/backends/embedding-runtime.js +265 -0
  8. package/dist/backends/episodes.d.ts +36 -0
  9. package/dist/backends/episodes.js +126 -0
  10. package/dist/backends/fastembed.d.ts +18 -0
  11. package/dist/backends/fastembed.js +95 -0
  12. package/dist/backends/glob.d.ts +10 -0
  13. package/dist/backends/glob.js +16 -0
  14. package/dist/backends/index.d.ts +6 -0
  15. package/dist/backends/index.js +78 -0
  16. package/dist/backends/migrations.d.ts +38 -0
  17. package/dist/backends/migrations.js +157 -0
  18. package/dist/backends/mmr.d.ts +45 -0
  19. package/dist/backends/mmr.js +82 -0
  20. package/dist/backends/proposals.d.ts +65 -0
  21. package/dist/backends/proposals.js +182 -0
  22. package/dist/backends/recall-log.d.ts +42 -0
  23. package/dist/backends/recall-log.js +143 -0
  24. package/dist/backends/retry.d.ts +29 -0
  25. package/dist/backends/retry.js +60 -0
  26. package/dist/backends/salience.d.ts +57 -0
  27. package/dist/backends/salience.js +186 -0
  28. package/dist/backends/sqlite-knowledge.d.ts +37 -0
  29. package/dist/backends/sqlite-knowledge.js +788 -0
  30. package/dist/backends/sqlite-vec.d.ts +64 -0
  31. package/dist/backends/sqlite-vec.js +737 -0
  32. package/dist/backends/ttl.d.ts +24 -0
  33. package/dist/backends/ttl.js +50 -0
  34. package/dist/backends/types.d.ts +562 -0
  35. package/dist/backends/types.js +8 -0
  36. package/dist/blocks/harness.d.ts +45 -0
  37. package/dist/blocks/harness.js +142 -0
  38. package/dist/blocks/model.d.ts +4 -0
  39. package/dist/blocks/model.js +56 -0
  40. package/dist/blocks/types.d.ts +40 -0
  41. package/dist/blocks/types.js +34 -0
  42. package/dist/categories.d.ts +16 -0
  43. package/dist/categories.js +29 -0
  44. package/dist/cli/args.d.ts +21 -0
  45. package/dist/cli/args.js +47 -0
  46. package/dist/cli/bootstrap.d.ts +2 -0
  47. package/dist/cli/bootstrap.js +214 -0
  48. package/dist/cli/doctor.d.ts +2 -0
  49. package/dist/cli/doctor.js +206 -0
  50. package/dist/cli/forget.d.ts +2 -0
  51. package/dist/cli/forget.js +97 -0
  52. package/dist/cli/harness.d.ts +2 -0
  53. package/dist/cli/harness.js +91 -0
  54. package/dist/cli/index.d.ts +2 -0
  55. package/dist/cli/index.js +127 -0
  56. package/dist/cli/inject.d.ts +2 -0
  57. package/dist/cli/inject.js +298 -0
  58. package/dist/cli/install.d.ts +2 -0
  59. package/dist/cli/install.js +137 -0
  60. package/dist/cli/io.d.ts +14 -0
  61. package/dist/cli/io.js +63 -0
  62. package/dist/cli/knowledge.d.ts +2 -0
  63. package/dist/cli/knowledge.js +216 -0
  64. package/dist/cli/memory.d.ts +2 -0
  65. package/dist/cli/memory.js +608 -0
  66. package/dist/cli/migrate.d.ts +2 -0
  67. package/dist/cli/migrate.js +116 -0
  68. package/dist/cli/recall.d.ts +2 -0
  69. package/dist/cli/recall.js +91 -0
  70. package/dist/cli/remember.d.ts +2 -0
  71. package/dist/cli/remember.js +111 -0
  72. package/dist/cli/serve.d.ts +2 -0
  73. package/dist/cli/serve.js +71 -0
  74. package/dist/cli/subcommands.d.ts +7 -0
  75. package/dist/cli/subcommands.js +11 -0
  76. package/dist/cli/tui/multi-select.d.ts +53 -0
  77. package/dist/cli/tui/multi-select.js +116 -0
  78. package/dist/cli/update-identity.d.ts +2 -0
  79. package/dist/cli/update-identity.js +90 -0
  80. package/dist/cli/update.d.ts +2 -0
  81. package/dist/cli/update.js +87 -0
  82. package/dist/cli/wake.d.ts +2 -0
  83. package/dist/cli/wake.js +53 -0
  84. package/dist/clients.d.ts +8 -0
  85. package/dist/clients.js +63 -0
  86. package/dist/config.d.ts +59 -0
  87. package/dist/config.js +160 -0
  88. package/dist/index.d.ts +15 -0
  89. package/dist/index.js +75 -0
  90. package/dist/injection/harnesses.d.ts +17 -0
  91. package/dist/injection/harnesses.js +51 -0
  92. package/dist/injection/render.d.ts +17 -0
  93. package/dist/injection/render.js +50 -0
  94. package/dist/injection/writer.d.ts +25 -0
  95. package/dist/injection/writer.js +118 -0
  96. package/dist/install/harnesses.d.ts +26 -0
  97. package/dist/install/harnesses.js +90 -0
  98. package/dist/install/names.d.ts +16 -0
  99. package/dist/install/names.js +36 -0
  100. package/dist/install/render.d.ts +18 -0
  101. package/dist/install/render.js +42 -0
  102. package/dist/install/skill-source.d.ts +1 -0
  103. package/dist/install/skill-source.js +11 -0
  104. package/dist/path-safety.d.ts +19 -0
  105. package/dist/path-safety.js +62 -0
  106. package/dist/server.d.ts +8 -0
  107. package/dist/server.js +571 -0
  108. package/dist/tools/append-maintenance-log.d.ts +52 -0
  109. package/dist/tools/append-maintenance-log.js +103 -0
  110. package/dist/tools/archive.d.ts +2 -0
  111. package/dist/tools/archive.js +21 -0
  112. package/dist/tools/bootstrap.d.ts +11 -0
  113. package/dist/tools/bootstrap.js +161 -0
  114. package/dist/tools/dossier.d.ts +1 -0
  115. package/dist/tools/dossier.js +102 -0
  116. package/dist/tools/eurorack-import.d.ts +47 -0
  117. package/dist/tools/eurorack-import.js +199 -0
  118. package/dist/tools/find-similar.d.ts +3 -0
  119. package/dist/tools/find-similar.js +26 -0
  120. package/dist/tools/forget.d.ts +6 -0
  121. package/dist/tools/forget.js +70 -0
  122. package/dist/tools/harness.d.ts +18 -0
  123. package/dist/tools/harness.js +54 -0
  124. package/dist/tools/identity-scaffold.d.ts +22 -0
  125. package/dist/tools/identity-scaffold.js +142 -0
  126. package/dist/tools/identity.d.ts +4 -0
  127. package/dist/tools/identity.js +226 -0
  128. package/dist/tools/knowledge-archive.d.ts +5 -0
  129. package/dist/tools/knowledge-archive.js +24 -0
  130. package/dist/tools/knowledge-history.d.ts +6 -0
  131. package/dist/tools/knowledge-history.js +55 -0
  132. package/dist/tools/knowledge-maintain.d.ts +9 -0
  133. package/dist/tools/knowledge-maintain.js +113 -0
  134. package/dist/tools/knowledge-merge.d.ts +8 -0
  135. package/dist/tools/knowledge-merge.js +40 -0
  136. package/dist/tools/knowledge-move.d.ts +10 -0
  137. package/dist/tools/knowledge-move.js +48 -0
  138. package/dist/tools/knowledge-purge.d.ts +5 -0
  139. package/dist/tools/knowledge-purge.js +20 -0
  140. package/dist/tools/knowledge-recall.d.ts +20 -0
  141. package/dist/tools/knowledge-recall.js +142 -0
  142. package/dist/tools/knowledge-restore.d.ts +4 -0
  143. package/dist/tools/knowledge-restore.js +24 -0
  144. package/dist/tools/knowledge-supersede.d.ts +6 -0
  145. package/dist/tools/knowledge-supersede.js +36 -0
  146. package/dist/tools/knowledge-verify.d.ts +8 -0
  147. package/dist/tools/knowledge-verify.js +39 -0
  148. package/dist/tools/knowledge-write.d.ts +25 -0
  149. package/dist/tools/knowledge-write.js +87 -0
  150. package/dist/tools/memory-audit.d.ts +3 -0
  151. package/dist/tools/memory-audit.js +47 -0
  152. package/dist/tools/memory-list.d.ts +4 -0
  153. package/dist/tools/memory-list.js +23 -0
  154. package/dist/tools/prune.d.ts +4 -0
  155. package/dist/tools/prune.js +28 -0
  156. package/dist/tools/recall.d.ts +4 -0
  157. package/dist/tools/recall.js +24 -0
  158. package/dist/tools/remember.d.ts +10 -0
  159. package/dist/tools/remember.js +43 -0
  160. package/dist/tools/restore.d.ts +2 -0
  161. package/dist/tools/restore.js +20 -0
  162. package/dist/tools/update-identity.d.ts +34 -0
  163. package/dist/tools/update-identity.js +194 -0
  164. package/dist/tools/update.d.ts +2 -0
  165. package/dist/tools/update.js +16 -0
  166. package/dist/transport/guards.d.ts +35 -0
  167. package/dist/transport/guards.js +107 -0
  168. package/dist/transport/http-server.d.ts +17 -0
  169. package/dist/transport/http-server.js +198 -0
  170. package/dist/transport/mcp-app-hello.d.ts +3 -0
  171. package/dist/transport/mcp-app-hello.js +66 -0
  172. package/dist/widgets/display-modes.html +131 -0
  173. package/dist/widgets/hello.html +138 -0
  174. package/dist/widgets/proposals.html +131 -0
  175. package/dist/widgets/top-of-mind.html +131 -0
  176. package/package.json +73 -0
@@ -0,0 +1,608 @@
1
+ /**
2
+ * loom memory — list / prune / similar / audit the memory store.
3
+ */
4
+ import { parseArgs } from 'node:util';
5
+ import { memoryList } from '../tools/memory-list.js';
6
+ import { prune } from '../tools/prune.js';
7
+ import { recomputeSalienceForContext, digestForContext } from '../backends/salience.js';
8
+ import { tapeForContext } from '../backends/episodes.js';
9
+ import { findSimilar } from '../tools/find-similar.js';
10
+ import { memoryAudit } from '../tools/memory-audit.js';
11
+ import { archive } from '../tools/archive.js';
12
+ import { restore } from '../tools/restore.js';
13
+ import { propose, listProposals, ratifyProposal, rejectProposal, UnknownProposalError, } from '../backends/proposals.js';
14
+ import { createBackend } from '../backends/index.js';
15
+ import { readRecallObservations, summarizeRecallObservations, formatRecallStats, parseSinceDuration, } from '../backends/recall-log.js';
16
+ import { assertStackVersionCompatible } from '../config.js';
17
+ import { extractGlobalFlags, resolveEnv } from './args.js';
18
+ import { renderJson } from './io.js';
19
+ const USAGE = `Usage: loom memory <subcommand> [options]
20
+
21
+ Subcommands:
22
+ list Browse memories (table or --json)
23
+ prune Report / remove expired and stale memories
24
+ similar Surface memories semantically near a ref or text
25
+ audit One-shot health report (counts, stale, duplicates, expired)
26
+ archive Soft-retire a memory with a tombstone (recoverable)
27
+ restore Return an archived memory to the active set
28
+ propose Stage a draft memory in the capture-propose queue (not yet canon)
29
+ proposals List pending proposals awaiting ratification
30
+ ratify Ratify a pending proposal into a real memory (with optional overrides)
31
+ reject Discard a pending proposal without committing it
32
+ recompute-salience Refresh stored salience from timestamps (consolidation lane)
33
+ digest Preview the Top-of-Mind boot digest
34
+ tape The episode tape: last --hours (default 24) across all bodies, time-ordered
35
+ recall-stats Summarize the recall observation log (hit rate, latency, misses)
36
+
37
+ Options (list):
38
+ --category <name> Filter
39
+ --project <name> Filter
40
+ --limit <n> Max entries
41
+ --json Emit MemoryEntry[]
42
+
43
+ Options (prune):
44
+ --stale-days <n> Stale threshold in days
45
+ --dry-run Report what would be pruned, don't delete
46
+ --json Emit PruneResult
47
+
48
+ Options (similar):
49
+ --ref <ref> Anchor memory ref (excludes self from results)
50
+ --text <text> Or anchor on fresh text
51
+ --category <name> Restrict candidates
52
+ --project <name> Restrict candidates
53
+ --limit <n> Max neighbours (default 10)
54
+ --min-relevance <f> Drop matches below this cosine (0..1)
55
+ --json Emit MemoryMatch[]
56
+
57
+ Options (audit):
58
+ --stale-days <n> Stale threshold in days (default 30)
59
+ --similarity-threshold <f> Duplicate floor, 0..1 (default 0.85)
60
+ --max-duplicates <n> Cap on duplicate pairs (default 20)
61
+ --json Emit AuditReport
62
+
63
+ Options (archive):
64
+ <ref> Memory ref to archive (positional)
65
+ --category <name> Category (used with --title)
66
+ --title <name> Title of memory to archive (used with --category)
67
+ --note <text> Tombstone note: why this memory is being retired
68
+ --json Emit ArchiveResult
69
+
70
+ Options (restore):
71
+ <ref> Memory ref to restore (positional)
72
+ --category <name> Category (used with --title)
73
+ --title <name> Title of archived memory to restore (used with --category)
74
+ --json Emit RestoreResult
75
+
76
+ Options (propose):
77
+ --category <name> Proposed category (required)
78
+ --title <name> Proposed title (required)
79
+ --content <text> Proposed content (required)
80
+ --project <name> Associated project
81
+ --ttl <value> Time-to-live ("7d", "permanent", ...)
82
+ --source <name> Where the proposal came from (e.g. a lane name)
83
+ --json Emit the staged ProposalRef
84
+
85
+ Options (proposals):
86
+ --json Emit ProposalRow[]
87
+
88
+ Options (ratify):
89
+ <id> Proposal id to ratify (positional)
90
+ --title <text> Override the proposed title on accept
91
+ --content <text> Override the proposed content on accept
92
+ --category <name> Override the proposed category on accept
93
+ --project <name> Override the proposed project on accept
94
+ --ttl <value> Override the proposed TTL on accept
95
+ --json Emit the resulting MemoryRef
96
+
97
+ Options (reject):
98
+ <id> Proposal id to reject (positional)
99
+ --json Emit { rejected: boolean }
100
+
101
+ Options (recall-stats):
102
+ --since <window> Look back this far: 30m, 24h, 7d (default), 2w
103
+ --json Emit RecallStats
104
+
105
+ Global: --context-dir, --help/-h
106
+ `;
107
+ const SUBCOMMANDS = new Set([
108
+ 'list', 'prune', 'similar', 'audit', 'archive', 'restore',
109
+ 'propose', 'proposals', 'ratify', 'reject',
110
+ 'recompute-salience', 'digest', 'tape', 'recall-stats',
111
+ ]);
112
+ export async function run(argv, io) {
113
+ const { flags: global, rest } = extractGlobalFlags(argv);
114
+ const sub = rest[0];
115
+ const subRest = rest.slice(1);
116
+ if (!sub || sub === '--help' || sub === '-h') {
117
+ io.stdout(USAGE);
118
+ return sub ? 0 : 2;
119
+ }
120
+ if (!SUBCOMMANDS.has(sub)) {
121
+ io.stderr(`Unknown memory subcommand: ${sub}\n${USAGE}`);
122
+ return 2;
123
+ }
124
+ // Subcommand-level --help, honoured BEFORE any side effect. Without this,
125
+ // the subcommands that take no flags (recompute-salience, digest) fell
126
+ // through to their body and ran: asking for help mutated the store.
127
+ if (subRest[0] === '--help' || subRest[0] === '-h') {
128
+ io.stdout(USAGE);
129
+ return 0;
130
+ }
131
+ const env = resolveEnv(global, io.env);
132
+ try {
133
+ assertStackVersionCompatible(env.contextDir);
134
+ }
135
+ catch (err) {
136
+ io.stderr(`${err.message}\n`);
137
+ return 1;
138
+ }
139
+ // recompute-salience — the consolidation lane's entry point (it-loom-salience):
140
+ // refresh the stored temperature for every memory from its timestamps.
141
+ if (sub === 'recompute-salience') {
142
+ if (subRest.length) {
143
+ io.stderr(`memory recompute-salience takes no arguments.\n${USAGE}`);
144
+ return 2;
145
+ }
146
+ const n = recomputeSalienceForContext(env.contextDir, Date.now());
147
+ io.stdout(`recomputed salience for ${n ?? 0} memories\n`);
148
+ return 0;
149
+ }
150
+ // digest — preview the assembled boot digest (the same view injected at identity-load).
151
+ if (sub === 'digest') {
152
+ if (subRest.length) {
153
+ io.stderr(`memory digest takes no arguments.\n${USAGE}`);
154
+ return 2;
155
+ }
156
+ const d = digestForContext(env.contextDir);
157
+ io.stdout((d ?? '(no digest — empty store)') + '\n');
158
+ return 0;
159
+ }
160
+ // tape — the episode tier (t-142): what happened across all bodies, time-ordered.
161
+ if (sub === 'tape') {
162
+ let parsed;
163
+ try {
164
+ parsed = parseArgs({ args: subRest, options: { hours: { type: 'string' } }, strict: true });
165
+ }
166
+ catch (err) {
167
+ io.stderr(`${err.message}\n${USAGE}`);
168
+ return 2;
169
+ }
170
+ const hours = parsed.values.hours ? Number(parsed.values.hours) : undefined;
171
+ if (hours !== undefined && !(hours > 0)) {
172
+ io.stderr(`--hours must be a positive number\n`);
173
+ return 2;
174
+ }
175
+ const t = tapeForContext(env.contextDir, { hours, tokenBudget: 6000 });
176
+ io.stdout((t ?? `(no episodes in the last ${hours ?? 24}h)`) + '\n');
177
+ return 0;
178
+ }
179
+ // recall-stats — read the local recall observation log (telemetry/recall.jsonl).
180
+ if (sub === 'recall-stats') {
181
+ let parsed;
182
+ try {
183
+ parsed = parseArgs({ args: subRest, options: { since: { type: 'string' } }, strict: true });
184
+ }
185
+ catch (err) {
186
+ io.stderr(`${err.message}\n${USAGE}`);
187
+ return 2;
188
+ }
189
+ const window = parsed.values.since ?? '7d';
190
+ let sinceMs;
191
+ try {
192
+ sinceMs = parseSinceDuration(window);
193
+ }
194
+ catch (err) {
195
+ io.stderr(`${err.message}\n`);
196
+ return 2;
197
+ }
198
+ const rows = readRecallObservations(env.contextDir, { since: new Date(Date.now() - sinceMs) });
199
+ const stats = summarizeRecallObservations(rows);
200
+ if (env.json) {
201
+ renderJson(io, { since: window, ...stats });
202
+ return 0;
203
+ }
204
+ io.stdout(formatRecallStats(stats, window) + '\n');
205
+ return 0;
206
+ }
207
+ if (sub === 'list') {
208
+ let parsed;
209
+ try {
210
+ parsed = parseArgs({
211
+ args: subRest,
212
+ options: {
213
+ category: { type: 'string' },
214
+ project: { type: 'string' },
215
+ limit: { type: 'string' },
216
+ },
217
+ strict: true,
218
+ allowPositionals: false,
219
+ });
220
+ }
221
+ catch (err) {
222
+ io.stderr(`${err.message}\n${USAGE}`);
223
+ return 2;
224
+ }
225
+ const limit = parsed.values.limit !== undefined
226
+ ? Number.parseInt(parsed.values.limit, 10)
227
+ : undefined;
228
+ if (limit !== undefined && (!Number.isInteger(limit) || limit <= 0)) {
229
+ io.stderr(`--limit must be a positive integer.\n`);
230
+ return 2;
231
+ }
232
+ const input = {
233
+ category: parsed.values.category,
234
+ project: parsed.values.project,
235
+ limit,
236
+ };
237
+ if (env.json) {
238
+ const backend = createBackend(env.contextDir);
239
+ renderJson(io, await backend.list(input));
240
+ return 0;
241
+ }
242
+ const text = await memoryList(env.contextDir, input);
243
+ io.stdout(text.endsWith('\n') ? text : text + '\n');
244
+ return 0;
245
+ }
246
+ if (sub === 'similar') {
247
+ let parsed;
248
+ try {
249
+ parsed = parseArgs({
250
+ args: subRest,
251
+ options: {
252
+ ref: { type: 'string' },
253
+ text: { type: 'string' },
254
+ category: { type: 'string' },
255
+ project: { type: 'string' },
256
+ limit: { type: 'string' },
257
+ 'min-relevance': { type: 'string' },
258
+ },
259
+ strict: true,
260
+ allowPositionals: false,
261
+ });
262
+ }
263
+ catch (err) {
264
+ io.stderr(`${err.message}\n${USAGE}`);
265
+ return 2;
266
+ }
267
+ if (!parsed.values.ref && !parsed.values.text) {
268
+ io.stderr(`memory similar requires --ref or --text.\n`);
269
+ return 2;
270
+ }
271
+ const limit = parsed.values.limit !== undefined
272
+ ? Number.parseInt(parsed.values.limit, 10)
273
+ : undefined;
274
+ if (limit !== undefined && (!Number.isInteger(limit) || limit <= 0)) {
275
+ io.stderr(`--limit must be a positive integer.\n`);
276
+ return 2;
277
+ }
278
+ const minRelevance = parsed.values['min-relevance'] !== undefined
279
+ ? Number.parseFloat(parsed.values['min-relevance'])
280
+ : undefined;
281
+ if (minRelevance !== undefined && (Number.isNaN(minRelevance) || minRelevance < 0 || minRelevance > 1)) {
282
+ io.stderr(`--min-relevance must be between 0 and 1.\n`);
283
+ return 2;
284
+ }
285
+ const input = {
286
+ ref: parsed.values.ref,
287
+ text: parsed.values.text,
288
+ category: parsed.values.category,
289
+ project: parsed.values.project,
290
+ limit,
291
+ minRelevance,
292
+ };
293
+ try {
294
+ if (env.json) {
295
+ const backend = createBackend(env.contextDir);
296
+ renderJson(io, await backend.findSimilar(input));
297
+ return 0;
298
+ }
299
+ const text = await findSimilar(env.contextDir, input);
300
+ io.stdout(text.endsWith('\n') ? text : text + '\n');
301
+ return 0;
302
+ }
303
+ catch (err) {
304
+ io.stderr(`${err.message}\n`);
305
+ return 1;
306
+ }
307
+ }
308
+ if (sub === 'audit') {
309
+ let parsed;
310
+ try {
311
+ parsed = parseArgs({
312
+ args: subRest,
313
+ options: {
314
+ 'stale-days': { type: 'string' },
315
+ 'similarity-threshold': { type: 'string' },
316
+ 'max-duplicates': { type: 'string' },
317
+ },
318
+ strict: true,
319
+ allowPositionals: false,
320
+ });
321
+ }
322
+ catch (err) {
323
+ io.stderr(`${err.message}\n${USAGE}`);
324
+ return 2;
325
+ }
326
+ const staleDays = parsed.values['stale-days'] !== undefined
327
+ ? Number.parseInt(parsed.values['stale-days'], 10)
328
+ : undefined;
329
+ if (staleDays !== undefined && (!Number.isInteger(staleDays) || staleDays <= 0)) {
330
+ io.stderr(`--stale-days must be a positive integer.\n`);
331
+ return 2;
332
+ }
333
+ const similarityThreshold = parsed.values['similarity-threshold'] !== undefined
334
+ ? Number.parseFloat(parsed.values['similarity-threshold'])
335
+ : undefined;
336
+ if (similarityThreshold !== undefined &&
337
+ (Number.isNaN(similarityThreshold) || similarityThreshold < 0 || similarityThreshold > 1)) {
338
+ io.stderr(`--similarity-threshold must be between 0 and 1.\n`);
339
+ return 2;
340
+ }
341
+ const maxDuplicates = parsed.values['max-duplicates'] !== undefined
342
+ ? Number.parseInt(parsed.values['max-duplicates'], 10)
343
+ : undefined;
344
+ if (maxDuplicates !== undefined && (!Number.isInteger(maxDuplicates) || maxDuplicates <= 0)) {
345
+ io.stderr(`--max-duplicates must be a positive integer.\n`);
346
+ return 2;
347
+ }
348
+ const options = { staleDays, similarityThreshold, maxDuplicates };
349
+ if (env.json) {
350
+ const backend = createBackend(env.contextDir);
351
+ renderJson(io, await backend.audit(options));
352
+ return 0;
353
+ }
354
+ const text = await memoryAudit(env.contextDir, options);
355
+ io.stdout(text.endsWith('\n') ? text : text + '\n');
356
+ return 0;
357
+ }
358
+ if (sub === 'archive') {
359
+ let parsed;
360
+ try {
361
+ parsed = parseArgs({
362
+ args: subRest,
363
+ options: {
364
+ category: { type: 'string' },
365
+ title: { type: 'string' },
366
+ note: { type: 'string' },
367
+ },
368
+ strict: true,
369
+ allowPositionals: true,
370
+ });
371
+ }
372
+ catch (err) {
373
+ io.stderr(`${err.message}\n${USAGE}`);
374
+ return 2;
375
+ }
376
+ const input = {
377
+ ref: parsed.positionals[0],
378
+ category: parsed.values.category,
379
+ title: parsed.values.title,
380
+ note: parsed.values.note,
381
+ };
382
+ if (!input.ref && !input.category) {
383
+ io.stderr(`Nothing to archive. Provide a ref or --category + --title.\n`);
384
+ return 2;
385
+ }
386
+ if (env.json) {
387
+ const backend = createBackend(env.contextDir);
388
+ renderJson(io, await backend.archive(input));
389
+ return 0;
390
+ }
391
+ const text = await archive(env.contextDir, input);
392
+ io.stdout(text.endsWith('\n') ? text : text + '\n');
393
+ return /not found/i.test(text) ? 3 : 0;
394
+ }
395
+ if (sub === 'restore') {
396
+ let parsed;
397
+ try {
398
+ parsed = parseArgs({
399
+ args: subRest,
400
+ options: {
401
+ category: { type: 'string' },
402
+ title: { type: 'string' },
403
+ },
404
+ strict: true,
405
+ allowPositionals: true,
406
+ });
407
+ }
408
+ catch (err) {
409
+ io.stderr(`${err.message}\n${USAGE}`);
410
+ return 2;
411
+ }
412
+ const input = {
413
+ ref: parsed.positionals[0],
414
+ category: parsed.values.category,
415
+ title: parsed.values.title,
416
+ };
417
+ if (!input.ref && !input.category) {
418
+ io.stderr(`Nothing to restore. Provide a ref or --category + --title.\n`);
419
+ return 2;
420
+ }
421
+ if (env.json) {
422
+ const backend = createBackend(env.contextDir);
423
+ renderJson(io, await backend.restore(input));
424
+ return 0;
425
+ }
426
+ const text = await restore(env.contextDir, input);
427
+ io.stdout(text.endsWith('\n') ? text : text + '\n');
428
+ return /not found/i.test(text) ? 3 : 0;
429
+ }
430
+ if (sub === 'propose') {
431
+ let parsed;
432
+ try {
433
+ parsed = parseArgs({
434
+ args: subRest,
435
+ options: {
436
+ category: { type: 'string' },
437
+ title: { type: 'string' },
438
+ content: { type: 'string' },
439
+ project: { type: 'string' },
440
+ ttl: { type: 'string' },
441
+ source: { type: 'string' },
442
+ },
443
+ strict: true,
444
+ allowPositionals: false,
445
+ });
446
+ }
447
+ catch (err) {
448
+ io.stderr(`${err.message}\n${USAGE}`);
449
+ return 2;
450
+ }
451
+ const { category, title, content, project, ttl, source } = parsed.values;
452
+ if (!category || !title || !content) {
453
+ io.stderr(`memory propose requires --category, --title, and --content.\n`);
454
+ return 2;
455
+ }
456
+ try {
457
+ const ref = propose(env.contextDir, { category, title, content, project, ttl, source });
458
+ if (env.json) {
459
+ renderJson(io, ref);
460
+ return 0;
461
+ }
462
+ io.stdout(`Proposal staged: #${ref.id} "${title}" — pending ratification\n`);
463
+ return 0;
464
+ }
465
+ catch (err) {
466
+ io.stderr(`${err.message}\n`);
467
+ return 1;
468
+ }
469
+ }
470
+ if (sub === 'proposals') {
471
+ if (subRest.length > 0) {
472
+ io.stderr(`memory proposals takes no positional arguments.\n${USAGE}`);
473
+ return 2;
474
+ }
475
+ const rows = listProposals(env.contextDir);
476
+ if (env.json) {
477
+ renderJson(io, rows);
478
+ return 0;
479
+ }
480
+ if (rows.length === 0) {
481
+ io.stdout(`No pending proposals.\n`);
482
+ return 0;
483
+ }
484
+ const lines = rows.map((r) => {
485
+ const src = r.source ? ` [${r.source}]` : '';
486
+ const body = (r.content ?? '').replace(/\s+/g, ' ').trim();
487
+ const clipped = body.length > 120 ? body.slice(0, 117).trimEnd() + '…' : body;
488
+ return `#${r.id} (${r.category ?? '?'})${src} ${r.title ?? '(untitled)'} — ${clipped}`;
489
+ });
490
+ io.stdout(`${rows.length} pending proposal(s):\n${lines.join('\n')}\n`);
491
+ return 0;
492
+ }
493
+ if (sub === 'ratify') {
494
+ let parsed;
495
+ try {
496
+ parsed = parseArgs({
497
+ args: subRest,
498
+ options: {
499
+ title: { type: 'string' },
500
+ content: { type: 'string' },
501
+ category: { type: 'string' },
502
+ project: { type: 'string' },
503
+ ttl: { type: 'string' },
504
+ },
505
+ strict: true,
506
+ allowPositionals: true,
507
+ });
508
+ }
509
+ catch (err) {
510
+ io.stderr(`${err.message}\n${USAGE}`);
511
+ return 2;
512
+ }
513
+ const id = Number.parseInt(parsed.positionals[0] ?? '', 10);
514
+ if (!Number.isInteger(id) || id <= 0) {
515
+ io.stderr(`memory ratify requires a positive proposal id.\n`);
516
+ return 2;
517
+ }
518
+ const overrides = {
519
+ title: parsed.values.title,
520
+ content: parsed.values.content,
521
+ category: parsed.values.category,
522
+ project: parsed.values.project,
523
+ ttl: parsed.values.ttl,
524
+ };
525
+ try {
526
+ const ref = await ratifyProposal(env.contextDir, id, overrides);
527
+ if (env.json) {
528
+ renderJson(io, ref);
529
+ return 0;
530
+ }
531
+ io.stdout(`Proposal #${id} ratified → ${ref.ref}\n`);
532
+ return 0;
533
+ }
534
+ catch (err) {
535
+ if (err instanceof UnknownProposalError) {
536
+ io.stderr(`Proposal #${id} not found.\n`);
537
+ return 3;
538
+ }
539
+ io.stderr(`Proposal #${id} refused — ${err.message}. It remains pending.\n`);
540
+ return 1;
541
+ }
542
+ }
543
+ if (sub === 'reject') {
544
+ let parsed;
545
+ try {
546
+ parsed = parseArgs({
547
+ args: subRest,
548
+ options: {},
549
+ strict: true,
550
+ allowPositionals: true,
551
+ });
552
+ }
553
+ catch (err) {
554
+ io.stderr(`${err.message}\n${USAGE}`);
555
+ return 2;
556
+ }
557
+ const id = Number.parseInt(parsed.positionals[0] ?? '', 10);
558
+ if (!Number.isInteger(id) || id <= 0) {
559
+ io.stderr(`memory reject requires a positive proposal id.\n`);
560
+ return 2;
561
+ }
562
+ const removed = rejectProposal(env.contextDir, id);
563
+ if (env.json) {
564
+ renderJson(io, { rejected: removed });
565
+ return removed ? 0 : 3;
566
+ }
567
+ if (removed) {
568
+ io.stdout(`Proposal #${id} rejected and removed.\n`);
569
+ return 0;
570
+ }
571
+ io.stderr(`Proposal #${id} not found.\n`);
572
+ return 3;
573
+ }
574
+ // prune
575
+ let parsed;
576
+ try {
577
+ parsed = parseArgs({
578
+ args: subRest,
579
+ options: {
580
+ 'stale-days': { type: 'string' },
581
+ 'dry-run': { type: 'boolean' },
582
+ },
583
+ strict: true,
584
+ allowPositionals: false,
585
+ });
586
+ }
587
+ catch (err) {
588
+ io.stderr(`${err.message}\n${USAGE}`);
589
+ return 2;
590
+ }
591
+ const staleDays = parsed.values['stale-days'] !== undefined
592
+ ? Number.parseInt(parsed.values['stale-days'], 10)
593
+ : undefined;
594
+ if (staleDays !== undefined && (!Number.isInteger(staleDays) || staleDays <= 0)) {
595
+ io.stderr(`--stale-days must be a positive integer.\n`);
596
+ return 2;
597
+ }
598
+ const dryRun = Boolean(parsed.values['dry-run']);
599
+ const options = { staleDays, dryRun };
600
+ if (env.json) {
601
+ const backend = createBackend(env.contextDir);
602
+ renderJson(io, await backend.prune(options));
603
+ return 0;
604
+ }
605
+ const text = await prune(env.contextDir, options);
606
+ io.stdout(text.endsWith('\n') ? text : text + '\n');
607
+ return 0;
608
+ }
@@ -0,0 +1,2 @@
1
+ import type { IOStreams } from './io.js';
2
+ export declare function run(argv: string[], io: IOStreams): Promise<number>;