quilltap 4.5.0-dev → 4.6.0-dev
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
package/lib/db-helpers.js
CHANGED
|
@@ -24,6 +24,89 @@ function resolveDataDir(overrideDir) {
|
|
|
24
24
|
return path.join(home, '.quilltap', 'data');
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
+
// Resolve the data dir + database passphrase a subcommand should use, given
|
|
28
|
+
// the raw flag values it parsed. `--instance Foo` looks up the registry; an
|
|
29
|
+
// explicit `--data-dir` or `--passphrase` still wins so callers can override.
|
|
30
|
+
// Errors out if both `--instance` and `--data-dir` were supplied — these
|
|
31
|
+
// configure the same thing two different ways and must not silently disagree.
|
|
32
|
+
//
|
|
33
|
+
// Precedence order (highest to lowest):
|
|
34
|
+
// 1. --data-dir (explicit override)
|
|
35
|
+
// 2. --instance (explicit instance)
|
|
36
|
+
// 3. registered default instance
|
|
37
|
+
// 4. QUILLTAP_DATA_DIR env var
|
|
38
|
+
// 5. OS platform default
|
|
39
|
+
//
|
|
40
|
+
// Returns `usedPlatformDefault: true` only when falling back to the true
|
|
41
|
+
// platform default (no flags, no env var, no registered default).
|
|
42
|
+
// Callers can use this to decide whether to prompt the user with a
|
|
43
|
+
// "did you forget --instance?" hint.
|
|
44
|
+
function resolveDataDirAndPassphrase({ dataDir, instance, passphrase }) {
|
|
45
|
+
if (dataDir && instance) {
|
|
46
|
+
throw new Error('Specify either --instance or --data-dir, not both.');
|
|
47
|
+
}
|
|
48
|
+
if (instance) {
|
|
49
|
+
const { resolveInstance } = require('./instances');
|
|
50
|
+
const inst = resolveInstance(instance);
|
|
51
|
+
return {
|
|
52
|
+
dataDir: path.join(inst.path, 'data'),
|
|
53
|
+
passphrase: passphrase || inst.passphrase || '',
|
|
54
|
+
instanceName: inst.name,
|
|
55
|
+
usedPlatformDefault: false,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
if (!dataDir && !process.env.QUILLTAP_DATA_DIR) {
|
|
59
|
+
const { getDefaultInstance, resolveInstance } = require('./instances');
|
|
60
|
+
const defaultName = getDefaultInstance();
|
|
61
|
+
if (defaultName) {
|
|
62
|
+
try {
|
|
63
|
+
const inst = resolveInstance(defaultName);
|
|
64
|
+
return {
|
|
65
|
+
dataDir: path.join(inst.path, 'data'),
|
|
66
|
+
passphrase: passphrase || inst.passphrase || '',
|
|
67
|
+
instanceName: inst.name,
|
|
68
|
+
usedPlatformDefault: false,
|
|
69
|
+
};
|
|
70
|
+
} catch {
|
|
71
|
+
// Fall through to env var / platform default if default resolution fails
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
const usedPlatformDefault = !dataDir && !process.env.QUILLTAP_DATA_DIR;
|
|
76
|
+
return {
|
|
77
|
+
dataDir: resolveDataDir(dataDir),
|
|
78
|
+
passphrase: passphrase || '',
|
|
79
|
+
instanceName: null,
|
|
80
|
+
usedPlatformDefault,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Print a one-line stderr hint when the CLI silently fell back to the platform
|
|
85
|
+
// default instance but the user has registered alternatives — Friday, Ignite,
|
|
86
|
+
// etc. The hint fires at most once per process so repeated openDb() calls
|
|
87
|
+
// inside a single subcommand stay quiet.
|
|
88
|
+
let _instanceHintPrinted = false;
|
|
89
|
+
function printDefaultInstanceHint(resolved) {
|
|
90
|
+
if (_instanceHintPrinted) return;
|
|
91
|
+
if (!resolved || !resolved.usedPlatformDefault) return;
|
|
92
|
+
if (process.env.QUILLTAP_QUIET_HINTS) return;
|
|
93
|
+
let registered;
|
|
94
|
+
try {
|
|
95
|
+
const { listInstances } = require('./instances');
|
|
96
|
+
registered = listInstances();
|
|
97
|
+
} catch {
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
if (!registered || registered.length === 0) return;
|
|
101
|
+
_instanceHintPrinted = true;
|
|
102
|
+
const names = registered.map((r) => r.name).join(', ');
|
|
103
|
+
process.stderr.write(
|
|
104
|
+
`Hint: using the default instance (${resolved.dataDir}). ` +
|
|
105
|
+
`Registered: ${names}. Pass --instance <name> to target one. ` +
|
|
106
|
+
`(set QUILLTAP_QUIET_HINTS=1 to silence)\n`
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
27
110
|
function promptPassphrase(prompt) {
|
|
28
111
|
return new Promise((resolve, reject) => {
|
|
29
112
|
const readline = require('readline');
|
|
@@ -115,10 +198,9 @@ async function loadDbKey(dataDir, passphrase) {
|
|
|
115
198
|
return tryDecrypt(passphrase);
|
|
116
199
|
}
|
|
117
200
|
|
|
118
|
-
function
|
|
119
|
-
const dbPath = path.join(dataDir, 'quilltap-mount-index.db');
|
|
201
|
+
function openEncryptedDb(dbPath, pepper, { readonly = true, friendlyName = 'database' } = {}) {
|
|
120
202
|
if (!fs.existsSync(dbPath)) {
|
|
121
|
-
throw new Error(
|
|
203
|
+
throw new Error(`${friendlyName} not found: ${dbPath}`);
|
|
122
204
|
}
|
|
123
205
|
|
|
124
206
|
let Database;
|
|
@@ -138,16 +220,103 @@ function openMountIndexDb(dataDir, pepper, { readonly = true } = {}) {
|
|
|
138
220
|
db.prepare('SELECT 1').get();
|
|
139
221
|
} catch (err) {
|
|
140
222
|
db.close();
|
|
141
|
-
throw new Error(`Cannot open
|
|
223
|
+
throw new Error(`Cannot open ${friendlyName}: ${err.message}\n` +
|
|
142
224
|
'The database may be encrypted with a different key, or the .dbkey file may be missing.');
|
|
143
225
|
}
|
|
144
226
|
|
|
145
227
|
return db;
|
|
146
228
|
}
|
|
147
229
|
|
|
230
|
+
function openMainDb(dataDir, pepper, opts = {}) {
|
|
231
|
+
return openEncryptedDb(path.join(dataDir, 'quilltap.db'), pepper, { ...opts, friendlyName: 'main database' });
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function openLlmLogsDb(dataDir, pepper, opts = {}) {
|
|
235
|
+
return openEncryptedDb(path.join(dataDir, 'quilltap-llm-logs.db'), pepper, { ...opts, friendlyName: 'LLM logs database' });
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function openMountIndexDb(dataDir, pepper, opts = {}) {
|
|
239
|
+
return openEncryptedDb(path.join(dataDir, 'quilltap-mount-index.db'), pepper, { ...opts, friendlyName: 'mount index database' });
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// ---------- shared name resolvers ----------
|
|
243
|
+
// Used by `db` and `memories` (and any future CLI namespace) to turn fuzzy
|
|
244
|
+
// user input — a UUID, a name, an alias, a substring — into a concrete row.
|
|
245
|
+
// Throws an `ambiguous` error (with `.ambiguous = true`) when multiple rows
|
|
246
|
+
// match so callers can surface a clean list instead of a stack trace.
|
|
247
|
+
|
|
248
|
+
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
249
|
+
|
|
250
|
+
function ambiguous(kind, rows) {
|
|
251
|
+
const list = rows.slice(0, 10).map(r => ` ${r.id} ${r.name || r.title || ''}`).join('\n');
|
|
252
|
+
const more = rows.length > 10 ? `\n … and ${rows.length - 10} more` : '';
|
|
253
|
+
const err = new Error(`Multiple ${kind}s match. Use a UUID or a more specific name:\n${list}${more}`);
|
|
254
|
+
err.ambiguous = true;
|
|
255
|
+
return err;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function resolveCharacter(db, query) {
|
|
259
|
+
if (UUID_RE.test(query)) {
|
|
260
|
+
const row = db.prepare('SELECT id, name, aliases FROM characters WHERE id = ?').get(query);
|
|
261
|
+
if (!row) throw new Error(`No character with id ${query}`);
|
|
262
|
+
return row;
|
|
263
|
+
}
|
|
264
|
+
const exact = db.prepare(
|
|
265
|
+
'SELECT id, name, aliases FROM characters WHERE LOWER(name) = LOWER(?)'
|
|
266
|
+
).all(query);
|
|
267
|
+
if (exact.length === 1) return exact[0];
|
|
268
|
+
if (exact.length > 1) {
|
|
269
|
+
throw ambiguous('character', exact);
|
|
270
|
+
}
|
|
271
|
+
const fuzzy = db.prepare(
|
|
272
|
+
'SELECT id, name, aliases FROM characters WHERE LOWER(name) LIKE LOWER(?) OR LOWER(aliases) LIKE LOWER(?) ORDER BY name'
|
|
273
|
+
).all(`%${query}%`, `%${query}%`);
|
|
274
|
+
if (fuzzy.length === 0) throw new Error(`No character matching '${query}'`);
|
|
275
|
+
if (fuzzy.length > 1) throw ambiguous('character', fuzzy);
|
|
276
|
+
return fuzzy[0];
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function resolveChat(db, query) {
|
|
280
|
+
if (UUID_RE.test(query)) {
|
|
281
|
+
const row = db.prepare('SELECT id, title, chatType, projectId FROM chats WHERE id = ?').get(query);
|
|
282
|
+
if (!row) throw new Error(`No chat with id ${query}`);
|
|
283
|
+
return row;
|
|
284
|
+
}
|
|
285
|
+
const fuzzy = db.prepare(
|
|
286
|
+
"SELECT id, title, chatType, projectId, lastMessageAt FROM chats WHERE LOWER(title) LIKE LOWER(?) ORDER BY lastMessageAt DESC"
|
|
287
|
+
).all(`%${query}%`);
|
|
288
|
+
if (fuzzy.length === 0) throw new Error(`No chat matching '${query}'`);
|
|
289
|
+
if (fuzzy.length > 1) throw ambiguous('chat', fuzzy);
|
|
290
|
+
return fuzzy[0];
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function resolveProject(db, query) {
|
|
294
|
+
if (UUID_RE.test(query)) {
|
|
295
|
+
const row = db.prepare('SELECT id, name FROM projects WHERE id = ?').get(query);
|
|
296
|
+
if (!row) throw new Error(`No project with id ${query}`);
|
|
297
|
+
return row;
|
|
298
|
+
}
|
|
299
|
+
const fuzzy = db.prepare(
|
|
300
|
+
'SELECT id, name FROM projects WHERE LOWER(name) LIKE LOWER(?) ORDER BY name'
|
|
301
|
+
).all(`%${query}%`);
|
|
302
|
+
if (fuzzy.length === 0) throw new Error(`No project matching '${query}'`);
|
|
303
|
+
if (fuzzy.length > 1) throw ambiguous('project', fuzzy);
|
|
304
|
+
return fuzzy[0];
|
|
305
|
+
}
|
|
306
|
+
|
|
148
307
|
module.exports = {
|
|
149
308
|
resolveDataDir,
|
|
309
|
+
resolveDataDirAndPassphrase,
|
|
310
|
+
printDefaultInstanceHint,
|
|
150
311
|
promptPassphrase,
|
|
151
312
|
loadDbKey,
|
|
313
|
+
openEncryptedDb,
|
|
314
|
+
openMainDb,
|
|
315
|
+
openLlmLogsDb,
|
|
152
316
|
openMountIndexDb,
|
|
317
|
+
UUID_RE,
|
|
318
|
+
ambiguous,
|
|
319
|
+
resolveCharacter,
|
|
320
|
+
resolveChat,
|
|
321
|
+
resolveProject,
|
|
153
322
|
};
|