quilltap 3.3.0-dev → 3.3.0-dev.108
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 +18 -1
- package/bin/quilltap.js +92 -14
- package/lib/theme-commands.js +1223 -0
- package/lib/theme-validation.js +386 -0
- package/package.json +5 -4
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
|
|
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
|
|
135
|
-
//
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|