@zenera/cli 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 CHANGED
@@ -41,7 +41,7 @@ Four commands, from nothing to an answer:
41
41
 
42
42
  ```sh
43
43
  npm i -g @zenera/cli # every vendor SDK comes with it
44
- zen key add openai # prompts with the echo off; stored in ~/.zenera
44
+ zen key add openai # asks for the key without showing it; stored in ~/.zenera
45
45
  zen init my-project # scaffolds a project and registers it
46
46
  cd my-project && zen run "introduce yourself"
47
47
  ```
@@ -49,7 +49,7 @@ cd my-project && zen run "introduce yourself"
49
49
  Then the rest of the loop:
50
50
 
51
51
  ```sh
52
- zen run # nothing to say yet — a TUI on a terminal
52
+ zen run # nothing to say yet — a full-screen terminal app (a TUI)
53
53
  zen check # validate the project and every file it names
54
54
  zen inspect # open the last run's report.html
55
55
  zen list --sessions # every project, its sessions and last run
@@ -150,17 +150,18 @@ you are not expected to hand-author `agents.yaml`.
150
150
 
151
151
  ## Commands
152
152
 
153
- | Command | Does |
154
- | --------- | ------------------------------------------------------------------------ |
155
- | `init` | Creates a project here, or in `<dir>`, and registers it. |
156
- | `list` | Every known project: sessions, last run, whether one is live. |
157
- | `open` | Opens a project in your editor. |
158
- | `key` | The credential keyring — add, check, switch, remove. |
159
- | `run` | Runs the project the TUI on a terminal, one shot otherwise. |
160
- | `inspect` | Opens or rebuilds a run's `report.html`. |
161
- | `models` | Resolves providers and models and validates the config, calling nothing. |
162
- | `sandbox` | Checks and prepares the container that command-line tools run in. |
163
- | `version` | CLI, library and Node versions. |
153
+ | Command | Does |
154
+ | --------- | -------------------------------------------------------------------- |
155
+ | `init` | Creates a project here, or in `<dir>`, and registers it. |
156
+ | `list` | Every known project: sessions, last run, whether one is live. |
157
+ | `open` | Opens a project in your editor. |
158
+ | `key` | The credential keyring — add, check, switch, remove. |
159
+ | `models` | What this machine can use list, search, test, pick. |
160
+ | `run` | Runs the project — the TUI on a terminal, a single answer otherwise. |
161
+ | `inspect` | Opens or rebuilds a run's `report.html`. |
162
+ | `check` | Validates the project and every file it names, and asks the models. |
163
+ | `sandbox` | Checks and prepares the container that command-line tools run in. |
164
+ | `version` | CLI, library and Node versions. |
164
165
 
165
166
  Commands can also come from a package installed alongside this one, so a new
166
167
  capability is a subcommand rather than a new binary to remember — one thing on
@@ -204,16 +205,16 @@ One keyring serves every provider, and a key goes in the same way whatever it
204
205
  is for:
205
206
 
206
207
  ```sh
