quilltap 4.4.0 → 4.5.0-dev.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 CHANGED
@@ -89,6 +89,109 @@ 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
+ ## Database Tool
93
+
94
+ 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.
95
+
96
+ ### Subcommands
97
+
98
+ ```bash
99
+ quilltap db schema # Tables grouped by domain
100
+ quilltap db schema chat_messages # Columns, indexes, DDL.md link
101
+ quilltap db schema --grep memory # Find tables/columns by substring
102
+
103
+ quilltap db find character Friday # Resolve a name to a UUID (fuzzy)
104
+ quilltap db find chat "physical prompts"
105
+ quilltap db find project "Quilltap"
106
+
107
+ quilltap db chats --character Friday # All chats containing a character
108
+ quilltap db chats --project "Quilltap" # All chats in a project
109
+ quilltap db messages --chat <id|title> --last 50 --full
110
+ quilltap db logs --chat <id|title> # LLM logs for a chat
111
+ quilltap db logs --message <id> # LLM logs for a single message
112
+ quilltap db logs --character Friday # LLM logs by character
113
+ quilltap db logs --tail 20 # Recent LLM logs
114
+
115
+ quilltap db message <id> # Full content of one message
116
+ quilltap db log <id> [--field request|response|both]
117
+ quilltap db memories --character Friday [--about Amy] [--source AUTO]
118
+ ```
119
+
120
+ ### Maintenance and Snapshots
121
+
122
+ ```bash
123
+ quilltap db optimize # VACUUM + ANALYZE + PRAGMA optimize (all DBs)
124
+ quilltap db optimize main # one DB; refuses while server is running
125
+
126
+ quilltap db backup # online snapshot of all three DBs
127
+ quilltap db backup main --out /tmp/snap # one DB to a chosen directory
128
+ quilltap db backup --json # parseable per-target sizes + durations
129
+
130
+ quilltap db integrity # cipher_integrity_check + integrity_check
131
+ quilltap db integrity llm-logs # one DB; exit 0 ok, 1 issues, 2 open failure
132
+ ```
133
+
134
+ `backup` and `integrity` are safe to run while the server is up; `optimize` refuses while a live lock is held. Backups default to `<dataDir>/backups/<timestamp>/` and inherit the source's encryption key transparently.
135
+
136
+ Most subcommands accept `--json` (for piping) and `--limit N`. Names are case-insensitive; aliases are searched alongside character names. Ambiguous matches print all candidates and exit non-zero.
137
+
138
+ ### Low-level options
139
+
140
+ ```bash
141
+ quilltap db --tables # List tables in active DB
142
+ quilltap db --count chat_messages # Row count
143
+ quilltap db "SELECT id FROM characters LIMIT 5" # Raw SQL
144
+ quilltap db --repl # Interactive prompt
145
+ quilltap db --llm-logs --tables # Target the LLM logs DB
146
+ quilltap db --mount-points --tables # Target the mount index DB
147
+ ```
148
+
149
+ In the REPL, `.cols <table>` and `.find <text>` mirror the subcommand helpers.
150
+
151
+ ## Document Stores (Scriptorium)
152
+
153
+ `quilltap docs` exposes the document-store machinery from the command line. Read-only verbs open the mount-index DB directly and work without the server; write and pipeline verbs talk to the running server via `/api/v1/mount-points/[id]`.
154
+
155
+ ```bash
156
+ # Read
157
+ quilltap docs list # All mounts
158
+ quilltap docs show <mount> # One mount, with counts
159
+ quilltap docs ls <mount> [path] [--links] # POSIX-flavoured listing (alias: dir)
160
+ quilltap docs read [--rendered] <mount> <path> # File contents → stdout
161
+ quilltap docs export <mount> <outputDir> # Mount → directory
162
+ quilltap docs find <pattern> # Substring match on file names (--mount, --ext, --type, --limit)
163
+ quilltap docs grep <pattern> # Substring match on extracted text (--mount, --ignore-case, -l, --max, --context)
164
+ quilltap docs status # Per-mount extraction + embedding rollup (--mount, --top)
165
+
166
+ # Server-required
167
+ quilltap docs scan <mount> # Trigger a rescan
168
+ quilltap docs reindex <mount> [path] [--force] # Re-extract + re-chunk
169
+ quilltap docs embed <mount> [path] [--force] [--wait] # Enqueue embedding jobs
170
+ quilltap docs write [--force] <mount> <path> [file] # Stdin or file → mount
171
+ quilltap docs delete <mount> <path> # Idempotent delete
172
+ quilltap docs mkdir <mount> <path> # Idempotent folder create
173
+ quilltap docs move <srcMount> <srcPath> <dstMount> <dstPath> # Move (hard-link when possible)
174
+ quilltap docs copy [--force] <srcMount> <srcPath> <dstMount> <dstPath>
175
+ ```
176
+
177
+ Mount arguments accept the mount name (case-insensitive) or a UUID; ambiguous names print candidates and exit non-zero. `--json` is supported by every verb; `reindex` and `embed` refuse to run without a reachable server.
178
+
179
+ ## Memories
180
+
181
+ `quilltap memories` exposes the same Commonplace Book that each character carries — searchable, sortable, graphable, but never writable. All verbs open the main encrypted DB read-only.
182
+
183
+ ```bash
184
+ quilltap memories ls # All holders, default sort: reinforcedImportance DESC
185
+ quilltap memories ls --character Ariadne --sort created --limit 10 # One holder, newest first
186
+ quilltap memories find "concrete examples" # Substring match on summary (--in content|both)
187
+ quilltap memories grep -i --max 3 --context 1 "concrete examples" # Pattern search inside content, with snippets
188
+ quilltap memories show <id|prefix> [--depth N] [--no-related] # Full record + related-memory neighbourhood
189
+ quilltap memories tree <id|prefix> [--depth N] [--max-nodes N] # ASCII walk of the bidirectional related-memory graph
190
+ quilltap memories status [--character <name|id>] # Per-holder rollup + dangling-edge check
191
+ ```
192
+
193
+ 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.
194
+
92
195
  ## Theme Management
