@zenera/cli 1.1.0
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/LICENSE +21 -0
- package/README.md +239 -0
- package/dist/args.d.ts +40 -0
- package/dist/args.js +99 -0
- package/dist/audit.d.ts +53 -0
- package/dist/audit.js +144 -0
- package/dist/banner.d.ts +13 -0
- package/dist/banner.js +103 -0
- package/dist/command.d.ts +14 -0
- package/dist/command.js +12 -0
- package/dist/commands/check.d.ts +3 -0
- package/dist/commands/check.js +287 -0
- package/dist/commands/index.d.ts +22 -0
- package/dist/commands/index.js +56 -0
- package/dist/commands/init.d.ts +3 -0
- package/dist/commands/init.js +157 -0
- package/dist/commands/inspect.d.ts +3 -0
- package/dist/commands/inspect.js +158 -0
- package/dist/commands/key.d.ts +3 -0
- package/dist/commands/key.js +335 -0
- package/dist/commands/list.d.ts +3 -0
- package/dist/commands/list.js +101 -0
- package/dist/commands/models.d.ts +9 -0
- package/dist/commands/models.js +120 -0
- package/dist/commands/open.d.ts +9 -0
- package/dist/commands/open.js +270 -0
- package/dist/commands/run.d.ts +3 -0
- package/dist/commands/run.js +167 -0
- package/dist/commands/sandbox.d.ts +3 -0
- package/dist/commands/sandbox.js +112 -0
- package/dist/commands/version.d.ts +6 -0
- package/dist/commands/version.js +39 -0
- package/dist/engine.d.ts +49 -0
- package/dist/engine.js +208 -0
- package/dist/external.d.ts +10 -0
- package/dist/external.js +56 -0
- package/dist/home.d.ts +31 -0
- package/dist/home.js +108 -0
- package/dist/ids.d.ts +12 -0
- package/dist/ids.js +44 -0
- package/dist/keys.d.ts +124 -0
- package/dist/keys.js +309 -0
- package/dist/lib.d.ts +9 -0
- package/dist/lib.js +31 -0
- package/dist/liveness.d.ts +23 -0
- package/dist/liveness.js +221 -0
- package/dist/main.d.ts +3 -0
- package/dist/main.js +155 -0
- package/dist/narrate.d.ts +19 -0
- package/dist/narrate.js +124 -0
- package/dist/podman.d.ts +46 -0
- package/dist/podman.js +254 -0
- package/dist/projects.d.ts +70 -0
- package/dist/projects.js +232 -0
- package/dist/resolve.d.ts +27 -0
- package/dist/resolve.js +138 -0
- package/dist/sandbox.d.ts +36 -0
- package/dist/sandbox.js +104 -0
- package/dist/scaffold.d.ts +29 -0
- package/dist/scaffold.js +220 -0
- package/dist/session.d.ts +77 -0
- package/dist/session.js +156 -0
- package/dist/term.d.ts +69 -0
- package/dist/term.js +242 -0
- package/dist/tui/app.d.ts +8 -0
- package/dist/tui/app.js +257 -0
- package/dist/tui/theme.d.ts +23 -0
- package/dist/tui/theme.js +134 -0
- package/dist/tui/wrap.d.ts +12 -0
- package/dist/tui/wrap.js +62 -0
- package/dist/validate.d.ts +145 -0
- package/dist/validate.js +959 -0
- package/package.json +76 -0
- package/templates/.github/copilot-instructions.md +1579 -0
- package/templates/.github/prompts/new-agent.prompt.md +38 -0
- package/templates/.github/prompts/new-skill.prompt.md +37 -0
- package/templates/.github/prompts/review-project.prompt.md +31 -0
- package/templates/.github/skills/zen-cli/SKILL.md +110 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Andrey Ryabov
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
# @zenera/cli
|
|
2
|
+
|
|
3
|
+
**`zen` — put a team of AI agents to work on your problem, in one command.**
|
|
4
|
+
|
|
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.
|
|
9
|
+
|
|
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.
|
|
14
|
+
|
|
15
|
+
[](https://www.npmjs.com/package/@zenera/cli)
|
|
16
|
+
[](LICENSE)
|
|
17
|
+
[](https://nodejs.org)
|
|
18
|
+
|
|
19
|
+
> Part of [ZeneraNeo](https://github.com/andreyryabov/ZeneraNeo). This is an
|
|
20
|
+
> open-source side project for experimentation and chore work — **not** the
|
|
21
|
+
> official Zenera AI Platform. It carries no support or stability promises.
|
|
22
|
+
|
|
23
|
+
## Install
|
|
24
|
+
|
|
25
|
+
Node.js 24+. Install the CLI together with at least one vendor SDK — they are
|
|
26
|
+
**optional peer dependencies**, so you only pay for the ones you use.
|
|
27
|
+
|
|
28
|
+
```sh
|
|
29
|
+
npm i -g @zenera/cli openai
|
|
30
|
+
# or @anthropic-ai/sdk, @google/genai, @openrouter/sdk — any mix of them
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Or without installing anything:
|
|
34
|
+
|
|
35
|
+
```sh
|
|
36
|
+
npx @zenera/cli --help
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Quickstart
|
|
40
|
+
|
|
41
|
+
Four commands, from nothing to an answer:
|
|
42
|
+
|
|
43
|
+
```sh
|
|
44
|
+
npm i -g @zenera/cli openai # the CLI, plus one vendor SDK
|
|
45
|
+
zen key add openai # prompts with the echo off; stored in ~/.zenera
|
|
46
|
+
zen init my-project # scaffolds a project and registers it
|
|
47
|
+
cd my-project && zen run "introduce yourself"
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Then the rest of the loop:
|
|
51
|
+
|
|
52
|
+
```sh
|
|
53
|
+
zen run # nothing to say yet — a TUI on a terminal
|
|
54
|
+
zen check # validate the project and every file it names
|
|
55
|
+
zen inspect # open the last run's report.html
|
|
56
|
+
zen list --sessions # every project, its sessions and last run
|
|
57
|
+
echo "triage this" | zen run --quiet | jq
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Or ask a question from wherever you are and get an answer back:
|
|
61
|
+
|
|
62
|
+
```sh
|
|
63
|
+
cd ~/code/some-repo
|
|
64
|
+
zen run my-project "summarise this repo and write NOTES.md"
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
A prompt on the command line asks nothing: a fresh session, the directory you
|
|
68
|
+
are standing in as the workspace, writable. `--session`, `--workspace` and
|
|
69
|
+
`--read-only` override that.
|
|
70
|
+
|
|
71
|
+
Then open the folder in your editor and tell your coding agent what the system
|
|
72
|
+
should do. It writes the agents; `zen run` runs them; `zen inspect` shows you
|
|
73
|
+
every request, tool call and token it spent.
|
|
74
|
+
|
|
75
|
+
## A worked example
|
|
76
|
+
|
|
77
|
+
A two-agent system that reads a repository and writes a note about it — the
|
|
78
|
+
whole thing, in three files.
|
|
79
|
+
|
|
80
|
+
```sh
|
|
81
|
+
zen init repo-notes && cd repo-notes
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
`agents.yaml` — who exists, and what each may reach for:
|
|
85
|
+
|
|
86
|
+
```yaml
|
|
87
|
+
default: reader
|
|
88
|
+
model: openai:gpt-5.4-mini
|
|
89
|
+
|
|
90
|
+
agents:
|
|
91
|
+
- name: reader
|
|
92
|
+
description: Reads the workspace and summarises what is in it.
|
|
93
|
+
system: agents/prompts/reader.md
|
|
94
|
+
tools: [workspace:read_file, workspace:list_dir, workspace:find_files]
|
|
95
|
+
handoffs: [writer]
|
|
96
|
+
|
|
97
|
+
- name: writer
|
|
98
|
+
description: Turns a summary into a file on disk.
|
|
99
|
+
system: agents/prompts/writer.md
|
|
100
|
+
tools: [workspace:*]
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
`agents/prompts/reader.md`:
|
|
104
|
+
|
|
105
|
+
```markdown
|
|
106
|
+
You explore a codebase and describe it plainly: what it is, how it is laid out,
|
|
107
|
+
how it is built and tested. Read before you conclude. When you have a picture,
|
|
108
|
+
hand off to `writer`.
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
`agents/prompts/writer.md`:
|
|
112
|
+
|
|
113
|
+
```markdown
|
|
114
|
+
You write the summary you were handed to `NOTES.md`, in Markdown, under 40
|
|
115
|
+
lines. Then say where you put it and stop.
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Check it, then point it at a real directory:
|
|
119
|
+
|
|
120
|
+
```sh
|
|
121
|
+
zen check # every file it names, validated
|
|
122
|
+
cd ~/code/some-repo
|
|
123
|
+
zen run repo-notes "summarise this repo" # this directory is the workspace
|
|
124
|
+
zen inspect --project repo-notes --open # what it actually did
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
## The idea
|
|
128
|
+
|
|
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.
|
|
134
|
+
|
|
135
|
+
```
|
|
136
|
+
my-project/
|
|
137
|
+
INSTRUCTIONS.md house rules, prepended to every agent
|
|
138
|
+
agents.yaml who exists, what they may reach for
|
|
139
|
+
agents/
|
|
140
|
+
prompts/<name>.md each agent's own brief
|
|
141
|
+
skills/<name>/ knowledge loaded on demand, not always-on
|
|
142
|
+
sessions/ one workspace, memory and trajectory each
|
|
143
|
+
<id>/
|
|
144
|
+
workspace/ what the agents can read and write
|
|
145
|
+
runs/<id>/ input, output, state, report.html, meta
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
`zen init` also writes `.github/copilot-instructions.md` — a standing brief that
|
|
149
|
+
explains this runtime to whatever coding agent you have open in that folder, so
|
|
150
|
+
you are not expected to hand-author `agents.yaml`.
|
|
151
|
+
|
|
152
|
+
## Commands
|
|
153
|
+
|
|
154
|
+
| Command | Does |
|
|
155
|
+
| --------- | ------------------------------------------------------------------------ |
|
|
156
|
+
| `init` | Creates a project here, or in `<dir>`, and registers it. |
|
|
157
|
+
| `list` | Every known project: sessions, last run, whether one is live. |
|
|
158
|
+
| `open` | Opens a project in your editor. |
|
|
159
|
+
| `key` | The credential keyring — add, check, switch, remove. |
|
|
160
|
+
| `run` | Runs the project — the TUI on a terminal, one shot otherwise. |
|
|
161
|
+
| `inspect` | Opens or rebuilds a run's `report.html`. |
|
|
162
|
+
| `models` | Resolves providers and models and validates the config, calling nothing. |
|
|
163
|
+
| `sandbox` | Checks and prepares the container that command-line tools run in. |
|
|
164
|
+
| `version` | CLI, library and Node versions. |
|
|
165
|
+
|
|
166
|
+
Commands can also come from a package installed alongside this one, so a new
|
|
167
|
+
capability is a subcommand rather than a new binary to remember — one thing on
|
|
168
|
+
your path, one keyring, one name. `zen --help` lists them whether or not they
|
|
169
|
+
are installed and says what to run if not; nothing is imported until you type
|
|
170
|
+
the command, so an uninstalled one costs nothing and an installed one costs
|
|
171
|
+
nothing until it is used.
|
|
172
|
+
|
|
173
|
+
| Command | Package | Does |
|
|
174
|
+
| ------- | --------------- | ---------------------------------------------- |
|
|
175
|
+
| `faker` | `@zenera/faker` | A mock API from an openapi/swagger document. |
|
|
176
|
+
| `rag` | `@zenera/rag` | Search an openapi/swagger document as a graph. |
|
|
177
|
+
|
|
178
|
+
```sh
|
|
179
|
+
npm i -g @zenera/faker
|
|
180
|
+
zen faker serve api/openapi.yaml --port 8787 # a working mock, bodies written by a model
|
|
181
|
+
|
|
182
|
+
npm i -g @zenera/rag
|
|
183
|
+
zen rag schema index --embedding openai:text-embedding-3-small ./specs/*.yaml
|
|
184
|
+
zen rag schema search --output-property "user billing history" --format ts
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
They use this keyring and these credentials, so there is nothing new to
|
|
188
|
+
configure. Details:
|
|
189
|
+
[@zenera/faker](https://github.com/andreyryabov/ZeneraNeo/blob/main/packages/faker/README.md)
|
|
190
|
+
·
|
|
191
|
+
[@zenera/rag](https://github.com/andreyryabov/ZeneraNeo/blob/main/packages/rag/README.md).
|
|
192
|
+
|
|
193
|
+
Global flags: `-h/--help`, `-v/--version`, `--json`, `-C <dir>`. Exit codes: `0`
|
|
194
|
+
ok, `1` the run failed, `2` bad invocation, `3` invalid project, `4` no usable
|
|
195
|
+
credential, `5` sandbox unavailable.
|
|
196
|
+
|
|
197
|
+
`stdout` is the answer, `stderr` is the narration, and `--json` is on every
|
|
198
|
+
command — so `zen run … | jq` is a supported way to use it, not an accident.
|
|
199
|
+
|
|
200
|
+
The binary is installed under three names: `zen`, `zn` and `zenera`.
|
|
201
|
+
|
|
202
|
+
## Concepts
|
|
203
|
+
|
|
204
|
+
- **Project** — a named directory holding a complete agent definition and the
|
|
205
|
+
sessions that ran against it. Self-describing: `agents.yaml` is what makes it
|
|
206
|
+
one, so moving or cloning the directory loses nothing.
|
|
207
|
+
- **Session** — a context that persists: one workspace, one memory, one blob
|
|
208
|
+
store, one accumulating trajectory. Resumable.
|
|
209
|
+
- **Run** — one prompt in, one answer out, inside a session. Recorded in full,
|
|
210
|
+
whether or not you were watching.
|
|
211
|
+
- **Workspace** — the directory the agents may read and write. A prompt given on
|
|
212
|
+
the command line uses the current directory; the TUI offers the session's own
|
|
213
|
+
empty folder and confirms anything outside it.
|
|
214
|
+
- **Keyring** — `~/.zenera/neo`, mode `0700`. Keys are materialised into the
|
|
215
|
+
environment just before a run, so a real env var always wins and a project
|
|
216
|
+
checked out on a machine without `zen` still runs.
|
|
217
|
+
|
|
218
|
+
## The library underneath
|
|
219
|
+
|
|
220
|
+
This is a shell over
|
|
221
|
+
[`@zenera/neo`](https://www.npmjs.com/package/@zenera/neo) — agents, models,
|
|
222
|
+
tools, skills, memory and an append-only trajectory. Use it directly when you
|
|
223
|
+
want the runtime inside your own application rather than on a terminal:
|
|
224
|
+
[its README](https://github.com/andreyryabov/ZeneraNeo/blob/main/packages/neo/README.md).
|
|
225
|
+
|
|
226
|
+
## Documentation
|
|
227
|
+
|
|
228
|
+
Full specification:
|
|
229
|
+
[packages/cli/DESIGN.md](https://github.com/andreyryabov/ZeneraNeo/blob/main/packages/cli/DESIGN.md).
|
|
230
|
+
Also
|
|
231
|
+
[DESIGN.md](https://github.com/andreyryabov/ZeneraNeo/blob/main/DESIGN.md) ·
|
|
232
|
+
[docs/projects.md](https://github.com/andreyryabov/ZeneraNeo/blob/main/docs/projects.md)
|
|
233
|
+
·
|
|
234
|
+
[docs/agents-yaml.md](https://github.com/andreyryabov/ZeneraNeo/blob/main/docs/agents-yaml.md).
|
|
235
|
+
|
|
236
|
+
## License
|
|
237
|
+
|
|
238
|
+
Early days and moving fast — issues, questions and pull requests are welcome.
|
|
239
|
+
[MIT](LICENSE).
|
package/dist/args.d.ts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { type ParseArgsConfig } from 'node:util';
|
|
2
|
+
type Options = NonNullable<ParseArgsConfig['options']>;
|
|
3
|
+
/**
|
|
4
|
+
* The name the program was launched under. One file is reached by several of
|
|
5
|
+
* them — `zen`, `zn`, `zenera` — and help that names a command the reader did
|
|
6
|
+
* not type is help about a different program.
|
|
7
|
+
*
|
|
8
|
+
* `argv[1]` keeps the symlink `bin` installed rather than its target, which is
|
|
9
|
+
* exactly the name that was typed. Running the file directly, or through a
|
|
10
|
+
* Windows shim, lands on `main.js` instead: there is no name to honour then,
|
|
11
|
+
* so the canonical one stands.
|
|
12
|
+
*/
|
|
13
|
+
export declare function invokedAs(fallback: string): string;
|
|
14
|
+
export interface Parsed<T> {
|
|
15
|
+
values: T;
|
|
16
|
+
positionals: string[];
|
|
17
|
+
}
|
|
18
|
+
export declare function parse<T>(args: readonly string[], options: Options, usage: string): Parsed<T>;
|
|
19
|
+
/** Exactly one positional, or none. More than one is a mistake worth naming. */
|
|
20
|
+
export declare function one(positionals: string[], what: string, usage: string): string | undefined;
|
|
21
|
+
export interface Split {
|
|
22
|
+
/** everything before the command name */
|
|
23
|
+
before: string[];
|
|
24
|
+
name?: string;
|
|
25
|
+
/** everything after it, verbatim */
|
|
26
|
+
after: string[];
|
|
27
|
+
}
|
|
28
|
+
/** The command name is the first bare word. */
|
|
29
|
+
export declare function split(argv: readonly string[]): Split;
|
|
30
|
+
/**
|
|
31
|
+
* Pulls the global flags back out of a command's arguments — `zen list --json`
|
|
32
|
+
* reads better than `zen --json list`, and both must work — and returns what is
|
|
33
|
+
* left for the command's own parser. Nothing after `--` is touched.
|
|
34
|
+
*/
|
|
35
|
+
export declare function extract(after: readonly string[]): {
|
|
36
|
+
rest: string[];
|
|
37
|
+
global: string[];
|
|
38
|
+
};
|
|
39
|
+
export {};
|
|
40
|
+
//# sourceMappingURL=args.d.ts.map
|
package/dist/args.js
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { basename } from 'node:path';
|
|
2
|
+
import { parseArgs } from 'node:util';
|
|
3
|
+
import { usageError } from "./term.js";
|
|
4
|
+
/**
|
|
5
|
+
* The name the program was launched under. One file is reached by several of
|
|
6
|
+
* them — `zen`, `zn`, `zenera` — and help that names a command the reader did
|
|
7
|
+
* not type is help about a different program.
|
|
8
|
+
*
|
|
9
|
+
* `argv[1]` keeps the symlink `bin` installed rather than its target, which is
|
|
10
|
+
* exactly the name that was typed. Running the file directly, or through a
|
|
11
|
+
* Windows shim, lands on `main.js` instead: there is no name to honour then,
|
|
12
|
+
* so the canonical one stands.
|
|
13
|
+
*/
|
|
14
|
+
export function invokedAs(fallback) {
|
|
15
|
+
const name = basename(process.argv[1] ?? '').replace(/\.[cm]?js$/, '');
|
|
16
|
+
return name && name !== 'main' && name !== 'index' ? name : fallback;
|
|
17
|
+
}
|
|
18
|
+
export function parse(args, options, usage) {
|
|
19
|
+
try {
|
|
20
|
+
const { values, positionals } = parseArgs({
|
|
21
|
+
args: [...args],
|
|
22
|
+
options,
|
|
23
|
+
strict: true,
|
|
24
|
+
allowPositionals: true,
|
|
25
|
+
});
|
|
26
|
+
return { values: values, positionals };
|
|
27
|
+
}
|
|
28
|
+
catch (err) {
|
|
29
|
+
throw usageError(err.message, usage);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
/** Exactly one positional, or none. More than one is a mistake worth naming. */
|
|
33
|
+
export function one(positionals, what, usage) {
|
|
34
|
+
if (positionals.length > 1) {
|
|
35
|
+
throw usageError(`expected at most one ${what}, got ${positionals.length}`, usage);
|
|
36
|
+
}
|
|
37
|
+
return positionals[0];
|
|
38
|
+
}
|
|
39
|
+
// ---------------------------------------------------------------------------
|
|
40
|
+
// Splitting the command line
|
|
41
|
+
//
|
|
42
|
+
// Lives here rather than in `main.ts` because it is the part with the sharp
|
|
43
|
+
// edge: `parseArgs` in non-strict mode silently turns an unrecognised
|
|
44
|
+
// `--no-check` into an *option*, so a frame that parsed the whole line would
|
|
45
|
+
// swallow every flag belonging to a command. Nothing after the command name is
|
|
46
|
+
// parsed here at all.
|
|
47
|
+
// ---------------------------------------------------------------------------
|
|
48
|
+
/** Flags the frame owns, wherever they appear. */
|
|
49
|
+
const GLOBAL_FLAGS = new Set(['--json', '--help', '-h']);
|
|
50
|
+
const GLOBAL_VALUED = new Set(['-C', '--directory']);
|
|
51
|
+
/** The command name is the first bare word. */
|
|
52
|
+
export function split(argv) {
|
|
53
|
+
const before = [];
|
|
54
|
+
for (let i = 0; i < argv.length; i++) {
|
|
55
|
+
const arg = argv[i];
|
|
56
|
+
if (arg === '--') {
|
|
57
|
+
return { before, name: argv[i + 1], after: argv.slice(i + 2) };
|
|
58
|
+
}
|
|
59
|
+
if (!arg.startsWith('-')) {
|
|
60
|
+
return { before, name: arg, after: argv.slice(i + 1) };
|
|
61
|
+
}
|
|
62
|
+
before.push(arg);
|
|
63
|
+
if (GLOBAL_VALUED.has(arg) && i + 1 < argv.length) {
|
|
64
|
+
before.push(argv[++i]);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return { before, after: [] };
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Pulls the global flags back out of a command's arguments — `zen list --json`
|
|
71
|
+
* reads better than `zen --json list`, and both must work — and returns what is
|
|
72
|
+
* left for the command's own parser. Nothing after `--` is touched.
|
|
73
|
+
*/
|
|
74
|
+
export function extract(after) {
|
|
75
|
+
const rest = [];
|
|
76
|
+
const global = [];
|
|
77
|
+
for (let i = 0; i < after.length; i++) {
|
|
78
|
+
const arg = after[i];
|
|
79
|
+
if (arg === '--') {
|
|
80
|
+
rest.push(...after.slice(i + 1));
|
|
81
|
+
break;
|
|
82
|
+
}
|
|
83
|
+
if (GLOBAL_FLAGS.has(arg)) {
|
|
84
|
+
global.push(arg);
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
if (GLOBAL_VALUED.has(arg)) {
|
|
88
|
+
global.push(arg, after[++i] ?? '');
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
if (arg.startsWith('--directory=') || arg.startsWith('--json=')) {
|
|
92
|
+
global.push(arg);
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
rest.push(arg);
|
|
96
|
+
}
|
|
97
|
+
return { rest, global };
|
|
98
|
+
}
|
|
99
|
+
//# sourceMappingURL=args.js.map
|
package/dist/audit.d.ts
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { type ModelRequirement } from '@zenera/neo';
|
|
2
|
+
import { type KeyStore, type Provider } from './keys.ts';
|
|
3
|
+
/** Which key of the config named a reference: `model:`/`models:` or `embedding:`/`embeddings:`. */
|
|
4
|
+
export type DeclaredRole = 'model' | 'embedding';
|
|
5
|
+
export interface ModelIssue {
|
|
6
|
+
/** the model as the config names it: an alias, or the ref itself */
|
|
7
|
+
name: string;
|
|
8
|
+
/** what the config declared it for */
|
|
9
|
+
role: DeclaredRole;
|
|
10
|
+
/** the provider it resolves to */
|
|
11
|
+
provider: string;
|
|
12
|
+
/** the variable that would carry the credential */
|
|
13
|
+
env: string;
|
|
14
|
+
/** `missing` — nothing to authenticate with. `dead` — rejected when checked. */
|
|
15
|
+
reason: 'missing' | 'dead';
|
|
16
|
+
/** the provider's own words, when it was the one to say no */
|
|
17
|
+
detail?: string;
|
|
18
|
+
/** the keyring provider the fix names, when the kind is one of them */
|
|
19
|
+
add?: Provider;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Whether a requirement is actually met, and under which variable.
|
|
23
|
+
*
|
|
24
|
+
* `satisfied` is the library's answer and it is about api keys, which is the
|
|
25
|
+
* wrong question for Vertex: that kind is `keyOptional` because it
|
|
26
|
+
* authenticates from a service-account file instead. So the file-shaped
|
|
27
|
+
* credential is looked for where the keyring keeps it.
|
|
28
|
+
*
|
|
29
|
+
* Exported because a report that named `VERTEX_API_KEY` while the audit
|
|
30
|
+
* silently checked `GOOGLE_APPLICATION_CREDENTIALS` would be two answers to
|
|
31
|
+
* one question, and the wrong one is the one people would act on.
|
|
32
|
+
*/
|
|
33
|
+
export declare function credentialFor(need: ModelRequirement): {
|
|
34
|
+
env: string;
|
|
35
|
+
present: boolean;
|
|
36
|
+
};
|
|
37
|
+
/**
|
|
38
|
+
* Reports the models a run could not reach. Best effort by design: a config
|
|
39
|
+
* that will not parse, or that names a provider that does not exist, is the
|
|
40
|
+
* loader's to report — precisely, and with the offending key named. Guessing
|
|
41
|
+
* at it here would only produce a worse version of the same message.
|
|
42
|
+
*
|
|
43
|
+
* Call *after* `KeyStore.materialize()`, so the keyring's keys are as visible
|
|
44
|
+
* here as they will be to the library.
|
|
45
|
+
*/
|
|
46
|
+
export declare function auditModels(projectDir: string, store: KeyStore): ModelIssue[];
|
|
47
|
+
/**
|
|
48
|
+
* One line, and the command that fixes it. A warning nobody can act on is
|
|
49
|
+
* noise, so the fix is part of the sentence rather than something to go and
|
|
50
|
+
* look up.
|
|
51
|
+
*/
|
|
52
|
+
export declare function describeIssue(issue: ModelIssue): string;
|
|
53
|
+
//# sourceMappingURL=audit.d.ts.map
|
package/dist/audit.js
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { projectRegistry, readProjectConfig, } from '@zenera/neo';
|
|
5
|
+
import { SHAPES, isProvider } from "./keys.js";
|
|
6
|
+
import { bold, dim } from "./term.js";
|
|
7
|
+
/**
|
|
8
|
+
* Every model the project names, keyed by how one would refer to it.
|
|
9
|
+
*
|
|
10
|
+
* The `models:` table comes first so a declared alias keeps its own name in the
|
|
11
|
+
* report; a `model:` that names one collapses onto it rather than appearing
|
|
12
|
+
* twice, and a `model:` that names no alias is a shorthand standing for itself.
|
|
13
|
+
*
|
|
14
|
+
* Embeddings are walked the same way and kept in their own list: an alias may
|
|
15
|
+
* legitimately appear under both keys meaning two different things, and they
|
|
16
|
+
* are two credentials to check either way.
|
|
17
|
+
*/
|
|
18
|
+
function declared(config) {
|
|
19
|
+
// The schema widens `reasoningEffort` to `string` on purpose, which is the
|
|
20
|
+
// one thing keeping a config's model spec from being a `ModelSpec`. The
|
|
21
|
+
// loader hands the same cast to `models.model()` for the same reason.
|
|
22
|
+
const models = new Map(Object.entries(config.models ?? {}));
|
|
23
|
+
const add = (into, ref) => {
|
|
24
|
+
if (ref && !into.has(ref)) {
|
|
25
|
+
into.set(ref, ref);
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
add(models, config.model);
|
|
29
|
+
for (const agent of config.agents) {
|
|
30
|
+
add(models, agent.model);
|
|
31
|
+
}
|
|
32
|
+
const embeddings = new Map(Object.entries(config.embeddings ?? {}));
|
|
33
|
+
add(embeddings, config.embedding);
|
|
34
|
+
return new Map([
|
|
35
|
+
['model', models],
|
|
36
|
+
['embedding', embeddings],
|
|
37
|
+
]);
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* `gcloud auth application-default login` writes here, and the GenAI SDK finds
|
|
41
|
+
* it with no variable set — so without this check every developer using ADC
|
|
42
|
+
* would be told their working Vertex setup is broken.
|
|
43
|
+
*/
|
|
44
|
+
function hasGcloudAdc() {
|
|
45
|
+
const dir = process.env.CLOUDSDK_CONFIG ?? join(homedir(), '.config', 'gcloud');
|
|
46
|
+
return existsSync(join(dir, 'application_default_credentials.json'));
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Whether a requirement is actually met, and under which variable.
|
|
50
|
+
*
|
|
51
|
+
* `satisfied` is the library's answer and it is about api keys, which is the
|
|
52
|
+
* wrong question for Vertex: that kind is `keyOptional` because it
|
|
53
|
+
* authenticates from a service-account file instead. So the file-shaped
|
|
54
|
+
* credential is looked for where the keyring keeps it.
|
|
55
|
+
*
|
|
56
|
+
* Exported because a report that named `VERTEX_API_KEY` while the audit
|
|
57
|
+
* silently checked `GOOGLE_APPLICATION_CREDENTIALS` would be two answers to
|
|
58
|
+
* one question, and the wrong one is the one people would act on.
|
|
59
|
+
*/
|
|
60
|
+
export function credentialFor(need) {
|
|
61
|
+
const provider = isProvider(need.kind) ? need.kind : undefined;
|
|
62
|
+
const shape = provider ? SHAPES[provider] : undefined;
|
|
63
|
+
if (shape?.holds === 'file') {
|
|
64
|
+
return { env: shape.env, present: Boolean(process.env[shape.env]) || hasGcloudAdc() };
|
|
65
|
+
}
|
|
66
|
+
return { env: need.apiKeyEnv, present: need.satisfied };
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Reports the models a run could not reach. Best effort by design: a config
|
|
70
|
+
* that will not parse, or that names a provider that does not exist, is the
|
|
71
|
+
* loader's to report — precisely, and with the offending key named. Guessing
|
|
72
|
+
* at it here would only produce a worse version of the same message.
|
|
73
|
+
*
|
|
74
|
+
* Call *after* `KeyStore.materialize()`, so the keyring's keys are as visible
|
|
75
|
+
* here as they will be to the library.
|
|
76
|
+
*/
|
|
77
|
+
export function auditModels(projectDir, store) {
|
|
78
|
+
let config;
|
|
79
|
+
try {
|
|
80
|
+
config = readProjectConfig(projectDir).config;
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
return [];
|
|
84
|
+
}
|
|
85
|
+
const registry = projectRegistry(config);
|
|
86
|
+
const issues = [];
|
|
87
|
+
for (const [role, refs] of declared(config)) {
|
|
88
|
+
for (const [name, ref] of refs) {
|
|
89
|
+
let need;
|
|
90
|
+
try {
|
|
91
|
+
need = registry.requirement(ref);
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
const { env, present } = credentialFor(need);
|
|
97
|
+
const provider = isProvider(need.kind) ? need.kind : undefined;
|
|
98
|
+
if (!present) {
|
|
99
|
+
issues.push({
|
|
100
|
+
name,
|
|
101
|
+
role,
|
|
102
|
+
provider: need.provider,
|
|
103
|
+
env,
|
|
104
|
+
reason: 'missing',
|
|
105
|
+
add: provider,
|
|
106
|
+
});
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
// A key that the provider itself rejected last time it was asked.
|
|
110
|
+
// Said as a warning rather than an error because a key can be
|
|
111
|
+
// reinstated between the check and the run, and a stale verdict must
|
|
112
|
+
// not be the thing that stops a run from being attempted.
|
|
113
|
+
const check = provider ? store.active(provider)?.check : undefined;
|
|
114
|
+
if (check?.state === 'dead') {
|
|
115
|
+
issues.push({
|
|
116
|
+
name,
|
|
117
|
+
role,
|
|
118
|
+
provider: need.provider,
|
|
119
|
+
env,
|
|
120
|
+
reason: 'dead',
|
|
121
|
+
detail: check.detail,
|
|
122
|
+
add: provider,
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return issues;
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* One line, and the command that fixes it. A warning nobody can act on is
|
|
131
|
+
* noise, so the fix is part of the sentence rather than something to go and
|
|
132
|
+
* look up.
|
|
133
|
+
*/
|
|
134
|
+
export function describeIssue(issue) {
|
|
135
|
+
const what = `${bold(issue.name)} (${issue.provider}${issue.role === 'embedding' ? ', embedding' : ''})`;
|
|
136
|
+
if (issue.reason === 'missing') {
|
|
137
|
+
const fix = issue.add ? `zen key add ${issue.add}` : `set ${issue.env}`;
|
|
138
|
+
return `${what} has no credential — ${issue.env} is not set; ${dim(fix)}`;
|
|
139
|
+
}
|
|
140
|
+
const why = issue.detail ? `: ${issue.detail}` : '';
|
|
141
|
+
const fix = `zen key check ${issue.add ?? ''}`.trim();
|
|
142
|
+
return `${what} was rejected when last checked${why} — ${dim(fix)}`;
|
|
143
|
+
}
|
|
144
|
+
//# sourceMappingURL=audit.js.map
|
package/dist/banner.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export interface BannerText {
|
|
2
|
+
/** drawn white */
|
|
3
|
+
head: string;
|
|
4
|
+
/** drawn in the neon accent */
|
|
5
|
+
accent: string;
|
|
6
|
+
/** the line underneath, dim */
|
|
7
|
+
subtitle: string;
|
|
8
|
+
}
|
|
9
|
+
export declare const NEO_BANNER: BannerText;
|
|
10
|
+
export declare function bannerLines(text: BannerText, columns?: number): string[];
|
|
11
|
+
/** Narration, and only for someone watching. */
|
|
12
|
+
export declare function printBanner(text: BannerText): void;
|
|
13
|
+
//# sourceMappingURL=banner.d.ts.map
|