quilltap 4.6.0-dev → 4.6.0-dev.39
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 +78 -22
- package/lib/completion/bash.template +152 -33
- package/lib/completion/fish.template +262 -87
- package/lib/completion/zsh.template +219 -26
- package/lib/db-commands.js +325 -0
- package/package.json +1 -1
|
@@ -16,6 +16,7 @@ _quilltap() {
|
|
|
16
16
|
'(-v --version)'{-v,--version}'[Show version number]'
|
|
17
17
|
'(-h --help)'{-h,--help}'[Show help message]'
|
|
18
18
|
'--update[Force re-download of application files]'
|
|
19
|
+
'--passphrase[Database passphrase]:passphrase:'
|
|
19
20
|
)
|
|
20
21
|
|
|
21
22
|
subcommands=(
|
|
@@ -25,6 +26,8 @@ _quilltap() {
|
|
|
25
26
|
'instances:Register or inspect named Quilltap instances'
|
|
26
27
|
'memories:Search, browse, and graph memories'
|
|
27
28
|
'memory-diff:Dump existing memories and dry-run re-extraction'
|
|
29
|
+
'logs:Tail or print an instance log file'
|
|
30
|
+
'migrations:Inspect migration status'
|
|
28
31
|
'completion:Generate shell completion scripts'
|
|
29
32
|
)
|
|
30
33
|
|
|
@@ -67,6 +70,12 @@ _quilltap_subcommand() {
|
|
|
67
70
|
memory-diff)
|
|
68
71
|
_quilltap_memory_diff
|
|
69
72
|
;;
|
|
73
|
+
logs)
|
|
74
|
+
_quilltap_logs
|
|
75
|
+
;;
|
|
76
|
+
migrations)
|
|
77
|
+
_quilltap_migrations
|
|
78
|
+
;;
|
|
70
79
|
completion)
|
|
71
80
|
_quilltap_completion
|
|
72
81
|
;;
|
|
@@ -74,7 +83,7 @@ _quilltap_subcommand() {
|
|
|
74
83
|
}
|
|
75
84
|
|
|
76
85
|
_quilltap_db() {
|
|
77
|
-
local -a subverbs
|
|
86
|
+
local -a subverbs db_opts
|
|
78
87
|
subverbs=(
|
|
79
88
|
'schema:Show database schema'
|
|
80
89
|
'find:Find entities by name'
|
|
@@ -89,11 +98,46 @@ _quilltap_db() {
|
|
|
89
98
|
'integrity:Check database integrity'
|
|
90
99
|
)
|
|
91
100
|
|
|
92
|
-
|
|
101
|
+
db_opts=(
|
|
102
|
+
'(-i --instance)'{-i,--instance}'[Registered instance name]:instance:_quilltap_instance_names'
|
|
103
|
+
'(-d --data-dir)'{-d,--data-dir}'[Data directory]:directory:_directories'
|
|
104
|
+
'--passphrase[Database passphrase]:passphrase:'
|
|
105
|
+
'--json[JSON output]'
|
|
106
|
+
'--limit[Result limit]:limit:'
|
|
107
|
+
'--grep[Substring search]:text:'
|
|
108
|
+
'--character[Character name or id]:character:'
|
|
109
|
+
'--project[Project name or id]:project:'
|
|
110
|
+
'--about[Subject character]:character:'
|
|
111
|
+
'--source[Memory source]:source:(AUTO MANUAL)'
|
|
112
|
+
'--chat[Chat name or id]:chat:'
|
|
113
|
+
'--message[Message id]:id:'
|
|
114
|
+
'--rendered[Render rich content]'
|
|
115
|
+
'--field[Log field]:field:(request response both)'
|
|
116
|
+
'--tail[Tail N rows]:n:'
|
|
117
|
+
'--last[Last N items]:n:'
|
|
118
|
+
'--full[Full output]'
|
|
119
|
+
'--from[Participant filter]:participant:'
|
|
120
|
+
'--type[Filter by type]:type:'
|
|
121
|
+
'--out[Output directory]:path:_files -/'
|
|
122
|
+
'--tables[List tables (low-level)]'
|
|
123
|
+
'--count[Count rows in table]:table:'
|
|
124
|
+
'--repl[Open SQL REPL]'
|
|
125
|
+
'--llm-logs[Target llm-logs database]'
|
|
126
|
+
'--mount-points[Target mount-index database]'
|
|
127
|
+
'--lock-status[Show instance lock status]'
|
|
128
|
+
'--lock-clean[Clean stale lock]'
|
|
129
|
+
'--lock-override[Override active lock]'
|
|
130
|
+
'(-h --help)'{-h,--help}'[Show help]'
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
if (( CURRENT == 2 )); then
|
|
134
|
+
_describe 'db subcommand' subverbs
|
|
135
|
+
fi
|
|
136
|
+
_arguments $db_opts
|
|
93
137
|
}
|
|
94
138
|
|
|
95
139
|
_quilltap_docs() {
|
|
96
|
-
local -a subverbs
|
|
140
|
+
local -a subverbs docs_opts
|
|
97
141
|
subverbs=(
|
|
98
142
|
'list:List all mount points'
|
|
99
143
|
'show:Details for one mount point'
|
|
@@ -115,11 +159,45 @@ _quilltap_docs() {
|
|
|
115
159
|
'copy:Copy file'
|
|
116
160
|
)
|
|
117
161
|
|
|
118
|
-
|
|
162
|
+
docs_opts=(
|
|
163
|
+
'(-i --instance)'{-i,--instance}'[Registered instance name]:instance:_quilltap_instance_names'
|
|
164
|
+
'(-d --data-dir)'{-d,--data-dir}'[Data directory]:directory:_directories'
|
|
165
|
+
'--passphrase[Database passphrase]:passphrase:'
|
|
166
|
+
'(-p --port)'{-p,--port}'[Server port]:port:'
|
|
167
|
+
'--json[JSON output]'
|
|
168
|
+
'--mount[Mount name or id]:mount:'
|
|
169
|
+
'--folder[Folder filter]:folder:'
|
|
170
|
+
'--force[Force operation]'
|
|
171
|
+
'--rendered[Render rich content]'
|
|
172
|
+
'--links[Include link details]'
|
|
173
|
+
'--type[Filter by type]:type:(file folder)'
|
|
174
|
+
'--ext[Extension filter]:ext:'
|
|
175
|
+
'--limit[Result limit]:n:'
|
|
176
|
+
'--max[Maximum results]:n:'
|
|
177
|
+
'--context[Context lines]:n:'
|
|
178
|
+
'--top[Top N results]:n:'
|
|
179
|
+
'--threshold[Similarity threshold]:value:'
|
|
180
|
+
'--ignore-case[Case-insensitive search]'
|
|
181
|
+
'-l[Paths only]'
|
|
182
|
+
'--wait[Wait for completion]'
|
|
183
|
+
'(-R --recursive)'{-R,--recursive}'[Recursive listing]'
|
|
184
|
+
'--sort[Sort field]:field:(name path size modified created)'
|
|
185
|
+
'(-r --reverse)'{-r,--reverse}'[Reverse sort order]'
|
|
186
|
+
'--depth[Maximum traversal depth]:n:'
|
|
187
|
+
'--max-nodes[Maximum nodes in graph]:n:'
|
|
188
|
+
'--long[Long-form output]'
|
|
189
|
+
'--semantic[Use semantic search]'
|
|
190
|
+
'(-h --help)'{-h,--help}'[Show help]'
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
if (( CURRENT == 2 )); then
|
|
194
|
+
_describe 'docs subcommand' subverbs
|
|
195
|
+
fi
|
|
196
|
+
_arguments $docs_opts
|
|
119
197
|
}
|
|
120
198
|
|
|
121
199
|
_quilltap_themes() {
|
|
122
|
-
local -a subverbs
|
|
200
|
+
local -a subverbs registry_verbs themes_opts registry_opts
|
|
123
201
|
subverbs=(
|
|
124
202
|
'list:List available themes'
|
|
125
203
|
'install:Install a theme'
|
|
@@ -132,17 +210,50 @@ _quilltap_themes() {
|
|
|
132
210
|
'registry:Manage registries'
|
|
133
211
|
)
|
|
134
212
|
|
|
135
|
-
|
|
213
|
+
themes_opts=(
|
|
214
|
+
'(-i --instance)'{-i,--instance}'[Registered instance name]:instance:_quilltap_instance_names'
|
|
215
|
+
'(-d --data-dir)'{-d,--data-dir}'[Data directory]:directory:_directories'
|
|
216
|
+
'(-o --output)'{-o,--output}'[Output path]:path:_files'
|
|
217
|
+
'(-h --help)'{-h,--help}'[Show help]'
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
if (( CURRENT == 2 )); then
|
|
221
|
+
_describe 'themes subcommand' subverbs
|
|
222
|
+
return
|
|
223
|
+
fi
|
|
224
|
+
|
|
225
|
+
if [[ "$words[2]" == "registry" ]]; then
|
|
226
|
+
registry_verbs=(
|
|
227
|
+
'list:List registries'
|
|
228
|
+
'add:Add a registry'
|
|
229
|
+
'remove:Remove a registry'
|
|
230
|
+
'refresh:Refresh registries'
|
|
231
|
+
'keygen:Generate Ed25519 key'
|
|
232
|
+
'sign:Sign a registry or bundle'
|
|
233
|
+
)
|
|
234
|
+
registry_opts=(
|
|
235
|
+
'(-k --key)'{-k,--key}'[Signing key path]:path:_files'
|
|
236
|
+
'(-n --name)'{-n,--name}'[Registry name]:name:'
|
|
237
|
+
'(-o --output)'{-o,--output}'[Output path]:path:_files'
|
|
238
|
+
)
|
|
239
|
+
if (( CURRENT == 3 )); then
|
|
240
|
+
_describe 'registry subcommand' registry_verbs
|
|
241
|
+
fi
|
|
242
|
+
_arguments $registry_opts
|
|
243
|
+
return
|
|
244
|
+
fi
|
|
245
|
+
|
|
246
|
+
_arguments $themes_opts
|
|
136
247
|
}
|
|
137
248
|
|
|
138
249
|
_quilltap_instances() {
|
|
139
|
-
local -a subverbs
|
|
250
|
+
local -a subverbs inst_opts
|
|
140
251
|
subverbs=(
|
|
141
252
|
'list:List registered instances'
|
|
142
|
-
'ls:List instances'
|
|
253
|
+
'ls:List registered instances'
|
|
143
254
|
'show:Show instance details'
|
|
144
|
-
'path:Show
|
|
145
|
-
'where:Show
|
|
255
|
+
'path:Show instances.json path'
|
|
256
|
+
'where:Show instances.json path'
|
|
146
257
|
'add:Register a new instance'
|
|
147
258
|
'create:Register a new instance'
|
|
148
259
|
'remove:Unregister an instance'
|
|
@@ -150,9 +261,32 @@ _quilltap_instances() {
|
|
|
150
261
|
'delete:Unregister an instance'
|
|
151
262
|
'set-passphrase:Set instance passphrase'
|
|
152
263
|
'passphrase:Set instance passphrase'
|
|
264
|
+
'default:Set/show/clear the default instance'
|
|
265
|
+
'rename:Rename an instance (preserves passphrase)'
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
inst_opts=(
|
|
269
|
+
'--names-only[Print one name per line (for completion)]'
|
|
270
|
+
'--json[JSON output]'
|
|
271
|
+
'--clear[Clear value (for default)]'
|
|
272
|
+
'(-h --help)'{-h,--help}'[Show help]'
|
|
153
273
|
)
|
|
154
274
|
|
|
155
|
-
|
|
275
|
+
if (( CURRENT == 2 )); then
|
|
276
|
+
_describe 'instances subcommand' subverbs
|
|
277
|
+
return
|
|
278
|
+
fi
|
|
279
|
+
|
|
280
|
+
case "$words[2]" in
|
|
281
|
+
show|remove|rm|delete|set-passphrase|passphrase|default|rename)
|
|
282
|
+
if (( CURRENT == 3 )); then
|
|
283
|
+
_values 'instance' ${(f)"$(command quilltap instances list --names-only 2>/dev/null)"}
|
|
284
|
+
return
|
|
285
|
+
fi
|
|
286
|
+
;;
|
|
287
|
+
esac
|
|
288
|
+
|
|
289
|
+
_arguments $inst_opts
|
|
156
290
|
}
|
|
157
291
|
|
|
158
292
|
_quilltap_memories() {
|
|
@@ -168,24 +302,44 @@ _quilltap_memories() {
|
|
|
168
302
|
)
|
|
169
303
|
|
|
170
304
|
mem_opts=(
|
|
171
|
-
'--
|
|
172
|
-
'--
|
|
173
|
-
'--
|
|
174
|
-
'--
|
|
175
|
-
'--project[Project name or id]'
|
|
176
|
-
'--since[Since timestamp]'
|
|
177
|
-
'--until[Until timestamp]'
|
|
178
|
-
'--min-importance[Minimum importance]'
|
|
179
|
-
'--min-reinforced[Minimum reinforcement]'
|
|
180
|
-
'--has-embedding[Has embedding]'
|
|
181
|
-
'--no-embedding[No embedding]'
|
|
182
|
-
'--sort[Sort field]'
|
|
183
|
-
'--limit[Result limit]'
|
|
305
|
+
'(-i --instance)'{-i,--instance}'[Registered instance name]:instance:_quilltap_instance_names'
|
|
306
|
+
'(-d --data-dir)'{-d,--data-dir}'[Data directory]:directory:_directories'
|
|
307
|
+
'--passphrase[Database passphrase]:passphrase:'
|
|
308
|
+
'(-p --port)'{-p,--port}'[Server port]:port:'
|
|
184
309
|
'--json[JSON output]'
|
|
185
|
-
'--
|
|
310
|
+
'--character[Character name or id]:character:'
|
|
311
|
+
'--about[Subject of memory]:character:'
|
|
312
|
+
'--source[Memory source]:source:(AUTO MANUAL)'
|
|
313
|
+
'--chat[Chat id or title]:chat:'
|
|
314
|
+
'--project[Project name or id]:project:'
|
|
315
|
+
'--since[Since timestamp]:date:'
|
|
316
|
+
'--until[Until timestamp]:date:'
|
|
317
|
+
'--min-importance[Minimum importance]:value:'
|
|
318
|
+
'--min-reinforced[Minimum reinforcement]:value:'
|
|
319
|
+
'--has-embedding[Only memories with embeddings]'
|
|
320
|
+
'--no-embedding[Only memories without embeddings]'
|
|
321
|
+
'--sort[Sort field]:field:(reinforced importance created accessed reinforcement-count links)'
|
|
322
|
+
'(-r --reverse)'{-r,--reverse}'[Reverse sort]'
|
|
323
|
+
'--limit[Result limit]:n:'
|
|
324
|
+
'--full-titles[Show full titles]'
|
|
325
|
+
'--in[Restrict find-in field]:field:'
|
|
326
|
+
'--no-related[Hide related neighbors]'
|
|
327
|
+
'--list[List-only output]'
|
|
328
|
+
'(-i --ignore-case)'{-i,--ignore-case}'[Case-insensitive]'
|
|
329
|
+
'(-l --paths-only)'{-l,--paths-only}'[Paths only]'
|
|
330
|
+
'--max[Maximum results]:n:'
|
|
331
|
+
'--context[Context lines]:n:'
|
|
332
|
+
'--depth[Walk depth]:n:'
|
|
333
|
+
'--max-nodes[Maximum nodes]:n:'
|
|
334
|
+
'--semantic[Semantic search]'
|
|
335
|
+
'--top[Top N results]:n:'
|
|
336
|
+
'--threshold[Similarity threshold]:value:'
|
|
337
|
+
'(-h --help)'{-h,--help}'[Show help]'
|
|
186
338
|
)
|
|
187
339
|
|
|
188
|
-
|
|
340
|
+
if (( CURRENT == 2 )); then
|
|
341
|
+
_describe 'memories subcommand' subverbs
|
|
342
|
+
fi
|
|
189
343
|
_arguments $mem_opts
|
|
190
344
|
}
|
|
191
345
|
|
|
@@ -193,7 +347,46 @@ _quilltap_memory_diff() {
|
|
|
193
347
|
_arguments \
|
|
194
348
|
'(-h --help)'{-h,--help}'[Show help]' \
|
|
195
349
|
'(-i --instance)'{-i,--instance}'[Use instance]:instance:_quilltap_instance_names' \
|
|
350
|
+
'(-d --data-dir)'{-d,--data-dir}'[Data directory]:directory:_directories' \
|
|
351
|
+
'--passphrase[Database passphrase]:passphrase:' \
|
|
352
|
+
'(-p --port)'{-p,--port}'[Server port]:port:' \
|
|
353
|
+
'--concurrency[Concurrency limit]:n:' \
|
|
354
|
+
'--out[Output file]:path:_files'
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
_quilltap_logs() {
|
|
358
|
+
_arguments \
|
|
359
|
+
'(-h --help)'{-h,--help}'[Show help]' \
|
|
360
|
+
'(-i --instance)'{-i,--instance}'[Use instance]:instance:_quilltap_instance_names' \
|
|
361
|
+
'(-d --data-dir)'{-d,--data-dir}'[Data directory]:directory:_directories' \
|
|
362
|
+
'--passphrase[Database passphrase]:passphrase:' \
|
|
363
|
+
'--stream[Which log stream]:stream:(combined error stdout stderr startup)' \
|
|
364
|
+
'--tail[Last N lines (0=full)]:n:' \
|
|
365
|
+
'(-f --follow)'{-f,--follow}'[Stream new lines]' \
|
|
366
|
+
'--grep[Regex filter]:pattern:'
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
_quilltap_migrations() {
|
|
370
|
+
local -a subverbs mig_opts
|
|
371
|
+
subverbs=(
|
|
372
|
+
'status:Migration status'
|
|
373
|
+
'pending:List pending migrations'
|
|
374
|
+
'run:Run pending migrations (use --dry-run)'
|
|
375
|
+
)
|
|
376
|
+
|
|
377
|
+
mig_opts=(
|
|
378
|
+
'(-i --instance)'{-i,--instance}'[Registered instance name]:instance:_quilltap_instance_names'
|
|
196
379
|
'(-d --data-dir)'{-d,--data-dir}'[Data directory]:directory:_directories'
|
|
380
|
+
'--passphrase[Database passphrase]:passphrase:'
|
|
381
|
+
'--dry-run[Dry run]'
|
|
382
|
+
'--json[JSON output]'
|
|
383
|
+
'(-h --help)'{-h,--help}'[Show help]'
|
|
384
|
+
)
|
|
385
|
+
|
|
386
|
+
if (( CURRENT == 2 )); then
|
|
387
|
+
_describe 'migrations subcommand' subverbs
|
|
388
|
+
fi
|
|
389
|
+
_arguments $mig_opts
|
|
197
390
|
}
|
|
198
391
|
|
|
199
392
|
_quilltap_completion() {
|
package/lib/db-commands.js
CHANGED
|
@@ -561,6 +561,12 @@ function cmdLog(args, ctx) {
|
|
|
561
561
|
if (!row) throw new Error(`No llm_log with id ${id}`);
|
|
562
562
|
if (json) return printJson(row);
|
|
563
563
|
|
|
564
|
+
let finishReason = null;
|
|
565
|
+
try {
|
|
566
|
+
const parsed = typeof row.response === 'string' ? JSON.parse(row.response) : row.response;
|
|
567
|
+
if (parsed && typeof parsed.finishReason === 'string') finishReason = parsed.finishReason;
|
|
568
|
+
} catch { /* leave null */ }
|
|
569
|
+
|
|
564
570
|
printRecord(`LLM log ${row.id}`, {
|
|
565
571
|
createdAt: row.createdAt,
|
|
566
572
|
type: row.type,
|
|
@@ -570,6 +576,7 @@ function cmdLog(args, ctx) {
|
|
|
570
576
|
messageId: row.messageId,
|
|
571
577
|
characterId: row.characterId,
|
|
572
578
|
durationMs: row.durationMs,
|
|
579
|
+
finishReason,
|
|
573
580
|
usage: row.usage,
|
|
574
581
|
cacheUsage: row.cacheUsage,
|
|
575
582
|
});
|
|
@@ -636,6 +643,323 @@ function cmdMemories(args, ctx) {
|
|
|
636
643
|
}
|
|
637
644
|
}
|
|
638
645
|
|
|
646
|
+
// ---------- verb: characters ----------
|
|
647
|
+
|
|
648
|
+
// Vault files that the character-properties overlay manages today. Must stay
|
|
649
|
+
// in sync with `CHARACTER_VAULT_DESCRIPTORS` in
|
|
650
|
+
// lib/database/repositories/character-properties-overlay.ts. When the 4.6
|
|
651
|
+
// vault-cutover migration lands, every character is expected to have all of
|
|
652
|
+
// these present.
|
|
653
|
+
const EXPECTED_VAULT_SINGLE_FILES = [
|
|
654
|
+
'properties.json',
|
|
655
|
+
'identity.md',
|
|
656
|
+
'description.md',
|
|
657
|
+
'manifesto.md',
|
|
658
|
+
'personality.md',
|
|
659
|
+
'example-dialogues.md',
|
|
660
|
+
'physical-description.md',
|
|
661
|
+
'physical-prompts.json',
|
|
662
|
+
];
|
|
663
|
+
|
|
664
|
+
function safeJsonArray(raw) {
|
|
665
|
+
if (raw == null || raw === '') return [];
|
|
666
|
+
try {
|
|
667
|
+
const v = JSON.parse(raw);
|
|
668
|
+
return Array.isArray(v) ? v : [];
|
|
669
|
+
} catch {
|
|
670
|
+
return [];
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
function normalizeEmpty(v) {
|
|
675
|
+
if (v == null) return '';
|
|
676
|
+
return v;
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
function inspectCharacterVault(row, mounts) {
|
|
680
|
+
// `flag` / `*Db` / divergence reporting only make sense before the 4.6
|
|
681
|
+
// vault cutover, when the DB still carried the content columns. After
|
|
682
|
+
// the cutover the columns are gone and the vault is the only source of
|
|
683
|
+
// truth — `row` won't carry them. Treat them as null and skip the
|
|
684
|
+
// divergence check; the file-presence count is still useful.
|
|
685
|
+
const preCutover = row.identity !== undefined
|
|
686
|
+
|| row.description !== undefined
|
|
687
|
+
|| row.systemPrompts !== undefined;
|
|
688
|
+
|
|
689
|
+
const status = {
|
|
690
|
+
id: row.id,
|
|
691
|
+
name: row.name,
|
|
692
|
+
flag: row.readPropertiesFromDocumentStore == null
|
|
693
|
+
? null
|
|
694
|
+
: Number(row.readPropertiesFromDocumentStore),
|
|
695
|
+
mountPointId: row.characterDocumentMountPointId || null,
|
|
696
|
+
vault: 'missing',
|
|
697
|
+
presentSingleFiles: 0,
|
|
698
|
+
expectedSingleFiles: EXPECTED_VAULT_SINGLE_FILES.length,
|
|
699
|
+
missingSingleFiles: [],
|
|
700
|
+
promptsVault: 0,
|
|
701
|
+
promptsDb: 0,
|
|
702
|
+
scenariosVault: 0,
|
|
703
|
+
scenariosDb: 0,
|
|
704
|
+
wardrobeVault: 0,
|
|
705
|
+
diverged: [],
|
|
706
|
+
issue: null,
|
|
707
|
+
preCutover,
|
|
708
|
+
};
|
|
709
|
+
|
|
710
|
+
if (preCutover) {
|
|
711
|
+
status.promptsDb = safeJsonArray(row.systemPrompts).length;
|
|
712
|
+
status.scenariosDb = safeJsonArray(row.scenarios).length;
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
if (!row.characterDocumentMountPointId) {
|
|
716
|
+
status.issue = 'no vault';
|
|
717
|
+
return status;
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
status.vault = 'present';
|
|
721
|
+
const mountPointId = row.characterDocumentMountPointId;
|
|
722
|
+
|
|
723
|
+
// One-shot listing of every link for this vault; the rest is just lookups.
|
|
724
|
+
const links = mounts.prepare(
|
|
725
|
+
'SELECT relativePath, fileId FROM doc_mount_file_links WHERE mountPointId = ?'
|
|
726
|
+
).all(mountPointId);
|
|
727
|
+
const byPath = new Map();
|
|
728
|
+
for (const link of links) {
|
|
729
|
+
byPath.set(link.relativePath.toLowerCase(), link);
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
for (const p of EXPECTED_VAULT_SINGLE_FILES) {
|
|
733
|
+
if (byPath.has(p)) {
|
|
734
|
+
status.presentSingleFiles++;
|
|
735
|
+
} else {
|
|
736
|
+
status.missingSingleFiles.push(p);
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
for (const [p] of byPath) {
|
|
741
|
+
if (p.startsWith('prompts/') && p.endsWith('.md')) status.promptsVault++;
|
|
742
|
+
else if (p.startsWith('scenarios/') && p.endsWith('.md')) status.scenariosVault++;
|
|
743
|
+
else if (p.startsWith('wardrobe/') && p.endsWith('.md')) status.wardrobeVault++;
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
// Compare vault contents to DB row for each managed field where the
|
|
747
|
+
// corresponding file is actually present. Only meaningful pre-cutover;
|
|
748
|
+
// post-cutover the DB no longer carries the columns to compare against.
|
|
749
|
+
if (preCutover) {
|
|
750
|
+
const docStmt = mounts.prepare(
|
|
751
|
+
'SELECT content FROM doc_mount_documents WHERE fileId = ?'
|
|
752
|
+
);
|
|
753
|
+
const readVault = (relPath) => {
|
|
754
|
+
const link = byPath.get(relPath);
|
|
755
|
+
if (!link) return null;
|
|
756
|
+
const doc = docStmt.get(link.fileId);
|
|
757
|
+
return doc ? doc.content : null;
|
|
758
|
+
};
|
|
759
|
+
|
|
760
|
+
const mdFields = [
|
|
761
|
+
['identity.md', 'identity'],
|
|
762
|
+
['description.md', 'description'],
|
|
763
|
+
['manifesto.md', 'manifesto'],
|
|
764
|
+
['personality.md', 'personality'],
|
|
765
|
+
['example-dialogues.md', 'exampleDialogues'],
|
|
766
|
+
];
|
|
767
|
+
for (const [vaultPath, dbField] of mdFields) {
|
|
768
|
+
const vault = readVault(vaultPath);
|
|
769
|
+
if (vault === null) continue;
|
|
770
|
+
const db = row[dbField] ?? '';
|
|
771
|
+
if (vault !== db) status.diverged.push(dbField);
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
const propsRaw = readVault('properties.json');
|
|
775
|
+
if (propsRaw !== null) {
|
|
776
|
+
try {
|
|
777
|
+
const props = JSON.parse(propsRaw);
|
|
778
|
+
const scalarChecks = [
|
|
779
|
+
['pronouns', row.pronouns],
|
|
780
|
+
['title', row.title],
|
|
781
|
+
['firstMessage', row.firstMessage],
|
|
782
|
+
['talkativeness', row.talkativeness],
|
|
783
|
+
];
|
|
784
|
+
for (const [k, dbVal] of scalarChecks) {
|
|
785
|
+
if (normalizeEmpty(props[k]) !== normalizeEmpty(dbVal)) {
|
|
786
|
+
status.diverged.push(k);
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
const vaultAliases = JSON.stringify(Array.isArray(props.aliases) ? props.aliases : []);
|
|
790
|
+
const dbAliases = JSON.stringify(safeJsonArray(row.aliases));
|
|
791
|
+
if (vaultAliases !== dbAliases) status.diverged.push('aliases');
|
|
792
|
+
// systemTransparency: tristate (0 / 1 / null), only reported if vault has it
|
|
793
|
+
if (props.systemTransparency !== undefined) {
|
|
794
|
+
if ((props.systemTransparency ?? null) !== (row.systemTransparency ?? null)) {
|
|
795
|
+
status.diverged.push('systemTransparency');
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
} catch {
|
|
799
|
+
status.diverged.push('properties.json:unparseable');
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
const physArr = safeJsonArray(row.physicalDescriptions);
|
|
804
|
+
const primary = physArr[0] || null;
|
|
805
|
+
const physMd = readVault('physical-description.md');
|
|
806
|
+
if (physMd !== null) {
|
|
807
|
+
const dbVal = primary && primary.fullDescription != null ? primary.fullDescription : '';
|
|
808
|
+
if (physMd !== dbVal) status.diverged.push('physicalDescription.fullDescription');
|
|
809
|
+
}
|
|
810
|
+
const physJsonRaw = readVault('physical-prompts.json');
|
|
811
|
+
if (physJsonRaw !== null) {
|
|
812
|
+
try {
|
|
813
|
+
const physJson = JSON.parse(physJsonRaw);
|
|
814
|
+
const promptChecks = [
|
|
815
|
+
['short', primary?.shortPrompt],
|
|
816
|
+
['medium', primary?.mediumPrompt],
|
|
817
|
+
['long', primary?.longPrompt],
|
|
818
|
+
['complete', primary?.completePrompt],
|
|
819
|
+
];
|
|
820
|
+
for (const [k, dbVal] of promptChecks) {
|
|
821
|
+
if (normalizeEmpty(physJson[k]) !== normalizeEmpty(dbVal)) {
|
|
822
|
+
status.diverged.push(`physical.${k}Prompt`);
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
} catch {
|
|
826
|
+
status.diverged.push('physical-prompts.json:unparseable');
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
if (status.promptsVault !== status.promptsDb) {
|
|
831
|
+
status.diverged.push(`systemPrompts:count(vault=${status.promptsVault},db=${status.promptsDb})`);
|
|
832
|
+
}
|
|
833
|
+
if (status.scenariosVault !== status.scenariosDb) {
|
|
834
|
+
status.diverged.push(`scenarios:count(vault=${status.scenariosVault},db=${status.scenariosDb})`);
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
if (status.missingSingleFiles.length === EXPECTED_VAULT_SINGLE_FILES.length) {
|
|
839
|
+
status.issue = 'vault empty';
|
|
840
|
+
} else if (status.missingSingleFiles.length > 0) {
|
|
841
|
+
status.issue = `${status.missingSingleFiles.length} files missing`;
|
|
842
|
+
} else if (status.diverged.length > 0) {
|
|
843
|
+
status.issue = `diverged (${status.diverged.length})`;
|
|
844
|
+
} else if (!preCutover) {
|
|
845
|
+
status.issue = 'ok (post-cutover, vault is canonical)';
|
|
846
|
+
} else if (status.flag === 1) {
|
|
847
|
+
status.issue = 'ok (vault authoritative)';
|
|
848
|
+
} else {
|
|
849
|
+
status.issue = 'ok (db matches vault)';
|
|
850
|
+
}
|
|
851
|
+
return status;
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
function cmdCharacters(args, ctx) {
|
|
855
|
+
const { flags, positional } = parseSubArgs(args);
|
|
856
|
+
const sub = positional[0] || 'status';
|
|
857
|
+
if (sub !== 'status') {
|
|
858
|
+
throw new Error(`Unknown characters subcommand: ${sub}. Try: status`);
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
const json = asBool(flags.json);
|
|
862
|
+
const limit = asInt(flags.limit, 0);
|
|
863
|
+
const onlyDiverged = asBool(flags.diverged);
|
|
864
|
+
const onlyBlocked = asBool(flags.blocked);
|
|
865
|
+
const idQuery = flags.id ? String(flags.id) : null;
|
|
866
|
+
|
|
867
|
+
const main = ctx.openMain();
|
|
868
|
+
const mounts = ctx.openMounts();
|
|
869
|
+
try {
|
|
870
|
+
// Probe the schema so this verb works both pre- and post-cutover: after
|
|
871
|
+
// the 4.6 migration the content columns are gone, so we can only ask
|
|
872
|
+
// for what's there.
|
|
873
|
+
const existing = new Set(
|
|
874
|
+
main.prepare('PRAGMA table_info(characters)')
|
|
875
|
+
.all()
|
|
876
|
+
.map(r => r.name)
|
|
877
|
+
);
|
|
878
|
+
const wanted = [
|
|
879
|
+
'id', 'name', 'characterDocumentMountPointId', 'systemTransparency',
|
|
880
|
+
'readPropertiesFromDocumentStore',
|
|
881
|
+
'identity', 'description', 'manifesto', 'personality', 'exampleDialogues',
|
|
882
|
+
'pronouns', 'aliases', 'title', 'firstMessage', 'talkativeness',
|
|
883
|
+
'physicalDescriptions', 'systemPrompts', 'scenarios',
|
|
884
|
+
];
|
|
885
|
+
const cols = wanted.filter(c => existing.has(c));
|
|
886
|
+
let sql = `SELECT ${cols.join(', ')} FROM characters`;
|
|
887
|
+
const params = [];
|
|
888
|
+
if (idQuery) {
|
|
889
|
+
const c = resolveCharacter(main, idQuery);
|
|
890
|
+
sql += ' WHERE id = ?';
|
|
891
|
+
params.push(c.id);
|
|
892
|
+
} else {
|
|
893
|
+
sql += ' ORDER BY name';
|
|
894
|
+
if (limit > 0) {
|
|
895
|
+
sql += ' LIMIT ?';
|
|
896
|
+
params.push(limit);
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
const rows = main.prepare(sql).all(...params);
|
|
900
|
+
|
|
901
|
+
const all = rows.map(r => inspectCharacterVault(r, mounts));
|
|
902
|
+
const filtered = all.filter(s => {
|
|
903
|
+
if (onlyBlocked && !(s.issue && (s.issue === 'no vault' || s.issue === 'vault empty' || s.issue.endsWith(' files missing')))) {
|
|
904
|
+
return false;
|
|
905
|
+
}
|
|
906
|
+
if (onlyDiverged && s.diverged.length === 0 && (!s.missingSingleFiles || s.missingSingleFiles.length === 0)) {
|
|
907
|
+
return false;
|
|
908
|
+
}
|
|
909
|
+
return true;
|
|
910
|
+
});
|
|
911
|
+
|
|
912
|
+
if (json) {
|
|
913
|
+
const summary = {
|
|
914
|
+
totalScanned: all.length,
|
|
915
|
+
returned: filtered.length,
|
|
916
|
+
counts: summarizeCharacterStatuses(all),
|
|
917
|
+
characters: filtered,
|
|
918
|
+
};
|
|
919
|
+
return printJson(summary);
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
const summary = summarizeCharacterStatuses(all);
|
|
923
|
+
const headline = `Scanned ${all.length} character${all.length === 1 ? '' : 's'}: ` +
|
|
924
|
+
`${summary.ok} ok, ${summary.diverged} diverged, ${summary.missingFiles} with missing files, ` +
|
|
925
|
+
`${summary.noVault} with no vault, ${summary.empty} empty.`;
|
|
926
|
+
console.log(headline);
|
|
927
|
+
console.log('');
|
|
928
|
+
printTable(filtered.map(s => ({
|
|
929
|
+
id: s.id.slice(0, 8),
|
|
930
|
+
name: truncate(s.name, 28),
|
|
931
|
+
flag: s.flag == null ? '-' : s.flag,
|
|
932
|
+
vault: s.vault,
|
|
933
|
+
files: s.vault === 'missing' ? '-' : `${s.presentSingleFiles}/${s.expectedSingleFiles}`,
|
|
934
|
+
prompts: s.vault === 'missing' ? '-' : `${s.promptsVault}/${s.promptsDb}`,
|
|
935
|
+
scenarios: s.vault === 'missing' ? '-' : `${s.scenariosVault}/${s.scenariosDb}`,
|
|
936
|
+
wardrobe: s.vault === 'missing' ? '-' : s.wardrobeVault,
|
|
937
|
+
status: truncate(s.issue, 60),
|
|
938
|
+
})));
|
|
939
|
+
|
|
940
|
+
if (filtered.length > 0 && filtered.some(s => s.diverged.length > 0)) {
|
|
941
|
+
console.log('');
|
|
942
|
+
console.log('Run with --json to see the full diverged-field list per character.');
|
|
943
|
+
}
|
|
944
|
+
} finally {
|
|
945
|
+
try { mounts.close(); } catch {}
|
|
946
|
+
try { main.close(); } catch {}
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
function summarizeCharacterStatuses(all) {
|
|
951
|
+
let ok = 0, diverged = 0, missingFiles = 0, noVault = 0, empty = 0;
|
|
952
|
+
for (const s of all) {
|
|
953
|
+
if (!s.issue) continue;
|
|
954
|
+
if (s.issue.startsWith('ok')) ok++;
|
|
955
|
+
else if (s.issue === 'no vault') noVault++;
|
|
956
|
+
else if (s.issue === 'vault empty') empty++;
|
|
957
|
+
else if (s.issue.endsWith(' files missing')) missingFiles++;
|
|
958
|
+
else if (s.issue.startsWith('diverged')) diverged++;
|
|
959
|
+
}
|
|
960
|
+
return { ok, diverged, missingFiles, noVault, empty };
|
|
961
|
+
}
|
|
962
|
+
|
|
639
963
|
// ---------- verb: optimize ----------
|
|
640
964
|
|
|
641
965
|
const OPTIMIZE_TARGETS = {
|
|
@@ -1105,6 +1429,7 @@ const VERBS = {
|
|
|
1105
1429
|
message: cmdMessage,
|
|
1106
1430
|
log: cmdLog,
|
|
1107
1431
|
memories: cmdMemories,
|
|
1432
|
+
characters: cmdCharacters,
|
|
1108
1433
|
optimize: cmdOptimize,
|
|
1109
1434
|
backup: cmdBackup,
|
|
1110
1435
|
integrity: cmdIntegrity,
|