quilltap 4.6.0-dev.99 → 4.6.0
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 +3 -1
- package/bin/quilltap.js +51 -19
- package/lib/completion/bash.template +4 -4
- package/lib/completion/fish.template +6 -1
- package/lib/completion/zsh.template +6 -1
- package/lib/db-commands.js +120 -38
- package/lib/db-helpers.js +90 -9
- package/lib/memories-commands.js +20 -16
- package/lib/migrations-commands.js +2 -2
- package/lib/native-modules.js +48 -2
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -115,6 +115,7 @@ quilltap db logs --tail 20 # Recent LLM logs
|
|
|
115
115
|
quilltap db message <id> # Full content of one message
|
|
116
116
|
quilltap db log <id> [--field request|response|both]
|
|
117
117
|
quilltap db memories --character Friday [--about Amy] [--source AUTO]
|
|
118
|
+
quilltap db characters status # Per-character vault readiness (--id, --diverged, --blocked)
|
|
118
119
|
```
|
|
119
120
|
|
|
120
121
|
### Maintenance and Snapshots
|
|
@@ -157,6 +158,7 @@ In the REPL, `.cols <table>` and `.find <text>` mirror the subcommand helpers.
|
|
|
157
158
|
quilltap docs list # All mounts
|
|
158
159
|
quilltap docs show <mount> # One mount, with counts
|
|
159
160
|
quilltap docs ls <mount> [path] [--links] # POSIX-flavoured listing (alias: dir)
|
|
161
|
+
quilltap docs tree <mount> [path] # ASCII tree of a folder hierarchy (--depth, --max-nodes)
|
|
160
162
|
quilltap docs read [--rendered] <mount> <path> # File contents → stdout
|
|
161
163
|
quilltap docs export <mount> <outputDir> # Mount → directory
|
|
162
164
|
quilltap docs find <pattern> # Substring match on file names (--mount, --ext, --type, --limit)
|
|
@@ -286,7 +288,7 @@ Dynamic completions shell out to `quilltap`'s own subcommands. If the active ins
|
|
|
286
288
|
|
|
287
289
|
## Requirements
|
|
288
290
|
|
|
289
|
-
- Node.js
|
|
291
|
+
- Node.js 24 or later
|
|
290
292
|
|
|
291
293
|
## Other Ways to Run Quilltap
|
|
292
294
|
|
package/bin/quilltap.js
CHANGED
|
@@ -116,6 +116,8 @@ Examples:
|
|
|
116
116
|
quilltap -d /mnt/data/quilltap # Custom data directory
|
|
117
117
|
quilltap -o # Start and open browser
|
|
118
118
|
quilltap --update # Re-download app files
|
|
119
|
+
quilltap --instance Friday db schema # Universal flags (-i/--instance, -d/--data-dir,
|
|
120
|
+
# --passphrase) work before or after a subcommand
|
|
119
121
|
|
|
120
122
|
More info: https://quilltap.ai
|
|
121
123
|
`);
|
|
@@ -1047,51 +1049,81 @@ async function dbCommand(args) {
|
|
|
1047
1049
|
}
|
|
1048
1050
|
}
|
|
1049
1051
|
|
|
1050
|
-
// Route to subcommand or main
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1052
|
+
// Route to subcommand or main.
|
|
1053
|
+
//
|
|
1054
|
+
// Universal flags (-i/--instance, -d/--data-dir, --passphrase, -p/--port) may
|
|
1055
|
+
// appear *before* the subcommand, e.g. `quilltap --instance Friday db schema`.
|
|
1056
|
+
// We locate the subcommand by walking the args and skipping those
|
|
1057
|
+
// value-taking flags (so an instance literally named "db" is not mistaken for
|
|
1058
|
+
// the subcommand), then hand every other arg — including the leading flags —
|
|
1059
|
+
// to the subcommand. Each subcommand parses these flags position-independently,
|
|
1060
|
+
// so they behave the same before or after the verb.
|
|
1061
|
+
const SUBCOMMANDS = new Set([
|
|
1062
|
+
'db', 'themes', 'docs', 'memories', 'instances', 'memory-diff', 'completion', 'logs', 'migrations',
|
|
1063
|
+
]);
|
|
1064
|
+
// Global flags that consume the following token as their value.
|
|
1065
|
+
const GLOBAL_VALUE_FLAGS = new Set(['-p', '--port', '-d', '--data-dir', '-i', '--instance', '--passphrase']);
|
|
1066
|
+
|
|
1067
|
+
function locateSubcommand(argv) {
|
|
1068
|
+
for (let k = 0; k < argv.length; k++) {
|
|
1069
|
+
const a = argv[k];
|
|
1070
|
+
if (GLOBAL_VALUE_FLAGS.has(a)) { k++; continue; } // skip the flag's value
|
|
1071
|
+
if (a.startsWith('-')) continue; // boolean / unknown flag
|
|
1072
|
+
return SUBCOMMANDS.has(a) ? k : -1; // first bare token decides
|
|
1073
|
+
}
|
|
1074
|
+
return -1;
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
const cliArgs = process.argv.slice(2);
|
|
1078
|
+
const subIdx = locateSubcommand(cliArgs);
|
|
1079
|
+
const subName = subIdx >= 0 ? cliArgs[subIdx] : '';
|
|
1080
|
+
// Everything except the subcommand token itself (leading global flags kept).
|
|
1081
|
+
const subArgs = subIdx >= 0 ? [...cliArgs.slice(0, subIdx), ...cliArgs.slice(subIdx + 1)] : [];
|
|
1082
|
+
|
|
1083
|
+
if (subName === 'db') {
|
|
1084
|
+
dbCommand(subArgs);
|
|
1085
|
+
} else if (subName === 'themes') {
|
|
1054
1086
|
const { themesCommand } = require('../lib/theme-commands');
|
|
1055
|
-
themesCommand(
|
|
1056
|
-
} else if (
|
|
1087
|
+
themesCommand(subArgs);
|
|
1088
|
+
} else if (subName === 'docs') {
|
|
1057
1089
|
const { docsCommand } = require('../lib/docs-commands');
|
|
1058
|
-
docsCommand(
|
|
1059
|
-
} else if (
|
|
1090
|
+
docsCommand(subArgs);
|
|
1091
|
+
} else if (subName === 'memories') {
|
|
1060
1092
|
const { memoriesCommand } = require('../lib/memories-commands');
|
|
1061
|
-
memoriesCommand(
|
|
1093
|
+
memoriesCommand(subArgs).catch(err => {
|
|
1062
1094
|
if (!err.silent) {
|
|
1063
1095
|
console.error(`Error: ${err.message}`);
|
|
1064
1096
|
}
|
|
1065
1097
|
const code = err.exitCode != null ? err.exitCode : (err.ambiguous ? 2 : 1);
|
|
1066
1098
|
process.exit(code);
|
|
1067
1099
|
});
|
|
1068
|
-
} else if (
|
|
1100
|
+
} else if (subName === 'instances') {
|
|
1069
1101
|
const { instancesCommand } = require('../lib/instances-commands');
|
|
1070
|
-
instancesCommand(
|
|
1102
|
+
instancesCommand(subArgs).catch(err => {
|
|
1071
1103
|
console.error(`Error: ${err.message}`);
|
|
1072
1104
|
process.exit(1);
|
|
1073
1105
|
});
|
|
1074
|
-
} else if (
|
|
1106
|
+
} else if (subName === 'memory-diff') {
|
|
1075
1107
|
const { memoryDiffCommand } = require('../lib/memory-diff-command');
|
|
1076
|
-
memoryDiffCommand(
|
|
1108
|
+
memoryDiffCommand(subArgs).catch(err => {
|
|
1077
1109
|
console.error(`Error: ${err.message}`);
|
|
1078
1110
|
process.exit(1);
|
|
1079
1111
|
});
|
|
1080
|
-
} else if (
|
|
1112
|
+
} else if (subName === 'completion') {
|
|
1081
1113
|
const { completionCommand } = require('../lib/completion-commands');
|
|
1082
|
-
completionCommand(
|
|
1114
|
+
completionCommand(subArgs).catch(err => {
|
|
1083
1115
|
console.error(`Error: ${err.message}`);
|
|
1084
1116
|
process.exit(1);
|
|
1085
1117
|
});
|
|
1086
|
-
} else if (
|
|
1118
|
+
} else if (subName === 'logs') {
|
|
1087
1119
|
const { logsCommand } = require('../lib/logs-commands');
|
|
1088
|
-
logsCommand(
|
|
1120
|
+
logsCommand(subArgs).catch(err => {
|
|
1089
1121
|
console.error(`Error: ${err.message}`);
|
|
1090
1122
|
process.exit(1);
|
|
1091
1123
|
});
|
|
1092
|
-
} else if (
|
|
1124
|
+
} else if (subName === 'migrations') {
|
|
1093
1125
|
const { migrationsCommand } = require('../lib/migrations-commands');
|
|
1094
|
-
migrationsCommand(
|
|
1126
|
+
migrationsCommand(subArgs).catch(err => {
|
|
1095
1127
|
console.error(`Error: ${err.message}`);
|
|
1096
1128
|
process.exit(1);
|
|
1097
1129
|
});
|
|
@@ -89,7 +89,7 @@ _quilltap_complete() {
|
|
|
89
89
|
return
|
|
90
90
|
;;
|
|
91
91
|
--sort)
|
|
92
|
-
COMPREPLY=($(compgen -W "name
|
|
92
|
+
COMPREPLY=($(compgen -W "name time size links reinforced importance created accessed reinforcement-count" -- "$cur"))
|
|
93
93
|
return
|
|
94
94
|
;;
|
|
95
95
|
--field)
|
|
@@ -109,10 +109,10 @@ _quilltap_complete() {
|
|
|
109
109
|
# Subcommand-specific completion
|
|
110
110
|
case "$subcommand" in
|
|
111
111
|
db)
|
|
112
|
-
local db_verbs="schema find chats messages logs message log memories optimize backup integrity"
|
|
112
|
+
local db_verbs="schema find chats messages logs message log memories characters optimize backup integrity"
|
|
113
113
|
local db_flags="--instance --data-dir --passphrase --json --limit --grep \
|
|
114
114
|
--character --project --about --source --chat --message --rendered --field \
|
|
115
|
-
--tail --last --full --from --type --out --help \
|
|
115
|
+
--tail --last --full --from --type --out --id --diverged --blocked --help \
|
|
116
116
|
--tables --count --repl --llm-logs --mount-points \
|
|
117
117
|
--lock-status --lock-clean --lock-override"
|
|
118
118
|
if [[ -z "$subverb" ]]; then
|
|
@@ -126,7 +126,7 @@ _quilltap_complete() {
|
|
|
126
126
|
fi
|
|
127
127
|
;;
|
|
128
128
|
docs)
|
|
129
|
-
local docs_verbs="list show files ls dir read export scan write delete mkdir move copy status find grep reindex embed"
|
|
129
|
+
local docs_verbs="list show files ls dir tree read export scan write delete mkdir move copy status find grep reindex embed"
|
|
130
130
|
local docs_flags="--mount --instance --data-dir --passphrase --port --json --help \
|
|
131
131
|
--force --rendered --links --folder --type --ext --limit --max --context --top --threshold \
|
|
132
132
|
--ignore-case -l --wait -R --recursive --sort -r --reverse --depth --max-nodes --long --semantic"
|
|
@@ -85,6 +85,7 @@ complete -c quilltap -n '__quilltap_using_subcommand db' -f -a 'logs' -d 'Get LL
|
|
|
85
85
|
complete -c quilltap -n '__quilltap_using_subcommand db' -f -a 'message' -d 'Get single message'
|
|
86
86
|
complete -c quilltap -n '__quilltap_using_subcommand db' -f -a 'log' -d 'Get single log entry'
|
|
87
87
|
complete -c quilltap -n '__quilltap_using_subcommand db' -f -a 'memories' -d 'List memories'
|
|
88
|
+
complete -c quilltap -n '__quilltap_using_subcommand db' -f -a 'characters' -d 'Per-character vault status'
|
|
88
89
|
complete -c quilltap -n '__quilltap_using_subcommand db' -f -a 'optimize' -d 'Optimize database'
|
|
89
90
|
complete -c quilltap -n '__quilltap_using_subcommand db' -f -a 'backup' -d 'Backup database'
|
|
90
91
|
complete -c quilltap -n '__quilltap_using_subcommand db' -f -a 'integrity' -d 'Check integrity'
|
|
@@ -114,6 +115,9 @@ complete -c quilltap -n '__quilltap_using_subcommand db' -l 'mount-points' -d 'T
|
|
|
114
115
|
complete -c quilltap -n '__quilltap_using_subcommand db' -l 'lock-status' -d 'Show instance lock status'
|
|
115
116
|
complete -c quilltap -n '__quilltap_using_subcommand db' -l 'lock-clean' -d 'Clean stale lock'
|
|
116
117
|
complete -c quilltap -n '__quilltap_using_subcommand db' -l 'lock-override' -d 'Override active lock'
|
|
118
|
+
complete -c quilltap -n '__quilltap_using_subcommand db' -l 'id' -d 'Single character by name or id' -x
|
|
119
|
+
complete -c quilltap -n '__quilltap_using_subcommand db' -l 'diverged' -d 'Only diverged characters'
|
|
120
|
+
complete -c quilltap -n '__quilltap_using_subcommand db' -l 'blocked' -d 'Only characters with vault issues'
|
|
117
121
|
|
|
118
122
|
# ---------- docs verbs ----------
|
|
119
123
|
complete -c quilltap -n '__quilltap_using_subcommand docs' -f -a 'list' -d 'List mount points'
|
|
@@ -121,6 +125,7 @@ complete -c quilltap -n '__quilltap_using_subcommand docs' -f -a 'show' -d 'Show
|
|
|
121
125
|
complete -c quilltap -n '__quilltap_using_subcommand docs' -f -a 'files' -d 'List files in mount'
|
|
122
126
|
complete -c quilltap -n '__quilltap_using_subcommand docs' -f -a 'ls' -d 'ls-style listing'
|
|
123
127
|
complete -c quilltap -n '__quilltap_using_subcommand docs' -f -a 'dir' -d 'Directory listing'
|
|
128
|
+
complete -c quilltap -n '__quilltap_using_subcommand docs' -f -a 'tree' -d 'ASCII tree view'
|
|
124
129
|
complete -c quilltap -n '__quilltap_using_subcommand docs' -f -a 'read' -d 'Print file contents'
|
|
125
130
|
complete -c quilltap -n '__quilltap_using_subcommand docs' -f -a 'export' -d 'Export mount'
|
|
126
131
|
complete -c quilltap -n '__quilltap_using_subcommand docs' -f -a 'scan' -d 'Trigger rescan'
|
|
@@ -153,7 +158,7 @@ complete -c quilltap -n '__quilltap_using_subcommand docs' -l 'ignore-case' -d '
|
|
|
153
158
|
complete -c quilltap -n '__quilltap_using_subcommand docs' -s 'l' -d 'Paths only'
|
|
154
159
|
complete -c quilltap -n '__quilltap_using_subcommand docs' -l 'wait' -d 'Wait for completion'
|
|
155
160
|
complete -c quilltap -n '__quilltap_using_subcommand docs' -l 'recursive' -s 'R' -d 'Recursive listing'
|
|
156
|
-
complete -c quilltap -n '__quilltap_using_subcommand docs' -l 'sort' -d 'Sort field' -x -a 'name
|
|
161
|
+
complete -c quilltap -n '__quilltap_using_subcommand docs' -l 'sort' -d 'Sort field' -x -a 'name time size links'
|
|
157
162
|
complete -c quilltap -n '__quilltap_using_subcommand docs' -l 'reverse' -s 'r' -d 'Reverse sort order'
|
|
158
163
|
complete -c quilltap -n '__quilltap_using_subcommand docs' -l 'depth' -d 'Maximum traversal depth' -x
|
|
159
164
|
complete -c quilltap -n '__quilltap_using_subcommand docs' -l 'max-nodes' -d 'Maximum graph nodes' -x
|
|
@@ -93,6 +93,7 @@ _quilltap_db() {
|
|
|
93
93
|
'message:Get a single message'
|
|
94
94
|
'log:Get a single LLM log entry'
|
|
95
95
|
'memories:List memories for a character'
|
|
96
|
+
'characters:Per-character vault status report'
|
|
96
97
|
'optimize:Optimize database'
|
|
97
98
|
'backup:Backup database'
|
|
98
99
|
'integrity:Check database integrity'
|
|
@@ -119,6 +120,9 @@ _quilltap_db() {
|
|
|
119
120
|
'--from[Participant filter]:participant:'
|
|
120
121
|
'--type[Filter by type]:type:'
|
|
121
122
|
'--out[Output directory]:path:_files -/'
|
|
123
|
+
'--id[Single character by name or id]:character:'
|
|
124
|
+
'--diverged[Only characters whose DB and vault differ]'
|
|
125
|
+
'--blocked[Only characters with vault issues]'
|
|
122
126
|
'--tables[List tables (low-level)]'
|
|
123
127
|
'--count[Count rows in table]:table:'
|
|
124
128
|
'--repl[Open SQL REPL]'
|
|
@@ -144,6 +148,7 @@ _quilltap_docs() {
|
|
|
144
148
|
'files:List files in a mount'
|
|
145
149
|
'ls:ls-style listing'
|
|
146
150
|
'dir:Directory listing'
|
|
151
|
+
'tree:ASCII tree view of a folder'
|
|
147
152
|
'read:Print file contents'
|
|
148
153
|
'export:Export mount to directory'
|
|
149
154
|
'scan:Trigger rescan'
|
|
@@ -181,7 +186,7 @@ _quilltap_docs() {
|
|
|
181
186
|
'-l[Paths only]'
|
|
182
187
|
'--wait[Wait for completion]'
|
|
183
188
|
'(-R --recursive)'{-R,--recursive}'[Recursive listing]'
|
|
184
|
-
'--sort[Sort field]:field:(name
|
|
189
|
+
'--sort[Sort field]:field:(name time size links)'
|
|
185
190
|
'(-r --reverse)'{-r,--reverse}'[Reverse sort order]'
|
|
186
191
|
'--depth[Maximum traversal depth]:n:'
|
|
187
192
|
'--max-nodes[Maximum nodes in graph]:n:'
|
package/lib/db-commands.js
CHANGED
|
@@ -10,6 +10,8 @@ const {
|
|
|
10
10
|
UUID_RE,
|
|
11
11
|
ambiguous,
|
|
12
12
|
resolveCharacter,
|
|
13
|
+
resolveCharactersByAlias,
|
|
14
|
+
readVaultAliases,
|
|
13
15
|
resolveChat,
|
|
14
16
|
resolveProject,
|
|
15
17
|
} = require('./db-helpers');
|
|
@@ -318,21 +320,56 @@ function cmdFind(args, ctx) {
|
|
|
318
320
|
}
|
|
319
321
|
|
|
320
322
|
function findCharacters(query, { json, limit, ctx }) {
|
|
323
|
+
// Aliases live in each character's vault `properties.json` post-4.6, not on
|
|
324
|
+
// the `characters` row, so name matching comes from the main DB and alias
|
|
325
|
+
// matching/display comes from the mount-index DB.
|
|
321
326
|
const db = ctx.openMain();
|
|
327
|
+
let mounts = null;
|
|
322
328
|
try {
|
|
329
|
+
mounts = ctx.openMounts();
|
|
330
|
+
} catch {
|
|
331
|
+
mounts = null; // mount-index DB absent — name-only matching, no aliases
|
|
332
|
+
}
|
|
333
|
+
try {
|
|
334
|
+
const SELECT_COLS =
|
|
335
|
+
'SELECT id, name, npc, isFavorite, controlledBy, characterDocumentMountPointId AS mp FROM characters';
|
|
323
336
|
let rows;
|
|
324
337
|
if (!query) {
|
|
325
|
-
rows = db.prepare(
|
|
338
|
+
rows = db.prepare(`${SELECT_COLS} ORDER BY name LIMIT ?`).all(limit);
|
|
326
339
|
} else if (UUID_RE.test(query)) {
|
|
327
|
-
rows = db.prepare(
|
|
340
|
+
rows = db.prepare(`${SELECT_COLS} WHERE id = ?`).all(query);
|
|
328
341
|
} else {
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
342
|
+
const byName = db
|
|
343
|
+
.prepare(`${SELECT_COLS} WHERE LOWER(name) LIKE LOWER(?) ORDER BY name`)
|
|
344
|
+
.all(`%${query}%`);
|
|
345
|
+
const seen = new Set(byName.map((r) => r.id));
|
|
346
|
+
rows = [...byName];
|
|
347
|
+
// Fold in alias matches from the vault, then re-fetch their rows so the
|
|
348
|
+
// output columns stay uniform.
|
|
349
|
+
for (const m of resolveCharactersByAlias(db, mounts, query)) {
|
|
350
|
+
if (seen.has(m.id)) continue;
|
|
351
|
+
seen.add(m.id);
|
|
352
|
+
const r = db.prepare(`${SELECT_COLS} WHERE id = ?`).get(m.id);
|
|
353
|
+
if (r) rows.push(r);
|
|
354
|
+
}
|
|
355
|
+
rows = rows.slice(0, limit);
|
|
332
356
|
}
|
|
333
|
-
|
|
334
|
-
|
|
357
|
+
|
|
358
|
+
// Attach vault-sourced aliases for display and drop the internal mount id.
|
|
359
|
+
const enriched = rows.map(({ mp, ...rest }) => ({
|
|
360
|
+
...rest,
|
|
361
|
+
aliases: readVaultAliases(mounts, mp),
|
|
362
|
+
}));
|
|
363
|
+
|
|
364
|
+
if (json) return printJson(enriched);
|
|
365
|
+
printTable(
|
|
366
|
+
enriched.map((r) => ({
|
|
367
|
+
...r,
|
|
368
|
+
aliases: r.aliases.length ? r.aliases.join(', ') : '',
|
|
369
|
+
})),
|
|
370
|
+
);
|
|
335
371
|
} finally {
|
|
372
|
+
if (mounts) { try { mounts.close(); } catch {} }
|
|
336
373
|
db.close();
|
|
337
374
|
}
|
|
338
375
|
}
|
|
@@ -388,7 +425,7 @@ function cmdChats(args, ctx) {
|
|
|
388
425
|
try {
|
|
389
426
|
let rows;
|
|
390
427
|
if (flags.character) {
|
|
391
|
-
const c = resolveCharacter(db, String(flags.character));
|
|
428
|
+
const c = resolveCharacter(db, String(flags.character), ctx.openMounts);
|
|
392
429
|
rows = db.prepare(
|
|
393
430
|
"SELECT id, title, chatType, messageCount, lastMessageAt, projectId " +
|
|
394
431
|
"FROM chats WHERE participants LIKE ? ORDER BY lastMessageAt DESC LIMIT ?"
|
|
@@ -488,7 +525,7 @@ function cmdLogs(args, ctx) {
|
|
|
488
525
|
} else if (flags.character) {
|
|
489
526
|
const main = ctx.openMain();
|
|
490
527
|
let c;
|
|
491
|
-
try { c = resolveCharacter(main, String(flags.character)); } finally { main.close(); }
|
|
528
|
+
try { c = resolveCharacter(main, String(flags.character), ctx.openMounts); } finally { main.close(); }
|
|
492
529
|
rows = logsDb.prepare(
|
|
493
530
|
'SELECT id, createdAt, type, provider, modelName, chatId, messageId, durationMs FROM llm_logs WHERE characterId = ? ORDER BY createdAt DESC LIMIT ?'
|
|
494
531
|
).all(c.id, limit);
|
|
@@ -605,11 +642,11 @@ function cmdMemories(args, ctx) {
|
|
|
605
642
|
|
|
606
643
|
const db = ctx.openMain();
|
|
607
644
|
try {
|
|
608
|
-
const holder = resolveCharacter(db, String(flags.character));
|
|
645
|
+
const holder = resolveCharacter(db, String(flags.character), ctx.openMounts);
|
|
609
646
|
const conditions = ['characterId = ?'];
|
|
610
647
|
const params = [holder.id];
|
|
611
648
|
if (flags.about) {
|
|
612
|
-
const a = resolveCharacter(db, String(flags.about));
|
|
649
|
+
const a = resolveCharacter(db, String(flags.about), ctx.openMounts);
|
|
613
650
|
conditions.push('aboutCharacterId = ?');
|
|
614
651
|
params.push(a.id);
|
|
615
652
|
}
|
|
@@ -645,18 +682,33 @@ function cmdMemories(args, ctx) {
|
|
|
645
682
|
|
|
646
683
|
// ---------- verb: characters ----------
|
|
647
684
|
|
|
648
|
-
//
|
|
649
|
-
// in sync with `CHARACTER_VAULT_DESCRIPTORS` in
|
|
650
|
-
// lib/database/repositories/
|
|
651
|
-
//
|
|
652
|
-
//
|
|
653
|
-
|
|
685
|
+
// Single-file vault documents the character-properties overlay manages. Must
|
|
686
|
+
// stay in sync with `CHARACTER_VAULT_DESCRIPTORS` in
|
|
687
|
+
// lib/database/repositories/vault-overlay/. Post-4.6-cutover the vault is the
|
|
688
|
+
// only home for these fields.
|
|
689
|
+
//
|
|
690
|
+
// REQUIRED files are written unconditionally by `writeCharacterVaultManagedFields`
|
|
691
|
+
// (empty string when the field is blank), so a healthy character always has all
|
|
692
|
+
// of them. The physical-* pair is OPTIONAL: the writer skips both when the
|
|
693
|
+
// character has no physicalDescription. It writes them as a pair, so having
|
|
694
|
+
// exactly one of the two is an inconsistency worth flagging.
|
|
695
|
+
const REQUIRED_VAULT_SINGLE_FILES = [
|
|
654
696
|
'properties.json',
|
|
655
697
|
'identity.md',
|
|
656
698
|
'description.md',
|
|
657
|
-
'manifesto.md',
|
|
658
699
|
'personality.md',
|
|
659
700
|
'example-dialogues.md',
|
|
701
|
+
];
|
|
702
|
+
// manifesto is nullable/optional. The full-projection writer writes an empty
|
|
703
|
+
// manifesto.md when the field is blank, but the patch-level write overlay only
|
|
704
|
+
// writes it when a patch carries a `manifesto` key — so a perfectly valid
|
|
705
|
+
// character can simply lack the file. On read, absent == empty == null, so we
|
|
706
|
+
// report manifesto's presence but never count its absence as "missing" or let
|
|
707
|
+
// it block. (Treated like the physical-* pair: optional, surfaced, not required.)
|
|
708
|
+
const OPTIONAL_VAULT_SINGLE_FILES = [
|
|
709
|
+
'manifesto.md',
|
|
710
|
+
];
|
|
711
|
+
const PHYSICAL_VAULT_FILES = [
|
|
660
712
|
'physical-description.md',
|
|
661
713
|
'physical-prompts.json',
|
|
662
714
|
];
|
|
@@ -695,8 +747,11 @@ function inspectCharacterVault(row, mounts) {
|
|
|
695
747
|
mountPointId: row.characterDocumentMountPointId || null,
|
|
696
748
|
vault: 'missing',
|
|
697
749
|
presentSingleFiles: 0,
|
|
698
|
-
expectedSingleFiles:
|
|
750
|
+
expectedSingleFiles: REQUIRED_VAULT_SINGLE_FILES.length,
|
|
699
751
|
missingSingleFiles: [],
|
|
752
|
+
manifestoPresent: false, // optional single file: present or not, both valid
|
|
753
|
+
physicalFilesPresent: 0, // 0, 1, or 2 of the optional physical-* pair
|
|
754
|
+
physicalInconsistent: false,
|
|
700
755
|
promptsVault: 0,
|
|
701
756
|
promptsDb: 0,
|
|
702
757
|
scenariosVault: 0,
|
|
@@ -729,7 +784,7 @@ function inspectCharacterVault(row, mounts) {
|
|
|
729
784
|
byPath.set(link.relativePath.toLowerCase(), link);
|
|
730
785
|
}
|
|
731
786
|
|
|
732
|
-
for (const p of
|
|
787
|
+
for (const p of REQUIRED_VAULT_SINGLE_FILES) {
|
|
733
788
|
if (byPath.has(p)) {
|
|
734
789
|
status.presentSingleFiles++;
|
|
735
790
|
} else {
|
|
@@ -737,6 +792,14 @@ function inspectCharacterVault(row, mounts) {
|
|
|
737
792
|
}
|
|
738
793
|
}
|
|
739
794
|
|
|
795
|
+
// Manifesto is optional: report presence, never require it.
|
|
796
|
+
status.manifestoPresent = OPTIONAL_VAULT_SINGLE_FILES.every((p) => byPath.has(p));
|
|
797
|
+
|
|
798
|
+
// Physical-* files are optional (a character may legitimately have no
|
|
799
|
+
// physical description). Both-or-neither is healthy; exactly one is not.
|
|
800
|
+
status.physicalFilesPresent = PHYSICAL_VAULT_FILES.filter((p) => byPath.has(p)).length;
|
|
801
|
+
status.physicalInconsistent = status.physicalFilesPresent === 1;
|
|
802
|
+
|
|
740
803
|
for (const [p] of byPath) {
|
|
741
804
|
if (p.startsWith('prompts/') && p.endsWith('.md')) status.promptsVault++;
|
|
742
805
|
else if (p.startsWith('scenarios/') && p.endsWith('.md')) status.scenariosVault++;
|
|
@@ -835,10 +898,18 @@ function inspectCharacterVault(row, mounts) {
|
|
|
835
898
|
}
|
|
836
899
|
}
|
|
837
900
|
|
|
838
|
-
|
|
901
|
+
const anyContent = status.presentSingleFiles > 0
|
|
902
|
+
|| status.manifestoPresent
|
|
903
|
+
|| status.physicalFilesPresent > 0
|
|
904
|
+
|| status.promptsVault > 0
|
|
905
|
+
|| status.scenariosVault > 0
|
|
906
|
+
|| status.wardrobeVault > 0;
|
|
907
|
+
if (!anyContent) {
|
|
839
908
|
status.issue = 'vault empty';
|
|
840
909
|
} else if (status.missingSingleFiles.length > 0) {
|
|
841
|
-
status.issue = `${status.missingSingleFiles.length} files missing`;
|
|
910
|
+
status.issue = `${status.missingSingleFiles.length} required files missing`;
|
|
911
|
+
} else if (status.physicalInconsistent) {
|
|
912
|
+
status.issue = 'physical files incomplete (1 of 2)';
|
|
842
913
|
} else if (status.diverged.length > 0) {
|
|
843
914
|
status.issue = `diverged (${status.diverged.length})`;
|
|
844
915
|
} else if (!preCutover) {
|
|
@@ -886,7 +957,7 @@ function cmdCharacters(args, ctx) {
|
|
|
886
957
|
let sql = `SELECT ${cols.join(', ')} FROM characters`;
|
|
887
958
|
const params = [];
|
|
888
959
|
if (idQuery) {
|
|
889
|
-
const c = resolveCharacter(main, idQuery);
|
|
960
|
+
const c = resolveCharacter(main, idQuery, ctx.openMounts);
|
|
890
961
|
sql += ' WHERE id = ?';
|
|
891
962
|
params.push(c.id);
|
|
892
963
|
} else {
|
|
@@ -920,22 +991,32 @@ function cmdCharacters(args, ctx) {
|
|
|
920
991
|
}
|
|
921
992
|
|
|
922
993
|
const summary = summarizeCharacterStatuses(all);
|
|
923
|
-
|
|
994
|
+
let headline = `Scanned ${all.length} character${all.length === 1 ? '' : 's'}: ` +
|
|
924
995
|
`${summary.ok} ok, ${summary.diverged} diverged, ${summary.missingFiles} with missing files, ` +
|
|
925
|
-
`${summary.noVault} with no vault, ${summary.empty} empty
|
|
996
|
+
`${summary.noVault} with no vault, ${summary.empty} empty`;
|
|
997
|
+
if (summary.physIncomplete > 0) headline += `, ${summary.physIncomplete} with incomplete physical files`;
|
|
998
|
+
headline += '.';
|
|
926
999
|
console.log(headline);
|
|
927
1000
|
console.log('');
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
1001
|
+
// The readPropertiesFromDocumentStore flag and the DB side of the
|
|
1002
|
+
// prompts/scenarios counts only exist before the 4.6 cutover. Post-cutover
|
|
1003
|
+
// (the normal case now) the vault is canonical, so drop the dead `flag`
|
|
1004
|
+
// column and show vault-only counts instead of misleading `vault/db`.
|
|
1005
|
+
const anyPreCutover = all.some(s => s.preCutover);
|
|
1006
|
+
printTable(filtered.map(s => {
|
|
1007
|
+
const missing = s.vault === 'missing';
|
|
1008
|
+
const row = { id: s.id.slice(0, 8), name: truncate(s.name, 28) };
|
|
1009
|
+
if (anyPreCutover) row.flag = s.flag == null ? '-' : s.flag;
|
|
1010
|
+
row.vault = s.vault;
|
|
1011
|
+
row.files = missing ? '-' : `${s.presentSingleFiles}/${s.expectedSingleFiles}`;
|
|
1012
|
+
row.manifesto = missing ? '-' : (s.manifestoPresent ? 'yes' : 'no');
|
|
1013
|
+
row.phys = missing ? '-' : `${s.physicalFilesPresent}/2`;
|
|
1014
|
+
row.prompts = missing ? '-' : (anyPreCutover ? `${s.promptsVault}/${s.promptsDb}` : String(s.promptsVault));
|
|
1015
|
+
row.scenarios = missing ? '-' : (anyPreCutover ? `${s.scenariosVault}/${s.scenariosDb}` : String(s.scenariosVault));
|
|
1016
|
+
row.wardrobe = missing ? '-' : s.wardrobeVault;
|
|
1017
|
+
row.status = truncate(s.issue, 60);
|
|
1018
|
+
return row;
|
|
1019
|
+
}));
|
|
939
1020
|
|
|
940
1021
|
if (filtered.length > 0 && filtered.some(s => s.diverged.length > 0)) {
|
|
941
1022
|
console.log('');
|
|
@@ -948,16 +1029,17 @@ function cmdCharacters(args, ctx) {
|
|
|
948
1029
|
}
|
|
949
1030
|
|
|
950
1031
|
function summarizeCharacterStatuses(all) {
|
|
951
|
-
let ok = 0, diverged = 0, missingFiles = 0, noVault = 0, empty = 0;
|
|
1032
|
+
let ok = 0, diverged = 0, missingFiles = 0, noVault = 0, empty = 0, physIncomplete = 0;
|
|
952
1033
|
for (const s of all) {
|
|
953
1034
|
if (!s.issue) continue;
|
|
954
1035
|
if (s.issue.startsWith('ok')) ok++;
|
|
955
1036
|
else if (s.issue === 'no vault') noVault++;
|
|
956
1037
|
else if (s.issue === 'vault empty') empty++;
|
|
957
1038
|
else if (s.issue.endsWith(' files missing')) missingFiles++;
|
|
1039
|
+
else if (s.issue.startsWith('physical files incomplete')) physIncomplete++;
|
|
958
1040
|
else if (s.issue.startsWith('diverged')) diverged++;
|
|
959
1041
|
}
|
|
960
|
-
return { ok, diverged, missingFiles, noVault, empty };
|
|
1042
|
+
return { ok, diverged, missingFiles, noVault, empty, physIncomplete };
|
|
961
1043
|
}
|
|
962
1044
|
|
|
963
1045
|
// ---------- verb: optimize ----------
|
package/lib/db-helpers.js
CHANGED
|
@@ -255,25 +255,104 @@ function ambiguous(kind, rows) {
|
|
|
255
255
|
return err;
|
|
256
256
|
}
|
|
257
257
|
|
|
258
|
-
|
|
258
|
+
// Read the `aliases` array out of a character vault's `properties.json`.
|
|
259
|
+
// The 4.6 vault cutover dropped the `aliases` (and `pronouns`) columns from
|
|
260
|
+
// the `characters` table — they now live only in the per-character vault — so
|
|
261
|
+
// any alias lookup has to go through the mount-index DB. `mounts` is that
|
|
262
|
+
// handle; `mountPointId` is `characters.characterDocumentMountPointId`.
|
|
263
|
+
// Returns [] for a missing/empty/unparseable vault so callers can treat the
|
|
264
|
+
// absence of aliases as "no match" rather than an error.
|
|
265
|
+
function readVaultAliases(mounts, mountPointId) {
|
|
266
|
+
if (!mounts || !mountPointId) return [];
|
|
267
|
+
const rec = mounts.prepare(
|
|
268
|
+
`SELECT d.content AS content
|
|
269
|
+
FROM doc_mount_file_links l
|
|
270
|
+
JOIN doc_mount_documents d ON d.fileId = l.fileId
|
|
271
|
+
WHERE l.mountPointId = ? AND LOWER(l.relativePath) = 'properties.json'
|
|
272
|
+
LIMIT 1`
|
|
273
|
+
).get(mountPointId);
|
|
274
|
+
if (!rec || !rec.content) return [];
|
|
275
|
+
try {
|
|
276
|
+
const props = JSON.parse(rec.content);
|
|
277
|
+
return Array.isArray(props.aliases)
|
|
278
|
+
? props.aliases.filter((a) => typeof a === 'string')
|
|
279
|
+
: [];
|
|
280
|
+
} catch {
|
|
281
|
+
return [];
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Find characters whose vault aliases contain `query` (case-insensitive
|
|
286
|
+
// substring). Requires a mount-index DB handle; returns [{ id, name }].
|
|
287
|
+
// Scans every vaulted character's `properties.json` — fine for a CLI, and only
|
|
288
|
+
// reached on the fuzzy-resolution fallback (a clean name match returns first).
|
|
289
|
+
function resolveCharactersByAlias(db, mounts, query) {
|
|
290
|
+
if (!mounts) return [];
|
|
291
|
+
const needle = query.toLowerCase();
|
|
292
|
+
const chars = db.prepare(
|
|
293
|
+
'SELECT id, name, characterDocumentMountPointId AS mp FROM characters WHERE characterDocumentMountPointId IS NOT NULL'
|
|
294
|
+
).all();
|
|
295
|
+
const matches = [];
|
|
296
|
+
for (const c of chars) {
|
|
297
|
+
const aliases = readVaultAliases(mounts, c.mp);
|
|
298
|
+
if (aliases.some((a) => a.toLowerCase().includes(needle))) {
|
|
299
|
+
matches.push({ id: c.id, name: c.name });
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
return matches;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Resolve a UUID / name / alias to a single { id, name } row.
|
|
306
|
+
//
|
|
307
|
+
// `openMounts` (optional) is a zero-arg function that returns a mount-index DB
|
|
308
|
+
// handle (e.g. `ctx.openMounts`). When supplied, the fuzzy fallback also
|
|
309
|
+
// matches against vault-stored aliases; when omitted (or if the mount-index DB
|
|
310
|
+
// can't be opened), resolution degrades gracefully to UUID + name only.
|
|
311
|
+
function resolveCharacter(db, query, openMounts = null) {
|
|
259
312
|
if (UUID_RE.test(query)) {
|
|
260
|
-
const row = db.prepare('SELECT id, name
|
|
313
|
+
const row = db.prepare('SELECT id, name FROM characters WHERE id = ?').get(query);
|
|
261
314
|
if (!row) throw new Error(`No character with id ${query}`);
|
|
262
315
|
return row;
|
|
263
316
|
}
|
|
264
317
|
const exact = db.prepare(
|
|
265
|
-
'SELECT id, name
|
|
318
|
+
'SELECT id, name FROM characters WHERE LOWER(name) = LOWER(?)'
|
|
266
319
|
).all(query);
|
|
267
320
|
if (exact.length === 1) return exact[0];
|
|
268
321
|
if (exact.length > 1) {
|
|
269
322
|
throw ambiguous('character', exact);
|
|
270
323
|
}
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
324
|
+
|
|
325
|
+
const candidates = db.prepare(
|
|
326
|
+
'SELECT id, name FROM characters WHERE LOWER(name) LIKE LOWER(?) ORDER BY name'
|
|
327
|
+
).all(`%${query}%`);
|
|
328
|
+
|
|
329
|
+
// Fold in alias matches from the vault when the caller gave us a way to
|
|
330
|
+
// reach the mount-index DB. Aliases moved into properties.json in 4.6.
|
|
331
|
+
if (openMounts) {
|
|
332
|
+
let mounts = null;
|
|
333
|
+
try {
|
|
334
|
+
mounts = openMounts();
|
|
335
|
+
} catch {
|
|
336
|
+
mounts = null; // mount-index DB absent on this instance — skip aliases
|
|
337
|
+
}
|
|
338
|
+
if (mounts) {
|
|
339
|
+
try {
|
|
340
|
+
const seen = new Set(candidates.map((r) => r.id));
|
|
341
|
+
for (const m of resolveCharactersByAlias(db, mounts, query)) {
|
|
342
|
+
if (!seen.has(m.id)) {
|
|
343
|
+
seen.add(m.id);
|
|
344
|
+
candidates.push(m);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
} finally {
|
|
348
|
+
try { mounts.close(); } catch {}
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
if (candidates.length === 0) throw new Error(`No character matching '${query}'`);
|
|
354
|
+
if (candidates.length > 1) throw ambiguous('character', candidates);
|
|
355
|
+
return candidates[0];
|
|
277
356
|
}
|
|
278
357
|
|
|
279
358
|
function resolveChat(db, query) {
|
|
@@ -317,6 +396,8 @@ module.exports = {
|
|
|
317
396
|
UUID_RE,
|
|
318
397
|
ambiguous,
|
|
319
398
|
resolveCharacter,
|
|
399
|
+
resolveCharactersByAlias,
|
|
400
|
+
readVaultAliases,
|
|
320
401
|
resolveChat,
|
|
321
402
|
resolveProject,
|
|
322
403
|
};
|
package/lib/memories-commands.js
CHANGED
|
@@ -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
|
-
|
|
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 {
|
|
@@ -26,7 +26,7 @@ function parseFlags(args) {
|
|
|
26
26
|
const a = args[i];
|
|
27
27
|
switch (a) {
|
|
28
28
|
case '-d': case '--data-dir': flags.dataDir = args[++i]; break;
|
|
29
|
-
case '--instance': flags.instance = args[++i]; break;
|
|
29
|
+
case '-i': case '--instance': flags.instance = args[++i]; break;
|
|
30
30
|
case '--passphrase': flags.passphrase = args[++i]; break;
|
|
31
31
|
case '--json': flags.json = true; break;
|
|
32
32
|
case '-h': case '--help': flags.help = true; break;
|
|
@@ -118,7 +118,7 @@ Commands:
|
|
|
118
118
|
|
|
119
119
|
Options:
|
|
120
120
|
-d, --data-dir <path> Use a specific data directory
|
|
121
|
-
--instance <name>
|
|
121
|
+
-i, --instance <name> Use a named instance
|
|
122
122
|
--passphrase <pass> Provide passphrase (prompts if needed)
|
|
123
123
|
--json Output as JSON
|
|
124
124
|
-h, --help Show this help message
|
package/lib/native-modules.js
CHANGED
|
@@ -21,6 +21,48 @@ function resolveModuleDir(moduleName) {
|
|
|
21
21
|
}
|
|
22
22
|
}
|
|
23
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
|
+
|
|
24
66
|
// Check if native modules are compiled for the current Node.js version.
|
|
25
67
|
// This handles the case where npx caches the package but the user upgrades
|
|
26
68
|
// Node.js — the cached native modules will have a stale NODE_MODULE_VERSION.
|
|
@@ -73,7 +115,10 @@ function ensureNativeModules() {
|
|
|
73
115
|
}
|
|
74
116
|
}
|
|
75
117
|
|
|
76
|
-
if (needsRebuild.length === 0)
|
|
118
|
+
if (needsRebuild.length === 0) {
|
|
119
|
+
reconcileNodePtySpawnHelper();
|
|
120
|
+
return true;
|
|
121
|
+
}
|
|
77
122
|
|
|
78
123
|
console.log(` Rebuilding native modules for Node.js ${process.version}...`);
|
|
79
124
|
|
|
@@ -84,6 +129,7 @@ function ensureNativeModules() {
|
|
|
84
129
|
});
|
|
85
130
|
console.log(' Done.');
|
|
86
131
|
console.log('');
|
|
132
|
+
reconcileNodePtySpawnHelper();
|
|
87
133
|
return true;
|
|
88
134
|
} catch (err) {
|
|
89
135
|
console.error('');
|
|
@@ -94,7 +140,7 @@ function ensureNativeModules() {
|
|
|
94
140
|
}
|
|
95
141
|
}
|
|
96
142
|
|
|
97
|
-
module.exports = { resolveModuleDir, ensureNativeModules, PACKAGE_DIR };
|
|
143
|
+
module.exports = { resolveModuleDir, ensureNativeModules, reconcileNodePtySpawnHelper, PACKAGE_DIR };
|
|
98
144
|
|
|
99
145
|
// Allow this file to be invoked directly as a postinstall script:
|
|
100
146
|
// node lib/native-modules.js
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "quilltap",
|
|
3
|
-
"version": "4.6.0
|
|
3
|
+
"version": "4.6.0",
|
|
4
4
|
"description": "Self-hosted AI workspace for writers, worldbuilders, and roleplayers. Run with npx quilltap.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Charles Sebold",
|
|
@@ -40,8 +40,8 @@
|
|
|
40
40
|
"better-sqlite3-multiple-ciphers": "^12.10.0",
|
|
41
41
|
"node-pty": "^1.1.0",
|
|
42
42
|
"sharp": "^0.34.5",
|
|
43
|
-
"tar": "^7.5.
|
|
44
|
-
"yauzl": "^3.3.
|
|
43
|
+
"tar": "^7.5.16",
|
|
44
|
+
"yauzl": "^3.3.2"
|
|
45
45
|
},
|
|
46
46
|
"engines": {
|
|
47
47
|
"node": ">=24.0.0"
|