quilltap 4.6.0-dev → 4.6.0-dev.106

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/bin/quilltap.js CHANGED
@@ -13,6 +13,7 @@ const {
13
13
  loadDbKey,
14
14
  } = require('../lib/db-helpers');
15
15
  const { resolveInstance } = require('../lib/instances');
16
+ const { resolveModuleDir, ensureNativeModules } = require('../lib/native-modules');
16
17
 
17
18
  const PACKAGE_DIR = path.resolve(__dirname, '..');
18
19
 
@@ -137,85 +138,6 @@ function openBrowser(url) {
137
138
  });
138
139
  }
139
140
 
140
- // Resolve a native module's directory, handling npm hoisting.
141
- // Returns the directory containing package.json, or null if not found.
142
- function resolveModuleDir(moduleName) {
143
- try {
144
- const pkgJson = require.resolve(moduleName + '/package.json');
145
- return path.dirname(pkgJson);
146
- } catch {
147
- return null;
148
- }
149
- }
150
-
151
- // Check if native modules are compiled for the current Node.js version.
152
- // This handles the case where npx caches the package but the user upgrades
153
- // Node.js — the cached native modules will have a stale NODE_MODULE_VERSION.
154
- function ensureNativeModules() {
155
- const needsRebuild = [];
156
-
157
- // Check better-sqlite3-multiple-ciphers (provides SQLCipher encryption support).
158
- // The main app depends on this via an npm alias as 'better-sqlite3', so we must
159
- // ensure the SQLCipher-capable version is available and link it as 'better-sqlite3'.
160
- // We must load the native binding directly to detect NODE_MODULE_VERSION mismatches.
161
- try {
162
- const modDir = resolveModuleDir('better-sqlite3-multiple-ciphers')
163
- || resolveModuleDir('better-sqlite3');
164
- if (!modDir) throw Object.assign(new Error('not found'), { code: 'MODULE_NOT_FOUND' });
165
- const bindingsPath = path.join(modDir, 'build', 'Release', 'better_sqlite3.node');
166
- require(bindingsPath);
167
- } catch (err) {
168
- if (err.message && err.message.includes('NODE_MODULE_VERSION')) {
169
- needsRebuild.push('better-sqlite3-multiple-ciphers');
170
- } else if (err.code === 'MODULE_NOT_FOUND') {
171
- needsRebuild.push('better-sqlite3-multiple-ciphers');
172
- }
173
- }
174
-
175
- // Check sharp: loads its native binding eagerly on require, but we use
176
- // the same explicit-path approach for consistency and reliability.
177
- try {
178
- require('sharp');
179
- } catch (err) {
180
- if (err.message && err.message.includes('NODE_MODULE_VERSION')) {
181
- needsRebuild.push('sharp');
182
- } else if (err.code === 'MODULE_NOT_FOUND') {
183
- needsRebuild.push('sharp');
184
- }
185
- }
186
-
187
- // Check node-pty: backs the Ariel terminal feature. Loaded dynamically by
188
- // pty-manager in the standalone server, so resolution must succeed and the
189
- // native binding's NODE_MODULE_VERSION must match the runtime.
190
- try {
191
- require('node-pty');
192
- } catch (err) {
193
- if (err.message && err.message.includes('NODE_MODULE_VERSION')) {
194
- needsRebuild.push('node-pty');
195
- } else if (err.code === 'MODULE_NOT_FOUND') {
196
- needsRebuild.push('node-pty');
197
- }
198
- }
199
-
200
- if (needsRebuild.length === 0) return;
201
-
202
- console.log(` Rebuilding native modules for Node.js ${process.version}...`);
203
-
204
- try {
205
- execSync(`npm rebuild ${needsRebuild.join(' ')}`, {
206
- cwd: PACKAGE_DIR,
207
- stdio: 'inherit',
208
- });
209
- console.log(' Done.');
210
- console.log('');
211
- } catch (err) {
212
- console.error('');
213
- console.error(` Warning: Failed to rebuild native modules: ${err.message}`);
214
- console.error(' Try running: npm rebuild --prefix ' + PACKAGE_DIR);
215
- console.error('');
216
- }
217
- }
218
-
219
141
  // Symlink native modules into the standalone directory's node_modules
