quilltap 4.9.0-dev.62 → 4.9.0-dev.65

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.
@@ -0,0 +1,146 @@
1
+ /**
2
+ * Behavioural guard for the shell-completion templates.
3
+ *
4
+ * The static coverage test next door proves every subcommand is *mentioned*.
5
+ * This one proves the completions still fire once flags are on the line — the
6
+ * failure the templates actually shipped with: `quilltap docs --instance
7
+ * Friday <TAB>` offered nothing, because the verb was looked up by counting
8
+ * words rather than by parsing them.
9
+ *
10
+ * Bash is driven for real (source the script, set COMP_WORDS/COMP_CWORD, read
11
+ * COMPREPLY back). Zsh's completion system can only be driven from inside a
12
+ * completion widget, so its template is checked structurally instead.
13
+ *
14
+ * @jest-environment node
15
+ */
16
+
17
+ 'use strict';
18
+
19
+ const fs = require('fs');
20
+ const os = require('os');
21
+ const path = require('path');
22
+ const { execFileSync } = require('child_process');
23
+
24
+ const COMPLETION_DIR = path.join(__dirname, '..', 'completion');
25
+
26
+ /** A `quilltap` on PATH that answers the completion lookups deterministically. */
27
+ function makeStubBin() {
28
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'quilltap-completion-'));
29
+ const stub = path.join(dir, 'quilltap');
30
+ fs.writeFileSync(
31
+ stub,
32
+ [
33
+ '#!/bin/sh',
34
+ 'case "$*" in',
35
+ ' *"instances list --names-only"*) printf "StubInstance\\n" ;;',
36
+ ' *"docs list --names-only"*) printf "Stub Store\\nOther Store\\n" ;;',
37
+ 'esac',
38
+ 'exit 0',
39
+ '',
40
+ ].join('\n'),
41
+ { mode: 0o755 }
42
+ );
43
+ return dir;
44
+ }
45
+
46
+ const STUB_BIN = makeStubBin();
47
+ afterAll(() => fs.rmSync(STUB_BIN, { recursive: true, force: true }));
48
+
49
+ /**
50
+ * Complete `line` with the bash template and return the candidate list.
51
+ * A trailing space means "start a new word", exactly as at a real prompt.
52
+ */
53
+ function bashComplete(line) {
54
+ const script = `
55
+ source ${JSON.stringify(path.join(COMPLETION_DIR, 'bash.template'))}
56
+ COMP_LINE=${JSON.stringify(line)}
57
+ COMP_POINT=\${#COMP_LINE}
58
+ eval "COMP_WORDS=(\$COMP_LINE)"
59
+ [[ "\$COMP_LINE" =~ [[:space:]]$ ]] && COMP_WORDS+=("")
60
+ COMP_CWORD=\$(( \${#COMP_WORDS[@]} - 1 ))
61
+ _quilltap_complete
62
+ printf '%s\\n' "\${COMPREPLY[@]}"
63
+ `;
64
+ return execFileSync('bash', ['-c', script], {
65
+ encoding: 'utf8',
66
+ env: { ...process.env, PATH: `${STUB_BIN}:${process.env.PATH}` },
67
+ })
68
+ .split('\n')
69
+ .filter(Boolean);
70
+ }
71
+
72
+ describe('bash completion survives flags on the line', () => {
73
+ it('offers docs verbs with no flags', () => {
74
+ expect(bashComplete('quilltap docs ')).toContain('list');
75
+ });
76
+
77
+ it.each([
78
+ ['an instance flag', 'quilltap docs --instance Friday '],
79
+ ['a short instance flag', 'quilltap docs -i Friday '],
80
+ ['a subcommand flag that takes a value', 'quilltap docs --limit 5 '],
81
+ ['a valueless flag', 'quilltap docs --json '],
82
+ ['flags on both sides', 'quilltap --instance Friday docs --json '],
83
+ ])('still offers docs verbs after %s', (_label, line) => {
84
+ expect(bashComplete(line)).toContain('list');
85
+ });
86
+
87
+ it('still offers db verbs after a flag', () => {
88
+ expect(bashComplete('quilltap db --limit 5 ')).toContain('characters');
89
+ });
90
+
91
+ it('still offers db characters verbs after a flag', () => {
92
+ expect(bashComplete('quilltap db characters --instance Friday ')).toContain('status');
93
+ });
94
+
95
+ it('treats -i as --ignore-case under memories, not --instance', () => {
96
+ const got = bashComplete('quilltap memories -i ');
97
+ expect(got).toContain('ls');
98
+ expect(got).not.toContain('StubInstance');
99
+ });
100
+ });
101
+
102
+ describe('bash completion looks up names against the addressed instance', () => {
103
+ it('completes --mount from the document stores', () => {
104
+ expect(bashComplete('quilltap docs --mount ')).toContain('Stub\\ Store');
105
+ });
106
+
107
+ it('completes a store positional for verbs that take one', () => {
108
+ expect(bashComplete('quilltap docs ls ')).toContain('Stub\\ Store');
109
+ });
110
+
111
+ it('completes the destination store of a move', () => {
112
+ expect(bashComplete('quilltap docs move Src a.md ')).toContain('Stub\\ Store');
113
+ });
114
+
115
+ it('does not offer stores where the verb takes none', () => {
116
+ expect(bashComplete('quilltap docs find ')).not.toContain('Stub\\ Store');
117
+ });
118
+ });
119
+
120
+ describe('zsh completion parses positions instead of counting words', () => {
121
+ const tpl = fs.readFileSync(path.join(COMPLETION_DIR, 'zsh.template'), 'utf8');
122
+
123
+ it('has no hard-coded word-index tests', () => {
124
+ // `(( CURRENT == 2 ))` is the bug: it only holds when the verb sits
125
+ // immediately after the subcommand, so any preceding flag hides it.
126
+ expect(tpl).not.toMatch(/\(\(\s*CURRENT\s*==/);
127
+ });
128
+
129
+ it('stops the top-level _arguments swallowing flags typed after the subcommand', () => {
130
+ // Without the (-) prefixes the rest-argument array comes back empty and
131
+ // _quilltap_subcommand has nothing to dispatch on.
132
+ expect(tpl).toContain("'(-): :->subcommand'");
133
+ expect(tpl).toContain("'(-)*::arg:->args'");
134
+ });
135
+
136
+ it('hands every subcommand verb to _arguments as a positional', () => {
137
+ const dispatchers = tpl.match(/'\(?-?\)?1: :->\w+'/g) || [];
138
+ expect(dispatchers.length).toBeGreaterThanOrEqual(6);
139
+ });
140
+
141
+ it('is syntactically valid', () => {
142
+ const file = path.join(STUB_BIN, '_quilltap');
143
+ fs.writeFileSync(file, tpl);
144
+ expect(() => execFileSync('zsh', ['-n', file], { stdio: 'pipe' })).not.toThrow();
145
+ });
146
+ });
@@ -3,6 +3,52 @@
3
3
  # Bash completion for quilltap
4
4
  # Source this file or place it in /etc/bash_completion.d/ or ~/.bash_completion.d/
5
5
 
6
+ # Fill COMPREPLY from newline-separated candidates on stdin. Store and
7
+ # instance names routinely contain spaces ("Project Files: The Estate"), which
8
+ # `compgen -W` would chop into separate candidates.
9
+ _quilltap_lines_compreply() {
10
+ local cur="$1" line
11
+ local oldifs="$IFS"
12
+ COMPREPLY=()
13
+ IFS=$'\n'
14
+ while read -r line; do
15
+ [[ -n "$line" ]] || continue
16
+ [[ "$line" == "$cur"* ]] && COMPREPLY+=("$(printf '%q' "$line")")
17
+ done
18
+ IFS="$oldifs"
19
+ }
20
+
21
+ # Which docs positionals name a document store, and which name a local path.
22
+ # move/copy/link take <srcMount> <srcPath> <dstMount> <dstPath>, so a store is
23
+ # wanted at both 2 and 4; every other store-taking verb takes it first.
24
+ _quilltap_docs_positional() {
25
+ local verb="$1" argpos="$2" cur="$3"
26
+ local store_verbs=" show files ls dir tree read export scan reindex embed write delete mkdir rmdir mvdir move copy link "
27
+
28
+ if [[ "$argpos" == "2" ]] && [[ "$store_verbs" == *" $verb "* ]]; then
29
+ _quilltap_lines_compreply "$cur" \
30
+ <<< "$(command quilltap docs list --names-only "${ctx_flags[@]}" 2>/dev/null)"
31
+ return
32
+ fi
33
+
34
+ case "$verb" in
35
+ move|copy|link)
36
+ if [[ "$argpos" == "4" ]]; then
37
+ _quilltap_lines_compreply "$cur" \
38
+ <<< "$(command quilltap docs list --names-only "${ctx_flags[@]}" 2>/dev/null)"
39
+ fi
40
+ ;;
41
+ export)
42
+ # `docs export <mount> <outputDir>` — the second one is a local directory.
43
+ [[ "$argpos" == "3" ]] && COMPREPLY=($(compgen -d -- "$cur"))
44
+ ;;
45
+ write)
46
+ # `docs write <mount> <path> [file]` — the optional source is local.
47
+ [[ "$argpos" == "4" ]] && COMPREPLY=($(compgen -f -- "$cur"))
48
+ ;;
49
+ esac
50
+ }
51
+
6
52
  _quilltap_complete() {
7
53
  local cur prev words cword
8
54
  COMPREPLY=()
@@ -17,23 +63,62 @@ _quilltap_complete() {
17
63
  # Top-level subcommands
18
64
  local top_cmds="db docs themes instances memories memory-diff recall-replay logs migrations maintenance file-verify completion"
19
65
 
66
+ # Flags that swallow the word after them. A flat list will not do: -o is the
67
+ # valueless global --open but themes' valued --output, and `memories`
68
+ # reserves -i for --ignore-case rather than --instance. Counting those wrong
69
+ # makes a flag's value look like the subcommand's verb, which is how
70
+ # `quilltap docs --limit 5 <TAB>` used to lose the verb list.
71
+ local vf_global=" -d --data-dir -i --instance -p --port --passphrase "
72
+ local vf_db=" --limit --grep --character --project --about --source --chat --message --field --tail --last --from --type --out --id --count "
73
+ local vf_docs=" --mount --folder --type --ext --limit --max --context --top --threshold --sort --depth --max-nodes "
74
+ local vf_themes=" -o --output -k --key -n --name "
75
+ local vf_memories=" -d --data-dir --instance --passphrase --port --character --about --source --chat --project --since --until --min-importance --min-reinforced --sort --limit --in --max --context --depth --max-nodes --top --threshold "
76
+ local vf_logs=" --stream --tail --grep "
77
+ local vf_memory_diff=" --concurrency --out "
78
+ local vf_recall_replay=" --turn --char --limit --port "
79
+ local vf_file_verify=" --stall-ms "
80
+
20
81
  # Get the subcommand (first non-option word after quilltap)
21
82
  local subcommand=""
22
83
  local subverb=""
84
+ local -a ctx_flags=()
85
+ local positional_count=0
23
86
  local i=1
24
87
  while [[ $i -lt $cword ]]; do
25
88
  local word="${words[$i]}"
89
+
90
+ # Which flags take a value depends on the subcommand seen so far; before
91
+ # one is seen, only the global flags are in play.
92
+ local valued="$vf_global"
93
+ case "$subcommand" in
94
+ db) valued="$vf_global$vf_db" ;;
95
+ docs) valued="$vf_global$vf_docs" ;;
96
+ themes) valued="$vf_global$vf_themes" ;;
97
+ memories) valued="$vf_memories" ;;
98
+ logs) valued="$vf_global$vf_logs" ;;
99
+ memory-diff) valued="$vf_global$vf_memory_diff" ;;
100
+ recall-replay) valued="$vf_global$vf_recall_replay" ;;
101
+ file-verify) valued="$vf_global$vf_file_verify" ;;
102
+ esac
103
+
104
+ if [[ "$valued" == *" $word "* ]]; then
105
+ # Remember how the user is addressing an instance so the live lookups
106
+ # below query that database rather than the default one.
107
+ case "$word" in
108
+ -d|--data-dir|-i|--instance|--passphrase)
109
+ if (( i + 1 < cword )); then
110
+ ctx_flags+=("$word" "${words[$((i + 1))]}")
111
+ fi
112
+ ;;
113
+ esac
114
+ # Takes a value, skip the next word
115
+ ((i += 2))
116
+ continue
117
+ fi
118
+
26
119
  case "$word" in
