@zenera/rag 1.1.3 → 1.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +68 -9
- package/dist/command.js +290 -32
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/schema/build.d.ts +2 -0
- package/dist/schema/build.js +62 -34
- package/dist/schema/entities.d.ts +2 -0
- package/dist/schema/entities.js +2 -1
- package/dist/schema/files.d.ts +17 -2
- package/dist/schema/files.js +36 -7
- package/dist/schema/lookup.d.ts +44 -0
- package/dist/schema/lookup.js +83 -0
- package/dist/schema/match.d.ts +36 -0
- package/dist/schema/match.js +85 -0
- package/dist/schema/progress.d.ts +26 -0
- package/dist/schema/progress.js +316 -0
- package/dist/schema/spec.d.ts +5 -0
- package/dist/schema/spec.js +34 -10
- package/dist/schema/subgraph.d.ts +6 -0
- package/dist/schema/subgraph.js +27 -0
- package/dist/schema/tools.js +140 -28
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -46,9 +46,27 @@ zen rag schema stats # what is in the index, and what built it
|
|
|
46
46
|
zen rag schema show Type:Invoice # a named node, with no search in between
|
|
47
47
|
```
|
|
48
48
|
|
|
49
|
+
Search ranks, which means it returns the top of a list — useful when the
|
|
50
|
+
question is vague, and no use at all when the question is whether something
|
|
51
|
+
exists. For that there is exact matching, which needs no embedder, no
|
|
52
|
+
credential and no network:
|
|
53
|
+
|
|
54
|
+
```sh
|
|
55
|
+
zen rag schema list methods --path "*/users*" # every route under /users
|
|
56
|
+
zen rag schema list types --name "*Password*" # every schema so named
|
|
57
|
+
zen rag schema grep password # every literal occurrence
|
|
58
|
+
zen rag schema grep "pass(word|phrase)" --regex
|
|
59
|
+
zen rag schema show --method GetCurrentUserInfo --format openapi --exact
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
`list` and `grep` report `found` as the true total even when `--limit` cuts the
|
|
63
|
+
printed rows, so a shortened answer still tells you how much there is. Nothing
|
|
64
|
+
matching exits 0 — an empty answer is an answer, and here it is a trustworthy
|
|
65
|
+
one: if `grep` finds nothing, the word is not in the description.
|
|
66
|
+
|
|
49
67
|
Non-interactive search is a machine interface: every field is a flag, the whole
|
|
50
|
-
query can arrive as one JSON object, `--json`
|
|
51
|
-
required, and an empty result exits 0.
|
|
68
|
+
query can arrive as one JSON object, the `--json` output keeps the same
|
|
69
|
+
structure from run to run, no terminal is required, and an empty result exits 0.
|
|
52
70
|
|
|
53
71
|
```sh
|
|
54
72
|
zen rag schema search --query - --format ts <<'JSON'
|
|
@@ -66,7 +84,9 @@ JSON
|
|
|
66
84
|
```
|
|
67
85
|
zen rag schema index <spec...> Read the documents and write a searchable index.
|
|
68
86
|
zen rag schema search Ask it something. --interactive for a prompt.
|
|
69
|
-
zen rag schema
|
|
87
|
+
zen rag schema list <what> Every method, type or property. No ranking.
|
|
88
|
+
zen rag schema grep <pattern> Every literal match across the whole index.
|
|
89
|
+
zen rag schema show [id...] Print named nodes, with no search in between.
|
|
70
90
|
zen rag schema stats What is in an index, and what built it.
|
|
71
91
|
```
|
|
72
92
|
|
|
@@ -76,6 +96,18 @@ by `--direction`, `--method-type`, `--limit`, `--max-hops`, `--max-nodes` and
|
|
|
76
96
|
the four `--exclude-*` filters, and rendered by `--format text | mermaid |
|
|
77
97
|
mermaid-flowchart | ts | openapi`. `zen help rag` prints the full table.
|
|
78
98
|
|
|
99
|
+
`list` takes `--name` and `--path`, `grep` takes `--regex`, `--case-sensitive`,
|
|
100
|
+
`--kind` and `--ids-only`. A pattern with `*` or `?` in it is a glob matched
|
|
101
|
+
against the whole name; a plain word is a substring, so `--name password` finds
|
|
102
|
+
`ResetPasswordPayload` rather than nothing. `show` takes ids, or `--method` and
|
|
103
|
+
`--type` by name, or `--source` for a whole document, and `--exact` to print
|
|
104
|
+
only what was named instead of its neighbourhood.
|
|
105
|
+
|
|
106
|
+
```sh
|
|
107
|
+
# Everything that mentions a token, rendered as TypeScript.
|
|
108
|
+
zen rag schema grep token --ids-only | xargs zen rag schema show --format ts
|
|
109
|
+
```
|
|
110
|
+
|
|
79
111
|
## From an agent
|
|
80
112
|
|
|
81
113
|
```ts
|
|
@@ -89,33 +121,60 @@ const index = await SchemaIndex.open(
|
|
|
89
121
|
const project = await loadProject('./my-project', { tools: schemaTools(index) });
|
|
90
122
|
```
|
|
91
123
|
|
|
92
|
-
|
|
124
|
+
Five tools in the group `schema`, selectable as `schema:*`:
|
|
93
125
|
|
|
94
126
|
| Tool | For |
|
|
95
127
|
| -------------------------- | ------------------------------------------------------- |
|
|
96
128
|
| `search_api` | the connected piece of the API that matches an intent |
|
|
97
129
|
| `describe_types` | named schemas as declarations that compile on their own |
|
|
98
130
|
| `find_types_with_property` | which types have a field of this name — no search |
|
|
99
|
-
| `
|
|
131
|
+
| `list_api` | the shape of the API: methods, types or fields |
|
|
132
|
+
| `grep_api` | every literal occurrence of a string — no search |
|
|
100
133
|
|
|
101
|
-
|
|
134
|
+
Only the first of those ranks anything. The rest are exact, because a model
|
|
135
|
+
told "no results" by a vector search has learned nothing: a ranking returns the
|
|
136
|
+
top of a list, so an empty answer and an absent thing look identical.
|
|
137
|
+
`find_types_with_property` is the one for the repair loop — when `tsc` says
|
|
102
138
|
`'password' does not exist in type 'PublicUserProfile'`, the model does not
|
|
103
139
|
need the word explained again, it needs the list of types that have one.
|
|
140
|
+
`grep_api` is the same instinct widened to the whole description.
|
|
104
141
|
|
|
105
142
|
## What an index is
|
|
106
143
|
|
|
107
144
|
```
|
|
108
145
|
schema-db/
|
|
146
|
+
├── README.md what this index holds — a live progress report while it builds
|
|
109
147
|
├── manifest.json written last — its absence means "not indexed"
|
|
110
148
|
├── graph.json topology and light attributes, read whole
|
|
111
|
-
├── schemas.json the raw schemas, read
|
|
149
|
+
├── schemas.json the raw schemas, read the first time one is needed in full
|
|
112
150
|
├── operations.json likewise, for the OpenAPI subset
|
|
151
|
+
├── sources/ the documents themselves, bundled, exactly as indexed
|
|
113
152
|
└── lance/ one table: a row per node, one text column, one vector
|
|
114
153
|
```
|
|
115
154
|
|
|
116
155
|
The manifest records which embedder made the vectors, and a search with a
|
|
117
156
|
different one is refused rather than answered with noise.
|
|
118
157
|
|
|
158
|
+
Indexing a large document is minutes of silence, so the directory says what is
|
|
159
|
+
happening to it. `README.md` appears first as a progress report — the documents,
|
|
160
|
+
the embedder, the step, how many entities have been embedded of how many, and
|
|
161
|
+
how long it has been going — rewritten at most every five seconds, and replaced
|
|
162
|
+
on completion by a description of what the index turned out to hold. A build
|
|
163
|
+
that dies leaves it saying so. While one runs, `.lock` names the process; a
|
|
164
|
+
second build of the same directory is refused, unless the lock is stale.
|
|
165
|
+
|
|
166
|
+
**An index is one portable thing.** Nothing in it names a path outside itself:
|
|
167
|
+
a document is known by a short name taken from its filename, and the document
|
|
168
|
+
itself is copied into `sources/` as it was indexed — bundled, so every external
|
|
169
|
+
`$ref` is already resolved and the copy stands alone. The directory can be moved,
|
|
170
|
+
committed or shipped whole and still says what it is made of. This matters
|
|
171
|
+
because a project's `assets/` is mounted at `/assets` inside an agent's sandbox,
|
|
172
|
+
so an index built here is read under a name this machine never sees. `--no-sources`
|
|
173
|
+
leaves the copies out, for an index that will never travel.
|
|
174
|
+
|
|
175
|
+
The copies are a record, not an input: rebuilding reads the files you name, not
|
|
176
|
+
the ones in `sources/`.
|
|
177
|
+
|
|
119
178
|
## Notes
|
|
120
179
|
|
|
121
180
|
- Documents are **bundled, not dereferenced**: `#/components/schemas/User` is
|
|
@@ -125,8 +184,8 @@ different one is refused rather than answered with noise.
|
|
|
125
184
|
- A **query parameter is a property**, like any field in a body. Nobody should
|
|
126
185
|
have to know in advance which one `page_size` is.
|
|
127
186
|
- Every type carries a **direction** — `input`, `output` or `both` — worked out
|
|
128
|
-
by propagating from the operations through composition, so a
|
|
129
|
-
honestly both rather than whichever side was read last.
|
|
187
|
+
by propagating from the operations through composition, so a type used on both
|
|
188
|
+
sides is honestly both rather than whichever side was read last.
|
|
130
189
|
- Filters reaching the store are **closed enums only**. Exclusion lists are
|
|
131
190
|
applied in JavaScript afterwards, so nothing a model wrote ever reaches a SQL
|
|
132
191
|
predicate.
|
package/dist/command.js
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
|
-
import { bold, CliError, cyan, dim, ensureHome, envNames, EXIT, form, isInteractive, json, KeyStore, note, parse, PROVIDERS, table, usageError, write, } from '@zenera/cli/lib';
|
|
1
|
+
import { bold, CliError, CURATED, cyan, dim, ensureHome, envNames, EXIT, form, isInteractive, json, KeyStore, note, parse, PROVIDERS, table, usageError, write, } from '@zenera/cli/lib';
|
|
2
2
|
import { createEmbedder } from '@zenera/neo';
|
|
3
3
|
import { relative, resolve } from 'node:path';
|
|
4
4
|
import { isFormat, present } from "./present.js";
|
|
5
5
|
import { isEmpty, parseQuery, QueryError } from "./query.js";
|
|
6
6
|
import { repl } from "./repl.js";
|
|
7
7
|
import { buildIndex } from "./schema/build.js";
|
|
8
|
-
import { assertSameEmbedding, openIndex, readManifest } from "./schema/files.js";
|
|
8
|
+
import { assertSameEmbedding, openIndex, readManifest, readSource, } from "./schema/files.js";
|
|
9
|
+
import { fields, grepNodes, listNodes, propertyCount } from "./schema/lookup.js";
|
|
10
|
+
import { isGlob, loose, matcher, PatternError, wildcard } from "./schema/match.js";
|
|
9
11
|
import { SchemaIndex } from "./schema/search.js";
|
|
10
|
-
import { stitch } from "./schema/subgraph.js";
|
|
12
|
+
import { select, stitch } from "./schema/subgraph.js";
|
|
11
13
|
// ---------------------------------------------------------------------------
|
|
12
14
|
// zen rag — an api description, as something to search
|
|
13
15
|
//
|
|
@@ -18,10 +20,19 @@ import { stitch } from "./schema/subgraph.js";
|
|
|
18
20
|
// required, and exit 0 when nothing matched — an empty answer is an answer, and
|
|
19
21
|
// a caller that has to tell "no results" from "the index is missing" by parsing
|
|
20
22
|
// stderr will get it wrong.
|
|
23
|
+
//
|
|
24
|
+
// `list`, `grep` and `show` are the other half, and they are deliberately not
|
|
25
|
+
// searches. A ranking can only ever hand back the top of a list, so it cannot
|
|
26
|
+
// answer "is there a field called `password` anywhere" — the honest answer to
|
|
27
|
+
// that question is every match or none, and these three give it without asking
|
|
28
|
+
// a model or a credential for permission.
|
|
21
29
|
// ---------------------------------------------------------------------------
|
|
22
|
-
const USAGE = 'zen rag schema <index|search|show|stats> [spec...]';
|
|
30
|
+
const USAGE = 'zen rag schema <index|search|list|grep|show|stats> [spec...]';
|
|
23
31
|
const INDEX_USAGE = 'zen rag schema index --embedding <ref> [--out <dir>] <spec...>';
|
|
24
32
|
const SEARCH_USAGE = 'zen rag schema search [--dir <dir>] [query...]';
|
|
33
|
+
const LIST_USAGE = 'zen rag schema list <methods|types|properties> [--dir <dir>]';
|
|
34
|
+
const GREP_USAGE = 'zen rag schema grep <pattern> [--dir <dir>]';
|
|
35
|
+
const SHOW_USAGE = 'zen rag schema show [id...] [--method <name>] [--type <name>]';
|
|
25
36
|
const DEFAULT_DIR = './schema-db';
|
|
26
37
|
export const command = {
|
|
27
38
|
summary: 'Search an openapi/swagger document as a graph.',
|
|
@@ -31,7 +42,9 @@ export const command = {
|
|
|
31
42
|
...table([
|
|
32
43
|
[' index <spec...>', dim('Read the documents and write a searchable index.')],
|
|
33
44
|
[' search', dim('Ask it something. --interactive for a prompt.')],
|
|
34
|
-
['
|
|
45
|
+
[' list <what>', dim('Every method, type or property matching a pattern.')],
|
|
46
|
+
[' grep <pattern>', dim('Every literal match, ranked by nothing.')],
|
|
47
|
+
[' show [id...]', dim('Print named nodes, with no search in between.')],
|
|
35
48
|
[' stats', dim('What is in an index, and what built it.')],
|
|
36
49
|
]),
|
|
37
50
|
'',
|
|
@@ -46,6 +59,7 @@ export const command = {
|
|
|
46
59
|
' --batch <n>',
|
|
47
60
|
dim('Texts per embedding request, and how often progress prints. Default 96.'),
|
|
48
61
|
],
|
|
62
|
+
[' --no-sources', dim('Do not keep a copy of each document in the index.')],
|
|
49
63
|
]),
|
|
50
64
|
'',
|
|
51
65
|
'Search terms (repeatable)',
|
|
@@ -81,6 +95,31 @@ export const command = {
|
|
|
81
95
|
[' --quiet', dim('No narration.')],
|
|
82
96
|
]),
|
|
83
97
|
'',
|
|
98
|
+
'Exact listing — no embedder, no credential',
|
|
99
|
+
...table([
|
|
100
|
+
[' list methods', dim('Operations. Filter with --path and --name.')],
|
|
101
|
+
[' list types', dim('Schemas. Filter with --name.')],
|
|
102
|
+
[' list properties', dim('Fields and parameters. Filter with --name.')],
|
|
103
|
+
[' grep <pattern>', dim('Substring over every node; --regex for a regex.')],
|
|
104
|
+
[' --case-sensitive', dim('grep: match the capitals too.')],
|
|
105
|
+
[' --kind <k>', dim('grep: method | type | property. Repeatable.')],
|
|
106
|
+
[' --ids-only', dim('grep: bare ids, to pipe into show.')],
|
|
107
|
+
[' --source <name>', dim('Only this document, as `stats` names it.')],
|
|
108
|
+
[' --limit <n>', dim('Keep at most n; the count still reports them all.')],
|
|
109
|
+
]),
|
|
110
|
+
'',
|
|
111
|
+
dim(' A pattern with * or ? is a glob over the whole name; otherwise it is'),
|
|
112
|
+
dim(' a substring, so --name password finds ResetPasswordPayload.'),
|
|
113
|
+
'',
|
|
114
|
+
'Show',
|
|
115
|
+
...table([
|
|
116
|
+
[' <id...>', dim('Node ids, e.g. Type:User or Property:User.email.')],
|
|
117
|
+
[' --method <name>', dim('An operation by name. * to take more. Repeatable.')],
|
|
118
|
+
[' --type <name>', dim('A schema by name. * to take more. Repeatable.')],
|
|
119
|
+
[' --source <name>', dim('A whole document, as it was indexed.')],
|
|
120
|
+
[' --exact', dim('Only what was named, without the neighbours.')],
|
|
121
|
+
]),
|
|
122
|
+
'',
|
|
84
123
|
dim(`Credentials come from the ${cyan('zen')} keyring — try ${cyan('zen key ls')}.`),
|
|
85
124
|
],
|
|
86
125
|
async run(ctx) {
|
|
@@ -93,6 +132,10 @@ export const command = {
|
|
|
93
132
|
return await index(tail, ctx);
|
|
94
133
|
case 'search':
|
|
95
134
|
return await search(tail, ctx);
|
|
135
|
+
case 'list':
|
|
136
|
+
return await list(tail, ctx);
|
|
137
|
+
case 'grep':
|
|
138
|
+
return await grep(tail, ctx);
|
|
96
139
|
case 'show':
|
|
97
140
|
return await show(tail, ctx);
|
|
98
141
|
case 'stats':
|
|
@@ -107,6 +150,7 @@ async function index(args, ctx) {
|
|
|
107
150
|
out: { type: 'string', short: 'o' },
|
|
108
151
|
embedding: { type: 'string' },
|
|
109
152
|
batch: { type: 'string' },
|
|
153
|
+
'no-sources': { type: 'boolean' },
|
|
110
154
|
quiet: { type: 'boolean' },
|
|
111
155
|
}, INDEX_USAGE);
|
|
112
156
|
if (positionals.length === 0) {
|
|
@@ -123,9 +167,10 @@ async function index(args, ctx) {
|
|
|
123
167
|
embeddingRef: values.embedding,
|
|
124
168
|
indexer: 'zenera-rag',
|
|
125
169
|
batch: values.batch ? count(values.batch, '--batch') : undefined,
|
|
170
|
+
sources: !values['no-sources'],
|
|
126
171
|
onRead: loud
|
|
127
172
|
? (summary) => {
|
|
128
|
-
printSources(summary.sources
|
|
173
|
+
printSources(summary.sources);
|
|
129
174
|
// The first batch can take a while and says nothing while it
|
|
130
175
|
// does; this is the line that makes that a wait, not a hang.
|
|
131
176
|
note(dim(` embedding ${summary.counts.entities} entities with ${chosen.id} …`));
|
|
@@ -153,9 +198,9 @@ function elapsed(since) {
|
|
|
153
198
|
const seconds = Math.round((Date.now() - since) / 1000);
|
|
154
199
|
return seconds < 60 ? `${seconds}s` : `${Math.floor(seconds / 60)}m${seconds % 60}s`;
|
|
155
200
|
}
|
|
156
|
-
function printSources(sources
|
|
201
|
+
function printSources(sources) {
|
|
157
202
|
const rows = sources.map((s) => ({
|
|
158
|
-
name:
|
|
203
|
+
name: s.file,
|
|
159
204
|
dialect: s.dialect,
|
|
160
205
|
cells: [s.paths, s.methods, s.types, s.properties],
|
|
161
206
|
}));
|
|
@@ -314,33 +359,248 @@ function check(value) {
|
|
|
314
359
|
}
|
|
315
360
|
}
|
|
316
361
|
// ---------------------------------------------------------------------------
|
|
317
|
-
//
|
|
362
|
+
// list, grep
|
|
363
|
+
//
|
|
364
|
+
// The deterministic half. Neither takes an embedder, because neither ranks
|
|
365
|
+
// anything: `list` filters on the attributes a node already has and `grep`
|
|
366
|
+
// reads the same materialized string the index was built from. What comes back
|
|
367
|
+
// is every match, and where a limit cut the list the count still reports the
|
|
368
|
+
// total — being shown three of three hundred is only useful if you are told
|
|
369
|
+
// which of the two happened.
|
|
370
|
+
// ---------------------------------------------------------------------------
|
|
371
|
+
const SUBJECTS = {
|
|
372
|
+
methods: 'method',
|
|
373
|
+
types: 'type',
|
|
374
|
+
properties: 'property',
|
|
375
|
+
};
|
|
376
|
+
async function list(args, ctx) {
|
|
377
|
+
const { values, positionals } = parse(args, {
|
|
378
|
+
dir: { type: 'string', short: 'd' },
|
|
379
|
+
name: MANY,
|
|
380
|
+
path: MANY,
|
|
381
|
+
source: { type: 'string' },
|
|
382
|
+
'method-type': { type: 'string' },
|
|
383
|
+
direction: { type: 'string' },
|
|
384
|
+
limit: { type: 'string' },
|
|
385
|
+
quiet: { type: 'boolean' },
|
|
386
|
+
}, LIST_USAGE);
|
|
387
|
+
const subject = positionals[0];
|
|
388
|
+
const kind = subject ? SUBJECTS[subject] : undefined;
|
|
389
|
+
if (!kind) {
|
|
390
|
+
throw usageError(subject ? `cannot list "${subject}"` : 'nothing named to list', `expected one of ${Object.keys(SUBJECTS).join(', ')}`);
|
|
391
|
+
}
|
|
392
|
+
if (positionals.length > 1) {
|
|
393
|
+
throw usageError('one subject at a time', LIST_USAGE);
|
|
394
|
+
}
|
|
395
|
+
const index = await openIndex(resolve(ctx.cwd, values.dir ?? DEFAULT_DIR));
|
|
396
|
+
const found = listNodes(index.graph, {
|
|
397
|
+
kind,
|
|
398
|
+
name: globs(values.name, '--name'),
|
|
399
|
+
path: globs(values.path, '--path'),
|
|
400
|
+
source: values.source,
|
|
401
|
+
methodType: oneOf(values['method-type'], ['read_only', 'read_write'], '--method-type'),
|
|
402
|
+
direction: oneOf(values.direction, ['input', 'output'], '--direction'),
|
|
403
|
+
limit: values.limit ? count(values.limit, '--limit') : undefined,
|
|
404
|
+
});
|
|
405
|
+
if (ctx.json) {
|
|
406
|
+
json({ found: found.found, truncated: found.truncated, rows: found.rows });
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
const lines = rowLines(index.graph, kind, found.rows);
|
|
410
|
+
if (lines.length > 0) {
|
|
411
|
+
write(lines.join('\n'));
|
|
412
|
+
}
|
|
413
|
+
if (!values.quiet) {
|
|
414
|
+
note(dim(` ${found.found} ${subject}${shown(found.found, found.rows.length)}`));
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
function rowLines(graph, kind, rows) {
|
|
418
|
+
if (kind === 'method') {
|
|
419
|
+
return table(rows.map((r) => [`${r.httpMethod} ${r.path}`, r.name, doc(r.doc)]));
|
|
420
|
+
}
|
|
421
|
+
if (kind === 'type') {
|
|
422
|
+
return table(rows.map((r) => [
|
|
423
|
+
r.name,
|
|
424
|
+
dim(fields(propertyCount(graph, r.id))),
|
|
425
|
+
dim(r.direction === 'none' ? '' : `(${r.direction})`),
|
|
426
|
+
doc(r.doc),
|
|
427
|
+
]));
|
|
428
|
+
}
|
|
429
|
+
return table(rows.map((r) => [
|
|
430
|
+
`${r.parent ? `${r.parent}.` : ''}${r.name}${r.required ? '' : '?'}`,
|
|
431
|
+
`: ${r.signature || 'unknown'}`,
|
|
432
|
+
doc(r.doc),
|
|
433
|
+
]));
|
|
434
|
+
}
|
|
435
|
+
async function grep(args, ctx) {
|
|
436
|
+
const { values, positionals } = parse(args, {
|
|
437
|
+
dir: { type: 'string', short: 'd' },
|
|
438
|
+
regex: { type: 'boolean' },
|
|
439
|
+
'case-sensitive': { type: 'boolean' },
|
|
440
|
+
kind: MANY,
|
|
441
|
+
source: { type: 'string' },
|
|
442
|
+
limit: { type: 'string' },
|
|
443
|
+
'ids-only': { type: 'boolean' },
|
|
444
|
+
quiet: { type: 'boolean' },
|
|
445
|
+
}, GREP_USAGE);
|
|
446
|
+
if (positionals.length === 0) {
|
|
447
|
+
throw usageError('no pattern given', GREP_USAGE);
|
|
448
|
+
}
|
|
449
|
+
if (positionals.length > 1) {
|
|
450
|
+
throw usageError('one pattern at a time — quote it if it has spaces', GREP_USAGE);
|
|
451
|
+
}
|
|
452
|
+
const kinds = (values.kind ?? []).map((k) => oneOf(k, ['method', 'type', 'property'], '--kind'));
|
|
453
|
+
const index = await openIndex(resolve(ctx.cwd, values.dir ?? DEFAULT_DIR));
|
|
454
|
+
const result = pattern(() => grepNodes(index.graph, matcher(positionals[0], {
|
|
455
|
+
regex: values.regex,
|
|
456
|
+
caseSensitive: values['case-sensitive'],
|
|
457
|
+
}), {
|
|
458
|
+
kinds,
|
|
459
|
+
source: values.source,
|
|
460
|
+
limit: values.limit ? count(values.limit, '--limit') : undefined,
|
|
461
|
+
}));
|
|
462
|
+
if (ctx.json) {
|
|
463
|
+
json({
|
|
464
|
+
found: result.found,
|
|
465
|
+
truncated: result.truncated,
|
|
466
|
+
matches: result.matches.map((m) => ({ id: m.id, ...m.attributes, text: m.text })),
|
|
467
|
+
});
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
if (result.matches.length > 0) {
|
|
471
|
+
const lines = values['ids-only']
|
|
472
|
+
? result.matches.map((m) => m.id)
|
|
473
|
+
: table(result.matches.map((m) => [m.id, dim(clip(m.text, 140))]));
|
|
474
|
+
write(lines.join('\n'));
|
|
475
|
+
}
|
|
476
|
+
if (!values.quiet && !values['ids-only']) {
|
|
477
|
+
note(dim(` ${result.found} match(es)${shown(result.found, result.matches.length)}`));
|
|
478
|
+
}
|
|
479
|
+
}
|
|
318
480
|
// ---------------------------------------------------------------------------
|
|
481
|
+
const shown = (found, kept) => (kept < found ? `, showing ${kept}` : '');
|
|
482
|
+
function globs(patterns, flag) {
|
|
483
|
+
if (!patterns || patterns.length === 0) {
|
|
484
|
+
return undefined;
|
|
485
|
+
}
|
|
486
|
+
return patterns.map((p) => pattern(() => loose(p), flag));
|
|
487
|
+
}
|
|
488
|
+
/** A bad pattern is a bad invocation, not a failure of the index. */
|
|
489
|
+
function pattern(run, flag) {
|
|
490
|
+
try {
|
|
491
|
+
return run();
|
|
492
|
+
}
|
|
493
|
+
catch (err) {
|
|
494
|
+
if (err instanceof PatternError) {
|
|
495
|
+
throw usageError(`${flag ? `${flag}: ` : ''}${err.message}`, USAGE);
|
|
496
|
+
}
|
|
497
|
+
throw err;
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
function oneOf(value, allowed, flag) {
|
|
501
|
+
if (value === undefined || value === 'any') {
|
|
502
|
+
return undefined;
|
|
503
|
+
}
|
|
504
|
+
if (!allowed.includes(value)) {
|
|
505
|
+
throw usageError(`${flag} cannot be "${value}"`, `expected ${allowed.join(' or ')}`);
|
|
506
|
+
}
|
|
507
|
+
return value;
|
|
508
|
+
}
|
|
509
|
+
const doc = (text) => (text ? dim(`— ${clip(text.replace(/\s+/g, ' '), 90)}`) : '');
|
|
510
|
+
const clip = (text, max) => text.length <= max ? text : `${text.slice(0, max - 1)}…`;
|
|
319
511
|
/**
|
|
320
512
|
* No embedder and no store: naming a node is a graph lookup, and asking for a
|
|
321
513
|
* credential to print something already on disk would be theatre.
|
|
514
|
+
*
|
|
515
|
+
* Ids are the precise way in, and `--method`/`--type` are the way in for
|
|
516
|
+
* someone who has a name rather than an id — which, with `--format openapi
|
|
517
|
+
* --exact`, is how a resolved slice of the document is got out.
|
|
322
518
|
*/
|
|
323
519
|
async function show(args, ctx) {
|
|
324
|
-
const
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
520
|
+
const { values, positionals } = parse(args, {
|
|
521
|
+
dir: { type: 'string', short: 'd' },
|
|
522
|
+
method: MANY,
|
|
523
|
+
type: MANY,
|
|
524
|
+
source: { type: 'string' },
|
|
525
|
+
exact: { type: 'boolean' },
|
|
526
|
+
format: { type: 'string' },
|
|
527
|
+
'max-nodes': { type: 'string' },
|
|
528
|
+
'no-docs': { type: 'boolean' },
|
|
529
|
+
quiet: { type: 'boolean' },
|
|
530
|
+
}, SHOW_USAGE);
|
|
329
531
|
const format = formatOf(values.format);
|
|
330
|
-
const
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
532
|
+
const dir = resolve(ctx.cwd, values.dir ?? DEFAULT_DIR);
|
|
533
|
+
// A whole document, verbatim: the copy kept at index time is the resolved
|
|
534
|
+
// original, and anything rebuilt from the graph would be a paraphrase.
|
|
535
|
+
if (values.source && format === 'openapi' && positionals.length === 0 && !named(values)) {
|
|
536
|
+
const document = await readSource(dir, values.source);
|
|
537
|
+
if (document) {
|
|
538
|
+
write(document);
|
|
539
|
+
return;
|
|
540
|
+
}
|
|
541
|
+
if (!values.quiet) {
|
|
542
|
+
note(dim(' this index kept no copy of the documents — rebuilding it from the graph'));
|
|
543
|
+
}
|
|
334
544
|
}
|
|
335
|
-
const
|
|
545
|
+
const index = await openIndex(dir);
|
|
546
|
+
const ids = resolveIds(index.graph, positionals, values);
|
|
547
|
+
const subgraphs = values.exact
|
|
548
|
+
? [select(index.graph, ids)]
|
|
549
|
+
: stitch(index.graph, ids.map((id) => ({ id, term: id, field: 'show', score: 1 })), {
|
|
550
|
+
maxNodes: values['max-nodes']
|
|
551
|
+
? count(values['max-nodes'], '--max-nodes')
|
|
552
|
+
: undefined,
|
|
553
|
+
});
|
|
336
554
|
const text = await present(index, subgraphs, format, { docs: !values['no-docs'] });
|
|
337
555
|
if (ctx.json) {
|
|
338
|
-
json({ subgraphs, rendered: text });
|
|
556
|
+
json({ ids, subgraphs, rendered: text });
|
|
339
557
|
}
|
|
340
558
|
else if (text) {
|
|
341
559
|
write(text);
|
|
342
560
|
}
|
|
343
561
|
}
|
|
562
|
+
const named = (values) => Boolean(values.method?.length || values.type?.length);
|
|
563
|
+
/** Ids as given, plus whatever the name selectors resolve to. */
|
|
564
|
+
function resolveIds(graph, ids, values) {
|
|
565
|
+
if (ids.length === 0 && !named(values) && !values.source) {
|
|
566
|
+
throw usageError('no node named', SHOW_USAGE);
|
|
567
|
+
}
|
|
568
|
+
const missing = ids.filter((id) => !graph.hasNode(id));
|
|
569
|
+
if (missing.length > 0) {
|
|
570
|
+
throw new CliError(`no such node: ${missing.join(', ')}`, EXIT.failed, 'ids look like `Type:User` or `Property:User.email`');
|
|
571
|
+
}
|
|
572
|
+
const out = new Set(ids);
|
|
573
|
+
for (const kind of ['method', 'type']) {
|
|
574
|
+
for (const wanted of values[kind] ?? []) {
|
|
575
|
+
// Selecting, not searching: a bare name means that name. A star is
|
|
576
|
+
// the way to ask for more than one.
|
|
577
|
+
const match = isGlob(wanted)
|
|
578
|
+
? pattern(() => wildcard(wanted), `--${kind}`)
|
|
579
|
+
: (name) => name === wanted;
|
|
580
|
+
const rows = listNodes(graph, { kind, name: [match], source: values.source });
|
|
581
|
+
// A selector that matched nothing is a wrong answer, not an empty
|
|
582
|
+
// one: the caller named something they believe is there.
|
|
583
|
+
if (rows.found === 0) {
|
|
584
|
+
throw new CliError(`no ${kind} called ${wanted}`, EXIT.failed, `try: zen rag schema list ${kind}s --name "${wanted}"`);
|
|
585
|
+
}
|
|
586
|
+
for (const row of rows.rows) {
|
|
587
|
+
out.add(row.id);
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
// `--source` on its own means the whole document.
|
|
592
|
+
if (out.size === 0 && values.source) {
|
|
593
|
+
for (const kind of ['method', 'type']) {
|
|
594
|
+
for (const row of listNodes(graph, { kind, source: values.source }).rows) {
|
|
595
|
+
out.add(row.id);
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
if (out.size === 0) {
|
|
599
|
+
throw new CliError(`nothing in this index came from ${values.source}`, EXIT.failed);
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
return [...out];
|
|
603
|
+
}
|
|
344
604
|
async function stats(args, ctx) {
|
|
345
605
|
const { values } = parse(args, { dir: { type: 'string', short: 'd' } }, 'zen rag schema stats [--dir <dir>]');
|
|
346
606
|
const dir = resolve(ctx.cwd, values.dir ?? DEFAULT_DIR);
|
|
@@ -359,7 +619,7 @@ async function stats(args, ctx) {
|
|
|
359
619
|
`fts ${yes(manifest.indexes.fts)} · vector ${yes(manifest.indexes.vector)}`,
|
|
360
620
|
],
|
|
361
621
|
]));
|
|
362
|
-
printSources(manifest.sources
|
|
622
|
+
printSources(manifest.sources);
|
|
363
623
|
notes(table([[' entities', String(manifest.counts.entities)]]).map(dim));
|
|
364
624
|
}
|
|
365
625
|
function notes(lines) {
|
|
@@ -383,22 +643,18 @@ async function embedder(ref) {
|
|
|
383
643
|
return createEmbedder(ref);
|
|
384
644
|
}
|
|
385
645
|
/**
|
|
386
|
-
* Well-known embedding models per provider
|
|
387
|
-
*
|
|
388
|
-
*
|
|
646
|
+
* Well-known embedding models per provider, read off the CLI's catalog table so
|
|
647
|
+
* there is one list rather than two that drift. Any ref the registry can parse
|
|
648
|
+
* works; these are the ones worth typing. Anthropic has none because it
|
|
649
|
+
* publishes no embeddings API at all.
|
|
389
650
|
*/
|
|
390
|
-
const
|
|
391
|
-
openai: ['text-embedding-3-small', 'text-embedding-3-large'],
|
|
392
|
-
google: ['gemini-embedding-001'],
|
|
393
|
-
vertex: ['gemini-embedding-001', 'text-embedding-005'],
|
|
394
|
-
openrouter: ['openai/text-embedding-3-small'],
|
|
395
|
-
};
|
|
651
|
+
const embeddingsOf = (provider) => CURATED[provider].filter((m) => m.roles.includes('embedding')).map((m) => m.id);
|
|
396
652
|
/** What could be passed, with the ones this machine can actually use first. */
|
|
397
653
|
function choices(keys, fromEnv) {
|
|
398
654
|
const rows = [];
|
|
399
655
|
const rest = [];
|
|
400
656
|
for (const provider of PROVIDERS) {
|
|
401
|
-
for (const model of
|
|
657
|
+
for (const model of embeddingsOf(provider)) {
|
|
402
658
|
const source = fromEnv.has(provider)
|
|
403
659
|
? 'environment'
|
|
404
660
|
: keys.active(provider)
|
|
@@ -415,7 +671,9 @@ function choices(keys, fromEnv) {
|
|
|
415
671
|
note(dim(' no provider on this machine has a credential — try: zen key add openai'));
|
|
416
672
|
note('');
|
|
417
673
|
}
|
|
418
|
-
|
|
674
|
+
// `pick` is the one that ends the question rather than restating it: it
|
|
675
|
+
// tries them and prints the first that answers.
|
|
676
|
+
return usageError('no embedder named', 'pass --embedding <ref>, or run: zen models pick --embedding');
|
|
419
677
|
}
|
|
420
678
|
function formatOf(value) {
|
|
421
679
|
if (value === undefined) {
|
package/dist/index.d.ts
CHANGED
|
@@ -5,6 +5,8 @@ export * from './schema/entities.ts';
|
|
|
5
5
|
export * from './schema/files.ts';
|
|
6
6
|
export * from './schema/graph.ts';
|
|
7
7
|
export * from './schema/hydrate.ts';
|
|
8
|
+
export * from './schema/lookup.ts';
|
|
9
|
+
export * from './schema/match.ts';
|
|
8
10
|
export * from './schema/render.ts';
|
|
9
11
|
export * from './schema/schema.ts';
|
|
10
12
|
export * from './schema/search.ts';
|
package/dist/index.js
CHANGED
|
@@ -5,6 +5,8 @@ export * from "./schema/entities.js";
|
|
|
5
5
|
export * from "./schema/files.js";
|
|
6
6
|
export * from "./schema/graph.js";
|
|
7
7
|
export * from "./schema/hydrate.js";
|
|
8
|
+
export * from "./schema/lookup.js";
|
|
9
|
+
export * from "./schema/match.js";
|
|
8
10
|
export * from "./schema/render.js";
|
|
9
11
|
export * from "./schema/schema.js";
|
|
10
12
|
export * from "./schema/search.js";
|
package/dist/schema/build.d.ts
CHANGED
|
@@ -11,6 +11,8 @@ export interface BuildOptions {
|
|
|
11
11
|
indexer: string;
|
|
12
12
|
/** texts sent to the embedder at once */
|
|
13
13
|
batch?: number;
|
|
14
|
+
/** keep a bundled copy of each document in the index. On by default. */
|
|
15
|
+
sources?: boolean;
|
|
14
16
|
signal?: AbortSignal;
|
|
15
17
|
/** what the documents turned out to hold, before a vector has been paid for */
|
|
16
18
|
onRead?: (summary: BuildSummary) => void;
|