220
142
  // so that standard Node.js resolution finds them without relying on NODE_PATH.
221
143
  function linkNativeModules(standaloneDir) {
@@ -266,25 +188,67 @@ function linkNativeModules(standaloneDir) {
266
188
  || resolveModuleDir('better-sqlite3');
267
189
  linkModule('better-sqlite3', betterSqlite3Dir);
268
190
 
269
- // Link node-pty — the standalone tarball strips it (platform-specific),
270
- // and pty-manager loads it via a dynamic require, so it needs to resolve
271
- // from standaloneDir/node_modules.
272
- const nodePtyDir = resolveModuleDir('node-pty');
273
- linkModule('node-pty', nodePtyDir);
274
- if (nodePtyDir) {
275
- // Some npm cache extractions strip the executable bit on spawn-helper,
276
- // causing pty.spawn() to fail with `posix_spawnp failed`. Restore it.
277
- const prebuildsDir = path.join(nodePtyDir, 'prebuilds');
278
- if (fs.existsSync(prebuildsDir)) {
279
- try {
280
- for (const entry of fs.readdirSync(prebuildsDir, { withFileTypes: true })) {
281
- if (!entry.isDirectory()) continue;
282
- const helper = path.join(prebuildsDir, entry.name, 'spawn-helper');
283
- if (fs.existsSync(helper)) {
284
- try { fs.chmodSync(helper, 0o755); } catch { /* best-effort */ }
191
+ // Link node-pty — but only when we have to. The standalone tarball ships
192
+ // node-pty intact with cross-platform prebuilds (darwin-arm64, darwin-x64,
193
+ // win32-arm64, win32-x64). On those platforms the tarball-shipped copy is
194
+ // already correct, and we MUST NOT replace it with a symlink to the
195
+ // npm-installed copy: `sudo npm install -g quilltap` on macOS can strip the
196
+ // executable bit off `spawn-helper`, and we can't chmod a root-owned file
197
+ // back as a non-root runtime user pty.spawn() then fails with
198
+ // `posix_spawnp failed`. Only Linux (which has no node-pty prebuild) and
199
+ // rebuild-failure cases need the symlink.
200
+ const standaloneNodePtyPath = path.join(standaloneNodeModules, 'node-pty');
201
+ const standaloneHasUsablePty = (() => {
202
+ try {
203
+ const stat = fs.lstatSync(standaloneNodePtyPath);
204
+ if (stat.isSymbolicLink()) return false; // prior broken-symlink state — replace it
205
+ if (!stat.isDirectory()) return false;
206
+ } catch {
207
+ return false;
208
+ }
209
+ const platformPrebuildDir = path.join(
210
+ standaloneNodePtyPath,
211
+ 'prebuilds',
212
+ `${process.platform}-${process.arch}`,
213
+ );
214
+ return fs.existsSync(platformPrebuildDir);
215
+ })();
216
+
217
+ if (!standaloneHasUsablePty) {
218
+ const nodePtyDir = resolveModuleDir('node-pty');
219
+ linkModule('node-pty', nodePtyDir);
220
+ if (nodePtyDir) {
221
+ // Some npm cache extractions (notably `sudo npm install -g`) strip the
222
+ // executable bit on spawn-helper. Restore it where we can. If chmod
223
+ // fails because we don't own the file (typical when the CLI was
224
+ // installed with sudo and is run as a non-root user), warn loudly —
225
+ // silent failure here produces the `posix_spawnp failed` runtime error
226
+ // with no actionable hint.
227
+ const prebuildsDir = path.join(nodePtyDir, 'prebuilds');
228
+ if (fs.existsSync(prebuildsDir)) {
229
+ try {
230
+ for (const entry of fs.readdirSync(prebuildsDir, { withFileTypes: true })) {
231
+ if (!entry.isDirectory()) continue;
232
+ const helper = path.join(prebuildsDir, entry.name, 'spawn-helper');
233
+ if (!fs.existsSync(helper)) continue;
234
+ try {
235
+ fs.chmodSync(helper, 0o755);
236
+ } catch (err) {
237
+ if (err && err.code === 'EPERM') {
238
+ console.error('');
239
+ console.error(` Warning: Could not make node-pty spawn-helper executable:`);
240
+ console.error(` ${helper}`);
241
+ console.error(` The file is owned by another user (likely root from a sudo'd`);
242
+ console.error(` global install). Terminal sessions will fail to spawn with`);
243
+ console.error(` "posix_spawnp failed" until this is fixed. Run:`);
244
+ console.error(` sudo chmod 755 "${helper}"`);
245
+ console.error('');
246
+ }
247
+ // Other errors are non-fatal — node-pty may still work if the bit was already set.
248
+ }
285
249
  }
286
- }
287
- } catch { /* best-effort */ }
250
+ } catch { /* best-effort */ }
251
+ }
288
252
  }
289
253
  }
290
254
 
@@ -740,6 +704,11 @@ Subcommands (high-level shortcuts; auto-pick the right database):
740
704
  log <id> Full request/response of a single LLM log
741
705
  memories --character <id> Memories held by a character
742
706
  (flags: --about <id|name> --source AUTO|MANUAL)
707
+ characters status Per-character vault readiness report:
708
+ flag value, vault present, single-file count,
709
+ Prompts/ and Scenarios/ folder counts, and any
710
+ divergence between DB columns and vault content.
711
+ (flags: --id <id|name> --diverged --blocked --limit N)
743
712
  optimize [target...] Run maintenance (VACUUM + ANALYZE + PRAGMA optimize)
744
713
  on the named databases, or all of them if no
745
714
  target is given. Targets: main, llm-logs,
@@ -762,6 +731,8 @@ Low-level options (legacy; still supported):
762
731
  --tables List all tables in the active database
763
732
  --count <table> Show row count for a table
764
733
  --repl Interactive SQL prompt (extras: .cols, .find)
734
+ --json Emit machine-readable JSON instead of a table
735
+ (works with --tables, --count, and raw SQL)
765
736
  --llm-logs Target the LLM logs database
766
737
  --mount-points Target the document mount-index database
767
738
  --data-dir <path> Override data directory (pass instance root)
@@ -869,6 +840,7 @@ async function dbCommand(args) {
869
840
  let lockStatus = false;
870
841
  let lockClean = false;
871
842
  let lockOverride = false;
843
+ let asJson = false;
872
844
 
873
845
  let i = 0;
874
846
  while (i < cleaned.length) {
@@ -878,6 +850,7 @@ async function dbCommand(args) {
878
850
  case '--tables': showTables = true; break;
879
851
  case '--count': countTable = cleaned[++i]; break;
880
852
  case '--repl': repl = true; break;
853
+ case '--json': asJson = true; break;
881
854
  case '--help': case '-h': showHelp = true; break;
882
855
  case '--lock-status': lockStatus = true; break;
883
856
  case '--lock-clean': lockClean = true; break;
@@ -971,22 +944,27 @@ async function dbCommand(args) {
971
944
  try {
972
945
  if (showTables) {
973
946
  const tables = db.prepare("SELECT name FROM sqlite_master WHERE type='table' ORDER BY name").all();
974
- for (const t of tables) console.log(t.name);
947
+ if (asJson) console.log(JSON.stringify(tables.map(t => t.name), null, 2));
948
+ else for (const t of tables) console.log(t.name);
975
949
  } else if (countTable) {
976
950
  const row = db.prepare(`SELECT count(*) as count FROM "${countTable}"`).get();
977
- console.log(row.count);
951
+ if (asJson) console.log(JSON.stringify({ table: countTable, count: row.count }, null, 2));
952
+ else console.log(row.count);
978
953
  } else if (sql) {
979
954
  const stmt = db.prepare(sql);
980
955
  if (stmt.reader) {
981
956
  const rows = stmt.all();
982
- if (rows.length === 0) {
957
+ if (asJson) {
958
+ console.log(JSON.stringify(rows, null, 2));
959
+ } else if (rows.length === 0) {
983
960
  console.log('(no results)');
984
961
  } else {
985
962
  console.table(rows);
986
963
  }
987
964
  } else {
988
965
  const info = stmt.run();
989
- console.log(`Changes: ${info.changes}`);
966
+ if (asJson) console.log(JSON.stringify({ changes: info.changes, lastInsertRowid: Number(info.lastInsertRowid) }, null, 2));
967
+ else console.log(`Changes: ${info.changes}`);
990
968
  }
991
969
  } else if (repl) {
992
970
  const readline = require('readline');
@@ -12,15 +12,19 @@ _quilltap_complete() {
12
12
  cword=$COMP_CWORD
13
13
 
14
14
  # Global options that can appear before subcommands
15
- local global_opts="-d --data-dir -i --instance -p --port -o --open -v --version -h --help --update"
15
+ local global_opts="-d --data-dir -i --instance -p --port -o --open -v --version -h --help --update --passphrase"
16
+
17
+ # Top-level subcommands
18
+ local top_cmds="db docs themes instances memories memory-diff logs migrations completion"
16
19
 
17
20
  # Get the subcommand (first non-option word after quilltap)
18
21
  local subcommand=""
22
+ local subverb=""
19
23
  local i=1
20
24
  while [[ $i -lt $cword ]]; do
21
25
  local word="${words[$i]}"
22
26
  case "$word" in
23
- -d|--data-dir|-i|--instance|-p|--port)
27
+ -d|--data-dir|-i|--instance|-p|--port|--passphrase)
24
28
  # These take a value, skip the next word
25
29
  ((i += 2))
26
30
  ;;
@@ -33,22 +37,26 @@ _quilltap_complete() {
33
37
  ((i += 1))
34
38
  ;;
35
39
  *)
36
- # This is the subcommand
37
- subcommand="$word"
38
- break
40
+ if [[ -z "$subcommand" ]]; then
41
+ subcommand="$word"
42
+ elif [[ -z "$subverb" ]]; then
43
+ subverb="$word"
44
+ fi
45
+ ((i += 1))
39
46
  ;;
40
47
  esac
41
48
  done
42
49
 
43
50
  # If we're completing a global flag value
44
- if [[ "$prev" == "-d" ]] || [[ "$prev" == "--data-dir" ]] || \
45
- [[ "$prev" == "-p" ]] || [[ "$prev" == "--port" ]]; then
46
- # No completion for paths or ports
51
+ if [[ "$prev" == "-d" ]] || [[ "$prev" == "--data-dir" ]]; then
52
+ # Complete with directories
53
+ COMPREPLY=($(compgen -d -- "$cur"))
54
+ return
55
+ fi
56
+ if [[ "$prev" == "-p" ]] || [[ "$prev" == "--port" ]] || [[ "$prev" == "--passphrase" ]]; then
47
57
  return
48
58
  fi
49
-
50
59
  if [[ "$prev" == "-i" ]] || [[ "$prev" == "--instance" ]]; then
51
- # Complete with instance names
52
60
  local instances=$(command quilltap instances list --names-only 2>/dev/null)
53
61
  COMPREPLY=($(compgen -W "$instances" -- "$cur"))
54
62
  return
@@ -59,53 +67,164 @@ _quilltap_complete() {
59
67
  if [[ "$cur" == -* ]]; then
60
68
  COMPREPLY=($(compgen -W "$global_opts" -- "$cur"))
61
69
  else
62
- COMPREPLY=($(compgen -W "db docs themes instances memories memory-diff completion" -- "$cur"))
70
+ COMPREPLY=($(compgen -W "$top_cmds" -- "$cur"))
63
71
  fi
64
72
  return
65
73
  fi
66
74
 
75
+ # Shared flag-value completions for any subcommand
76
+ case "$prev" in
77
+ --mount)
78
+ local mounts=$(command quilltap docs list --names-only 2>/dev/null)
79
+ COMPREPLY=($(compgen -W "$mounts" -- "$cur"))
80
+ return
81
+ ;;
82
+ --character|--about)
83
+ # No live source; suggest common literals
84
+ COMPREPLY=($(compgen -W "all self none" -- "$cur"))
85
+ return
86
+ ;;
87
+ --source)
88
+ COMPREPLY=($(compgen -W "AUTO MANUAL" -- "$cur"))
89
+ return
90
+ ;;
91
+ --sort)
92
+ COMPREPLY=($(compgen -W "name path size modified created reinforced importance accessed reinforcement-count links" -- "$cur"))
93
+ return
94
+ ;;
95
+ --field)
96
+ COMPREPLY=($(compgen -W "request response both" -- "$cur"))
97
+ return
98
+ ;;
99
+ --type)
100
+ COMPREPLY=($(compgen -W "file folder" -- "$cur"))
101
+ return
102
+ ;;
103
+ --stream)
104
+ COMPREPLY=($(compgen -W "combined error stdout stderr startup" -- "$cur"))
105
+ return
106
+ ;;
107
+ esac
108
+
67
109
  # Subcommand-specific completion
68
- local subcommand_opts=""
69
110
  case "$subcommand" in
70
111
  db)
71
- subcommand_opts="schema find chats messages logs message log memories optimize backup integrity"
72
- if [[ "$cur" == -* ]]; then
73
- subcommand_opts="$subcommand_opts --instance --data-dir --json --help"
112
+ local db_verbs="schema find chats messages logs message log memories optimize backup integrity"
113
+ local db_flags="--instance --data-dir --passphrase --json --limit --grep \
114
+ --character --project --about --source --chat --message --rendered --field \
115
+ --tail --last --full --from --type --out --help \
116
+ --tables --count --repl --llm-logs --mount-points \
117
+ --lock-status --lock-clean --lock-override"
118
+ if [[ -z "$subverb" ]]; then
119
+ if [[ "$cur" == -* ]]; then
120
+ COMPREPLY=($(compgen -W "$db_flags" -- "$cur"))
121
+ else
122
+ COMPREPLY=($(compgen -W "$db_verbs" -- "$cur"))
123
+ fi
124
+ else
125
+ COMPREPLY=($(compgen -W "$db_flags" -- "$cur"))
74
126
  fi
75
- COMPREPLY=($(compgen -W "$subcommand_opts" -- "$cur"))
76
127
  ;;
77
128
  docs)
78
129
  local docs_verbs="list show files ls dir read export scan write delete mkdir move copy status find grep reindex embed"
79
- if [[ "$cur" == -* ]]; then
80
- docs_verbs="$docs_verbs --mount --instance --data-dir --port --json --help --force --rendered --links"
130
+ local docs_flags="--mount --instance --data-dir --passphrase --port --json --help \
131
+ --force --rendered --links --folder --type --ext --limit --max --context --top --threshold \
132
+ --ignore-case -l --wait -R --recursive --sort -r --reverse --depth --max-nodes --long --semantic"
133
+ if [[ -z "$subverb" ]]; then
134
+ if [[ "$cur" == -* ]]; then
135
+ COMPREPLY=($(compgen -W "$docs_flags" -- "$cur"))
136
+ else
137
+ COMPREPLY=($(compgen -W "$docs_verbs" -- "$cur"))
138
+ fi
139
+ else
140
+ COMPREPLY=($(compgen -W "$docs_flags" -- "$cur"))
81
141
  fi
82
- COMPREPLY=($(compgen -W "$docs_verbs" -- "$cur"))
83
142
  ;;
84
143
  themes)
85
- subcommand_opts="list install uninstall validate export create search update registry"
86
- if [[ "$cur" == -* ]]; then
87
- subcommand_opts="$subcommand_opts --instance --data-dir --output --help"
144
+ local themes_verbs="list install uninstall validate export create search update registry"
145
+ local themes_flags="--instance --data-dir --output -o --help"
146
+ if [[ -z "$subverb" ]]; then
147
+ if [[ "$cur" == -* ]]; then
148
+ COMPREPLY=($(compgen -W "$themes_flags" -- "$cur"))
149
+ else
150
+ COMPREPLY=($(compgen -W "$themes_verbs" -- "$cur"))
151
+ fi
152
+ elif [[ "$subverb" == "registry" ]]; then
153
+ local registry_verbs="list add remove refresh keygen sign"
154
+ local registry_flags="--key -k --name -n --output -o --help"
155
+ if [[ "$cur" == -* ]]; then
156
+ COMPREPLY=($(compgen -W "$registry_flags" -- "$cur"))
157
+ else
158
+ COMPREPLY=($(compgen -W "$registry_verbs" -- "$cur"))
159
+ fi
160
+ else
161
+ COMPREPLY=($(compgen -W "$themes_flags" -- "$cur"))
88
162
  fi
89
- COMPREPLY=($(compgen -W "$subcommand_opts" -- "$cur"))
90
163
  ;;
91
164
  instances)
92
- subcommand_opts="list ls show path where add create remove rm delete set-passphrase passphrase"
93
- if [[ "$cur" == -* ]]; then
94
- subcommand_opts="$subcommand_opts --help"
165
+ local inst_verbs="list ls show path where add create remove rm delete set-passphrase passphrase default rename"
166
+ local inst_flags="--names-only --json --clear --help"
167
+ if [[ -z "$subverb" ]]; then
168
+ if [[ "$cur" == -* ]]; then
169
+ COMPREPLY=($(compgen -W "$inst_flags" -- "$cur"))
170
+ else
171
+ COMPREPLY=($(compgen -W "$inst_verbs" -- "$cur"))
172
+ fi
173
+ else
174
+ # Most instances verbs take a name as positional; offer registered names
175
+ case "$subverb" in
176
+ show|remove|rm|delete|set-passphrase|passphrase|default|rename)
177
+ local instances=$(command quilltap instances list --names-only 2>/dev/null)
178
+ if [[ "$cur" == -* ]]; then
179
+ COMPREPLY=($(compgen -W "$inst_flags" -- "$cur"))
180
+ else
181
+ COMPREPLY=($(compgen -W "$instances" -- "$cur"))
182
+ fi
183
+ ;;
184
+ *)
185
+ COMPREPLY=($(compgen -W "$inst_flags" -- "$cur"))
186
+ ;;
187
+ esac
95
188
  fi
