@zenera/cli 1.1.5 → 1.1.8

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.
@@ -48,8 +48,8 @@ export const EXTERNAL = {
48
48
  },
49
49
  rag: {
50
50
  package: '@zenera/rag',
51
- summary: 'Search an openapi/swagger document as a graph.',
52
- usage: 'zen rag schema <index|search|list|grep|show|stats> [spec...]',
51
+ summary: 'Retrieval over a corpus: index it, then ask it something.',
52
+ usage: 'zen rag <subject> <command> [args...]',
53
53
  install: 'npm i -g @zenera/rag',
54
54
  banner: { head: 'Zenera', accent: 'Rag', subtitle: 'Api Retrieval' },
55
55
  },
package/dist/podman.d.ts CHANGED
@@ -34,15 +34,6 @@ export interface PodmanStatus {
34
34
  imagePresent?: boolean;
35
35
  }
36
36
  export declare function ensurePodmanReady(opts?: PodmanOptions): Promise<void>;
37
- /**
38
- * Builds the project's Dockerfile under its tag.
39
- *
40
- * Run every time rather than skipped when the tag already exists, because the
41
- * tag is a hash of the Dockerfile and its context and the engine's layer cache
42
- * is a hash of the same thing plus the base image. Asking it is cheap, and it
43
- * is the only thing that notices when `FROM node:24` starts meaning a different
44
- * node:24.
45
- */
46
37
  /**
47
38
  * A build that ran and failed, rather than a host that could not be asked.
48
39
  *
package/dist/podman.js CHANGED
@@ -167,15 +167,6 @@ function parseMachines(stdout) {
167
167
  // ---------------------------------------------------------------------------
168
168
  // The image
169
169
  // ---------------------------------------------------------------------------
170
- /**
171
- * Builds the project's Dockerfile under its tag.
172
- *
173
- * Run every time rather than skipped when the tag already exists, because the
174
- * tag is a hash of the Dockerfile and its context and the engine's layer cache
175
- * is a hash of the same thing plus the base image. Asking it is cheap, and it
176
- * is the only thing that notices when `FROM node:24` starts meaning a different
177
- * node:24.
178
- */
179
170
  /**
180
171
  * A build that ran and failed, rather than a host that could not be asked.
181
172
  *
@@ -204,7 +195,9 @@ async function build(engine, spec, run, force = false) {
204
195
  return;
205
196
  }
206
197
  }
207
- note(dim(`building ${spec.tag} from ${spec.dockerfile}`));
198
+ note(dim(`${force ? 'rebuilding' : 'building'} the sandbox image from ${spec.dockerfile}`));
199
+ note(dim(' the agent runs its commands inside a container, so the image is needed first'));
200
+ note(dim(' this takes a few minutes; later runs reuse it until the Dockerfile changes'));
208
201
  const built = await stream(engine, ['build', '--tag', spec.tag, '--file', spec.dockerfile, spec.context], run, BUILD_MS);
209
202
  if (built.code !== 0) {
210
203
  throw new BuildError(`could not build ${spec.dockerfile}`, EXIT.sandbox, last(built) || 'run the build by hand to see what the engine says');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zenera/cli",
3
- "version": "1.1.5",
3
+ "version": "1.1.8",
4
4
  "description": "Command-line front end for @zenera/neo: agentic projects you can run, share and commit.",
5
5
  "keywords": [
6
6
  "agents",
@@ -51,7 +51,7 @@
51
51
  "@inkjs/ui": "^2.0.0",
52
52
  "ink": "^7.1.1",
53
53
  "react": "^19.2.8",
54
- "@zenera/neo": "^1.1.5",
54
+ "@zenera/neo": "^1.1.8",
55
55
  "@anthropic-ai/sdk": "^0.120.0",
56
56
  "@google/genai": "^2.18.0",
57
57
  "@openrouter/sdk": "^1.2.80",
@@ -1,13 +1,13 @@
1
1
  ---
2
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, and giving it to an agent as tools.
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.
4
4
  ---
5
5
 
6
6
  # The schema index
7
7
 
8
8
  A schema index is an OpenAPI/Swagger description turned into something that can
9
9
  be **asked a question**. It is built once, on disk, and answered from without a
10
- model: `zen rag schema index` writes it, and five commands read it.
10
+ model: `zen rag schema index` writes it, and six commands read it.
11
11
 
12
12
  It exists because a real specification does not fit in a context window, and
13
13
  grepping it does not help. The parts that answer "how do I reset a password?"
@@ -18,7 +18,7 @@ word "password" appears in forty places that are not the one you want.
18
18
  ## The commands
19
19
 
20
20
  ```
21
- zen rag schema <index|search|list|grep|show|stats> [spec...]
21
+ zen rag schema <index|search|list|grep|trace|show|stats> [spec...]
22
22
  ```
23
23
 
24
24
  | Command | Answers | Embedder? | Typical |
@@ -27,21 +27,34 @@ zen rag schema <index|search|list|grep|show|stats> [spec...]
27
27
  | `search` | _what is this API's way to do X?_ | **yes** | seconds |
28
28
  | `list` | _what methods/types/fields are there?_ | no | instant |
29
29
  | `grep` | _does the string X appear anywhere?_ | no | instant |
30
+ | `trace` | _which call can reach this field?_ | no | instant |
30
31
  | `show` | _print exactly these things_ | no | instant |
31
32
  | `stats` | _what is in this index?_ | no | instant |
32
33
 
33
34
  Only `search` ranks, and only `search` costs a network round trip — it embeds
34
- the query before it can compare anything. The other four read `graph.json` off
35
+ the query before it can compare anything. The other five read `graph.json` off
35
36
  the disk and answer in milliseconds, so reach for `search` when the question is
36
- vague and for `list`/`grep` when it is precise. If a search feels slow, it is
37
- that one embedding call, not the index: near-zero CPU for several seconds is
38
- the tell.
37
+ vague and for `list`/`grep`/`trace` when it is precise. If a search feels slow,
38
+ it is that one embedding call, not the index: near-zero CPU for several seconds
39
+ is the tell.
39
40
 
40
41
  > **`search` takes bare words as the query, not as a subcommand.**
41
42
  > `zen rag schema search list methods` does not list anything — it runs a
42
43
  > semantic search for the phrase _"list methods"_ and returns ten ranked
43
44
  > guesses. The listing command is `zen rag schema list methods`.
44
45
 
46
+ > **Never reach for shell `grep`, `rg`, `find`, `cat` or `jq` here.**
47
+ > Not on the specification, not on `graph.json`, not on anything under the
48
+ > index directory. `zen rag schema grep` and `zen rag schema list` are the
49
+ > exact-matching commands, they are local, they need no credential, and they
50
+ > answer in milliseconds. Shell tools on the same files are strictly worse:
51
+ > they match raw YAML/JSON lines rather than nodes, so they cannot tell a
52
+ > field from a `$ref` from a description, cannot say which operation a hit
53
+ > belongs to, cannot filter by kind or direction, and they miss every name
54
+ > the index normalised. A `grep -r password openapi.yaml` returns forty lines
55
+ > of text; `zen rag schema grep password` returns the nodes, with their ids,
56
+ > ready to hand back to `show`.
57
+
45
58
  ## Why it is a graph and not a search box
46
59
 
47
60
  Two structures, kept together, because neither answers alone:
@@ -124,8 +137,9 @@ zen rag schema index <spec...> [--embedding <ref>] [-o <dir>] [--batch <n>]
124
137
  | Flag | Default | Meaning |
125
138
  | ------------------- | ------------- | ------------------------------------------------------------ |
126
139
  | `--embedding <ref>` | — | Which embedder makes the vectors. Omit it to see the choices |
127
- | `-o`, `--out <dir>` | `./schema-db` | Where the index goes |
140
+ | `-o`, `--out <dir>` | `./schema-db` | Where the index goes; `$ZEN_SCHEMA_DB` if that is set |
128
141
  | `--batch <n>` | `96` | Texts per embedding request, and how often progress prints |
142
+ | `--no-sources` | — | Keep no copy of the documents; `show --source` rebuilds them |
129
143
  | `--quiet` | — | No narration |
130
144
 
131
145
  ```sh
@@ -149,6 +163,27 @@ a real environment variable always wins.
149
163
  Rebuild the index when the specification changes. Nothing watches it, and a
150
164
  stale index is a confident wrong answer.
151
165
 
166
+ ## Which index gets read
167
+
168
+ Every reading command takes `-d`, `--dir`. Without one:
169
+
170
+ 1. `$ZEN_SCHEMA_DB`, if it is set. **Set this once** instead of typing `-d` on
171
+ every command — `export ZEN_SCHEMA_DB=/assets/schema-db`.
172
+ 2. Otherwise the **nearest index** to the working directory: here, then a short
173
+ way down into it, then up a level and again, stopping at your home
174
+ directory. The one chosen is named on stderr as it is used, so an answer is
175
+ never anonymous.
176
+ 3. Otherwise `./schema-db`, which is only so the error names the directory you
177
+ were expecting.
178
+
179
+ What is looked for is a `manifest.json` — an index is self-describing, so
180
+ nothing searches for a directory _called_ `schema-db` and one called anything
181
+ else is found the same way. `schema-db` is just the name a new one is given.
182
+
183
+ Two indexes the same distance away is refused rather than guessed at: the wrong
184
+ index does not fail, it answers confidently about a different API. Name one
185
+ with `-d`, or set `ZEN_SCHEMA_DB`.
186
+
152
187
  ## Searching it
153
188
 
154
189
  ```
@@ -181,23 +216,24 @@ response field in `--output-property`, an action in `--method`.
181
216
 
182
217
  ### Shaping the answer
183
218
 
184
- | Flag | Default | Meaning |
185
- | --------------------------- | ------------- | ------------------------------------------------------- |
186
- | `-d`, `--dir <dir>` | `./schema-db` | Which index |
187
- | `--embedding <ref>` | the index's | Must be the one the index was built with |
188
- | `--direction <d>` | `any` | `input`, `output` or `any` |
189
- | `--method-type <t>` | `any` | `read_only` (GET/HEAD/OPTIONS) or `read_write` |
190
- | `--exclude-id <id>` | — | Drop a node. Repeatable |
191
- | `--exclude-method <name>` | — | Drop an operation by name. Repeatable |
192
- | `--exclude-type <name>` | — | Drop a schema by name. Repeatable |
193
- | `--exclude-property <name>` | — | Drop a field by name. Repeatable |
194
- | `--limit <n>` | `5` | Seeds kept per term |
195
- | `--max-hops <n>` | `3` | How far apart two hits may be and still join |
196
- | `--max-nodes <n>` | `200` | Nodes per result |
197
- | `--format <f>` | `text` | `text`, `mermaid`, `mermaid-flowchart`, `ts`, `openapi` |
198
- | `--no-docs` | — | Leave the descriptions out |
199
- | `--interactive` | — | Prompt, search, refine. Needs a terminal |
200
- | `--quiet` | — | No narration |
219
+ | Flag | Default | Meaning |
220
+ | --------------------------- | ----------- | ------------------------------------------------------- |
221
+ | `-d`, `--dir <dir>` | found | Which index — see "Which index gets read" |
222
+ | `--embedding <ref>` | the index's | Must be the one the index was built with |
223
+ | `--direction <d>` | `any` | `input`, `output` or `any` |
224
+ | `--method-type <t>` | `any` | `read_only` (GET/HEAD/OPTIONS) or `read_write` |
225
+ | `--exclude-id <id>` | — | Drop a node. Repeatable |
226
+ | `--exclude-method <name>` | — | Drop an operation by name. Repeatable |
227
+ | `--exclude-type <name>` | — | Drop a schema by name. Repeatable |
228
+ | `--exclude-property <name>` | — | Drop a field by name. Repeatable |
229
+ | `--limit <n>` | `5` | Seeds kept per term |
230
+ | `--max-hops <n>` | `3` | How far apart two hits may be and still join |
231
+ | `--max-nodes <n>` | `200` | Nodes per result |
232
+ | `--format <f>` | `text` | `text`, `mermaid`, `mermaid-flowchart`, `ts`, `openapi` |
233
+ | `--show-source` | — | Tag each operation and schema with its document |
234
+ | `--no-docs` | — | Leave the descriptions out |
235
+ | `--interactive` | — | Prompt, search, refine. Needs a terminal |
236
+ | `--quiet` | — | No narration |
201
237
 
202
238
  ```sh
203
239
  zen rag schema search --method "reset a user password" --format ts
@@ -281,6 +317,8 @@ think it is?".
281
317
  | The question | The command |
282
318
  | ------------------------------------------------ | ---------------------------------------- |
283
319
  | "how do I reset a password with this API?" | `search --method "reset a password"` |
320
+ | _anything you would have run `grep` for_ | `grep` / `list` — never the shell |
321
+ | "which call can reach this field?" | `trace <field>` |
284
322
  | "what does the create-user request look like?" | `search --input-type "create user"` |
285
323
  | "what operations exist under /users?" | `list methods --path "*/users*"` |
