quilltap 4.6.0-dev → 4.6.0-dev.105

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.
@@ -5,6 +5,7 @@ const {
5
5
  printDefaultInstanceHint,
6
6
  loadDbKey,
7
7
  openMainDb,
8
+ openMountIndexDb,
8
9
  UUID_RE,
9
10
  resolveCharacter,
10
11
  resolveChat,
@@ -179,7 +180,10 @@ async function openDb(flags) {
179
180
  const { dataDir, passphrase } = resolved;
180
181
  const pepper = await loadDbKey(dataDir, passphrase);
181
182
  const db = openMainDb(dataDir, pepper, { readonly: true });
182
- return { db, dataDir };
183
+ // Lazy opener for the mount-index DB so `resolveCharacter` can match
184
+ // vault-stored aliases (4.6 cutover moved them out of the `characters` row).
185
+ const openMounts = () => openMountIndexDb(dataDir, pepper, { readonly: true });
186
+ return { db, dataDir, openMounts };
183
187
  }
184
188
 
185
189
  // ---------- filter / sort builders ----------
@@ -188,13 +192,13 @@ async function openDb(flags) {
188
192
  // `{ where: 'WHERE m.x = ? AND ...', params: [...], meta: { characterId, ... } }`.
189
193
  // `meta` exposes resolved IDs so callers can decide e.g. whether to show a
190
194
  // per-row holder column.
191
- function buildWhereClause(db, flags) {
195
+ function buildWhereClause(db, flags, openMounts = null) {
192
196
  const clauses = [];
193
197
  const params = [];
194
198
  const meta = { characterId: null, aboutId: null, chatId: null, projectId: null, allCharacters: true };
195
199
 
196
200
  if (flags.character && flags.character !== 'all') {
197
- const c = resolveCharacter(db, flags.character);
201
+ const c = resolveCharacter(db, flags.character, openMounts);
198
202
  clauses.push('m.characterId = ?');
199
203
  params.push(c.id);
200
204
  meta.characterId = c.id;
@@ -207,7 +211,7 @@ function buildWhereClause(db, flags) {
207
211
  } else if (flags.about === 'none') {
208
212
  clauses.push('m.aboutCharacterId IS NULL');
209
213
  } else {
210
- const a = resolveCharacter(db, flags.about);
214
+ const a = resolveCharacter(db, flags.about, openMounts);
211
215
  clauses.push('m.aboutCharacterId = ?');
212
216
  params.push(a.id);
213
217
  meta.aboutId = a.id;
@@ -395,9 +399,9 @@ function renderJson(obj) {
395
399
  // ---------- ls ----------
396
400
 
397
401
  async function cmdLs(flags) {
398
- const { db } = await openDb(flags);
402
+ const { db, openMounts } = await openDb(flags);
399
403
  try {
400
- const { where, params, meta } = buildWhereClause(db, flags);
404
+ const { where, params, meta } = buildWhereClause(db, flags, openMounts);
401
405
  const { order, impField } = buildOrderBy(flags.sort, flags.reverse);
402
406
  const limit = flags.limit > 0 ? flags.limit : 50;
403
407
  const sql = `${SELECT_BASE} ${where} ORDER BY ${order} LIMIT ?`;
@@ -512,9 +516,9 @@ async function cmdFind(flags, positional) {
512
516
  if (!['summary', 'content', 'both'].includes(inWhere)) {
513
517
  throw new Error(`--in must be one of: summary, content, both (got '${inWhere}')`);
514
518
  }
515
- const { db } = await openDb(flags);
519
+ const { db, openMounts } = await openDb(flags);
516
520
  try {
517
- const { where, params, meta } = buildWhereClause(db, flags);
521
+ const { where, params, meta } = buildWhereClause(db, flags, openMounts);
518
522
  const like = `%${pattern}%`;
519
523
 
520
524
  const matchClauses = [];
@@ -584,9 +588,9 @@ async function cmdSemanticGrep(flags, query) {
584
588
  // Resolve character locally so the server gets a stable UUID.
585
589
  let characterId;
586
590
  {
587
- const { db } = await openDb(flags);
591
+ const { db, openMounts } = await openDb(flags);
588
592
  try {
589
- const resolved = resolveCharacter(db, flags.character);
593
+ const resolved = resolveCharacter(db, flags.character, openMounts);
590
594
  characterId = resolved.id;
591
595
  } finally {
592
596
  db.close();
@@ -675,9 +679,9 @@ async function cmdGrep(flags, positional) {
675
679
  if (flags.semantic) {
676
680
  return cmdSemanticGrep(flags, pattern);
677
681
  }
678
- const { db } = await openDb(flags);
682
+ const { db, openMounts } = await openDb(flags);
679
683
  try {
680
- const { where, params } = buildWhereClause(db, flags);
684
+ const { where, params } = buildWhereClause(db, flags, openMounts);
681
685
  // Always restrict to rows whose content can match — quick pre-filter so we
682
686
  // don't read all 32k rows into JS just to drop most of them.
683
687
  const likeNeedle = flags.ignoreCase ? `%${pattern.toLowerCase()}%` : `%${pattern}%`;
@@ -1044,11 +1048,11 @@ function graphToJson(node) {
1044
1048
  // ---------- status ----------
1045
1049
 
1046
1050
  async function cmdStatus(flags) {
1047
- const { db } = await openDb(flags);
1051
+ const { db, openMounts } = await openDb(flags);
1048
1052
  try {
1049
1053
  let holderRows;
1050
1054
  if (flags.character && flags.character !== 'all') {
1051
- const c = resolveCharacter(db, flags.character);
1055
+ const c = resolveCharacter(db, flags.character, openMounts);
1052
1056
  holderRows = [{ id: c.id, name: c.name }];
1053
1057
  } else {
1054
1058
  holderRows = db.prepare(`
@@ -1180,12 +1184,12 @@ function renderStatusBlock(holder, stats) {
1180
1184
  // ---------- validate ----------
1181
1185
 
1182
1186
  async function cmdValidate(flags) {
1183
- const { db } = await openDb(flags);
1187
+ const { db, openMounts } = await openDb(flags);
1184
1188
  try {
1185
1189
  let holderIds = null;
1186
1190
  let holderRows;
1187
1191
  if (flags.character && flags.character !== 'all') {
1188
- const c = resolveCharacter(db, flags.character);
1192
+ const c = resolveCharacter(db, flags.character, openMounts);
1189
1193
  holderRows = [{ id: c.id, name: c.name }];
1190
1194
  holderIds = [c.id];
1191
1195
  } else {
@@ -0,0 +1,151 @@
1
+ 'use strict';
2
+
3
+ // Shared helpers for keeping native modules compiled against the current Node
4
+ // ABI. Used by both the runtime CLI entry (bin/quilltap.js) and the package's
5
+ // `postinstall` hook, so a fresh install picks up the correct binaries up front
6
+ // and a later Node upgrade still self-heals on first run.
7
+
8
+ const path = require('path');
9
+ const { execSync } = require('child_process');
10
+
11
+ const PACKAGE_DIR = path.resolve(__dirname, '..');
12
+
13
+ // Resolve a native module's directory, handling npm hoisting.
14
+ // Returns the directory containing package.json, or null if not found.
15
+ function resolveModuleDir(moduleName) {
16
+ try {
17
+ const pkgJson = require.resolve(moduleName + '/package.json', { paths: [PACKAGE_DIR] });
18
+ return path.dirname(pkgJson);
19
+ } catch {
20
+ return null;
21
+ }
22
+ }
23
+
24
+ // node-pty needs a `spawn-helper` executable beside the pty.node it loads, or
25
+ // pty.spawn() fails with `posix_spawnp failed`. An ABI rebuild lands a fresh
26
+ // build/Release/pty.node (which node-pty's loader prefers over prebuilds/) but
27
+ // emits only the addon, not node-pty's separate spawn-helper target; tar/extract
28
+ // can also drop the exec bit on the shipped prebuilds/*/spawn-helper. spawn-helper
29
+ // is a plain executable (no Node linkage) so the prebuilt copy is ABI-independent
30
+ // and safe to reuse. Best-effort; never throws.
31
+ function reconcileNodePtySpawnHelper() {
32
+ if (process.platform === 'win32') return; // conpty has no spawn-helper
33
+ const fs = require('fs');
34
+ try {
35
+ const nodePtyDir = resolveModuleDir('node-pty');
36
+ if (!nodePtyDir) return;
37
+ const prebuildsDir = path.join(nodePtyDir, 'prebuilds');
38
+ const prebuiltHelper = path.join(prebuildsDir, `${process.platform}-${process.arch}`, 'spawn-helper');
39
+
40
+ if (fs.existsSync(prebuildsDir)) {
41
+ for (const entry of fs.readdirSync(prebuildsDir)) {
42
+ const helper = path.join(prebuildsDir, entry, 'spawn-helper');
43
+ if (fs.existsSync(helper)) {
44
+ try { fs.chmodSync(helper, 0o755); } catch { /* best-effort */ }
45
+ }
46
+ }
47
+ }
48
+
49
+ for (const buildType of ['Release', 'Debug']) {
50
+ const buildDir = path.join(nodePtyDir, 'build', buildType);
51
+ const builtAddon = path.join(buildDir, 'pty.node');
52
+ const builtHelper = path.join(buildDir, 'spawn-helper');
53
+ if (fs.existsSync(builtHelper)) {
54
+ try { fs.chmodSync(builtHelper, 0o755); } catch { /* best-effort */ }
55
+ } else if (fs.existsSync(builtAddon) && fs.existsSync(prebuiltHelper)) {
56
+ fs.copyFileSync(prebuiltHelper, builtHelper);
57
+ fs.chmodSync(builtHelper, 0o755);
58
+ console.log(` node-pty: backfilled build/${buildType}/spawn-helper from prebuilds`);
59
+ }
60
+ }
61
+ } catch {
62
+ // best-effort — node-pty terminals are optional; never block the CLI
63
+ }
64
+ }
65
+
66
+ // Check if native modules are compiled for the current Node.js version.
67
+ // This handles the case where npx caches the package but the user upgrades
68
+ // Node.js — the cached native modules will have a stale NODE_MODULE_VERSION.
69
+ // Returns true if everything was healthy or successfully rebuilt; false on
70
+ // rebuild failure. Never throws.
71
+ function ensureNativeModules() {
72
+ const needsRebuild = [];
73
+
74
+ // Check better-sqlite3-multiple-ciphers (provides SQLCipher encryption support).
75
+ // The main app depends on this via an npm alias as 'better-sqlite3', so we must
76
+ // ensure the SQLCipher-capable version is available and link it as 'better-sqlite3'.
77
+ // We must load the native binding directly to detect NODE_MODULE_VERSION mismatches.
78
+ try {
79
+ const modDir = resolveModuleDir('better-sqlite3-multiple-ciphers')
80
+ || resolveModuleDir('better-sqlite3');
81
+ if (!modDir) throw Object.assign(new Error('not found'), { code: 'MODULE_NOT_FOUND' });
82
+ const bindingsPath = path.join(modDir, 'build', 'Release', 'better_sqlite3.node');
83
+ require(bindingsPath);
84
+ } catch (err) {
85
+ if (err.message && err.message.includes('NODE_MODULE_VERSION')) {
86
+ needsRebuild.push('better-sqlite3-multiple-ciphers');
87
+ } else if (err.code === 'MODULE_NOT_FOUND') {
88
+ needsRebuild.push('better-sqlite3-multiple-ciphers');
89
+ }
90
+ }
91
+
92
+ // Check sharp: loads its native binding eagerly on require.
93
+ try {
94
+ require.resolve('sharp', { paths: [PACKAGE_DIR] });
95
+ require('sharp');
96
+ } catch (err) {
97
+ if (err.message && err.message.includes('NODE_MODULE_VERSION')) {
98
+ needsRebuild.push('sharp');
99
+ } else if (err.code === 'MODULE_NOT_FOUND') {
100
+ needsRebuild.push('sharp');
101
+ }
102
+ }
103
+
104
+ // Check node-pty: backs the Ariel terminal feature. Loaded dynamically by
105
+ // pty-manager in the standalone server, so resolution must succeed and the
106
+ // native binding's NODE_MODULE_VERSION must match the runtime.
107
+ try {
108
+ require.resolve('node-pty', { paths: [PACKAGE_DIR] });
109
+ require('node-pty');
110
+ } catch (err) {
111
+ if (err.message && err.message.includes('NODE_MODULE_VERSION')) {
112
+ needsRebuild.push('node-pty');
113
+ } else if (err.code === 'MODULE_NOT_FOUND') {
114
+ needsRebuild.push('node-pty');
115
+ }
116
+ }
117
+
118
+ if (needsRebuild.length === 0) {
119
+ reconcileNodePtySpawnHelper();
120
+ return true;
121
+ }
122
+
123
+ console.log(` Rebuilding native modules for Node.js ${process.version}...`);
124
+
125
+ try {
126
+ execSync(`npm rebuild ${needsRebuild.join(' ')}`, {
127
+ cwd: PACKAGE_DIR,
128
+ stdio: 'inherit',
129
+ });
130
+ console.log(' Done.');
131
+ console.log('');
132
+ reconcileNodePtySpawnHelper();
133
+ return true;
134
+ } catch (err) {
135
+ console.error('');
136
+ console.error(` Warning: Failed to rebuild native modules: ${err.message}`);
137
+ console.error(' Try running: npm rebuild --prefix ' + PACKAGE_DIR);
138
+ console.error('');
139
+ return false;
140
+ }
141
+ }
142
+
143
+ module.exports = { resolveModuleDir, ensureNativeModules, reconcileNodePtySpawnHelper, PACKAGE_DIR };
144
+
145
+ // Allow this file to be invoked directly as a postinstall script:
146
+ // node lib/native-modules.js
147
+ // Exits 0 on success or graceful warning; never blocks npm install on failure.
148
+ if (require.main === module) {
149
+ ensureNativeModules();
150
+ process.exit(0);
151
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "quilltap",
3
- "version": "4.6.0-dev",
3
+ "version": "4.6.0-dev.105",
4
4
  "description": "Self-hosted AI workspace for writers, worldbuilders, and roleplayers. Run with npx quilltap.",
5
5
  "author": {
6
6
  "name": "Charles Sebold",
@@ -27,6 +27,9 @@
27
27
  "bin": {
28
28
  "quilltap": "bin/quilltap.js"
29
29
  },
30
+ "scripts": {
31
+ "postinstall": "node lib/native-modules.js"
32
+ },
30
33
  "files": [
31
34
  "bin/",
32
35
  "lib/",
@@ -34,11 +37,11 @@
34
37
  ],
35
38
  "dependencies": {
36
39
  "@napi-rs/canvas": "^0.1.100",
37
- "better-sqlite3-multiple-ciphers": "^12.9.0",
40
+ "better-sqlite3-multiple-ciphers": "^12.10.0",
38
41
  "node-pty": "^1.1.0",
39
42
  "sharp": "^0.34.5",
40
43
  "tar": "^7.5.15",
41
- "yauzl": "^3.3.0"
44
+ "yauzl": "^3.3.1"
42
45
  },
43
46
  "engines": {
44
47
  "node": ">=24.0.0"