@zenera/cli 1.1.10 → 1.1.11

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.
Files changed (41) hide show
  1. package/README.md +49 -15
  2. package/dist/cache.d.ts +98 -0
  3. package/dist/cache.js +301 -0
  4. package/dist/catalog.d.ts +3 -0
  5. package/dist/catalog.js +35 -11
  6. package/dist/commands/cache.d.ts +7 -0
  7. package/dist/commands/cache.js +245 -0
  8. package/dist/commands/check.js +6 -3
  9. package/dist/commands/index.js +2 -0
  10. package/dist/commands/key.js +68 -14
  11. package/dist/commands/models.js +17 -1
  12. package/dist/commands/run.js +11 -3
  13. package/dist/commands/sandbox.js +70 -23
  14. package/dist/home.d.ts +2 -2
  15. package/dist/home.js +2 -2
  16. package/dist/keys.d.ts +22 -0
  17. package/dist/keys.js +105 -2
  18. package/dist/lib.d.ts +1 -0
  19. package/dist/lib.js +1 -0
  20. package/dist/liveness.js +11 -0
  21. package/dist/resolve.d.ts +4 -0
  22. package/dist/resolve.js +43 -17
  23. package/dist/term.d.ts +23 -2
  24. package/dist/term.js +215 -8
  25. package/dist/tui/app.d.ts +10 -0
  26. package/dist/tui/app.js +470 -54
  27. package/dist/tui/theme.d.ts +6 -2
  28. package/dist/tui/theme.js +14 -8
  29. package/dist/tui/wrap.d.ts +92 -0
  30. package/dist/tui/wrap.js +147 -2
  31. package/dist/validate.d.ts +2 -0
  32. package/dist/validate.js +87 -2
  33. package/package.json +2 -2
  34. package/templates/editor/.github/copilot-instructions.md +50 -13
  35. package/templates/editor/.github/prompts/new-agent.prompt.md +5 -2
  36. package/templates/editor/.github/prompts/sync-with-spec.prompt.md +4 -0
  37. package/templates/editor/.github/skills/zen-cli/references/faker.md +18 -8
  38. package/templates/editor/.github/skills/zen-cli/references/keys.md +7 -7
  39. package/templates/editor/.github/skills/zen-rag-docs/SKILL.md +575 -0
  40. package/templates/editor/.github/skills/{api-schema-index → zen-rag-schema}/SKILL.md +2 -2
  41. package/templates/editor/.vscode/settings.json +1 -1
@@ -91,17 +91,27 @@ Probes are synthetic on purpose — real request bodies never reach a prompt.
91
91
 
92
92
  ## The cache
93
93
 
94
- Under `~/.zenera/neo/faker/generators/<key>/`, one directory per operation. A
95
- generator that a model gave up on is remembered, so a hopeless operation is not
96
- re-asked on every request; a _transient_ failure a 429, a dropped socket — is
97
- not, because it is about this minute rather than this operation.
98
-
99
- `zen faker cache clear` removes the generators and the container together. They
100
- have to go together: the container's name is a hash of its configuration, so
101
- deleting the directory alone would leave a stopped container bind-mounted onto a
94
+ Generators live in the machine's shared cache, `~/.zenera/neo/cache/faker-generator/`,
95
+ keyed by the operation's shape so the same spec served from two directories is
96
+ written once, and a spec edit produces a new key rather than a stale answer. A
97
+ generator that a model gave up on is remembered for the life of the process, so
98
+ a hopeless operation is not re-asked on every request; a _transient_ failure — a
99
+ 429, a dropped socket is not, because it is about this minute rather than this
100
+ operation.
101
+
102
+ `~/.zenera/neo/faker/` is the container's workspace, and scratch: a cache hit is
103
+ copied into it, because the container can only run what is under its mount.
104
+
105
+ `zen faker cache ls` lists what has been generated. `zen faker cache clear`
106
+ removes the generators, the workspace and the container together. They have to
107
+ go together: the container's name is a hash of its configuration, so deleting
108
+ the workspace alone would leave a stopped container bind-mounted onto a
102
109
  directory that no longer exists, and every generator would fail with
103
110
  `python3: can't open file '/workspace/generators/…/gen.py'`.
104
111
 
112
+ `zen cache ls --kind faker-generator` reaches the same entries from the other
113
+ side, along with everything else this machine has cached.
114
+
105
115
  ## Documents it accepts
106
116
 
107
117
  Swagger 2 and OpenAPI 3.0/3.1, JSON or YAML, `$ref`s resolved. Several documents
@@ -25,8 +25,8 @@ keys; one of them is active.
25
25
  | `zen key show <ref> [--reveal]` | Masked by default |
26
26
  | `zen key env [provider …]` | Shell exports, for other tools |
27
27
 