286
324
  | "how many operations are there at all?" | `list methods` (or `--json` for `found`) |
@@ -290,9 +328,9 @@ think it is?".
290
328
  | "is this index the right one?" | `stats` |
291
329
 
292
330
  The rule: **a question about meaning is a `search`; a question about presence,
293
- count or spelling is a `list` or a `grep`.** Search cannot answer the second
294
- kind, because a ranking always returns its best guesses whether or not any of
295
- them are right.
331
+ count or spelling is a `list` or a `grep`; a question about reachability is a
332
+ `trace`.** Search cannot answer the last two, because a ranking always returns
333
+ its best guesses whether or not any of them are right.
296
334
 
297
335
  ### Exact matching, when the question is whether something exists
298
336
 
@@ -307,15 +345,16 @@ zen rag schema grep <pattern> [-d <dir>] [filters…]
307
345
 
308
346
  | Flag | For | Meaning |
309
347
  | ------------------- | ------ | ---------------------------------------------- |
310
- | `--name <p>` | `list` | Match the name. Repeatable |
311
- | `--path <p>` | `list` | Match the route (methods). Repeatable |
348
+ | `--name <p>` | both | Match the name. Repeatable |
349
+ | `--path <p>` | both | Match the route it sits on. Repeatable |
350
+ | `--regex` | both | Read the patterns as regular expressions |
351
+ | `--case-sensitive` | both | Stop ignoring case |
352
+ | `--source <name>` | both | Only one document, as `stats` names it |
353
+ | `--show-source` | both | Print which document each row came from |
312
354
  | `--method-type <t>` | `list` | `read_only`, `read_write` or `any` |
