quilltap 4.5.0-dev → 4.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/README.md +178 -0
- package/bin/quilltap.js +226 -20
- package/lib/completion/bash.template +121 -0
- package/lib/completion/fish.template +93 -0
- package/lib/completion/zsh.template +209 -0
- package/lib/completion-commands.js +77 -0
- package/lib/db-commands.js +1142 -0
- package/lib/db-helpers.js +173 -4
- package/lib/docs-commands.js +2157 -172
- package/lib/graph-integrity.js +105 -0
- package/lib/instances-commands.js +342 -0
- package/lib/instances.js +335 -0
- package/lib/lock-helpers.js +117 -0
- package/lib/logs-commands.js +383 -0
- package/lib/memories-commands.js +1374 -0
- package/lib/memory-diff-command.js +19 -3
- package/lib/migrations-commands.js +324 -0
- package/lib/theme-commands.js +18 -0
- package/package.json +1 -1
|
@@ -0,0 +1,1374 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const {
|
|
4
|
+
resolveDataDirAndPassphrase,
|
|
5
|
+
printDefaultInstanceHint,
|
|
6
|
+
loadDbKey,
|
|
7
|
+
openMainDb,
|
|
8
|
+
UUID_RE,
|
|
9
|
+
resolveCharacter,
|
|
10
|
+
resolveChat,
|
|
11
|
+
resolveProject,
|
|
12
|
+
} = require('./db-helpers');
|
|
13
|
+
const { scanDanglingEdges } = require('./graph-integrity');
|
|
14
|
+
|
|
15
|
+
// ---------- colour & marker helpers ----------
|
|
16
|
+
|
|
17
|
+
const RESET = '\x1b[0m';
|
|
18
|
+
const BOLD = '\x1b[1m';
|
|
19
|
+
const DIM = '\x1b[2m';
|
|
20
|
+
const GREEN = '\x1b[32m';
|
|
21
|
+
const RED = '\x1b[31m';
|
|
22
|
+
const YELLOW = '\x1b[33m';
|
|
23
|
+
const CYAN = '\x1b[36m';
|
|
24
|
+
|
|
25
|
+
function isTty() {
|
|
26
|
+
return Boolean(process.stdout.isTTY);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function colorize(text, color) {
|
|
30
|
+
if (!isTty()) return text;
|
|
31
|
+
return `${color}${text}${RESET}`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function importanceColor(v) {
|
|
35
|
+
if (v == null) return DIM;
|
|
36
|
+
if (v >= 0.7) return GREEN;
|
|
37
|
+
if (v >= 0.4) return YELLOW;
|
|
38
|
+
return DIM + RED;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function formatImportance(v) {
|
|
42
|
+
if (v == null) return ' -';
|
|
43
|
+
const s = Number(v).toFixed(2);
|
|
44
|
+
return s.padStart(4);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// ---------- argument parsing ----------
|
|
48
|
+
|
|
49
|
+
const SORT_FIELDS = new Set([
|
|
50
|
+
'reinforced', 'importance', 'created', 'accessed', 'reinforcement-count', 'links',
|
|
51
|
+
]);
|
|
52
|
+
|
|
53
|
+
function parseFlags(args) {
|
|
54
|
+
const flags = {
|
|
55
|
+
// globals
|
|
56
|
+
dataDir: '',
|
|
57
|
+
instance: '',
|
|
58
|
+
passphrase: '',
|
|
59
|
+
json: false,
|
|
60
|
+
help: false,
|
|
61
|
+
// shared filters
|
|
62
|
+
character: '',
|
|
63
|
+
about: '',
|
|
64
|
+
source: '',
|
|
65
|
+
chat: '',
|
|
66
|
+
project: '',
|
|
67
|
+
since: '',
|
|
68
|
+
until: '',
|
|
69
|
+
minImportance: null,
|
|
70
|
+
minReinforced: null,
|
|
71
|
+
hasEmbedding: null, // null = unset, true / false otherwise
|
|
72
|
+
// ls / find / grep
|
|
73
|
+
sort: '',
|
|
74
|
+
reverse: false,
|
|
75
|
+
limit: 0,
|
|
76
|
+
fullTitles: false,
|
|
77
|
+
// find
|
|
78
|
+
findIn: '',
|
|
79
|
+
// grep
|
|
80
|
+
ignoreCase: false,
|
|
81
|
+
pathsOnly: false,
|
|
82
|
+
max: 0,
|
|
83
|
+
context: 0,
|
|
84
|
+
// show / tree
|
|
85
|
+
depth: -1,
|
|
86
|
+
maxNodes: 0,
|
|
87
|
+
noRelated: false,
|
|
88
|
+
// validate
|
|
89
|
+
list: false,
|
|
90
|
+
// semantic search
|
|
91
|
+
semantic: false,
|
|
92
|
+
top: 0,
|
|
93
|
+
threshold: -1,
|
|
94
|
+
port: 3000,
|
|
95
|
+
};
|
|
96
|
+
const positional = [];
|
|
97
|
+
let i = 0;
|
|
98
|
+
while (i < args.length) {
|
|
99
|
+
const a = args[i];
|
|
100
|
+
switch (a) {
|
|
101
|
+
case '-d': case '--data-dir': flags.dataDir = args[++i]; break;
|
|
102
|
+
case '--instance': flags.instance = args[++i]; break;
|
|
103
|
+
case '--passphrase': flags.passphrase = args[++i]; break;
|
|
104
|
+
case '--json': flags.json = true; break;
|
|
105
|
+
case '-h': case '--help': flags.help = true; break;
|
|
106
|
+
|
|
107
|
+
case '--character': flags.character = args[++i]; break;
|
|
108
|
+
case '--about': flags.about = args[++i]; break;
|
|
109
|
+
case '--source': flags.source = args[++i]; break;
|
|
110
|
+
case '--chat': flags.chat = args[++i]; break;
|
|
111
|
+
case '--project': flags.project = args[++i]; break;
|
|
112
|
+
case '--since': flags.since = args[++i]; break;
|
|
113
|
+
case '--until': flags.until = args[++i]; break;
|
|
114
|
+
case '--min-importance': flags.minImportance = parseFloat(args[++i]); break;
|
|
115
|
+
case '--min-reinforced': flags.minReinforced = parseFloat(args[++i]); break;
|
|
116
|
+
case '--has-embedding': flags.hasEmbedding = true; break;
|
|
117
|
+
case '--no-embedding': flags.hasEmbedding = false; break;
|
|
118
|
+
|
|
119
|
+
case '--sort': flags.sort = args[++i]; break;
|
|
120
|
+
case '-r': case '--reverse': flags.reverse = true; break;
|
|
121
|
+
case '--limit': flags.limit = parseInt(args[++i], 10) || 0; break;
|
|
122
|
+
case '--full-titles': flags.fullTitles = true; break;
|
|
123
|
+
|
|
124
|
+
case '--in': flags.findIn = args[++i]; break;
|
|
125
|
+
|
|
126
|
+
case '-i': case '--ignore-case': flags.ignoreCase = true; break;
|
|
127
|
+
case '-l': case '--paths-only': flags.pathsOnly = true; break;
|
|
128
|
+
case '--max': flags.max = parseInt(args[++i], 10) || 0; break;
|
|
129
|
+
case '--context': flags.context = parseInt(args[++i], 10) || 0; break;
|
|
130
|
+
|
|
131
|
+
case '--depth': flags.depth = parseInt(args[++i], 10); break;
|
|
132
|
+
case '--max-nodes': flags.maxNodes = parseInt(args[++i], 10) || 0; break;
|
|
133
|
+
case '--no-related': flags.noRelated = true; break;
|
|
134
|
+
|
|
135
|
+
case '--list': flags.list = true; break;
|
|
136
|
+
|
|
137
|
+
case '--semantic': flags.semantic = true; break;
|
|
138
|
+
case '--top': flags.top = parseInt(args[++i], 10) || 0; break;
|
|
139
|
+
case '--threshold': {
|
|
140
|
+
const v = parseFloat(args[++i]);
|
|
141
|
+
if (isNaN(v) || v < 0 || v > 1) {
|
|
142
|
+
console.error('Error: --threshold must be a number between 0 and 1');
|
|
143
|
+
process.exit(1);
|
|
144
|
+
}
|
|
145
|
+
flags.threshold = v;
|
|
146
|
+
break;
|
|
147
|
+
}
|
|
148
|
+
case '--port': {
|
|
149
|
+
const p = parseInt(args[++i], 10);
|
|
150
|
+
if (isNaN(p) || p < 1 || p > 65535) {
|
|
151
|
+
console.error('Error: --port must be between 1 and 65535');
|
|
152
|
+
process.exit(1);
|
|
153
|
+
}
|
|
154
|
+
flags.port = p;
|
|
155
|
+
break;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
default:
|
|
159
|
+
if (a.startsWith('-')) {
|
|
160
|
+
console.error(`Unknown option: ${a}`);
|
|
161
|
+
process.exit(1);
|
|
162
|
+
}
|
|
163
|
+
positional.push(a);
|
|
164
|
+
}
|
|
165
|
+
i++;
|
|
166
|
+
}
|
|
167
|
+
return { flags, positional };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// ---------- db open ----------
|
|
171
|
+
|
|
172
|
+
async function openDb(flags) {
|
|
173
|
+
const resolved = resolveDataDirAndPassphrase({
|
|
174
|
+
dataDir: flags.dataDir,
|
|
175
|
+
instance: flags.instance,
|
|
176
|
+
passphrase: flags.passphrase,
|
|
177
|
+
});
|
|
178
|
+
printDefaultInstanceHint(resolved);
|
|
179
|
+
const { dataDir, passphrase } = resolved;
|
|
180
|
+
const pepper = await loadDbKey(dataDir, passphrase);
|
|
181
|
+
const db = openMainDb(dataDir, pepper, { readonly: true });
|
|
182
|
+
return { db, dataDir };
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// ---------- filter / sort builders ----------
|
|
186
|
+
|
|
187
|
+
// Build a SQL WHERE clause + parameters from parsed flags. Returns
|
|
188
|
+
// `{ where: 'WHERE m.x = ? AND ...', params: [...], meta: { characterId, ... } }`.
|
|
189
|
+
// `meta` exposes resolved IDs so callers can decide e.g. whether to show a
|
|
190
|
+
// per-row holder column.
|
|
191
|
+
function buildWhereClause(db, flags) {
|
|
192
|
+
const clauses = [];
|
|
193
|
+
const params = [];
|
|
194
|
+
const meta = { characterId: null, aboutId: null, chatId: null, projectId: null, allCharacters: true };
|
|
195
|
+
|
|
196
|
+
if (flags.character && flags.character !== 'all') {
|
|
197
|
+
const c = resolveCharacter(db, flags.character);
|
|
198
|
+
clauses.push('m.characterId = ?');
|
|
199
|
+
params.push(c.id);
|
|
200
|
+
meta.characterId = c.id;
|
|
201
|
+
meta.allCharacters = false;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (flags.about) {
|
|
205
|
+
if (flags.about === 'self') {
|
|
206
|
+
clauses.push('m.aboutCharacterId = m.characterId');
|
|
207
|
+
} else if (flags.about === 'none') {
|
|
208
|
+
clauses.push('m.aboutCharacterId IS NULL');
|
|
209
|
+
} else {
|
|
210
|
+
const a = resolveCharacter(db, flags.about);
|
|
211
|
+
clauses.push('m.aboutCharacterId = ?');
|
|
212
|
+
params.push(a.id);
|
|
213
|
+
meta.aboutId = a.id;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (flags.source) {
|
|
218
|
+
const s = String(flags.source).toUpperCase();
|
|
219
|
+
if (s !== 'AUTO' && s !== 'MANUAL') {
|
|
220
|
+
throw new Error(`--source must be AUTO or MANUAL (got '${flags.source}')`);
|
|
221
|
+
}
|
|
222
|
+
clauses.push('m.source = ?');
|
|
223
|
+
params.push(s);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (flags.chat) {
|
|
227
|
+
if (flags.chat === 'none') {
|
|
228
|
+
clauses.push('m.chatId IS NULL');
|
|
229
|
+
} else {
|
|
230
|
+
const ch = resolveChat(db, flags.chat);
|
|
231
|
+
clauses.push('m.chatId = ?');
|
|
232
|
+
params.push(ch.id);
|
|
233
|
+
meta.chatId = ch.id;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if (flags.project) {
|
|
238
|
+
const p = resolveProject(db, flags.project);
|
|
239
|
+
clauses.push('m.projectId = ?');
|
|
240
|
+
params.push(p.id);
|
|
241
|
+
meta.projectId = p.id;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (flags.since) {
|
|
245
|
+
clauses.push('m.createdAt >= ?');
|
|
246
|
+
params.push(flags.since);
|
|
247
|
+
}
|
|
248
|
+
if (flags.until) {
|
|
249
|
+
clauses.push('m.createdAt <= ?');
|
|
250
|
+
params.push(flags.until);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if (flags.minImportance != null && !Number.isNaN(flags.minImportance)) {
|
|
254
|
+
clauses.push('m.importance >= ?');
|
|
255
|
+
params.push(flags.minImportance);
|
|
256
|
+
}
|
|
257
|
+
if (flags.minReinforced != null && !Number.isNaN(flags.minReinforced)) {
|
|
258
|
+
clauses.push('m.reinforcedImportance >= ?');
|
|
259
|
+
params.push(flags.minReinforced);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
if (flags.hasEmbedding === true) {
|
|
263
|
+
clauses.push('m.embedding IS NOT NULL');
|
|
264
|
+
} else if (flags.hasEmbedding === false) {
|
|
265
|
+
clauses.push('m.embedding IS NULL');
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const where = clauses.length ? `WHERE ${clauses.join(' AND ')}` : '';
|
|
269
|
+
return { where, params, meta };
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function buildOrderBy(sortFlag, reverse) {
|
|
273
|
+
// SQL ORDER BY fragment + which column the rendered `imp` field reflects.
|
|
274
|
+
// Default is `reinforced`, which is what the recall path uses.
|
|
275
|
+
const field = sortFlag || 'reinforced';
|
|
276
|
+
let order;
|
|
277
|
+
let impField = 'reinforcedImportance';
|
|
278
|
+
switch (field) {
|
|
279
|
+
case 'reinforced':
|
|
280
|
+
order = 'm.reinforcedImportance DESC, m.createdAt DESC';
|
|
281
|
+
break;
|
|
282
|
+
case 'importance':
|
|
283
|
+
order = 'm.importance DESC, m.createdAt DESC';
|
|
284
|
+
impField = 'importance';
|
|
285
|
+
break;
|
|
286
|
+
case 'created':
|
|
287
|
+
order = 'm.createdAt DESC';
|
|
288
|
+
break;
|
|
289
|
+
case 'accessed':
|
|
290
|
+
order = 'COALESCE(m.lastAccessedAt, m.createdAt) DESC';
|
|
291
|
+
break;
|
|
292
|
+
case 'reinforcement-count':
|
|
293
|
+
order = 'm.reinforcementCount DESC, m.reinforcedImportance DESC';
|
|
294
|
+
break;
|
|
295
|
+
case 'links':
|
|
296
|
+
// SQLite's json_array_length tolerates NULL / empty arrays.
|
|
297
|
+
order = "json_array_length(COALESCE(m.relatedMemoryIds, '[]')) DESC, m.reinforcedImportance DESC";
|
|
298
|
+
break;
|
|
299
|
+
default:
|
|
300
|
+
throw new Error(`Unknown --sort field '${field}'. Valid: ${[...SORT_FIELDS].join(', ')}`);
|
|
301
|
+
}
|
|
302
|
+
if (reverse) {
|
|
303
|
+
// Flip every DESC→ASC and ASC→DESC in the fragment.
|
|
304
|
+
order = order.replace(/\bDESC\b/g, '__ASC__').replace(/\bASC\b/g, 'DESC').replace(/__ASC__/g, 'ASC');
|
|
305
|
+
}
|
|
306
|
+
return { order, impField };
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// SELECT fragment that pulls every column the renderers need, including the
|
|
310
|
+
// joined-in character / chat names. `m.*` is followed by aliases for the joins
|
|
311
|
+
// so callers don't have to re-resolve UUIDs.
|
|
312
|
+
const SELECT_BASE = `
|
|
313
|
+
SELECT
|
|
314
|
+
m.id,
|
|
315
|
+
m.characterId,
|
|
316
|
+
m.aboutCharacterId,
|
|
317
|
+
m.chatId,
|
|
318
|
+
m.projectId,
|
|
319
|
+
m.sourceMessageId,
|
|
320
|
+
m.content,
|
|
321
|
+
m.summary,
|
|
322
|
+
m.keywords,
|
|
323
|
+
m.tags,
|
|
324
|
+
m.importance,
|
|
325
|
+
m.reinforcedImportance,
|
|
326
|
+
m.reinforcementCount,
|
|
327
|
+
m.lastReinforcedAt,
|
|
328
|
+
m.lastAccessedAt,
|
|
329
|
+
m.source,
|
|
330
|
+
m.relatedMemoryIds,
|
|
331
|
+
m.createdAt,
|
|
332
|
+
m.updatedAt,
|
|
333
|
+
CASE WHEN m.embedding IS NULL THEN 0 ELSE 1 END AS hasEmbedding,
|
|
334
|
+
holder.name AS holderName,
|
|
335
|
+
aboutChar.name AS aboutCharName,
|
|
336
|
+
chats.title AS chatTitle,
|
|
337
|
+
projects.name AS projectName
|
|
338
|
+
FROM memories m
|
|
339
|
+
LEFT JOIN characters holder ON holder.id = m.characterId
|
|
340
|
+
LEFT JOIN characters aboutChar ON aboutChar.id = m.aboutCharacterId
|
|
341
|
+
LEFT JOIN chats ON chats.id = m.chatId
|
|
342
|
+
LEFT JOIN projects ON projects.id = m.projectId
|
|
343
|
+
`;
|
|
344
|
+
|
|
345
|
+
// ---------- output helpers ----------
|
|
346
|
+
|
|
347
|
+
function truncate(str, n) {
|
|
348
|
+
if (str == null) return '';
|
|
349
|
+
const s = String(str);
|
|
350
|
+
if (s.length <= n) return s;
|
|
351
|
+
return s.slice(0, n - 1) + '…';
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function aboutLabel(row) {
|
|
355
|
+
if (row.aboutCharacterId == null) return '(none)';
|
|
356
|
+
if (row.aboutCharacterId === row.characterId) return 'self';
|
|
357
|
+
return row.aboutCharName || '(unknown)';
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function chatLabel(row, fullTitles) {
|
|
361
|
+
if (row.chatId == null) return '(manual entry)';
|
|
362
|
+
const t = row.chatTitle || '(untitled)';
|
|
363
|
+
return fullTitles ? t : truncate(t, 32);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function linkCount(row) {
|
|
367
|
+
if (!row.relatedMemoryIds) return 0;
|
|
368
|
+
try {
|
|
369
|
+
const arr = JSON.parse(row.relatedMemoryIds);
|
|
370
|
+
return Array.isArray(arr) ? arr.length : 0;
|
|
371
|
+
} catch {
|
|
372
|
+
return 0;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function shortId(id) {
|
|
377
|
+
return id ? id.slice(0, 8) : '';
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function summaryWidth() {
|
|
381
|
+
const cols = process.stdout.columns || 120;
|
|
382
|
+
// Reserve enough for the other columns; let summary take the rest.
|
|
383
|
+
// Minimum 30, maximum 120.
|
|
384
|
+
const reserved = 80;
|
|
385
|
+
const w = cols - reserved;
|
|
386
|
+
if (w < 30) return 30;
|
|
387
|
+
if (w > 120) return 120;
|
|
388
|
+
return w;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function renderJson(obj) {
|
|
392
|
+
process.stdout.write(JSON.stringify(obj, null, 2) + '\n');
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// ---------- ls ----------
|
|
396
|
+
|
|
397
|
+
async function cmdLs(flags) {
|
|
398
|
+
const { db } = await openDb(flags);
|
|
399
|
+
try {
|
|
400
|
+
const { where, params, meta } = buildWhereClause(db, flags);
|
|
401
|
+
const { order, impField } = buildOrderBy(flags.sort, flags.reverse);
|
|
402
|
+
const limit = flags.limit > 0 ? flags.limit : 50;
|
|
403
|
+
const sql = `${SELECT_BASE} ${where} ORDER BY ${order} LIMIT ?`;
|
|
404
|
+
const rows = db.prepare(sql).all(...params, limit);
|
|
405
|
+
|
|
406
|
+
if (flags.json) {
|
|
407
|
+
renderJson({
|
|
408
|
+
sort: impField,
|
|
409
|
+
count: rows.length,
|
|
410
|
+
memories: rows.map(rowToJson),
|
|
411
|
+
});
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
renderTable(rows, { showHolder: meta.allCharacters, impField, fullTitles: flags.fullTitles });
|
|
416
|
+
} finally {
|
|
417
|
+
db.close();
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
function rowToJson(row) {
|
|
422
|
+
let keywords = [];
|
|
423
|
+
let tags = [];
|
|
424
|
+
let relatedMemoryIds = [];
|
|
425
|
+
try { keywords = JSON.parse(row.keywords || '[]'); } catch { /* ignore */ }
|
|
426
|
+
try { tags = JSON.parse(row.tags || '[]'); } catch { /* ignore */ }
|
|
427
|
+
try { relatedMemoryIds = JSON.parse(row.relatedMemoryIds || '[]'); } catch { /* ignore */ }
|
|
428
|
+
return {
|
|
429
|
+
id: row.id,
|
|
430
|
+
characterId: row.characterId,
|
|
431
|
+
holder: row.holderName,
|
|
432
|
+
aboutCharacterId: row.aboutCharacterId,
|
|
433
|
+
aboutCharacter: row.aboutCharName,
|
|
434
|
+
chatId: row.chatId,
|
|
435
|
+
chatTitle: row.chatTitle,
|
|
436
|
+
projectId: row.projectId,
|
|
437
|
+
projectName: row.projectName,
|
|
438
|
+
sourceMessageId: row.sourceMessageId,
|
|
439
|
+
source: row.source,
|
|
440
|
+
importance: row.importance,
|
|
441
|
+
reinforcedImportance: row.reinforcedImportance,
|
|
442
|
+
reinforcementCount: row.reinforcementCount,
|
|
443
|
+
lastReinforcedAt: row.lastReinforcedAt,
|
|
444
|
+
lastAccessedAt: row.lastAccessedAt,
|
|
445
|
+
hasEmbedding: Boolean(row.hasEmbedding),
|
|
446
|
+
keywords,
|
|
447
|
+
tags,
|
|
448
|
+
relatedMemoryIds,
|
|
449
|
+
summary: row.summary,
|
|
450
|
+
content: row.content,
|
|
451
|
+
createdAt: row.createdAt,
|
|
452
|
+
updatedAt: row.updatedAt,
|
|
453
|
+
};
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
function renderTable(rows, { showHolder, impField, fullTitles }) {
|
|
457
|
+
if (rows.length === 0) {
|
|
458
|
+
console.log('(no memories matched the filters)');
|
|
459
|
+
return;
|
|
460
|
+
}
|
|
461
|
+
const impHeader = impField === 'importance' ? 'imp' : 'imp';
|
|
462
|
+
const sumW = summaryWidth();
|
|
463
|
+
|
|
464
|
+
const header = [
|
|
465
|
+
showHolder ? 'holder'.padEnd(14) : null,
|
|
466
|
+
impHeader.padStart(4),
|
|
467
|
+
'rein'.padStart(4),
|
|
468
|
+
'src'.padEnd(6),
|
|
469
|
+
'about'.padEnd(20),
|
|
470
|
+
'chat'.padEnd(fullTitles ? 24 : 32),
|
|
471
|
+
'links'.padStart(5),
|
|
472
|
+
'emb',
|
|
473
|
+
'summary',
|
|
474
|
+
].filter(Boolean).join(' ');
|
|
475
|
+
console.log(colorize(header, DIM));
|
|
476
|
+
console.log(colorize(header.replace(/[^\s]/g, '-'), DIM));
|
|
477
|
+
|
|
478
|
+
for (const row of rows) {
|
|
479
|
+
const impVal = impField === 'importance' ? row.importance : row.reinforcedImportance;
|
|
480
|
+
const impStr = colorize(formatImportance(impVal), importanceColor(impVal));
|
|
481
|
+
const rein = String(row.reinforcementCount ?? 0).padStart(4);
|
|
482
|
+
const src = (row.source || '').padEnd(6);
|
|
483
|
+
const about = truncate(aboutLabel(row), 20).padEnd(20);
|
|
484
|
+
const chat = truncate(chatLabel(row, fullTitles), fullTitles ? 24 : 32).padEnd(fullTitles ? 24 : 32);
|
|
485
|
+
const links = String(linkCount(row)).padStart(5);
|
|
486
|
+
const emb = row.hasEmbedding ? 'Y' : '-';
|
|
487
|
+
const summary = truncate(row.summary || '', sumW);
|
|
488
|
+
|
|
489
|
+
const cells = [
|
|
490
|
+
showHolder ? truncate(row.holderName || '(?)', 14).padEnd(14) : null,
|
|
491
|
+
impStr,
|
|
492
|
+
rein,
|
|
493
|
+
src,
|
|
494
|
+
about,
|
|
495
|
+
chat,
|
|
496
|
+
links,
|
|
497
|
+
emb,
|
|
498
|
+
summary,
|
|
499
|
+
].filter(c => c !== null);
|
|
500
|
+
console.log(cells.join(' '));
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// ---------- find ----------
|
|
505
|
+
|
|
506
|
+
async function cmdFind(flags, positional) {
|
|
507
|
+
const pattern = positional[0];
|
|
508
|
+
if (!pattern) {
|
|
509
|
+
throw new Error('memories find: missing <pattern>. Usage: quilltap memories find [filters] <pattern>');
|
|
510
|
+
}
|
|
511
|
+
const inWhere = flags.findIn || 'summary';
|
|
512
|
+
if (!['summary', 'content', 'both'].includes(inWhere)) {
|
|
513
|
+
throw new Error(`--in must be one of: summary, content, both (got '${inWhere}')`);
|
|
514
|
+
}
|
|
515
|
+
const { db } = await openDb(flags);
|
|
516
|
+
try {
|
|
517
|
+
const { where, params, meta } = buildWhereClause(db, flags);
|
|
518
|
+
const like = `%${pattern}%`;
|
|
519
|
+
|
|
520
|
+
const matchClauses = [];
|
|
521
|
+
const matchParams = [];
|
|
522
|
+
if (inWhere === 'summary' || inWhere === 'both') {
|
|
523
|
+
matchClauses.push('m.summary LIKE ?');
|
|
524
|
+
matchParams.push(like);
|
|
525
|
+
}
|
|
526
|
+
if (inWhere === 'content' || inWhere === 'both') {
|
|
527
|
+
matchClauses.push('m.content LIKE ?');
|
|
528
|
+
matchParams.push(like);
|
|
529
|
+
}
|
|
530
|
+
const matchSql = `(${matchClauses.join(' OR ')})`;
|
|
531
|
+
|
|
532
|
+
let order;
|
|
533
|
+
let orderParams = [];
|
|
534
|
+
let impField = 'reinforcedImportance';
|
|
535
|
+
if (flags.sort) {
|
|
536
|
+
const ob = buildOrderBy(flags.sort, flags.reverse);
|
|
537
|
+
order = ob.order;
|
|
538
|
+
impField = ob.impField;
|
|
539
|
+
} else {
|
|
540
|
+
// Relevance ranking: summary-hit > content-only-hit > then reinforced + recency.
|
|
541
|
+
order = `CASE WHEN m.summary LIKE ? THEN 1 ELSE 2 END,
|
|
542
|
+
m.reinforcedImportance DESC,
|
|
543
|
+
m.createdAt DESC`;
|
|
544
|
+
orderParams = [like];
|
|
545
|
+
if (flags.reverse) {
|
|
546
|
+
// Reverse the relevance dimension by inverting the CASE outcome.
|
|
547
|
+
order = `CASE WHEN m.summary LIKE ? THEN 2 ELSE 1 END,
|
|
548
|
+
m.reinforcedImportance ASC,
|
|
549
|
+
m.createdAt ASC`;
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
const limit = flags.limit > 0 ? flags.limit : 50;
|
|
554
|
+
const fullWhere = where ? `${where} AND ${matchSql}` : `WHERE ${matchSql}`;
|
|
555
|
+
const sql = `${SELECT_BASE} ${fullWhere} ORDER BY ${order} LIMIT ?`;
|
|
556
|
+
const allParams = [...params, ...matchParams, ...orderParams, limit];
|
|
557
|
+
const rows = db.prepare(sql).all(...allParams);
|
|
558
|
+
|
|
559
|
+
if (flags.json) {
|
|
560
|
+
renderJson({
|
|
561
|
+
pattern,
|
|
562
|
+
in: inWhere,
|
|
563
|
+
count: rows.length,
|
|
564
|
+
memories: rows.map(rowToJson),
|
|
565
|
+
});
|
|
566
|
+
return;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
renderTable(rows, { showHolder: meta.allCharacters, impField, fullTitles: flags.fullTitles });
|
|
570
|
+
} finally {
|
|
571
|
+
db.close();
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
// ---------- grep ----------
|
|
576
|
+
|
|
577
|
+
async function cmdSemanticGrep(flags, query) {
|
|
578
|
+
if (!flags.character || flags.character === 'all') {
|
|
579
|
+
console.error('Error: memories grep --semantic requires --character <name|id>.');
|
|
580
|
+
console.error('The server search endpoint scopes results to one holder at a time.');
|
|
581
|
+
process.exit(1);
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
// Resolve character locally so the server gets a stable UUID.
|
|
585
|
+
let characterId;
|
|
586
|
+
{
|
|
587
|
+
const { db } = await openDb(flags);
|
|
588
|
+
try {
|
|
589
|
+
const resolved = resolveCharacter(db, flags.character);
|
|
590
|
+
characterId = resolved.id;
|
|
591
|
+
} finally {
|
|
592
|
+
db.close();
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
// Sanity-check: the server validates `characterId` as a UUID. If the local
|
|
596
|
+
// resolution returned something else (shouldn't happen), bail early with a
|
|
597
|
+
// useful message rather than letting the server emit a generic "Validation
|
|
598
|
+
// error".
|
|
599
|
+
if (!/^[0-9a-fA-F-]{36}$/.test(characterId)) {
|
|
600
|
+
console.error(`Error: could not resolve --character to a UUID (got "${characterId}").`);
|
|
601
|
+
console.error('Pass --character <name|id|all> against the same instance the dev server is running.');
|
|
602
|
+
process.exit(1);
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
const top = flags.top > 0 ? flags.top : 20;
|
|
606
|
+
const threshold = flags.threshold >= 0 ? flags.threshold : 0.5;
|
|
607
|
+
const port = flags.port || 3000;
|
|
608
|
+
const url = `http://localhost:${port}/api/v1/memories?action=search`;
|
|
609
|
+
|
|
610
|
+
const body = { characterId, query, limit: top, minScore: threshold };
|
|
611
|
+
if (flags.source) body.source = flags.source;
|
|
612
|
+
if (flags.minImportance != null) body.minImportance = flags.minImportance;
|
|
613
|
+
|
|
614
|
+
let res;
|
|
615
|
+
try {
|
|
616
|
+
res = await fetch(url, {
|
|
617
|
+
method: 'POST',
|
|
618
|
+
headers: { 'Content-Type': 'application/json' },
|
|
619
|
+
body: JSON.stringify(body),
|
|
620
|
+
});
|
|
621
|
+
} catch (err) {
|
|
622
|
+
console.error(`Could not reach Quilltap server at http://localhost:${port}: ${err.message}`);
|
|
623
|
+
console.error('Semantic search requires the running server (the embedding provider lives there).');
|
|
624
|
+
console.error('Start it with: npm run dev');
|
|
625
|
+
process.exit(1);
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
let payload = null;
|
|
629
|
+
try {
|
|
630
|
+
const text = await res.text();
|
|
631
|
+
payload = text ? JSON.parse(text) : null;
|
|
632
|
+
} catch { /* leave payload null */ }
|
|
633
|
+
|
|
634
|
+
if (!res.ok) {
|
|
635
|
+
if (payload && payload.error && /dimension/i.test(payload.error)) {
|
|
636
|
+
console.error(`Error: ${payload.error}`);
|
|
637
|
+
console.error('Re-apply the embedding profile, or switch the active profile back to one that matches the corpus.');
|
|
638
|
+
process.exit(1);
|
|
639
|
+
}
|
|
640
|
+
const msg = (payload && payload.error) || `HTTP ${res.status}`;
|
|
641
|
+
console.error(`Error: ${msg}`);
|
|
642
|
+
process.exit(1);
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
if (flags.json) {
|
|
646
|
+
process.stdout.write(JSON.stringify(payload, null, 2) + '\n');
|
|
647
|
+
return;
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
const memories = (payload && payload.memories) || [];
|
|
651
|
+
if (memories.length === 0) {
|
|
652
|
+
console.log('(no matches)');
|
|
653
|
+
return;
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
for (const m of memories) {
|
|
657
|
+
const score = (m.score ?? 0).toFixed(3);
|
|
658
|
+
const summary = (m.summary || '').replace(/\s+/g, ' ').trim();
|
|
659
|
+
const trimmed = summary.length > 100 ? summary.slice(0, 100) + '…' : summary;
|
|
660
|
+
console.log(`${score} imp ${(m.reinforcedImportance ?? m.importance ?? 0).toFixed(2)} ${trimmed}`);
|
|
661
|
+
const content = (m.content || '').replace(/\s+/g, ' ').trim();
|
|
662
|
+
const snippet = content.length > 240 ? content.slice(0, 240) + '…' : content;
|
|
663
|
+
if (snippet) console.log(` ${snippet}`);
|
|
664
|
+
}
|
|
665
|
+
console.log('');
|
|
666
|
+
console.log(`${memories.length} match${memories.length === 1 ? '' : 'es'}` +
|
|
667
|
+
(payload.usedEmbedding === false ? ' (fallback keyword search — no usable embeddings)' : ''));
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
async function cmdGrep(flags, positional) {
|
|
671
|
+
const pattern = positional[0];
|
|
672
|
+
if (!pattern) {
|
|
673
|
+
throw new Error('memories grep: missing <pattern>. Usage: quilltap memories grep [filters] <pattern>');
|
|
674
|
+
}
|
|
675
|
+
if (flags.semantic) {
|
|
676
|
+
return cmdSemanticGrep(flags, pattern);
|
|
677
|
+
}
|
|
678
|
+
const { db } = await openDb(flags);
|
|
679
|
+
try {
|
|
680
|
+
const { where, params } = buildWhereClause(db, flags);
|
|
681
|
+
// Always restrict to rows whose content can match — quick pre-filter so we
|
|
682
|
+
// don't read all 32k rows into JS just to drop most of them.
|
|
683
|
+
const likeNeedle = flags.ignoreCase ? `%${pattern.toLowerCase()}%` : `%${pattern}%`;
|
|
684
|
+
const contentClause = flags.ignoreCase
|
|
685
|
+
? 'LOWER(m.content) LIKE ?'
|
|
686
|
+
: 'm.content LIKE ?';
|
|
687
|
+
const fullWhere = where ? `${where} AND ${contentClause}` : `WHERE ${contentClause}`;
|
|
688
|
+
|
|
689
|
+
const limit = flags.limit > 0 ? flags.limit : 50;
|
|
690
|
+
const sql = `${SELECT_BASE} ${fullWhere}
|
|
691
|
+
ORDER BY m.reinforcedImportance DESC, m.createdAt DESC
|
|
692
|
+
LIMIT ?`;
|
|
693
|
+
const rows = db.prepare(sql).all(...params, likeNeedle, limit);
|
|
694
|
+
|
|
695
|
+
const maxPerFile = flags.max > 0 ? flags.max : 5;
|
|
696
|
+
const ctxLines = flags.context > 0 ? flags.context : 0;
|
|
697
|
+
const results = [];
|
|
698
|
+
|
|
699
|
+
for (const row of rows) {
|
|
700
|
+
const matches = findMatches(row.content || '', pattern, {
|
|
701
|
+
ignoreCase: flags.ignoreCase,
|
|
702
|
+
max: maxPerFile,
|
|
703
|
+
context: ctxLines,
|
|
704
|
+
});
|
|
705
|
+
if (matches.length === 0) continue;
|
|
706
|
+
results.push({ row, matches });
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
if (flags.json) {
|
|
710
|
+
renderJson({
|
|
711
|
+
pattern,
|
|
712
|
+
ignoreCase: flags.ignoreCase,
|
|
713
|
+
count: results.length,
|
|
714
|
+
matches: results.map(({ row, matches }) => ({
|
|
715
|
+
id: row.id,
|
|
716
|
+
holder: row.holderName,
|
|
717
|
+
aboutCharacter: row.aboutCharName,
|
|
718
|
+
chatTitle: row.chatTitle,
|
|
719
|
+
importance: row.reinforcedImportance,
|
|
720
|
+
matches,
|
|
721
|
+
})),
|
|
722
|
+
});
|
|
723
|
+
return;
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
if (results.length === 0) {
|
|
727
|
+
console.log('(no matches)');
|
|
728
|
+
return;
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
if (flags.pathsOnly) {
|
|
732
|
+
for (const { row } of results) {
|
|
733
|
+
console.log(row.id);
|
|
734
|
+
}
|
|
735
|
+
return;
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
for (const { row, matches } of results) {
|
|
739
|
+
const header = `${colorize(shortId(row.id), CYAN)} (holder: ${row.holderName || '?'}, imp ${formatImportance(row.reinforcedImportance).trim()}, chat: "${truncate(chatLabel(row, flags.fullTitles), flags.fullTitles ? 60 : 32)}"):`;
|
|
740
|
+
console.log(header);
|
|
741
|
+
for (const m of matches) {
|
|
742
|
+
const snippetLines = m.context.map((line, idx) => {
|
|
743
|
+
const isMatchLine = idx === m.matchIndexInContext;
|
|
744
|
+
const lineNumPrefix = ` line ${m.line + idx - m.matchIndexInContext}:`;
|
|
745
|
+
if (!isTty()) return `${lineNumPrefix} ${line}`;
|
|
746
|
+
const colored = isMatchLine
|
|
747
|
+
? highlightMatch(line, pattern, flags.ignoreCase)
|
|
748
|
+
: colorize(line, DIM);
|
|
749
|
+
return `${lineNumPrefix} ${colored}`;
|
|
750
|
+
});
|
|
751
|
+
console.log(snippetLines.join('\n'));
|
|
752
|
+
}
|
|
753
|
+
console.log('');
|
|
754
|
+
}
|
|
755
|
+
} finally {
|
|
756
|
+
db.close();
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
function findMatches(text, pattern, { ignoreCase, max, context }) {
|
|
761
|
+
if (!text) return [];
|
|
762
|
+
const lines = text.split('\n');
|
|
763
|
+
const needle = ignoreCase ? pattern.toLowerCase() : pattern;
|
|
764
|
+
const out = [];
|
|
765
|
+
for (let i = 0; i < lines.length; i++) {
|
|
766
|
+
const hay = ignoreCase ? lines[i].toLowerCase() : lines[i];
|
|
767
|
+
if (hay.includes(needle)) {
|
|
768
|
+
const ctxStart = Math.max(0, i - context);
|
|
769
|
+
const ctxEnd = Math.min(lines.length, i + context + 1);
|
|
770
|
+
const slice = lines.slice(ctxStart, ctxEnd);
|
|
771
|
+
out.push({
|
|
772
|
+
line: i + 1,
|
|
773
|
+
text: lines[i],
|
|
774
|
+
context: slice,
|
|
775
|
+
matchIndexInContext: i - ctxStart,
|
|
776
|
+
});
|
|
777
|
+
if (out.length >= max) break;
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
return out;
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
function highlightMatch(line, pattern, ignoreCase) {
|
|
784
|
+
if (!pattern) return line;
|
|
785
|
+
try {
|
|
786
|
+
const escaped = pattern.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
787
|
+
const re = new RegExp(escaped, ignoreCase ? 'gi' : 'g');
|
|
788
|
+
return line.replace(re, (m) => colorize(m, BOLD + YELLOW));
|
|
789
|
+
} catch {
|
|
790
|
+
return line;
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
// ---------- show ----------
|
|
795
|
+
|
|
796
|
+
async function cmdShow(flags, positional) {
|
|
797
|
+
const idArg = positional[0];
|
|
798
|
+
if (!idArg) {
|
|
799
|
+
throw new Error('memories show: missing <id>. Usage: quilltap memories show <id|prefix>');
|
|
800
|
+
}
|
|
801
|
+
const { db } = await openDb(flags);
|
|
802
|
+
try {
|
|
803
|
+
const id = resolveMemoryId(db, idArg);
|
|
804
|
+
const row = db.prepare(`${SELECT_BASE} WHERE m.id = ?`).get(id);
|
|
805
|
+
if (!row) {
|
|
806
|
+
throw new Error(`Memory ${id} not found`);
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
const depthRaw = flags.depth;
|
|
810
|
+
let depth = depthRaw < 0 ? 1 : depthRaw;
|
|
811
|
+
if (flags.noRelated) depth = 0;
|
|
812
|
+
if (depth > 4) depth = 4;
|
|
813
|
+
const maxNodes = flags.maxNodes > 0 ? flags.maxNodes : 100;
|
|
814
|
+
|
|
815
|
+
let graph = null;
|
|
816
|
+
if (depth > 0) {
|
|
817
|
+
graph = traverseMemoryGraph(db, id, depth, maxNodes);
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
// Pull holder + about character vault IDs so the JSON view is fully resolved.
|
|
821
|
+
if (flags.json) {
|
|
822
|
+
const json = rowToJson(row);
|
|
823
|
+
if (graph) json.related = graphToJson(graph.root);
|
|
824
|
+
if (graph) {
|
|
825
|
+
json.graphMeta = {
|
|
826
|
+
visited: graph.visited,
|
|
827
|
+
cycles: graph.cycles,
|
|
828
|
+
truncated: graph.truncated,
|
|
829
|
+
};
|
|
830
|
+
}
|
|
831
|
+
renderJson(json);
|
|
832
|
+
return;
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
renderShowText(row, graph, depth);
|
|
836
|
+
} finally {
|
|
837
|
+
db.close();
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
function resolveMemoryId(db, idArg) {
|
|
842
|
+
if (UUID_RE.test(idArg)) {
|
|
843
|
+
const row = db.prepare('SELECT id FROM memories WHERE id = ?').get(idArg);
|
|
844
|
+
if (!row) throw new Error(`No memory with id ${idArg}`);
|
|
845
|
+
return row.id;
|
|
846
|
+
}
|
|
847
|
+
if (idArg.length < 8) {
|
|
848
|
+
throw new Error('Memory id prefix must be at least 8 characters.');
|
|
849
|
+
}
|
|
850
|
+
const prefix = idArg.toLowerCase();
|
|
851
|
+
const rows = db.prepare("SELECT id FROM memories WHERE LOWER(id) LIKE ? LIMIT 11").all(`${prefix}%`);
|
|
852
|
+
if (rows.length === 0) {
|
|
853
|
+
throw new Error(`No memory matching prefix '${idArg}'`);
|
|
854
|
+
}
|
|
855
|
+
if (rows.length > 1) {
|
|
856
|
+
const list = rows.slice(0, 10).map(r => ` ${r.id}`).join('\n');
|
|
857
|
+
const err = new Error(`Multiple memories match prefix '${idArg}':\n${list}${rows.length > 10 ? '\n …' : ''}`);
|
|
858
|
+
err.ambiguous = true;
|
|
859
|
+
throw err;
|
|
860
|
+
}
|
|
861
|
+
return rows[0].id;
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
function renderShowText(row, graph, depth) {
|
|
865
|
+
const sep = '─'.repeat(77);
|
|
866
|
+
console.log(`Memory ${row.id}`);
|
|
867
|
+
console.log(sep);
|
|
868
|
+
const holderName = row.holderName || '?';
|
|
869
|
+
const aboutName = row.aboutCharacterId == null
|
|
870
|
+
? '(none)'
|
|
871
|
+
: row.aboutCharacterId === row.characterId
|
|
872
|
+
? 'self'
|
|
873
|
+
: row.aboutCharName || '(unknown)';
|
|
874
|
+
console.log(` Holder: ${holderName} (${shortId(row.characterId)})`);
|
|
875
|
+
console.log(` About: ${aboutName}${row.aboutCharacterId && row.aboutCharacterId !== row.characterId ? ` (${shortId(row.aboutCharacterId)})` : ''}`);
|
|
876
|
+
console.log(` Source: ${row.source || '(?)'}`);
|
|
877
|
+
const reinf = row.reinforcedImportance != null ? Number(row.reinforcedImportance).toFixed(2) : '?';
|
|
878
|
+
const baseImp = row.importance != null ? Number(row.importance).toFixed(2) : '?';
|
|
879
|
+
console.log(` Importance: ${reinf} (reinforced from ${baseImp}, count: ${row.reinforcementCount ?? 0})`);
|
|
880
|
+
if (row.createdAt) console.log(` Created: ${row.createdAt}`);
|
|
881
|
+
if (row.lastAccessedAt) console.log(` Last access: ${row.lastAccessedAt}`);
|
|
882
|
+
if (row.lastReinforcedAt) console.log(` Last reinf.: ${row.lastReinforcedAt}`);
|
|
883
|
+
console.log(` Embedding: ${row.hasEmbedding ? 'present' : '(none)'}`);
|
|
884
|
+
if (row.chatId) {
|
|
885
|
+
console.log(` Chat: "${row.chatTitle || '(untitled)'}" (${shortId(row.chatId)})`);
|
|
886
|
+
if (row.sourceMessageId) {
|
|
887
|
+
console.log(` Source msg: ${shortId(row.sourceMessageId)} (in chat above)`);
|
|
888
|
+
}
|
|
889
|
+
} else {
|
|
890
|
+
console.log(` Chat: (manual entry)`);
|
|
891
|
+
}
|
|
892
|
+
if (row.projectId) {
|
|
893
|
+
console.log(` Project: ${row.projectName || '(?)'} (${shortId(row.projectId)})`);
|
|
894
|
+
} else {
|
|
895
|
+
console.log(` Project: (none)`);
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
try {
|
|
899
|
+
const kw = JSON.parse(row.keywords || '[]');
|
|
900
|
+
if (kw.length) console.log(` Keywords: [${kw.join(', ')}]`);
|
|
901
|
+
} catch { /* ignore */ }
|
|
902
|
+
try {
|
|
903
|
+
const tags = JSON.parse(row.tags || '[]');
|
|
904
|
+
if (tags.length) console.log(` Tags: [${tags.join(', ')}]`);
|
|
905
|
+
} catch { /* ignore */ }
|
|
906
|
+
|
|
907
|
+
console.log('');
|
|
908
|
+
console.log('Summary:');
|
|
909
|
+
console.log(` ${row.summary || '(no summary)'}`);
|
|
910
|
+
console.log('');
|
|
911
|
+
console.log('Content:');
|
|
912
|
+
for (const line of (row.content || '').split('\n')) {
|
|
913
|
+
console.log(` ${line}`);
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
if (depth > 0 && graph && graph.root) {
|
|
917
|
+
const direct = (graph.root.children || []).filter(c => c && !c.cycle && !c.missing);
|
|
918
|
+
console.log('');
|
|
919
|
+
console.log(`Related (${direct.length} direct, --depth ${depth}):`);
|
|
920
|
+
for (const child of graph.root.children || []) {
|
|
921
|
+
if (!child) continue;
|
|
922
|
+
if (child.cycle) {
|
|
923
|
+
console.log(` ↺ ${shortId(child.id)} (already shown)`);
|
|
924
|
+
continue;
|
|
925
|
+
}
|
|
926
|
+
if (child.missing) {
|
|
927
|
+
console.log(` ✗ ${shortId(child.id)} (deleted or missing)`);
|
|
928
|
+
continue;
|
|
929
|
+
}
|
|
930
|
+
const imp = child.reinforcedImportance != null ? Number(child.reinforcedImportance).toFixed(2) : '?';
|
|
931
|
+
const sum = truncate(child.summary || '', 80);
|
|
932
|
+
console.log(` ▸ ${shortId(child.id)} (imp ${imp}) "${sum}"`);
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
// ---------- tree ----------
|
|
938
|
+
|
|
939
|
+
async function cmdTree(flags, positional) {
|
|
940
|
+
const idArg = positional[0];
|
|
941
|
+
if (!idArg) {
|
|
942
|
+
throw new Error('memories tree: missing <id>. Usage: quilltap memories tree <id|prefix>');
|
|
943
|
+
}
|
|
944
|
+
const { db } = await openDb(flags);
|
|
945
|
+
try {
|
|
946
|
+
const id = resolveMemoryId(db, idArg);
|
|
947
|
+
let depth = flags.depth >= 0 ? flags.depth : 2;
|
|
948
|
+
if (depth > 4) depth = 4;
|
|
949
|
+
const maxNodes = flags.maxNodes > 0 ? Math.min(flags.maxNodes, 1000) : 100;
|
|
950
|
+
|
|
951
|
+
const graph = traverseMemoryGraph(db, id, depth, maxNodes);
|
|
952
|
+
|
|
953
|
+
if (flags.json) {
|
|
954
|
+
renderJson({
|
|
955
|
+
root: graphToJson(graph.root),
|
|
956
|
+
visited: graph.visited,
|
|
957
|
+
cycles: graph.cycles,
|
|
958
|
+
truncated: graph.truncated,
|
|
959
|
+
depth,
|
|
960
|
+
maxNodes,
|
|
961
|
+
});
|
|
962
|
+
return;
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
if (!graph.root) {
|
|
966
|
+
console.log('(no nodes — max-nodes cap hit before root could render)');
|
|
967
|
+
return;
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
renderTreeNode(graph.root, '', true, true);
|
|
971
|
+
console.log('');
|
|
972
|
+
const depthSuffix = graph.truncated ? `, depth ${depth} reached (max-nodes ${maxNodes} hit)` : `, depth ${depth} reached`;
|
|
973
|
+
console.log(`${graph.visited} nodes visited, ${graph.cycles} cycles detected${depthSuffix}.`);
|
|
974
|
+
} finally {
|
|
975
|
+
db.close();
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
function renderTreeNode(node, prefix, isLast, isRoot) {
|
|
980
|
+
if (!node) return;
|
|
981
|
+
if (node.cycle) {
|
|
982
|
+
console.log(`${prefix}${isLast ? '└─' : '├─'} ↺ ${shortId(node.id)} (already shown)`);
|
|
983
|
+
return;
|
|
984
|
+
}
|
|
985
|
+
if (node.missing) {
|
|
986
|
+
console.log(`${prefix}${isLast ? '└─' : '├─'} ✗ ${shortId(node.id)} (deleted or missing)`);
|
|
987
|
+
return;
|
|
988
|
+
}
|
|
989
|
+
const imp = node.reinforcedImportance != null ? Number(node.reinforcedImportance).toFixed(2) : '?';
|
|
990
|
+
const sum = truncate(node.summary || '', 80);
|
|
991
|
+
if (isRoot) {
|
|
992
|
+
console.log(`${shortId(node.id)} (imp ${imp}) "${sum}"`);
|
|
993
|
+
} else {
|
|
994
|
+
console.log(`${prefix}${isLast ? '└─' : '├─'} ${shortId(node.id)} (imp ${imp}) "${sum}"`);
|
|
995
|
+
}
|
|
996
|
+
const children = node.children || [];
|
|
997
|
+
for (let i = 0; i < children.length; i++) {
|
|
998
|
+
const child = children[i];
|
|
999
|
+
const last = i === children.length - 1;
|
|
1000
|
+
const childPrefix = isRoot ? '' : prefix + (isLast ? ' ' : '│ ');
|
|
1001
|
+
renderTreeNode(child, childPrefix, last, false);
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
// Walk the related-memory graph rooted at `rootId`. Maintains a visited-set
|
|
1006
|
+
// for cycle handling; renders dangling edges as { missing: true } leaves;
|
|
1007
|
+
// halts early when the visit count would exceed `maxNodes`.
|
|
1008
|
+
function traverseMemoryGraph(db, rootId, maxDepth, maxNodes) {
|
|
1009
|
+
const visited = new Set();
|
|
1010
|
+
let cycleCount = 0;
|
|
1011
|
+
let truncated = false;
|
|
1012
|
+
const stmt = db.prepare(
|
|
1013
|
+
'SELECT id, summary, reinforcedImportance, relatedMemoryIds FROM memories WHERE id = ?'
|
|
1014
|
+
);
|
|
1015
|
+
|
|
1016
|
+
function walk(id, depth) {
|
|
1017
|
+
if (visited.size >= maxNodes) { truncated = true; return null; }
|
|
1018
|
+
if (visited.has(id)) { cycleCount++; return { id, cycle: true }; }
|
|
1019
|
+
visited.add(id);
|
|
1020
|
+
const row = stmt.get(id);
|
|
1021
|
+
if (!row) return { id, missing: true };
|
|
1022
|
+
if (depth >= maxDepth) return { ...row, children: [] };
|
|
1023
|
+
let childIds = [];
|
|
1024
|
+
try { childIds = JSON.parse(row.relatedMemoryIds || '[]'); } catch { childIds = []; }
|
|
1025
|
+
const children = childIds.map(cid => walk(cid, depth + 1)).filter(Boolean);
|
|
1026
|
+
return { ...row, children };
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
return { root: walk(rootId, 0), visited: visited.size, cycles: cycleCount, truncated };
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
function graphToJson(node) {
|
|
1033
|
+
if (!node) return null;
|
|
1034
|
+
if (node.cycle) return { id: node.id, cycle: true };
|
|
1035
|
+
if (node.missing) return { id: node.id, missing: true };
|
|
1036
|
+
return {
|
|
1037
|
+
id: node.id,
|
|
1038
|
+
summary: node.summary,
|
|
1039
|
+
reinforcedImportance: node.reinforcedImportance,
|
|
1040
|
+
children: (node.children || []).map(graphToJson),
|
|
1041
|
+
};
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
// ---------- status ----------
|
|
1045
|
+
|
|
1046
|
+
async function cmdStatus(flags) {
|
|
1047
|
+
const { db } = await openDb(flags);
|
|
1048
|
+
try {
|
|
1049
|
+
let holderRows;
|
|
1050
|
+
if (flags.character && flags.character !== 'all') {
|
|
1051
|
+
const c = resolveCharacter(db, flags.character);
|
|
1052
|
+
holderRows = [{ id: c.id, name: c.name }];
|
|
1053
|
+
} else {
|
|
1054
|
+
holderRows = db.prepare(`
|
|
1055
|
+
SELECT c.id, c.name, COUNT(m.id) AS cnt
|
|
1056
|
+
FROM characters c
|
|
1057
|
+
LEFT JOIN memories m ON m.characterId = c.id
|
|
1058
|
+
GROUP BY c.id
|
|
1059
|
+
HAVING cnt > 0
|
|
1060
|
+
ORDER BY cnt DESC, c.name
|
|
1061
|
+
`).all();
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
const report = [];
|
|
1065
|
+
for (const holder of holderRows) {
|
|
1066
|
+
const stats = computeHolderStats(db, holder.id);
|
|
1067
|
+
report.push({ holder, stats });
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
if (flags.json) {
|
|
1071
|
+
renderJson({
|
|
1072
|
+
holders: report.map(({ holder, stats }) => ({
|
|
1073
|
+
holder: { id: holder.id, name: holder.name },
|
|
1074
|
+
...stats,
|
|
1075
|
+
})),
|
|
1076
|
+
});
|
|
1077
|
+
return;
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
for (const { holder, stats } of report) {
|
|
1081
|
+
renderStatusBlock(holder, stats);
|
|
1082
|
+
console.log('');
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
// After per-holder rendering, log any dangling edges to stderr so the user
|
|
1086
|
+
// notices the inconsistency without it cluttering the structured output.
|
|
1087
|
+
const danglingTotal = report.reduce((sum, r) => sum + r.stats.graph.danglingEdges, 0);
|
|
1088
|
+
if (danglingTotal > 0) {
|
|
1089
|
+
process.stderr.write(
|
|
1090
|
+
`Warning: ${danglingTotal} dangling related-memory edges across all holders. ` +
|
|
1091
|
+
`These are JSON UUIDs in relatedMemoryIds that no longer resolve to a memory.\n`
|
|
1092
|
+
);
|
|
1093
|
+
}
|
|
1094
|
+
} finally {
|
|
1095
|
+
db.close();
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
function computeHolderStats(db, characterId) {
|
|
1100
|
+
const counts = db.prepare(`
|
|
1101
|
+
SELECT
|
|
1102
|
+
COUNT(*) AS total,
|
|
1103
|
+
SUM(CASE WHEN source = 'AUTO' THEN 1 ELSE 0 END) AS auto,
|
|
1104
|
+
SUM(CASE WHEN source = 'MANUAL' THEN 1 ELSE 0 END) AS manual,
|
|
1105
|
+
SUM(CASE WHEN aboutCharacterId = characterId THEN 1 ELSE 0 END) AS selfRef,
|
|
1106
|
+
SUM(CASE WHEN aboutCharacterId IS NOT NULL AND aboutCharacterId != characterId THEN 1 ELSE 0 END) AS aboutOthers,
|
|
1107
|
+
SUM(CASE WHEN aboutCharacterId IS NULL THEN 1 ELSE 0 END) AS legacy,
|
|
1108
|
+
SUM(CASE WHEN embedding IS NOT NULL THEN 1 ELSE 0 END) AS withEmbedding,
|
|
1109
|
+
SUM(CASE WHEN embedding IS NULL THEN 1 ELSE 0 END) AS withoutEmbedding
|
|
1110
|
+
FROM memories
|
|
1111
|
+
WHERE characterId = ?
|
|
1112
|
+
`).get(characterId);
|
|
1113
|
+
|
|
1114
|
+
const graph = scanDanglingEdges(db, { characterId });
|
|
1115
|
+
|
|
1116
|
+
const topMemories = db.prepare(`
|
|
1117
|
+
SELECT id, summary, reinforcedImportance
|
|
1118
|
+
FROM memories
|
|
1119
|
+
WHERE characterId = ?
|
|
1120
|
+
ORDER BY reinforcedImportance DESC, createdAt DESC
|
|
1121
|
+
LIMIT 5
|
|
1122
|
+
`).all(characterId);
|
|
1123
|
+
|
|
1124
|
+
return {
|
|
1125
|
+
counts: {
|
|
1126
|
+
total: counts.total || 0,
|
|
1127
|
+
auto: counts.auto || 0,
|
|
1128
|
+
manual: counts.manual || 0,
|
|
1129
|
+
},
|
|
1130
|
+
aboutDistribution: {
|
|
1131
|
+
selfReferential: counts.selfRef || 0,
|
|
1132
|
+
aboutOthers: counts.aboutOthers || 0,
|
|
1133
|
+
legacy: counts.legacy || 0,
|
|
1134
|
+
},
|
|
1135
|
+
embeddings: {
|
|
1136
|
+
present: counts.withEmbedding || 0,
|
|
1137
|
+
missing: counts.withoutEmbedding || 0,
|
|
1138
|
+
},
|
|
1139
|
+
graph: {
|
|
1140
|
+
withLinks: graph.withLinks,
|
|
1141
|
+
isolated: graph.isolated,
|
|
1142
|
+
avgDegree: graph.avgDegree,
|
|
1143
|
+
maxDegree: graph.maxDegree,
|
|
1144
|
+
danglingEdges: graph.danglingEdges,
|
|
1145
|
+
},
|
|
1146
|
+
top: topMemories,
|
|
1147
|
+
};
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
function renderStatusBlock(holder, stats) {
|
|
1151
|
+
console.log(`Holder: ${holder.name} (${shortId(holder.id)})`);
|
|
1152
|
+
console.log(` Total memories: ${stats.counts.total}`);
|
|
1153
|
+
console.log(` AUTO: ${stats.counts.auto}`);
|
|
1154
|
+
console.log(` MANUAL: ${stats.counts.manual}`);
|
|
1155
|
+
console.log(` About-distribution:`);
|
|
1156
|
+
console.log(` self-referential: ${stats.aboutDistribution.selfReferential}`);
|
|
1157
|
+
console.log(` about-others: ${stats.aboutDistribution.aboutOthers}`);
|
|
1158
|
+
const legacySuffix = stats.aboutDistribution.legacy > 0 ? ' ⚠ run alignment migration?' : '';
|
|
1159
|
+
console.log(` legacy (NULL): ${stats.aboutDistribution.legacy}${legacySuffix}`);
|
|
1160
|
+
console.log(` Embeddings:`);
|
|
1161
|
+
console.log(` present: ${stats.embeddings.present}`);
|
|
1162
|
+
const missingSuffix = stats.embeddings.missing > 0 ? ' ⚠ may not be recallable' : '';
|
|
1163
|
+
console.log(` missing: ${stats.embeddings.missing}${missingSuffix}`);
|
|
1164
|
+
console.log(` Graph:`);
|
|
1165
|
+
console.log(` nodes with links: ${stats.graph.withLinks}`);
|
|
1166
|
+
console.log(` isolated (0 links): ${stats.graph.isolated}`);
|
|
1167
|
+
console.log(` avg degree: ${stats.graph.avgDegree}`);
|
|
1168
|
+
console.log(` max degree: ${stats.graph.maxDegree}`);
|
|
1169
|
+
const danglingSuffix = stats.graph.danglingEdges > 0 ? ' ⚠ JSON UUIDs that no longer resolve' : '';
|
|
1170
|
+
console.log(` dangling edges: ${stats.graph.danglingEdges}${danglingSuffix}`);
|
|
1171
|
+
if (stats.top.length) {
|
|
1172
|
+
console.log(` Top by reinforcedImportance:`);
|
|
1173
|
+
for (const t of stats.top) {
|
|
1174
|
+
const imp = t.reinforcedImportance != null ? Number(t.reinforcedImportance).toFixed(2) : '?';
|
|
1175
|
+
console.log(` ${shortId(t.id)} (imp ${imp}) "${truncate(t.summary || '', 80)}"`);
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
// ---------- validate ----------
|
|
1181
|
+
|
|
1182
|
+
async function cmdValidate(flags) {
|
|
1183
|
+
const { db } = await openDb(flags);
|
|
1184
|
+
try {
|
|
1185
|
+
let holderIds = null;
|
|
1186
|
+
let holderRows;
|
|
1187
|
+
if (flags.character && flags.character !== 'all') {
|
|
1188
|
+
const c = resolveCharacter(db, flags.character);
|
|
1189
|
+
holderRows = [{ id: c.id, name: c.name }];
|
|
1190
|
+
holderIds = [c.id];
|
|
1191
|
+
} else {
|
|
1192
|
+
holderRows = db.prepare(`
|
|
1193
|
+
SELECT c.id, c.name FROM characters c
|
|
1194
|
+
WHERE EXISTS (SELECT 1 FROM memories m WHERE m.characterId = c.id)
|
|
1195
|
+
ORDER BY c.name
|
|
1196
|
+
`).all();
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
const includePairs = Boolean(flags.list);
|
|
1200
|
+
let totalDangling = 0;
|
|
1201
|
+
let totalEdges = 0;
|
|
1202
|
+
let totalNodes = 0;
|
|
1203
|
+
const perHolder = [];
|
|
1204
|
+
for (const holder of holderRows) {
|
|
1205
|
+
const stats = scanDanglingEdges(db, {
|
|
1206
|
+
characterId: holder.id,
|
|
1207
|
+
includePairs,
|
|
1208
|
+
});
|
|
1209
|
+
totalDangling += stats.danglingEdges;
|
|
1210
|
+
totalEdges += stats.totalEdges;
|
|
1211
|
+
totalNodes += stats.nodes;
|
|
1212
|
+
perHolder.push({ holder, stats });
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
if (flags.json) {
|
|
1216
|
+
renderJson({
|
|
1217
|
+
scanned: holderIds ? 'single' : 'all',
|
|
1218
|
+
characterIds: holderRows.map(h => h.id),
|
|
1219
|
+
totals: {
|
|
1220
|
+
nodes: totalNodes,
|
|
1221
|
+
edges: totalEdges,
|
|
1222
|
+
danglingEdges: totalDangling,
|
|
1223
|
+
},
|
|
1224
|
+
holders: perHolder.map(({ holder, stats }) => ({
|
|
1225
|
+
holder: { id: holder.id, name: holder.name },
|
|
1226
|
+
...stats,
|
|
1227
|
+
})),
|
|
1228
|
+
});
|
|
1229
|
+
} else {
|
|
1230
|
+
const scope = holderIds ? `holder ${holderRows[0].name}` : `${holderRows.length} holders`;
|
|
1231
|
+
const drift = totalEdges > 0 ? ((totalDangling / totalEdges) * 100).toFixed(2) : '0.00';
|
|
1232
|
+
console.log(`Scanned ${scope}: ${totalNodes} memories, ${totalEdges} edges, ${totalDangling} dangling (${drift}%).`);
|
|
1233
|
+
if (totalDangling === 0) {
|
|
1234
|
+
console.log(colorize('Graph is clean.', GREEN));
|
|
1235
|
+
} else if (flags.list) {
|
|
1236
|
+
console.log('');
|
|
1237
|
+
for (const { holder, stats } of perHolder) {
|
|
1238
|
+
if (!stats.danglingPairs || stats.danglingPairs.length === 0) continue;
|
|
1239
|
+
console.log(colorize(`${holder.name} (${shortId(holder.id)})`, BOLD));
|
|
1240
|
+
for (const pair of stats.danglingPairs) {
|
|
1241
|
+
console.log(` ${shortId(pair.sourceId)} → ${pair.targetIds.map(shortId).join(', ')}`);
|
|
1242
|
+
}
|
|
1243
|
+
}
|
|
1244
|
+
} else {
|
|
1245
|
+
console.log(colorize('Run with --list to see offending source memory IDs and dangling targets.', YELLOW));
|
|
1246
|
+
}
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
if (totalDangling > 0) {
|
|
1250
|
+
process.exitCode = 1;
|
|
1251
|
+
}
|
|
1252
|
+
} finally {
|
|
1253
|
+
db.close();
|
|
1254
|
+
}
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
// ---------- help ----------
|
|
1258
|
+
|
|
1259
|
+
function printMemoriesHelp() {
|
|
1260
|
+
console.log(`
|
|
1261
|
+
Quilltap Memories Tool
|
|
1262
|
+
|
|
1263
|
+
Usage: quilltap memories <subcommand> [filters] [options]
|
|
1264
|
+
|
|
1265
|
+
Subcommands:
|
|
1266
|
+
ls [filters] [--sort <field>] [-r] [--limit N] [--json]
|
|
1267
|
+
List memories (default sort:
|
|
1268
|
+
reinforcedImportance DESC).
|
|
1269
|
+
find [filters] [--in summary|content|both] [--limit N] <pattern>
|
|
1270
|
+
Substring search on summary
|
|
1271
|
+
and/or content.
|
|
1272
|
+
grep [filters] [-i] [-l] [--max N] [--context N] <pattern>
|
|
1273
|
+
Pattern search inside content
|
|
1274
|
+
with snippets.
|
|
1275
|
+
show <id|prefix> [--depth N] [--no-related] [--json]
|
|
1276
|
+
Full record + related-memory
|
|
1277
|
+
neighbourhood.
|
|
1278
|
+
tree <id|prefix> [--depth N] [--max-nodes N] [--json]
|
|
1279
|
+
ASCII walk of the
|
|
1280
|
+
related-memory graph.
|
|
1281
|
+
status [--character <name|id>] [--json] Per-holder rollup + dangling-
|
|
1282
|
+
edge check.
|
|
1283
|
+
validate [--character <name|id>] [--list] Read-only memory-graph health
|
|
1284
|
+
[--json] check. Exits 1 if any
|
|
1285
|
+
dangling edges remain.
|
|
1286
|
+
|
|
1287
|
+
Shared filter flags:
|
|
1288
|
+
--character <name|id|all> Holder. Default: all.
|
|
1289
|
+
--about <name|id|self|none> Subject (aboutCharacterId).
|
|
1290
|
+
--source AUTO|MANUAL Restrict by source.
|
|
1291
|
+
--chat <id|title|none> Source chat ('none' for manual memories).
|
|
1292
|
+
--project <id|name> Project context.
|
|
1293
|
+
--since <date> ISO date floor on createdAt.
|
|
1294
|
+
--until <date> ISO date ceiling on createdAt.
|
|
1295
|
+
--min-importance <n> Floor on raw importance.
|
|
1296
|
+
--min-reinforced <n> Floor on reinforcedImportance.
|
|
1297
|
+
--has-embedding Only memories with an embedding.
|
|
1298
|
+
--no-embedding Only memories WITHOUT an embedding.
|
|
1299
|
+
|
|
1300
|
+
Sort flags (ls / find / grep):
|
|
1301
|
+
--sort <field> reinforced (default) | importance | created |
|
|
1302
|
+
accessed | reinforcement-count | links
|
|
1303
|
+
-r, --reverse Flip the sort order.
|
|
1304
|
+
--limit N Default 50.
|
|
1305
|
+
--full-titles Don't truncate chat titles to 32 chars.
|
|
1306
|
+
|
|
1307
|
+
Global flags (may appear before or after the subcommand):
|
|
1308
|
+
-d, --data-dir <path> Override data directory.
|
|
1309
|
+
--instance <name> Use a registered instance.
|
|
1310
|
+
--passphrase <pass> Decrypt .dbkey if peppered.
|
|
1311
|
+
--json Machine-readable output.
|
|
1312
|
+
-h, --help Show this help.
|
|
1313
|
+
|
|
1314
|
+
Note: -i is reserved here for 'grep --ignore-case'. Use the long --instance
|
|
1315
|
+
form to target a registered instance.
|
|
1316
|
+
|
|
1317
|
+
All memories verbs are read-only. They open the main encrypted database
|
|
1318
|
+
(quilltap.db) and never write to it.
|
|
1319
|
+
|
|
1320
|
+
Examples:
|
|
1321
|
+
quilltap memories ls
|
|
1322
|
+
quilltap memories ls --character Ariadne --sort created --limit 10
|
|
1323
|
+
quilltap memories find "concrete examples"
|
|
1324
|
+
quilltap memories grep -i --max 3 --context 1 "concrete examples"
|
|
1325
|
+
quilltap memories show abc12345 --depth 2
|
|
1326
|
+
quilltap memories tree abc12345 --depth 3
|
|
1327
|
+
quilltap memories status --character Ariadne
|
|
1328
|
+
quilltap memories validate
|
|
1329
|
+
quilltap memories validate --list
|
|
1330
|
+
`);
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1333
|
+
// ---------- dispatcher ----------
|
|
1334
|
+
|
|
1335
|
+
async function memoriesCommand(args) {
|
|
1336
|
+
if (args.length === 0 || args[0] === '-h' || args[0] === '--help') {
|
|
1337
|
+
printMemoriesHelp();
|
|
1338
|
+
return;
|
|
1339
|
+
}
|
|
1340
|
+
// The dispatcher in bin/quilltap.js already strips global flags that appear
|
|
1341
|
+
// BEFORE the verb. Anything left here is verb + flags. The first positional
|
|
1342
|
+
// is the verb.
|
|
1343
|
+
const { flags, positional } = parseFlags(args);
|
|
1344
|
+
if (flags.help) { printMemoriesHelp(); return; }
|
|
1345
|
+
const verb = positional.shift();
|
|
1346
|
+
if (!verb) {
|
|
1347
|
+
printMemoriesHelp();
|
|
1348
|
+
return;
|
|
1349
|
+
}
|
|
1350
|
+
switch (verb) {
|
|
1351
|
+
case 'ls': await cmdLs(flags); break;
|
|
1352
|
+
case 'find': await cmdFind(flags, positional); break;
|
|
1353
|
+
case 'grep': await cmdGrep(flags, positional); break;
|
|
1354
|
+
case 'show': await cmdShow(flags, positional); break;
|
|
1355
|
+
case 'tree': await cmdTree(flags, positional); break;
|
|
1356
|
+
case 'status': await cmdStatus(flags); break;
|
|
1357
|
+
case 'validate': await cmdValidate(flags); break;
|
|
1358
|
+
default:
|
|
1359
|
+
console.error(`Unknown memories subcommand: ${verb}`);
|
|
1360
|
+
console.error("Run 'quilltap memories --help' for a list.");
|
|
1361
|
+
process.exit(1);
|
|
1362
|
+
}
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1365
|
+
module.exports = {
|
|
1366
|
+
memoriesCommand,
|
|
1367
|
+
// Exported for unit tests:
|
|
1368
|
+
parseFlags,
|
|
1369
|
+
buildWhereClause,
|
|
1370
|
+
buildOrderBy,
|
|
1371
|
+
traverseMemoryGraph,
|
|
1372
|
+
findMatches,
|
|
1373
|
+
resolveMemoryId,
|
|
1374
|
+
};
|