207
- zen key add <provider> # prompts with the echo off
208
- zen key add <provider> < key.txt # or pipe it
208
+ zen key add <provider> # asks for the key without showing it
209
+ zen key add <provider> < key.txt # or pipe it in
209
210
  ```
210
211
 
211
- The value never comes from argv — a command line lands in `ps`, in shell
212
- history and in CI logs so piped stdin and the echo-off prompt are the only
213
- two ways in. Entries live in `~/.zenera/neo/keys.json` (mode `0600`) and are
214
- materialised into the environment just before a run, so a real environment
215
- variable always wins and a project checked out on a machine without `zen` still
216
- runs.
212
+ The value is never given as an argument — a command line is visible to anyone
213
+ listing running processes, is saved in your shell history and is captured in CI
214
+ logs so piping it in and the hidden prompt are the only two ways. Entries live
215
+ in `~/.zenera/neo/keys.json`, in a file only you can read, and are copied into
216
+ the environment just before a run, so an environment variable you set yourself
217
+ always wins and a project checked out on a machine without `zen` still runs.
217
218
 
218
219
  | Provider | The value is | Exported as |
219
220
  | ------------ | ------------------------------------- | -------------------------------- |
@@ -234,22 +235,76 @@ A model reference is `[provider[/api]:]model`, and the first segment names a
234
235
  **provider, not a vendor**. So `vertex:gemini-3.5-flash` and
235
236
  `google:gemini-3.5-flash` are the same model reached through two different
236
237
  services, needing two different credentials — and a bare `gpt-5.4-mini` goes to
237
- the default provider, `openai`. `zen models` resolves every reference in a
238
- project against what is stored, and calls nothing.
238
+ the default provider, `openai`. `zen check` resolves every reference in a
239
+ project against what is stored, says which credential each one needs, and spends
240
+ a few tokens asking each of them to answer — the only way to catch a model id
241
+ this account is not served. `--no-models` stops before the asking.
242
+
243
+ ### Which models you can use
244
+
245
+ `zen check` answers _does my project work_. `zen models` answers _what can I
246
+ use_, needs no project, and asks the providers themselves:
247
+
248
+ ```sh
249
+ zen models # who has a credential, and what is cached
250
+ zen models openai # everything OpenAI serves this account
251
+ zen models search haiku --tools --free # narrow it
252
+ zen models show openrouter:anthropic/claude-haiku-4.5
253
+ zen models test vertex:gemini-embedding-001 # one real call, one verdict
254
+ zen models pick --embedding # the first ref that answers, on stdout
255
+ ```
256
+
257
+ Listings are cached for a day in `~/.zenera/neo/catalog`. When a provider cannot
258
+ be asked the last listing is used and said to be stale; only if there was never
259
+ one does a short built-in list stand in.
260
+
261
+ `test` distinguishes three failures, because they want three different actions.
262
+ A **refused** model means the credential was rejected. A **blocked** one means
263
+ the credential was accepted and the account then said no — an API switched off,
264
+ an empty balance, a model this key was never granted — and the fix comes with
265
+ it:
266
+
267
+ ```
268
+ $ zen models test vertex:gemini-embedding-001
269
+ vertex:gemini-embedding-001 blocked Vertex AI API has not been used in project my-proj …
270
+ vertex:gemini-embedding-001: gcloud services enable aiplatform.googleapis.com --project my-proj
271
+ error 1 of 1 did not answer
272
+ find one that does: zen models pick --embedding
273
+ ```
274
+
275
+ `pick` tries a short list of candidates one at a time and stops at the first
276
+ that works, printing the bare ref on stdout — so recovering from the above is
277
+ one substitution, whether a person or an agent is doing it:
278
+
279
+ ```sh
280
+ zen rag schema index --embedding "$(zen models pick --embedding)" ./specs/*.yaml
281
+ ```
239
282
 
240
283
  ### Vertex AI
241
284
 
242
- Vertex accepts either shape, and which one you gave is read off the value.
285
+ Vertex takes two kinds of credential, and you never say which you are giving:
286
+ if the value is a path to a file that exists it is a service-account key, and
287
+ otherwise it is treated as an API key. The name you choose for the entry has no
288
+ say in it — `vertex/express` is simply an entry called `express`, exactly as
289
+ `vertex/prod` is one called `prod`, and either name can hold either kind.
243
290
 
244
291
  The usual one is a **service-account JSON file** — give its path, not its
245
- contents:
292
+ contents. Run the command with nothing piped and it asks:
293
+
294
+ ```sh
295
+ zen key add vertex --location us-central1
296
+ # Paste the key, or a path to the file: /Users/you/keys/vertex-sa.json
297
+ ```
298
+
299
+ The prompt is read by `zen`, not by your shell, so give a full path there — `~`
300
+ is not expanded. In a script, pipe the path in instead:
246
301
 
247
302
  ```sh
248
- printf '%s' ~/keys/vertex-sa.json | zen key add vertex --location us-central1
303
+ echo ~/keys/vertex-sa.json | zen key add vertex --location us-central1
249
304
  ```
250
305
 
251
- The file is copied into `~/.zenera/neo/keys/` at mode `0600`, so moving or
252
- cleaning up the original later cannot break it.
306
+ The file is copied into `~/.zenera/neo/keys/`, where only you can read it, so
307
+ moving or cleaning up the original later cannot break it.
253
308
 
254
309
  - `--location <region>` is worth setting. It must be `global` or a **concrete
255
310
  region**; multi-region names like `us` are rejected with a 404. `global`
@@ -258,10 +313,73 @@ cleaning up the original later cannot break it.
258
313
  - `--project <id>` is only needed when the `project_id` inside the file is not
259
314
  the project you want.
260
315
 
261
- The alternative is an **express-mode API key**, an ordinary secret under
262
- `VERTEX_API_KEY`. Express mode addresses no project, so `--project` and
316
+ The alternative is an **express-mode API key** a single secret, stored under
317
+ `VERTEX_API_KEY`. It is the Vertex console's way of handing out access without a
318
+ service account, and it needs neither a project nor a region, so `--project` and
263
319
  `--location` mean nothing there and are not stored.
264
320
 
321
+ ### Gemini, three ways
322
+
323
+ The same Gemini models are reachable through three different credentials, and
324
+ which one you hold decides the prefix a model reference needs.
325
+
326
+ **AI Studio** — one key and nothing else to configure, the shortest way to a
327
+ working `gemini-3.5-flash`:
328
+
329
+ ```sh
330
+ zen key add google # asks for the key without showing it
331
+ zen check # google:gemini-3.5-flash now resolves
332
+ ```
333
+
334
+ **Vertex, service account** — what production usually runs on. Give the path
335
+ and a region, because the file says which project it belongs to but never which
336
+ region to call:
337
+
338
+ ```sh
339
+ echo ~/keys/vertex-sa.json | zen key add vertex --location us-central1
340
+ ```
341
+
342
+ Add `--project` only when the `project_id` inside the file is not the one you
343
+ want to bill:
344
+
345
+ ```sh
346
+ echo ~/keys/vertex-sa.json \
347
+ | zen key add vertex --project other-project --location europe-west4
348
+ ```
349
+
350
+ **Vertex, express mode** — paste the key at the prompt; no flags apply:
351
+
352
+ ```sh
353
+ zen key add vertex
354
+ ```
355
+
356
+ Holding several at once is the ordinary case. Name them and switch:
357
+
358
+ ```sh
359
+ echo ~/keys/prod-sa.json | zen key add vertex/prod --location us-central1
360
+ echo ~/keys/dev-sa.json | zen key add vertex/dev --location global
361
+ zen key add vertex/express # the express key, same provider
362
+
363
+ zen key use vertex/dev # which one the next run uses
364
+ zen key ls --check # all three, and whether they still work
365
+ zen key show vertex/prod # masked; --reveal prints the path
366
+ ```
367
+
368
+ `google` and `vertex` can both be configured — they are separate entries for
369
+ separate services, and the reference picks:
370
+
371
+ ```sh
372
+ zen run --model google:gemini-3.5-flash "summarise this repo"
373
+ zen run --model vertex:gemini-3.5-flash "summarise this repo"
374
+ ```
375
+
376
+ If you have already run `gcloud auth application-default login`, that login is
377
+ itself a usable credential: `zen key ls` shows it as `adc`, marked `~` because
378
+ it came from outside the keyring, and Vertex works with nothing stored at all.
379
+ Anything already set in `GOOGLE_APPLICATION_CREDENTIALS`, `VERTEX_API_KEY` or
380
+ `GEMINI_API_KEY` is listed the same way and wins over the keyring, so it is
381
+ always visible which credential a run will actually use.
382
+
265
383
  ### More than one key per provider
266
384
 
267
385
  Entries are named, so a provider can hold several and one of them is active:
@@ -275,32 +393,34 @@ zen key env openai # shell exports, for other tools
275
393
  zen key rm openai/work
276
394
  ```
277
395
 
278
- `zen key ls` marks the active entry with `*`, and something it found in your
279
- real environment or in `gcloud`'s ADC with `~`, so it is always clear where a
280
- working provider actually comes from.
396
+ `zen key ls` marks the active entry with `*`, and anything it found outside the
397
+ keyring — in your environment, or in a `gcloud` login with `~`, so it is always
398
+ clear where a working provider actually comes from.
281
399
 
282
400
  ## Concepts
283
401
 
284
402
  - **Project** — a named directory holding a complete agent definition and the
285
403
  sessions that ran against it. Self-describing: `agents.yaml` is what makes it
286
404
  one, so moving or cloning the directory loses nothing.
287
- - **Session** — a context that persists: one workspace, one memory, one blob
288
- store, one accumulating trajectory. Resumable.
405
+ - **Session** — a context that persists: one workspace, one memory, one store
406
+ for large files, and a record of everything that happened, added to as it
407
+ goes. Resumable.
289
408
  - **Run** — one prompt in, one answer out, inside a session. Recorded in full,
290
409
  whether or not you were watching.
291
410
  - **Workspace** — the directory the agents may read and write. A prompt given on
292
411
  the command line uses the current directory; the TUI offers the session's own
293
412
  empty folder and confirms anything outside it.
294
- - **Keyring** — `~/.zenera/neo`, mode `0700`. Keys are materialised into the
295
- environment just before a run, so a real env var always wins and a project
296
- checked out on a machine without `zen` still runs.
413
+ - **Keyring** — `~/.zenera/neo`, readable only by you. Keys are copied into the
414
+ environment just before a run, so an environment variable you set yourself
415
+ always wins and a project checked out on a machine without `zen` still runs.
297
416
 
298
417
  ## The library underneath
299
418
 
300
419
  This is a shell over
301
420
  [`@zenera/neo`](https://www.npmjs.com/package/@zenera/neo) — agents, models,
302
- tools, skills, memory and an append-only trajectory. Use it directly when you
303
- want the runtime inside your own application rather than on a terminal:
421
+ tools, skills, memory and a running record of everything that happened. Use it
422
+ directly when you want the runtime inside your own application rather than on a
423
+ terminal:
304
424
  [its README](https://github.com/andreyryabov/ZeneraNeo/blob/main/packages/neo/README.md).
305
425
 
306
426
  ## Documentation
package/dist/audit.d.ts CHANGED
@@ -11,10 +11,13 @@ export interface ModelIssue {
11
11
  provider: string;
12
12
  /** the variable that would carry the credential */