27
- -d|--data-dir|-i|--instance|-p|--port|--passphrase)
28
- # These take a value, skip the next word
29
- ((i += 2))
30
- ;;
31
- -o|--open|-v|--version|-h|--help|--update)
32
- # Flags without values
33
- ((i += 1))
34
- ;;
35
120
  -*)
36
- # Unknown flag, skip
121
+ # Valueless (or unknown) flag
37
122
  ((i += 1))
38
123
  ;;
39
124
  *)
@@ -42,6 +127,10 @@ _quilltap_complete() {
42
127
  elif [[ -z "$subverb" ]]; then
43
128
  subverb="$word"
44
129
  fi
130
+ # Counts the subcommand itself, so the word under the cursor sits at
131
+ # positional_count within the subcommand: 1 is its verb, 2 the verb's
132
+ # first argument, and so on.
133
+ ((positional_count += 1))
45
134
  ((i += 1))
46
135
  ;;
47
136
  esac
@@ -56,9 +145,9 @@ _quilltap_complete() {
56
145
  if [[ "$prev" == "-p" ]] || [[ "$prev" == "--port" ]] || [[ "$prev" == "--passphrase" ]]; then
57
146
  return
58
147
  fi
59
- if [[ "$prev" == "-i" ]] || [[ "$prev" == "--instance" ]]; then
60
- local instances=$(command quilltap instances list --names-only 2>/dev/null)
61
- COMPREPLY=($(compgen -W "$instances" -- "$cur"))
148
+ if [[ "$prev" == "--instance" ]] || { [[ "$prev" == "-i" ]] && [[ "$subcommand" != "memories" ]]; }; then
149
+ _quilltap_lines_compreply "$cur" \
150
+ <<< "$(command quilltap instances list --names-only 2>/dev/null)"
62
151
  return
63
152
  fi
64
153
 
@@ -75,8 +164,8 @@ _quilltap_complete() {
75
164
  # Shared flag-value completions for any subcommand
76
165
  case "$prev" in
77
166
  --mount)
78
- local mounts=$(command quilltap docs list --names-only 2>/dev/null)
79
- COMPREPLY=($(compgen -W "$mounts" -- "$cur"))
167
+ _quilltap_lines_compreply "$cur" \
168
+ <<< "$(command quilltap docs list --names-only "${ctx_flags[@]}" 2>/dev/null)"
80
169
  return
81
170
  ;;
82
171
  --character|--about)
@@ -137,16 +226,14 @@ _quilltap_complete() {
137
226
  docs)
138
227
  local docs_verbs="list show files ls dir tree read export scan write delete mkdir move copy link rmdir mvdir status docker-mounts find grep reindex embed"
139
228
  local docs_flags="--mount --instance --data-dir --passphrase --port --json --help \
140
- --force --rendered --links --folder --type --ext --limit --max --context --top --threshold \
229
+ --uri --base64 --force --rendered --links --folder --type --ext --limit --max --context --top --threshold \
141
230
  --ignore-case -l --wait -R --recursive --sort -r --reverse --depth --max-nodes --long --semantic"
142
- if [[ -z "$subverb" ]]; then
143
- if [[ "$cur" == -* ]]; then
144
- COMPREPLY=($(compgen -W "$docs_flags" -- "$cur"))
145
- else
146
- COMPREPLY=($(compgen -W "$docs_verbs" -- "$cur"))
147
- fi
148
- else
231
+ if [[ "$cur" == -* ]]; then
149
232
  COMPREPLY=($(compgen -W "$docs_flags" -- "$cur"))
233
+ elif [[ -z "$subverb" ]]; then
234
+ COMPREPLY=($(compgen -W "$docs_verbs" -- "$cur"))
235
+ else
236
+ _quilltap_docs_positional "$subverb" "$positional_count" "$cur"
150
237
  fi
151
238
  ;;
152
239
  themes)
