@zenera/cli 1.1.14 → 1.1.15

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
@@ -1,25 +1,96 @@
1
+ <div align="center">
2
+ <picture>
3
+ <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/andreyryabov/ZeneraNeo/main/docs/imgs/banner-neo-dark.svg">
4
+ <img src="https://raw.githubusercontent.com/andreyryabov/ZeneraNeo/main/docs/imgs/banner-neo-light.svg" alt="ZENERA NEO" width="720">
5
+ </picture>
6
+
7
+ [![npm](https://img.shields.io/npm/v/@zenera/cli.svg)](https://www.npmjs.com/package/@zenera/cli)
8
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/andreyryabov/ZeneraNeo/blob/main/LICENSE)
9
+ [![Node](https://img.shields.io/badge/node-%E2%89%A524-brightgreen.svg)](https://nodejs.org)
10
+
11
+ </div>
12
+
1
13
  # @zenera/cli
2
14
 
3
- **`zen` — put a team of AI agents to work on your problem, in one command.**
15
+ **`zen` — build specialist agents. Share them. Run them from the command line.**
4
16
 
5
- Describe the job in plain language and you get back a working multi-agent
6
- system: specialists that reason, read and write files, run real commands, look
7
- things up when they need to, and hand work to each other until the job is done.
8
- No framework to learn, no application to build, no glue code to maintain.
17
+ Not one general assistant that is passable at everything — a team built for the
18
+ work you actually keep doing, under a name you can type:
9
19
 
10
- The system is a folder — so it is yours. Commit it, review it, improve it, send
11
- it to a colleague, or start from one someone else already built and make it your
12
- own. It runs the same everywhere, on whichever models you prefer, and it never
13
- carries your keys with it.
20
+ ```sh
21
+ zen run accountant "Q3 — match the receipts drawer to the ledger, then update the return"
22
+ zen run accountant "this MSA against our playbook — redlines, and what I must not sign"
23
+ zen run analyst "why churn doubled in the EU accounts — the exports are in this folder"
24
+ zen run analyst "what changed in EU battery regulation this year, with sources"
25
+ zen run devops "this week's advisories — which of them actually reach our code"
26
+ zen run devops "the 03:12 outage — timeline, contributing factors, owners"
27
+ ```
14
28
 
15
- [![npm](https://img.shields.io/npm/v/@zenera/cli.svg)](https://www.npmjs.com/package/@zenera/cli)
16
- [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
17
- [![Node](https://img.shields.io/badge/node-%E2%89%A524-brightgreen.svg)](https://nodejs.org)
29
+ Three agents there, six things asked of them: an agent you have built is not a
30
+ script with one job, it is a specialist you keep going back to. And it is work
31
+ that comes back — every quarter, every sprint, every deal — which is exactly the
32
+ work nobody has shipped you a product for.
33
+
34
+ Once the agent exists it is a command: it works on the files you are standing
35
+ in, runs real commands in a container, searches the web when it has to, and
36
+ records every token it spent doing it.
37
+
38
+ Getting there is three beats:
39
+
40
+ 1. **Describe it.** You write `SPECIFICATION.md` — what the system is for,
41
+ which specialists it needs, what each may reach for, what _done_ means. The
42
+ rest of the folder is built to implement it.
43
+ 2. **Test it.** `zen check` validates the project and every file it names,
44
+ `zen run` exercises it for real, `zen inspect` opens the record of what it
45
+ actually did. When it comes out wrong you fix the specification, not the
46
+ prompts, and go round again.
47
+ 3. **Share it.** It is a folder — commit it, review it in a pull request, send
48
+ it. One `zen init <dir>` on a project that arrived by clone registers it
49
+ without touching a thing, and it is their command now too.
18
50
 
19
51
  > Part of [ZeneraNeo](https://github.com/andreyryabov/ZeneraNeo). This is an
20
52
  > open-source side project for experimentation and chore work — **not** the
21
53
  > official Zenera AI Platform. It carries no support or stability promises.
22
54
 
55
+ ## What you build
56
+
57
+ Half a page of specification turns into a system like one of these. Nothing here
58
+ ships with the CLI — each is a folder somebody wrote, and could send you.
59
+
60
+ | Project | The team inside it |
61
+ | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
62
+ | **accountant** | A bookkeeper that reads and classifies every receipt, statement, invoice and agreement in the folder; a reconciler that holds each against a ledger line or a playbook position and lists what has no pair; a preparer that fills the return or drafts the redlines; a checker that refuses anything no document substantiates. |
63
+ | **analyst** | A planner that splits the question into branches; researchers that run in parallel over your files and the web; a statistician that writes and runs Python in the sandbox; an editor that joins it into one answer where every claim carries its source. |
64
+ | **devops** | A gatherer that pulls advisories, logs, alerts and deploys into one picture; a tracer that works out whether a vulnerable path is reachable at all, or which change actually broke it; an engineer that plans the fix, applies it a step at a time, and stops the moment a check fails. |
65
+
66
+ Build one, commit it, and it runs the same on anyone else's machine — on
67
+ whichever models they prefer, and without carrying your keys.
68
+
69
+ ## What makes it different
70
+
71
+ A coding agent with custom instructions can already play several roles, and a
72
+ framework can already wire agents together. What neither gives you:
73
+
74
+ - **A system that outlives the session.** Custom instructions and subagents live
75
+ in a chat that ends. Here the system is a directory — versioned, reviewable,
76
+ diffable, sendable. Improving it is a commit, not a paste.
77
+ - **A specification that is enforced, not decorative.** `SPECIFICATION.md` is
78
+ the source; where it and the files disagree, it wins. `/sync-with-spec`
79
+ reconciles the two in both directions and `zen check` fails the project when
80
+ they drift — with a code, a location and a fix per finding. A prose document
81
+ nobody checks is a README; this one is checked.
82
+ - **No application to build.** Frameworks hand you a library and leave you
83
+ owning a program: graph code, state plumbing, retries, transport. Here an
84
+ agent is an instruction, a model, tools, skills, who it may hand work to, and
85
+ how it fans out. That is the whole list, and it is YAML. There is no glue code
86
+ to maintain because there is no glue.
87
+ - **A complete record, by default.** Every run writes its input, output and
88
+ state, plus a self-contained `report.html` — the graph, every request, every
89
+ token, every cost. Not a tracing vendor you sign up for; a file in the folder.
90
+ - **Your keys stay yours.** They live in `~/.zenera`, never in the project. What
91
+ you share is the system; the recipient runs it on their own credentials and
92
+ their own choice of vendor.
93
+
23
94
  ## Install
24
95
 
25
96
  Node.js 24+. One command — the OpenAI, Anthropic, Google and OpenRouter SDKs
@@ -43,44 +114,165 @@ Four commands, from nothing to an answer:
43
114
  npm i -g @zenera/cli # every vendor SDK comes with it
44
115
  zen key add openai # asks for the key without showing it; stored in ~/.zenera
45
116
  zen init my-project # scaffolds a project and registers it
46
- cd my-project && zen run "introduce yourself"
117
+ zen run my-project "introduce yourself"
118
+ ```
119
+
120
+ The name works from anywhere after that, and the directory you are standing in
121
+ is what it works on:
122
+
123
+ ```sh
124
+ cd ~/code/some-repo
125
+ zen run my-project "summarise this repo and write NOTES.md"
47
126
  ```
48
127
 
49
- Then the rest of the loop:
128
+ Then, day to day:
50
129
 
51
130
  ```sh
52
- zen run # nothing to say yet — a full-screen terminal app (a TUI)
53
- zen check # validate the project and every file it names
131
+ zen run my-project # no prompt given: opens a full-screen terminal app (a TUI)
132
+ zen check my-project # validate the project and every file it names
54
133
  zen inspect # open the last run's report.html
55
134
  zen list --sessions # every project, its sessions and last run
56
- echo "triage this" | zen run --quiet | jq
135
+ echo "triage this" | zen run my-project --quiet | jq
57
136
  ```
58
137
 
59
- Or ask a question from wherever you are and get an answer back:
138
+ Standing inside the project, the name is optional: a bare `zen run`, `zen check`
139
+ or `zen inspect` means the one you are in.
140
+
141
+ Giving a prompt on the command line skips every question: it starts a fresh
142
+ session and uses the current directory as the workspace, with write access.
143
+ `--session`, `--workspace` and `--read-only` override that.
144
+
145
+ To change what the system does, open the project in your editor with
146
+ `zen open`, edit `SPECIFICATION.md` to say what you actually want built, and
147
+ send `/sync-with-spec` in the editor's chat. That is the next section.
148
+
149
+ ## You write the specification; a coding agent writes the system
150
+
151
+ `zen init` writes `SPECIFICATION.md` first, and everything else in the folder
152
+ implements it. Where the two disagree the specification wins, so a change to
153
+ what the system does starts there and not in `agents.yaml`.
154
+
155
+ The one `zen init` writes is not a heading list — it is a true specification of
156
+ the project just scaffolded, so you can read it against the files beside it
157
+ before changing a word:
158
+
159
+ ```markdown
160
+ ## Agents
161
+
162
+ One agent, `default`, which is the entry point and the whole system. It has
163
+ nobody to hand work to, because there is no second job to hand on.
164
+
165
+ ## Done means
166
+
167
+ - The change asked for is in the workspace, or the question is answered from
168
+ what is actually in it.
169
+ - Everything the agent claims a command did, that command actually did — it was
170
+ run, and its output was read.
171
+ - The reply names what changed, file by file, and says what it deliberately did
172
+ not do.
173
+ ```
174
+
175
+ ### How you actually edit it
176
+
177
+ `SPECIFICATION.md` is ordinary Markdown, so any editor will do — but the loop is
178
+ meant to be driven from a coding agent's chat, and `zen init` installs the
179
+ prompts that make each step a single command:
60
180
 
61
181
  ```sh
62
- cd ~/code/some-repo
63
- zen run my-project "summarise this repo and write NOTES.md"
182
+ zen open my-project # or, standing in the folder: zen open
183
+ ```
184
+
185
+ That launches the editor you already use: the one this terminal belongs to, or
186
+ `$ZENERA_EDITOR`, or the first of VS Code, Cursor, Windsurf, Zed, Sublime Text
187
+ or IntelliJ it can find. `--editor <cmd>` names one outright.
188
+
189
+ In the window that opens:
190
+
191
+ 1. **Edit `SPECIFICATION.md`** — what the system is for, which agents exist,
192
+ what each may reach for, and what _done_ means.
193
+ 2. **Open the chat panel and send `/sync-with-spec`.** It reads the
194
+ specification and every file implementing it, works out the difference in
195
+ both directions, and changes the smallest thing that closes each gap.
196
+ 3. **Read `SPECIFICATION-FEEDBACK.md`**, which it writes for anything it could
197
+ not do without guessing. Answer its questions by editing `SPECIFICATION.md`
198
+ — not by editing prompts — and send `/sync-with-spec` again.
199
+
200
+ Back in the terminal: `zen check`, then `zen run`.
201
+
202
+ <details>
203
+ <summary>The chat commands <code>zen init</code> installs</summary>
204
+
205
+ They are prompt files under `.github/prompts/`, which VS Code and its forks
206
+ offer as chat slash-commands. Both `zen init` and `zen open` write them fresh,
207
+ so they never go stale — and edits to them do not survive.
208
+
209
+ | In chat | Does |
210
+ | ----------------- | ----------------------------------------------------------- |
211
+ | `/sync-with-spec` | Makes every file match `SPECIFICATION.md`, both directions. |
212
+ | `/review-project` | Reads the project as a reviewer would, and reports. |
213
+ | `/new-agent` | Adds an agent — prompt, wiring and hand-offs. |
214
+ | `/new-skill` | Adds a skill under `agents/skills/`. |
215
+
216
+ In an editor that does not support prompt files, paste the contents of
217
+ `.github/prompts/sync-with-spec.prompt.md` into its chat instead — it is only a
218
+ prompt. Alongside them, `.github/copilot-instructions.md` is the standing brief
219
+ that explains this runtime to whatever agent is reading, and
220
+ `.github/skills/zen-cli/` is the CLI's own reference for it.
221
+
222
+ </details>
223
+
224
+ ### The loop
225
+
226
+ ```
227
+ edit SPECIFICATION.md → /sync-with-spec → zen check → scripts/_setup.sh
228
+ ↑ ↓
229
+ └──────────────── read the report ←──────── zen run ─────┘
64
230
  ```
65
231
 
66
- A prompt on the command line asks nothing: a fresh session, the directory you
67
- are standing in as the workspace, writable. `--session`, `--workspace` and
68
- `--read-only` override that.
232
+ The last turn is the one that matters. When a run comes out wrong, the fix is
233
+ the sentence that was missing from the specification; the prompt edit follows
234
+ from that. Prompts patched directly drift away from the document meant to
235
+ describe them, and a project whose specification is no longer true is a project
236
+ with no specification.
69
237
 
70
- Then open the folder in your editor and tell your coding agent what the system
71
- should do. It writes the agents; `zen run` runs them; `zen inspect` shows you
72
- every request, tool call and token it spent.
238
+ You are not expected to hand-author any of this. `zen check` is written to be
239
+ read by a model as much as by a person — every finding carries a code, a
240
+ location and its fix — so "fix my project" is one instruction. Full guide:
241
+ [docs/specification.md](https://github.com/andreyryabov/ZeneraNeo/blob/main/docs/specification.md).
73
242
 
74
243
  ## A worked example
75
244
 
76
- A two-agent system that reads a repository and writes a note about it — the
77
- whole thing, in three files.
245
+ From a sentence to a working two-agent system, without hand-writing any wiring.
78
246
 
79
247
  ```sh
80
248
  zen init repo-notes && cd repo-notes
249
+ zen open # opens this folder in your editor
250
+ ```
251
+
252
+ Replace `SPECIFICATION.md` with what you want. This is the whole input — no
253
+ YAML, no prompt files:
254
+
255
+ ```markdown
256
+ # Specification
257
+
258
+ ## Purpose
259
+
260
+ Read a codebase and leave a short written note about it in the workspace.
261
+
262
+ ## Agents
263
+
264
+ - `reader` — explores the workspace and forms a picture of it. May read files
265
+ and list directories, and nothing else. Hands off to `writer`.
266
+ - `writer` — turns that picture into `NOTES.md`. May write to the workspace.
267
+
268
+ ## Done means
269
+
270
+ - `NOTES.md` exists, is Markdown, and is under 40 lines.
271
+ - Every claim in it comes from a file that was actually read.
81
272
  ```
82
273
 
83
- `agents.yaml` — who exists, and what each may reach for:
274
+ Send `/sync-with-spec` in the editor's chat. It writes `agents.yaml` — who
275
+ exists, and what each may reach for:
84
276
 
85
277
  ```yaml
86
278
  default: reader
@@ -100,6 +292,11 @@ agents:
100
292
  handoffs: [reader] # a hand-off does not return by itself; give it a way back
101
293
  ```
102
294
 
295
+ — and one prompt file per agent.
296
+
297
+ <details>
298
+ <summary>The prompt files it writes</summary>
299
+
103
300
  `agents/prompts/reader.md`:
104
301
 
105
302
  ```markdown
@@ -115,6 +312,28 @@ You write the summary you were handed to `NOTES.md`, in Markdown, under 40
115
312
  lines. Then say where you put it and stop.
116
313
  ```
117
314
 
315
+ </details>
316
+
317
+ <details>
318
+ <summary>What each <code>agents.yaml</code> key means</summary>
319
+
320
+ | Key | Is |
321
+ | ------------- | --------------------------------------------------------------------- |
322
+ | `name` | how everything else refers to this agent |
323
+ | `description` | what it is for — read by the _other_ agents when deciding to hand off |
324
+ | `system` | its own brief, in prose, at `agents/prompts/<name>.md` |
325
+ | `model` | this agent's model; the top-level one otherwise |
326
+ | `tools` | what it may reach for — `workspace:*`, `sandbox:*`, a named tool |
327
+ | `handoffs` | who it may pass the work to |
328
+ | `skills` | knowledge pulled in mid-run instead of carried in every prompt |
329
+
330
+ The file is validated strictly at load: an unknown tool, a handoff to nobody, a
331
+ missing prompt file — each fails immediately, naming the offending key, rather
332
+ than surfacing three turns into a run as a confused model. Full reference:
333
+ [docs/agents-yaml.md](https://github.com/andreyryabov/ZeneraNeo/blob/main/docs/agents-yaml.md).
334
+
335
+ </details>
336
+
118
337
  Check it, then point it at a real directory:
119
338
 
120
339
  ```sh
@@ -124,13 +343,19 @@ zen run repo-notes "summarise this repo" # this directory is the workspace
124
343
  zen inspect --project repo-notes --open # what it actually did
125
344
  ```
126
345
 
127
- ## The idea
346
+ That is the end of it: `repo-notes` is now a command like any other, in any
347
+ directory, for as long as the folder exists. Commit the folder and whoever
348
+ clones it types `zen init repo-notes` once — a directory that is already a
349
+ project is registered, not rebuilt, and nothing in it is touched — and the name
350
+ is a command on their machine too.
128
351
 
129
- An **agentic project is a folder**. Prompts, agent wiring, skills and tool
130
- selections are files — Markdown and YAML — not code buried inside an
131
- application. That folder can be committed, copied to another machine, reviewed
132
- in a pull request, and handed to someone else who runs it with one command.
133
- Credentials live in `$HOME`, so the project never contains a secret.
352
+ ## A project is a folder
353
+
354
+ Prompts, agent wiring, skills and tool selections are all files — Markdown and
355
+ YAML — rather than code buried inside an application. So the folder can be
356
+ committed, copied to another machine, reviewed in a pull request, and handed to
357
+ someone else who runs it with one command. Credentials live in `$HOME`, never in
358
+ the project, so there is no secret to strip before sharing it.
134
359
 
135
360
  ```
136
361
  my-project/
@@ -149,15 +374,33 @@ my-project/
149
374
  runs/<id>/ input, output, state, report.html, meta
150
375
  ```
151
376
 
152
- `SPECIFICATION.md` comes first because the rest implements it. The one `zen init`
153
- writes is a true specification of the project it just scaffolded; replace it with
154
- what you are building, then run `/sync-with-spec` in your editor to make the
155
- files match. See
156
- [docs/specification.md](https://github.com/andreyryabov/ZeneraNeo/blob/main/docs/specification.md).
377
+ Everything above the `sessions/` line is the system; everything below it is what
378
+ happened when it ran. Nothing here is tied to the machine it was written on,
379
+ which is the point of the CLI: it makes agentic systems shareable the way
380
+ repositories are shareable.
157
381
 
158
- `zen init` also writes `.github/copilot-instructions.md` — a standing brief that
159
- explains this runtime to whatever coding agent you have open in that folder, so
160
- you are not expected to hand-author `agents.yaml`.
382
+ `zen init` also writes `.vscode/` and a `.github/` tree — a standing brief that
383
+ explains this runtime to whatever coding agent you open the folder with, plus
384
+ the prompts and skills it needs to keep the project matching its specification.
385
+
386
+ ## Concepts
387
+
388
+ - **Project** — a named directory holding a complete agent definition and the
389
+ sessions that have run against it. It is self-describing: `agents.yaml` is
390
+ what makes a directory a project, so moving or cloning it loses nothing.
391
+ - **Session** — a context that persists across runs: one workspace, one memory,
392
+ one store for large files, and a record of everything that happened, added to
393
+ as it goes. Resumable.
394
+ - **Run** — one prompt in, one answer out, inside a session. Recorded in full,
395
+ whether or not you were watching.
396
+ - **Workspace** — the directory the agents may read and write. A prompt given on
397
+ the command line uses the current directory; the TUI offers the session's own
398
+ empty folder instead, and asks before using anything outside it.
399
+ - **Keyring** — `~/.zenera/neo`, readable only by you. See
400
+ [Credentials](#credentials).
401
+ - **Cache** — `~/.zenera/neo/cache`, one place for work already done: vectors,
402
+ parses, model listings, generated mocks. Shared by every project on the
403
+ machine, and never emptied by anything but you. See [Cache](#cache--work-you-have-already-paid-for).
161
404
 
162
405
  ## Commands
163
406
 
@@ -176,12 +419,14 @@ you are not expected to hand-author `agents.yaml`.
176
419
  | `cache` | What work has been kept, and getting rid of it. |
177
420
  | `version` | CLI, library and Node versions. |
178
421
 
179
- Commands can also come from a package installed alongside this one, so a new
180
- capability is a subcommand rather than a new binary to remember — one thing on
181
- your path, one keyring, one name. `zen --help` lists them whether or not they
182
- are installed and says what to run if not; nothing is imported until you type
183
- the command, so an uninstalled one costs nothing and an installed one costs
184
- nothing until it is used.
422
+ ### Commands from other packages
423
+
424
+ A command can also come from a package installed alongside this one, so a new
425
+ capability arrives as a subcommand instead of another binary to remember: one
426
+ thing on your path, one keyring, one name. `zen --help` lists these whether or
427
+ not they are installed, and tells you what to install if not. Nothing is
428
+ imported until you type the command, so an uninstalled one costs you nothing and
429
+ an installed one costs nothing until it is used.
185
430
 
186
431
  | Command | Package | Does |
187
432
  | ------- | --------------- | ---------------------------------------------- |
@@ -203,15 +448,25 @@ configure. Details:
203
448
  ·
204
449
  [@zenera/rag](https://github.com/andreyryabov/ZeneraNeo/blob/main/packages/rag/README.md).
205
450
 
206
- Global flags: `-h/--help`, `-v/--version`, `--json`, `-C <dir>`. Exit codes: `0`
207
- ok, `1` the run failed, `2` bad invocation, `3` invalid project, `4` no usable
208
- credential, `5` sandbox unavailable.
209
-
210
- `stdout` is the answer, `stderr` is the narration, and `--json` is on every
211
- command — so `zen run … | jq` is a supported way to use it, not an accident.
451
+ ### Conventions
212
452
 
213
453
  The binary is installed under three names: `zen`, `zn` and `zenera`.
214
454
 
455
+ Global flags: `-h/--help`, `-v/--version`, `--json`, `-C <dir>`.
456
+
457
+ `stdout` carries the answer and `stderr` carries the narration, and `--json`
458
+ works on every command — so `zen run … | jq` is a supported way to use this, not
459
+ an accident.
460
+
461
+ | Exit code | Meaning |
462
+ | --------- | -------------------- |
463
+ | `0` | ok |
464
+ | `1` | the run failed |
465
+ | `2` | bad invocation |
466
+ | `3` | invalid project |
467
+ | `4` | no usable credential |
468
+ | `5` | sandbox unavailable |
469
+
215
470
  ## Credentials
216
471
 
217
472
  One keyring serves every provider, and a key goes in the same way whatever it
@@ -222,12 +477,14 @@ zen key add <provider> # asks for the key without showing it
222
477
  zen key add <provider> < key.txt # or pipe it in
223
478
  ```
224
479
 
225
- The value is never given as an argument — a command line is visible to anyone
480
+ The value is never passed as an argument. A command line is visible to anyone
226
481
  listing running processes, is saved in your shell history and is captured in CI
227
- logs — so piping it in and the hidden prompt are the only two ways. Entries live
228
- in `~/.zenera/neo/keys.json`, in a file only you can read, and are copied into
229
- the environment just before a run, so an environment variable you set yourself
230
- always wins and a project checked out on a machine without `zen` still runs.
482
+ logs — so the hidden prompt and piping it in are the only two ways.
483
+
484
+ Entries live in `~/.zenera/neo/keys.json`, in a file only you can read. They are
485
+ copied into the environment just before a run, which has two consequences worth
486
+ knowing: an environment variable you set yourself always wins, and a project
487
+ checked out on a machine without `zen` still runs.
231
488
 
232
489
  | Provider | The value is | Exported as |
233
490
  | ------------ | ------------------------------------- | -------------------------------- |
@@ -247,11 +504,13 @@ behind a proxy — is not a key that is wrong. `--no-check` skips the call.
247
504
  A model reference is `[provider[/api]:]model`, and the first segment names a
248
505
  **provider, not a vendor**. So `vertex:gemini-3.5-flash` and
249
506
  `google:gemini-3.5-flash` are the same model reached through two different
250
- services, needing two different credentials — and a bare `gpt-5.4-mini` goes to
251
- the default provider, `openai`. `zen check` resolves every reference in a
252
- project against what is stored, says which credential each one needs, and spends
253
- a few tokens asking each of them to answer — the only way to catch a model id
254
- this account is not served. `--no-models` stops before the asking.
507
+ services, needing two different credentials; a bare `gpt-5.4-mini` goes to the
508
+ default provider, `openai`.
509
+
510
+ `zen check` resolves every reference in a project against what is stored and
511
+ says which credential each one needs. It then spends a few tokens asking each
512
+ model to answer, which is the only way to catch a model id this account is not
513
+ served. `--no-models` stops before that.
255
514
 
256
515
  ### Which models you can use
257
516
 
@@ -267,15 +526,20 @@ zen models test vertex:gemini-embedding-001 # one real call, one verdict
267
526
  zen models pick --embedding # the first ref that answers, on stdout
268
527
  ```
269
528
 
270
- Listings are cached for a day in `~/.zenera/neo/cache`. When a provider cannot
271
- be asked the last listing is used and said to be stale; only if there was never
272
- one does a short built-in list stand in.
529
+ Listings are cached for a day in `~/.zenera/neo/cache`. If a provider cannot be
530
+ reached, the last listing is used and marked stale; only when there has never
531
+ been one does a short built-in list stand in.
273
532
 
274
- `test` distinguishes three failures, because they want three different actions.
275
- A **refused** model means the credential was rejected. A **blocked** one means
276
- the credential was accepted and the account then said no — an API switched off,
277
- an empty balance, a model this key was never granted — and the fix comes with
278
- it:
533
+ `zen models test` separates three kinds of failure, because each calls for a
534
+ different action:
535
+
536
+ - **refused** — the provider rejected the credential. Fix or replace the key.
537
+ - **blocked** — the credential was accepted and the account then said no: an API
538
+ switched off, an empty balance, a model this key was never granted. The key is
539
+ fine; the account needs changing, and the command that changes it is printed
540
+ with the error.
541
+ - **unknown** — the question never arrived (offline, proxy, timeout). Nothing is
542
+ wrong with the key; try again.
279
543
 
280
544
  ```
281
545
  $ zen models test vertex:gemini-embedding-001
@@ -285,164 +549,192 @@ error 1 of 1 did not answer
285
549
  find one that does: zen models pick --embedding
286
550
  ```
287
551
 
288
- `pick` tries a short list of candidates one at a time and stops at the first
289
- that works, printing the bare ref on stdout — so recovering from the above is
290
- one substitution, whether a person or an agent is doing it:
552
+ `zen models pick` tries a short list of candidates one at a time, stops at the
553
+ first that works, and prints the bare reference on stdout. So recovering from
554
+ the above is a single substitution, whether a person or an agent is doing it:
291
555
 
292
556
  ```sh
293
557
  zen rag schema index --embedding "$(zen models pick --embedding)" ./specs/*.yaml
294
558
  ```
295
559
 
296
- ### Vertex AI
560
+ ### Setting up each provider
297
561
 
298
- Vertex takes two kinds of credential, and you never say which you are giving:
299
- if the value is a path to a file that exists it is a service-account key, and
300
- otherwise it is treated as an API key. The name you choose for the entry has no
301
- say in it — `vertex/express` is simply an entry called `express`, exactly as
302
- `vertex/prod` is one called `prod`, and either name can hold either kind.
562
+ The shape is the same everywhere — `zen key add <provider>`, then answer the
563
+ prompt. Open the one you need for its specifics.
303
564
 
304
- The usual one is a **service-account JSON file** — give its path, not its
305
- contents. Run the command with nothing piped and it asks:
565
+ <details>
566
+ <summary><strong>OpenAI</strong> — one secret, exported as <code>OPENAI_API_KEY</code></summary>
306
567
 
307
568
  ```sh
308
- zen key add vertex --gcp-location us-central1
309
- # Paste the key, or a path to the file: /Users/you/keys/vertex-sa.json
569
+ zen key add openai
310
570
  ```
311
571
 
312
- The prompt is read by `zen`, not by your shell, so give a full path there — `~`
313
- is not expanded. In a script, pipe the path in instead:
572
+ `openai` is the default provider, so a reference with no prefix comes here:
573
+ `gpt-5.4-mini` and `openai:gpt-5.4-mini` are the same model.
574
+
575
+ </details>
576
+
577
+ <details>
578
+ <summary><strong>Anthropic</strong> — one secret, exported as <code>ANTHROPIC_API_KEY</code></summary>
314
579
 
315
580
  ```sh
316
- echo ~/keys/vertex-sa.json | zen key add vertex --gcp-location us-central1
581
+ zen key add anthropic
582
+ zen run --model anthropic:claude-sonnet-4-5 "summarise this repo"
317
583
  ```
318
584
 
319
- The file is copied into `~/.zenera/neo/keys/`, where only you can read it, so
320
- moving or cleaning up the original later cannot break it.
585
+ </details>
321
586
 
322
- - `--gcp-location <region>` is worth setting. It takes a concrete region, or one
323
- of the endpoints that route across regions: `us` and `eu` pool capacity while
324
- keeping processing inside that territory, `global` takes whatever is free and
325
- promises no residency. `global` pays about ten seconds of cold start on the
326
- first request each process makes — a region answers in about two.
327
- - Which models a location serves is per model, and not guessable. In one
328
- project, `gemini-embedding-2` answered at `us` but 404'd at `us-central1`,
329
- while `gemini-2.5-flash` did the opposite. New models often reach `global`,
330
- `us` and `eu` first. `zen models test vertex:<model>` is what settles it —
331
- `zen key add` only establishes that the credential itself works.
332
- - `--gcp-project <id>` is only needed when the `project_id` inside the file is not
333
- the project you want.
587
+ <details>
588
+ <summary><strong>OpenRouter</strong> — one secret, exported as <code>OPENROUTER_API_KEY</code></summary>
334
589
 
335
- The alternative is an **express-mode API key** — a single secret, stored under
336
- `VERTEX_API_KEY`. It is the Vertex console's way of handing out access without a
337
- service account, and it needs neither a project nor a region, so `--gcp-project`
338
- and `--gcp-location` mean nothing there and are not stored.
590
+ One credential for many vendors' models. The model id keeps its own vendor
591
+ prefix, after the `openrouter:` one:
339
592
 
340
- ### Gemini, three ways
593
+ ```sh
594
+ zen key add openrouter
595
+ zen models search haiku --tools --free
596
+ zen run --model openrouter:anthropic/claude-haiku-4.5 "summarise this repo"
597
+ ```
341
598
 
342
- The same Gemini models are reachable through three different credentials, and
343
- which one you hold decides the prefix a model reference needs.
599
+ </details>
344
600
 
345
- **AI Studio** — one key and nothing else to configure, the shortest way to a
346
- working `gemini-3.5-flash`:
601
+ <details>
602
+ <summary><strong>Google AI Studio</strong> — one secret, exported as <code>GEMINI_API_KEY</code></summary>
603
+
604
+ One key and nothing else to configure: the shortest way to a working Gemini
605
+ model.
347
606
 
348
607
  ```sh
349
- zen key add google # asks for the key without showing it
608
+ zen key add google
350
609
  zen check # google:gemini-3.5-flash now resolves
351
610
  ```
352
611
 
353
- **Vertex, service account** — what production usually runs on. Give the path
354
- and a region, because the file says which project it belongs to but never which
355
- region to call:
612
+ The same Gemini models are also served by Vertex AI, under a different
613
+ credential. Both can be configured at once — they are separate entries for
614
+ separate services, and the prefix on the model reference decides which is used:
356
615
 
357
616
  ```sh
358
- echo ~/keys/vertex-sa.json | zen key add vertex --gcp-location us-central1
617
+ zen run --model google:gemini-3.5-flash "summarise this repo"
618
+ zen run --model vertex:gemini-3.5-flash "summarise this repo"
359
619
  ```
360
620
 
361
- Add `--gcp-project` only when the `project_id` inside the file is not the one you
362
- want to bill:
621
+ </details>
622
+
623
+ <details>
624
+ <summary><strong>Vertex AI</strong> — a service-account file, or an express-mode key</summary>
625
+
626
+ Vertex accepts two kinds of credential, and you never declare which one you are
627
+ giving: if the value is a path to a file that exists it is treated as a
628
+ service-account key, otherwise as an API key. The name you give the entry has no
629
+ bearing on this — `vertex/express` is just an entry called `express`, exactly as
630
+ `vertex/prod` is one called `prod`, and either name can hold either kind.
631
+
632
+ **A service-account JSON file** is the usual one, and what production normally
633
+ runs on. Give its path, not its contents. Run the command with nothing piped and
634
+ it asks:
363
635
 
364
636
  ```sh
365
- echo ~/keys/vertex-sa.json \
366
- | zen key add vertex --gcp-project other-project --gcp-location europe-west4
637
+ zen key add vertex --gcp-location us-central1
638
+ # Paste the key, or a path to the file: /Users/you/keys/vertex-sa.json
367
639
  ```
368
640
 
369
- **Vertex, express mode** — paste the key at the prompt; no flags apply:
641
+ The prompt is read by `zen`, not by your shell, so give a full path there — `~`
642
+ is not expanded. In a script, pipe the path in instead:
370
643
 
371
644
  ```sh
372
- zen key add vertex
645
+ echo ~/keys/vertex-sa.json | zen key add vertex --gcp-location us-central1
373
646
  ```
374
647
 
375
- Holding several at once is the ordinary case. Name them and switch:
648
+ The file is copied into `~/.zenera/neo/keys/`, where only you can read it, so
649
+ moving or cleaning up the original later cannot break it.
376
650
 
377
- ```sh
378
- echo ~/keys/prod-sa.json | zen key add vertex/prod --gcp-location us-central1
379
- echo ~/keys/dev-sa.json | zen key add vertex/dev --gcp-location global
380
- zen key add vertex/express # the express key, same provider
651
+ - `--gcp-location <region>` is worth setting, because the file says which project
652
+ it belongs to but never which region to call. It takes a concrete region, or
653
+ one of the endpoints that route across regions: `us` and `eu` pool capacity
654
+ while keeping processing inside that territory, and `global` takes whatever is
655
+ free and promises no residency. `global` costs about ten seconds of cold start
656
+ on the first request each process makes; a named region answers in about two.
657
+ - Which models a location serves varies per model and is not guessable. In one
658
+ project, `gemini-embedding-2` answered at `us` but 404'd at `us-central1`,
659
+ while `gemini-2.5-flash` did the opposite. New models often reach `global`,
660
+ `us` and `eu` first. `zen models test vertex:<model>` is what settles it —
661
+ `zen key add` only establishes that the credential itself works.
662
+ - `--gcp-project <id>` is only needed when the `project_id` inside the file is
663
+ not the project you want to bill:
664
+
665
+ ```sh
666
+ echo ~/keys/vertex-sa.json \
667
+ | zen key add vertex --gcp-project other-project --gcp-location europe-west4
668
+ ```
669
+
670
+ **An express-mode API key** is the alternative: a single secret, stored under
671
+ `VERTEX_API_KEY`. It is the Vertex console's way of granting access without a
672
+ service account, and it needs neither a project nor a region, so `--gcp-project`
673
+ and `--gcp-location` mean nothing there and are not stored.
381
674
 
382
- zen key use vertex/dev # which one the next run uses
383
- zen key ls --check # all three, and whether they still work
384
- zen key show vertex/prod # masked; --reveal prints the path
675
+ ```sh
676
+ zen key add vertex # paste the key at the prompt; no flags apply
385
677
  ```
386
678
 
387
- `google` and `vertex` can both be configured — they are separate entries for
388
- separate services, and the reference picks:
679
+ **An existing `gcloud` login counts too.** If you have run
680
+ `gcloud auth application-default login`, Vertex works with nothing stored at
681
+ all: `zen key ls` shows that login as `adc`, marked `~` because it came from
682
+ outside the keyring. Anything already set in `GOOGLE_APPLICATION_CREDENTIALS`,
683
+ `VERTEX_API_KEY` or `GEMINI_API_KEY` is listed the same way and wins over the
684
+ keyring, so it is always visible which credential a run will actually use.
685
+
686
+ </details>
687
+
688
+ <details>
689
+ <summary><strong>Exa</strong> — one secret, exported as <code>EXA_API_KEY</code></summary>
690
+
691
+ Not a model provider: this is what backs the search tool an agent reaches for
692
+ when it has to look something up on the web.
389
693
 
390
694
  ```sh
391
- zen run --model google:gemini-3.5-flash "summarise this repo"
392
- zen run --model vertex:gemini-3.5-flash "summarise this repo"
695
+ zen key add exa
393
696
  ```
394
697
 
395
- If you have already run `gcloud auth application-default login`, that login is
396
- itself a usable credential: `zen key ls` shows it as `adc`, marked `~` because
397
- it came from outside the keyring, and Vertex works with nothing stored at all.
398
- Anything already set in `GOOGLE_APPLICATION_CREDENTIALS`, `VERTEX_API_KEY` or
399
- `GEMINI_API_KEY` is listed the same way and wins over the keyring, so it is
400
- always visible which credential a run will actually use.
698
+ </details>
401
699
 
402
- ### More than one key per provider
700
+ <details>
701
+ <summary><strong>More than one key for the same provider</strong></summary>
403
702
 
404
- Entries are named, so a provider can hold several and one of them is active:
703
+ Entries are named, so a provider can hold several, one of which is active:
405
704
 
406
705
  ```sh
407
706
  zen key add openai/work # a second entry
408
- zen key use openai/work # which one runs use
707
+ zen key use openai/work # the one that runs will use
409
708
  zen key ls --check # everything stored, and whether it still works
410
709
  zen key show vertex/default # masked — --reveal prints the secret
411
710
  zen key env openai # shell exports, for other tools
412
711
  zen key rm openai/work
413
712
  ```
414
713
 
714
+ It is the same for Vertex, where holding several at once is the ordinary case:
715
+
716
+ ```sh
717
+ echo ~/keys/prod-sa.json | zen key add vertex/prod --gcp-location us-central1
718
+ echo ~/keys/dev-sa.json | zen key add vertex/dev --gcp-location global
719
+ zen key add vertex/express # the express key, same provider
720
+
721
+ zen key use vertex/dev # which one the next run uses
722
+ ```
723
+
415
724
  `zen key ls` marks the active entry with `*`, and anything it found outside the
416
725
  keyring — in your environment, or in a `gcloud` login — with `~`, so it is always
417
726
  clear where a working provider actually comes from.
418
727
 
419
- ## Concepts
728
+ </details>
420
729
 
421
- - **Project** — a named directory holding a complete agent definition and the
422
- sessions that ran against it. Self-describing: `agents.yaml` is what makes it
423
- one, so moving or cloning the directory loses nothing.
424
- - **Session** — a context that persists: one workspace, one memory, one store
425
- for large files, and a record of everything that happened, added to as it
426
- goes. Resumable.
427
- - **Run** — one prompt in, one answer out, inside a session. Recorded in full,
428
- whether or not you were watching.
429
- - **Workspace** — the directory the agents may read and write. A prompt given on
430
- the command line uses the current directory; the TUI offers the session's own
431
- empty folder and confirms anything outside it.
432
- - **Keyring** — `~/.zenera/neo`, readable only by you. Keys are copied into the
433
- environment just before a run, so an environment variable you set yourself
434
- always wins and a project checked out on a machine without `zen` still runs.
435
- - **Cache** — `~/.zenera/neo/cache`, one place for work already done: vectors,
436
- parses, model listings, generated mocks. Shared by every project on the
437
- machine, and never evicted by anything but you.
730
+ ## Cache — work you have already paid for
438
731
 
439
- ## What has already been paid for
440
-
441
- Embedding a paragraph, parsing a document, asking a provider what it serves —
442
- all expensive, all perfectly repeatable. They are kept in one store,
443
- `~/.zenera/neo/cache/<kind>/`, keyed by every input that produced them. Change
444
- an input and you are asking a different question, which is why nothing in there
445
- is ever invalidated: it is simply never asked for again.
732
+ Embedding a paragraph, parsing a document, asking a provider what it serves: all
733
+ of these are slow or costly, and all of them give the same answer every time. So
734
+ the answers are kept in `~/.zenera/neo/cache/<kind>/`, under a key built from
735
+ every input that produced them. Change any input and the key changes, so you get
736
+ a fresh answer rather than a stale one. That is why entries are never
737
+ invalidated: one that no longer matches anything is simply never read again.
446
738
 
447
739
  ```
448
740
  zen cache ls # what is kept, by kind
@@ -452,12 +744,14 @@ zen cache prune --max-size 2GB # or keep it under a ceiling
452
744
  zen cache clear --kind docs-parse # throw one kind away
453
745
  ```
454
746
 
455
- Age is when an entry was last _used_, not when it was written, so a vector a
456
- weekly rebuild reads is never old. Nothing evicts on its own — a store that
457
- quietly deletes things is only ever noticed when it has deleted the wrong one —
458
- so retention is a decision made out loud, here. Nothing in it is precious
459
- either: every entry is work that can be done again, and the only cost of
460
- removing one is paying for it a second time.
747
+ Age means when an entry was last _read_, not when it was written, so a vector
748
+ that a weekly rebuild keeps using never counts as old.
749
+
750
+ Nothing is ever evicted automatically. A store that quietly deletes things is
751
+ only noticed once it has deleted the wrong one, so every removal is something
752
+ you ask for. Nothing in the cache is precious either: every entry is work that
753
+ can be done again, and the only cost of deleting one is paying for it a second
754
+ time.
461
755
 
462
756
  ## The library underneath
463
757
 
@@ -470,19 +764,18 @@ terminal:
470
764
 
471
765
  ## Documentation
472
766
 
473
- [docs/specification.md](https://github.com/andreyryabov/ZeneraNeo/blob/main/docs/specification.md)
474
- — writing the specification the project implements ·
475
- [docs/projects.md](https://github.com/andreyryabov/ZeneraNeo/blob/main/docs/projects.md)
476
- — the folder a project is ·
477
- [docs/agents-yaml.md](https://github.com/andreyryabov/ZeneraNeo/blob/main/docs/agents-yaml.md)
478
- — every key in the configuration file.
479
-
480
- [docs/knowledge.md](https://github.com/andreyryabov/ZeneraNeo/blob/main/docs/knowledge.md)
481
- — a searchable document index ·
482
- [docs/integrations.md](https://github.com/andreyryabov/ZeneraNeo/blob/main/docs/integrations.md)
483
- — an API description, searchable and mockable ·
484
- [docs/memory.md](https://github.com/andreyryabov/ZeneraNeo/blob/main/docs/memory.md)
485
- — what agents keep between sessions.
767
+ - [docs/specification.md](https://github.com/andreyryabov/ZeneraNeo/blob/main/docs/specification.md)
768
+ — writing the specification the project implements.
769
+ - [docs/projects.md](https://github.com/andreyryabov/ZeneraNeo/blob/main/docs/projects.md)
770
+ — what the project folder contains, file by file.
771
+ - [docs/agents-yaml.md](https://github.com/andreyryabov/ZeneraNeo/blob/main/docs/agents-yaml.md)
772
+ — every key in the configuration file.
773
+ - [docs/knowledge.md](https://github.com/andreyryabov/ZeneraNeo/blob/main/docs/knowledge.md)
774
+ — building a searchable document index.
775
+ - [docs/integrations.md](https://github.com/andreyryabov/ZeneraNeo/blob/main/docs/integrations.md)
776
+ — making an API description searchable and mockable.
777
+ - [docs/memory.md](https://github.com/andreyryabov/ZeneraNeo/blob/main/docs/memory.md)
778
+ — what agents keep between sessions.
486
779
 
487
780
  ## License
488
781
 
@@ -9,7 +9,7 @@ import { probeAll } from "../liveness.js";
9
9
  import { ensurePodmanReady } from "../podman.js";
10
10
  import { isProjectDir, Registry } from "../projects.js";
11
11
  import { scaffold } from "../scaffold.js";
12
- import { bold, cyan, dim, green, invalidError, json, note, progress, usageError, write, yellow, } from "../term.js";
12
+ import { bold, cyan, dim, green, json, note, progress, usageError, write, yellow, } from "../term.js";
13
13
  const USAGE = 'zen init [dir] [--name <name>] [--model <ref>] [--force]';
14
14
  /**
15
15
  * The model each provider gets scaffolded with, when it is the one chosen.
@@ -101,6 +101,11 @@ export const init = {
101
101
  'find it by name. Editor files (.vscode/settings.json and the .github/',
102
102
  'tree) are written alongside, replacing any already there.',
103
103
  '',
104
+ 'A directory that is already a project is adopted rather than rebuilt:',
105
+ 'it is registered under its own name, and nothing in it is touched.',
106
+ 'That is how a project someone else wrote becomes a name on this',
107
+ 'machine after a clone.',
108
+ '',
104
109
  'SPECIFICATION.md states what the project is for and what it may do,',
105
110
  'and describes the one that was just written. Say what you are building',
106
111
  'there, then run /sync-with-spec in your editor to make the rest match.',
@@ -122,7 +127,8 @@ export const init = {
122
127
  const dir = resolve(ctx.cwd, one(positionals, 'directory', USAGE) ?? '.');
123
128
  const name = values.name ?? basename(dir);
124
129
  if (isProjectDir(dir)) {
125
- throw invalidError(`${dir} is already a project`);
130
+ await adopt(dir, values, ctx.json);
131
+ return;
126
132
  }
127
133
  if (existsSync(dir) && readdirSync(dir).length > 0 && !values.force) {
128
134
  throw usageError(`${dir} is not empty`, 'pass --force to write into it anyway');
@@ -177,6 +183,44 @@ export const init = {
177
183
  write(dir);
178
184
  },
179
185
  };
186
+ /**
187
+ * Takes a directory that is already a project into the registry.
188
+ *
189
+ * The registry lives in `$HOME`, so a project that arrives by clone, copy or
190
+ * `mv` is a working project that no name resolves to — and the only thing
191
+ * missing is the entry. Scaffolding over it would be the wrong repair: the
192
+ * files are somebody's work, and this writes none of them.
193
+ *
194
+ * Idempotent, because the honest answer to `zen init` in a project that is
195
+ * already listed is "it is", not an error.
196
+ */
197
+ async function adopt(dir, values, quiet) {
198
+ ensureHome();
199
+ const registry = await Registry.open();
200
+ const listed = registry.findPath(dir);
201
+ const name = values.name ?? listed?.name ?? basename(dir);
202
+ const renamed = listed !== undefined && listed.name !== name;
203
+ registry.add(name, dir);
204
+ registry.save();
205
+ const sandbox = await prepareSandbox(dir, quiet);
206
+ if (quiet) {
207
+ json({ name, path: dir, adopted: true, listed: listed !== undefined, sandbox });
208
+ return;
209
+ }
210
+ const what = listed === undefined
211
+ ? green('registered')
212
+ : renamed
213
+ ? green('renamed')
214
+ : dim('already registered');
215
+ note(`${what} ${bold(name)} ${dim(dir)}`);
216
+ // Nothing here was scaffolded, so the flags that shape a scaffold did not apply.
217
+ if (values.model) {
218
+ note(`${yellow('--model ignored')} ${dim('— the model this project uses is in agents.yaml')}`);
219
+ }
220
+ note();
221
+ note(`Next: ${cyan(`cd ${dir}`)} then ${cyan('zen check')}`);
222
+ write(dir);
223
+ }
180
224
  /**
181
225
  * Builds the sandbox image the project was just given, here rather than on the
182
226
  * first run.
package/dist/validate.js CHANGED
@@ -88,7 +88,7 @@ export async function validateProject(opts) {
88
88
  where: root,
89
89
  message: 'this directory is not registered, so `zen open` and `zen list` will not ' +
90
90
  'find it by name (naming it by path, as now, works either way)',
91
- fix: `register it: zen init ${root} --force`,
91
+ fix: `register it: zen init ${root}`,
92
92
  });
93
93
  }
94
94
  // -----------------------------------------------------------------------
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zenera/cli",
3
- "version": "1.1.14",
3
+ "version": "1.1.15",
4
4
  "description": "Command-line front end for @zenera/neo: agentic projects you can run, share and commit.",
5
5
  "keywords": [
6
6
  "agents",
@@ -51,7 +51,7 @@
51
51
  "@inkjs/ui": "^2.0.0",
52
52
  "ink": "^7.1.1",
53
53
  "react": "^19.2.8",
54
- "@zenera/neo": "^1.1.14",
54
+ "@zenera/neo": "^1.1.15",
55
55
  "@anthropic-ai/sdk": "^0.120.0",
56
56
  "@google/genai": "^2.18.0",
57
57
  "@openrouter/sdk": "^1.2.80",