@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.
Files changed (78) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +239 -0
  3. package/dist/args.d.ts +40 -0
  4. package/dist/args.js +99 -0
  5. package/dist/audit.d.ts +53 -0
  6. package/dist/audit.js +144 -0
  7. package/dist/banner.d.ts +13 -0
  8. package/dist/banner.js +103 -0
  9. package/dist/command.d.ts +14 -0
  10. package/dist/command.js +12 -0
  11. package/dist/commands/check.d.ts +3 -0
  12. package/dist/commands/check.js +287 -0
  13. package/dist/commands/index.d.ts +22 -0
  14. package/dist/commands/index.js +56 -0
  15. package/dist/commands/init.d.ts +3 -0
  16. package/dist/commands/init.js +157 -0
  17. package/dist/commands/inspect.d.ts +3 -0
  18. package/dist/commands/inspect.js +158 -0
  19. package/dist/commands/key.d.ts +3 -0
  20. package/dist/commands/key.js +335 -0
  21. package/dist/commands/list.d.ts +3 -0
  22. package/dist/commands/list.js +101 -0
  23. package/dist/commands/models.d.ts +9 -0
  24. package/dist/commands/models.js +120 -0
  25. package/dist/commands/open.d.ts +9 -0
  26. package/dist/commands/open.js +270 -0
  27. package/dist/commands/run.d.ts +3 -0
  28. package/dist/commands/run.js +167 -0
  29. package/dist/commands/sandbox.d.ts +3 -0
  30. package/dist/commands/sandbox.js +112 -0
  31. package/dist/commands/version.d.ts +6 -0
  32. package/dist/commands/version.js +39 -0
  33. package/dist/engine.d.ts +49 -0
  34. package/dist/engine.js +208 -0
  35. package/dist/external.d.ts +10 -0
  36. package/dist/external.js +56 -0
  37. package/dist/home.d.ts +31 -0
  38. package/dist/home.js +108 -0
  39. package/dist/ids.d.ts +12 -0
  40. package/dist/ids.js +44 -0
  41. package/dist/keys.d.ts +124 -0
  42. package/dist/keys.js +309 -0
  43. package/dist/lib.d.ts +9 -0
  44. package/dist/lib.js +31 -0
  45. package/dist/liveness.d.ts +23 -0
  46. package/dist/liveness.js +221 -0
  47. package/dist/main.d.ts +3 -0
  48. package/dist/main.js +155 -0
  49. package/dist/narrate.d.ts +19 -0
  50. package/dist/narrate.js +124 -0
  51. package/dist/podman.d.ts +46 -0
  52. package/dist/podman.js +254 -0
  53. package/dist/projects.d.ts +70 -0
  54. package/dist/projects.js +232 -0
  55. package/dist/resolve.d.ts +27 -0
  56. package/dist/resolve.js +138 -0
  57. package/dist/sandbox.d.ts +36 -0
  58. package/dist/sandbox.js +104 -0
  59. package/dist/scaffold.d.ts +29 -0
  60. package/dist/scaffold.js +220 -0
  61. package/dist/session.d.ts +77 -0
  62. package/dist/session.js +156 -0
  63. package/dist/term.d.ts +69 -0
  64. package/dist/term.js +242 -0
  65. package/dist/tui/app.d.ts +8 -0
  66. package/dist/tui/app.js +257 -0
  67. package/dist/tui/theme.d.ts +23 -0
  68. package/dist/tui/theme.js +134 -0
  69. package/dist/tui/wrap.d.ts +12 -0
  70. package/dist/tui/wrap.js +62 -0
  71. package/dist/validate.d.ts +145 -0
  72. package/dist/validate.js +959 -0
  73. package/package.json +76 -0
  74. package/templates/.github/copilot-instructions.md +1579 -0
  75. package/templates/.github/prompts/new-agent.prompt.md +38 -0
  76. package/templates/.github/prompts/new-skill.prompt.md +37 -0
  77. package/templates/.github/prompts/review-project.prompt.md +31 -0
  78. 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
+ [![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)
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
@@ -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
@@ -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