96
- COMPREPLY=($(compgen -W "$subcommand_opts" -- "$cur"))
97
189
  ;;
98
190
  memories)
99
191
  local mem_verbs="ls find grep show tree status validate"
100
- if [[ "$cur" == -* ]]; then
101
- mem_verbs="$mem_verbs --character --about --source --chat --project --since --until --min-importance --min-reinforced --has-embedding --no-embedding --sort --instance --data-dir --json --help"
192
+ local mem_flags="--character --about --source --chat --project --since --until \
193
+ --min-importance --min-reinforced --has-embedding --no-embedding \
194
+ --sort -r --reverse --limit --full-titles --in --no-related --list \
195
+ --ignore-case -i --paths-only -l --max --context --depth --max-nodes \
196
+ --semantic --top --threshold \
197
+ --instance --data-dir --passphrase --port --json --help"
198
+ if [[ -z "$subverb" ]]; then
199
+ if [[ "$cur" == -* ]]; then
200
+ COMPREPLY=($(compgen -W "$mem_flags" -- "$cur"))
201
+ else
202
+ COMPREPLY=($(compgen -W "$mem_verbs" -- "$cur"))
203
+ fi
204
+ else
205
+ COMPREPLY=($(compgen -W "$mem_flags" -- "$cur"))
102
206
  fi
