quilltap 3.3.0-dev → 3.3.0-dev.111

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 CHANGED
@@ -89,6 +89,23 @@ Quilltap stores its database, files, and logs in a platform-specific directory:
89
89
 
90
90
  Override with `--data-dir` or the `QUILLTAP_DATA_DIR` environment variable.
91
91
 
92
+ ## Theme Management
93
+
94
+ The CLI includes theme management commands:
95
+
96
+ ```bash
97
+ quilltap themes list # List all installed themes
98
+ quilltap themes install my.qtap-theme # Install a .qtap-theme bundle
99
+ quilltap themes validate my.qtap-theme # Validate a bundle
100
+ quilltap themes uninstall my-theme # Uninstall a bundle theme
101
+ quilltap themes export earl-grey # Export any theme as a bundle
102
+ quilltap themes create sunset # Scaffold a new theme
103
+ quilltap themes search "dark" # Search registries
104
+ quilltap themes update # Check for theme updates
105
+ quilltap themes registry list # List configured registries
106
+ quilltap themes registry add <url> # Add a registry source
107
+ ```
108
+
92
109
  ## Requirements
93
110
 
94
111
  - Node.js 18 or later
@@ -96,7 +113,7 @@ Override with `--data-dir` or the `QUILLTAP_DATA_DIR` environment variable.
96
113
  ## Other Ways to Run Quilltap
97
114
 
