quilltap 4.11.0 → 4.12.0-dev.1

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
@@ -65,7 +65,7 @@ quilltap --update
65
65
 
66
66
  ## How It Works
67
67
 
68
- The `quilltap` npm package is a lightweight CLI launcher (~10 KB). On first run, it downloads the pre-built application from [GitHub Releases](https://github.com/foundry-9/quilltap-server/releases) and caches it in a platform-specific directory. Native modules (`better-sqlite3`, `sharp`) are compiled for your platform when you install the npm package.
68
+ The `quilltap` npm package is a lightweight CLI launcher (~10 KB). On first run, it downloads the pre-built application from [GitHub Releases](https://github.com/foundry-9/quilltap-server/releases) and caches it in a platform-specific directory. Native modules (`better-sqlite3`, `sharp`) ship as prebuilt binaries for your platform — nothing is compiled on install. On Linux, the SQLCipher driver needs glibc 2.35 or newer (Ubuntu 22.04, Debian 12, RHEL 9 or newer); older hosts should run the Docker image.
69
69
 
70
70
  ### Cache Locations
71
71
 
package/bin/quilltap.js CHANGED
@@ -347,7 +347,7 @@ async function main() {
347
347
  process.exit(1);
348
348
  }
349
349
 
350
- // Ensure native modules are compiled for the current Node.js version
350
+ // Ensure native modules load under the current Node.js version
351
351
  ensureNativeModules();
352
352
 
353
353
  // Symlink native modules into standalone dir so standard resolution finds them
@@ -1199,9 +1199,9 @@ const subName = subIdx >= 0 ? cliArgs[subIdx] : '';
1199
1199
  const subArgs = subIdx >= 0 ? [...cliArgs.slice(0, subIdx), ...cliArgs.slice(subIdx + 1)] : [];
1200
1200
 
1201
1201
  // Subcommands load the SQLCipher binding directly and never reach main()'s
1202
- // native-module heal, so self-heal the database ABI here first. Cheap no-op when
1203
- // healthy; rebuilds (with a friendly notice, not an error) only on a real
1204
- // Node-ABI mismatch — e.g. after the user upgrades Node under a cached install.
1202
+ // native-module check, so check the driver here first. Silent when healthy;
1203
+ // when the prebuilt binary won't load (unsupported platform, old glibc) it
1204
+ // explains why with a warning rather than an error.
1205
1205
  // `file-verify` is excluded: it's a pure-fs recovery tool that never opens the
1206
1206
  // database, so it must work even when the native binding is broken.
1207
1207
  if (subName && subName !== 'file-verify') {
@@ -1,9 +1,15 @@
1
1
  'use strict';
2
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.
3
+ // Shared helpers for keeping native modules *usable* under the running Node.
4
+ // Used by both the runtime CLI entry (bin/quilltap.js) and the package's
5
+ // `postinstall` hook.
6
+ //
7
+ // The SQLCipher driver (better-sqlite3-multiple-ciphers, aliased as
8
+ // better-sqlite3) is a Node-API addon that ships one prebuilt binary per
9
+ // platform inside its npm tarball, so there is nothing to compile and no Node
10
+ // ABI to drift. The only ways it can fail to load are a platform with no
11
+ // prebuild or a host libc older than the binary; we detect and explain those,
12
+ // never rebuild. sharp and node-pty keep their `npm rebuild` heal.
7
13
 
8
14
  const path = require('path');
9
15
  const { execSync } = require('child_process');
@@ -21,123 +27,61 @@ function resolveModuleDir(moduleName) {
21
27
  }
22
28
  }
23
29
 
24
- // Locate the SQLCipher binding (better-sqlite3-multiple-ciphers, aliased as
25
- // better-sqlite3). Returns the absolute path to better_sqlite3.node, or null.
30
+ const SUPPORTED_DRIVER_TARGETS = [
31
+ 'darwin-x64', 'darwin-arm64',
32
+ 'linux-x64', 'linux-arm64',
33
+ 'linuxmusl-x64', 'linuxmusl-arm64',
34
+ 'win32-x64', 'win32-arm64',
35
+ ];
36
+ const GLIBC_FLOOR = '2.35';
37
+
38
+ // Which SQLCipher binding file would the driver's loader pick on this host?
39
+ // Asks the package's own `lib/binding.js#getPrebuildPath()` (required by
40
+ // absolute path on purpose: the package's `exports` map blocks the subpath
41
+ // form). A v12 or from-source copy has no prebuilds, so fall back to the
42
+ // from-source build/ location to describe that install truthfully. Returns the absolute
43
+ // path, or null when neither exists.
26
44
  function betterSqlite3BindingPath() {
45
+ const fs = require('fs');
27
46
  const modDir = resolveModuleDir('better-sqlite3-multiple-ciphers')
28
47
  || resolveModuleDir('better-sqlite3');
29
48
  if (!modDir) return null;
30
- return path.join(modDir, 'build', 'Release', 'better_sqlite3.node');
31
- }
32
-
33
- // Read the Node ABI (NODE_MODULE_VERSION) a node-gyp/NAN addon was compiled
34
- // against, by scanning for its `node_register_module_v<ABI>` export — a few-KB
35
- // byte read, no dlopen, no rebuild. Returns the ABI as a string, or null when
36
- // the symbol is absent (e.g. an N-API build, which is ABI-stable) or unreadable.
37
- function readCompiledAbi(bindingPath) {
38
49
  try {
39
- const buf = require('fs').readFileSync(bindingPath);
40
- const m = /node_register_module_v(\d+)/.exec(buf.toString('latin1'));
41
- return m ? m[1] : null;
50
+ const prebuild = require(path.join(modDir, 'lib', 'binding.js')).getPrebuildPath();
51
+ if (prebuild) return prebuild;
42
52
  } catch {
43
- return null;
53
+ // older layout — no lib/binding.js
44
54
  }
55
+ const built = path.join(modDir, 'build', 'Release', 'better_sqlite3.node');
56
+ return fs.existsSync(built) ? built : null;
45
57
  }
46
58
 
47
- // True when the SQLCipher binding is missing or was built for a different Node
48
- // ABI than the one we're running. Reads the compiled-for ABI straight from the
49
- // binary; only falls back to an actual load probe if the symbol can't be read.
50
- function betterSqlite3NeedsRebuild() {
51
- const bindingPath = betterSqlite3BindingPath();
52
- if (!bindingPath) return true; // unresolvable → needs (re)build
53
- if (!require('fs').existsSync(bindingPath)) return true;
54
- const compiledAbi = readCompiledAbi(bindingPath);
55
- if (compiledAbi) return compiledAbi !== process.versions.modules;
56
- // Symbol unreadable — fall back to the authoritative dlopen probe.
57
- try {
58
- require(bindingPath);
59
- return false;
60
- } catch (err) {
61
- return !!(err.message && err.message.includes('NODE_MODULE_VERSION'));
59
+ // Turn a failed driver load into a reason the user can act on. Pure; exported
60
+ // for tests. Returns { kind, message, hint }.
61
+ function classifyDriverLoadFailure({ platform, arch, prebuildPath, error }) {
62
+ if (!prebuildPath) {
63
+ return {
64
+ kind: 'unsupported-platform',
65
+ message: `the SQLCipher driver has no prebuilt binary for ${platform}-${arch}.`,
66
+ hint: `Supported targets: ${SUPPORTED_DRIVER_TARGETS.join(', ')}. ` +
67
+ 'On anything else, run the Quilltap Docker image.',
68
+ };
62
69
  }
63
- }
64
-
65
- // Locate an executable installed by a dependency, given the package directory
66
- // that needs it. Checks the package's own `.bin` first, then the `.bin` beside
67
- // it (the hoisted case, which is the usual one). Returns null when absent.
68
- function findBinFor(pkgDir, tool) {
69
- const fs = require('fs');
70
- const names = process.platform === 'win32' ? [`${tool}.cmd`, `${tool}.exe`, tool] : [tool];
71
- const dirs = [
72
- path.join(pkgDir, 'node_modules', '.bin'), // nested install
73
- path.join(pkgDir, '..', '.bin'), // hoisted alongside the package
74
- ];
75
- for (const dir of dirs) {
76
- for (const name of names) {
77
- const candidate = path.join(dir, name);
78
- if (fs.existsSync(candidate)) return candidate;
79
- }
70
+ const errMessage = (error && error.message) || String(error || 'unknown error');
71
+ const glibc = /GLIBC_(\d+\.\d+)/.exec(errMessage);
72
+ if (glibc) {
73
+ return {
74
+ kind: 'glibc-too-old',
75
+ message: `the SQLCipher driver needs glibc ${GLIBC_FLOOR} or newer ` +
76
+ `(this host lacks GLIBC_${glibc[1]}).`,
77
+ hint: 'Use Ubuntu 22.04, Debian 12, RHEL 9 or newer, or run the Quilltap Docker image.',
78
+ };
80
79
  }
81
- return null;
82
- }
83
-
84
- // Rebuild ONE native package in place, addressed by its directory.
85
- //
86
- // Deliberately does NOT go through `npm rebuild <name>`, which fails this job
87
- // two different ways:
88
- //
89
- // 1. npm refuses install scripts for any package not listed in the root
90
- // package.json `allowScripts` map (keyed `name@version`). The root
91
- // SQLCipher copy is installed under the ALIAS `better-sqlite3`, which is
92
- // not a key there, so the rebuild dies with EALLOWSCRIPTS.
93
- // 2. Asking for the real name (`better-sqlite3-multiple-ciphers`) at the root
94
- // instead resolves a phantom directory: npm reports "rebuilt dependencies
95
- // successfully" and the binding on disk is untouched. A success message is
96
- // worse than an error.
97
- //
98
- // Running the package's own build chain (`prebuild-install || node-gyp rebuild`,
99
- // exactly what its `install` script does) in its own directory sidesteps both:
100
- // no name resolution, no npm script policy. `prebuild-install` downloads the
101
- // prebuilt binary for the running ABI and needs network; node-gyp compiles and
102
- // needs a toolchain.
103
- //
104
- // Returns { ok, reason }. Never throws. Verifies the result rather than trusting
105
- // the exit code — see (2): the whole failure mode here is a rebuild that claims
106
- // to have worked.
107
- function rebuildNativePackage(pkgDir, bindingPath) {
108
- const fs = require('fs');
109
- if (!fs.existsSync(pkgDir)) return { ok: false, reason: `no package at ${pkgDir}` };
110
-
111
- const before = fs.existsSync(bindingPath) ? readCompiledAbi(bindingPath) : null;
112
- const attempts = [];
113
-
114
- const prebuild = findBinFor(pkgDir, 'prebuild-install');
115
- if (prebuild) attempts.push({ label: 'prebuild-install', cmd: `"${prebuild}"` });
116
-
117
- const nodeGyp = findBinFor(pkgDir, 'node-gyp');
118
- if (nodeGyp) attempts.push({ label: 'node-gyp rebuild', cmd: `"${nodeGyp}" rebuild --release` });
119
-
120
- if (attempts.length === 0) {
121
- return { ok: false, reason: 'neither prebuild-install nor node-gyp is installed' };
122
- }
123
-
124
- const failures = [];
125
- for (const attempt of attempts) {
126
- try {
127
- execSync(attempt.cmd, { cwd: pkgDir, stdio: 'inherit' });
128
- } catch (err) {
129
- failures.push(`${attempt.label}: ${err.message.split('\n')[0]}`);
130
- continue;
131
- }
132
- // Trust nothing but the binary. A tool can exit 0 having done nothing.
133
- const after = fs.existsSync(bindingPath) ? readCompiledAbi(bindingPath) : null;
134
- if (after === process.versions.modules) return { ok: true, reason: attempt.label };
135
- failures.push(
136
- `${attempt.label}: exited 0 but the binding is still ${after ?? 'missing'} ` +
137
- `(wanted ${process.versions.modules}${before ? `, was ${before}` : ''})`,
138
- );
139
- }
140
- return { ok: false, reason: failures.join('; ') };
80
+ return {
81
+ kind: 'load-failed',
82
+ message: `the SQLCipher driver failed to load: ${errMessage.split('\n')[0]}`,
83
+ hint: 'Reinstall the quilltap package; if it persists, file an issue quoting the line above.',
84
+ };
141
85
  }
142
86
 
143
87
  // Rebuild the named native modules against the current Node ABI. Prints a
@@ -163,36 +107,30 @@ function rebuildModules(moduleNames) {
163
107
  }
164
108
  }
165
109
 
166
- // Fast pre-flight for the ONE ABI-fragile native module every DB path needs:
167
- // better-sqlite3-multiple-ciphers (SQLCipher). sharp and node-pty are N-API and
168
- // ABI-stable, so they can't hit this failure. Detects an ABI mismatch from the
169
- // binary itself and rebuilds before anything tries to load it, so a Node upgrade
170
- // self-heals instead of throwing. Cheap no-op when already healthy. Never throws.
110
+ // Fast pre-flight for the SQLCipher driver every DB path needs. Loads the
111
+ // binding the driver would pick; when it won't load, prints why and what to do.
112
+ // Prints nothing when healthy, never rebuilds, never throws. Returns true when
113
+ // the binding loads, false otherwise.
171
114
  function ensureDatabaseNativeModule() {
115
+ let prebuildPath = null;
116
+ let error = null;
172
117
  try {
173
- if (!betterSqlite3NeedsRebuild()) return true;
174
- } catch {
175
- return true; // detection hiccup — let the real load be the source of truth
176
- }
177
-
178
- const bindingPath = betterSqlite3BindingPath();
179
- if (!bindingPath) {
180
- console.error(' Warning: could not locate the SQLCipher binding to rebuild.');
181
- return false;
182
- }
183
- // build/Release/better_sqlite3.node → the package directory above it.
184
- const pkgDir = path.resolve(path.dirname(bindingPath), '..', '..');
185
-
186
- console.log(` Rebuilding the SQLCipher binding for Node.js ${process.version}...`);
187
- const result = rebuildNativePackage(pkgDir, bindingPath);
188
- if (result.ok) {
189
- console.log(` Done (${result.reason}).`);
190
- console.log('');
191
- return true;
118
+ prebuildPath = betterSqlite3BindingPath();
119
+ if (prebuildPath) require(prebuildPath);
120
+ } catch (err) {
121
+ error = err;
192
122
  }
123
+ if (prebuildPath && !error) return true;
124
+
125
+ const { message, hint } = classifyDriverLoadFailure({
126
+ platform: process.platform,
127
+ arch: process.arch,
128
+ prebuildPath,
129
+ error,
130
+ });
193
131
  console.error('');
194
- console.error(` Warning: failed to rebuild the SQLCipher binding — ${result.reason}`);
195
- console.error(` Try running: (cd ${pkgDir} && ./node_modules/.bin/prebuild-install)`);
132
+ console.error(` Warning: ${message}`);
133
+ console.error(` Try: ${hint}`);
196
134
  console.error('');
197
135
  return false;
198
136
  }
@@ -239,22 +177,15 @@ function reconcileNodePtySpawnHelper() {
239
177
  }
240
178
  }
241
179
 
242
- // Check if native modules are compiled for the current Node.js version.
243
- // This handles the case where npx caches the package but the user upgrades
244
- // Node.js — the cached native modules will have a stale NODE_MODULE_VERSION.
245
- // Returns true if everything was healthy or successfully rebuilt; false on
246
- // rebuild failure. Never throws.
180
+ // Check that native modules load under the current Node.js version. sharp and
181
+ // node-pty are rebuilt when npx cached them under an older Node; the SQLCipher
182
+ // driver is only checked and explained (see ensureDatabaseNativeModule).
183
+ // Returns true if everything was healthy or successfully rebuilt; false
184
+ // otherwise. Never throws.
247
185
  function ensureNativeModules() {
186
+ const driverOk = ensureDatabaseNativeModule();
248
187
  const needsRebuild = [];
249
188
 
250
- // Check better-sqlite3-multiple-ciphers (provides SQLCipher encryption support).
251
- // The main app depends on this via an npm alias as 'better-sqlite3', so we must
252
- // ensure the SQLCipher-capable version is available and link it as 'better-sqlite3'.
253
- // This is the only ABI-fragile binding — detected straight from the binary.
254
- if (betterSqlite3NeedsRebuild()) {
255
- needsRebuild.push('better-sqlite3-multiple-ciphers');
256
- }
257
-
258
189
  // Check sharp: loads its native binding eagerly on require.
259
190
  try {
260
191
  require.resolve('sharp', { paths: [PACKAGE_DIR] });
@@ -283,19 +214,16 @@ function ensureNativeModules() {
283
214
 
284
215
  if (needsRebuild.length === 0) {
285
216
  reconcileNodePtySpawnHelper();
286
- return true;
217
+ return driverOk;
287
218
  }
288
219
 
289
- return rebuildModules(needsRebuild);
220
+ return rebuildModules(needsRebuild) && driverOk;
290
221
  }
291
222
 
292
223
  module.exports = {
293
224
  resolveModuleDir,
294
- readCompiledAbi,
295
225
  betterSqlite3BindingPath,
296
- betterSqlite3NeedsRebuild,
297
- findBinFor,
298
- rebuildNativePackage,
226
+ classifyDriverLoadFailure,
299
227
  ensureDatabaseNativeModule,
300
228
  ensureNativeModules,
301
229
  reconcileNodePtySpawnHelper,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "quilltap",
3
- "version": "4.11.0",
3
+ "version": "4.12.0-dev.1",
4
4
  "description": "Self-hosted AI workspace for writers, worldbuilders, and roleplayers. Run with npx quilltap.",
5
5
  "author": {
6
6
  "name": "Charles Sebold",
@@ -37,7 +37,7 @@
37
37
  ],
38
38
  "dependencies": {
39
39
  "@napi-rs/canvas": "^1.0.13",
40
- "better-sqlite3-multiple-ciphers": "^12.11.1",
40
+ "better-sqlite3-multiple-ciphers": "^13.0.3",
41
41
  "node-pty": "^1.1.0",
42
42
  "sharp": "^0.35.4",
43
43
  "tar": "^7.5.22",