@@ -8,6 +8,10 @@ function __quilltap_instance_names
8
8
  command quilltap instances list --names-only 2>/dev/null
9
9
  end
10
10
 
11
+ function __quilltap_mount_names
12
+ command quilltap docs list --names-only 2>/dev/null
13
+ end
14
+
11
15
  function __quilltap_no_subcommand
12
16
  set -l cmd (commandline -opc)
13
17
  if test (count $cmd) -lt 2
@@ -157,7 +161,7 @@ complete -c quilltap -n '__quilltap_using_subcommand docs' -f -a 'rmdir' -d 'Rem
157
161
  complete -c quilltap -n '__quilltap_using_subcommand docs' -f -a 'mvdir' -d 'Rename or move a folder'
158
162
 
159
163
  # docs flags
160
- complete -c quilltap -n '__quilltap_using_subcommand docs' -l 'mount' -d 'Mount name or id' -x
164
+ complete -c quilltap -n '__quilltap_using_subcommand docs' -l 'mount' -d 'Mount name or id' -x -a '(__quilltap_mount_names)'
161
165
  complete -c quilltap -n '__quilltap_using_subcommand docs' -l 'port' -s 'p' -d 'Server port' -x
162
166
  complete -c quilltap -n '__quilltap_using_subcommand docs' -l 'folder' -d 'Folder filter' -x