93
196
 
94
197
  The CLI includes theme management commands:
@@ -106,6 +209,81 @@ quilltap themes registry list # List configured registries
106
209
  quilltap themes registry add <url> # Add a registry source
107
210
  ```
108
211
 
212
+ ## Shell Completion
213
+
214
+ Tab-completion for bash, zsh, and fish. Pick the block that matches your shell.
215
+
216
+ ### Bash
217
+
218
+ Append the generated script to `~/.bashrc`:
219
+
220
+ ```bash
221
+ quilltap completion bash >> ~/.bashrc
222
+ ```
223
+
224
+ Or drop it into a system completion directory:
225
+
226
+ ```bash
227
+ quilltap completion bash > /usr/local/etc/bash_completion.d/quilltap
228
+ # Linux with admin rights: /etc/bash_completion.d/quilltap
229
+ ```
230
+
231
+ Restart the shell, or `source ~/.bashrc`.
232
+
233
+ ### Zsh
234
+
235
+ Two reasonable ways to wire this up; pick one.
236
+
237
+ **Option A — one line in `.zshrc`** (simpler; adds noticeable shell-startup latency because `quilltap` runs every time you open a new shell):
238
+
239
+ ```zsh
240
+ # In ~/.zshrc:
241
+ source <(quilltap completion zsh)
242
+ ```
243
+
244
+ **Option B — canonical `fpath` setup** (faster; what zsh expects):
245
+
246
+ ```zsh
247
+ # In ~/.zshrc, before compinit runs:
248
+ fpath=(~/.zsh/completions $fpath)
249
+ autoload -Uz compinit
250
+ compinit
251
+ ```
252
+
253
+ Then once, from any shell:
254
+
255
+ ```zsh
256
+ mkdir -p ~/.zsh/completions
257
+ quilltap completion zsh > ~/.zsh/completions/_quilltap
258
+ ```
259
+
260
+ The leading underscore on `_quilltap` is the zsh convention — it tells `compinit` this is a completion definition file rather than a regular autoloaded function.
261
+
262
+ **oh-my-zsh users:** the framework runs `compinit` for you, so either set the `fpath` line *before* the framework loads, or delete the cache (`rm -f ~/.zcompdump*`) after dropping the file in and start a new shell. The more idiomatic location under oh-my-zsh is:
263
+
264
+ ```zsh
265
+ mkdir -p ~/.oh-my-zsh/custom/plugins/quilltap
266
+ quilltap completion zsh > ~/.oh-my-zsh/custom/plugins/quilltap/_quilltap
267
+ # then add `quilltap` to the plugins=(...) line in ~/.zshrc
268
+ ```
269
+
270
+ ### Fish
271
+
272
+ ```fish
273
+ quilltap completion fish > ~/.config/fish/completions/quilltap.fish
274
+ ```
275
+
276
+ Fish picks new completion files up automatically — no shell restart needed.
277
+
278
+ ### What gets completed
279
+
280
+ - **Subcommands**: `quilltap d<TAB>` → `db docs`
281
+ - **Sub-verbs per namespace**: `quilltap db s<TAB>` → `schema show`
282
+ - **Instance names**: `quilltap --instance Fr<TAB>` → registered instances
283
+ - **Mount names**: `quilltap docs ls --mount Qu<TAB>` → mount points in the active instance
284
+
285
+ Dynamic completions shell out to `quilltap`'s own subcommands. If the active instance is encrypted and no passphrase is reachable, the completion silently returns nothing rather than prompting in the middle of a tab.
286
+
109
287
  ## Requirements
110
288
 
111
289
  - Node.js 18 or later
package/bin/quilltap.js CHANGED
@@ -5,7 +5,14 @@ const { fork, exec, execSync } = require('child_process');
5
5
  const path = require('path');
6
6
  const fs = require('fs');
7
7
  const { getCacheDir, isCacheValid, ensureStandalone } = require('../lib/download-manager');
8
- const { resolveDataDir, promptPassphrase, loadDbKey } = require('../lib/db-helpers');
8
+ const {
9
+ resolveDataDir,
10
+ resolveDataDirAndPassphrase,
11
+ printDefaultInstanceHint,
12
+ promptPassphrase,
13
+ loadDbKey,
14
+ } = require('../lib/db-helpers');
15
+ const { resolveInstance } = require('../lib/instances');
9
16
 
10
17
  const PACKAGE_DIR = path.resolve(__dirname, '..');
11
18
 
@@ -20,6 +27,7 @@ function parseArgs(argv) {
20
27
  const opts = {
21
28
  port: 3000,
22
29
  dataDir: '',
30
+ instance: '',
23
31
  open: false,
24
32
  help: false,
25
33
  version: false,
@@ -42,6 +50,10 @@ function parseArgs(argv) {
42
50
  case '-d':
43
51
  opts.dataDir = args[++i];
44
52
  break;
53
+ case '--instance':
54
+ case '-i':
55
+ opts.instance = args[++i];
56
+ break;
45
57
  case '--open':
46
58
  case '-o':
47
59
  opts.open = true;
@@ -79,11 +91,14 @@ Subcommands:
79
91
  db Query encrypted databases
80
92
  themes Manage theme bundles
81
93
  docs Inspect, read, and export document mounts
94
+ memories Search, browse, and graph memories
95
+ instances Register / inspect named Quilltap instances
82
96
  memory-diff <chatId> Dump existing memories and dry-run re-extraction for a chat
83
97
 
84
98
  Options:
85
99
  -p, --port <number> Port to listen on (default: 3000)
86
100
  -d, --data-dir <path> Data directory (default: platform-specific)
101
+ -i, --instance <name> Use a registered instance (see 'quilltap instances')
87
102
  -o, --open Open browser after server starts
88
103
  -v, --version Show version number
89
104
  --update Force re-download of application files
@@ -346,7 +361,20 @@ async function main() {
346
361
  HOSTNAME: '0.0.0.0',
347
362
  };
348
363
 
349
- if (opts.dataDir) {
364
+ if (opts.dataDir && opts.instance) {
365
+ console.error('Error: Specify either --instance or --data-dir, not both.');
366
+ process.exit(1);
367
+ }
368
+ if (opts.instance) {
369
+ try {
370
+ const inst = resolveInstance(opts.instance);
371
+ env.QUILLTAP_DATA_DIR = inst.path;
372
+ opts.dataDir = inst.path; // surfaced in the startup banner below
373
+ } catch (err) {
374
+ console.error(`Error: ${err.message}`);
375
+ process.exit(1);
376
+ }
377
+ } else if (opts.dataDir) {
350
378
  env.QUILLTAP_DATA_DIR = path.resolve(opts.dataDir);
351
379
  }
352
380
 
@@ -690,16 +718,55 @@ function printDbHelp() {
690
718
  Quilltap Database Tool
691
719
 
692
720
  Usage: quilltap db [options] [sql]
721
+ quilltap db <subcommand> [args]
693
722
 
694
723
  Query your encrypted Quilltap database directly.
695
724
 
696
- Options:
697
- --tables List all tables
725
+ Subcommands (high-level shortcuts; auto-pick the right database):
726
+ schema [table] Schema overview, or details for one table
727
+ schema --grep <text> Find tables/columns whose name matches
728
+ find character [query] List or look up characters by name/id
729
+ find chat [query] List or look up chats by title/id
730
+ find project [query] List or look up projects by name/id
731
+ chats --character <name|id> Chats containing a character
732
+ chats --project <name|id> Chats in a project
733
+ messages --chat <id|title> Recent messages in a chat
734
+ (flags: --last N --full --from <pid> --type <t>)
735
+ logs --chat <id|title> LLM logs for a chat
736
+ logs --message <id> LLM logs for a single message
737
+ logs --character <name|id> LLM logs for a character
738
+ logs --tail N Most recent LLM logs
739
+ message <id> Full content of a single chat message
740
+ log <id> Full request/response of a single LLM log
741
+ memories --character <id> Memories held by a character
742
+ (flags: --about <id|name> --source AUTO|MANUAL)
743
+ optimize [target...] Run maintenance (VACUUM + ANALYZE + PRAGMA optimize)
744
+ on the named databases, or all of them if no
745
+ target is given. Targets: main, llm-logs,
746
+ mount-points (or "all"). Refuses to run while a
747
+ live Quilltap instance holds the lock.
748
+ backup [target...] [--out <dir>]
749
+ Online consistent snapshot of the encrypted
750
+ databases. Safe while the server is running.
751
+ Default destination: <dataDir>/backups/<timestamp>/.
752
+ Each snapshot is re-opened with the same key and
753
+ verified with PRAGMA quick_check.
754
+ integrity [target...] Run PRAGMA cipher_integrity_check and
755
+ PRAGMA integrity_check on the encrypted
756
+ databases. Read-only; safe alongside a live
757
+ instance. Exit code: 0 ok, 1 issues, 2 open
758
+ failure.
759
+ Most subcommands also accept --json and --limit N.
760
+
761
+ Low-level options (legacy; still supported):
762
+ --tables List all tables in the active database
698
763
  --count <table> Show row count for a table
699
- --repl Interactive SQL prompt
764
+ --repl Interactive SQL prompt (extras: .cols, .find)
700
765
  --llm-logs Target the LLM logs database
701
766
  --mount-points Target the document mount-index database
702
- --data-dir <path> Override data directory
767
+ --data-dir <path> Override data directory (pass instance root)
768
+ -i, --instance <name> Use a registered instance from instances.json
769
+ (see 'quilltap instances --help')
703
770
  --passphrase <pass> Provide passphrase for encrypted .dbkey
704
771
  -h, --help Show this help
705
772
 
@@ -713,22 +780,85 @@ will check the QUILLTAP_DB_PASSPHRASE environment variable, then prompt
713
780
  interactively (with hidden input) if a TTY is available.
714
781
 
715
782
  Examples:
783
+ quilltap db schema # tables grouped by domain
784
+ quilltap db schema chat_messages # columns + indexes + DDL link
785
+ quilltap db find character Friday # name → uuid
786
+ quilltap db chats --character Friday # all chats with Friday
787
+ quilltap db messages --chat <id> --last 50 --full # recent messages
788
+ quilltap db logs --chat "physical prompts" # llm logs for a chat
789
+ quilltap db log <log-id> # full request/response
790
+ quilltap db optimize # VACUUM + ANALYZE all DBs
791
+ quilltap db optimize llm-logs # only the LLM logs DB
792
+ quilltap db backup # snapshot all DBs to <dataDir>/backups/<ts>/
793
+ quilltap db backup main --out /tmp/qtap-snap # snapshot only the main DB
794
+ quilltap db integrity # cipher_integrity_check + integrity_check
716
795
  quilltap db --tables
717
796
  quilltap db "SELECT count(*) FROM characters"
718
797
  quilltap db --count messages
719
798
  quilltap db --repl
720
- quilltap db --llm-logs --tables
721
- quilltap db --mount-points --tables
722
- quilltap db --mount-points "SELECT id, name FROM doc_mount_points"
723
799
  quilltap db --lock-status
724
- quilltap db --lock-clean
725
800
  QUILLTAP_DB_PASSPHRASE=secret quilltap db --tables
726
801
  `);
