quilltap 3.2.0-dev.18 → 3.2.0-dev.38
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/bin/quilltap.js +249 -1
- package/package.json +2 -2
package/bin/quilltap.js
CHANGED
|
@@ -359,4 +359,252 @@ async function main() {
|
|
|
359
359
|
});
|
|
360
360
|
}
|
|
361
361
|
|
|
362
|
-
|
|
362
|
+
// ============================================================================
|
|
363
|
+
// db subcommand — query encrypted databases directly
|
|
364
|
+
// ============================================================================
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* Resolve the data directory using the same platform logic as lib/paths.ts.
|
|
368
|
+
*/
|
|
369
|
+
function resolveDataDir(overrideDir) {
|
|
370
|
+
if (overrideDir) {
|
|
371
|
+
const resolved = overrideDir.startsWith('~')
|
|
372
|
+
? path.join(require('os').homedir(), overrideDir.slice(1))
|
|
373
|
+
: overrideDir;
|
|
374
|
+
return path.join(resolved, 'data');
|
|
375
|
+
}
|
|
376
|
+
const os = require('os');
|
|
377
|
+
if (process.env.QUILLTAP_DATA_DIR) {
|
|
378
|
+
return path.join(process.env.QUILLTAP_DATA_DIR, 'data');
|
|
379
|
+
}
|
|
380
|
+
const home = os.homedir();
|
|
381
|
+
if (process.platform === 'darwin') return path.join(home, 'Library', 'Application Support', 'Quilltap', 'data');
|
|
382
|
+
if (process.platform === 'win32') return path.join(process.env.APPDATA || path.join(home, 'AppData', 'Roaming'), 'Quilltap', 'data');
|
|
383
|
+
return path.join(home, '.quilltap', 'data');
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* Read and decrypt the .dbkey file to get the SQLCipher key.
|
|
388
|
+
*/
|
|
389
|
+
function loadDbKey(dataDir, passphrase) {
|
|
390
|
+
const crypto = require('crypto');
|
|
391
|
+
const dbkeyPath = path.join(dataDir, 'quilltap.dbkey');
|
|
392
|
+
if (!fs.existsSync(dbkeyPath)) {
|
|
393
|
+
return null; // No .dbkey file — DB may be unencrypted
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
const data = JSON.parse(fs.readFileSync(dbkeyPath, 'utf8'));
|
|
397
|
+
const INTERNAL_PASSPHRASE = '__quilltap_no_passphrase__';
|
|
398
|
+
|
|
399
|
+
// Strip legacy hasPassphrase field if present
|
|
400
|
+
if ('hasPassphrase' in data) {
|
|
401
|
+
delete data.hasPassphrase;
|
|
402
|
+
fs.writeFileSync(dbkeyPath, JSON.stringify(data, null, 2), { mode: 0o600 });
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// Helper to attempt decryption with a given passphrase
|
|
406
|
+
function tryDecrypt(pass) {
|
|
407
|
+
const salt = Buffer.from(data.salt, 'hex');
|
|
408
|
+
const key = crypto.pbkdf2Sync(pass, new Uint8Array(salt), data.kdfIterations, 32, data.kdfDigest);
|
|
409
|
+
const iv = Buffer.from(data.iv, 'hex');
|
|
410
|
+
const decipher = crypto.createDecipheriv(data.algorithm, new Uint8Array(key), new Uint8Array(iv));
|
|
411
|
+
decipher.setAuthTag(new Uint8Array(Buffer.from(data.authTag, 'hex')));
|
|
412
|
+
let plaintext = decipher.update(data.ciphertext, 'hex', 'utf8');
|
|
413
|
+
plaintext += decipher.final('utf8');
|
|
414
|
+
|
|
415
|
+
const hash = crypto.createHash('sha256').update(plaintext).digest('hex');
|
|
416
|
+
if (hash !== data.pepperHash) {
|
|
417
|
+
throw new Error('Pepper hash mismatch');
|
|
418
|
+
}
|
|
419
|
+
return plaintext;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// Try internal passphrase first (no user passphrase case)
|
|
423
|
+
try {
|
|
424
|
+
return tryDecrypt(INTERNAL_PASSPHRASE);
|
|
425
|
+
} catch {
|
|
426
|
+
// Internal passphrase failed — need user passphrase
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// User passphrase required
|
|
430
|
+
if (!passphrase) {
|
|
431
|
+
throw new Error('This database requires a passphrase. Use --passphrase <pass>');
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
return tryDecrypt(passphrase);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
function printDbHelp() {
|
|
438
|
+
console.log(`
|
|
439
|
+
Quilltap Database Tool
|
|
440
|
+
|
|
441
|
+
Usage: quilltap db [options] [sql]
|
|
442
|
+
|
|
443
|
+
Query your encrypted Quilltap database directly.
|
|
444
|
+
|
|
445
|
+
Options:
|
|
446
|
+
--tables List all tables
|
|
447
|
+
--count <table> Show row count for a table
|
|
448
|
+
--repl Interactive SQL prompt
|
|
449
|
+
--llm-logs Target the LLM logs database
|
|
450
|
+
--data-dir <path> Override data directory
|
|
451
|
+
--passphrase <pass> Provide passphrase for encrypted .dbkey
|
|
452
|
+
-h, --help Show this help
|
|
453
|
+
|
|
454
|
+
Examples:
|
|
455
|
+
quilltap db --tables
|
|
456
|
+
quilltap db "SELECT count(*) FROM characters"
|
|
457
|
+
quilltap db --count messages
|
|
458
|
+
quilltap db --repl
|
|
459
|
+
quilltap db --llm-logs --tables
|
|
460
|
+
`);
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
async function dbCommand(args) {
|
|
464
|
+
let dataDirOverride = '';
|
|
465
|
+
let passphrase = '';
|
|
466
|
+
let useLlmLogs = false;
|
|
467
|
+
let showTables = false;
|
|
468
|
+
let countTable = '';
|
|
469
|
+
let repl = false;
|
|
470
|
+
let sql = '';
|
|
471
|
+
let showHelp = false;
|
|
472
|
+
|
|
473
|
+
let i = 0;
|
|
474
|
+
while (i < args.length) {
|
|
475
|
+
switch (args[i]) {
|
|
476
|
+
case '--data-dir': case '-d': dataDirOverride = args[++i]; break;
|
|
477
|
+
case '--passphrase': passphrase = args[++i]; break;
|
|
478
|
+
case '--llm-logs': useLlmLogs = true; break;
|
|
479
|
+
case '--tables': showTables = true; break;
|
|
480
|
+
case '--count': countTable = args[++i]; break;
|
|
481
|
+
case '--repl': repl = true; break;
|
|
482
|
+
case '--help': case '-h': showHelp = true; break;
|
|
483
|
+
default:
|
|
484
|
+
if (args[i].startsWith('-')) {
|
|
485
|
+
console.error(`Unknown option: ${args[i]}`);
|
|
486
|
+
process.exit(1);
|
|
487
|
+
}
|
|
488
|
+
sql = args[i];
|
|
489
|
+
break;
|
|
490
|
+
}
|
|
491
|
+
i++;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
if (showHelp) {
|
|
495
|
+
printDbHelp();
|
|
496
|
+
process.exit(0);
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
const dataDir = resolveDataDir(dataDirOverride);
|
|
500
|
+
const dbFilename = useLlmLogs ? 'quilltap-llm-logs.db' : 'quilltap.db';
|
|
501
|
+
const dbPath = path.join(dataDir, dbFilename);
|
|
502
|
+
|
|
503
|
+
if (!fs.existsSync(dbPath)) {
|
|
504
|
+
console.error(`Database not found: ${dbPath}`);
|
|
505
|
+
process.exit(1);
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
// Load encryption key
|
|
509
|
+
let pepper;
|
|
510
|
+
try {
|
|
511
|
+
pepper = loadDbKey(dataDir, passphrase);
|
|
512
|
+
} catch (err) {
|
|
513
|
+
console.error(`Error: ${err.message}`);
|
|
514
|
+
process.exit(1);
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
// Open database
|
|
518
|
+
const Database = require('better-sqlite3');
|
|
519
|
+
const db = new Database(dbPath, { readonly: !repl });
|
|
520
|
+
|
|
521
|
+
if (pepper) {
|
|
522
|
+
const keyHex = Buffer.from(pepper, 'base64').toString('hex');
|
|
523
|
+
db.pragma(`key = "x'${keyHex}'"`);
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
try {
|
|
527
|
+
// Verify database is readable
|
|
528
|
+
db.prepare('SELECT 1').get();
|
|
529
|
+
} catch (err) {
|
|
530
|
+
console.error(`Cannot open database: ${err.message}`);
|
|
531
|
+
console.error('The database may be encrypted with a different key, or the .dbkey file may be missing.');
|
|
532
|
+
db.close();
|
|
533
|
+
process.exit(1);
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
try {
|
|
537
|
+
if (showTables) {
|
|
538
|
+
const tables = db.prepare("SELECT name FROM sqlite_master WHERE type='table' ORDER BY name").all();
|
|
539
|
+
for (const t of tables) console.log(t.name);
|
|
540
|
+
} else if (countTable) {
|
|
541
|
+
const row = db.prepare(`SELECT count(*) as count FROM "${countTable}"`).get();
|
|
542
|
+
console.log(row.count);
|
|
543
|
+
} else if (sql) {
|
|
544
|
+
const stmt = db.prepare(sql);
|
|
545
|
+
if (stmt.reader) {
|
|
546
|
+
const rows = stmt.all();
|
|
547
|
+
if (rows.length === 0) {
|
|
548
|
+
console.log('(no results)');
|
|
549
|
+
} else {
|
|
550
|
+
console.table(rows);
|
|
551
|
+
}
|
|
552
|
+
} else {
|
|
553
|
+
const info = stmt.run();
|
|
554
|
+
console.log(`Changes: ${info.changes}`);
|
|
555
|
+
}
|
|
556
|
+
} else if (repl) {
|
|
557
|
+
const readline = require('readline');
|
|
558
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout, prompt: 'quilltap> ' });
|
|
559
|
+
console.log(`Connected to ${dbPath}`);
|
|
560
|
+
console.log('Type .tables, .schema <table>, or SQL. Ctrl+D to exit.\n');
|
|
561
|
+
rl.prompt();
|
|
562
|
+
rl.on('line', (line) => {
|
|
563
|
+
const trimmed = line.trim();
|
|
564
|
+
if (!trimmed) { rl.prompt(); return; }
|
|
565
|
+
try {
|
|
566
|
+
if (trimmed === '.tables') {
|
|
567
|
+
const tables = db.prepare("SELECT name FROM sqlite_master WHERE type='table' ORDER BY name").all();
|
|
568
|
+
for (const t of tables) console.log(t.name);
|
|
569
|
+
} else if (trimmed.startsWith('.schema')) {
|
|
570
|
+
const table = trimmed.split(/\s+/)[1];
|
|
571
|
+
if (!table) { console.log('Usage: .schema <table>'); }
|
|
572
|
+
else {
|
|
573
|
+
const row = db.prepare("SELECT sql FROM sqlite_master WHERE name = ?").get(table);
|
|
574
|
+
console.log(row ? row.sql : `Table '${table}' not found`);
|
|
575
|
+
}
|
|
576
|
+
} else {
|
|
577
|
+
const stmt = db.prepare(trimmed);
|
|
578
|
+
if (stmt.reader) {
|
|
579
|
+
const rows = stmt.all();
|
|
580
|
+
if (rows.length === 0) console.log('(no results)');
|
|
581
|
+
else console.table(rows);
|
|
582
|
+
} else {
|
|
583
|
+
const info = stmt.run();
|
|
584
|
+
console.log(`Changes: ${info.changes}`);
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
} catch (err) {
|
|
588
|
+
console.error(`Error: ${err.message}`);
|
|
589
|
+
}
|
|
590
|
+
rl.prompt();
|
|
591
|
+
});
|
|
592
|
+
rl.on('close', () => {
|
|
593
|
+
db.close();
|
|
594
|
+
process.exit(0);
|
|
595
|
+
});
|
|
596
|
+
return; // Don't close db yet — REPL is interactive
|
|
597
|
+
} else {
|
|
598
|
+
printDbHelp();
|
|
599
|
+
}
|
|
600
|
+
} finally {
|
|
601
|
+
if (!repl) db.close();
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
// Route to subcommand or main
|
|
606
|
+
if (process.argv[2] === 'db') {
|
|
607
|
+
dbCommand(process.argv.slice(3));
|
|
608
|
+
} else {
|
|
609
|
+
main();
|
|
610
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "quilltap",
|
|
3
|
-
"version": "3.2.0-dev.
|
|
3
|
+
"version": "3.2.0-dev.38",
|
|
4
4
|
"description": "Self-hosted AI workspace for writers, worldbuilders, and roleplayers. Run with npx quilltap.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Charles Sebold",
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
"README.md"
|
|
34
34
|
],
|
|
35
35
|
"dependencies": {
|
|
36
|
-
"better-sqlite3": "
|
|
36
|
+
"better-sqlite3": "npm:better-sqlite3-multiple-ciphers@^12.6.2",
|
|
37
37
|
"sharp": "^0.34.5",
|
|
38
38
|
"tar": "^7.4.3"
|
|
39
39
|
},
|