quilltap 4.5.0-dev → 4.5.0-dev.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/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 openMountIndexDb(dataDir, pepper, { readonly = true } = {}) {
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(`Mount index database not found: ${dbPath}`);
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 mount index database: ${err.message}\n` +
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
  };