98
115
  - **Electron desktop app** (macOS, Windows) - [Download](https://github.com/foundry-9/quilltap/releases)
99
- - **Docker** - `docker run -d -p 3000:3000 -v /path/to/data:/app/quilltap csebold/quilltap`
116
+ - **Docker** - `docker run -d -p 3000:3000 -v /path/to/data:/app/quilltap foundry9/quilltap`
100
117
 
101
118
  ## Links
102
119
 
package/bin/quilltap.js CHANGED
@@ -57,6 +57,7 @@ function parseArgs(argv) {
57
57
  opts.help = true;
58
58
  break;
59
59
  default:
60
+ // Allow subcommands to pass through (they're handled before parseArgs)
60
61
  console.error(`Unknown argument: ${args[i]}`);
61
62
  console.error('Run "quilltap --help" for usage information.');
62
63
  process.exit(1);
@@ -73,6 +74,10 @@ Quilltap - Self-hosted AI workspace
73
74
 
74
75
  Usage: quilltap [options]
75
76
 
77
+ Subcommands:
78
+ db Query encrypted databases
79
+ themes Manage theme bundles
80
+
76
81
  Options:
77
82
  -p, --port <number> Port to listen on (default: 3000)
78
83
  -d, --data-dir <path> Data directory (default: platform-specific)
@@ -131,20 +136,21 @@ function resolveModuleDir(moduleName) {
131
136
  function ensureNativeModules() {
132
137
  const needsRebuild = [];
133
138
 
134
- // Check better-sqlite3: it lazy-loads the native .node binary only when you
135
- // create a Database, so a bare require('better-sqlite3') always succeeds.
139
+ // Check better-sqlite3-multiple-ciphers (provides SQLCipher encryption support).
140
+ // The main app depends on this via an npm alias as 'better-sqlite3', so we must
141
+ // ensure the SQLCipher-capable version is available and link it as 'better-sqlite3'.
136
142
  // We must load the native binding directly to detect NODE_MODULE_VERSION mismatches.
137
- // Use require.resolve to find it regardless of npm hoisting.
138
143
  try {
139
- const modDir = resolveModuleDir('better-sqlite3');
144
+ const modDir = resolveModuleDir('better-sqlite3-multiple-ciphers')
145
+ || resolveModuleDir('better-sqlite3');
140
146
  if (!modDir) throw Object.assign(new Error('not found'), { code: 'MODULE_NOT_FOUND' });
141
147
  const bindingsPath = path.join(modDir, 'build', 'Release', 'better_sqlite3.node');
142
148
  require(bindingsPath);
143
149
  } catch (err) {
144
150
  if (err.message && err.message.includes('NODE_MODULE_VERSION')) {
145
- needsRebuild.push('better-sqlite3');
151
+ needsRebuild.push('better-sqlite3-multiple-ciphers');
146
152
  } else if (err.code === 'MODULE_NOT_FOUND') {
147
- needsRebuild.push('better-sqlite3');
153
+ needsRebuild.push('better-sqlite3-multiple-ciphers');
148
154
  }
149
155
  }
150
156
 
@@ -223,8 +229,10 @@ function linkNativeModules(standaloneDir) {
223
229
  }
224
230
  }
225
231
 
226
- // Link better-sqlite3
227
- const betterSqlite3Dir = resolveModuleDir('better-sqlite3');
232
+ // Link better-sqlite3-multiple-ciphers as 'better-sqlite3' (the app imports it
233
+ // via npm alias). Prefer the SQLCipher build; fall back to plain better-sqlite3.
234
+ const betterSqlite3Dir = resolveModuleDir('better-sqlite3-multiple-ciphers')
235
+ || resolveModuleDir('better-sqlite3');
228
236
  linkModule('better-sqlite3', betterSqlite3Dir);
229
237
 
230
238
  // Link sharp
@@ -383,10 +391,59 @@ function resolveDataDir(overrideDir) {
383
391
  return path.join(home, '.quilltap', 'data');
384
392
  }
385
393
 
394
+ /**
395
+ * Prompt for a passphrase interactively with hidden input.
396
+ * Returns a promise that resolves to the entered passphrase.
397
+ */
398
+ function promptPassphrase(prompt) {
399
+ return new Promise((resolve, reject) => {
400
+ const readline = require('readline');
401
+ if (!process.stdin.isTTY) {
402
+ reject(new Error('This database requires a passphrase. Use --passphrase <pass> or set QUILLTAP_DB_PASSPHRASE'));
403
+ return;
404
+ }
405
+ process.stdout.write(prompt || 'Passphrase: ');
406
+ const rl = readline.createInterface({ input: process.stdin, terminal: false });
407
+ // Disable echo by switching stdin to raw mode
408
+ process.stdin.setRawMode(true);
409
+ process.stdin.resume();
410
+ let passphrase = '';
411
+ const onData = (ch) => {
412
+ const c = ch.toString();
413
+ if (c === '\n' || c === '\r' || c === '\u0004') {
414
+ // Enter or Ctrl+D — done
415
+ process.stdin.setRawMode(false);
416
+ process.stdin.removeListener('data', onData);
417
+ process.stdin.pause();
418
+ rl.close();
419
+ process.stdout.write('\n');
420
+ resolve(passphrase);
421
+ } else if (c === '\u0003') {
422
+ // Ctrl+C — abort
423
+ process.stdin.setRawMode(false);
424
+ process.stdin.removeListener('data', onData);
425
+ process.stdin.pause();
426
+ rl.close();
427
+ process.stdout.write('\n');
428
+ process.exit(130);
429
+ } else if (c === '\u007F' || c === '\b') {
430
+ // Backspace
431
+ if (passphrase.length > 0) {
432
+ passphrase = passphrase.slice(0, -1);
433
+ }
434
+ } else {
435
+ passphrase += c;
436
+ }
437
+ };
438
+ process.stdin.on('data', onData);
439
+ });
440
+ }
441
+
386
442
  /**
387
443
  * Read and decrypt the .dbkey file to get the SQLCipher key.
444
+ * If passphrase is needed and not provided, prompts interactively.
388
445
  */
389
- function loadDbKey(dataDir, passphrase) {
446
+ async function loadDbKey(dataDir, passphrase) {
390
447
  const crypto = require('crypto');
391
448
  const dbkeyPath = path.join(dataDir, 'quilltap.dbkey');
392
449
  if (!fs.existsSync(dbkeyPath)) {
@@ -426,9 +483,17 @@ function loadDbKey(dataDir, passphrase) {
426
483
  // Internal passphrase failed — need user passphrase
427
484
  }
428
485
 
429
- // User passphrase required
486
+ // Check environment variable if no CLI passphrase provided
487
+ if (!passphrase && process.env.QUILLTAP_DB_PASSPHRASE) {
488
+ passphrase = process.env.QUILLTAP_DB_PASSPHRASE;
489
+ }
490
+
491
+ // Prompt interactively if still no passphrase
430
492
  if (!passphrase) {
431
- throw new Error('This database requires a passphrase. Use --passphrase <pass>');
493
+ passphrase = await promptPassphrase('Database passphrase: ');
494
+ if (!passphrase) {
495
+ throw new Error('No passphrase provided');
496
+ }
432
497
  }
433
498
 
434
499
  return tryDecrypt(passphrase);
@@ -451,12 +516,17 @@ Options:
451
516
  --passphrase <pass> Provide passphrase for encrypted .dbkey
452
517
  -h, --help Show this help
453
518
 
519
+ If a passphrase is required and not provided via --passphrase, the tool
520
+ will check the QUILLTAP_DB_PASSPHRASE environment variable, then prompt
521
+ interactively (with hidden input) if a TTY is available.
522
+
454
523
  Examples:
455
524
  quilltap db --tables
456
525
  quilltap db "SELECT count(*) FROM characters"
457
526
  quilltap db --count messages
458
527
  quilltap db --repl
459
528
  quilltap db --llm-logs --tables
529
+ QUILLTAP_DB_PASSPHRASE=secret quilltap db --tables
460
530
  `);
461
531
  }
462
532
 
@@ -508,14 +578,19 @@ async function dbCommand(args) {
508
578
  // Load encryption key
509
579
  let pepper;
510
580
  try {
511
- pepper = loadDbKey(dataDir, passphrase);
581
+ pepper = await loadDbKey(dataDir, passphrase);
512
582
  } catch (err) {
513
583
  console.error(`Error: ${err.message}`);
514
584
  process.exit(1);
515
585
  }
516
586
 
517
- // Open database
518
- const Database = require('better-sqlite3');
587
+ // Open database — prefer SQLCipher-capable build
588
+ let Database;
589
+ try {
590
+ Database = require('better-sqlite3-multiple-ciphers');
591
+ } catch {
592
+ Database = require('better-sqlite3');
593
+ }
519
594
  const db = new Database(dbPath, { readonly: !repl });
520
595
 
521
596
  if (pepper) {
@@ -605,6 +680,9 @@ async function dbCommand(args) {
605
680
  // Route to subcommand or main
606
681
  if (process.argv[2] === 'db') {
607
682
  dbCommand(process.argv.slice(3));
683
+ } else if (process.argv[2] === 'themes') {
684
+ const { themesCommand } = require('../lib/theme-commands');
685
+ themesCommand(process.argv.slice(3));
608
686
  } else {
609
687
  main();
610
688
  }