@zenera/rag 1.1.5 → 1.1.6
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 +40 -4
- package/dist/command.js +76 -23
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/schema/files.js +1 -1
- package/dist/schema/locate.d.ts +20 -0
- package/dist/schema/locate.js +141 -0
- package/dist/schema/lookup.d.ts +11 -1
- package/dist/schema/lookup.js +28 -1
- package/dist/schema/match.d.ts +4 -0
- package/dist/schema/match.js +7 -0
- package/dist/schema/render.d.ts +8 -0
- package/dist/schema/render.js +12 -2
- package/dist/schema/tools.d.ts +2 -0
- package/dist/schema/tools.js +55 -13
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -59,6 +59,22 @@ zen rag schema grep "pass(word|phrase)" --regex
|
|
|
59
59
|
zen rag schema show --method GetCurrentUserInfo --format openapi --exact
|
|
60
60
|
```
|
|
61
61
|
|
|
62
|
+
Patterns are globs by default and regular expressions under `--regex`, on
|
|
63
|
+
`list` as well as `grep`, which is the only way to say "one of these prefixes":
|
|
64
|
+
|
|
65
|
+
```sh
|
|
66
|
+
zen rag schema list methods --regex --path "^/(users|teams)/"
|
|
67
|
+
zen rag schema grep status --path "/invoices/*" --kind property
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
`grep` takes the same `--name` and `--path` constraints `list` takes, so a
|
|
71
|
+
common word can be narrowed to one corner of the API instead of being read out
|
|
72
|
+
of every document at once.
|
|
73
|
+
|
|
74
|
+
When an index holds more than one document, `--show-source` names the one each
|
|
75
|
+
row came from — `[source: billing_api_v2]` — on `list`, `grep`, `search` and
|
|
76
|
+
`show` alike, so the document does not have to be recovered from `--json`.
|
|
77
|
+
|
|
62
78
|
`list` and `grep` report `found` as the true total even when `--limit` cuts the
|
|
63
79
|
printed rows, so a shortened answer still tells you how much there is. Nothing
|
|
64
80
|
matching exits 0 — an empty answer is an answer, and here it is a trustworthy
|
|
@@ -79,6 +95,22 @@ zen rag schema search --query - --format ts <<'JSON'
|
|
|
79
95
|
JSON
|
|
80
96
|
```
|
|
81
97
|
|
|
98
|
+
## Which index
|
|
99
|
+
|
|
100
|
+
Every reading command takes `-d, --dir`. Without one, `$ZEN_SCHEMA_DB` is used
|
|
101
|
+
if it is set; without that, the nearest index to the working directory is found
|
|
102
|
+
and named on stderr as it is used.
|
|
103
|
+
|
|
104
|
+
Nearest means what it says: this directory, then a short way down into it, then
|
|
105
|
+
up a level and again, stopping at your home directory. What is looked for is a
|
|
106
|
+
`manifest.json` — an index is self-describing, so nothing here searches for a
|
|
107
|
+
directory called `schema-db`, and an index called anything else is found just
|
|
108
|
+
the same. `schema-db` is only the name a new one is given.
|
|
109
|
+
|
|
110
|
+
Two indexes the same distance away is a question, not a tie to break, and it is
|
|
111
|
+
refused: the wrong index does not fail, it answers confidently about a
|
|
112
|
+
different API. Name one with `--dir`, or set `ZEN_SCHEMA_DB`.
|
|
113
|
+
|
|
82
114
|
## Commands
|
|
83
115
|
|
|
84
116
|
```
|
|
@@ -96,10 +128,14 @@ by `--direction`, `--method-type`, `--limit`, `--max-hops`, `--max-nodes` and
|
|
|
96
128
|
the four `--exclude-*` filters, and rendered by `--format text | mermaid |
|
|
97
129
|
mermaid-flowchart | ts | openapi`. `zen help rag` prints the full table.
|
|
98
130
|
|
|
99
|
-
`list`
|
|
100
|
-
`--
|
|
101
|
-
|
|
102
|
-
|
|
131
|
+
`list` and `grep` share `--name`, `--path`, `--regex`, `--case-sensitive`,
|
|
132
|
+
`--source`, `--show-source` and `--limit`; `grep` adds `--kind` and
|
|
133
|
+
`--ids-only`. A pattern with `*` or `?` in it is a glob matched against the
|
|
134
|
+
whole name; a plain word is a substring, so `--name password` finds
|
|
135
|
+
`ResetPasswordPayload` rather than nothing; under `--regex` it is a regular
|
|
136
|
+
expression either way. `--path` selects on the route an operation sits on, and
|
|
137
|
+
on the route a parameter's operation sits on — a schema belongs to no one
|
|
138
|
+
route, so `--path` never selects one. `show` takes ids, or `--method` and
|
|
103
139
|
`--type` by name, or `--source` for a whole document, and `--exact` to print
|
|
104
140
|
only what was named instead of its neighbourhood.
|
|
105
141
|
|
package/dist/command.js
CHANGED
|
@@ -6,8 +6,10 @@ import { isEmpty, parseQuery, QueryError } from "./query.js";
|
|
|
6
6
|
import { repl } from "./repl.js";
|
|
7
7
|
import { buildIndex } from "./schema/build.js";
|
|
8
8
|
import { assertSameEmbedding, openIndex, readManifest, readSource, } from "./schema/files.js";
|
|
9
|
+
import { DEFAULT_DIR, DIR_ENV, locateIndex, outputDir } from "./schema/locate.js";
|
|
9
10
|
import { fields, grepNodes, listNodes, propertyCount } from "./schema/lookup.js";
|
|
10
11
|
import { isGlob, loose, matcher, PatternError, wildcard } from "./schema/match.js";
|
|
12
|
+
import { sourceTag } from "./schema/render.js";
|
|
11
13
|
import { SchemaIndex } from "./schema/search.js";
|
|
12
14
|
import { select, stitch } from "./schema/subgraph.js";
|
|
13
15
|
// ---------------------------------------------------------------------------
|
|
@@ -33,7 +35,6 @@ const SEARCH_USAGE = 'zen rag schema search [--dir <dir>] [query...]';
|
|
|
33
35
|
const LIST_USAGE = 'zen rag schema list <methods|types|properties> [--dir <dir>]';
|
|
34
36
|
const GREP_USAGE = 'zen rag schema grep <pattern> [--dir <dir>]';
|
|
35
37
|
const SHOW_USAGE = 'zen rag schema show [id...] [--method <name>] [--type <name>]';
|
|
36
|
-
const DEFAULT_DIR = './schema-db';
|
|
37
38
|
export const command = {
|
|
38
39
|
summary: 'Search an openapi/swagger document as a graph.',
|
|
39
40
|
usage: USAGE,
|
|
@@ -54,7 +55,10 @@ export const command = {
|
|
|
54
55
|
' --embedding <ref>',
|
|
55
56
|
dim('Which embedder makes the vectors. Omit it to be shown the choices.'),
|
|
56
57
|
],
|
|
57
|
-
[
|
|
58
|
+
[
|
|
59
|
+
' -o, --out <dir>',
|
|
60
|
+
dim(`Where the index goes. Default ${DEFAULT_DIR}, or ${DIR_ENV}.`),
|
|
61
|
+
],
|
|
58
62
|
[
|
|
59
63
|
' --batch <n>',
|
|
60
64
|
dim('Texts per embedding request, and how often progress prints. Default 96.'),
|
|
@@ -78,7 +82,7 @@ export const command = {
|
|
|
78
82
|
'',
|
|
79
83
|
'Search filters and shape',
|
|
80
84
|
...table([
|
|
81
|
-
[' -d, --dir <dir>', dim(`Which index.
|
|
85
|
+
[' -d, --dir <dir>', dim(`Which index. Found from here if unset; see ${DIR_ENV}.`)],
|
|
82
86
|
[' --embedding <ref>', dim('Must be the one the index was built with.')],
|
|
83
87
|
[' --direction <d>', dim('input | output | any. Default any.')],
|
|
84
88
|
[' --method-type <t>', dim('read_only | read_write | any. Default any.')],
|
|
@@ -90,6 +94,7 @@ export const command = {
|
|
|
90
94
|
[' --max-hops <n>', dim('How far apart two hits may be. Default 3.')],
|
|
91
95
|
[' --max-nodes <n>', dim('Nodes per result. Default 200.')],
|
|
92
96
|
[' --format <f>', dim('text | mermaid | mermaid-flowchart | ts | openapi.')],
|
|
97
|
+
[' --show-source', dim('Name the document each operation and schema came from.')],
|
|
93
98
|
[' --no-docs', dim('Leave the descriptions out.')],
|
|
94
99
|
[' --interactive', dim('Prompt, search, refine. Needs a terminal.')],
|
|
95
100
|
[' --quiet', dim('No narration.')],
|
|
@@ -99,17 +104,22 @@ export const command = {
|
|
|
99
104
|
...table([
|
|
100
105
|
[' list methods', dim('Operations. Filter with --path and --name.')],
|
|
101
106
|
[' list types', dim('Schemas. Filter with --name.')],
|
|
102
|
-
[' list properties', dim('Fields and parameters. Filter with --name.')],
|
|
107
|
+
[' list properties', dim('Fields and parameters. Filter with --name and --path.')],
|
|
103
108
|
[' grep <pattern>', dim('Substring over every node; --regex for a regex.')],
|
|
104
|
-
[' --
|
|
109
|
+
[' --regex', dim('Read every pattern as a regex, list and grep alike.')],
|
|
110
|
+
[' --case-sensitive', dim('Match the capitals too.')],
|
|
105
111
|
[' --kind <k>', dim('grep: method | type | property. Repeatable.')],
|
|
112
|
+
[' --name <p>', dim('grep too: only nodes whose name matches. Repeatable.')],
|
|
113
|
+
[' --path <p>', dim('grep too: only what sits on a matching route.')],
|
|
106
114
|
[' --ids-only', dim('grep: bare ids, to pipe into show.')],
|
|
107
115
|
[' --source <name>', dim('Only this document, as `stats` names it.')],
|
|
116
|
+
[' --show-source', dim('Print which document each row came from.')],
|
|
108
117
|
[' --limit <n>', dim('Keep at most n; the count still reports them all.')],
|
|
109
118
|
]),
|
|
110
119
|
'',
|
|
111
120
|
dim(' A pattern with * or ? is a glob over the whole name; otherwise it is'),
|
|
112
|
-
dim(' a substring, so --name password finds ResetPasswordPayload.'),
|
|
121
|
+
dim(' a substring, so --name password finds ResetPasswordPayload. With'),
|
|
122
|
+
dim(' --regex it is a regex either way, so --path "^/(users|teams)/" works.'),
|
|
113
123
|
'',
|
|
114
124
|
'Show',
|
|
115
125
|
...table([
|
|
@@ -117,9 +127,13 @@ export const command = {
|
|
|
117
127
|
[' --method <name>', dim('An operation by name. * to take more. Repeatable.')],
|
|
118
128
|
[' --type <name>', dim('A schema by name. * to take more. Repeatable.')],
|
|
119
129
|
[' --source <name>', dim('A whole document, as it was indexed.')],
|
|
130
|
+
[' --show-source', dim('Name the document each node came from.')],
|
|
120
131
|
[' --exact', dim('Only what was named, without the neighbours.')],
|
|
121
132
|
]),
|
|
122
133
|
'',
|
|
134
|
+
dim(`Without --dir, the index is the nearest one at or above the working`),
|
|
135
|
+
dim(`directory; ${cyan(DIR_ENV)} names it outright.`),
|
|
136
|
+
'',
|
|
123
137
|
dim(`Credentials come from the ${cyan('zen')} keyring — try ${cyan('zen key ls')}.`),
|
|
124
138
|
],
|
|
125
139
|
async run(ctx) {
|
|
@@ -156,7 +170,7 @@ async function index(args, ctx) {
|
|
|
156
170
|
if (positionals.length === 0) {
|
|
157
171
|
throw usageError('no document given', INDEX_USAGE);
|
|
158
172
|
}
|
|
159
|
-
const out =
|
|
173
|
+
const out = outputDir(ctx.cwd, values.out);
|
|
160
174
|
const loud = !values.quiet && !ctx.json;
|
|
161
175
|
const chosen = await embedder(values.embedding);
|
|
162
176
|
const started = Date.now();
|
|
@@ -250,14 +264,19 @@ const SEARCH_OPTIONS = {
|
|
|
250
264
|
format: { type: 'string' },
|
|
251
265
|
'no-docs': { type: 'boolean' },
|
|
252
266
|
'only-hits': { type: 'boolean' },
|
|
267
|
+
'show-source': { type: 'boolean' },
|
|
253
268
|
interactive: { type: 'boolean' },
|
|
254
269
|
quiet: { type: 'boolean' },
|
|
255
270
|
};
|
|
256
271
|
async function search(args, ctx) {
|
|
257
272
|
const { values, positionals } = parse(args, SEARCH_OPTIONS, SEARCH_USAGE);
|
|
258
|
-
const dir =
|
|
273
|
+
const dir = indexDir(ctx, values.dir);
|
|
259
274
|
const format = formatOf(values.format);
|
|
260
|
-
const options = {
|
|
275
|
+
const options = {
|
|
276
|
+
docs: !values['no-docs'],
|
|
277
|
+
onlyHits: values['only-hits'],
|
|
278
|
+
source: values['show-source'],
|
|
279
|
+
};
|
|
261
280
|
const query = { ...(await fromStdin(values.query)), ...fromFlags(values, positionals) };
|
|
262
281
|
// Everything that can be wrong about the invocation is settled before a
|
|
263
282
|
// credential is asked for, so a typo is a usage error and not a login.
|
|
@@ -381,6 +400,9 @@ async function list(args, ctx) {
|
|
|
381
400
|
source: { type: 'string' },
|
|
382
401
|
'method-type': { type: 'string' },
|
|
383
402
|
direction: { type: 'string' },
|
|
403
|
+
regex: { type: 'boolean' },
|
|
404
|
+
'case-sensitive': { type: 'boolean' },
|
|
405
|
+
'show-source': { type: 'boolean' },
|
|
384
406
|
limit: { type: 'string' },
|
|
385
407
|
quiet: { type: 'boolean' },
|
|
386
408
|
}, LIST_USAGE);
|
|
@@ -392,11 +414,12 @@ async function list(args, ctx) {
|
|
|
392
414
|
if (positionals.length > 1) {
|
|
393
415
|
throw usageError('one subject at a time', LIST_USAGE);
|
|
394
416
|
}
|
|
395
|
-
const
|
|
417
|
+
const how = { regex: values.regex, caseSensitive: values['case-sensitive'] };
|
|
418
|
+
const index = await openIndex(indexDir(ctx, values.dir));
|
|
396
419
|
const found = listNodes(index.graph, {
|
|
397
420
|
kind,
|
|
398
|
-
name:
|
|
399
|
-
path:
|
|
421
|
+
name: patterns(values.name, '--name', how),
|
|
422
|
+
path: patterns(values.path, '--path', how),
|
|
400
423
|
source: values.source,
|
|
401
424
|
methodType: oneOf(values['method-type'], ['read_only', 'read_write'], '--method-type'),
|
|
402
425
|
direction: oneOf(values.direction, ['input', 'output'], '--direction'),
|
|
@@ -406,7 +429,7 @@ async function list(args, ctx) {
|
|
|
406
429
|
json({ found: found.found, truncated: found.truncated, rows: found.rows });
|
|
407
430
|
return;
|
|
408
431
|
}
|
|
409
|
-
const lines = rowLines(index.graph, kind, found.rows);
|
|
432
|
+
const lines = rowLines(index.graph, kind, found.rows, values['show-source']);
|
|
410
433
|
if (lines.length > 0) {
|
|
411
434
|
write(lines.join('\n'));
|
|
412
435
|
}
|
|
@@ -414,21 +437,24 @@ async function list(args, ctx) {
|
|
|
414
437
|
note(dim(` ${found.found} ${subject}${shown(found.found, found.rows.length)}`));
|
|
415
438
|
}
|
|
416
439
|
}
|
|
417
|
-
function rowLines(graph, kind, rows) {
|
|
440
|
+
function rowLines(graph, kind, rows, showSource = false) {
|
|
441
|
+
const from = (r) => (showSource ? [dim(sourceTag(r.source))] : []);
|
|
418
442
|
if (kind === 'method') {
|
|
419
|
-
return table(rows.map((r) => [`${r.httpMethod} ${r.path}`, r.name, doc(r.doc)]));
|
|
443
|
+
return table(rows.map((r) => [`${r.httpMethod} ${r.path}`, r.name, ...from(r), doc(r.doc)]));
|
|
420
444
|
}
|
|
421
445
|
if (kind === 'type') {
|
|
422
446
|
return table(rows.map((r) => [
|
|
423
447
|
r.name,
|
|
424
448
|
dim(fields(propertyCount(graph, r.id))),
|
|
425
449
|
dim(r.direction === 'none' ? '' : `(${r.direction})`),
|
|
450
|
+
...from(r),
|
|
426
451
|
doc(r.doc),
|
|
427
452
|
]));
|
|
428
453
|
}
|
|
429
454
|
return table(rows.map((r) => [
|
|
430
455
|
`${r.parent ? `${r.parent}.` : ''}${r.name}${r.required ? '' : '?'}`,
|
|
431
456
|
`: ${r.signature || 'unknown'}`,
|
|
457
|
+
...from(r),
|
|
432
458
|
doc(r.doc),
|
|
433
459
|
]));
|
|
434
460
|
}
|
|
@@ -438,7 +464,10 @@ async function grep(args, ctx) {
|
|
|
438
464
|
regex: { type: 'boolean' },
|
|
439
465
|
'case-sensitive': { type: 'boolean' },
|
|
440
466
|
kind: MANY,
|
|
467
|
+
name: MANY,
|
|
468
|
+
path: MANY,
|
|
441
469
|
source: { type: 'string' },
|
|
470
|
+
'show-source': { type: 'boolean' },
|
|
442
471
|
limit: { type: 'string' },
|
|
443
472
|
'ids-only': { type: 'boolean' },
|
|
444
473
|
quiet: { type: 'boolean' },
|
|
@@ -450,13 +479,18 @@ async function grep(args, ctx) {
|
|
|
450
479
|
throw usageError('one pattern at a time — quote it if it has spaces', GREP_USAGE);
|
|
451
480
|
}
|
|
452
481
|
const kinds = (values.kind ?? []).map((k) => oneOf(k, ['method', 'type', 'property'], '--kind'));
|
|
453
|
-
|
|
482
|
+
// The pattern is read as the flags say; the constraints are always names,
|
|
483
|
+
// so they stay globs-or-substrings even under --regex on the pattern.
|
|
484
|
+
const how = { caseSensitive: values['case-sensitive'] };
|
|
485
|
+
const index = await openIndex(indexDir(ctx, values.dir));
|
|
454
486
|
const result = pattern(() => grepNodes(index.graph, matcher(positionals[0], {
|
|
455
487
|
regex: values.regex,
|
|
456
488
|
caseSensitive: values['case-sensitive'],
|
|
457
489
|
}), {
|
|
458
490
|
kinds,
|
|
459
491
|
source: values.source,
|
|
492
|
+
name: patterns(values.name, '--name', how),
|
|
493
|
+
path: patterns(values.path, '--path', how),
|
|
460
494
|
limit: values.limit ? count(values.limit, '--limit') : undefined,
|
|
461
495
|
}));
|
|
462
496
|
if (ctx.json) {
|
|
@@ -470,7 +504,11 @@ async function grep(args, ctx) {
|
|
|
470
504
|
if (result.matches.length > 0) {
|
|
471
505
|
const lines = values['ids-only']
|
|
472
506
|
? result.matches.map((m) => m.id)
|
|
473
|
-
: table(result.matches.map((m) => [
|
|
507
|
+
: table(result.matches.map((m) => [
|
|
508
|
+
m.id,
|
|
509
|
+
...(values['show-source'] ? [dim(sourceTag(m.attributes.source))] : []),
|
|
510
|
+
dim(clip(m.text, 140)),
|
|
511
|
+
]));
|
|
474
512
|
write(lines.join('\n'));
|
|
475
513
|
}
|
|
476
514
|
if (!values.quiet && !values['ids-only']) {
|
|
@@ -479,11 +517,22 @@ async function grep(args, ctx) {
|
|
|
479
517
|
}
|
|
480
518
|
// ---------------------------------------------------------------------------
|
|
481
519
|
const shown = (found, kept) => (kept < found ? `, showing ${kept}` : '');
|
|
482
|
-
|
|
483
|
-
|
|
520
|
+
/**
|
|
521
|
+
* Where the index is, said out loud when nobody named it. Finding one and not
|
|
522
|
+
* saying which would make every answer here unattributable.
|
|
523
|
+
*/
|
|
524
|
+
function indexDir(ctx, flag) {
|
|
525
|
+
const { dir, from } = locateIndex(ctx.cwd, flag);
|
|
526
|
+
if (from === 'found' && !ctx.json) {
|
|
527
|
+
note(dim(` using ${relative(ctx.cwd, dir) || dir}`));
|
|
528
|
+
}
|
|
529
|
+
return dir;
|
|
530
|
+
}
|
|
531
|
+
function patterns(values, flag, options = {}) {
|
|
532
|
+
if (!values || values.length === 0) {
|
|
484
533
|
return undefined;
|
|
485
534
|
}
|
|
486
|
-
return
|
|
535
|
+
return values.map((p) => pattern(() => loose(p, options), flag));
|
|
487
536
|
}
|
|
488
537
|
/** A bad pattern is a bad invocation, not a failure of the index. */
|
|
489
538
|
function pattern(run, flag) {
|
|
@@ -526,10 +575,11 @@ async function show(args, ctx) {
|
|
|
526
575
|
format: { type: 'string' },
|
|
527
576
|
'max-nodes': { type: 'string' },
|
|
528
577
|
'no-docs': { type: 'boolean' },
|
|
578
|
+
'show-source': { type: 'boolean' },
|
|
529
579
|
quiet: { type: 'boolean' },
|
|
530
580
|
}, SHOW_USAGE);
|
|
531
581
|
const format = formatOf(values.format);
|
|
532
|
-
const dir =
|
|
582
|
+
const dir = indexDir(ctx, values.dir);
|
|
533
583
|
// A whole document, verbatim: the copy kept at index time is the resolved
|
|
534
584
|
// original, and anything rebuilt from the graph would be a paraphrase.
|
|
535
585
|
if (values.source && format === 'openapi' && positionals.length === 0 && !named(values)) {
|
|
@@ -551,7 +601,10 @@ async function show(args, ctx) {
|
|
|
551
601
|
? count(values['max-nodes'], '--max-nodes')
|
|
552
602
|
: undefined,
|
|
553
603
|
});
|
|
554
|
-
const text = await present(index, subgraphs, format, {
|
|
604
|
+
const text = await present(index, subgraphs, format, {
|
|
605
|
+
docs: !values['no-docs'],
|
|
606
|
+
source: values['show-source'],
|
|
607
|
+
});
|
|
555
608
|
if (ctx.json) {
|
|
556
609
|
json({ ids, subgraphs, rendered: text });
|
|
557
610
|
}
|
|
@@ -603,7 +656,7 @@ function resolveIds(graph, ids, values) {
|
|
|
603
656
|
}
|
|
604
657
|
async function stats(args, ctx) {
|
|
605
658
|
const { values } = parse(args, { dir: { type: 'string', short: 'd' } }, 'zen rag schema stats [--dir <dir>]');
|
|
606
|
-
const dir =
|
|
659
|
+
const dir = indexDir(ctx, values.dir);
|
|
607
660
|
const manifest = await readManifest(dir);
|
|
608
661
|
if (ctx.json) {
|
|
609
662
|
json(manifest);
|
package/dist/index.d.ts
CHANGED
|
@@ -5,6 +5,7 @@ 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/locate.ts';
|
|
8
9
|
export * from './schema/lookup.ts';
|
|
9
10
|
export * from './schema/match.ts';
|
|
10
11
|
export * from './schema/render.ts';
|
package/dist/index.js
CHANGED
|
@@ -5,6 +5,7 @@ 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/locate.js";
|
|
8
9
|
export * from "./schema/lookup.js";
|
|
9
10
|
export * from "./schema/match.js";
|
|
10
11
|
export * from "./schema/render.js";
|
package/dist/schema/files.js
CHANGED
|
@@ -83,7 +83,7 @@ export async function readManifest(dir) {
|
|
|
83
83
|
text = await readFile(join(dir, MANIFEST_FILE), 'utf8');
|
|
84
84
|
}
|
|
85
85
|
catch {
|
|
86
|
-
throw new CliError(`${dir} does not hold an index`, EXIT.invalid, 'build one
|
|
86
|
+
throw new CliError(`${dir} does not hold an index`, EXIT.invalid, 'build one with `zen rag schema index`, or name an existing one with --dir or $ZEN_SCHEMA_DB');
|
|
87
87
|
}
|
|
88
88
|
const manifest = JSON.parse(text);
|
|
89
89
|
if (manifest.version !== INDEX_VERSION) {
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/** The name a new index is given. Nothing searches for it; only `index --out` writes it. */
|
|
2
|
+
export declare const DEFAULT_DIR = "./schema-db";
|
|
3
|
+
export declare const DIR_ENV = "ZEN_SCHEMA_DB";
|
|
4
|
+
/** How the directory was arrived at, which is what decides whether to say so. */
|
|
5
|
+
export type DirSource = 'flag' | 'env' | 'found' | 'default';
|
|
6
|
+
export interface Located {
|
|
7
|
+
dir: string;
|
|
8
|
+
from: DirSource;
|
|
9
|
+
}
|
|
10
|
+
export interface LocateOptions {
|
|
11
|
+
env?: NodeJS.ProcessEnv;
|
|
12
|
+
/** do not climb above this; the home directory, or the root, by default */
|
|
13
|
+
ceiling?: string;
|
|
14
|
+
}
|
|
15
|
+
export declare function locateIndex(cwd: string, flag?: string, options?: LocateOptions): Located;
|
|
16
|
+
/** Where a new index goes: the same environment variable, minus the search. */
|
|
17
|
+
export declare function outputDir(cwd: string, flag?: string, env?: NodeJS.ProcessEnv): string;
|
|
18
|
+
/** An index is a directory with a manifest in it; nothing else is asserted here. */
|
|
19
|
+
export declare function isIndex(dir: string): boolean;
|
|
20
|
+
//# sourceMappingURL=locate.d.ts.map
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { CliError, EXIT } from '@zenera/cli/lib';
|
|
2
|
+
import { readdirSync, statSync } from 'node:fs';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
import { dirname, isAbsolute, join, parse, relative, resolve } from 'node:path';
|
|
5
|
+
import { MANIFEST_FILE } from "./files.js";
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
// Which index, when nobody said
|
|
8
|
+
//
|
|
9
|
+
// `--dir` and `$ZEN_SCHEMA_DB` are taken as written, missing or not: naming a
|
|
10
|
+
// directory that turns out not to hold an index has to fail saying so, because
|
|
11
|
+
// quietly using a different one would be a worse answer than an error.
|
|
12
|
+
//
|
|
13
|
+
// With neither, the directory is looked for. There is no list of blessed names
|
|
14
|
+
// here and there should not be — an index is self-describing, so what is being
|
|
15
|
+
// looked for is a `manifest.json`, not a directory called `schema-db`.
|
|
16
|
+
// `schema-db` is only the name a *new* index is given, and nothing reads it
|
|
17
|
+
// back. The search is nearest-first: this directory, then what is under it,
|
|
18
|
+
// then up a level and again, so `/assets/…/whatever` is reachable from
|
|
19
|
+
// `/workspace` because the two meet at a shared root on the way up.
|
|
20
|
+
//
|
|
21
|
+
// Three things bound it, and each is bounding a different kind of mistake.
|
|
22
|
+
// Depth and a visit budget bound the cost. The ceiling — the home directory,
|
|
23
|
+
// or the filesystem root when the search began outside it — bounds the
|
|
24
|
+
// blast radius, because an index in someone else's tree is not yours. And two
|
|
25
|
+
// indexes at the same distance is an ambiguity rather than a tie to break:
|
|
26
|
+
// choosing one silently is the one failure worth ruling out entirely, since
|
|
27
|
+
// the wrong index does not error, it answers confidently about another API.
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
/** The name a new index is given. Nothing searches for it; only `index --out` writes it. */
|
|
30
|
+
export const DEFAULT_DIR = './schema-db';
|
|
31
|
+
export const DIR_ENV = 'ZEN_SCHEMA_DB';
|
|
32
|
+
/** Far enough to climb out of a package into its workspace, not far enough to roam. */
|
|
33
|
+
const MAX_LEVELS = 6;
|
|
34
|
+
/** How far below a directory an index may sit and still count as being in it. */
|
|
35
|
+
const MAX_DEPTH = 3;
|
|
36
|
+
/** A directory with more entries than this is a data store, not a place to keep an index. */
|
|
37
|
+
const MAX_ENTRIES = 128;
|
|
38
|
+
/** Directories the whole search may read, however it is shaped. */
|
|
39
|
+
const MAX_VISITS = 400;
|
|
40
|
+
export function locateIndex(cwd, flag, options = {}) {
|
|
41
|
+
const env = options.env ?? process.env;
|
|
42
|
+
if (flag) {
|
|
43
|
+
return { dir: resolve(cwd, flag), from: 'flag' };
|
|
44
|
+
}
|
|
45
|
+
const named = env[DIR_ENV]?.trim();
|
|
46
|
+
if (named) {
|
|
47
|
+
return { dir: resolve(cwd, named), from: 'env' };
|
|
48
|
+
}
|
|
49
|
+
const found = search(resolve(cwd), options.ceiling ?? ceilingFor(cwd));
|
|
50
|
+
// Nothing found still answers with the default, so the error names the
|
|
51
|
+
// directory everyone expects rather than the last place that was searched.
|
|
52
|
+
return found
|
|
53
|
+
? { dir: found, from: 'found' }
|
|
54
|
+
: { dir: resolve(cwd, DEFAULT_DIR), from: 'default' };
|
|
55
|
+
}
|
|
56
|
+
/** Where a new index goes: the same environment variable, minus the search. */
|
|
57
|
+
export function outputDir(cwd, flag, env = process.env) {
|
|
58
|
+
return resolve(cwd, flag ?? env[DIR_ENV]?.trim() ?? DEFAULT_DIR);
|
|
59
|
+
}
|
|
60
|
+
/** An index is a directory with a manifest in it; nothing else is asserted here. */
|
|
61
|
+
export function isIndex(dir) {
|
|
62
|
+
try {
|
|
63
|
+
return statSync(join(dir, MANIFEST_FILE)).isFile();
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
function search(cwd, ceiling) {
|
|
70
|
+
const budget = { left: MAX_VISITS, seen: new Set() };
|
|
71
|
+
let dir = cwd;
|
|
72
|
+
for (let level = 0; level < MAX_LEVELS; level++) {
|
|
73
|
+
const found = nearest(dir, budget);
|
|
74
|
+
if (found.length === 1) {
|
|
75
|
+
return found[0];
|
|
76
|
+
}
|
|
77
|
+
if (found.length > 1) {
|
|
78
|
+
throw new CliError(`more than one index is equally close to here: ${found.join(', ')}`, EXIT.usage, `say which with --dir, or set ${DIR_ENV}`);
|
|
79
|
+
}
|
|
80
|
+
budget.seen.add(dir);
|
|
81
|
+
const up = dirname(dir);
|
|
82
|
+
if (up === dir || dir === ceiling) {
|
|
83
|
+
break;
|
|
84
|
+
}
|
|
85
|
+
dir = up;
|
|
86
|
+
}
|
|
87
|
+
return undefined;
|
|
88
|
+
}
|
|
89
|
+
/** Every index at the shallowest depth that has any, so a tie can be reported as one. */
|
|
90
|
+
function nearest(root, budget) {
|
|
91
|
+
let frontier = [root];
|
|
92
|
+
for (let depth = 0; depth <= MAX_DEPTH && frontier.length > 0; depth++) {
|
|
93
|
+
const found = frontier.filter(isIndex);
|
|
94
|
+
if (found.length > 0) {
|
|
95
|
+
return found;
|
|
96
|
+
}
|
|
97
|
+
const next = [];
|
|
98
|
+
for (const dir of frontier) {
|
|
99
|
+
if (budget.left <= 0) {
|
|
100
|
+
return [];
|
|
101
|
+
}
|
|
102
|
+
budget.left--;
|
|
103
|
+
next.push(...children(dir).filter((child) => !budget.seen.has(child)));
|
|
104
|
+
}
|
|
105
|
+
frontier = next;
|
|
106
|
+
}
|
|
107
|
+
return [];
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* The subdirectories of one directory. Hidden directories and `node_modules`
|
|
111
|
+
* are skipped: an index kept out of sight is not one anybody meant to be found
|
|
112
|
+
* by looking.
|
|
113
|
+
*/
|
|
114
|
+
function children(dir) {
|
|
115
|
+
let entries;
|
|
116
|
+
try {
|
|
117
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
return [];
|
|
121
|
+
}
|
|
122
|
+
if (entries.length > MAX_ENTRIES) {
|
|
123
|
+
return [];
|
|
124
|
+
}
|
|
125
|
+
return entries
|
|
126
|
+
.filter((e) => e.isDirectory() && !e.name.startsWith('.') && e.name !== 'node_modules')
|
|
127
|
+
.map((e) => join(dir, e.name));
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Home is the ceiling for anyone working inside it. Starting outside it —
|
|
131
|
+
* a container whose workspace is `/workspace`, a CI checkout — there is no
|
|
132
|
+
* home to stay within, so the root is the only stop.
|
|
133
|
+
*/
|
|
134
|
+
function ceilingFor(cwd) {
|
|
135
|
+
const home = homedir();
|
|
136
|
+
const below = relative(home, resolve(cwd));
|
|
137
|
+
return below === '' || (!below.startsWith('..') && !isAbsolute(below))
|
|
138
|
+
? home
|
|
139
|
+
: parse(resolve(cwd)).root;
|
|
140
|
+
}
|
|
141
|
+
//# sourceMappingURL=locate.js.map
|
package/dist/schema/lookup.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type ApiGraph, type NodeAttrs, type NodeKind } from './graph.ts';
|
|
2
2
|
import { type Matcher } from './match.ts';
|
|
3
3
|
export interface Row extends NodeAttrs {
|
|
4
4
|
id: string;
|
|
@@ -28,6 +28,9 @@ export interface Match {
|
|
|
28
28
|
export interface GrepFilter {
|
|
29
29
|
kinds?: readonly string[];
|
|
30
30
|
source?: string;
|
|
31
|
+
/** the same two constraints `list` takes, so one question has one spelling */
|
|
32
|
+
name?: readonly Matcher[];
|
|
33
|
+
path?: readonly Matcher[];
|
|
31
34
|
limit?: number;
|
|
32
35
|
}
|
|
33
36
|
export interface Grep {
|
|
@@ -41,4 +44,11 @@ export declare function grepNodes(graph: ApiGraph, match: Matcher, filter?: Grep
|
|
|
41
44
|
export declare function propertyCount(graph: ApiGraph, id: string): number;
|
|
42
45
|
/** That count, said properly, in the one phrasing the CLI and the tools share. */
|
|
43
46
|
export declare const fields: (n: number) => string;
|
|
47
|
+
/**
|
|
48
|
+
* The route a node sits on. An operation carries its own; a parameter carries
|
|
49
|
+
* the operation's name instead, so it is looked up. A schema has no route at
|
|
50
|
+
* all and never will — the same DTO is returned by half the API — which is
|
|
51
|
+
* why a `--path` filter is a filter on the operations and what hangs off them.
|
|
52
|
+
*/
|
|
53
|
+
export declare function routeOf(graph: ApiGraph, id: string, a: NodeAttrs): string;
|
|
44
54
|
//# sourceMappingURL=lookup.d.ts.map
|
package/dist/schema/lookup.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { textOf } from "./entities.js";
|
|
2
|
+
import { methodId } from "./graph.js";
|
|
2
3
|
import { PatternError } from "./match.js";
|
|
3
4
|
/** A scan is bounded, because a pattern may have come from a model. */
|
|
4
5
|
const DEADLINE_MS = 2000;
|
|
@@ -12,7 +13,7 @@ export function listNodes(graph, filter) {
|
|
|
12
13
|
if (filter.name && !filter.name.some((match) => match(a.name))) {
|
|
13
14
|
return;
|
|
14
15
|
}
|
|
15
|
-
if (filter.path && !
|
|
16
|
+
if (filter.path && !matchesRoute(graph, id, a, filter.path)) {
|
|
16
17
|
return;
|
|
17
18
|
}
|
|
18
19
|
rows.push({ ...a, id });
|
|
@@ -36,6 +37,12 @@ export function grepNodes(graph, match, filter = {}) {
|
|
|
36
37
|
if (filter.source && a.source !== filter.source) {
|
|
37
38
|
continue;
|
|
38
39
|
}
|
|
40
|
+
if (filter.name && !filter.name.some((match) => match(a.name))) {
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
if (filter.path && !matchesRoute(graph, id, a, filter.path)) {
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
39
46
|
const text = textOf(graph, id);
|
|
40
47
|
if (match(text)) {
|
|
41
48
|
matches.push({ id, attributes: a, text });
|
|
@@ -53,7 +60,27 @@ export function propertyCount(graph, id) {
|
|
|
53
60
|
}
|
|
54
61
|
/** That count, said properly, in the one phrasing the CLI and the tools share. */
|
|
55
62
|
export const fields = (n) => `${n} ${n === 1 ? 'field' : 'fields'}`;
|
|
63
|
+
/**
|
|
64
|
+
* The route a node sits on. An operation carries its own; a parameter carries
|
|
65
|
+
* the operation's name instead, so it is looked up. A schema has no route at
|
|
66
|
+
* all and never will — the same DTO is returned by half the API — which is
|
|
67
|
+
* why a `--path` filter is a filter on the operations and what hangs off them.
|
|
68
|
+
*/
|
|
69
|
+
export function routeOf(graph, id, a) {
|
|
70
|
+
if (a.path) {
|
|
71
|
+
return a.path;
|
|
72
|
+
}
|
|
73
|
+
if (a.kind !== 'property' || !a.parent) {
|
|
74
|
+
return '';
|
|
75
|
+
}
|
|
76
|
+
const owner = methodId(a.parent);
|
|
77
|
+
return graph.hasNode(owner) ? graph.getNodeAttribute(owner, 'path') : '';
|
|
78
|
+
}
|
|
56
79
|
// ---------------------------------------------------------------------------
|
|
80
|
+
function matchesRoute(graph, id, a, patterns) {
|
|
81
|
+
const route = routeOf(graph, id, a);
|
|
82
|
+
return route !== '' && patterns.some((match) => match(route));
|
|
83
|
+
}
|
|
57
84
|
function passes(a, filter) {
|
|
58
85
|
if (filter.source && a.source !== filter.source) {
|
|
59
86
|
return false;
|
package/dist/schema/match.d.ts
CHANGED
|
@@ -29,6 +29,10 @@ export declare const isGlob: (pattern: string) => boolean;
|
|
|
29
29
|
* a glob; without one, a substring — because `password` typed into `--name` is
|
|
30
30
|
* a search for the word, and a whole-string match would answer nothing and
|
|
31
31
|
* look like the field does not exist.
|
|
32
|
+
*
|
|
33
|
+
* `regex` settles it outright, and has to: a star is punctuation in both
|
|
34
|
+
* languages, so `^/(users|teams)/.*` read as a glob would match nothing and
|
|
35
|
+
* never say why.
|
|
32
36
|
*/
|
|
33
37
|
export declare function loose(pattern: string, options?: MatchOptions): Matcher;
|
|
34
38
|
/** True when any of the patterns matches; no patterns means no opinion. */
|
package/dist/schema/match.js
CHANGED
|
@@ -53,8 +53,15 @@ export const isGlob = (pattern) => /[*?]/.test(pattern);
|
|
|
53
53
|
* a glob; without one, a substring — because `password` typed into `--name` is
|
|
54
54
|
* a search for the word, and a whole-string match would answer nothing and
|
|
55
55
|
* look like the field does not exist.
|
|
56
|
+
*
|
|
57
|
+
* `regex` settles it outright, and has to: a star is punctuation in both
|
|
58
|
+
* languages, so `^/(users|teams)/.*` read as a glob would match nothing and
|
|
59
|
+
* never say why.
|
|
56
60
|
*/
|
|
57
61
|
export function loose(pattern, options = {}) {
|
|
62
|
+
if (options.regex) {
|
|
63
|
+
return matcher(pattern, options);
|
|
64
|
+
}
|
|
58
65
|
return isGlob(pattern) ? wildcard(pattern, options) : matcher(pattern, options);
|
|
59
66
|
}
|
|
60
67
|
/** True when any of the patterns matches; no patterns means no opinion. */
|
package/dist/schema/render.d.ts
CHANGED
|
@@ -3,7 +3,15 @@ export type RenderFormat = 'text' | 'mermaid' | 'mermaid-flowchart';
|
|
|
3
3
|
export interface RenderOptions {
|
|
4
4
|
docs?: boolean;
|
|
5
5
|
maxDoc?: number;
|
|
6
|
+
/** name the document each operation and schema came from */
|
|
7
|
+
source?: boolean;
|
|
6
8
|
}
|
|
9
|
+
/**
|
|
10
|
+
* Which document something came from, spelled one way everywhere. An index
|
|
11
|
+
* over four revisions of one API has four `GET /infra/tier-0s`, and a listing
|
|
12
|
+
* that does not say which is which is a listing you have to go and check.
|
|
13
|
+
*/
|
|
14
|
+
export declare const sourceTag: (source: string) => string;
|
|
7
15
|
export declare function render(sub: Subgraph, format: RenderFormat, options?: RenderOptions): string;
|
|
8
16
|
export declare function toText(sub: Subgraph, options?: RenderOptions): string;
|
|
9
17
|
export declare function toMermaid(sub: Subgraph, options?: RenderOptions): string;
|
package/dist/schema/render.js
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
const HIT = '»';
|
|
2
|
+
/**
|
|
3
|
+
* Which document something came from, spelled one way everywhere. An index
|
|
4
|
+
* over four revisions of one API has four `GET /infra/tier-0s`, and a listing
|
|
5
|
+
* that does not say which is which is a listing you have to go and check.
|
|
6
|
+
*/
|
|
7
|
+
export const sourceTag = (source) => (source ? `[source: ${source}]` : '');
|
|
2
8
|
export function render(sub, format, options = {}) {
|
|
3
9
|
switch (format) {
|
|
4
10
|
case 'mermaid':
|
|
@@ -44,7 +50,7 @@ export function toText(sub, options = {}) {
|
|
|
44
50
|
function methodLines(view, method, options) {
|
|
45
51
|
const a = method.attributes;
|
|
46
52
|
const out = [
|
|
47
|
-
` ${mark(method)}${a.httpMethod} ${a.path} ${a.name}${doc(method, options, ' —')}`,
|
|
53
|
+
` ${mark(method)}${a.httpMethod} ${a.path} ${a.name}${from(method, options)}${doc(method, options, ' —')}`,
|
|
48
54
|
];
|
|
49
55
|
for (const edge of view.out(method.id, 'HAS_PARAM')) {
|
|
50
56
|
const node = view.node(edge.target);
|
|
@@ -62,7 +68,7 @@ function methodLines(view, method, options) {
|
|
|
62
68
|
}
|
|
63
69
|
function typeLines(view, type, options) {
|
|
64
70
|
const out = [
|
|
65
|
-
` ${mark(type)}${type.attributes.name}${side(type.attributes)}${doc(type, options, ' —')}`,
|
|
71
|
+
` ${mark(type)}${type.attributes.name}${side(type.attributes)}${from(type, options)}${doc(type, options, ' —')}`,
|
|
66
72
|
];
|
|
67
73
|
const composes = view.out(type.id, 'COMPOSES').map((e) => view.name(e.target));
|
|
68
74
|
if (composes.length > 0) {
|
|
@@ -96,6 +102,10 @@ function doc(node, options, lead) {
|
|
|
96
102
|
}
|
|
97
103
|
return `${lead} ${clip(node.attributes.doc, options.maxDoc ?? 120)}`;
|
|
98
104
|
}
|
|
105
|
+
function from(node, options) {
|
|
106
|
+
const tag = options.source ? sourceTag(node.attributes.source) : '';
|
|
107
|
+
return tag ? ` ${tag}` : '';
|
|
108
|
+
}
|
|
99
109
|
// ---------------------------------------------------------------------------
|
|
100
110
|
// Mermaid
|
|
101
111
|
// ---------------------------------------------------------------------------
|
package/dist/schema/tools.d.ts
CHANGED
|
@@ -5,6 +5,8 @@ export interface SchemaToolOptions {
|
|
|
5
5
|
/** what `format` defaults to when the model does not say */
|
|
6
6
|
format?: Format;
|
|
7
7
|
docs?: boolean;
|
|
8
|
+
/** name the document each answer came from; on by default past one document */
|
|
9
|
+
source?: boolean;
|
|
8
10
|
}
|
|
9
11
|
export declare function schemaTools<TCtx = unknown>(index: SchemaIndex, options?: SchemaToolOptions): AnyTool<TCtx>[];
|
|
10
12
|
//# sourceMappingURL=tools.d.ts.map
|
package/dist/schema/tools.js
CHANGED
|
@@ -4,6 +4,7 @@ import { isEmpty, parseQuery, QueryError } from "../query.js";
|
|
|
4
4
|
import { toTypeScript } from "./hydrate.js";
|
|
5
5
|
import { fields, grepNodes, listNodes, propertyCount } from "./lookup.js";
|
|
6
6
|
import { loose, matcher, PatternError } from "./match.js";
|
|
7
|
+
import { sourceTag } from "./render.js";
|
|
7
8
|
import { stitch } from "./subgraph.js";
|
|
8
9
|
// ---------------------------------------------------------------------------
|
|
9
10
|
// The same index, given to an agent
|
|
@@ -31,6 +32,10 @@ const MAX_ROWS = 200;
|
|
|
31
32
|
export function schemaTools(index, options = {}) {
|
|
32
33
|
const fallback = options.format ?? 'text';
|
|
33
34
|
const docs = options.docs ?? true;
|
|
35
|
+
// With one document there is nothing to disambiguate and naming it on every
|
|
36
|
+
// line is prompt spent saying the same word; with several it is the only
|
|
37
|
+
// way to tell two revisions of one API apart.
|
|
38
|
+
const source = options.source ?? index.manifest.sources.length > 1;
|
|
34
39
|
const searchApi = tool({
|
|
35
40
|
name: 'search_api',
|
|
36
41
|
group: GROUP,
|
|
@@ -107,7 +112,10 @@ export function schemaTools(index, options = {}) {
|
|
|
107
112
|
found: result.subgraphs.length,
|
|
108
113
|
ids: result.subgraphs.flatMap((s) => s.hits),
|
|
109
114
|
truncated: result.subgraphs.some((s) => s.truncated),
|
|
110
|
-
api: await present(index, result.subgraphs, chosen(format, fallback), {
|
|
115
|
+
api: await present(index, result.subgraphs, chosen(format, fallback), {
|
|
116
|
+
docs,
|
|
117
|
+
source,
|
|
118
|
+
}),
|
|
111
119
|
};
|
|
112
120
|
},
|
|
113
121
|
});
|
|
@@ -209,12 +217,24 @@ export function schemaTools(index, options = {}) {
|
|
|
209
217
|
description: 'Match the name. A plain word matches anywhere in it; use * and ? ' +
|
|
210
218
|
'to match the whole name, e.g. "*Password*".',
|
|
211
219
|
},
|
|
212
|
-
path: {
|
|
220
|
+
path: {
|
|
221
|
+
type: 'string',
|
|
222
|
+
description: 'Match the route, e.g. "/users*". Keeps operations and their ' +
|
|
223
|
+
'parameters; a schema sits on no one route, so it is left out.',
|
|
224
|
+
},
|
|
225
|
+
regex: {
|
|
226
|
+
type: 'boolean',
|
|
227
|
+
description: 'Read `name` and `path` as regular expressions instead.',
|
|
228
|
+
},
|
|
213
229
|
method_type: {
|
|
214
230
|
type: 'string',
|
|
215
231
|
enum: ['read_only', 'read_write', 'any'],
|
|
216
232
|
},
|
|
217
233
|
direction: { type: 'string', enum: ['input', 'output', 'any'] },
|
|
234
|
+
source: {
|
|
235
|
+
type: 'string',
|
|
236
|
+
description: 'Only this document, when the index holds more than one.',
|
|
237
|
+
},
|
|
218
238
|
limit: {
|
|
219
239
|
type: 'integer',
|
|
220
240
|
description: `Rows to return. Default ${DEFAULT_ROWS}.`,
|
|
@@ -222,7 +242,7 @@ export function schemaTools(index, options = {}) {
|
|
|
222
242
|
},
|
|
223
243
|
additionalProperties: false,
|
|
224
244
|
},
|
|
225
|
-
execute: async ({ kind, name, path, method_type, direction, limit }) => {
|
|
245
|
+
execute: async ({ kind, name, path, regex, method_type, direction, source: only, limit, }) => {
|
|
226
246
|
const subject = SUBJECTS[kind ?? 'methods'];
|
|
227
247
|
if (!subject) {
|
|
228
248
|
return { error: `cannot list "${kind}"`, hint: 'kind is methods, types or fields' };
|
|
@@ -231,8 +251,9 @@ export function schemaTools(index, options = {}) {
|
|
|
231
251
|
try {
|
|
232
252
|
result = listNodes(index.graph, {
|
|
233
253
|
kind: subject,
|
|
234
|
-
name: name ? [loose(name)] : undefined,
|
|
235
|
-
path: path ? [loose(path)] : undefined,
|
|
254
|
+
name: name ? [loose(name, { regex })] : undefined,
|
|
255
|
+
path: path ? [loose(path, { regex })] : undefined,
|
|
256
|
+
source: only,
|
|
236
257
|
methodType: enumerated(method_type),
|
|
237
258
|
direction: enumerated(direction),
|
|
238
259
|
limit: Math.min(limit ?? DEFAULT_ROWS, MAX_ROWS),
|
|
@@ -244,7 +265,7 @@ export function schemaTools(index, options = {}) {
|
|
|
244
265
|
return {
|
|
245
266
|
found: result.found,
|
|
246
267
|
truncated: result.truncated,
|
|
247
|
-
[PLURALS[subject]]: result.rows.map((r) => line(index.graph, subject, r)),
|
|
268
|
+
[PLURALS[subject]]: result.rows.map((r) => line(index.graph, subject, r, source)),
|
|
248
269
|
};
|
|
249
270
|
},
|
|
250
271
|
});
|
|
@@ -255,7 +276,8 @@ export function schemaTools(index, options = {}) {
|
|
|
255
276
|
'operations, schemas and fields alike. No embeddings and no ranking, so ' +
|
|
256
277
|
'nothing is missed for being an unusual word or an odd spelling. This is the ' +
|
|
257
278
|
'tool for "does X exist anywhere", and for checking that a search which ' +
|
|
258
|
-
'returned nothing really means there is nothing.'
|
|
279
|
+
'returned nothing really means there is nothing. Narrow it with `path` or ' +
|
|
280
|
+
'`name` when the word is common and only one corner of the API is meant.',
|
|
259
281
|
parameters: {
|
|
260
282
|
type: 'object',
|
|
261
283
|
properties: {
|
|
@@ -268,6 +290,18 @@ export function schemaTools(index, options = {}) {
|
|
|
268
290
|
description: 'Read the pattern as a regular expression instead.',
|
|
269
291
|
},
|
|
270
292
|
kind: { type: 'string', enum: ['method', 'type', 'property'] },
|
|
293
|
+
name: {
|
|
294
|
+
type: 'string',
|
|
295
|
+
description: 'Only nodes whose own name matches this. * and ? allowed.',
|
|
296
|
+
},
|
|
297
|
+
path: {
|
|
298
|
+
type: 'string',
|
|
299
|
+
description: 'Only what sits on a matching route, e.g. "/users*".',
|
|
300
|
+
},
|
|
301
|
+
source: {
|
|
302
|
+
type: 'string',
|
|
303
|
+
description: 'Only this document, when the index holds more than one.',
|
|
304
|
+
},
|
|
271
305
|
limit: {
|
|
272
306
|
type: 'integer',
|
|
273
307
|
description: `Matches to return. Default ${DEFAULT_ROWS}. \`found\` always counts them all.`,
|
|
@@ -276,11 +310,14 @@ export function schemaTools(index, options = {}) {
|
|
|
276
310
|
required: ['pattern'],
|
|
277
311
|
additionalProperties: false,
|
|
278
312
|
},
|
|
279
|
-
execute: async ({ pattern, regex, kind, limit }) => {
|
|
313
|
+
execute: async ({ pattern, regex, kind, name, path, source: only, limit }) => {
|
|
280
314
|
let result;
|
|
281
315
|
try {
|
|
282
316
|
result = grepNodes(index.graph, matcher(pattern, { regex }), {
|
|
283
317
|
kinds: kind ? [kind] : undefined,
|
|
318
|
+
name: name ? [loose(name)] : undefined,
|
|
319
|
+
path: path ? [loose(path)] : undefined,
|
|
320
|
+
source: only,
|
|
284
321
|
limit: Math.min(limit ?? DEFAULT_ROWS, MAX_ROWS),
|
|
285
322
|
});
|
|
286
323
|
}
|
|
@@ -296,7 +333,11 @@ export function schemaTools(index, options = {}) {
|
|
|
296
333
|
return {
|
|
297
334
|
found: result.found,
|
|
298
335
|
truncated: result.truncated,
|
|
299
|
-
matches: result.matches.map((m) => ({
|
|
336
|
+
matches: result.matches.map((m) => ({
|
|
337
|
+
id: m.id,
|
|
338
|
+
...(source ? { source: m.attributes.source } : {}),
|
|
339
|
+
text: m.text,
|
|
340
|
+
})),
|
|
300
341
|
};
|
|
301
342
|
},
|
|
302
343
|
});
|
|
@@ -315,16 +356,17 @@ const PLURALS = {
|
|
|
315
356
|
property: 'properties',
|
|
316
357
|
};
|
|
317
358
|
/** One row, as the line a model reads rather than an object it has to walk. */
|
|
318
|
-
function line(graph, kind, row) {
|
|
359
|
+
function line(graph, kind, row, source = false) {
|
|
360
|
+
const from = source ? ` ${sourceTag(row.source)}` : '';
|
|
319
361
|
if (kind === 'method') {
|
|
320
|
-
return `${row.httpMethod} ${row.path} ${row.name}${row.doc ? ` — ${row.doc}` : ''}`;
|
|
362
|
+
return `${row.httpMethod} ${row.path} ${row.name}${from}${row.doc ? ` — ${row.doc}` : ''}`;
|
|
321
363
|
}
|
|
322
364
|
if (kind === 'type') {
|
|
323
365
|
const side = row.direction === 'none' ? '' : ` (${row.direction})`;
|
|
324
|
-
return `${row.name}${side} ${fields(propertyCount(graph, row.id))}${row.doc ? ` — ${row.doc}` : ''}`;
|
|
366
|
+
return `${row.name}${side} ${fields(propertyCount(graph, row.id))}${from}${row.doc ? ` — ${row.doc}` : ''}`;
|
|
325
367
|
}
|
|
326
368
|
const owner = row.parent ? `${row.parent}.` : '';
|
|
327
|
-
return `${owner}${row.name}${row.required ? '' : '?'}: ${row.signature || 'unknown'}`;
|
|
369
|
+
return `${owner}${row.name}${row.required ? '' : '?'}: ${row.signature || 'unknown'}${from}`;
|
|
328
370
|
}
|
|
329
371
|
const enumerated = (value) => value && value !== 'any' ? value : undefined;
|
|
330
372
|
function list(description) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zenera/rag",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.6",
|
|
4
4
|
"description": "Retrieval over API descriptions: openapi/swagger documents as a searchable graph.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"agents",
|
|
@@ -53,7 +53,7 @@
|
|
|
53
53
|
"@apidevtools/swagger-parser": "^12.0.0",
|
|
54
54
|
"@lancedb/lancedb": "^0.38.0",
|
|
55
55
|
"graphology": "^0.26.0",
|
|
56
|
-
"@zenera/cli": "^1.1.
|
|
57
|
-
"@zenera/neo": "^1.1.
|
|
56
|
+
"@zenera/cli": "^1.1.6",
|
|
57
|
+
"@zenera/neo": "^1.1.6"
|
|
58
58
|
}
|
|
59
59
|
}
|