@zenera/cli 1.1.28 → 1.1.29
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 +73 -24
- package/dist/audit.d.ts +2 -2
- package/dist/audit.js +11 -4
- package/dist/candidates.d.ts +9 -0
- package/dist/candidates.js +17 -0
- package/dist/catalog.d.ts +10 -5
- package/dist/catalog.js +93 -12
- package/dist/commands/completion.d.ts +9 -0
- package/dist/commands/completion.js +70 -0
- package/dist/commands/index.js +2 -0
- package/dist/commands/init.js +1 -1
- package/dist/commands/key.js +142 -11
- package/dist/commands/meta.d.ts +3 -0
- package/dist/commands/meta.js +134 -29
- package/dist/commands/models.js +56 -98
- package/dist/completion-install.d.ts +16 -0
- package/dist/completion-install.js +85 -0
- package/dist/completion.d.ts +9 -0
- package/dist/completion.js +93 -0
- package/dist/keys.d.ts +27 -4
- package/dist/keys.js +63 -3
- package/dist/liveness.js +73 -3
- package/dist/main.js +22 -0
- package/dist/postinstall.d.ts +2 -0
- package/dist/postinstall.js +10 -0
- package/dist/sandbox.js +4 -3
- package/dist/term.d.ts +2 -0
- package/dist/term.js +2 -0
- package/dist/tui/links.d.ts +2 -0
- package/dist/tui/links.js +31 -7
- package/dist/tui/models.d.ts +5 -0
- package/dist/tui/models.js +21 -14
- package/dist/validate.js +2 -2
- package/package.json +8 -6
- package/postinstall.mjs +7 -0
- package/templates/completions/zen.bash +16 -0
- package/templates/completions/zen.fish +22 -0
- package/templates/completions/zen.zsh +28 -0
- package/templates/editor/.github/copilot-instructions.md +5 -4
- package/templates/editor/.github/prompts/spec-edit.prompt.md +250 -0
- package/templates/editor/.github/skills/zen-cli/SKILL.md +2 -2
- package/templates/editor/.github/skills/zen-cli/references/keys.md +27 -2
- package/templates/editor/.github/skills/zen-cli/references/models.md +22 -23
package/README.md
CHANGED
|
@@ -117,6 +117,28 @@ Or without installing anything:
|
|
|
117
117
|
npx @zenera/cli --help
|
|
118
118
|
```
|
|
119
119
|
|
|
120
|
+
Tab completion for zsh, bash and fish - commands, projects, stored prompts - is
|
|
121
|
+
set up automatically by the global npm install. It appends a guarded loader to
|
|
122
|
+
your current shell's startup file, once. **Open a new terminal** to activate it;
|
|
123
|
+
the installer cannot update the terminal where you ran npm.
|
|
124
|
+
|
|
125
|
+
Setup respects `ZDOTDIR` for zsh and `XDG_CONFIG_HOME` for fish. For Bash on
|
|
126
|
+
macOS it covers both login shells (the active `.bash_profile`, `.bash_login` or
|
|
127
|
+
`.profile`) and non-login shells (`.bashrc`). The loader does nothing when `zen`
|
|
128
|
+
is no longer installed.
|
|
129
|
+
|
|
130
|
+
Local installs and `npx` do not change shell files. Automatic setup is skipped
|
|
131
|
+
under root/sudo, in CI, on Windows, or when `$SHELL` is missing or unsupported.
|
|
132
|
+
Disabling lifecycle scripts with `--ignore-scripts` also disables setup.
|
|
133
|
+
If automatic setup is skipped or fails, or to enable another shell, run:
|
|
134
|
+
|
|
135
|
+
```sh
|
|
136
|
+
zen completion --install # the shell you are in; --yes to skip the question
|
|
137
|
+
zen completion fish --install # or name one
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
Or add it yourself: `eval "$(zen completion zsh)"` in `~/.zshrc`.
|
|
141
|
+
|
|
120
142
|
## Quickstart
|
|
121
143
|
|
|
122
144
|
From nothing to a specialized agent system:
|
|
@@ -557,24 +579,25 @@ The commands to run next are printed for you to read and type.
|
|
|
557
579
|
|
|
558
580
|
## Commands
|
|
559
581
|
|
|
560
|
-
| Command
|
|
561
|
-
|
|
|
562
|
-
| `init`
|
|
563
|
-
| `list`
|
|
564
|
-
| `open`
|
|
565
|
-
| `key`
|
|
566
|
-
| `models`
|
|
567
|
-
| `run`
|
|
568
|
-
| `meta`
|
|
569
|
-
| `inspect`
|
|
570
|
-
| `view`
|
|
571
|
-
| `memory`
|
|
572
|
-
| `check`
|
|
573
|
-
| `export`
|
|
574
|
-
| `import`
|
|
575
|
-
| `sandbox`
|
|
576
|
-
| `cache`
|
|
577
|
-
| `
|
|
582
|
+
| Command | Does |
|
|
583
|
+
| ------------ | -------------------------------------------------------------------- |
|
|
584
|
+
| `init` | Creates a project here, or in `<dir>`, and registers it. |
|
|
585
|
+
| `list` | Every known project: sessions, last run, whether one is live. |
|
|
586
|
+
| `open` | Opens a project in your editor. |
|
|
587
|
+
| `key` | The credential keyring - add, check, switch, remove. |
|
|
588
|
+
| `models` | What this machine can use - list, search, test, pick. |
|
|
589
|
+
| `run` | Runs the project - the TUI on a terminal, a single answer otherwise. |
|
|
590
|
+
| `meta` | Runs the meta agent over the project - the one that edits it. |
|
|
591
|
+
| `inspect` | Opens or rebuilds a run's `report.html`. |
|
|
592
|
+
| `view` | Shows a folder in the browser, redrawn live as its files change. |
|
|
593
|
+
| `memory` | What the agents remember - size, listing, one node, or a whole page. |
|
|
594
|
+
| `check` | Validates the project and every file it names, and asks the models. |
|
|
595
|
+
| `export` | Writes the project to a shareable zip archive. |
|
|
596
|
+
| `import` | Unpacks one, registers it, and runs nothing in it. |
|
|
597
|
+
| `sandbox` | Checks and prepares the container that command-line tools run in. |
|
|
598
|
+
| `cache` | What work has been kept, and getting rid of it. |
|
|
599
|
+
| `completion` | Tab completion for zsh, bash and fish - printed, or installed. |
|
|
600
|
+
| `version` | CLI, library and Node versions. |
|
|
578
601
|
|
|
579
602
|
### Commands from other packages
|
|
580
603
|
|
|
@@ -760,6 +783,9 @@ fixes and the memory are making runs cheaper; each case has its own
|
|
|
760
783
|
every try. `start` prints a local address that shows those pages in a browser,
|
|
761
784
|
with their diagrams drawn, updated in place as the tuning writes them.
|
|
762
785
|
|
|
786
|
+
The whole method, with how to write a rubric that tunes the right thing:
|
|
787
|
+
[docs/finetuning.md](https://github.com/andreyryabov/ZeneraNeo/blob/main/docs/finetuning.md).
|
|
788
|
+
|
|
763
789
|
## Credentials
|
|
764
790
|
|
|
765
791
|
One keyring serves every provider, and a key goes in the same way whatever it
|
|
@@ -816,7 +842,6 @@ zen models openai # everything OpenAI serves this acco
|
|
|
816
842
|
zen models search haiku --tools --free # narrow it
|
|
817
843
|
zen models show openrouter:anthropic/claude-haiku-4.5
|
|
818
844
|
zen models test vertex:gemini-embedding-001 # one real call, one verdict
|
|
819
|
-
zen models pick --embedding # the first ref that answers, on stdout
|
|
820
845
|
zen models browse # walk providers and models with the arrow keys
|
|
821
846
|
```
|
|
822
847
|
|
|
@@ -840,15 +865,15 @@ $ zen models test vertex:gemini-embedding-001
|
|
|
840
865
|
vertex:gemini-embedding-001 blocked Vertex AI API has not been used in project my-proj …
|
|
841
866
|
fix vertex:gemini-embedding-001 gcloud services enable aiplatform.googleapis.com --project my-proj
|
|
842
867
|
error 1 of 1 did not answer
|
|
843
|
-
|
|
868
|
+
see what else there is: zen models ls --embeddings
|
|
844
869
|
```
|
|
845
870
|
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
the above is a single substitution, whether a person or an agent is doing it:
|
|
871
|
+
To take another model instead, list what the account serves, test one, and put
|
|
872
|
+
its ref in `agents.yaml` or the flag that needs it:
|
|
849
873
|
|
|
850
874
|
```sh
|
|
851
|
-
zen
|
|
875
|
+
zen models ls --embeddings
|
|
876
|
+
zen models test openai:text-embedding-3-small
|
|
852
877
|
```
|
|
853
878
|
|
|
854
879
|
`zen models browse [provider]` is the interactive way in: arrow keys through
|
|
@@ -1026,6 +1051,26 @@ clear where a working provider actually comes from.
|
|
|
1026
1051
|
|
|
1027
1052
|
</details>
|
|
1028
1053
|
|
|
1054
|
+
<details>
|
|
1055
|
+
<summary><strong>Every model through a Bifrost gateway</strong></summary>
|
|
1056
|
+
|
|
1057
|
+
With one virtual key in place of every vendor key:
|
|
1058
|
+
|
|
1059
|
+
```sh
|
|
1060
|
+
zen key add bifrost --url https://bifrost.example.com
|
|
1061
|
+
zen key mode bifrost # every run goes through the gateway
|
|
1062
|
+
zen key mode direct # back to the vendor keys
|
|
1063
|
+
```
|
|
1064
|
+
|
|
1065
|
+
`agents.yaml` does not change. Each model keeps its own vendor's protocol and
|
|
1066
|
+
is sent to the gateway's matching endpoint - `vertex:gemini-3.5-flash` reaches
|
|
1067
|
+
`/genai` as `vertex/gemini-3.5-flash` - so thinking and cache accounting
|
|
1068
|
+
survive the trip. The whole story - variables, the sandbox, `zen models`, known
|
|
1069
|
+
limits - is in
|
|
1070
|
+
[docs/gateway.md](https://github.com/andreyryabov/ZeneraNeo/blob/main/docs/gateway.md).
|
|
1071
|
+
|
|
1072
|
+
</details>
|
|
1073
|
+
|
|
1029
1074
|
## Cache - work you have already paid for
|
|
1030
1075
|
|
|
1031
1076
|
Embedding a paragraph, parsing a document, asking a provider what it serves: all
|
|
@@ -1075,6 +1120,10 @@ terminal:
|
|
|
1075
1120
|
- making an API description searchable and mockable.
|
|
1076
1121
|
- [docs/memory.md](https://github.com/andreyryabov/ZeneraNeo/blob/main/docs/memory.md)
|
|
1077
1122
|
- what agents keep between sessions.
|
|
1123
|
+
- [docs/finetuning.md](https://github.com/andreyryabov/ZeneraNeo/blob/main/docs/finetuning.md)
|
|
1124
|
+
- building a dataset, writing rubrics, and tuning the project on them.
|
|
1125
|
+
- [docs/gateway.md](https://github.com/andreyryabov/ZeneraNeo/blob/main/docs/gateway.md)
|
|
1126
|
+
- every model through a Bifrost gateway, with one virtual key.
|
|
1078
1127
|
|
|
1079
1128
|
## License
|
|
1080
1129
|
|
package/dist/audit.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type ModelRequirement } from '@zenera/neo';
|
|
2
|
-
import { type
|
|
2
|
+
import { type KeyOwner, type KeyStore } from './keys.ts';
|
|
3
3
|
/** Which key of the config named a reference: `model:`/`models:` or `embedding:`/`embeddings:`. */
|
|
4
4
|
export type DeclaredRole = 'model' | 'embedding';
|
|
5
5
|
export interface ModelIssue {
|
|
@@ -19,7 +19,7 @@ export interface ModelIssue {
|
|
|
19
19
|
/** what to do about a refusal that a new key would not fix */
|
|
20
20
|
fix?: string;
|
|
21
21
|
/** the keyring provider the fix names, when the kind is one of them */
|
|
22
|
-
add?:
|
|
22
|
+
add?: KeyOwner;
|
|
23
23
|
}
|
|
24
24
|
/**
|
|
25
25
|
* Whether a requirement is actually met, and under which variable.
|
package/dist/audit.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { projectRegistry, readProjectConfig, } from '@zenera/neo';
|
|
2
|
-
import { envNames, form, gcloudAdc, isProvider, SHAPES, } from "./keys.js";
|
|
2
|
+
import { envNames, form, gcloudAdc, isOwner, isProvider, SHAPES, } from "./keys.js";
|
|
3
3
|
import { bold, dim } from "./term.js";
|
|
4
4
|
/**
|
|
5
5
|
* Every model the project names, keyed by how one would refer to it.
|
|
@@ -48,6 +48,9 @@ function declared(config) {
|
|
|
48
48
|
* such answer, so it names the form the provider is usually reached by.
|
|
49
49
|
*/
|
|
50
50
|
export function credentialFor(need) {
|
|
51
|
+
if (need.gateway) {
|
|
52
|
+
return { env: need.apiKeyEnv, present: need.satisfied };
|
|
53
|
+
}
|
|
51
54
|
const provider = isProvider(need.kind) ? need.kind : undefined;
|
|
52
55
|
// A provider with a file form can authenticate without an api key, which is
|
|
53
56
|
// the only thing `satisfied` knows how to look for.
|
|
@@ -86,7 +89,11 @@ export function auditModels(projectDir, store) {
|
|
|
86
89
|
continue;
|
|
87
90
|
}
|
|
88
91
|
const { env, present } = credentialFor(need);
|
|
89
|
-
const provider =
|
|
92
|
+
const provider = need.gateway && isOwner(need.gateway)
|
|
93
|
+
? need.gateway
|
|
94
|
+
: isProvider(need.kind)
|
|
95
|
+
? need.kind
|
|
96
|
+
: undefined;
|
|
90
97
|
if (!present) {
|
|
91
98
|
issues.push({
|
|
92
99
|
name,
|
|
@@ -132,8 +139,8 @@ export function describeIssue(issue) {
|
|
|
132
139
|
}
|
|
133
140
|
const why = issue.detail ? `: ${issue.detail}` : '';
|
|
134
141
|
if (issue.reason === 'blocked') {
|
|
135
|
-
const
|
|
136
|
-
return `${what} authenticated and was then refused${why} — ${dim(issue.fix ??
|
|
142
|
+
const list = `zen models ls --${issue.role === 'embedding' ? 'embeddings' : 'chat'}`;
|
|
143
|
+
return `${what} authenticated and was then refused${why} — ${dim(issue.fix ?? list)}`;
|
|
137
144
|
}
|
|
138
145
|
const fix = `zen key check ${issue.add ?? ''}`.trim();
|
|
139
146
|
return `${what} was rejected when last checked${why} — ${dim(fix)}`;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export interface Candidate {
|
|
2
|
+
value: string;
|
|
3
|
+
description?: string;
|
|
4
|
+
}
|
|
5
|
+
/** Candidates the word being typed is a prefix of. */
|
|
6
|
+
export declare const matching: (found: readonly Candidate[], prefix: string) => Candidate[];
|
|
7
|
+
/** Registered projects whose directory is still there. */
|
|
8
|
+
export declare function projectNames(): Promise<Candidate[]>;
|
|
9
|
+
//# sourceMappingURL=candidates.d.ts.map
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import * as Projects from "./projects.js";
|
|
3
|
+
/** Candidates the word being typed is a prefix of. */
|
|
4
|
+
export const matching = (found, prefix) => found.filter((c) => c.value.startsWith(prefix));
|
|
5
|
+
/** Registered projects whose directory is still there. */
|
|
6
|
+
export async function projectNames() {
|
|
7
|
+
try {
|
|
8
|
+
const registry = await Projects.Registry.open();
|
|
9
|
+
return registry.entries
|
|
10
|
+
.filter((e) => existsSync(e.path))
|
|
11
|
+
.map((e) => ({ value: e.name, description: e.path }));
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return [];
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
//# sourceMappingURL=candidates.js.map
|
package/dist/catalog.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type Gateway } from '@zenera/neo';
|
|
1
2
|
import { type KeyCheck, type Provider } from './keys.ts';
|
|
2
3
|
/**
|
|
3
4
|
* What a model is *for*. A model can hold more than one — Gemini's embedding
|
|
@@ -50,6 +51,11 @@ export interface Catalog {
|
|
|
50
51
|
fetchedAt: string;
|
|
51
52
|
/** why the live call was not used, when it was tried and failed */
|
|
52
53
|
problem?: KeyCheck;
|
|
54
|
+
/** the gateway it was listed through, when the keyring is in a gateway mode */
|
|
55
|
+
via?: {
|
|
56
|
+
name: string;
|
|
57
|
+
url: string;
|
|
58
|
+
};
|
|
53
59
|
}
|
|
54
60
|
/** A day. Model lists change on the scale of weeks; a stale row costs a retry. */
|
|
55
61
|
export declare const CATALOG_TTL_MS: number;
|
|
@@ -64,11 +70,8 @@ export declare const CATALOG_KIND = "catalog";
|
|
|
64
70
|
*/
|
|
65
71
|
export declare const CURATED: Record<Provider, readonly Omit<CatalogEntry, 'ref' | 'provider' | 'source'>[]>;
|
|
66
72
|
/**
|
|
67
|
-
* The
|
|
68
|
-
*
|
|
69
|
-
* Cheap and fast first. `pick` exists to answer "give me something that works"
|
|
70
|
-
* in one round trip where it can, and the small models answer soonest and cost
|
|
71
|
-
* least when the answer is thrown away — which it always is.
|
|
73
|
+
* The cheap, fast models per provider and role, best first. `zen init` takes
|
|
74
|
+
* its default embedder from here.
|
|
72
75
|
*/
|
|
73
76
|
export declare const PREFERRED: Record<Provider, {
|
|
74
77
|
chat: readonly string[];
|
|
@@ -85,6 +88,8 @@ export declare function fromVertexPartners(): Promise<CatalogEntry[]>;
|
|
|
85
88
|
* `partners` is the same seam for Vertex's second listing.
|
|
86
89
|
*/
|
|
87
90
|
export declare function fetchCatalog(provider: Provider, client?: unknown, partners?: () => Promise<CatalogEntry[]>): Promise<CatalogEntry[]>;
|
|
91
|
+
/** What the gateway serves under one provider's name, enriched like a vendor listing. */
|
|
92
|
+
export declare function fetchGatewayCatalog(provider: Provider, at: Gateway): Promise<CatalogEntry[]>;
|
|
88
93
|
/** The key one provider's listing is filed under, version and all. */
|
|
89
94
|
export declare const catalogKey: (provider: Provider) => string;
|
|
90
95
|
export interface CatalogOptions {
|
package/dist/catalog.js
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
// `source` for exactly that reason: a curated guess must never be mistaken for
|
|
17
17
|
// the vendor's own word.
|
|
18
18
|
// ---------------------------------------------------------------------------
|
|
19
|
-
import { ModelRegistry, vertexEndpoint } from '@zenera/neo';
|
|
19
|
+
import { GATEWAY_KEY_HEADER, GATEWAY_NAME, GATEWAY_UPSTREAMS, gatewayOf, ModelRegistry, vertexEndpoint, } from '@zenera/neo';
|
|
20
20
|
import { Cache, cacheKey } from "./cache.js";
|
|
21
21
|
import { PROVIDERS } from "./keys.js";
|
|
22
22
|
import { classify } from "./liveness.js";
|
|
@@ -72,11 +72,8 @@ export const CURATED = {
|
|
|
72
72
|
],
|
|
73
73
|
};
|
|
74
74
|
/**
|
|
75
|
-
* The
|
|
76
|
-
*
|
|
77
|
-
* Cheap and fast first. `pick` exists to answer "give me something that works"
|
|
78
|
-
* in one round trip where it can, and the small models answer soonest and cost
|
|
79
|
-
* least when the answer is thrown away — which it always is.
|
|
75
|
+
* The cheap, fast models per provider and role, best first. `zen init` takes
|
|
76
|
+
* its default embedder from here.
|
|
80
77
|
*/
|
|
81
78
|
export const PREFERRED = {
|
|
82
79
|
openai: {
|
|
@@ -369,6 +366,65 @@ export async function fetchCatalog(provider, client = new ModelRegistry().client
|
|
|
369
366
|
return enrich(provider, await fromRouter(client, provider));
|
|
370
367
|
}
|
|
371
368
|
}
|
|
369
|
+
const UPSTREAM_OF = new Map(PROVIDERS.map((p) => [GATEWAY_UPSTREAMS[p], p]));
|
|
370
|
+
/** One request per gateway per process, however many providers ask. */
|
|
371
|
+
const listings = new Map();
|
|
372
|
+
function gatewayListing(at) {
|
|
373
|
+
const key = `${at.baseURL}\0${at.apiKey ?? ''}`;
|
|
374
|
+
let listing = listings.get(key);
|
|
375
|
+
if (!listing) {
|
|
376
|
+
listing = fetchGatewayListing(at);
|
|
377
|
+
listings.set(key, listing);
|
|
378
|
+
listing.catch(() => listings.delete(key));
|
|
379
|
+
}
|
|
380
|
+
return listing;
|
|
381
|
+
}
|
|
382
|
+
async function fetchGatewayListing(at) {
|
|
383
|
+
const res = await fetch(`${at.baseURL}/v1/models`, {
|
|
384
|
+
headers: at.apiKey
|
|
385
|
+
? { authorization: `Bearer ${at.apiKey}`, [GATEWAY_KEY_HEADER]: at.apiKey }
|
|
386
|
+
: {},
|
|
387
|
+
signal: AbortSignal.timeout(30_000),
|
|
388
|
+
});
|
|
389
|
+
if (!res.ok) {
|
|
390
|
+
const body = await res.text().catch(() => '');
|
|
391
|
+
throw Object.assign(new Error(`${GATEWAY_NAME} answered ${res.status}: ${body}`), {
|
|
392
|
+
status: res.status,
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
const { data } = (await res.json());
|
|
396
|
+
const by = new Map();
|
|
397
|
+
for (const model of data ?? []) {
|
|
398
|
+
if (typeof model.id !== 'string') {
|
|
399
|
+
continue;
|
|
400
|
+
}
|
|
401
|
+
const slash = model.id.indexOf('/');
|
|
402
|
+
const provider = slash > 0 ? UPSTREAM_OF.get(model.id.slice(0, slash)) : undefined;
|
|
403
|
+
const id = model.id.slice(slash + 1);
|
|
404
|
+
const role = openaiRole(id);
|
|
405
|
+
if (!provider || !id || !role) {
|
|
406
|
+
continue;
|
|
407
|
+
}
|
|
408
|
+
const rows = by.get(provider) ?? [];
|
|
409
|
+
rows.push(entry(provider, {
|
|
410
|
+
id,
|
|
411
|
+
roles: [role],
|
|
412
|
+
name: typeof model.name === 'string' ? model.name : undefined,
|
|
413
|
+
contextLength: typeof model.context_length === 'number' ? model.context_length : undefined,
|
|
414
|
+
created: iso(typeof model.created === 'number' ? model.created : undefined),
|
|
415
|
+
publisher: provider === 'vertex' ? publisherOf(id) : undefined,
|
|
416
|
+
}));
|
|
417
|
+
by.set(provider, rows);
|
|
418
|
+
}
|
|
419
|
+
return by;
|
|
420
|
+
}
|
|
421
|
+
function publisherOf(id) {
|
|
422
|
+
return /^claude-/.test(id) ? 'anthropic' : id.includes('/') ? id.split('/')[0] : 'google';
|
|
423
|
+
}
|
|
424
|
+
/** What the gateway serves under one provider's name, enriched like a vendor listing. */
|
|
425
|
+
export async function fetchGatewayCatalog(provider, at) {
|
|
426
|
+
return enrich(provider, (await gatewayListing(at)).get(provider) ?? []);
|
|
427
|
+
}
|
|
372
428
|
// ---------------------------------------------------------------------------
|
|
373
429
|
// Enrichment and cache
|
|
374
430
|
// ---------------------------------------------------------------------------
|
|
@@ -407,6 +463,10 @@ const store = () => new Cache(CATALOG_KIND, { mode: 0o644 });
|
|
|
407
463
|
* to both. A project left to the service-account file is not seen here.
|
|
408
464
|
*/
|
|
409
465
|
function scope(provider) {
|
|
466
|
+
const at = gatewayOf();
|
|
467
|
+
if (at) {
|
|
468
|
+
return cacheKey(GATEWAY_NAME, at.baseURL);
|
|
469
|
+
}
|
|
410
470
|
if (provider !== 'vertex') {
|
|
411
471
|
return undefined;
|
|
412
472
|
}
|
|
@@ -433,19 +493,31 @@ function readCache(provider) {
|
|
|
433
493
|
* model list to four rows.
|
|
434
494
|
*/
|
|
435
495
|
export async function loadCatalog(provider, opts = {}) {
|
|
496
|
+
const at = gatewayOf();
|
|
497
|
+
const via = at ? { via: { name: GATEWAY_NAME, url: at.baseURL } } : {};
|
|
498
|
+
// The built-in list names vendor models the gateway may not serve at all.
|
|
499
|
+
const fallback = () => (at ? [] : curated(provider));
|
|
436
500
|
const cached = readCache(provider);
|
|
437
501
|
const fresh = cached && Date.now() - new Date(cached.fetchedAt).getTime() < CATALOG_TTL_MS
|
|
438
502
|
? cached
|
|
439
503
|
: undefined;
|
|
440
504
|
if (fresh && !opts.refresh) {
|
|
441
|
-
return {
|
|
505
|
+
return {
|
|
506
|
+
provider,
|
|
507
|
+
entries: fresh.entries,
|
|
508
|
+
origin: 'cache',
|
|
509
|
+
fetchedAt: fresh.fetchedAt,
|
|
510
|
+
...via,
|
|
511
|
+
};
|
|
442
512
|
}
|
|
443
513
|
if (!opts.offline) {
|
|
444
514
|
try {
|
|
445
|
-
const entries =
|
|
515
|
+
const entries = at
|
|
516
|
+
? await fetchGatewayCatalog(provider, at)
|
|
517
|
+
: await fetchCatalog(provider);
|
|
446
518
|
const fetchedAt = new Date().toISOString();
|
|
447
519
|
store().put(catalogKey(provider), { provider, fetchedAt, entries });
|
|
448
|
-
return { provider, entries, origin: 'live', fetchedAt };
|
|
520
|
+
return { provider, entries, origin: 'live', fetchedAt, ...via };
|
|
449
521
|
}
|
|
450
522
|
catch (err) {
|
|
451
523
|
const problem = classify(err);
|
|
@@ -456,25 +528,34 @@ export async function loadCatalog(provider, opts = {}) {
|
|
|
456
528
|
origin: 'stale',
|
|
457
529
|
fetchedAt: cached.fetchedAt,
|
|
458
530
|
problem,
|
|
531
|
+
...via,
|
|
459
532
|
};
|
|
460
533
|
}
|
|
461
534
|
return {
|
|
462
535
|
provider,
|
|
463
|
-
entries:
|
|
536
|
+
entries: fallback(),
|
|
464
537
|
origin: 'curated',
|
|
465
538
|
fetchedAt: new Date(0).toISOString(),
|
|
466
539
|
problem,
|
|
540
|
+
...via,
|
|
467
541
|
};
|
|
468
542
|
}
|
|
469
543
|
}
|
|
470
544
|
if (cached) {
|
|
471
|
-
return {
|
|
545
|
+
return {
|
|
546
|
+
provider,
|
|
547
|
+
entries: cached.entries,
|
|
548
|
+
origin: 'stale',
|
|
549
|
+
fetchedAt: cached.fetchedAt,
|
|
550
|
+
...via,
|
|
551
|
+
};
|
|
472
552
|
}
|
|
473
553
|
return {
|
|
474
554
|
provider,
|
|
475
|
-
entries:
|
|
555
|
+
entries: fallback(),
|
|
476
556
|
origin: 'curated',
|
|
477
557
|
fetchedAt: new Date(0).toISOString(),
|
|
558
|
+
...via,
|
|
478
559
|
};
|
|
479
560
|
}
|
|
480
561
|
// ---------------------------------------------------------------------------
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Command } from '../command.ts';
|
|
2
|
+
export { install, installed, loader, rcFile } from '../completion-install.ts';
|
|
3
|
+
/**
|
|
4
|
+
* Prints the shell's side of tab completion. The script is a few lines that
|
|
5
|
+
* call back into `zen __complete`, so the project names and stored prompts it
|
|
6
|
+
* offers are always read from the machine it runs on, never baked in.
|
|
7
|
+
*/
|
|
8
|
+
export declare const completion: Command;
|
|
9
|
+
//# sourceMappingURL=completion.d.ts.map
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { basename } from 'node:path';
|
|
3
|
+
import { one, parse } from "../args.js";
|
|
4
|
+
import { install, installed, loader, rcFiles, shellOf } from "../completion-install.js";
|
|
5
|
+
import { TEMPLATES } from "../scaffold.js";
|
|
6
|
+
import { bold, confirm, usageError, write } from "../term.js";
|
|
7
|
+
export { install, installed, loader, rcFile } from "../completion-install.js";
|
|
8
|
+
const USAGE = 'zen completion [<zsh|bash|fish>] [--install [--yes]]';
|
|
9
|
+
/**
|
|
10
|
+
* Prints the shell's side of tab completion. The script is a few lines that
|
|
11
|
+
* call back into `zen __complete`, so the project names and stored prompts it
|
|
12
|
+
* offers are always read from the machine it runs on, never baked in.
|
|
13
|
+
*/
|
|
14
|
+
export const completion = {
|
|
15
|
+
summary: 'Print the tab-completion script for your shell.',
|
|
16
|
+
usage: USAGE,
|
|
17
|
+
banner: { head: 'Zenera', accent: 'Completion', subtitle: 'Shell Integration', hue: 'magenta' },
|
|
18
|
+
quiet: () => true,
|
|
19
|
+
details: [
|
|
20
|
+
'Global npm installs set this up automatically. Open a new shell afterward.',
|
|
21
|
+
'',
|
|
22
|
+
'Add one line to your shell’s startup file:',
|
|
23
|
+
' zsh eval "$(zen completion zsh)" in ~/.zshrc',
|
|
24
|
+
' bash eval "$(zen completion bash)" in ~/.bashrc',
|
|
25
|
+
' fish zen completion fish | source in ~/.config/fish/config.fish',
|
|
26
|
+
'',
|
|
27
|
+
'Or let zen do it — it appends that line to the file, once, after asking:',
|
|
28
|
+
' zen completion --install for the shell you are in',
|
|
29
|
+
' zen completion zsh --install --yes without asking',
|
|
30
|
+
'',
|
|
31
|
+
'Then, in a new shell:',
|
|
32
|
+
' zen meta run my-project /<Tab> the project’s stored prompts',
|
|
33
|
+
' zen meta <Tab> run, prompts, resume, model …',
|
|
34
|
+
' zen run <Tab> your projects',
|
|
35
|
+
'',
|
|
36
|
+
'The prompts are the files in .github/prompts/ — the same ones an editor',
|
|
37
|
+
'offers as slash commands. Where there is nothing to offer, file names are.',
|
|
38
|
+
],
|
|
39
|
+
run: async (ctx) => {
|
|
40
|
+
const { values, positionals } = parse(ctx.args, { install: { type: 'boolean' }, yes: { type: 'boolean' } }, USAGE);
|
|
41
|
+
const name = one(positionals, 'shell', USAGE) ?? basename(process.env.SHELL ?? '');
|
|
42
|
+
const shell = shellOf(name);
|
|
43
|
+
if (!shell) {
|
|
44
|
+
throw usageError(name ? `no completion for "${name}"` : 'which shell?', 'try: zen completion zsh|bash|fish');
|
|
45
|
+
}
|
|
46
|
+
if (values.yes && !values.install) {
|
|
47
|
+
throw usageError('--yes only goes with --install', USAGE);
|
|
48
|
+
}
|
|
49
|
+
if (values.install) {
|
|
50
|
+
const files = rcFiles(shell).filter((file) => !installed(shell, file));
|
|
51
|
+
if (files.length === 0) {
|
|
52
|
+
write(`Already set up in ${rcFiles(shell).join(', ')}.`);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
const line = bold(loader(shell));
|
|
56
|
+
if (!values.yes && !(await confirm(`Add ${line} to ${files.join(', ')}?`, true))) {
|
|
57
|
+
write('Nothing changed.');
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
for (const file of files) {
|
|
61
|
+
install(shell, file);
|
|
62
|
+
write(`Added to ${file}.`);
|
|
63
|
+
}
|
|
64
|
+
write('Open a new shell to use tab completion.');
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
process.stdout.write(readFileSync(`${TEMPLATES}/completions/zen.${shell}`, 'utf8'));
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
//# sourceMappingURL=completion.js.map
|
package/dist/commands/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { cache } from "./cache.js";
|
|
2
2
|
import { check } from "./check.js";
|
|
3
|
+
import { completion } from "./completion.js";
|
|
3
4
|
import { pack } from "./export.js";
|
|
4
5
|
import { unpack } from "./import.js";
|
|
5
6
|
import { init } from "./init.js";
|
|
@@ -39,6 +40,7 @@ export const COMMANDS = {
|
|
|
39
40
|
memory,
|
|
40
41
|
sandbox,
|
|
41
42
|
cache,
|
|
43
|
+
completion,
|
|
42
44
|
version,
|
|
43
45
|
};
|
|
44
46
|
/** Names that are not listed in help but still work. */
|
package/dist/commands/init.js
CHANGED
|
@@ -47,7 +47,7 @@ const DEFAULT_MODEL = {
|
|
|
47
47
|
/**
|
|
48
48
|
* What vectorises the project's memory, given the provider it ends up on.
|
|
49
49
|
*
|
|
50
|
-
* Taken from the
|
|
50
|
+
* Taken from the catalogue's `PREFERRED` table rather than written out
|
|
51
51
|
* again here, so the scaffold cannot drift from what the catalogue calls
|
|
52
52
|
* current. Anthropic publishes no embeddings API, and a `--model` naming a
|
|
53
53
|
* provider we do not ship gives nothing to guess from; both scaffold without
|