313
355
  | `--direction <d>` | `list` | `input`, `output` or `any` |
314
- | `--regex` | `grep` | Read the pattern as a regular expression |
315
- | `--case-sensitive` | `grep` | Stop ignoring case |
316
356
  | `--kind <k>` | `grep` | `method`, `type` or `property`. Repeatable |
317
357
  | `--ids-only` | `grep` | Bare ids, one per line, for piping |
318
- | `--source <name>` | both | Only one document, as `stats` names it |
319
358
  | `--limit <n>` | both | Print at most n; `found` still counts them all |
320
359
  | `--json` | both | `{found, truncated, rows}` / `…, matches}` |
321
360
  | `--quiet` | both | No narration |
@@ -330,6 +369,7 @@ zen rag schema list properties --name password # every field so named
330
369
  zen rag schema grep password # every literal occurrence
331
370
  zen rag schema grep "pass(word|phrase)" --regex
332
371
  zen rag schema grep password --kind type --ids-only
372
+ zen rag schema grep status --path "/invoices/*" # the word, in one corner
333
373
  ```
334
374
 
335
375
  `list` walks one kind of node and matches its structured fields; `grep` matches
@@ -338,6 +378,27 @@ so the two agree on what the API says. A pattern with `*` or `?` is a glob
338
378
  matched against the whole string; a plain word is a substring, so `--name
