quilltap 4.7.0-dev.117 → 4.7.0-dev.129

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -89,6 +89,20 @@ Quilltap stores its database, files, and logs in a platform-specific directory:
89
89
 
90
90
  Override with `--data-dir` or the `QUILLTAP_DATA_DIR` environment variable.
91
91
 
92
+ ## Named Instances
93
+
94
+ Register a data directory once and point the CLI at it by name, instead of repeating `--data-dir`:
95
+
96
+ ```bash
97
+ quilltap instances add Friday ~/iCloud/Quilltap/Friday # Register (prompts for the passphrase if encrypted)
98
+ quilltap instances list # Registered instances (* marks the default)
99
+ quilltap instances default Friday # Make it the fall-through for flag-free runs
100
+ quilltap instances rename Friday Weekday # Rename, preserving the stored passphrase
101
+ quilltap instances remove Friday # Unregister
102
+ ```
103
+
104
+ Every subcommand then accepts `--instance <name>` in place of `--data-dir`. The registry lives at `<app-support>/Quilltap/instances.json` (mode 0600; e.g. `~/Library/Application Support/Quilltap/instances.json` on macOS). **Resolution precedence:** `--data-dir` > `--instance` > registered default > `QUILLTAP_DATA_DIR` > the OS platform default. Pass the **instance root** (e.g. `~/iCloud/Quilltap/Friday`), not its `data/` subdirectory.
105
+
92
106
  ## Database Tool
93
107
 
94
108
  The encrypted SQLite databases (main, LLM logs, mount index) can be queried directly via `quilltap db`. There are two modes: high-level subcommands that auto-pick the right database and resolve characters/chats/projects by name, and a low-level path for arbitrary SQL.
@@ -216,6 +230,17 @@ quilltap memories status [--character <name|id>] # Per-hol
216
230
 
217
231
  Shared filter flags apply to `ls`, `find`, `grep`, and `status` where they make sense: `--character`, `--about` (with `self` / `none` shortcuts), `--source`, `--chat` (with `none` for manual entries), `--project`, `--since`, `--until`, `--min-importance`, `--min-reinforced`, `--has-embedding` / `--no-embedding`. Sort flags (`--sort reinforced|importance|created|accessed|reinforcement-count|links`, plus `-r` to reverse) apply to `ls`, `find`, and `grep`. Names accept fuzzy substrings; ambiguous names print candidates and exit 2. `--json` is supported by every verb. The legacy `quilltap db memories --character <name>` verb remains undisturbed.
218
232
 
233
+ ## Memory Extraction Dry-Run
234
+
235
+ `quilltap memory-diff <chatId>` is a diagnostic that dumps a chat's existing memories and dry-runs re-extraction against it **without writing anything** — useful for seeing how an extractor change would land on a real conversation.
236
+
237
+ ```bash
238
+ quilltap memory-diff <chatId> --instance Friday # Report to the current directory
239
+ quilltap memory-diff <chatId> --out /tmp/diff --concurrency 8
240
+ ```
241
+
242
+ Needs a running server (`--port`, default 3000) to reach the extraction pipeline. `--out <dir>` sets the report destination (default: cwd); `--concurrency N` bounds parallel turns (default 4, max 32).
243
+
219
244
  ## Maintenance & Cleanup
220
245
 
221
246
  `quilltap maintenance` is the manual trigger for the retention sweeps that otherwise run on the server's daily maintenance tick. It reaps data with no bearing on characters, stories, or memories.
@@ -228,6 +253,31 @@ quilltap maintenance run --instance Friday # Run the sweeps once (lock-gated;
228
253
 
229
254
  `maintenance run` is a DB writer: it claims `<dataDir>/quilltap.lock` and **refuses while a running Quilltap server holds it** — stop the server first. Because it can only run with the server down, it performs the sweeps expressible as direct SQL/filesystem work: reaping finished background jobs (COMPLETED after 7 days, DEAD after 30, keyed off `completedAt`), closed terminal sessions older than 30 days plus their transcript files, and orphaned mount-index files. The **stale-chat asset collapse** (superseded story-backgrounds and wardrobe avatars) needs the server's file-storage machinery and runs only on the server's daily tick — `status` reports a stale-chat count so you can see the backlog. Retention windows mirror `lib/background-jobs/maintenance/retention-constants.ts`.