727
802
  }
728
803
 
729
804
  async function dbCommand(args) {
805
+ // ---- Pre-flight: extract dataDir/passphrase, then check for a verb subcommand
806
+ // before the legacy flag loop. This lets `quilltap db <verb>` dispatch
807
+ // to the new high-level commands without touching the legacy path.
808
+ const { isVerb, runVerb, makeCtx } = require('../lib/db-commands');
809
+
810
+ // Strip --data-dir / --passphrase / --instance out so they can appear anywhere
811
+ // on the line, including before or after the verb.
812
+ const cleaned = [];
730
813
  let dataDirOverride = '';
731
814
  let passphrase = '';
815
+ let instanceName = '';
816
+ for (let j = 0; j < args.length; j++) {
817
+ const a = args[j];
818
+ if (a === '--data-dir' || a === '-d') { dataDirOverride = args[++j]; continue; }
819
+ if (a === '--passphrase') { passphrase = args[++j]; continue; }
820
+ if (a === '--instance' || a === '-i') { instanceName = args[++j]; continue; }
821
+ cleaned.push(a);
822
+ }
823
+
824
+ // Find first non-flag arg to test for verb
825
+ const firstPositional = cleaned.find(a => !a.startsWith('-'));
826
+ if (firstPositional && isVerb(firstPositional)) {
827
+ let resolved;
828
+ try {
829
+ resolved = resolveDataDirAndPassphrase({
830
+ dataDir: dataDirOverride,
831
+ instance: instanceName,
832
+ passphrase,
833
+ });
834
+ } catch (err) {
835
+ console.error(`Error: ${err.message}`);
836
+ process.exit(1);
837
+ }
838
+ printDefaultInstanceHint(resolved);
839
+ const dataDir = resolved.dataDir;
840
+ let pepper;
841
+ try {
842
+ pepper = await loadDbKey(dataDir, resolved.passphrase);
843
+ } catch (err) {
844
+ console.error(`Error: ${err.message}`);
845
+ process.exit(1);
846
+ }
847
+ // Re-strip global flags but preserve everything else (verb + its flags)
848
+ const ctx = makeCtx(dataDir, pepper);
849
+ try {
850
+ await runVerb(cleaned, ctx);
851
+ } catch (err) {
852
+ if (!err.silent) {
853
+ console.error(`Error: ${err.message}`);
854
+ }
855
+ const code = err.exitCode != null ? err.exitCode : (err.ambiguous ? 2 : 1);
856
+ process.exit(code);
857
+ }
858
+ return;
859
+ }
860
+
861
+ // ---- Legacy flag-based path (unchanged) ----
732
862
  let useLlmLogs = false;
733
863
  let useMountPoints = false;
734
864
  let showTables = false;
@@ -741,25 +871,23 @@ async function dbCommand(args) {
741
871
  let lockOverride = false;
742
872
 
743
873
  let i = 0;
744
- while (i < args.length) {
745
- switch (args[i]) {
746
- case '--data-dir': case '-d': dataDirOverride = args[++i]; break;
747
- case '--passphrase': passphrase = args[++i]; break;
874
+ while (i < cleaned.length) {
875
+ switch (cleaned[i]) {
748
876
  case '--llm-logs': useLlmLogs = true; break;
749
877
  case '--mount-points': useMountPoints = true; break;
750
878
  case '--tables': showTables = true; break;
751
- case '--count': countTable = args[++i]; break;
879
+ case '--count': countTable = cleaned[++i]; break;
752
880
  case '--repl': repl = true; break;
753
881
  case '--help': case '-h': showHelp = true; break;
754
882
  case '--lock-status': lockStatus = true; break;
755
883
  case '--lock-clean': lockClean = true; break;
756
884
  case '--lock-override': lockOverride = true; break;
757
885
  default:
758
- if (args[i].startsWith('-')) {
759
- console.error(`Unknown option: ${args[i]}`);
886
+ if (cleaned[i].startsWith('-')) {
887
+ console.error(`Unknown option: ${cleaned[i]}`);
760
888
  process.exit(1);
761
889
  }
762
- sql = args[i];
890
+ sql = cleaned[i];
763
891
  break;
764
892
  }
765
893
  i++;
@@ -770,7 +898,20 @@ async function dbCommand(args) {
770
898
  process.exit(0);
771
899
  }
772
900
 
773
- const dataDir = resolveDataDir(dataDirOverride);
901
+ let legacyResolved;
902
+ try {
903
+ legacyResolved = resolveDataDirAndPassphrase({
904
+ dataDir: dataDirOverride,
905
+ instance: instanceName,
906
+ passphrase,
907
+ });
908
+ } catch (err) {
909
+ console.error(`Error: ${err.message}`);
910
+ process.exit(1);
911
+ }
912
+ printDefaultInstanceHint(legacyResolved);
913
+ const dataDir = legacyResolved.dataDir;
914
+ passphrase = legacyResolved.passphrase;
774
915
 
775
916
  // ---- Instance lock commands (no database open required) ----
776
917
  if (lockStatus || lockClean || lockOverride) {
@@ -851,7 +992,7 @@ async function dbCommand(args) {
851
992
  const readline = require('readline');
852
993
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout, prompt: 'quilltap> ' });
853
994
  console.log(`Connected to ${dbPath}`);
854
- console.log('Type .tables, .schema <table>, or SQL. Ctrl+D to exit.\n');
995
+ console.log('Type .tables, .schema <table>, .cols <table>, .find <text>, or SQL. Ctrl+D to exit.\n');
855
996
  rl.prompt();
856
997
  rl.on('line', (line) => {
857
998
  const trimmed = line.trim();
@@ -867,6 +1008,38 @@ async function dbCommand(args) {
867
1008
  const row = db.prepare("SELECT sql FROM sqlite_master WHERE name = ?").get(table);
868
1009
  console.log(row ? row.sql : `Table '${table}' not found`);
869
1010
  }
1011
+ } else if (trimmed.startsWith('.cols')) {
1012
+ const table = trimmed.split(/\s+/)[1];
1013
+ if (!table) { console.log('Usage: .cols <table>'); }
1014
+ else {
1015
+ const cols = db.prepare(`PRAGMA table_info("${table.replace(/"/g, '""')}")`).all();
1016
+ if (cols.length === 0) console.log(`Table '${table}' not found`);
1017
+ else console.table(cols.map(c => ({
1018
+ name: c.name, type: c.type,
1019
+ notNull: c.notnull ? 'NOT NULL' : '',
1020
+ default: c.dflt_value === null ? '' : c.dflt_value,
1021
+ pk: c.pk ? 'pk' : '',
1022
+ })));
1023
+ }
1024
+ } else if (trimmed.startsWith('.find')) {
1025
+ const needle = trimmed.slice(5).trim();
1026
+ if (!needle) { console.log('Usage: .find <text>'); }
1027
+ else {
1028
+ const lc = needle.toLowerCase();
1029
+ const tables = db.prepare("SELECT name FROM sqlite_master WHERE type='table' ORDER BY name").all().map(r => r.name);
1030
+ const matches = [];
1031
+ for (const t of tables) {
1032
+ const tableHit = t.toLowerCase().includes(lc);
1033
+ const cols = db.prepare(`PRAGMA table_info("${t.replace(/"/g, '""')}")`).all();
1034
+ const colHits = cols.filter(c => c.name.toLowerCase().includes(lc)).map(c => c.name);
1035
+ if (tableHit || colHits.length) matches.push({ table: t, tableMatch: tableHit, columns: colHits });
1036
+ }
1037
+ if (matches.length === 0) console.log(`No tables or columns matching '${needle}'`);
1038
+ else for (const m of matches) {
1039
+ const flag = m.tableMatch ? '*' : ' ';
1040
+ console.log(`${flag} ${m.table}${m.columns.length ? ' ' + m.columns.map(c => '.' + c).join(', ') : ''}`);
1041
+ }
1042
+ }
870
1043
  } else {
871
1044
  const stmt = db.prepare(trimmed);
872
1045
  if (stmt.reader) {
@@ -905,12 +1078,45 @@ if (process.argv[2] === 'db') {
905
1078
  } else if (process.argv[2] === 'docs') {
906
1079
  const { docsCommand } = require('../lib/docs-commands');
907
1080
  docsCommand(process.argv.slice(3));
1081
+ } else if (process.argv[2] === 'memories') {
1082
+ const { memoriesCommand } = require('../lib/memories-commands');
1083
+ memoriesCommand(process.argv.slice(3)).catch(err => {
1084
+ if (!err.silent) {
1085
+ console.error(`Error: ${err.message}`);
1086
+ }
1087
+ const code = err.exitCode != null ? err.exitCode : (err.ambiguous ? 2 : 1);
1088
+ process.exit(code);
1089
+ });
1090
+ } else if (process.argv[2] === 'instances') {
1091
+ const { instancesCommand } = require('../lib/instances-commands');
1092
+ instancesCommand(process.argv.slice(3)).catch(err => {
1093
+ console.error(`Error: ${err.message}`);
1094
+ process.exit(1);
1095
+ });
908
1096
  } else if (process.argv[2] === 'memory-diff') {
909
1097
  const { memoryDiffCommand } = require('../lib/memory-diff-command');
910
1098
  memoryDiffCommand(process.argv.slice(3)).catch(err => {
911
1099
  console.error(`Error: ${err.message}`);
912
1100
  process.exit(1);
913
1101
  });
1102
+ } else if (process.argv[2] === 'completion') {
1103
+ const { completionCommand } = require('../lib/completion-commands');
1104
+ completionCommand(process.argv.slice(3)).catch(err => {
1105
+ console.error(`Error: ${err.message}`);
1106
+ process.exit(1);
1107
+ });
1108
+ } else if (process.argv[2] === 'logs') {
1109
+ const { logsCommand } = require('../lib/logs-commands');
1110
+ logsCommand(process.argv.slice(3)).catch(err => {
1111
+ console.error(`Error: ${err.message}`);
1112
+ process.exit(1);
1113
+ });
1114
+ } else if (process.argv[2] === 'migrations') {
1115
+ const { migrationsCommand } = require('../lib/migrations-commands');
1116
+ migrationsCommand(process.argv.slice(3)).catch(err => {
1117
+ console.error(`Error: ${err.message}`);
1118
+ process.exit(1);
1119
+ });
914
1120
  } else {
915
1121
  main();
916
1122
  }
@@ -0,0 +1,121 @@
1
+ #!/bin/bash
2
+
3
+ # Bash completion for quilltap
4
+ # Source this file or place it in /etc/bash_completion.d/ or ~/.bash_completion.d/
5
+
6
+ _quilltap_complete() {
7
+ local cur prev words cword
8
+ COMPREPLY=()
9
+ cur="${COMP_WORDS[COMP_CWORD]}"
10
+ prev="${COMP_WORDS[COMP_CWORD-1]}"
11
+ words=("${COMP_WORDS[@]}")
12
+ cword=$COMP_CWORD
13
+
14
+ # Global options that can appear before subcommands
15
+ local global_opts="-d --data-dir -i --instance -p --port -o --open -v --version -h --help --update"
16
+
17
+ # Get the subcommand (first non-option word after quilltap)
18
+ local subcommand=""
19
+ local i=1
20
+ while [[ $i -lt $cword ]]; do
21
+ local word="${words[$i]}"
22
+ case "$word" in
23
+ -d|--data-dir|-i|--instance|-p|--port)
24
+ # These take a value, skip the next word
25
+ ((i += 2))
26
+ ;;
27
+ -o|--open|-v|--version|-h|--help|--update)
28
+ # Flags without values
29
+ ((i += 1))
30
+ ;;
31
+ -*)
32
+ # Unknown flag, skip
33
+ ((i += 1))
34
+ ;;
35
+ *)
36
+ # This is the subcommand
37
+ subcommand="$word"
38
+ break
39
+ ;;
40
+ esac
41
+ done
42
+
43
+ # If we're completing a global flag value
44
+ if [[ "$prev" == "-d" ]] || [[ "$prev" == "--data-dir" ]] || \
45
+ [[ "$prev" == "-p" ]] || [[ "$prev" == "--port" ]]; then
46
+ # No completion for paths or ports
47
+ return
48
+ fi
49
+
50
+ if [[ "$prev" == "-i" ]] || [[ "$prev" == "--instance" ]]; then
51
+ # Complete with instance names
52
+ local instances=$(command quilltap instances list --names-only 2>/dev/null)
53
+ COMPREPLY=($(compgen -W "$instances" -- "$cur"))
54
+ return
55
+ fi
56
+
57
+ # If no subcommand yet, complete with subcommands and global options
58
+ if [[ -z "$subcommand" ]]; then
59
+ if [[ "$cur" == -* ]]; then
60
+ COMPREPLY=($(compgen -W "$global_opts" -- "$cur"))
61
+ else
62
+ COMPREPLY=($(compgen -W "db docs themes instances memories memory-diff completion" -- "$cur"))
63
+ fi
64
+ return
65
+ fi
66
+
67
+ # Subcommand-specific completion
68
+ local subcommand_opts=""
69
+ case "$subcommand" in
70
+ db)
71
+ subcommand_opts="schema find chats messages logs message log memories optimize backup integrity"
72
+ if [[ "$cur" == -* ]]; then
73
+ subcommand_opts="$subcommand_opts --instance --data-dir --json --help"
74
+ fi
75
+ COMPREPLY=($(compgen -W "$subcommand_opts" -- "$cur"))
76
+ ;;
77
+ docs)
78
+ local docs_verbs="list show files ls dir read export scan write delete mkdir move copy status find grep reindex embed"
79
+ if [[ "$cur" == -* ]]; then
80
+ docs_verbs="$docs_verbs --mount --instance --data-dir --port --json --help --force --rendered --links"
81
+ fi
82
+ COMPREPLY=($(compgen -W "$docs_verbs" -- "$cur"))
83
+ ;;
84
+ themes)
85
+ subcommand_opts="list install uninstall validate export create search update registry"
86
+ if [[ "$cur" == -* ]]; then
87
+ subcommand_opts="$subcommand_opts --instance --data-dir --output --help"
88
+ fi
89
+ COMPREPLY=($(compgen -W "$subcommand_opts" -- "$cur"))
90
+ ;;
91
+ instances)
92
+ subcommand_opts="list ls show path where add create remove rm delete set-passphrase passphrase"
93
+ if [[ "$cur" == -* ]]; then
94
+ subcommand_opts="$subcommand_opts --help"
95
+ fi
96
+ COMPREPLY=($(compgen -W "$subcommand_opts" -- "$cur"))
97
+ ;;
98
+ memories)
99
+ local mem_verbs="ls find grep show tree status validate"
100
+ if [[ "$cur" == -* ]]; then
101
+ mem_verbs="$mem_verbs --character --about --source --chat --project --since --until --min-importance --min-reinforced --has-embedding --no-embedding --sort --instance --data-dir --json --help"
102
+ fi
103
+ COMPREPLY=($(compgen -W "$mem_verbs" -- "$cur"))
104
+ ;;
105
+ memory-diff)
106
+ if [[ "$cur" == -* ]]; then
107
+ subcommand_opts="--instance --data-dir --help"
108
+ COMPREPLY=($(compgen -W "$subcommand_opts" -- "$cur"))
109
+ fi
110
+ ;;
111
+ completion)
112
+ if [[ "$cur" == -* ]]; then
113
+ COMPREPLY=($(compgen -W "--help" -- "$cur"))
114
+ else
115
+ COMPREPLY=($(compgen -W "bash zsh fish" -- "$cur"))
116
+ fi
117
+ ;;
118
+ esac
119
+ }
120
+
121
+ complete -F _quilltap_complete quilltap