339
379
  password` finds `ResetPasswordPayload` and `--name "Password*"` finds nothing.
340
380
 
381
+ **`--regex` is the only way to say "one of these"** — a glob has no alternation.
382
+ On `list` it turns every pattern into a regular expression. On `grep` it turns
383
+ the **pattern** into one; `--name` and `--path` stay globs-or-substrings there,
384
+ because they are always names:
385
+
386
+ ```sh
387
+ zen rag schema list methods --regex --path "^/(users|teams)/"
388
+ zen rag schema list types --regex --name "(Request|Response)$"
389
+ zen rag schema grep "pass(word|phrase)" --regex --path "*/users*"
390
+ ```
391
+
392
+ `--name` and `--path` are constraints on `grep` as well, which is what makes a
393
+ common word usable: `grep status` across a whole API is unreadable,
394
+ `grep status --path "/invoices/*" --kind property` is an answer. On `list
395
+ properties` and on `grep`, `--path` matches the route a parameter's operation
396
+ sits on; a schema belongs to no one route, so `--path` never selects one.
397
+
398
+ When an index holds several documents, `--show-source` puts
399
+ `[source: billing_api_v2]` on every row, so which document answered does not
400
+ have to be recovered from `--json`.
401
+
341
402
  Both report `found` as the true total even when `--limit` shortens what is
342
403
  printed, so a cut answer never misreports how much there is. Nothing matching
343
404
  exits 0 with empty stdout — and that emptiness is trustworthy, which is the
@@ -349,23 +410,116 @@ whole point of them.
349
410
  zen rag schema grep token --ids-only | xargs zen rag schema show --format ts
350
411
  ```
351
412
 
413
+ ### Upwards, from a field to the calls that carry it
414
+
415
+ Finding the field is half the job. The other half — which operation can
416
+ actually reach it — is a walk up the `$ref`s.
417
+
418
+ `search` does part of it: it stitches its seeds into one connected piece and
419
+ prints what each operation accepts and returns, so a lucky search does show the
420
+ call. But it joins only what **ranked**, only within `--max-hops` (3 by
421
+ default), and it never names the chain — and the call almost never repeats the
422
+ word, so `GET /users/{userId}` and `city` have nothing in common except the
423
+ edges between them. `trace` follows those edges instead of guessing at them:
424
+ exhaustive, and certain.
425
+
426
+ ```
427
+ zen rag schema trace <pattern|id...> [-d <dir>] [filters…]
428
+ ```
429
+
430
+ ```sh
431
+ zen rag schema trace city
432
+ ```
433
+
434
+ ```
435
+ Property:Address.city
436
+ GET /users/{userId} getUser output PublicUserProfile.address → Address.city
437
+ ```
438
+
439
+ One command instead of three lookups and a guess: find the field, find what
440
+ holds `Address`, find what accepts _that_, and hope you followed every branch.
441
+ The last column is the whole route, so the shape of the call can be read off
442
+ the answer.
443
+
444
+ | Flag | Default | Meaning |
445
+ | ----------------- | ------------------ | ---------------------------------------------------- |
446
+ | `--kind <k>` | types + properties | `method`, `type` or `property`. Repeatable |
447
+ | `--direction <d>` | `any` | Only the calls that accept it, or that return it |
448
+ | `--max-hops <n>` | `8` | How far up to walk |
449
+ | `--limit <n>` | — | Trace at most n matching nodes |
450
+ | `--routes <n>` | — | Operations printed per node; `found` counts them all |
451
+ | `--ids-only` | — | Bare operation ids, one per line, for piping |
452
+ | `--regex` | — | Read the pattern as a regex; `--case-sensitive` too |
453
+ | `--source <name>` | — | Only nodes from one document |
454
+ | `--show-source` | — | Print which document each operation came from |
455
+
456
+ A bare word matches the way `list --name` does — a substring, or a glob when it
457
+ has `*` or `?`. A node id (`Type:User`) is taken as that node rather than as a
458
+ pattern. Operations are left out of a name match on purpose: they are where a
459
+ trace ends, not where one starts.
460
+
461
+ ```sh
462
+ zen rag schema trace password --direction input
463
+ zen rag schema trace "*Settings" --kind type
464
+ zen rag schema trace mfa_secret --ids-only | xargs zen rag schema show --format openapi
465
+ ```
466
+
467
+ `no operation reaches it` is a real answer, and one worth having: the schema is
468
+ unreachable in this document, so no request will ever carry it.
469
+
470
+ ### Instead of the shell
471
+
472
+ Every reflex that reaches for a shell tool has a command here that answers the
473
+ same question better. Add `-d <dir>` when the index is not the nearest one, or
474
+ name it once with `ZEN_SCHEMA_DB`.
475
+
476
+ | The reflex | The command |
477
+ | --------------------------------------- | ----------------------------------------------------- |
478
+ | `grep -ri password spec.yaml` | `zen rag schema grep password` |
479
+ | `grep -r password` \| _only in schemas_ | `zen rag schema grep password --kind type` |
480
+ | `grep -E "pass(word\|phrase)"` | `zen rag schema grep "pass(word\|phrase)" --regex` |
481
+ | `grep password` (case matters) | `zen rag schema grep password --case-sensitive` |
482
+ | `grep -c` / `wc -l` | `--json`, and read `found` — it counts past `--limit` |
483
+ | `grep -l` / `grep -o` for piping | `zen rag schema grep password --ids-only` |
484
+ | `grep "/users" spec.yaml` | `zen rag schema list methods --path "*/users*"` |
485
+ | `grep -i "updateuser"` | `zen rag schema list methods --name "*Update*"` |
486
+ | `grep "UserSettings"` | `zen rag schema list types --name "*UserSettings*"` |
487
+ | `grep -A5 password` for the field | `zen rag schema list properties --name "*password*"` |
488
+ | `grep -rl password specs/` (which one?) | `zen rag schema grep password --show-source` |
489
+ | `cat`/`yq` a schema out of the document | `zen rag schema show --type UserSettings --format ts` |
490
+ | `ls` the index directory | `zen rag schema stats` |
491
+
492
+ ```sh
493
+ export ZEN_SCHEMA_DB=/assets/schema-db # once, then never again
494
+ zen rag schema grep "password" --limit 10
495
+ zen rag schema list methods --path "*user*"
496
+ zen rag schema list types --name "*UserSettings*"
497
+ zen rag schema list properties --name "*password*" --show-source
498
+ ```
499
+
500
+ If none of these fits the question, the question is about meaning, and the
501
+ answer is `search` — still not the shell.
502
+
352
503
  ### Naming what you want in `show`