230
255
 
256
+ ## Logs
257
+
258
+ `quilltap logs` tails or prints an instance's log files without hunting down the logs directory. Prefer it over `tail -f` — it follows across log rotation and can merge streams.
259
+
260
+ ```bash
261
+ quilltap logs # Last 100 lines of combined.log
262
+ quilltap logs --tail 0 --stream error # Whole error log
263
+ quilltap logs -f --grep "MEMORY_EXTRACTION" # Follow, filtered by regex
264
+ quilltap logs --stream combined,error # Merge streams with [stream] prefixes
265
+ ```
266
+
267
+ Flags: `--stream combined|error|stdout|stderr|startup` (comma-separated for multiple), `--tail N` (default 100; `0` = full file), `--follow` / `-f`, `--grep <pattern>` (JS regex). Resolves the logs directory via the same `--instance` / `--data-dir` plumbing as the rest of the CLI.
268
+
269
+ ## Migrations
270
+
271
+ `quilltap migrations` inspects migration state read-only. The actual runner stays at server startup (where the loading screen reports progress), so the CLI deliberately won't apply anything.
272
+
273
+ ```bash
274
+ quilltap migrations status # Counts: in-source vs recorded-applied vs not-yet-recorded
275
+ quilltap migrations pending # Just the not-yet-recorded list
276
+ quilltap migrations run --dry-run # List what would run (refuses without --dry-run)
277
+ ```
278
+
279
+ `--json` works on all three. "Not yet recorded" includes migrations whose `shouldRun()` is `false` on this instance — the CLI doesn't evaluate the predicate, so it can't distinguish "would skip" from "would run."
280
+
231
281
  ## Theme Management
232
282
 
233
283
  The CLI includes theme management commands:
package/bin/quilltap.js CHANGED
@@ -94,7 +94,12 @@ Subcommands:
94
94
  docs Inspect, read, and export document mounts
95
95
  memories Search, browse, and graph memories
96
96
  instances Register / inspect named Quilltap instances
97
+ logs Tail or print an instance log file
98
+ migrations Inspect migration status (status / pending / run)
99
+ maintenance Run retention / cleanup sweeps (status / run)
100
+ file-verify Force-download cloud-evicted data files (iCloud, etc.)
97
101
  memory-diff <chatId> Dump existing memories and dry-run re-extraction for a chat
102
+ completion <shell> Generate a shell completion script (bash / zsh / fish)
98
103
 
99
104
  Options:
100
105
  -p, --port <number> Port to listen on (default: 3000)