163
167
  complete -c quilltap -n '__quilltap_using_subcommand docs' -l 'force' -d 'Force operation'
@@ -2,12 +2,27 @@
2
2
 
3
3
  # Zsh completion for quilltap
4
4
 
5
+ # Position is worked out by _arguments, never by counting words. A hard-coded
6
+ # `CURRENT == 2` test means the verb only completes when it sits immediately
7
+ # after the subcommand, so `quilltap docs --instance Friday <TAB>` offers
8
+ # nothing at all. Every function below hands its option list *and* its
9
+ # positional specs to one `_arguments -C` call and branches on $state, so flags
10
+ # may appear anywhere the CLI itself accepts them.
11
+
5
12
  local ret=1
6
13
 
7
14
  _quilltap() {
8
15
  local -a subcommands
9
16
  local -a global_options
10
17
 
18
+ # The whole command line, kept before _arguments rebases `words` for the
19
+ # subcommand dispatch. The lookup helpers read it so that an --instance
20
+ # typed anywhere picks which database they query.
21
+ local -a _quilltap_line
22
+ local -i _quilltap_cword
23
+ _quilltap_line=("${words[@]}")
24
+ _quilltap_cword=$CURRENT
25
+
11
26
  global_options=(
12
27
  '(-d --data-dir)'{-d,--data-dir}'[Data directory]:directory:_directories'
13
28
  '(-i --instance)'{-i,--instance}'[Use a registered instance]:instance:_quilltap_instance_names'
@@ -34,10 +49,14 @@ _quilltap() {
34
49
  'completion:Generate shell completion scripts'
35
50
  )
36
51
 
52
+ # (-) on both positionals stops the top-level _arguments from swallowing a
53
+ # flag typed *after* the subcommand: without it `quilltap docs --instance
54
+ # Friday <TAB>` parses --instance as a global option, leaving the rest-arg
55
+ # array empty and the subcommand dispatch with nothing to dispatch on.
37
56
  _arguments -C \
38
- '1: :->subcommand' \
39
57
  "${global_options[@]}" \
40
- '*::arg:->args'
58
+ '(-): :->subcommand' \
59
+ '(-)*::arg:->args'
41
60
 
42
61
  case "$state" in
43
62
  subcommand)
@@ -74,7 +93,7 @@ _quilltap_subcommand() {
74
93
  _quilltap_memory_diff
75
94
  ;;
76
95
  recall-replay)
77
- _arguments '--turn[interchange to replay]:turn:' '--char[character id]:char:' '--limit[rows per path]:limit:' '--port[server port]:port:' '--json[raw JSON output]' '--help[show help]'
96
+ _quilltap_recall_replay
78
97
  ;;
79
98
  logs)
80
99
  _quilltap_logs
@@ -86,7 +105,7 @@ _quilltap_subcommand() {
86
105
  _quilltap_maintenance
87
106
  ;;
88
107
  file-verify)
89
- _arguments '--all[read every top-level file, not just dataless ones]' '--stall-ms[per-chunk stall threshold in ms]:ms:' '--json[machine-readable output]' '--help[show help]'
108
+ _quilltap_file_verify
90
109
  ;;
91
110
  completion)
92
111
  _quilltap_completion
@@ -95,7 +114,10 @@ _quilltap_subcommand() {
95
114
  }
96
115
 