353
504
 
354
505
  ```sh
355
506
  zen rag schema show --method GetCurrentUserInfo --format openapi --exact
356
507
  zen rag schema show --type "*Invoice*" --format ts
357
508
  zen rag schema show --source billing-api --format openapi
509
+ zen rag schema show --type "*Invoice*" --show-source
358
510
  ```
359
511
 
360
512
  Ids are one way in, but `--method` and `--type` take the names you already
361
513
  have. A bare name means exactly that name; add `*` to take more than one.
362
- `--exact` prints only what was named instead of the neighbourhood around it,
363
- which with `--format openapi` gives a valid self-contained slice of the
364
- specification enough to generate a client or a mock payload from.
514
+ `--show-source` names the document each node came from, which is the quick way
515
+ to tell two versions of the same API apart. `--exact` prints only what was
516
+ named instead of the neighbourhood around it, which with `--format openapi`
517
+ gives a valid self-contained slice of the specification — enough to generate a
518
+ client or a mock payload from.
365
519
 
366
520
  ## Giving it to an agent
367
521
 
368
- The same engine, as five tools in the group `schema`. An agent takes them all
522
+ The same engine, as six tools in the group `schema`. An agent takes them all
369
523
  with `schema:*` in its `tools:`.
370
524
 
371
525
  ```ts
@@ -386,18 +540,103 @@ const project = await loadProject('./my-project', { tools: schemaTools(index) })
386
540
  | `find_types_with_property` | every schema with a field of this name — exact lookup, no searching |
387
541
  | `list_api` | methods, types or fields by name — complete, and counted in full |
388
542
  | `grep_api` | every literal occurrence of a string — the way to prove absence |
543
+ | `trace_api` | up from a field or schema to the operations that carry it |
389
544
 
390
- Only `search_api` ranks; the other four are exact. `find_types_with_property`
545
+ Only `search_api` ranks; the other five are exact. `find_types_with_property`
391
546
  is the one for the repair loop. When `tsc` says `'password' does not exist in
392
547
  type 'PublicUserProfile'`, the model does not need the word explained again —
393
548
  it needs the list of types that _do_ have one, and embedding the word will only
394
549
  rank the guess it already made near the top. `grep_api` is the same instinct
395
550
  widened: it is how a model checks that a search returning nothing really means
396
- there is nothing.
551
+ there is nothing. `trace_api` is the step after either of them: a field is of
552
+ no use until the call that carries it is known, and no ranking will find that
553
+ call — the operation and the field share no words, only edges.
554
+
555
+ `list_api` and `grep_api` take `name`, `path`, `regex` and `source`, so a
556
+ common word can be narrowed to one route or one document rather than read out
557
+ in full. When an index holds more than one document, both tools name the source
558
+ of every row without being asked — with two versions of the same API indexed
559
+ together, which one answered is part of the answer.
397
560
 
398
561
  Tell the agent in its prompt to search before it writes a call, and to put the
399
562
  intent in the narrow field. A model left to itself puts everything in `all`.
400
563
 
564
+ ## Wiring it into a project means writing the project a skill
565
+
566
+ **Whenever an index is used by a Zenera project — as `schema:*` tools, or as
567
+ `zen rag` reachable from the agent's sandbox — write a skill for it in that
568
+ project.** Not optional, and not the same thing as passing the tools in.
569
+
570
+ Wiring alone leaves the model to infer everything that matters. A tool
571
+ description says what `grep_api` does; it cannot say that this index holds the
572
+ NSX policy API, that names are `snake_case`, that every route is under
573
+ `/policy/api/v1`, or that `list_api` is the right first move here because the
574
+ API has three hundred operations and search will hand back five. That is
575
+ project knowledge, and project knowledge belongs in a skill — where it is
576
+ loaded only when the model is actually working on this API, instead of sitting
577
+ in the system prompt of every run.
578
+
579
+ ```
580
+ <project>/agents/skills/<api>-api/SKILL.md
581
+ ```
582
+
583
+ Frontmatter is `name` and `description`; the description is what the model
584
+ reads when choosing, so it must name the API and the questions it answers.
585
+ Add `tools: [search_api, list_api, grep_api, trace_api, describe_types,
586
+ find_types_with_property]` if the skill should be what unlocks them.
587
+
588
+ ### What the skill has to say
589
+
590
+ | Section | Because |
591
+ | --------------- | ------------------------------------------------------------------------------------------------------------------ |
592
+ | Which API | The name, the version, and the document it was built from |
593
+ | Where the index | `ZEN_SCHEMA_DB`, or the `-d` to pass — an agent cannot guess a path |
594
+ | Which command | Meaning → `search`; presence, spelling or a count → `list`/`grep`; which endpoint carries a field → `trace` |
595
+ | Never the shell | State it outright. `grep`/`rg`/`jq` on the spec is the default reflex |
596
+ | The conventions | Auth, base path, pagination, casing, error envelope — none of it is in the graph |
597
+ | Worked examples | Two or three, with **real operation and schema names from this index** |
598
+ | The repair loop | Compiler said the field is not on the type → `find_types_with_property`, then `trace` for the call that carries it |
599
+
600
+ Best practice, in order of how often it is got wrong:
601
+
602
+ 1. **Use real names.** `list_api types --name "*Policy*"` with output the model
603
+ will actually see beats a generic `<TypeName>` placeholder, because the
604
+ names are the anchors it steers by.
605
+ 2. **Say which command answers which question**, and say that a ranking cannot
606
+ prove absence. Left alone a model searches for everything, gets five ranked
607
+ guesses, and writes a call against the best of them.
608
+ 3. **Keep it short.** A skill is prompt. One screen of routing rules and
609
+ conventions beats a transcription of this document — link to `zen rag
610
+ schema --help` for the flags.
611
+ 4. **Re-index, then re-read the skill.** Both go stale against the same
612
+ change, and a skill quoting operations that no longer exist is worse than
613
+ none.
614
+ 5. **One skill per API**, named after it. Two APIs in one skill and the model
615
+ mixes their conventions.
616
+
617
+ ```md
618
+ ---
619
+ name: billing-api
620
+ description: How to find the right call in the Acme Billing API (v2) — which schema
621
+ carries which field, and which endpoint accepts it. Use before writing any request.
622
+ ---
623
+
624
+ # The Billing API
625
+
626
+ Indexed at `/assets/schema-db` (already in `$ZEN_SCHEMA_DB`). 214 operations,
627
+ all under `/v2`. Bearer token in `Authorization`; cursors, never page numbers.
628
+
629
+ - Vague question ("how do I cancel a subscription?") → `search_api`, intent in
630
+ the narrowest field: `methods`, not `all`.
631
+ - Does X exist, how is it spelled, how many are there → `list_api` / `grep_api`.
632
+ These are complete; a search is not, and cannot prove absence.
633
+ - Which endpoint carries this field → `trace_api`. Never guess the owner.
634
+ - Never `grep`/`rg`/`jq` the spec — the tools above are local and exact.
635
+
636
+ Worked: the invoice total is `Invoice.amount_due` (minor units), returned by
637
+ `GetInvoice` and `ListInvoices`; `trace_api of: amount_due` shows both.
638
+ ```
639
+
401
640
  ## When it goes wrong
402
641
 
403
642
  | Symptom | Cause |
@@ -28,7 +28,7 @@ zen sandbox [status|up|pull|clean|disk] [options]
28
28
  zen version
29
29
 
30
30
  zen faker <serve|build|cache> [spec...]
31
- zen rag schema <index|search|list|grep|show|stats> [spec...]
31
+ zen rag schema <index|search|list|grep|trace|show|stats> [spec...]
32
32
  ```