13
13
  env: string;
14
- /** `missing` — nothing to authenticate with. `dead` — rejected when checked. */
15
- reason: 'missing' | 'dead';
14
+ /** `missing` — nothing to authenticate with. `dead` — rejected when checked.
15
+ * `blocked` accepted, and then refused by the account behind it. */
16
+ reason: 'missing' | 'dead' | 'blocked';
16
17
  /** the provider's own words, when it was the one to say no */
17
18
  detail?: string;
19
+ /** what to do about a refusal that a new key would not fix */
20
+ fix?: string;
18
21
  /** the keyring provider the fix names, when the kind is one of them */
19
22
  add?: Provider;
20
23
  }
package/dist/audit.js CHANGED
@@ -103,14 +103,15 @@ export function auditModels(projectDir, store) {
103
103
  // reinstated between the check and the run, and a stale verdict must
104
104
  // not be the thing that stops a run from being attempted.
105
105
  const check = provider ? store.active(provider)?.check : undefined;
106
- if (check?.state === 'dead') {
106
+ if (check?.state === 'dead' || check?.state === 'blocked') {
107
107
  issues.push({
108
108
  name,
109
109
  role,
110
110
  provider: need.provider,
111
111
  env,
112
- reason: 'dead',
112
+ reason: check.state,
113
113
  detail: check.detail,
114
+ fix: check.fix,
114
115
  add: provider,
115
116
  });
116
117
  }
@@ -130,6 +131,10 @@ export function describeIssue(issue) {
130
131
  return `${what} has no credential — ${issue.env} is not set; ${dim(fix)}`;
131
132
  }
132
133
  const why = issue.detail ? `: ${issue.detail}` : '';
134
+ if (issue.reason === 'blocked') {
135
+ const pick = `zen models pick --${issue.role === 'embedding' ? 'embedding' : 'chat'}`;
136
+ return `${what} authenticated and was then refused${why} — ${dim(issue.fix ?? pick)}`;
137
+ }
133
138
  const fix = `zen key check ${issue.add ?? ''}`.trim();
134
139
  return `${what} was rejected when last checked${why} — ${dim(fix)}`;
135
140
  }
@@ -0,0 +1,111 @@
1
+ import { type KeyCheck, type Provider } from './keys.ts';
2
+ /**
3
+ * What a model is *for*. A model can hold more than one — Gemini's embedding
4
+ * endpoint and its chat endpoint are the same catalog row on some providers —
5
+ * so this is a set rather than a field.
6
+ */
7
+ export type Role = 'chat' | 'embedding' | 'image' | 'audio';
8
+ export interface CatalogEntry {
9
+ /** `provider:id` — paste-able straight into `agents.yaml` */
10
+ ref: string;
11
+ /** the id that goes on the wire, with any resource prefix already stripped */
12
+ id: string;
13
+ provider: Provider;
14
+ roles: Role[];
15
+ name?: string;
16
+ description?: string;
17
+ /** total tokens the model will accept in one request */
18
+ contextLength?: number;
19
+ maxOutputTokens?: number;
20
+ /** embeddings only, when the vendor publishes it */
21
+ dimensions?: number;
22
+ modalities?: {
23
+ input?: string[];
24
+ output?: string[];
25
+ };
26
+ supports?: {
27
+ tools?: boolean;
28
+ reasoning?: boolean;
29
+ vision?: boolean;
30
+ };
31
+ /** USD per token, as the vendor writes it — strings, because they are tiny */
32
+ pricing?: {
33
+ prompt?: string;
34
+ completion?: string;
35
+ free?: boolean;
36
+ };
37
+ /** ISO date the model was published, when known */
38
+ created?: string;
39
+ source: 'live' | 'curated';
40
+ }
41
+ /** A provider's listing, and an honest account of where it came from. */
42
+ export interface Catalog {
43
+ provider: Provider;
44
+ entries: CatalogEntry[];
45
+ /** `live` asked just now; `cache` a fresh file; `stale` an expired one; `curated` the fallback */
46
+ origin: 'live' | 'cache' | 'stale' | 'curated';
47
+ /** when the entries were actually fetched, not when they were read */
48
+ fetchedAt: string;
49
+ /** why the live call was not used, when it was tried and failed */
50
+ problem?: KeyCheck;
51
+ }
52
+ /** A day. Model lists change on the scale of weeks; a stale row costs a retry. */
53
+ export declare const CATALOG_TTL_MS: number;
54
+ /**
55
+ * Enough to work with when the provider cannot be asked — offline, no
56
+ * credential, or a listing endpoint that is down. Deliberately short: this is
57
+ * the set worth typing, not the set that exists.
58
+ *
59
+ * Anthropic publishes no embeddings API at all, which is why it has no
60
+ * embedding row here and why the registry throws rather than guessing.
61
+ */
62
+ export declare const CURATED: Record<Provider, readonly Omit<CatalogEntry, 'ref' | 'provider' | 'source'>[]>;
63
+ /**
64
+ * The order `zen models pick` walks, per provider and per role.
65
+ *
66
+ * Cheap and fast first. `pick` exists to answer "give me something that works"
67
+ * in one round trip where it can, and the small models answer soonest and cost
68
+ * least when the answer is thrown away — which it always is.
69
+ */
70
+ export declare const PREFERRED: Record<Provider, {
71
+ chat: readonly string[];
72
+ embedding: readonly string[];
73
+ }>;
74
+ /**
75
+ * Asks one provider what it serves. Throws whatever the SDK throws.
76
+ *
77
+ * `client` is a seam, not a feature: the four adapters are the part most likely
78
+ * to break when a vendor reshapes a payload, and they are untestable if the
79
+ * only way to reach them is a credential and a network. It mirrors
80
+ * `ProviderSpec.client`, which exists in the library for the same reason.
81
+ */
82
+ export declare function fetchCatalog(provider: Provider, client?: unknown): Promise<CatalogEntry[]>;
83
+ export interface CatalogOptions {
84
+ /** ignore a fresh cache and ask the provider again */
85
+ refresh?: boolean;
86
+ /** do not make a network call at all — cache, however old, then curated */
87
+ offline?: boolean;
88
+ }
89
+ /**
90
+ * The listing for one provider, from the cheapest source that can answer.
91
+ *
92
+ * Fresh cache, else the provider, else a *stale* cache, else the curated table.
93
+ * Stale-before-curated is the important ordering: yesterday's real answer from
94
+ * this account beats today's guess about accounts in general, and a listing
95
+ * that failed because the wifi dropped should not silently shrink someone's
96
+ * model list to four rows.
97
+ */
98
+ export declare function loadCatalog(provider: Provider, opts?: CatalogOptions): Promise<Catalog>;
99
+ /** Every provider asked at once — they are independent and mostly latency. */
100
+ export declare function loadCatalogs(providers: readonly Provider[], opts?: CatalogOptions): Promise<Catalog[]>;
101
+ export interface Filters {
102
+ roles?: readonly Role[];
103
+ tools?: boolean;
104
+ vision?: boolean;
105
+ free?: boolean;
106
+ minContext?: number;
107
+ }
108
+ export declare function matches(row: CatalogEntry, query: string, filters?: Filters): boolean;
109
+ /** The providers a search covers when none was named. */
110
+ export declare const catalogProviders: () => readonly Provider[];
111
+ //# sourceMappingURL=catalog.d.ts.map