97
116
  _quilltap_db() {
117
+ local curcontext="$curcontext" state line
118
+ typeset -A opt_args
98
119
  local -a subverbs db_opts
120
+
99
121
  subverbs=(
100
122
  'schema:Show database schema'
101
123
  'find:Find entities by name'
@@ -147,46 +169,74 @@ _quilltap_db() {
147
169
  '(-h --help)'{-h,--help}'[Show help]'
148
170
  )
149
171
 
150
- if (( CURRENT == 2 )); then
151
- _describe 'db subcommand' subverbs
152
- return
153
- fi
172
+ # (-) again: a flag typed after the verb belongs to the verb's own
173
+ # _arguments call below, not to this one.
174
+ _arguments -C $db_opts \
175
+ '(-): :->verb' \
176
+ '(-)*::arg:->rest'
154
177
 
155
- if [[ "$words[2]" == "characters" ]]; then
156
- local -a char_verbs char_opts
157
- char_verbs=(
158
- 'status:Per-character vault status report'
159
- 'archives:List archived characters and ARCHIVE bundles'
160
- 'archive:Archive a character (runs through the server)'
161
- 'rehydrate:Wake an archived character (runs through the server)'
162
- 'export:Write a plaintext .qtap for a character'
163
- )
164
- char_opts=(
165
- '(-i --instance)'{-i,--instance}'[Registered instance name]:instance:_quilltap_instance_names'
166
- '(-d --data-dir)'{-d,--data-dir}'[Data directory]:directory:_directories'
167
- '--passphrase[Database passphrase]:passphrase:'
168
- '--json[JSON output]'
169
- '--limit[Result limit]:limit:'
170
- '--diverged[Only characters whose DB and vault differ]'
171
- '--blocked[Only characters with vault issues]'
172
- '--id[Single character by name or id]:character:'
173
- '--write[Perform the archive/rehydrate write]'
174
- '(-p --port)'{-p,--port}'[Server port]:port:'
175
- '--out[Output .qtap path]:path:_files'
176
- '(-h --help)'{-h,--help}'[Show help]'
177
- )
178
- if (( CURRENT == 3 )); then
179
- _describe 'characters subcommand' char_verbs
180
- fi
181
- _arguments $char_opts
182
- return
183
- fi
178
+ case "$state" in
179
+ verb)
180
+ _describe -t commands 'db subcommand' subverbs
181
+ ;;
182
+ rest)
183
+ case "$line[1]" in
184
+ characters)
185
+ _quilltap_db_characters
186
+ ;;
187
+ *)
188
+ # Nothing to complete positionally, but the flags still apply.
189
+ _arguments $db_opts
190
+ ;;
191
+ esac
192
+ ;;
193
+ esac
194
+ }
184
195
 
185
- _arguments $db_opts
196
+ _quilltap_db_characters() {
197
+ local curcontext="$curcontext" state line
198
+ typeset -A opt_args
199
+ local -a char_verbs char_opts
200
+
201
+ char_verbs=(
202
+ 'status:Per-character vault status report'
203
+ 'archives:List archived characters and ARCHIVE bundles'
204
+ 'archive:Archive a character (runs through the server)'
205
+ 'rehydrate:Wake an archived character (runs through the server)'
206
+ 'export:Write a plaintext .qtap for a character'
207
+ )
208
+
209
+ char_opts=(
210
+ '(-i --instance)'{-i,--instance}'[Registered instance name]:instance:_quilltap_instance_names'
211
+ '(-d --data-dir)'{-d,--data-dir}'[Data directory]:directory:_directories'
212
+ '--passphrase[Database passphrase]:passphrase:'
213
+ '--json[JSON output]'
214
+ '--limit[Result limit]:limit:'
215
+ '--diverged[Only characters whose DB and vault differ]'
216
+ '--blocked[Only characters with vault issues]'
217
+ '--id[Single character by name or id]:character:'
218
+ '--write[Perform the archive/rehydrate write]'
219
+ '(-p --port)'{-p,--port}'[Server port]:port:'
220
+ '--out[Output .qtap path]:path:_files'
221
+ '(-h --help)'{-h,--help}'[Show help]'
222
+ )
223
+
224
+ _arguments -C $char_opts \
225
+ '1: :->cverb' \
226
+ '*: :'
227
+
228
+ case "$state" in
229
+ cverb)
230
+ _describe -t commands 'characters subcommand' char_verbs
231
+ ;;
232
+ esac
186
233
  }
187
234
 