33
33
 
34
34
  ## Read the reference before answering
@@ -1,7 +1,7 @@
1
1
  # API search — `zen rag`
2
2
 
3
3
  ```
4
- zen rag schema <index|search|list|grep|show|stats> [spec...]
4
+ zen rag schema <index|search|list|grep|trace|show|stats> [spec...]
5
5
  ```
6
6
 
7
7
  Provided by `@zenera/rag` — `npm i -g @zenera/rag` if `zen rag` says it is not
@@ -25,8 +25,10 @@ zen rag schema index <spec...> [--embedding <ref>] [-o <dir>] [--batch <n>]
25
25
  | Flag | Default | Meaning |
26
26
  | ------------------- | ------------- | ------------------------------------------------------------ |
27
27
  | `--embedding <ref>` | — | Which embedder makes the vectors. Omit it to see the choices |
28
- | `-o`, `--out <dir>` | `./schema-db` | Where the index goes |
28
+ | `-o`, `--out <dir>` | `./schema-db` | Where the index goes; `$ZEN_SCHEMA_DB` if set |
29
29
  | `--batch <n>` | `96` | Texts per embedding request, and how often progress prints |
30
+ | `--no-sources` | — | Keep no copy of the documents in the index |
31
+ | `--quiet` | — | No narration |
30
32
 
31
33
  ```
32
34
  zen rag schema index openapi.yaml --embedding openai:text-embedding-3-small
@@ -49,6 +51,21 @@ lance/ the vector and full-text indexes
49
51
  The manifest records the embedding ref **and** the embedder's own id, so a
50
52
  search with a different model is refused rather than quietly returning nonsense.
51
53
 
54
+ ## Which index gets read
55
+
56
+ Every reading command takes `-d`, `--dir`. Without one:
57
+
58
+ 1. `$ZEN_SCHEMA_DB`, if it is set.
59
+ 2. Otherwise the **nearest index** to the working directory — here, then a
60
+ short way down, then up a level and again, stopping at your home directory.
61
+ The one used is named on stderr.
62
+ 3. Otherwise `./schema-db`, so the error names the directory you expected.
63
+
64
+ What is looked for is a `manifest.json`, not a directory called `schema-db`, so
65
+ an index called anything else is found the same way. Two the same distance away
66
+ is refused rather than guessed at — name one with `-d`, or set `ZEN_SCHEMA_DB`
67
+ once and stop typing it.
68
+
52
69
  ## `search`
53
70
 
54
71
  ```
@@ -84,23 +101,24 @@ search good. A request field belongs in `--input-property`, a response field in
84
101
 
85
102
  ### Filters and shape
86
103
 