@@ -1103,7 +1108,7 @@ async function dbCommand(args) {
1103
1108
  // to the subcommand. Each subcommand parses these flags position-independently,
1104
1109
  // so they behave the same before or after the verb.
1105
1110
  const SUBCOMMANDS = new Set([
1106
- 'db', 'themes', 'docs', 'memories', 'instances', 'memory-diff', 'completion', 'logs', 'migrations', 'maintenance',
1111
+ 'db', 'themes', 'docs', 'memories', 'instances', 'memory-diff', 'completion', 'logs', 'migrations', 'maintenance', 'file-verify',
1107
1112
  ]);
1108
1113
  // Global flags that consume the following token as their value.
1109
1114
  const GLOBAL_VALUE_FLAGS = new Set(['-p', '--port', '-d', '--data-dir', '-i', '--instance', '--passphrase']);
@@ -1128,7 +1133,9 @@ const subArgs = subIdx >= 0 ? [...cliArgs.slice(0, subIdx), ...cliArgs.slice(sub
1128
1133
  // native-module heal, so self-heal the database ABI here first. Cheap no-op when
1129
1134
  // healthy; rebuilds (with a friendly notice, not an error) only on a real
1130
1135
  // Node-ABI mismatch — e.g. after the user upgrades Node under a cached install.
1131
- if (subName) {
1136
+ // `file-verify` is excluded: it's a pure-fs recovery tool that never opens the
1137
+ // database, so it must work even when the native binding is broken.
1138
+ if (subName && subName !== 'file-verify') {
1132
1139
  ensureDatabaseNativeModule();
1133
1140
  }
1134
1141
 
@@ -1185,6 +1192,12 @@ if (subName === 'db') {
1185
1192
  console.error(`Error: ${err.message}`);
1186
1193
  process.exit(1);
1187
1194
  });
1195
+ } else if (subName === 'file-verify') {
1196
+ const { fileVerifyCommand } = require('../lib/file-verify-commands');
1197
+ fileVerifyCommand(subArgs).catch(err => {
1198
+ console.error(`Error: ${err.message}`);
1199
+ process.exit(1);
1200
+ });
1188
1201
  } else {
1189
1202
  main();
1190
1203
  }
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Guard test: the shell-completion templates and the top-level `--help` must
3
+ * stay in sync with the real subcommand dispatch table in bin/quilltap.js.
4
+ *
5
+ * This is the check that was missing when the docs/completions drifted behind
6
+ * the CLI (e.g. the `maintenance` subcommand shipped without ever being added
7
+ * to any completion script or to `quilltap --help`). If you add a top-level
8
+ * subcommand to SUBCOMMANDS, this test fails until you also teach the three
9
+ * completion templates and printHelp() about it.
10
+ *
11
+ * @jest-environment node
12
+ */
13
+
14
+ 'use strict';
15
+
16
+ const fs = require('fs');
17
+ const path = require('path');
18
+
19
+ const BIN = path.join(__dirname, '..', '..', 'bin', 'quilltap.js');
20
+ const COMPLETION_DIR = path.join(__dirname, '..', 'completion');
21
+
22
+ function readBin() {
23
+ return fs.readFileSync(BIN, 'utf8');
24
+ }
25
+
26
+ /** Parse the authoritative `const SUBCOMMANDS = new Set([...])` literal. */
27
+ function readSubcommands(src) {
28
+ const m = src.match(/const SUBCOMMANDS = new Set\(\[([\s\S]*?)\]\)/);
29
+ if (!m) throw new Error('Could not locate SUBCOMMANDS set in bin/quilltap.js');
30
+ return m[1]
31
+ .split(',')
32
+ .map((s) => s.trim().replace(/^['"]|['"]$/g, ''))
33
+ .filter(Boolean);
34
+ }
35
+
36
+ const SRC = readBin();
37
+ const SUBCOMMANDS = readSubcommands(SRC);
38
+
39
+ describe('CLI subcommand surface stays documented', () => {
40
+ it('parses a non-trivial subcommand set from bin/quilltap.js', () => {
41
+ expect(SUBCOMMANDS).toContain('db');
42
+ expect(SUBCOMMANDS).toContain('maintenance');
43
+ expect(SUBCOMMANDS.length).toBeGreaterThanOrEqual(10);
44
+ });
45
+
46
+ it.each(['bash', 'zsh', 'fish'])(
47
+ '%s completion template lists every top-level subcommand',
48
+ (shell) => {
49
+ const tpl = fs.readFileSync(path.join(COMPLETION_DIR, `${shell}.template`), 'utf8');
50
+ const missing = SUBCOMMANDS.filter((sub) => !tpl.includes(sub));
51
+ expect(missing).toEqual([]);
52
+ }
53
+ );
54
+
55
+ it('top-level --help lists every subcommand', () => {
56
+ const help = SRC.match(/function printHelp\(\) \{([\s\S]*?)\n\}/);
57
+ expect(help).toBeTruthy();
58
+ const missing = SUBCOMMANDS.filter((sub) => !help[1].includes(sub));
59
+ expect(missing).toEqual([]);
60
+ });
61
+ });
@@ -15,7 +15,7 @@ _quilltap_complete() {
15
15
  local global_opts="-d --data-dir -i --instance -p --port -o --open -v --version -h --help --update --passphrase"
16
16
 
17
17
  # Top-level subcommands
18
- local top_cmds="db docs themes instances memories memory-diff logs migrations completion"
18
+ local top_cmds="db docs themes instances memories memory-diff logs migrations maintenance file-verify completion"
19
19
 
20
20
  # Get the subcommand (first non-option word after quilltap)
21
21
  local subcommand=""
@@ -126,7 +126,7 @@ _quilltap_complete() {
126
126
  fi
127
127
  ;;
128
128
  docs)
129
- local docs_verbs="list show files ls dir tree 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 link rmdir mvdir 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"
@@ -227,6 +227,19 @@ _quilltap_complete() {
227
227
  COMPREPLY=($(compgen -W "$mig_flags" -- "$cur"))
228
228
  fi
229
229
  ;;
230
+ maintenance)
231
+ local maint_verbs="status run"
232
+ local maint_flags="--instance --data-dir --passphrase --json --help"
233
+ if [[ -z "$subverb" ]]; then
234
+ if [[ "$cur" == -* ]]; then
235
+ COMPREPLY=($(compgen -W "$maint_flags" -- "$cur"))
236
+ else
237
+ COMPREPLY=($(compgen -W "$maint_verbs" -- "$cur"))
238
+ fi
239
+ else
240
+ COMPREPLY=($(compgen -W "$maint_flags" -- "$cur"))
241
+ fi
242
+ ;;
230
243
  completion)
231
244
  if [[ "$cur" == -* ]]; then
232
245
  COMPREPLY=($(compgen -W "--help" -- "$cur"))
@@ -15,7 +15,7 @@ function __quilltap_no_subcommand
15
15
  end
16
16
  for i in (seq 2 (count $cmd))
17
17
  switch $cmd[$i]
18
- case db docs themes instances memories memory-diff logs migrations completion
18
+ case db docs themes instances memories memory-diff logs migrations maintenance file-verify completion
19
19
  return 1
20
20
  end
21
21
  end
@@ -27,7 +27,7 @@ function __quilltap_using_subcommand
27
27
  set -l cmd (commandline -opc)
28
28
  for i in (seq 2 (count $cmd))
29
29
  switch $cmd[$i]
30
- case db docs themes instances memories memory-diff logs migrations completion
30
+ case db docs themes instances memories memory-diff logs migrations maintenance file-verify completion
31
31
  test "$cmd[$i]" = "$target"
32
32
  return $status
33
33
  end
@@ -57,6 +57,8 @@ complete -c quilltap -n '__quilltap_no_subcommand' -f -a 'memories' -d 'Search a
57
57
  complete -c quilltap -n '__quilltap_no_subcommand' -f -a 'memory-diff' -d 'Memory extraction dry-run'
58
58
  complete -c quilltap -n '__quilltap_no_subcommand' -f -a 'logs' -d 'Tail or print log files'
59
59
  complete -c quilltap -n '__quilltap_no_subcommand' -f -a 'migrations' -d 'Inspect migration status'
60
+ complete -c quilltap -n '__quilltap_no_subcommand' -f -a 'maintenance' -d 'Run retention/cleanup sweeps'
61
+ complete -c quilltap -n '__quilltap_no_subcommand' -f -a 'file-verify' -d 'Force-download cloud-evicted data files'
60
62
  complete -c quilltap -n '__quilltap_no_subcommand' -f -a 'completion' -d 'Generate shell completion'
61
63
 
62
64
  # Global options
@@ -140,6 +142,9 @@ complete -c quilltap -n '__quilltap_using_subcommand docs' -f -a 'delete' -d 'De
140
142
  complete -c quilltap -n '__quilltap_using_subcommand docs' -f -a 'mkdir' -d 'Create folder'
141
143
  complete -c quilltap -n '__quilltap_using_subcommand docs' -f -a 'move' -d 'Move file'
142
144
  complete -c quilltap -n '__quilltap_using_subcommand docs' -f -a 'copy' -d 'Copy file'
145
+ complete -c quilltap -n '__quilltap_using_subcommand docs' -f -a 'link' -d 'Hard-link a file into another mount'
146
+ complete -c quilltap -n '__quilltap_using_subcommand docs' -f -a 'rmdir' -d 'Remove a folder'
147
+ complete -c quilltap -n '__quilltap_using_subcommand docs' -f -a 'mvdir' -d 'Rename or move a folder'
143
148
 
144
149
  # docs flags
145
150
  complete -c quilltap -n '__quilltap_using_subcommand docs' -l 'mount' -d 'Mount name or id' -x
@@ -268,6 +273,10 @@ complete -c quilltap -n '__quilltap_using_subcommand migrations' -f -a 'pending'
268
273
  complete -c quilltap -n '__quilltap_using_subcommand migrations' -f -a 'run' -d 'Run pending migrations'
269
274
  complete -c quilltap -n '__quilltap_using_subcommand migrations' -l 'dry-run' -d 'Dry run (required for run)'
270
275
 
276
+ # ---------- maintenance verbs ----------
277
+ complete -c quilltap -n '__quilltap_using_subcommand maintenance' -f -a 'status' -d 'Show dry-run retention counts'
278
+ complete -c quilltap -n '__quilltap_using_subcommand maintenance' -f -a 'run' -d 'Run retention/cleanup sweeps (lock-gated)'
279
+
271
280
  # ---------- completion sub-args ----------
272
281
  complete -c quilltap -n '__quilltap_using_subcommand completion' -f -a 'bash' -d 'Bash completion'
273
282
  complete -c quilltap -n '__quilltap_using_subcommand completion' -f -a 'zsh' -d 'Zsh completion'
@@ -28,6 +28,8 @@ _quilltap() {
28
28
  'memory-diff:Dump existing memories and dry-run re-extraction'
29
29
  'logs:Tail or print an instance log file'
30
30
  'migrations:Inspect migration status'
31
+ 'maintenance:Run retention/cleanup sweeps'
32
+ 'file-verify:Force-download cloud-evicted data files'
31
33
  'completion:Generate shell completion scripts'
32
34
  )
33
35
 
@@ -76,6 +78,9 @@ _quilltap_subcommand() {
76
78
  migrations)
77
79
  _quilltap_migrations
78
80
  ;;
81
+ maintenance)
82
+ _quilltap_maintenance
83
+ ;;
79
84
  completion)
80
85
  _quilltap_completion
81
86
  ;;
@@ -163,6 +168,9 @@ _quilltap_docs() {
163
168
  'mkdir:Create folder'
164
169
  'move:Move file'
165
170
  'copy:Copy file'
171
+ 'link:Hard-link a file into another mount'
172
+ 'rmdir:Remove a folder'
173
+ 'mvdir:Rename or move a folder'
166
174
  )
167
175
 
168
176
  docs_opts=(
@@ -395,6 +403,27 @@ _quilltap_migrations() {
395
403
  _arguments $mig_opts
396
404
  }
397
405
 
406
+ _quilltap_maintenance() {
407
+ local -a subverbs maint_opts
408
+ subverbs=(
409
+ 'status:Show dry-run retention counts'
410
+ 'run:Run retention/cleanup sweeps (lock-gated)'
411
+ )
412
+
413
+ maint_opts=(
414
+ '(-i --instance)'{-i,--instance}'[Registered instance name]:instance:_quilltap_instance_names'
415
+ '(-d --data-dir)'{-d,--data-dir}'[Data directory]:directory:_directories'
416
+ '--passphrase[Database passphrase]:passphrase:'
417
+ '--json[JSON output]'
418
+ '(-h --help)'{-h,--help}'[Show help]'
419
+ )
420
+
421
+ if (( CURRENT == 2 )); then
422
+ _describe 'maintenance subcommand' subverbs
423
+ fi
424
+ _arguments $maint_opts
425
+ }
426
+
398
427
  _quilltap_completion() {
399
428
  _values 'shell' bash zsh fish
400
429
  }
@@ -0,0 +1,222 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * `quilltap file-verify` — force-download an instance's cloud-evicted data files.
5
+ *
6
+ * An instance can live in a cloud-synced folder (iCloud Drive on macOS today;
7
+ * OneDrive / Google Drive File Stream on Windows later). Those providers EVICT
8
+ * idle files to dataless placeholders. If a database file is still dataless when
9
+ * SQLite/SQLCipher opens it, the read fails with "file is not a database" or
10
+ * returns partially-materialized garbage — which has wedged whole startups.
11
+ *
12
+ * The server does this automatically at boot (Phase -1, before the .dbkey is
13
+ * read). This command is the manual/diagnostic twin for the CLI, which opens the
14
+ * encrypted databases directly and benefits from the same guarantee. It needs no
15
+ * passphrase and never decrypts anything — it just reads the bytes to nowhere,
16
+ * which is what faults a dataless file in.
17
+ *
18
+ * Logic MIRRORS the TypeScript source of truth at
19
+ * `lib/startup/materialize-cloud-files.ts`. Keep them in sync.
20
+ */
21
+
22
+ const fs = require('fs');
23
+ const path = require('path');
24
+ const { resolveDataDirAndPassphrase, printDefaultInstanceHint } = require('./db-helpers');
25
+
26
+ const DEFAULT_STALL_MS = 30_000;
27
+ const READ_CHUNK_BYTES = 8 * 1024 * 1024;
28
+
29
+ // ---------- detection (platform seam) ----------
30
+
31
+ // macOS dataless heuristic: a real file (size > 0) with no allocated blocks has
32
+ // not been materialized locally (mirrors the SF_DATALESS flag). Zero-byte files
33
+ // are never flagged. Other platforms fall through as a no-op for now.
34
+ function isDatalessStat(stat) {
35
+ return stat.size > 0 && stat.blocks === 0;
36
+ }
37
+
38
+ function listTopLevelFiles(dataDir) {
39
+ let entries;
40
+ try {
41
+ entries = fs.readdirSync(dataDir, { withFileTypes: true });
42
+ } catch {
43
+ return [];
44
+ }
45
+ const out = [];
46
+ for (const entry of entries) {
47
+ if (!entry.isFile()) continue; // skip directories (backups/) and symlinks
48
+ const full = path.join(dataDir, entry.name);
49
+ let stat;
50
+ try {
51
+ stat = fs.statSync(full);
52
+ } catch {
53
+ continue;
54
+ }
55
+ out.push({ name: entry.name, full, size: stat.size, dataless: isDatalessStat(stat) });
56
+ }
57
+ return out;
58
+ }
59
+
60
+ // ---------- streaming materialize with per-chunk stall guard ----------
61
+
62
+ function streamMaterialize(filePath, stallMs) {
63
+ return new Promise((resolve, reject) => {
64
+ const stream = fs.createReadStream(filePath, { highWaterMark: READ_CHUNK_BYTES });
65
+ let timer;
66
+ const arm = () => {
67
+ clearTimeout(timer);
68
+ timer = setTimeout(() => {
69
+ stream.destroy(new Error(`stalled — no data for ${Math.round(stallMs / 1000)}s`));
70
+ }, stallMs);
71
+ };
72
+ arm();
73
+ stream.on('data', () => arm()); // discard chunk; the read faults the bytes in
74
+ stream.on('end', () => {
75
+ clearTimeout(timer);
76
+ resolve();
77
+ });
78
+ stream.on('error', (err) => {
79
+ clearTimeout(timer);
80
+ reject(err);
81
+ });
82
+ });
83
+ }
84
+
85
+ // ---------- argument parsing ----------
86
+
87
+ function parseFlags(args) {
88
+ const flags = {
89
+ dataDir: '',
90
+ instance: '',
91
+ all: false,
92
+ json: false,
93
+ help: false,
94
+ stallMs: DEFAULT_STALL_MS,
95
+ };
96
+ let i = 0;
97
+ while (i < args.length) {
98
+ const a = args[i];
99
+ switch (a) {
100
+ case '-d': case '--data-dir': flags.dataDir = args[++i]; break;
101
+ case '-i': case '--instance': flags.instance = args[++i]; break;
102
+ case '--all': flags.all = true; break;
103
+ case '--json': flags.json = true; break;
104
+ case '--stall-ms': flags.stallMs = Number(args[++i]) || DEFAULT_STALL_MS; break;
105
+ case '-h': case '--help': flags.help = true; break;
106
+ default:
107
+ if (a.startsWith('-')) console.error(`unknown flag: ${a}`);
108
+ break;
109
+ }
110
+ i++;
111
+ }
112
+ return flags;
113
+ }
114
+
115
+ function printHelp() {
116
+ console.log(`Usage: quilltap file-verify [options]
117
+
118
+ Force-download the instance's cloud-evicted (dataless) data files so the
119
+ databases are fully local before anything opens them. Reads each placeholder to
120
+ nowhere, which faults it in through the cloud provider (iCloud Drive, etc.).
121
+ Safe to run repeatedly; a no-op when nothing is evicted. macOS only for now.
122
+
123
+ Only the TOP-LEVEL files of the data directory are considered (the backups/
124
+ subdirectory is left alone).
125
+
126
+ Options:
127
+ -d, --data-dir <path> Use a specific data directory (instance root)
128
+ -i, --instance <name> Use a named instance
129
+ --all Read every top-level file, not just dataless ones
130
+ --stall-ms <ms> Treat a download as stalled after this many ms with
131
+ no bytes (per-chunk, not per-file; default ${DEFAULT_STALL_MS})
132
+ --json Output as JSON
133
+ -h, --help Show this help message
134
+
135
+ Examples:
136
+ quilltap file-verify --instance Ignite
137
+ quilltap file-verify --instance Friday --json
138
+ quilltap file-verify -d ~/iCloud/Quilltap/Ignite
139
+ `);
140
+ }
141
+
142
+ // ---------- main entry point ----------
143
+
144
+ async function fileVerifyCommand(args) {
145
+ const flags = parseFlags(args);
146
+ if (flags.help) {
147
+ printHelp();
148
+ return;
149
+ }
150
+
151
+ let resolved;
152
+ try {
153
+ resolved = resolveDataDirAndPassphrase({ dataDir: flags.dataDir, instance: flags.instance });
154
+ } catch (err) {
155
+ console.error(`Error: ${err.message}`);
156
+ process.exit(1);
157
+ }
158
+ printDefaultInstanceHint(resolved);
159
+ // resolved.dataDir already points at the instance's data/ directory.
160
+ const targetDir = resolved.dataDir;
161
+
162
+ const files = listTopLevelFiles(targetDir);
163
+ const targets = flags.all ? files : files.filter((f) => f.dataless);
164
+
165
+ const summary = {
166
+ dataDir: targetDir,
167
+ checked: files.length,
168
+ targeted: targets.length,
169
+ downloaded: 0,
170
+ failed: 0,
171
+ failedNames: [],
172
+ };
173
+
174
+ if (targets.length === 0) {
175
+ if (flags.json) {
176
+ console.log(JSON.stringify(summary, null, 2));
177
+ } else {
178
+ console.log(
179
+ `Nothing to fetch — all ${files.length} top-level file(s) in ${targetDir} are already ashore.`,
180
+ );
181
+ }
182
+ return;
183
+ }
184
+
185
+ if (!flags.json) {
186
+ console.log(
187
+ `Fetching ${targets.length} file(s) down from the cloud (of ${files.length} top-level):`,
188
+ );
189
+ }
190
+
191
+ for (let i = 0; i < targets.length; i++) {
192
+ const file = targets[i];
193
+ const mb = (file.size / (1024 * 1024)).toFixed(1);
194
+ if (!flags.json) {
195
+ process.stdout.write(` Coaxing «${file.name}» (${mb} MB) down — ${i + 1}/${targets.length}… `);
196
+ }
197
+ try {
198
+ await streamMaterialize(file.full, flags.stallMs);
199
+ summary.downloaded++;
200
+ if (!flags.json) console.log('secured.');
201
+ } catch (err) {
202
+ summary.failed++;
203
+ summary.failedNames.push(file.name);
204
+ if (!flags.json) console.log(`failed (${err.message}).`);
205
+ }
206
+ }
207
+
208
+ if (flags.json) {
209
+ console.log(JSON.stringify(summary, null, 2));
210
+ } else {
211
+ console.log(
212
+ `Done — ${summary.downloaded} secured, ${summary.failed} failed.` +
213
+ (summary.failed ? ` Failed: ${summary.failedNames.join(', ')}` : ''),
214
+ );
215
+ }
216
+
217
+ if (summary.failed > 0) process.exit(1);
218
+ }
219
+
220
+ module.exports = {
221
+ fileVerifyCommand,
222
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "quilltap",
3
- "version": "4.7.0-dev.117",
3
+ "version": "4.7.0-dev.129",
4
4
  "description": "Self-hosted AI workspace for writers, worldbuilders, and roleplayers. Run with npx quilltap.",
5
5
  "author": {
6
6
  "name": "Charles Sebold",