188
235
  _quilltap_docs() {
236
+ local curcontext="$curcontext" state line
237
+ typeset -A opt_args
189
238
  local -a subverbs docs_opts
239
+
190
240
  subverbs=(
191
241
  'list:List all mount points'
192
242
  'show:Details for one mount point'
@@ -219,10 +269,12 @@ _quilltap_docs() {
219
269
  '--passphrase[Database passphrase]:passphrase:'
220
270
  '(-p --port)'{-p,--port}'[Server port]:port:'
221
271
  '--json[JSON output]'
222
- '--mount[Mount name or id]:mount:'
272
+ '--uri[Show canonical qtap:// URIs as the locator]'
273
+ '--mount[Mount name or id]:mount:_quilltap_mount_names'
223
274
  '--folder[Folder filter]:folder:'
224
275
  '--force[Force operation]'
225
276
  '--rendered[Render rich content]'
277
+ '--base64[Base64 transfer for binary files]'
226
278
  '--links[Include link details]'
227
279
  '--type[Filter by type]:type:(file folder)'
228
280
  '--ext[Extension filter]:ext:'
@@ -244,14 +296,61 @@ _quilltap_docs() {
244
296
  '(-h --help)'{-h,--help}'[Show help]'
245
297
  )
246
298
 
247
- if (( CURRENT == 2 )); then
248
- _describe 'docs subcommand' subverbs
299
+ _arguments -C $docs_opts \
300
+ '1: :->verb' \
301
+ '2: :->pos2' \
302
+ '3: :->pos3' \
303
+ '4: :->pos4' \
304
+ '*: :'
305
+
306
+ case "$state" in
307
+ verb)
308
+ _describe -t commands 'docs subcommand' subverbs
309
+ ;;
310
+ pos2|pos3|pos4)
311
+ _quilltap_docs_positional ${state#pos}
312
+ ;;
313
+ esac
314
+ }
315
+
316
+ # Which docs positionals name a document store, and which name a local path.
317
+ # `move`/`copy`/`link` take <srcMount> <srcPath> <dstMount> <dstPath>, so a
318
+ # store is wanted at both 2 and 4; everything else that takes a store takes it
319
+ # first. `$line` comes from the caller's _arguments -C.
320
+ _quilltap_docs_positional() {
321
+ local -i n=$1
322
+ local verb="$line[1]"
323
+ local -a store_verbs
324
+ store_verbs=(
325
+ show files ls dir tree read export scan reindex embed
326
+ write delete mkdir rmdir mvdir move copy link
327
+ )
328
+
329
+ if (( n == 2 )) && (( ${store_verbs[(I)$verb]} )); then
330
+ _quilltap_mount_names
331
+ return
249
332
  fi
250
- _arguments $docs_opts
333
+
334
+ case "$verb" in
335
+ move|copy|link)
336
+ (( n == 4 )) && _quilltap_mount_names
337
+ ;;
338
+ export)
339
+ # `docs export <mount> <outputDir>` — the second one is a local directory.
340
+ (( n == 3 )) && _directories
341
+ ;;
342
+ write)
343
+ # `docs write <mount> <path> [file]` — the optional source is local.
344
+ (( n == 4 )) && _files
345
+ ;;
346
+ esac
251
347
  }
252
348
 
253
349
  _quilltap_themes() {
254
- local -a subverbs registry_verbs themes_opts registry_opts
350
+ local curcontext="$curcontext" state line
351
+ typeset -A opt_args
352
+ local -a subverbs themes_opts
353
+
255
354
  subverbs=(
256
355
  'list:List available themes'
257
356
  'install:Install a theme'
@@ -271,37 +370,70 @@ _quilltap_themes() {
271
370
  '(-h --help)'{-h,--help}'[Show help]'
272
371
  )
273
372
 
274
- if (( CURRENT == 2 )); then
275
- _describe 'themes subcommand' subverbs
276
- return
277
- fi
373
+ # (-) again: a flag typed after the verb belongs to the verb's own
374
+ # _arguments call below, not to this one.
375
+ _arguments -C $themes_opts \
376
+ '(-): :->verb' \
377
+ '(-)*::arg:->rest'
278
378
 
279
- if [[ "$words[2]" == "registry" ]]; then
280
- registry_verbs=(
281
- 'list:List registries'
282
- 'add:Add a registry'
283
- 'remove:Remove a registry'
284
- 'refresh:Refresh registries'
285
- 'keygen:Generate Ed25519 key'
286
- 'sign:Sign a registry or bundle'
287
- )
288
- registry_opts=(
289
- '(-k --key)'{-k,--key}'[Signing key path]:path:_files'
290
- '(-n --name)'{-n,--name}'[Registry name]:name:'
291
- '(-o --output)'{-o,--output}'[Output path]:path:_files'
292
- )
293
- if (( CURRENT == 3 )); then
294
- _describe 'registry subcommand' registry_verbs
295
- fi
296
- _arguments $registry_opts
297
- return
298
- fi
379
+ case "$state" in
380
+ verb)
381
+ _describe -t commands 'themes subcommand' subverbs
382
+ ;;
383
+ rest)
384
+ case "$line[1]" in
385
+ registry)
386
+ _quilltap_themes_registry
387
+ ;;
388
+ *)
389
+ _arguments $themes_opts
390
+ ;;
391
+ esac
392
+ ;;
393
+ esac
394
+ }
395
+
396
+ _quilltap_themes_registry() {
397
+ local curcontext="$curcontext" state line
398
+ typeset -A opt_args
399
+ local -a registry_verbs registry_opts
400
+
401
+ registry_verbs=(
402
+ 'list:List registries'
403
+ 'add:Add a registry'
404
+ 'remove:Remove a registry'
405
+ 'refresh:Refresh registries'
406
+ 'keygen:Generate Ed25519 key'
407
+ 'sign:Sign a registry or bundle'
408
+ )
299
409
 
300
- _arguments $themes_opts
410
+ # themesCommand() parses -d/-i/-o/-h anywhere in the argv, including after
411
+ # `registry`, so they belong in this list too.
412
+ registry_opts=(
413
+ '(-k --key)'{-k,--key}'[Signing key path]:path:_files'
414
+ '(-n --name)'{-n,--name}'[Registry name]:name:'
415
+ '(-o --output)'{-o,--output}'[Output path]:path:_files'
416
+ '(-i --instance)'{-i,--instance}'[Registered instance name]:instance:_quilltap_instance_names'
417
+ '(-d --data-dir)'{-d,--data-dir}'[Data directory]:directory:_directories'
418
+ '(-h --help)'{-h,--help}'[Show help]'
419
+ )
420
+
421
+ _arguments -C $registry_opts \
422
+ '1: :->rverb' \
423
+ '*: :'
424
+
425
+ case "$state" in
426
+ rverb)
427
+ _describe -t commands 'registry subcommand' registry_verbs
428
+ ;;
429
+ esac
301
430
  }
302
431
 