87
- | Flag | Default | Meaning |
88
- | --------------------------- | ------------- | ------------------------------------------------------- |
89
- | `-d`, `--dir <dir>` | `./schema-db` | Which index |
90
- | `--embedding <ref>` | the index's | Must be the one the index was built with |
91
- | `--direction <d>` | `any` | `input`, `output` or `any` |
92
- | `--method-type <t>` | `any` | `read_only`, `read_write` or `any` |
93
- | `--exclude-id <id>` | — | Drop a node. Repeatable |
94
- | `--exclude-method <name>` | — | Drop an operation by name. Repeatable |
95
- | `--exclude-type <name>` | — | Drop a schema by name. Repeatable |
96
- | `--exclude-property <name>` | — | Drop a field by name. Repeatable |
97
- | `--limit <n>` | `5` | Seeds kept per term |
98
- | `--max-hops <n>` | `3` | How far apart two hits may be |
99
- | `--max-nodes <n>` | `200` | Nodes per result |
100
- | `--format <f>` | `text` | `text`, `mermaid`, `mermaid-flowchart`, `ts`, `openapi` |
101
- | `--no-docs` | — | Leave the descriptions out |
102
- | `--interactive` | — | Prompt, search, refine. Needs a terminal |
103
- | `--quiet` | — | No narration |
104
+ | Flag | Default | Meaning |
105
+ | --------------------------- | ----------- | ------------------------------------------------------- |
106
+ | `-d`, `--dir <dir>` | found | Which index — see "Which index gets read" |
107
+ | `--embedding <ref>` | the index's | Must be the one the index was built with |
108
+ | `--direction <d>` | `any` | `input`, `output` or `any` |
109
+ | `--method-type <t>` | `any` | `read_only`, `read_write` or `any` |
110
+ | `--exclude-id <id>` | — | Drop a node. Repeatable |
111
+ | `--exclude-method <name>` | — | Drop an operation by name. Repeatable |
112
+ | `--exclude-type <name>` | — | Drop a schema by name. Repeatable |
113
+ | `--exclude-property <name>` | — | Drop a field by name. Repeatable |
114
+ | `--limit <n>` | `5` | Seeds kept per term |
115
+ | `--max-hops <n>` | `3` | How far apart two hits may be |
116
+ | `--max-nodes <n>` | `200` | Nodes per result |
117
+ | `--format <f>` | `text` | `text`, `mermaid`, `mermaid-flowchart`, `ts`, `openapi` |
118
+ | `--show-source` | — | Tag each operation and schema with its document |
119
+ | `--no-docs` | — | Leave the descriptions out |
120
+ | `--interactive` | — | Prompt, search, refine. Needs a terminal |
121
+ | `--quiet` | — | No narration |
104
122
 
105
123
  ```
106
124
  zen rag schema search --method "reset a user password" --format ts
@@ -135,7 +153,7 @@ quit
135
153
 
136
154
  ```
137
155
  zen rag schema list <methods|types|properties> [-d <dir>] [--name <p>] [--path <p>]
138
- zen rag schema grep <pattern> [-d <dir>] [--regex] [--kind <k>] [--ids-only]
156
+ zen rag schema grep <pattern> [-d <dir>] [--name <p>] [--path <p>] [--kind <k>]
139
157
  ```
140
158
 
141
159
  Exact, and therefore complete. `search` ranks, so it can only hand back the top
@@ -144,26 +162,44 @@ read `graph.json` directly, with no embedder, no credential and no network.
144
162
 
145
163
  | Flag | For | Meaning |
146
164
  | ------------------- | ------ | -------------------------------------------- |
147
- | `--name <p>` | `list` | Match the name. Repeatable |
148
- | `--path <p>` | `list` | Match the route. Repeatable |
165
+ | `--name <p>` | both | Match the name. Repeatable |
166
+ | `--path <p>` | both | Match the route it sits on. Repeatable |
167
+ | `--regex` | both | Read the patterns as regular expressions |
168
+ | `--case-sensitive` | both | Stop ignoring case |
149
169
  | `--source <name>` | both | Only nodes from one document |
170
+ | `--show-source` | both | Print which document each row came from |
150
171
  | `--method-type <t>` | `list` | `read_only`, `read_write` or `any` |
151
172
  | `--direction <d>` | `list` | `input`, `output` or `any` |
152
- | `--regex` | `grep` | Read the pattern as a regular expression |
153
- | `--case-sensitive` | `grep` | Stop ignoring case |
154
173
  | `--kind <k>` | `grep` | `method`, `type` or `property`. Repeatable |
155
174
  | `--ids-only` | `grep` | Just the ids, one per line, for piping |
156
175
  | `--limit <n>` | both | Rows to print. `found` still counts them all |
176
+ | `--quiet` | both | No narration |
177
+
178
+ Under `--json`: `{found, truncated, rows}` from `list`, `{found, truncated,
179
+ matches}` from `grep`. `grep` takes **one** pattern — quote it if it has
180
+ spaces — and `list` **one** subject.
157
181
 
158
182
  A pattern with `*` or `?` is a glob matched against the whole string; a plain
159
183
  word is a substring. So `--name password` finds `ResetPasswordPayload`, and
160
- `--name "Password*"` finds nothing, because nothing starts with it.
184
+ `--name "Password*"` finds nothing, because nothing starts with it. `--regex`
185
+ makes it a regular expression instead — the only way to say "one of these". On
186
+ `list` it applies to every pattern; on `grep` it applies to the pattern, while
187
+ `--name` and `--path` stay globs-or-substrings:
188
+
189
+ ```
190
+ zen rag schema list methods --regex --path "^/(users|teams)/"
191
+ ```
192
+
193
+ `--path` selects on the route an operation sits on, and on the route a
194
+ parameter's operation sits on. A schema belongs to no one route, so `--path`
195
+ never selects one.
161
196
 
162
197
  ```
163
198
  zen rag schema list methods --path "*/users*"
164
199
  zen rag schema list types --name "*Password*"
165
200
  zen rag schema grep password
166
201
  zen rag schema grep "pass(word|phrase)" --regex
202
+ zen rag schema grep status --path "/invoices/*" --kind property
167
203
  zen rag schema grep token --ids-only | xargs zen rag schema show --format ts
