quilltap 3.3.0-dev.17 → 3.3.0-dev.176
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 +361 -4
- package/package.json +5 -5
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
|
@@ -391,10 +391,59 @@ function resolveDataDir(overrideDir) {
|
|
|
391
391
|
return path.join(home, '.quilltap', 'data');
|
|
392
392
|
}
|
|
393
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
|
+
|
|
394
442
|
/**
|
|
395
443
|
* Read and decrypt the .dbkey file to get the SQLCipher key.
|
|
444
|
+
* If passphrase is needed and not provided, prompts interactively.
|
|
396
445
|
*/
|
|
397
|
-
function loadDbKey(dataDir, passphrase) {
|
|
446
|
+
async function loadDbKey(dataDir, passphrase) {
|
|
398
447
|
const crypto = require('crypto');
|
|
399
448
|
const dbkeyPath = path.join(dataDir, 'quilltap.dbkey');
|
|
400
449
|
if (!fs.existsSync(dbkeyPath)) {
|
|
@@ -434,14 +483,297 @@ function loadDbKey(dataDir, passphrase) {
|
|
|
434
483
|
// Internal passphrase failed — need user passphrase
|
|
435
484
|
}
|
|
436
485
|
|
|
437
|
-
//
|
|
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
|
|
438
492
|
if (!passphrase) {
|
|
439
|
-
|
|
493
|
+
passphrase = await promptPassphrase('Database passphrase: ');
|
|
494
|
+
if (!passphrase) {
|
|
495
|
+
throw new Error('No passphrase provided');
|
|
496
|
+
}
|
|
440
497
|
}
|
|
441
498
|
|
|
442
499
|
return tryDecrypt(passphrase);
|
|
443
500
|
}
|
|
444
501
|
|
|
502
|
+
// ============================================================================
|
|
503
|
+
// Instance Lock CLI Commands
|
|
504
|
+
// ============================================================================
|
|
505
|
+
|
|
506
|
+
/**
|
|
507
|
+
* Check whether a PID is alive using signal 0.
|
|
508
|
+
*/
|
|
509
|
+
function isPidAlive(pid) {
|
|
510
|
+
try {
|
|
511
|
+
process.kill(pid, 0);
|
|
512
|
+
return true;
|
|
513
|
+
} catch (err) {
|
|
514
|
+
return err.code === 'EPERM'; // EPERM = exists but no permission
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
/**
|
|
519
|
+
* Verify a PID looks like a Node/Quilltap process (best-effort).
|
|
520
|
+
*/
|
|
521
|
+
function verifyPidIsNode(pid, expectedArgv0) {
|
|
522
|
+
try {
|
|
523
|
+
if (process.platform === 'linux') {
|
|
524
|
+
try {
|
|
525
|
+
const cmdline = fs.readFileSync(`/proc/${pid}/cmdline`, 'utf8');
|
|
526
|
+
const cmd = cmdline.split('\0')[0] || '';
|
|
527
|
+
return /node|electron|quilltap|next-server/i.test(cmd);
|
|
528
|
+
} catch {
|
|
529
|
+
return true; // Can't read — assume match
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
if (process.platform === 'darwin') {
|
|
533
|
+
const { execSync } = require('child_process');
|
|
534
|
+
const output = execSync(`ps -p ${pid} -o comm=`, {
|
|
535
|
+
encoding: 'utf8', timeout: 2000, stdio: ['pipe', 'pipe', 'pipe'],
|
|
536
|
+
}).trim();
|
|
537
|
+
return /node|electron|quilltap|next-server/i.test(output);
|
|
538
|
+
}
|
|
539
|
+
if (process.platform === 'win32') {
|
|
540
|
+
const { execSync } = require('child_process');
|
|
541
|
+
const output = execSync(`tasklist /FI "PID eq ${pid}" /NH`, {
|
|
542
|
+
encoding: 'utf8', timeout: 2000, stdio: ['pipe', 'pipe', 'pipe'],
|
|
543
|
+
}).trim();
|
|
544
|
+
return /node|electron|quilltap|next-server/i.test(output);
|
|
545
|
+
}
|
|
546
|
+
return true;
|
|
547
|
+
} catch {
|
|
548
|
+
return true;
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
/**
|
|
553
|
+
* Handle --lock-status, --lock-clean, --lock-override commands.
|
|
554
|
+
*/
|
|
555
|
+
function handleLockCommand(dataDir, opts) {
|
|
556
|
+
const lockPath = path.join(dataDir, 'quilltap.lock');
|
|
557
|
+
const hostname = require('os').hostname();
|
|
558
|
+
|
|
559
|
+
// Read lock file
|
|
560
|
+
let lock = null;
|
|
561
|
+
try {
|
|
562
|
+
if (fs.existsSync(lockPath)) {
|
|
563
|
+
const raw = fs.readFileSync(lockPath, 'utf8');
|
|
564
|
+
lock = JSON.parse(raw);
|
|
565
|
+
}
|
|
566
|
+
} catch (err) {
|
|
567
|
+
if (opts.lockStatus) {
|
|
568
|
+
console.log('Lock file exists but is corrupt or unreadable.');
|
|
569
|
+
console.log(` Path: ${lockPath}`);
|
|
570
|
+
if (opts.lockClean) {
|
|
571
|
+
fs.unlinkSync(lockPath);
|
|
572
|
+
console.log(' Removed corrupt lock file.');
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
return;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
// --lock-status: display current lock state
|
|
579
|
+
if (opts.lockStatus) {
|
|
580
|
+
if (!lock) {
|
|
581
|
+
console.log('No instance lock found. Database is not currently claimed.');
|
|
582
|
+
console.log(` Lock path: ${lockPath}`);
|
|
583
|
+
return;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
const sameHost = lock.hostname === hostname;
|
|
587
|
+
const alive = sameHost && isPidAlive(lock.pid);
|
|
588
|
+
const isNode = alive ? verifyPidIsNode(lock.pid, lock.processArgv0 || '') : false;
|
|
589
|
+
|
|
590
|
+
// Status line
|
|
591
|
+
let status;
|
|
592
|
+
if (alive && isNode) {
|
|
593
|
+
status = '\x1b[32mACTIVE\x1b[0m (process confirmed running)';
|
|
594
|
+
} else if (alive && !isNode) {
|
|
595
|
+
status = '\x1b[33mSUSPECT\x1b[0m (PID alive but does not look like Quilltap — possible PID reuse)';
|
|
596
|
+
} else if (!sameHost) {
|
|
597
|
+
// Different hostname — could be a VM/container on this machine
|
|
598
|
+
const isVMOrContainer = ['docker', 'lima', 'wsl2'].includes(lock.environment);
|
|
599
|
+
const heartbeatAgeMs = lock.lastHeartbeat
|
|
600
|
+
? Date.now() - new Date(lock.lastHeartbeat).getTime()
|
|
601
|
+
: Infinity;
|
|
602
|
+
const heartbeatFreshMs = 5 * 60 * 1000;
|
|
603
|
+
|
|
604
|
+
if (isVMOrContainer && heartbeatAgeMs < heartbeatFreshMs) {
|
|
605
|
+
const ageStr = Math.round(heartbeatAgeMs / 1000) + 's';
|
|
606
|
+
status = `\x1b[32mACTIVE (${lock.environment}, heartbeat ${ageStr} ago)\x1b[0m`;
|
|
607
|
+
} else if (isVMOrContainer) {
|
|
608
|
+
status = `\x1b[33mSTALE (${lock.environment}, no recent heartbeat)\x1b[0m — will be auto-claimed on next startup`;
|
|
609
|
+
} else {
|
|
610
|
+
status = '\x1b[33mSTALE (different host)\x1b[0m — will be auto-claimed on next startup';
|
|
611
|
+
}
|
|
612
|
+
} else {
|
|
613
|
+
status = '\x1b[31mSTALE (process dead)\x1b[0m — will be auto-claimed on next startup';
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
console.log(`Instance Lock Status: ${status}`);
|
|
617
|
+
console.log();
|
|
618
|
+
console.log(` PID: ${lock.pid}`);
|
|
619
|
+
console.log(` Hostname: ${lock.hostname}${sameHost ? ' (this host)' : ' (different host)'}`);
|
|
620
|
+
console.log(` Environment: ${lock.environment || 'unknown'}`);
|
|
621
|
+
console.log(` Process: ${lock.processTitle || 'unknown'}`);
|
|
622
|
+
console.log(` Started: ${lock.startedAt || 'unknown'}`);
|
|
623
|
+
|
|
624
|
+
// Show heartbeat age if available
|
|
625
|
+
if (lock.lastHeartbeat) {
|
|
626
|
+
const heartbeatAge = Math.round((Date.now() - new Date(lock.lastHeartbeat).getTime()) / 1000);
|
|
627
|
+
let heartbeatDisplay;
|
|
628
|
+
if (heartbeatAge < 120) {
|
|
629
|
+
heartbeatDisplay = `${heartbeatAge}s ago`;
|
|
630
|
+
} else if (heartbeatAge < 7200) {
|
|
631
|
+
heartbeatDisplay = `${Math.round(heartbeatAge / 60)}m ago`;
|
|
632
|
+
} else {
|
|
633
|
+
heartbeatDisplay = `${Math.round(heartbeatAge / 3600)}h ago`;
|
|
634
|
+
}
|
|
635
|
+
// Warn if heartbeat is older than 5 minutes (process may be hung)
|
|
636
|
+
if (alive && heartbeatAge > 300) {
|
|
637
|
+
heartbeatDisplay = `\x1b[33m${heartbeatDisplay} (stale — process may be hung)\x1b[0m`;
|
|
638
|
+
}
|
|
639
|
+
console.log(` Heartbeat: ${heartbeatDisplay}`);
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
console.log(` Lock file: ${lockPath}`);
|
|
643
|
+
|
|
644
|
+
if (lock.history && lock.history.length > 0) {
|
|
645
|
+
console.log();
|
|
646
|
+
console.log(` Recent history (${lock.history.length} entries):`);
|
|
647
|
+
const recent = lock.history.slice(-10);
|
|
648
|
+
for (const entry of recent) {
|
|
649
|
+
const ts = entry.timestamp ? entry.timestamp.replace('T', ' ').replace(/\.\d+Z$/, 'Z') : '?';
|
|
650
|
+
const detail = entry.detail ? ` — ${entry.detail}` : '';
|
|
651
|
+
console.log(` [${ts}] ${entry.event} (PID ${entry.pid})${detail}`);
|
|
652
|
+
}
|
|
653
|
+
if (lock.history.length > 10) {
|
|
654
|
+
console.log(` ... and ${lock.history.length - 10} earlier entries`);
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
return;
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
// --lock-clean: remove stale locks only
|
|
661
|
+
if (opts.lockClean) {
|
|
662
|
+
if (!lock) {
|
|
663
|
+
console.log('No lock file found. Nothing to clean.');
|
|
664
|
+
return;
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
const sameHost = lock.hostname === hostname;
|
|
668
|
+
const alive = sameHost && isPidAlive(lock.pid);
|
|
669
|
+
|
|
670
|
+
if (alive) {
|
|
671
|
+
const isNode = verifyPidIsNode(lock.pid, lock.processArgv0 || '');
|
|
672
|
+
if (isNode) {
|
|
673
|
+
console.log(`Lock is held by a live Quilltap process (PID ${lock.pid}). Cannot clean.`);
|
|
674
|
+
console.log('Stop the running instance first, or use --lock-override to force.');
|
|
675
|
+
process.exit(1);
|
|
676
|
+
} else {
|
|
677
|
+
console.log(`Lock references PID ${lock.pid} which is alive but does NOT look like a Quilltap process.`);
|
|
678
|
+
console.log('This is likely a stale lock with a reused PID. Removing.');
|
|
679
|
+
}
|
|
680
|
+
} else if (!sameHost) {
|
|
681
|
+
// Different hostname — check if it's a VM/container with a recent heartbeat
|
|
682
|
+
const isVMOrContainer = ['docker', 'lima', 'wsl2'].includes(lock.environment);
|
|
683
|
+
const heartbeatAgeMs = lock.lastHeartbeat
|
|
684
|
+
? Date.now() - new Date(lock.lastHeartbeat).getTime()
|
|
685
|
+
: Infinity;
|
|
686
|
+
const heartbeatFreshMs = 5 * 60 * 1000;
|
|
687
|
+
|
|
688
|
+
if (isVMOrContainer && heartbeatAgeMs < heartbeatFreshMs) {
|
|
689
|
+
const ageStr = Math.round(heartbeatAgeMs / 1000) + 's';
|
|
690
|
+
console.log(`Lock is held by a live ${lock.environment} instance (heartbeat ${ageStr} ago). Cannot clean.`);
|
|
691
|
+
console.log('Stop the other instance first, or use --lock-override to force.');
|
|
692
|
+
process.exit(1);
|
|
693
|
+
} else if (isVMOrContainer) {
|
|
694
|
+
console.log(`Lock was held by ${lock.environment} (${lock.hostname}) with no recent heartbeat. Removing stale lock.`);
|
|
695
|
+
} else {
|
|
696
|
+
console.log(`Lock was held by a different host (${lock.hostname}). Removing stale lock.`);
|
|
697
|
+
}
|
|
698
|
+
} else {
|
|
699
|
+
console.log(`Lock was held by PID ${lock.pid} which is no longer running. Removing stale lock.`);
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
// Write a final history entry before deleting
|
|
703
|
+
if (!lock.history) lock.history = [];
|
|
704
|
+
lock.history.push({
|
|
705
|
+
event: 'stale-claimed',
|
|
706
|
+
pid: process.pid,
|
|
707
|
+
hostname: hostname,
|
|
708
|
+
timestamp: new Date().toISOString(),
|
|
709
|
+
detail: `Cleaned via CLI (quilltap db --lock-clean)`,
|
|
710
|
+
});
|
|
711
|
+
|
|
712
|
+
// Write final state then remove
|
|
713
|
+
try {
|
|
714
|
+
fs.writeFileSync(lockPath, JSON.stringify(lock, null, 2) + '\n', 'utf8');
|
|
715
|
+
} catch { /* best effort */ }
|
|
716
|
+
|
|
717
|
+
try {
|
|
718
|
+
fs.unlinkSync(lockPath);
|
|
719
|
+
console.log('Lock file removed.');
|
|
720
|
+
} catch (err) {
|
|
721
|
+
console.error(`Failed to remove lock file: ${err.message}`);
|
|
722
|
+
process.exit(1);
|
|
723
|
+
}
|
|
724
|
+
return;
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
// --lock-override: forcibly claim the lock
|
|
728
|
+
if (opts.lockOverride) {
|
|
729
|
+
if (!lock) {
|
|
730
|
+
console.log('No lock file found. Nothing to override.');
|
|
731
|
+
return;
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
const sameHost = lock.hostname === hostname;
|
|
735
|
+
const alive = sameHost && isPidAlive(lock.pid);
|
|
736
|
+
|
|
737
|
+
if (alive) {
|
|
738
|
+
const isNode = verifyPidIsNode(lock.pid, lock.processArgv0 || '');
|
|
739
|
+
if (!isNode) {
|
|
740
|
+
console.error(`Lock override rejected: PID ${lock.pid} is alive but does not appear to be`);
|
|
741
|
+
console.error('a Quilltap/Node process. The PID may have been reused. Verify manually.');
|
|
742
|
+
process.exit(1);
|
|
743
|
+
}
|
|
744
|
+
console.log(`WARNING: Overriding lock held by live process (PID ${lock.pid}, ${lock.environment || 'unknown'}).`);
|
|
745
|
+
console.log('The other instance may corrupt the database if it is still writing.');
|
|
746
|
+
} else {
|
|
747
|
+
console.log(`Overriding stale lock (PID ${lock.pid} is no longer running).`);
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
// Record override in history
|
|
751
|
+
if (!lock.history) lock.history = [];
|
|
752
|
+
lock.history.push({
|
|
753
|
+
event: 'override',
|
|
754
|
+
pid: process.pid,
|
|
755
|
+
hostname: hostname,
|
|
756
|
+
timestamp: new Date().toISOString(),
|
|
757
|
+
detail: `Manual override via CLI (quilltap db --lock-override)` +
|
|
758
|
+
(alive ? ` — overriding live PID ${lock.pid}` : ` — PID ${lock.pid} was dead`),
|
|
759
|
+
});
|
|
760
|
+
|
|
761
|
+
// Write final state then remove so next startup gets a clean acquire
|
|
762
|
+
try {
|
|
763
|
+
fs.writeFileSync(lockPath, JSON.stringify(lock, null, 2) + '\n', 'utf8');
|
|
764
|
+
} catch { /* best effort */ }
|
|
765
|
+
|
|
766
|
+
try {
|
|
767
|
+
fs.unlinkSync(lockPath);
|
|
768
|
+
console.log('Lock file removed. Next Quilltap startup will acquire a fresh lock.');
|
|
769
|
+
} catch (err) {
|
|
770
|
+
console.error(`Failed to remove lock file: ${err.message}`);
|
|
771
|
+
process.exit(1);
|
|
772
|
+
}
|
|
773
|
+
return;
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
|
|
445
777
|
function printDbHelp() {
|
|
446
778
|
console.log(`
|
|
447
779
|
Quilltap Database Tool
|
|
@@ -459,12 +791,24 @@ Options:
|
|
|
459
791
|
--passphrase <pass> Provide passphrase for encrypted .dbkey
|
|
460
792
|
-h, --help Show this help
|
|
461
793
|
|
|
794
|
+
Instance Lock Commands:
|
|
795
|
+
--lock-status Show the current instance lock state
|
|
796
|
+
--lock-clean Remove stale locks (dead processes only)
|
|
797
|
+
--lock-override Forcibly claim the lock for this process
|
|
798
|
+
|
|
799
|
+
If a passphrase is required and not provided via --passphrase, the tool
|
|
800
|
+
will check the QUILLTAP_DB_PASSPHRASE environment variable, then prompt
|
|
801
|
+
interactively (with hidden input) if a TTY is available.
|
|
802
|
+
|
|
462
803
|
Examples:
|
|
463
804
|
quilltap db --tables
|
|
464
805
|
quilltap db "SELECT count(*) FROM characters"
|
|
465
806
|
quilltap db --count messages
|
|
466
807
|
quilltap db --repl
|
|
467
808
|
quilltap db --llm-logs --tables
|
|
809
|
+
quilltap db --lock-status
|
|
810
|
+
quilltap db --lock-clean
|
|
811
|
+
QUILLTAP_DB_PASSPHRASE=secret quilltap db --tables
|
|
468
812
|
`);
|
|
469
813
|
}
|
|
470
814
|
|
|
@@ -477,6 +821,9 @@ async function dbCommand(args) {
|
|
|
477
821
|
let repl = false;
|
|
478
822
|
let sql = '';
|
|
479
823
|
let showHelp = false;
|
|
824
|
+
let lockStatus = false;
|
|
825
|
+
let lockClean = false;
|
|
826
|
+
let lockOverride = false;
|
|
480
827
|
|
|
481
828
|
let i = 0;
|
|
482
829
|
while (i < args.length) {
|
|
@@ -488,6 +835,9 @@ async function dbCommand(args) {
|
|
|
488
835
|
case '--count': countTable = args[++i]; break;
|
|
489
836
|
case '--repl': repl = true; break;
|
|
490
837
|
case '--help': case '-h': showHelp = true; break;
|
|
838
|
+
case '--lock-status': lockStatus = true; break;
|
|
839
|
+
case '--lock-clean': lockClean = true; break;
|
|
840
|
+
case '--lock-override': lockOverride = true; break;
|
|
491
841
|
default:
|
|
492
842
|
if (args[i].startsWith('-')) {
|
|
493
843
|
console.error(`Unknown option: ${args[i]}`);
|
|
@@ -505,6 +855,13 @@ async function dbCommand(args) {
|
|
|
505
855
|
}
|
|
506
856
|
|
|
507
857
|
const dataDir = resolveDataDir(dataDirOverride);
|
|
858
|
+
|
|
859
|
+
// ---- Instance lock commands (no database open required) ----
|
|
860
|
+
if (lockStatus || lockClean || lockOverride) {
|
|
861
|
+
handleLockCommand(dataDir, { lockStatus, lockClean, lockOverride });
|
|
862
|
+
return;
|
|
863
|
+
}
|
|
864
|
+
|
|
508
865
|
const dbFilename = useLlmLogs ? 'quilltap-llm-logs.db' : 'quilltap.db';
|
|
509
866
|
const dbPath = path.join(dataDir, dbFilename);
|
|
510
867
|
|
|
@@ -516,7 +873,7 @@ async function dbCommand(args) {
|
|
|
516
873
|
// Load encryption key
|
|
517
874
|
let pepper;
|
|
518
875
|
try {
|
|
519
|
-
pepper = loadDbKey(dataDir, passphrase);
|
|
876
|
+
pepper = await loadDbKey(dataDir, passphrase);
|
|
520
877
|
} catch (err) {
|
|
521
878
|
console.error(`Error: ${err.message}`);
|
|
522
879
|
process.exit(1);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "quilltap",
|
|
3
|
-
"version": "3.3.0-dev.
|
|
3
|
+
"version": "3.3.0-dev.176",
|
|
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,12 +33,12 @@
|
|
|
33
33
|
"README.md"
|
|
34
34
|
],
|
|
35
35
|
"dependencies": {
|
|
36
|
-
"better-sqlite3-multiple-ciphers": "^12.
|
|
36
|
+
"better-sqlite3-multiple-ciphers": "^12.8.0",
|
|
37
37
|
"sharp": "^0.34.5",
|
|
38
|
-
"tar": "^7.
|
|
39
|
-
"yauzl": "^3.2.
|
|
38
|
+
"tar": "^7.5.13",
|
|
39
|
+
"yauzl": "^3.2.1"
|
|
40
40
|
},
|
|
41
41
|
"engines": {
|
|
42
|
-
"node": ">=
|
|
42
|
+
"node": ">=22.0.0"
|
|
43
43
|
}
|
|
44
44
|
}
|