103
- COMPREPLY=($(compgen -W "$mem_verbs" -- "$cur"))
104
207
  ;;
105
208
  memory-diff)
106
- if [[ "$cur" == -* ]]; then
107
- subcommand_opts="--instance --data-dir --help"
108
- COMPREPLY=($(compgen -W "$subcommand_opts" -- "$cur"))
209
+ local md_flags="--instance --data-dir --passphrase --port --concurrency --out --help"
210
+ COMPREPLY=($(compgen -W "$md_flags" -- "$cur"))
211
+ ;;
212
+ logs)
213
+ local logs_flags="--stream --tail -f --follow --grep \
214
+ --instance --data-dir --passphrase --help"
215
+ COMPREPLY=($(compgen -W "$logs_flags" -- "$cur"))
216
+ ;;
217
+ migrations)
218
+ local mig_verbs="status pending run"
219
+ local mig_flags="--dry-run --json --instance --data-dir --passphrase --help"
220
+ if [[ -z "$subverb" ]]; then
221
+ if [[ "$cur" == -* ]]; then
222
+ COMPREPLY=($(compgen -W "$mig_flags" -- "$cur"))
223
+ else
224
+ COMPREPLY=($(compgen -W "$mig_verbs" -- "$cur"))
225
+ fi
226
+ else
227
+ COMPREPLY=($(compgen -W "$mig_flags" -- "$cur"))
109
228
  fi
110
229
  ;;
111
230
  completion)