168
204
  ```
169
205
 
@@ -171,11 +207,71 @@ No match exits 0 with nothing on stdout — that is the answer, and unlike an
171
207
  empty search it is a reliable one. Under `--limit`, `found` is still the true
172
208
  total, so a shortened answer never misreports how much there is.
173
209
 
210
+ ## `trace`
211
+
212
+ ```
213
+ zen rag schema trace <pattern|id...> [-d <dir>] [--kind <k>] [--direction <d>]
214
+ [--max-hops <n>] [--limit <n>] [--routes <n>]
215
+ [--ids-only] [--regex] [--case-sensitive] [--source <name>]
216
+ [--show-source]
217
+ ```
218
+
219
+ The question `list` and `grep` leave you holding: you have found the field, so
220
+ **which call can reach it?** `trace` walks up the `$ref`s from every node of
221
+ that name to the operations that accept or return it, and prints the chain in
222
+ between. Also a plain graph read — no embedder, no credential.
223
+
224
+ ```
225
+ zen rag schema trace city
226
+ ```
227
+
228
+ ```
229
+ Property:Address.city
230
+ GET /users/{userId} getUser output PublicUserProfile.address → Address.city
231
+ ```
232
+
233
+ By hand that is three lookups and a guess: find the field, find what holds
234
+ `Address`, find what accepts _that_, and hope you followed every branch.
235
+
236
+ `search` does some of this already — it stitches its seeds into one connected
237
+ piece and prints what each operation accepts and returns. But it links only
238
+ what **ranked**, only within `--max-hops`, and it never names the chain; and
239
+ `getUser` and `city` share no word, so the field has to rank on its own and the
240
+ call has to land near it. `trace` follows the edges instead, which are already
241
+ there and are certain.
242
+
243
+ | Flag | Default | Meaning |
244
+ | ----------------- | ------------------ | ---------------------------------------------------- |
245
+ | `--kind <k>` | types + properties | `method`, `type` or `property`. Repeatable |
246
+ | `--direction <d>` | `any` | Keep only the calls that accept it, or return it |
247
+ | `--max-hops <n>` | `8` | How far up to walk |
248
+ | `--limit <n>` | — | Trace at most n matching nodes |
249
+ | `--routes <n>` | — | Operations printed per node; `found` counts them all |
250
+ | `--ids-only` | — | Bare operation ids, one per line, for piping |
251
+ | `--regex` | — | Read the pattern as a regex; `--case-sensitive` too |
252
+ | `--source <name>` | — | Only nodes from one document |
253
+ | `--show-source` | — | Print which document each operation came from |
254
+
255
+ A bare word is matched the way `list --name` matches it — substring, or a glob
256
+ when it has `*` or `?`. A node id (`Type:User`) is taken as that node and not
257
+ as a pattern. Operations are left out of a name match on purpose: they are
258
+ where a trace ends.
259
+
260
+ ```
261
+ zen rag schema trace password --direction input
262
+ zen rag schema trace "*Settings" --kind type
263
+ zen rag schema trace mfa_secret --ids-only | xargs zen rag schema show --format openapi
264
+ ```
265
+
266
+ A node nothing carries prints `no operation reaches it` — a real answer, and
267
+ one worth having: it means the schema is unreachable in this document.
268
+
174
269
  ## `show`
175
270
 
176
271
  ```
177
- zen rag schema show [id...] [-d <dir>] [--format <f>]
178
- [--method <name>] [--type <name>] [--source <name>] [--exact]
272
+ zen rag schema show [id...] [-d <dir>] [--format <f>] [--exact]
273
+ [--method <name>] [--type <name>] [--source <name>]
274
+ [--show-source] [--max-nodes <n>] [--no-docs] [--quiet]
179
275
  ```
180
276
 
181
277
  Prints named nodes with no search in between. Needs no embedder and no
@@ -183,9 +279,19 @@ credential — it is a read of the graph.
183
279
 
184
280
  Ids are one way in; `--method` and `--type` name things directly, which is
185
281
  usually what you have. A bare name means exactly that name; add `*` to select
186
- more than one. `--source <name>` takes a whole document. `--exact` prints only
187
- what was named instead of the neighbourhood around it with `--format openapi`
188
- that is a valid, self-contained slice of the specification.
282
+ more than one. `--source <name>` takes a whole document, and `--show-source`
283
+ tags each node with the document it came from. `--exact` prints only what was
284
+ named instead of the neighbourhood around it with `--format openapi` that is
285
+ a valid, self-contained slice of the specification. Without it the neighbours
286
+ are stitched in, up to `--max-nodes`.
287
+
288
+ `--source <name> --format openapi` with nothing else named prints the
289
+ **verbatim** document as it was indexed, unless the index was built
290
+ `--no-sources`, in which case it is rebuilt from the graph and says so.
291
+
292
+ A name that matches nothing is an **error**, not an empty answer: naming
293
+ something is a claim that it is there, and `list --name` is the command for
294
+ asking whether it is.
189
295
 
190
296
  ```
191
297
  zen rag schema show --method GetCurrentUserInfo --format openapi --exact
@@ -212,10 +318,12 @@ documents it came from. Also needs no embedder.
212
318
  | `find_types_with_property` | Every schema with a field of this name — exact lookup, no searching |
213
319
  | `list_api` | Methods, types or fields by name — complete, and counted in full |
214
320
  | `grep_api` | Every literal occurrence of a string — the way to prove absence |
321
+ | `trace_api` | Up from a field or schema to the operations that carry it |
215
322
 
216
323
  Only `search_api` ranks. Reach for the others whenever the question is whether
217
324
  something exists, because a search that returns nothing and a thing that is not
218
- there look exactly the same.
325
+ there look exactly the same. `trace_api` is the one for after a field has been
326
+ found and the endpoint to call is what is actually wanted.
219
327
 
220
328
  They share the group `schema`, so an agent takes them with `schema:*` in its
221
329
  `tools:`.