28
- `zen key add` also takes `--project` and `--location`, for a Vertex service
29
- account.
28
+ `zen key add` also takes `--gcp-project` and `--gcp-location`, for a Vertex
29
+ service account.
30
30
 
31
31
  ```
32
32
  zen key add openai # prompts, echo off
@@ -56,18 +56,18 @@ echo-off prompt and from nowhere else.
56
56
 
57
57
  A **service-account file** is absorbed into `~/.zenera/neo/keys/` and
58
58
  `GOOGLE_APPLICATION_CREDENTIALS` points at it. It wants a project too, from
59
- `--project`, or `GOOGLE_CLOUD_PROJECT`, or the `project_id` inside the file, and
60
- a region from `--location` (`global` otherwise). Application Default
59
+ `--gcp-project`, or `GOOGLE_CLOUD_PROJECT`, or the `project_id` inside the file,
60
+ and a region from `--gcp-location` (`global` otherwise). Application Default
61
61
  Credentials from `gcloud auth application-default login` work with no entry at
62
62
  all.
63
63
 
64
64
  An **express-mode key** is an ordinary secret under `VERTEX_API_KEY`, and
65
65
  addresses no project: a key and a project are alternatives, and sending both
66
- gets a `403` that mentions neither. `--project` and `--location` are therefore
67
- refused alongside a key.
66
+ gets a `403` that mentions neither. `--gcp-project` and `--gcp-location` are
67
+ therefore refused alongside a key.
68
68
 
69
69
  ```
70
- zen key add vertex --project acme-prod --location europe-west4
70
+ zen key add vertex --gcp-project acme-prod --gcp-location europe-west4
71
71
  # paste a path → the file shape
72
72
  # paste a key → the express shape
