@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.
- package/CHANGELOG.md +475 -0
- package/LICENSE +661 -0
- package/README.md +702 -0
- package/assets/demo.cast +396 -0
- package/assets/skill/SKILL.md +192 -0
- package/dist/backends/embedding-runtime.d.ts +39 -0
- package/dist/backends/embedding-runtime.js +265 -0
- package/dist/backends/episodes.d.ts +36 -0
- package/dist/backends/episodes.js +126 -0
- package/dist/backends/fastembed.d.ts +18 -0
- package/dist/backends/fastembed.js +95 -0
- package/dist/backends/glob.d.ts +10 -0
- package/dist/backends/glob.js +16 -0
- package/dist/backends/index.d.ts +6 -0
- package/dist/backends/index.js +78 -0
- package/dist/backends/migrations.d.ts +38 -0
- package/dist/backends/migrations.js +157 -0
- package/dist/backends/mmr.d.ts +45 -0
- package/dist/backends/mmr.js +82 -0
- package/dist/backends/proposals.d.ts +65 -0
- package/dist/backends/proposals.js +182 -0
- package/dist/backends/recall-log.d.ts +42 -0
- package/dist/backends/recall-log.js +143 -0
- package/dist/backends/retry.d.ts +29 -0
- package/dist/backends/retry.js +60 -0
- package/dist/backends/salience.d.ts +57 -0
- package/dist/backends/salience.js +186 -0
- package/dist/backends/sqlite-knowledge.d.ts +37 -0
- package/dist/backends/sqlite-knowledge.js +788 -0
- package/dist/backends/sqlite-vec.d.ts +64 -0
- package/dist/backends/sqlite-vec.js +737 -0
- package/dist/backends/ttl.d.ts +24 -0
- package/dist/backends/ttl.js +50 -0
- package/dist/backends/types.d.ts +562 -0
- package/dist/backends/types.js +8 -0
- package/dist/blocks/harness.d.ts +45 -0
- package/dist/blocks/harness.js +142 -0
- package/dist/blocks/model.d.ts +4 -0
- package/dist/blocks/model.js +56 -0
- package/dist/blocks/types.d.ts +40 -0
- package/dist/blocks/types.js +34 -0
- package/dist/categories.d.ts +16 -0
- package/dist/categories.js +29 -0
- package/dist/cli/args.d.ts +21 -0
- package/dist/cli/args.js +47 -0
- package/dist/cli/bootstrap.d.ts +2 -0
- package/dist/cli/bootstrap.js +214 -0
- package/dist/cli/doctor.d.ts +2 -0
- package/dist/cli/doctor.js +206 -0
- package/dist/cli/forget.d.ts +2 -0
- package/dist/cli/forget.js +97 -0
- package/dist/cli/harness.d.ts +2 -0
- package/dist/cli/harness.js +91 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +127 -0
- package/dist/cli/inject.d.ts +2 -0
- package/dist/cli/inject.js +298 -0
- package/dist/cli/install.d.ts +2 -0
- package/dist/cli/install.js +137 -0
- package/dist/cli/io.d.ts +14 -0
- package/dist/cli/io.js +63 -0
- package/dist/cli/knowledge.d.ts +2 -0
- package/dist/cli/knowledge.js +216 -0
- package/dist/cli/memory.d.ts +2 -0
- package/dist/cli/memory.js +608 -0
- package/dist/cli/migrate.d.ts +2 -0
- package/dist/cli/migrate.js +116 -0
- package/dist/cli/recall.d.ts +2 -0
- package/dist/cli/recall.js +91 -0
- package/dist/cli/remember.d.ts +2 -0
- package/dist/cli/remember.js +111 -0
- package/dist/cli/serve.d.ts +2 -0
- package/dist/cli/serve.js +71 -0
- package/dist/cli/subcommands.d.ts +7 -0
- package/dist/cli/subcommands.js +11 -0
- package/dist/cli/tui/multi-select.d.ts +53 -0
- package/dist/cli/tui/multi-select.js +116 -0
- package/dist/cli/update-identity.d.ts +2 -0
- package/dist/cli/update-identity.js +90 -0
- package/dist/cli/update.d.ts +2 -0
- package/dist/cli/update.js +87 -0
- package/dist/cli/wake.d.ts +2 -0
- package/dist/cli/wake.js +53 -0
- package/dist/clients.d.ts +8 -0
- package/dist/clients.js +63 -0
- package/dist/config.d.ts +59 -0
- package/dist/config.js +160 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +75 -0
- package/dist/injection/harnesses.d.ts +17 -0
- package/dist/injection/harnesses.js +51 -0
- package/dist/injection/render.d.ts +17 -0
- package/dist/injection/render.js +50 -0
- package/dist/injection/writer.d.ts +25 -0
- package/dist/injection/writer.js +118 -0
- package/dist/install/harnesses.d.ts +26 -0
- package/dist/install/harnesses.js +90 -0
- package/dist/install/names.d.ts +16 -0
- package/dist/install/names.js +36 -0
- package/dist/install/render.d.ts +18 -0
- package/dist/install/render.js +42 -0
- package/dist/install/skill-source.d.ts +1 -0
- package/dist/install/skill-source.js +11 -0
- package/dist/path-safety.d.ts +19 -0
- package/dist/path-safety.js +62 -0
- package/dist/server.d.ts +8 -0
- package/dist/server.js +571 -0
- package/dist/tools/append-maintenance-log.d.ts +52 -0
- package/dist/tools/append-maintenance-log.js +103 -0
- package/dist/tools/archive.d.ts +2 -0
- package/dist/tools/archive.js +21 -0
- package/dist/tools/bootstrap.d.ts +11 -0
- package/dist/tools/bootstrap.js +161 -0
- package/dist/tools/dossier.d.ts +1 -0
- package/dist/tools/dossier.js +102 -0
- package/dist/tools/eurorack-import.d.ts +47 -0
- package/dist/tools/eurorack-import.js +199 -0
- package/dist/tools/find-similar.d.ts +3 -0
- package/dist/tools/find-similar.js +26 -0
- package/dist/tools/forget.d.ts +6 -0
- package/dist/tools/forget.js +70 -0
- package/dist/tools/harness.d.ts +18 -0
- package/dist/tools/harness.js +54 -0
- package/dist/tools/identity-scaffold.d.ts +22 -0
- package/dist/tools/identity-scaffold.js +142 -0
- package/dist/tools/identity.d.ts +4 -0
- package/dist/tools/identity.js +226 -0
- package/dist/tools/knowledge-archive.d.ts +5 -0
- package/dist/tools/knowledge-archive.js +24 -0
- package/dist/tools/knowledge-history.d.ts +6 -0
- package/dist/tools/knowledge-history.js +55 -0
- package/dist/tools/knowledge-maintain.d.ts +9 -0
- package/dist/tools/knowledge-maintain.js +113 -0
- package/dist/tools/knowledge-merge.d.ts +8 -0
- package/dist/tools/knowledge-merge.js +40 -0
- package/dist/tools/knowledge-move.d.ts +10 -0
- package/dist/tools/knowledge-move.js +48 -0
- package/dist/tools/knowledge-purge.d.ts +5 -0
- package/dist/tools/knowledge-purge.js +20 -0
- package/dist/tools/knowledge-recall.d.ts +20 -0
- package/dist/tools/knowledge-recall.js +142 -0
- package/dist/tools/knowledge-restore.d.ts +4 -0
- package/dist/tools/knowledge-restore.js +24 -0
- package/dist/tools/knowledge-supersede.d.ts +6 -0
- package/dist/tools/knowledge-supersede.js +36 -0
- package/dist/tools/knowledge-verify.d.ts +8 -0
- package/dist/tools/knowledge-verify.js +39 -0
- package/dist/tools/knowledge-write.d.ts +25 -0
- package/dist/tools/knowledge-write.js +87 -0
- package/dist/tools/memory-audit.d.ts +3 -0
- package/dist/tools/memory-audit.js +47 -0
- package/dist/tools/memory-list.d.ts +4 -0
- package/dist/tools/memory-list.js +23 -0
- package/dist/tools/prune.d.ts +4 -0
- package/dist/tools/prune.js +28 -0
- package/dist/tools/recall.d.ts +4 -0
- package/dist/tools/recall.js +24 -0
- package/dist/tools/remember.d.ts +10 -0
- package/dist/tools/remember.js +43 -0
- package/dist/tools/restore.d.ts +2 -0
- package/dist/tools/restore.js +20 -0
- package/dist/tools/update-identity.d.ts +34 -0
- package/dist/tools/update-identity.js +194 -0
- package/dist/tools/update.d.ts +2 -0
- package/dist/tools/update.js +16 -0
- package/dist/transport/guards.d.ts +35 -0
- package/dist/transport/guards.js +107 -0
- package/dist/transport/http-server.d.ts +17 -0
- package/dist/transport/http-server.js +198 -0
- package/dist/transport/mcp-app-hello.d.ts +3 -0
- package/dist/transport/mcp-app-hello.js +66 -0
- package/dist/widgets/display-modes.html +131 -0
- package/dist/widgets/hello.html +138 -0
- package/dist/widgets/proposals.html +131 -0
- package/dist/widgets/top-of-mind.html +131 -0
- 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
|
+
}
|