303
432
  _quilltap_instances() {
433
+ local curcontext="$curcontext" state line
434
+ typeset -A opt_args
304
435
  local -a subverbs inst_opts
436
+
305
437
  subverbs=(
306
438
  'list:List registered instances'
307
439
  'ls:List registered instances'
@@ -326,25 +458,30 @@ _quilltap_instances() {
326
458
  '(-h --help)'{-h,--help}'[Show help]'
327
459
  )
328
460
 
329
- if (( CURRENT == 2 )); then
330
- _describe 'instances subcommand' subverbs
331
- return
332
- fi
461
+ _arguments -C $inst_opts \
462
+ '1: :->verb' \
463
+ '2: :->name' \
464
+ '*: :'
333
465
 
334
- case "$words[2]" in
335
- show|remove|rm|delete|set-passphrase|passphrase|default|rename)
336
- if (( CURRENT == 3 )); then
337
- _values 'instance' ${(f)"$(command quilltap instances list --names-only 2>/dev/null)"}
338
- return
339
- fi
466
+ case "$state" in
467
+ verb)
468
+ _describe -t commands 'instances subcommand' subverbs
469
+ ;;
470
+ name)
471
+ case "$line[1]" in
472
+ show|remove|rm|delete|set-passphrase|passphrase|default|rename)
473
+ _quilltap_instance_names
474
+ ;;
475
+ esac
340
476
  ;;
341
477
  esac
342
-
343
- _arguments $inst_opts
344
478
  }
345
479
 