73
73
  ```
@@ -0,0 +1,575 @@
1
+ ---
2
+ name: zen-rag-docs
3
+ description: What a document index is, how to build one from a folder of `.md` files with `zen rag docs index` (or `npx @zenera/cli`), and how to ask it something — searching by meaning and narrowing to a release, a heading or a table, listing and grepping it exactly instead of reaching for shell `grep`/`rg`, reading a section back verbatim, giving it to an agent as tools, and writing the project skill that a wired-in index requires. For an OpenAPI description instead of prose, see `zen-rag-schema`.
4
+ ---
5
+
6
+ # The document index
7
+
8
+ A document index is a pile of markdown turned into something that can be
9
+ **asked a question**. It is built once, on disk, and answered from — and what
10
+ comes back is not a summary but the documents themselves: the passages that
11
+ matched, quoted verbatim, with their line numbers.
12
+
13
+ It exists because a documentation tree does not fit in a context window and
14
+ grepping it does not help. The paragraph that answers "how are rate limits
15
+ counted?" does not contain the word "counted", sits four headings deep, and the
16
+ number you actually need is in a table two screens further down. Forty `grep`
17
+ hits for `limit` are not an answer; the eight lines around one of them are.
18
+
19
+ ## The commands
20
+
21
+ ```
22
+ zen rag docs <index|search|list|grep|show|stats> [path...]
23
+ ```
24
+
25
+ | Command | Answers | Embedder? | Typical |
26
+ | -------- | ------------------------------------------ | --------- | ------- |
27
+ | `index` | builds the thing | yes | minutes |
28
+ | `search` | _where does this corpus talk about X?_ | **yes** | seconds |
29
+ | `list` | _what documents/headings/tables are here?_ | no | instant |
30
+ | `grep` | _does the string X appear anywhere?_ | no | instant |
31
+ | `show` | _print this section, verbatim_ | no | instant |
32
+ | `stats` | _what is in this index?_ | no | instant |
33
+
34
+ Only `search` ranks, and only `search` costs a network round trip — it embeds
35
+ the query before it can compare anything. The other four read `outline.json`
36
+ and the copies in `sources/` off the disk and answer in milliseconds. Reach for
37
+ `search` when the question is vague and for `list`/`grep`/`show` when it is
38
+ precise. If a search feels slow, it is that one embedding call, not the index:
39
+ near-zero CPU for several seconds is the tell.
40
+
41
+ > **Never reach for shell `grep`, `rg`, `find`, `cat` or `head` here.**
42
+ > Not on the source tree, not on anything under the index directory.
43
+ > `zen rag docs grep` and `zen rag docs list` are the exact-matching commands,
44
+ > they are local, they need no credential, and they answer in milliseconds.
45
+ > Shell tools on the same files are strictly worse: they match raw lines, so
46
+ > they cannot say which heading a hit sits under, cannot narrow to one release
47
+ > of a tree by name, cannot tell a table row from the prose around it, and
48
+ > cannot report how many matches there were past the ones they printed.
49
+ > `rg -n 'Retry-After' docs/` gives you paths and offsets; `zen rag docs grep
50
+ "Retry-After"` gives you the line, the document, and the section it belongs
51
+ > to, counted in full.
52
+
53
+ ## What is in one
54
+
55
+ Three things, and the third is the point.
56
+
57
+ | Piece | Is |
58
+ | -------------- | --------------------------------------------------------- |
59
+ | `outline.json` | every heading and every table, with the lines they cover |
60
+ | `sources/` | the documents themselves, verbatim, exactly as indexed |
61
+ | `lance/` | one row per chunk: two texts, one vector, and the filters |
62
+
63
+ ```
64
+ docs-db/
65
+ ├── README.md what this index holds — a live progress report while it builds
66
+ ├── manifest.json written LAST — its absence means "not indexed"
67
+ ├── outline.json headings and tables, read whole
68
+ ├── sources/ the documents, verbatim — where every quoted line comes from
69
+ └── lance/ the chunks
70
+ ```
71
+
72
+ `sources/` is not a convenience. A search returns **line ranges**, and the lines
73
+ are read back out of those copies, so what you are shown is the document and not
74
+ a reconstruction of it. That is also what makes an index one portable thing:
75
+ nothing in it names a path outside itself, so it can be committed, shipped, or
76
+ mounted somewhere else in an agent's sandbox and still answer. (A schema index
77
+ has `--no-sources`; a document index does not, because there the copies _are_
78
+ the answer.)
79
+
80
+ `manifest.json` records **which embedder made the vectors**, so a search with a
81
+ different model is refused rather than answered with noise.
82
+
83
+ ### How a document is cut up
84
+
85
+ Chunking follows the markdown rather than a character count: a paragraph, a
86
+ list, a fenced code block, the frontmatter. Every chunk knows the heading it
87
+ sits under, which is what makes `--section` a filter and not a hope.
88
+
89
+ Tables are indexed **twice over** — once as a descriptor carrying the caption
90
+ and the column names, and once per row, with the header row travelling
91
+ alongside so the columns are still named wherever a row lands. A row too wide
92
+ to be one chunk is cut into column groups with the key column repeated. That is
93
+ why `--kind table_row` is a useful thing to ask for: a limits table is a
94
+ hundred facts, not one paragraph.
95
+
96
+ Every chunk gets a `kind`: `paragraph`, `list`, `table`, `table_row`, `code`,
97
+ `frontmatter`, `html`.
98
+
99
+ ## Installing
100
+
101
+ `zen rag` ships in `@zenera/rag`, a sibling of the CLI. It has no binary of its
102
+ own — installing it adds the `rag` subcommand to `zen`, which is also where the
103
+ credentials already live.
104
+
105
+ ```sh
106
+ npm i -g @zenera/cli @zenera/rag # then: zen rag docs …
107
+ ```
108
+
109
+ For a one-off, without installing anything, **both** packages must be in the
110
+ same temporary install or `zen` will report `rag` as not installed:
111
+
112
+ ```sh
113
+ npx --package @zenera/cli --package @zenera/rag -- zen rag docs index ./docs --embedding openai:text-embedding-3-small
114
+ ```
115
+
116
+ Every example below is spelled `zen …`; prefix it with that `npx` form if you
117
+ have not installed globally.
118
+
119
+ ## Building an index from `.md` files
120
+
121
+ ```
122
+ zen rag docs index <path...> [--embedding <ref>] [-o <dir>] [--batch <n>] [--chunk-tokens <n>]
123
+ ```
124
+
125
+ ```sh
126
+ zen rag docs index ./docs --embedding openai:text-embedding-3-small
127
+ ```
128
+
129
+ That is the whole thing: point it at a directory and it walks it.
130
+
131
+ ### What gets read
132
+
133
+ | You name | What happens |
134
+ | --------------- | ----------------------------------------------------------------- |
135
+ | a **file** | that file |
136
+ | a **directory** | walked, recursively |
137
+ | a **glob** | anchored at the deepest directory it names outright, then matched |
138
+
139
+ `.md`, `.markdown`, `.txt` and `.text` are read; `.txt` and `.text` are treated
140
+ as prose with no headings. Anything else is not a document and is not indexed.
141
+ Hidden directories and `node_modules` are skipped — a file nobody can see is
142
+ not one anybody meant to index. A file over 16 MB is skipped as a data dump and
143
+ said so on stderr, next to the per-document table.
144
+
145
+ Several paths can go into one index, and usually should: one index over a whole
146
+ documentation tree is what makes "which release says this?" answerable at all.
147
+
148
+ ```sh
149
+ zen rag docs index ./docs ./README.md ./packages/*/README.md --embedding openai:text-embedding-3-small
150
+ zen rag docs index "releases/nsx_4.*/**/*.md" --embedding google:gemini-embedding-001 -o .index/nsx
151
+ ```
152
+
153
+ ### The name is the identity
154
+
155
+ Every document is given a **name**: its path relative to the common root of
156
+ everything you named. Nothing anywhere records where the file was on the machine
157
+ that built the index.
158
+
159
+ That name is what `--file` patterns match, so it is worth arranging on purpose.
160
+ Indexing two release trees at once keeps `nsx_4.1.0/api/routing.md` and
161
+ `nsx_4.2.0/api/routing.md` apart, and a search can be pinned to one of them.
162
+ Indexing one tree from inside it gives you `api/routing.md`. Two files that
163
+ would land on the same name are deduped rather than merged.
164
+
165
+ ### The flags
166
+
167
+ | Flag | Default | Meaning |
168
+ | -------------------- | ----------- | ------------------------------------------------------------ |
169
+ | `--embedding <ref>` | — | Which embedder makes the vectors. Omit it to see the choices |
170
+ | `-o`, `--out <dir>` | `./docs-db` | Where the index goes; `$ZEN_DOCS_DB` if that is set |
171
+ | `--batch <n>` | `96` | Texts per embedding request, and how often progress prints |
172
+ | `--chunk-tokens <n>` | `384` | Target chunk size. Leave it alone unless you have a reason |
173
+ | `--quiet` | — | No narration |
174
+
175
+ The embedding reference names a provider first — `openai:text-embedding-3-small`,
176
+ not a bare model id. Credentials come from the `zen` keyring (`zen key ls`), and
177
+ a real environment variable always wins.
178
+
179
+ ### What it prints
180
+
181
+ A per-document table (lines, sections, tables, chunks) with a total row, a line
182
+ per skipped file with the reason, then a progress line per batch. **stdout is
183
+ the output directory and nothing else**, so `DIR=$(zen rag docs index …)` works;
184
+ everything else is stderr.
185
+
186
+ This is the one command here that spends money and time: it embeds every chunk.
187
+ While it runs, the output directory narrates itself — `README.md` is a live
188
+ progress report, rewritten at most every five seconds, replaced on completion by
189
+ a description of what the index turned out to hold, and left saying so if the
190
+ build dies. A `.lock` names the process, so a second build of the same directory
191
+ is refused unless the lock is stale.
192
+
193
+ Rebuild the index when the documents change. Nothing watches them, and a stale
194
+ index is a confident wrong answer — worse here than anywhere, because the quoted
195
+ lines will look exactly as authoritative as they did when they were true.
196
+
197
+ ## Which index gets read
198
+
199
+ Every reading command takes `-d`, `--dir`. Without one:
200
+
201
+ 1. `$ZEN_DOCS_DB`, if it is set. **Set this once** instead of typing `-d` on
202
+ every command — `export ZEN_DOCS_DB=/assets/docs-db`.
203
+ 2. Otherwise the **nearest index** to the working directory: here, then a short
204
+ way down into it, then up a level and again, stopping at your home
205
+ directory. The one chosen is named on stderr as it is used, so an answer is
206
+ never anonymous.
207
+ 3. Otherwise `./docs-db`, which is only so the error names the directory you
208
+ were expecting.
209
+
210
+ What is looked for is a `manifest.json`, and the search is **scoped by kind** —
211
+ a `docs` index and a `schema` index can sit in the same tree without either
212
+ shadowing the other, and nothing searches for a directory literally called
213
+ `docs-db`. That is just the name a new one is given.
214
+
215
+ Two document indexes the same distance away is refused rather than guessed at:
216
+ the wrong index does not fail, it answers confidently about another corpus.
217
+ Name one with `-d`, or set `ZEN_DOCS_DB`.
218
+
219
+ ## Searching it
220
+
221
+ ```
222
+ zen rag docs search [text...] [narrowings…] [shape…]
223
+ ```
224
+
225
+ ```sh
226
+ zen rag docs search "how are rate limits counted"
227
+ ```
228
+
229
+ The answer is the corpus quoting itself:
230
+
231
+ ```
232
+ ## nsx_4.2.0/api/routing.md — 9 of 148 lines
233
+
234
+ 5 | ## Rate limits
235
+ 7 | Requests are counted per tenant and rejected past the limit.
236
+ ... 12 lines omitted (Retries, Backoff) ...
237
+ 24 | | route | limit | window |
238
+ 25 | | --- | --- | --- |
239
+ 27 | | /api/users | 250 | 1m |
240
+ ```
241
+
242
+ Every line carries its number, and the gaps say what was left out. That is
243
+ deliberate: a passage you cannot point at is a passage you cannot do anything
244
+ with. The numbers are what `show` and `read_docs` take.
245
+
246
+ ### Narrowing is the interface
247
+
248
+ Nobody finds the paragraph they want on the first ask. They search, see it is
249
+ the wrong release, and search again inside one. So the second call is the same
250
+ question with a narrowing on it — not a different command.
251
+
252
+ | Flag | Narrows to |
253
+ | ------------------------ | ------------------------------------------------------------------------------------ |
254
+ | `-f`, `--file <p>` | documents whose **name** matches. Repeatable |
255
+ | `--exclude-file <p>` | everything but those. Repeatable |
256
+ | `-s`, `--section <name>` | one heading and whatever nests inside it. Repeatable |
257
+ | `--kind <k>` | `paragraph`, `list`, `table`, `table_row`, `code`, `frontmatter`, `html`. Repeatable |
258
+ | `--mode <m>` | `hybrid` (default), `vector`, `text` |
259
+ | `--exclude-id <id>` | a passage already seen. Repeatable |
260
+
261
+ ```sh
262
+ zen rag docs search --file "nsx_4.2.*/api/**" "rate limit for the users route"
263
+ zen rag docs search --section "Rate limits" --kind table "requests per minute"
264
+ zen rag docs search --mode text "X-RateLimit-Remaining"
265
+ ```
266
+
267
+ A `--file` pattern with `*` or `?` is a glob matched against the whole document
268
+ name; a plain word is a substring. So `--file routing` finds
269
+ `nsx_4.2.0/api/routing.md` and `--file "routing*"` finds nothing.
270
+
271
+ `--section` takes a heading title, a structure id, or the structure path an
272
+ earlier answer printed. A title that appears in four documents becomes four
273
+ scopes, not a guess at one.
274
+
275
+ **`--mode text` is the one to remember.** `hybrid` blends meaning and wording,
276
+ which is right for a question; when what you have is an exact string — an error
277
+ message, a header name, a flag — `text` is exact wording only, and meaning can
278
+ only dilute it.
279
+
280
+ The narrowings are resolved against the manifest and the outline **before** the
281
+ store is touched, so a `--file` pattern that matches nothing says so instead of
282
+ quietly searching everything.
283
+
284
+ ### Shaping the answer
285
+
286
+ | Flag | Default | Meaning |
287
+ | -------------------- | ------- | ----------------------------------------- |
288
+ | `-d`, `--dir <dir>` | found | Which index — see "Which index gets read" |
289
+ | `--embedding <ref>` | index's | Must be the one the index was built with |
290
+ | `--limit <n>` | `8` | Passages kept |
291
+ | `-B`, `--before <n>` | `0` | Extra lines quoted before each passage |
292
+ | `-A`, `--after <n>` | `0` | Extra lines quoted after each passage |
293
+ | `--max-lines <n>` | `400` | A ceiling on the whole answer |
294
+ | `--no-numbers` | — | Quote the lines without their numbers |
295
+ | `--hits` | — | One row per passage instead of the text |
296
+ | `--interactive` | — | Prompt, search, narrow. Needs a terminal |
297
+ | `--quiet` | — | No narration |
298
+
299
+ `--hits` is the fast triage: it prints where the passages are without spending
300
+ the lines on them, and you follow up with `show`.
301
+
302
+ ### As a machine interface
303
+
304
+ Non-interactive search is a tool, not an afterthought. `--json` is a stable
305
+ shape — `{matches, files, scope, mode, considered, truncated}` — no terminal is
306
+ needed, and **an empty result exits 0**: a caller must never have to tell
307
+ "nothing matched" from "the index is missing" by parsing stderr.
308
+
309
+ Everything about the invocation is validated before an embedder is constructed,
310
+ so a typo is a usage error and not a credential error.
311
+
312
+ ### `--interactive`
313
+
314
+ A prompt that keeps the query between searches, and excludes what it has
315
+ already shown you so asking again moves on:
316
+
317
+ ```
318
+ <text> search
319
+ file <pattern> only documents matching it; blank for all
320
+ not <pattern> drop documents matching it
321
+ section <name> only under this heading, by title, id or path
322
+ kind <k> paragraph | list | table | table_row | code | frontmatter | html
323
+ mode <m> hybrid | vector | text
324
+ limit <n> passages per search
325
+ files the documents currently in scope
326
+ show the query as it stands
327
+ reset forget it, narrowings and exclusions alike
328
+ quit
329
+ ```
330
+
331
+ ## Reading it without searching
332
+
333
+ None of these need an embedder or a credential — they are plain reads of the
334
+ outline and the copies on disk.
335
+
336
+ ```
337
+ zen rag docs list <files|sections|tables> [-d <dir>] [filters…]
338
+ zen rag docs grep <pattern> [-d <dir>] [filters…]
339
+ zen rag docs show <file> [--section <name>] [--lines <from-to>]
340
+ zen rag docs stats
341
+ ```
342
+
343
+ | Flag | For | Meaning |
344
+ | ------------------------ | ------ | ---------------------------------------------- |
345
+ | `-f`, `--file <p>` | both | Only these documents. Repeatable |
346
+ | `--exclude-file <p>` | both | Drop these documents. Repeatable |
347
+ | `-s`, `--section <name>` | both | Only under this heading. Repeatable |
348
+ | `--depth <n>` | `list` | `sections` only: deepest heading level to show |
349
+ | `--regex` | both | Read the patterns as regular expressions |
350
+ | `--case-sensitive` | both | Stop ignoring case |
351
+ | `--limit <n>` | both | Print at most n; `found` still counts them all |
352
+ | `--json` | both | `{found, rows, truncated}` |
353
+ | `--quiet` | both | No narration |
354
+
355
+ ```sh
356
+ zen rag docs list files # every document, and what it holds
357
+ zen rag docs list sections --file "api/**" # every heading, with its line span
358
+ zen rag docs list sections --depth 2 # just the shape of the tree
359
+ zen rag docs list tables # every table, with its columns
360
+ zen rag docs grep "Retry-After" # every matching line, and its section
361
+ zen rag docs grep "X-RateLimit-\w+" --regex --file "api/**"
362
+ zen rag docs show api/routing.md --section "Rate limits"
363
+ zen rag docs show api/routing.md --lines 40-80
364
+ zen rag docs stats
365
+ ```
366
+
367
+ `list files` reports the name, title, format, and how many lines, sections,
368
+ tables and chunks each document holds — which is also the fastest way to learn
369
+ the names your `--file` patterns will be matched against. `list sections` is a
370
+ table of contents with line spans; `list tables` names every table's caption,
371
+ columns and row count, which is how you find the limits table without reading
372
+ the prose around it.
373
+
374
+ `grep` reports `found` as the **true total** even when `--limit` cuts the rows,
375
+ so unlike a search it can answer whether a string appears at all. Every hit
376
+ carries the innermost heading it sits under, so a match has a place. Nothing
377
+ matching exits 0 with empty stdout — and that emptiness is trustworthy, which is
378
+ the whole point of it.
379
+
380
+ `show` prints a document, a named section, or a line range, verbatim, with a
381
+ line-number gutter (`--no-numbers` to drop it). It is the natural follow-up to
382
+ every search: find the passage, then read what is actually around it.
383
+
384
+ `stats` says what is in an index and what built it — the documents, the counts,
385
+ the embedding model. It is the fastest way to answer "is this index the one I
386
+ think it is?".
387
+
388
+ ### Which one to reach for
389
+
390
+ | The question | The command |
391
+ | ------------------------------------------------- | ----------------------------------------------- |
392
+ | "how does this thing handle retries?" | `search "how are retries handled"` |
393
+ | _anything you would have run `grep` for_ | `grep` / `list` — never the shell |
394
+ | "does `X-Request-Id` appear anywhere?" | `grep X-Request-Id` |
395
+ | "what does the rate-limits section actually say?" | `show <file> --section "Rate limits"` |
396
+ | "which documents are even in here?" | `list files` |
397
+ | "what is the shape of this manual?" | `list sections --depth 2` |
398
+ | "where is the table of per-route limits?" | `list tables --file "api/**"` |
399
+ | "the exact error string, not something like it" | `search --mode text "connection reset by peer"` |
400
+ | "same question, but only the 4.2 docs" | `search --file "nsx_4.2*/**" "…"` |
401
+ | "is this index the right one?" | `stats` |
402
+
403
+ The rule: **a question about meaning is a `search`; a question about presence,
404
+ spelling or a count is a `list` or a `grep`; a question about what a passage
405
+ actually says is a `show`.** Search cannot answer the middle one, because a
406
+ ranking always returns its best guesses whether or not any of them are right.
407
+
408
+ ### Instead of the shell
409
+
410
+ Add `-d <dir>` when the index is not the nearest one, or name it once with
411
+ `ZEN_DOCS_DB`.
412
+
413
+ | The reflex | The command |
414
+ | ----------------------------------- | ----------------------------------------------------- |
415
+ | `grep -rn "Retry-After" docs/` | `zen rag docs grep "Retry-After"` |
416
+ | `rg -i retry --glob 'api/**'` | `zen rag docs grep retry --file "api/**"` |
417
+ | `grep -E "X-RateLimit-\w+"` | `zen rag docs grep "X-RateLimit-\w+" --regex` |
418
+ | `grep -c` / `wc -l` | `--json`, and read `found` — it counts past `--limit` |
419
+ | `find docs -name '*.md'` | `zen rag docs list files` |
420
+ | `grep '^#' file.md` | `zen rag docs list sections --file file.md` |
421
+ | `sed -n '40,80p' file.md` | `zen rag docs show file.md --lines 40-80` |
422
+ | `cat file.md` (to find one section) | `zen rag docs show file.md --section "Rate limits"` |
423
+ | `ls` the index directory | `zen rag docs stats` |
424
+
425
+ ```sh
426
+ export ZEN_DOCS_DB=/assets/docs-db # once, then never again
427
+ zen rag docs grep "Retry-After" --limit 10
428
+ zen rag docs list sections --file "api/**" --depth 3
429
+ ```
430
+
431
+ If none of these fits the question, the question is about meaning, and the
432
+ answer is `search` — still not the shell.
433
+
434
+ ## Giving it to an agent
435
+
436
+ The same engine, as four tools in the group `docs`. An agent takes them all
437
+ with `docs:*` in its `tools:`.
438
+
439
+ ```ts
440
+ import { createEmbedder, loadProject } from '@zenera/neo';
441
+ import { docs } from '@zenera/rag';
442
+
443
+ const index = await docs.DocsIndex.open(
444
+ './docs-db',
445
+ createEmbedder('openai:text-embedding-3-small'),
446
+ );
447
+ const project = await loadProject('./my-project', { tools: docs.docsTools(index) });
448
+ ```
449
+
450
+ | Tool | For |
451
+ | ------------- | ------------------------------------------------------------ |
452
+ | `search_docs` | the passages that match, quoted with their line numbers |
453
+ | `list_docs` | the documents, their headings, or their tables — no search |
454
+ | `grep_docs` | every matching line, counted in full — no search |
455
+ | `read_docs` | a section or a line range, verbatim and with nothing omitted |
456
+
457
+ Only `search_docs` ranks; the other three are exact, because a model told "no
458
+ results" by a vector search has learned nothing — a ranking returns the top of a
459
+ list, so an empty answer and an absent thing look identical. `grep_docs` is how
460
+ "it is not written down anywhere" can actually be concluded.
461
+
462
+ `search_docs` is shaped for the **second** call rather than the first. The first
463
+ is always a sentence and always returns some of the wrong tree; the second is
464
+ the same sentence with `files: ["nsx_4.2*/api/**"]`, or `section: "Rate limits"`,
465
+ or `kind: ["table"]`. Those are parameters and not separate tools, so narrowing
466
+ costs one call instead of three. `exclude_ids` takes the ids from an earlier
467
+ answer, so asking again moves on instead of repeating itself.
468
+
469
+ Every answer carries line numbers and `read_docs` takes them. That is the loop
470
+ the whole subject exists for: **find the passage, read around it, then edit the
471
+ file the passage came from.**
472
+
473
+ Tell the agent in its prompt to call `list_docs` once before its first search —
474
+ the document names are what every `files` pattern is matched against, and a
475
+ model guessing at them narrows to nothing and concludes the corpus is empty.
476
+
477
+ ## Wiring it into a project means writing the project a skill
478
+
479
+ **Whenever an index is used by a Zenera project — as `docs:*` tools, or as
480
+ `zen rag` reachable from the agent's sandbox — write a skill for it in that
481
+ project.** Not optional, and not the same thing as passing the tools in.
482
+
483
+ Wiring alone leaves the model to infer everything that matters. A tool
484
+ description says what `grep_docs` does; it cannot say that this index holds the
485
+ NSX 4.1 and 4.2 manuals side by side, that every answer must be pinned to a
486
+ release with `files`, that the API reference lives under `*/api/**` and the
487
+ task guides under `*/guides/**`, or that the numbers anybody actually wants are
488
+ in tables and so `kind: ["table_row"]` is the right first move. That is project
489
+ knowledge, and project knowledge belongs in a skill — where it is loaded only
490
+ when the model is working on this corpus, instead of sitting in the system
491
+ prompt of every run.
492
+
493
+ ```
494
+ <project>/agents/skills/<corpus>-docs/SKILL.md
495
+ ```
496
+
497
+ Frontmatter is `name` and `description`; the description is what the model reads
498
+ when choosing, so it must name the corpus and the questions it answers. Add
499
+ `tools: [search_docs, list_docs, grep_docs, read_docs]` if the skill should be
500
+ what unlocks them.
501
+
502
+ ### What the skill has to say
503
+
504
+ | Section | Because |
505
+ | ------------------ | ----------------------------------------------------------------------------------- |
506
+ | Which corpus | What it is, which version(s), and when it was indexed |
507
+ | Where the index | `ZEN_DOCS_DB`, or the `-d` to pass — an agent cannot guess a path |
508
+ | The document names | The shape `--file`/`files` patterns match, with two real examples |
509
+ | Which command | Meaning → `search`; presence or a count → `grep`; what it says → `show`/`read_docs` |
510
+ | Never the shell | State it outright. `grep`/`rg`/`cat` on the tree is the default reflex |
511
+ | How to narrow | Which `files` prefixes matter here, and when the answer is a table |
512
+ | Worked examples | Two or three, with **real document names and headings from this index** |
513
+ | Citing | Answer with the document name and line numbers, because the tools give them |
514
+
515
+ Best practice, in order of how often it is got wrong:
516
+
517
+ 1. **Use real names.** `list_docs` output the model will actually see beats a
518
+ generic `<file>` placeholder, because the names are the anchors it steers by.
519
+ 2. **Say which command answers which question**, and say that a ranking cannot
520
+ prove absence. Left alone a model searches for everything, gets eight ranked
521
+ guesses, and answers from the best of them.
522
+ 3. **Name the narrowing that matters here.** Two releases in one index is the
523
+ common case, and an unpinned search silently mixes them.
524
+ 4. **Keep it short.** A skill is prompt. One screen of routing rules beats a
525
+ transcription of this document — link to `zen rag docs --help` for the flags.
526
+ 5. **Re-index, then re-read the skill.** Both go stale against the same change,
527
+ and a skill quoting headings that no longer exist is worse than none.
528
+ 6. **One skill per corpus**, named after it. Two corpora in one skill and the
529
+ model mixes their conventions.
530
+
531
+ ```md
532
+ ---
533
+ name: nsx-docs
534
+ description: How to find an answer in the NSX 4.1 and 4.2 manuals — which document says it,
535
+ what it says verbatim, and which release it is true of. Use before answering anything
536
+ about NSX behaviour, limits or API routes.
537
+ ---
538
+
539
+ # The NSX manuals
540
+
541
+ Indexed at `/assets/docs-db` (already in `$ZEN_DOCS_DB`). Two releases side by
542
+ side; document names begin `nsx_4.1.0/` or `nsx_4.2.0/`, then `api/` for the
543
+ reference and `guides/` for the task documentation.
544
+
545
+ - Always pin the release: `files: ["nsx_4.2.0/**"]`. An unpinned search mixes them.
546
+ - Vague question ("how does edge failover work?") → `search_docs`, then ask again
547
+ with `section` or `files` once you can see which half of the tree it is in.
548
+ - Does X exist, how is it spelled, how many are there → `grep_docs`. It is
549
+ complete; a search is not, and cannot prove absence.
550
+ - Limits and defaults are in tables → add `kind: ["table_row"]`.
551
+ - Then `read_docs` the lines around the hit before answering. Never answer from
552
+ the excerpt alone.
553
+ - Never `grep`/`rg`/`cat` the tree — the tools above are local and exact.
554
+
555
+ Worked: per-route rate limits are the table under "Rate limits" in
556
+ `nsx_4.2.0/api/routing.md` (lines 24-31); the retry envelope is described two
557
+ sections down. Cite the document name and the line numbers.
558
+ ```
559
+
560
+ ## When it goes wrong
561
+
562
+ | Symptom | Cause |
563
+ | ------------------------------------------------ | ------------------------------------------------------------------------------------- |
564
+ | "not installed" | `@zenera/rag` is not resolvable — install it, or use the two-`--package` npx form |
565
+ | `nothing to index` | No `.md`/`.markdown`/`.txt`/`.text` under those paths, or they are all hidden |
566
+ | A document you expected is missing | Hidden directory, `node_modules`, an unlisted extension, or over 16 MB — check stderr |
567
+ | Refused for a different embedding | The index records the model that built it; re-index or pass the right `--embedding` |
568
+ | No manifest / not an index | A build that did not finish. `manifest.json` is written last on purpose |
569
+ | A build is refused | `.lock` — another build is running. It is taken over when the process is gone |
570
+ | `provider "openai": no api key` | `zen key ls` — the keyring, or a real environment variable |
571
+ | A usage error before any credential is asked for | Deliberate: everything about the invocation is checked first, so a typo is a typo |
572
+ | `no document matched --file` | The pattern is matched against the document **name**; `list files` prints them |
573
+ | Nothing matched | Exit 0 with an empty answer. Try fewer words, or `--mode text` for an exact string |
574
+ | Answers quoting text that is no longer there | Nothing watches the documents. Re-index after they change |
575
+ | A search took ten seconds, using no CPU | One embedding round trip, not the index. `list`/`grep`/`show` make none |
@@ -1,6 +1,6 @@
1
1
  ---
2
- name: api-schema-index
3
- description: What a schema index is, how it finds the right call inside a large OpenAPI/Swagger document, and how to build and query one with `zen rag schema` (or `npx @zenera/cli`) — searching it by meaning, listing and grepping it exactly instead of reaching for shell `grep`/`rg`, tracing a field up to the operations that carry it, giving it to an agent as tools, and writing the project skill that a wired-in index requires.
2
+ name: zen-rag-schema
3
+ description: What a schema index is, how it finds the right call inside a large OpenAPI/Swagger document, and how to build and query one with `zen rag schema` (or `npx @zenera/cli`) — searching it by meaning, listing and grepping it exactly instead of reaching for shell `grep`/`rg`, tracing a field up to the operations that carry it, giving it to an agent as tools, and writing the project skill that a wired-in index requires. For a pile of markdown instead of an API description, see `zen-rag-docs`.
4
4
  ---
5
5
 
6
6
  # The schema index
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "chat.useNestedAgentsMdFiles": false,
3
3
  "chat.tools.terminal.autoApprove": {
4
- "zen": true
4
+ "*": true
5
5
  }
6
6
  }