346
480
  _quilltap_memories() {
481
+ local curcontext="$curcontext" state line
482
+ typeset -A opt_args
347
483
  local -a subverbs mem_opts
484
+
348
485
  subverbs=(
349
486
  'ls:List memories'
350
487
  'find:Substring search'
@@ -356,10 +493,10 @@ _quilltap_memories() {
356
493
  )
357
494
 
358
495
  mem_opts=(
359
- '(-i --instance)'{-i,--instance}'[Registered instance name]:instance:_quilltap_instance_names'
496
+ '--instance[Registered instance name]:instance:_quilltap_instance_names'
360
497
  '(-d --data-dir)'{-d,--data-dir}'[Data directory]:directory:_directories'
361
498
  '--passphrase[Database passphrase]:passphrase:'
362
- '(-p --port)'{-p,--port}'[Server port]:port:'
499
+ '--port[Server port]:port:'
363
500
  '--json[JSON output]'
364
501
  '--character[Character name or id]:character:'
365
502
  '--about[Subject of memory]:character:'
@@ -379,7 +516,7 @@ _quilltap_memories() {
379
516
  '--in[Restrict find-in field]:field:'
380
517
  '--no-related[Hide related neighbors]'
381
518
  '--list[List-only output]'
382
- '(-i --ignore-case)'{-i,--ignore-case}'[Case-insensitive]'
519
+ '(-i --ignore-case)'{-i,--ignore-case}'[Case-insensitive (memories reserves -i for this, not --instance)]'
383
520
  '(-l --paths-only)'{-l,--paths-only}'[Paths only]'
384
521
  '--max[Maximum results]:n:'
385
522
  '--context[Context lines]:n:'
@@ -391,10 +528,15 @@ _quilltap_memories() {
391
528
  '(-h --help)'{-h,--help}'[Show help]'
392
529
  )
393
530
 
394
- if (( CURRENT == 2 )); then
395
- _describe 'memories subcommand' subverbs
396
- fi
397
- _arguments $mem_opts
531
+ _arguments -C $mem_opts \
532
+ '1: :->verb' \
533
+ '*: :'
534
+
535
+ case "$state" in
536
+ verb)
537
+ _describe -t commands 'memories subcommand' subverbs
538
+ ;;
539
+ esac
398
540
  }
399
541
 
400
542
  _quilltap_memory_diff() {
@@ -405,7 +547,22 @@ _quilltap_memory_diff() {
405
547
  '--passphrase[Database passphrase]:passphrase:' \
406
548
  '(-p --port)'{-p,--port}'[Server port]:port:' \
407
549
  '--concurrency[Concurrency limit]:n:' \
408
- '--out[Output file]:path:_files'
550
+ '--out[Output file]:path:_files' \
551
+ '*: :'
552
+ }
553
+
554
+ _quilltap_recall_replay() {
555
+ _arguments \
556
+ '(-h --help)'{-h,--help}'[Show help]' \
557
+ '(-i --instance)'{-i,--instance}'[Use instance]:instance:_quilltap_instance_names' \
558
+ '(-d --data-dir)'{-d,--data-dir}'[Data directory]:directory:_directories' \
559
+ '--passphrase[Database passphrase]:passphrase:' \
560
+ '--turn[interchange to replay]:turn:' \
561
+ '--char[character id]:char:' \
562
+ '--limit[rows per path]:limit:' \
563
+ '--port[server port]:port:' \
564
+ '--json[raw JSON output]' \
565
+ '*: :'
409
566
  }
410
567
 
411
568
  _quilltap_logs() {
@@ -417,11 +574,27 @@ _quilltap_logs() {
417
574
  '--stream[Which log stream]:stream:(combined error stdout stderr startup)' \
418
575
  '--tail[Last N lines (0=full)]:n:' \
419
576
  '(-f --follow)'{-f,--follow}'[Stream new lines]' \
420
- '--grep[Regex filter]:pattern:'
577
+ '--grep[Regex filter]:pattern:' \
578
+ '*: :'
579
+ }
580
+
581
+ _quilltap_file_verify() {
582
+ _arguments \
583
+ '(-h --help)'{-h,--help}'[Show help]' \
584
+ '(-i --instance)'{-i,--instance}'[Use instance]:instance:_quilltap_instance_names' \
585
+ '(-d --data-dir)'{-d,--data-dir}'[Data directory]:directory:_directories' \
586
+ '--passphrase[Database passphrase]:passphrase:' \
587
+ '--all[read every top-level file, not just dataless ones]' \
588
+ '--stall-ms[per-chunk stall threshold in ms]:ms:' \
589
+ '--json[machine-readable output]' \
590
+ '*: :'
421
591
  }
422
592
 
423
593
  _quilltap_migrations() {
594
+ local curcontext="$curcontext" state line
595
+ typeset -A opt_args
424
596
  local -a subverbs mig_opts
597
+
425
598
  subverbs=(
426
599
  'status:Migration status'
427
600
  'pending:List pending migrations'
@@ -437,14 +610,22 @@ _quilltap_migrations() {
437
610
  '(-h --help)'{-h,--help}'[Show help]'
438
611
  )
439
612
 
440
- if (( CURRENT == 2 )); then
441
- _describe 'migrations subcommand' subverbs
442
- fi
443
- _arguments $mig_opts
613
+ _arguments -C $mig_opts \
614
+ '1: :->verb' \
615
+ '*: :'
616
+
617
+ case "$state" in
618
+ verb)
619
+ _describe -t commands 'migrations subcommand' subverbs
620
+ ;;
621
+ esac
444
622
  }
445
623
 
446
624
  _quilltap_maintenance() {
625
+ local curcontext="$curcontext" state line
626
+ typeset -A opt_args
447
627
  local -a subverbs maint_opts
628
+
448
629
  subverbs=(
449
630
  'status:Show dry-run retention counts'
450
631
  'run:Run retention/cleanup sweeps (lock-gated)'
@@ -458,20 +639,71 @@ _quilltap_maintenance() {
458
639
  '(-h --help)'{-h,--help}'[Show help]'
459
640
  )
460
641
 
461
- if (( CURRENT == 2 )); then
462
- _describe 'maintenance subcommand' subverbs
463
- fi
464
- _arguments $maint_opts
642
+ _arguments -C $maint_opts \
643
+ '1: :->verb' \
644
+ '*: :'
645
+
646
+ case "$state" in
647
+ verb)
648
+ _describe -t commands 'maintenance subcommand' subverbs
649
+ ;;
650
+ esac
465
651
  }
466
652
 
467
653
  _quilltap_completion() {
468
- _values 'shell' bash zsh fish
654
+ local curcontext="$curcontext" state line
655
+ typeset -A opt_args
656
+
657
+ _arguments -C \
658
+ '(-h --help)'{-h,--help}'[Show help]' \
659
+ '1: :(bash zsh fish)' \
660
+ '*: :'
661
+ }
662
+
663
+ # ---------------------------------------------------------------------------
664
+ # Lookup helpers
665
+ #
666
+ # These shell out to quilltap itself, so they have to be told which instance
667
+ # the user is addressing — otherwise `--instance Friday` completes mount names
668
+ # from the default instance. _quilltap_ctx_flags rebuilds the addressing flags
669
+ # from the command line being typed and the helpers pass them straight through.
670
+ # ---------------------------------------------------------------------------
671
+
672
+ _quilltap_ctx_flags() {
673
+ ctx_flags=()
674
+ local -i i=2
675
+ while (( i <= $#_quilltap_line )); do
676
+ case "$_quilltap_line[i]" in
677
+ -i|--instance|-d|--data-dir|--passphrase)
678
+ # The word under the cursor is a half-typed value, not an answer.
679
+ if (( i + 1 <= $#_quilltap_line && i + 1 != _quilltap_cword )) \
680
+ && [[ -n "$_quilltap_line[i+1]" ]]; then
681
+ ctx_flags+=("$_quilltap_line[i]" "${(Q)_quilltap_line[i+1]}")
682
+ fi
683
+ (( i += 2 ))
684
+ ;;
685
+ *)
686
+ (( i += 1 ))
687
+ ;;
688
+ esac
689
+ done
469
690
  }
470
691
 
471
692
  _quilltap_instance_names() {
472
- local instances
693
+ local -a instances expl
473
694
  instances=(${(f)"$(command quilltap instances list --names-only 2>/dev/null)"})
474
- _values 'instance' $instances
695
+ (( $#instances )) || return 1
696
+ # compadd -a, not _values: instance and store names may contain spaces and
697
+ # colons, which _values would chop into value:description pairs.
698
+ _wanted instances expl 'instance' compadd -a instances
699
+ }
700
+
701
+ _quilltap_mount_names() {
702
+ local -a ctx_flags mounts expl
703
+ _quilltap_ctx_flags
704
+ mounts=(${(f)"$(command quilltap docs list --names-only $ctx_flags 2>/dev/null)"})
705
+ (( $#mounts )) || return 1
706
+ _wanted mounts expl 'document store' compadd -a mounts
475
707
  }
476
708
 
477
709
  _quilltap "$@"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "quilltap",
3
- "version": "4.9.0-dev.62",
3
+ "version": "4.9.0-dev.65",
4
4
  "description": "Self-hosted AI workspace for writers, worldbuilders, and roleplayers. Run with npx quilltap.",
5
5
  "author": {
6
6
  "name": "Charles Sebold",