@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/dist/ids.js ADDED
@@ -0,0 +1,44 @@
1
+ import { randomBytes } from 'node:crypto';
2
+ // ---------------------------------------------------------------------------
3
+ // Identifiers
4
+ //
5
+ // `YYYYMMDD-HHMMSS-xxxx`, local time, four hex characters of entropy.
6
+ //
7
+ // Sortable as a plain string, readable without a decoder, and collision-free
8
+ // when two runs start inside the same second. An epoch integer gives up the
9
+ // first two properties and a uuid gives up all three.
10
+ // ---------------------------------------------------------------------------
11
+ const STAMP = /^\d{8}-\d{6}-[0-9a-f]{4}$/;
12
+ export function stamp(now = new Date()) {
13
+ const p = (n, w = 2) => String(n).padStart(w, '0');
14
+ const date = `${now.getFullYear()}${p(now.getMonth() + 1)}${p(now.getDate())}`;
15
+ const time = `${p(now.getHours())}${p(now.getMinutes())}${p(now.getSeconds())}`;
16
+ return `${date}-${time}-${randomBytes(2).toString('hex')}`;
17
+ }
18
+ /**
19
+ * Ids arrive from `--session` and `--run` and become path segments, so they are
20
+ * checked rather than trusted. The shape has no `.` and no separator, which
21
+ * makes traversal unrepresentable rather than merely rejected.
22
+ */
23
+ export function isStamp(value) {
24
+ return STAMP.test(value);
25
+ }
26
+ /** Human-readable form of a stamp: `2026-08-25 14:30:12`. */
27
+ export function stampDate(id) {
28
+ if (!isStamp(id)) {
29
+ return id;
30
+ }
31
+ const [d, t] = id.split('-');
32
+ return (`${d.slice(0, 4)}-${d.slice(4, 6)}-${d.slice(6, 8)} ` +
33
+ `${t.slice(0, 2)}:${t.slice(2, 4)}:${t.slice(4, 6)}`);
34
+ }
35
+ /** ISO instant a stamp names, for `ago()`. Local time in, local time out. */
36
+ export function stampInstant(id) {
37
+ if (!isStamp(id)) {
38
+ return undefined;
39
+ }
40
+ const [d, t] = id.split('-');
41
+ const at = new Date(Number(d.slice(0, 4)), Number(d.slice(4, 6)) - 1, Number(d.slice(6, 8)), Number(t.slice(0, 2)), Number(t.slice(2, 4)), Number(t.slice(4, 6)));
42
+ return Number.isNaN(at.getTime()) ? undefined : at.toISOString();
43
+ }
44
+ //# sourceMappingURL=ids.js.map
package/dist/keys.d.ts ADDED
@@ -0,0 +1,124 @@
1
+ /** The vendors a *model* can come from — the library's provider kinds, verbatim. */
2
+ export declare const PROVIDERS: readonly ["openai", "anthropic", "google", "vertex", "openrouter"];
3
+ export type Provider = (typeof PROVIDERS)[number];
4
+ /**
5
+ * Credentials that are not a way to reach a model.
6
+ *
7
+ * A tool can need a key as much as a model can, and the reasons a keyring
8
+ * exists — one place, 0600, materialised into the environment before a run —
9
+ * do not care which. What does care is everything that reasons about *models*:
10
+ * a machine holding nothing but an Exa key cannot run an agent, and saying it
11
+ * can would move the failure from `zen run`'s first line to its first turn.
12
+ * Hence two lists rather than one longer one, and `kind` on the shape so the
13
+ * few places that must tell them apart are made to say which they mean.
14
+ */
15
+ export declare const SERVICES: readonly ["exa"];
16
+ export type Service = (typeof SERVICES)[number];
17
+ /** Anything the keyring can hold a credential for. */
18
+ export declare const OWNERS: readonly ["openai", "anthropic", "google", "vertex", "openrouter", "exa"];
19
+ export type KeyOwner = Provider | Service;
20
+ interface ProviderShape {
21
+ /** whether this is somewhere a model lives, or something a tool calls */
22
+ kind: 'model' | 'service';
23
+ /** environment variable the library reads */
24
+ env: string;
25
+ /** what the value is: a secret string, or a path to a credentials file */
26
+ holds: 'secret' | 'file';
27
+ label: string;
28
+ /** where to get one, printed when there is none */
29
+ where: string;
30
+ }
31
+ /**
32
+ * Vertex is the odd one. The GenAI SDK resolves Application Default
33
+ * Credentials itself, so what is stored is a service-account *file* and what is
34
+ * exported is a path — not a key. Pretending otherwise would mean inventing a
35
+ * credential shape Google does not have.
36
+ */
37
+ export declare const SHAPES: Record<KeyOwner, ProviderShape>;
38
+ export declare function isProvider(name: string): name is Provider;
39
+ export declare function isOwner(name: string): name is KeyOwner;
40
+ export declare function assertOwner(name: string): KeyOwner;
41
+ export type Liveness = 'live' | 'dead' | 'unknown';
42
+ export interface KeyCheck {
43
+ state: Liveness;
44
+ at: string;
45
+ /** the provider's own words when it said no, or ours when we could not ask */
46
+ detail?: string;
47
+ }
48
+ export interface KeyEntry {
49
+ provider: KeyOwner;
50
+ /** unique within a provider; `default` unless the user says otherwise */
51
+ name: string;
52
+ holds: 'secret' | 'file';
53
+ /** the secret itself, or a path relative to the key directory */
54
+ value: string;
55
+ addedAt: string;
56
+ check?: KeyCheck;
57
+ }
58
+ export declare const keyId: (e: Pick<KeyEntry, "provider" | "name">) => string;
59
+ /**
60
+ * `provider` or `provider/name`. Returned separately rather than as a string so
61
+ * callers cannot accidentally re-split it, and so a name that is not a legal
62
+ * path segment is rejected here, once, before it can become a filename.
63
+ */
64
+ export declare function parseRef(ref: string): {
65
+ provider: KeyOwner;
66
+ name?: string;
67
+ };
68
+ export declare class KeyStore {
69
+ #private;
70
+ private constructor();
71
+ static open(): Promise<KeyStore>;
72
+ get entries(): readonly KeyEntry[];
73
+ /** Every entry for a provider, the active one first. */
74
+ for(provider: KeyOwner): KeyEntry[];
75
+ find(provider: KeyOwner, name: string): KeyEntry | undefined;
76
+ /** The entry a run would use, or undefined when the provider has none. */
77
+ active(provider: KeyOwner): KeyEntry | undefined;
78
+ isActive(entry: KeyEntry): boolean;
79
+ /**
80
+ * Adds or replaces. A file-shaped credential is *copied* into the key
81
+ * directory: the point of a store is that the credential survives the
82
+ * original being moved, renamed or cleaned up, and a stored path that
83
+ * silently stops resolving is worse than no store at all.
84
+ */
85
+ add(provider: KeyOwner, name: string, raw: string): KeyEntry;
86
+ remove(provider: KeyOwner, name: string): boolean;
87
+ use(provider: KeyOwner, name: string): KeyEntry;
88
+ record(entry: KeyEntry, check: KeyCheck): void;
89
+ save(): void;
90
+ /** Absolute path behind a file-shaped entry. */
91
+ fileOf(entry: KeyEntry): string;
92
+ /** The plaintext an entry stands for — the only way out of the store. */
93
+ reveal(entry: KeyEntry): string;
94
+ /**
95
+ * What the library would see. Real environment variables win, so CI,
96
+ * `docker run -e` and a one-off `OPENAI_API_KEY=… zen run` all behave
97
+ * exactly as they did before the store existed.
98
+ *
99
+ * Services are included by default, because a tool reads its key from the
100
+ * environment for exactly the same reason a model adapter does.
101
+ */
102
+ environment(only?: KeyOwner[]): Record<string, string>;
103
+ /** Applies `environment()` to this process. Returns what it set. */
104
+ materialize(only?: KeyOwner[]): Record<string, string>;
105
+ }
106
+ /**
107
+ * Enough of a secret to recognise it, never enough to use it. Short values are
108
+ * hidden outright rather than half-shown — a twelve-character secret with eight
109
+ * characters visible is not masked, it is inconvenienced.
110
+ */
111
+ export declare function mask(secret: string): string;
112
+ export declare function describe(store: KeyStore, entry: KeyEntry): string;
113
+ /**
114
+ * Called before a run: says plainly that there is no way to reach a model,
115
+ * rather than letting the SDK raise it three frames deeper as a 401.
116
+ *
117
+ * Only model providers count. A keyring holding nothing but an Exa key can
118
+ * search the web and cannot think, and reporting that as usable would trade
119
+ * one clear error here for an obscure one on the first turn.
120
+ */
121
+ export declare function assertUsable(store: KeyStore): void;
122
+ export declare function assertNotEmpty(store: KeyStore): void;
123
+ export {};
124
+ //# sourceMappingURL=keys.d.ts.map
package/dist/keys.js ADDED
@@ -0,0 +1,309 @@
1
+ import { chmodSync, copyFileSync, existsSync, statSync } from 'node:fs';
2
+ import { join, resolve } from 'node:path';
3
+ import { EXA_API_KEY_ENV } from '@zenera/neo';
4
+ import { assertPrivate, ensureDir, paths, readJson, writeJson } from "./home.js";
5
+ import { CliError, EXIT, credentialError, usageError } from "./term.js";
6
+ // ---------------------------------------------------------------------------
7
+ // The keyring
8
+ //
9
+ // The library reads credentials from the environment and from `${VAR}`
10
+ // expansion in `agents.yaml`, and it will keep doing so. This store is a CLI
11
+ // feature the library never learns about: before any command touches
12
+ // `loadProject`, the selected credentials are materialized into `process.env`.
13
+ //
14
+ // That single decision buys three things. Nothing downstream changes.
15
+ // `${OPENAI_API_KEY}` in a config keeps working. And a project checked out on a
16
+ // machine that has never seen `zen` still runs, because the environment is
17
+ // still the interface.
18
+ // ---------------------------------------------------------------------------
19
+ /** The vendors a *model* can come from — the library's provider kinds, verbatim. */
20
+ export const PROVIDERS = ['openai', 'anthropic', 'google', 'vertex', 'openrouter'];
21
+ /**
22
+ * Credentials that are not a way to reach a model.
23
+ *
24
+ * A tool can need a key as much as a model can, and the reasons a keyring
25
+ * exists — one place, 0600, materialised into the environment before a run —
26
+ * do not care which. What does care is everything that reasons about *models*:
27
+ * a machine holding nothing but an Exa key cannot run an agent, and saying it
28
+ * can would move the failure from `zen run`'s first line to its first turn.
29
+ * Hence two lists rather than one longer one, and `kind` on the shape so the
30
+ * few places that must tell them apart are made to say which they mean.
31
+ */
32
+ export const SERVICES = ['exa'];
33
+ /** Anything the keyring can hold a credential for. */
34
+ export const OWNERS = [...PROVIDERS, ...SERVICES];
35
+ /**
36
+ * Vertex is the odd one. The GenAI SDK resolves Application Default
37
+ * Credentials itself, so what is stored is a service-account *file* and what is
38
+ * exported is a path — not a key. Pretending otherwise would mean inventing a
39
+ * credential shape Google does not have.
40
+ */
41
+ export const SHAPES = {
42
+ openai: {
43
+ kind: 'model',
44
+ env: 'OPENAI_API_KEY',
45
+ holds: 'secret',
46
+ label: 'OpenAI',
47
+ where: 'https://platform.openai.com/api-keys',
48
+ },
49
+ anthropic: {
50
+ kind: 'model',
51
+ env: 'ANTHROPIC_API_KEY',
52
+ holds: 'secret',
53
+ label: 'Anthropic',
54
+ where: 'https://console.anthropic.com/settings/keys',
55
+ },
56
+ google: {
57
+ kind: 'model',
58
+ env: 'GEMINI_API_KEY',
59
+ holds: 'secret',
60
+ label: 'Google AI Studio',
61
+ where: 'https://aistudio.google.com/apikey',
62
+ },
63
+ vertex: {
64
+ kind: 'model',
65
+ env: 'GOOGLE_APPLICATION_CREDENTIALS',
66
+ holds: 'file',
67
+ label: 'Vertex AI',
68
+ where: 'a service-account JSON key from the GCP console',
69
+ },
70
+ openrouter: {
71
+ kind: 'model',
72
+ env: 'OPENROUTER_API_KEY',
73
+ holds: 'secret',
74
+ label: 'OpenRouter',
75
+ where: 'https://openrouter.ai/settings/keys',
76
+ },
77
+ exa: {
78
+ kind: 'service',
79
+ env: EXA_API_KEY_ENV,
80
+ holds: 'secret',
81
+ label: 'Exa',
82
+ where: 'https://dashboard.exa.ai/api-keys',
83
+ },
84
+ };
85
+ export function isProvider(name) {
86
+ return PROVIDERS.includes(name);
87
+ }
88
+ export function isOwner(name) {
89
+ return OWNERS.includes(name);
90
+ }
91
+ export function assertOwner(name) {
92
+ if (!isOwner(name)) {
93
+ throw usageError(`unknown provider "${name}"`, `known providers: ${OWNERS.join(', ')}`);
94
+ }
95
+ return name;
96
+ }
97
+ const EMPTY = { version: 1, entries: [], active: {} };
98
+ const NAME = /^[A-Za-z0-9][A-Za-z0-9_-]*$/;
99
+ export const keyId = (e) => `${e.provider}/${e.name}`;
100
+ /**
101
+ * `provider` or `provider/name`. Returned separately rather than as a string so
102
+ * callers cannot accidentally re-split it, and so a name that is not a legal
103
+ * path segment is rejected here, once, before it can become a filename.
104
+ */
105
+ export function parseRef(ref) {
106
+ const [head, ...rest] = ref.split('/');
107
+ if (rest.length > 1) {
108
+ throw usageError(`"${ref}" is not a key reference`, 'use provider or provider/name');
109
+ }
110
+ const provider = assertOwner(head);
111
+ const name = rest[0];
112
+ if (name !== undefined && !NAME.test(name)) {
113
+ throw usageError(`"${name}" is not a usable key name`, 'letters, digits, - and _ only');
114
+ }
115
+ return { provider, name };
116
+ }
117
+ // ---------------------------------------------------------------------------
118
+ // Store
119
+ // ---------------------------------------------------------------------------
120
+ export class KeyStore {
121
+ #file;
122
+ constructor(file) {
123
+ this.#file = file;
124
+ }
125
+ static async open() {
126
+ const path = paths.keys();
127
+ assertPrivate(path);
128
+ const file = await readJson(path, EMPTY);
129
+ return new KeyStore({ ...EMPTY, ...file });
130
+ }
131
+ get entries() {
132
+ return this.#file.entries;
133
+ }
134
+ /** Every entry for a provider, the active one first. */
135
+ for(provider) {
136
+ const active = this.#file.active[provider];
137
+ return this.#file.entries
138
+ .filter((e) => e.provider === provider)
139
+ .sort((a, b) => Number(b.name === active) - Number(a.name === active));
140
+ }
141
+ find(provider, name) {
142
+ return this.#file.entries.find((e) => e.provider === provider && e.name === name);
143
+ }
144
+ /** The entry a run would use, or undefined when the provider has none. */
145
+ active(provider) {
146
+ const chosen = this.#file.active[provider];
147
+ if (chosen) {
148
+ const hit = this.find(provider, chosen);
149
+ if (hit) {
150
+ return hit;
151
+ }
152
+ }
153
+ return this.#file.entries.find((e) => e.provider === provider);
154
+ }
155
+ isActive(entry) {
156
+ return this.active(entry.provider)?.name === entry.name;
157
+ }
158
+ /**
159
+ * Adds or replaces. A file-shaped credential is *copied* into the key
160
+ * directory: the point of a store is that the credential survives the
161
+ * original being moved, renamed or cleaned up, and a stored path that
162
+ * silently stops resolving is worse than no store at all.
163
+ */
164
+ add(provider, name, raw) {
165
+ if (!NAME.test(name)) {
166
+ throw usageError(`"${name}" is not a usable key name`, 'letters, digits, - and _ only');
167
+ }
168
+ const shape = SHAPES[provider];
169
+ const entry = {
170
+ provider,
171
+ name,
172
+ holds: shape.holds,
173
+ value: shape.holds === 'file' ? this.#absorb(provider, name, raw) : raw,
174
+ addedAt: new Date().toISOString(),
175
+ };
176
+ const at = this.#file.entries.findIndex((e) => e.provider === provider && e.name === name);
177
+ if (at >= 0) {
178
+ this.#file.entries[at] = entry;
179
+ }
180
+ else {
181
+ this.#file.entries.push(entry);
182
+ }
183
+ this.#file.active[provider] ??= name;
184
+ return entry;
185
+ }
186
+ remove(provider, name) {
187
+ const at = this.#file.entries.findIndex((e) => e.provider === provider && e.name === name);
188
+ if (at < 0) {
189
+ return false;
190
+ }
191
+ this.#file.entries.splice(at, 1);
192
+ if (this.#file.active[provider] === name) {
193
+ delete this.#file.active[provider];
194
+ const next = this.#file.entries.find((e) => e.provider === provider);
195
+ if (next) {
196
+ this.#file.active[provider] = next.name;
197
+ }
198
+ }
199
+ return true;
200
+ }
201
+ use(provider, name) {
202
+ const entry = this.find(provider, name);
203
+ if (!entry) {
204
+ throw usageError(`no key ${provider}/${name}`, 'see: zen key ls');
205
+ }
206
+ this.#file.active[provider] = name;
207
+ return entry;
208
+ }
209
+ record(entry, check) {
210
+ const hit = this.find(entry.provider, entry.name);
211
+ if (hit) {
212
+ hit.check = check;
213
+ }
214
+ }
215
+ save() {
216
+ ensureDir(paths.home());
217
+ writeJson(paths.keys(), this.#file);
218
+ }
219
+ /** Absolute path behind a file-shaped entry. */
220
+ fileOf(entry) {
221
+ return join(paths.keyDir(), entry.value);
222
+ }
223
+ /** The plaintext an entry stands for — the only way out of the store. */
224
+ reveal(entry) {
225
+ return entry.holds === 'file' ? this.fileOf(entry) : entry.value;
226
+ }
227
+ /**
228
+ * What the library would see. Real environment variables win, so CI,
229
+ * `docker run -e` and a one-off `OPENAI_API_KEY=… zen run` all behave
230
+ * exactly as they did before the store existed.
231
+ *
232
+ * Services are included by default, because a tool reads its key from the
233
+ * environment for exactly the same reason a model adapter does.
234
+ */
235
+ environment(only) {
236
+ const env = {};
237
+ for (const provider of only ?? OWNERS) {
238
+ const entry = this.active(provider);
239
+ if (!entry) {
240
+ continue;
241
+ }
242
+ const { env: name } = SHAPES[provider];
243
+ if (process.env[name]) {
244
+ continue;
245
+ }
246
+ env[name] = this.reveal(entry);
247
+ }
248
+ return env;
249
+ }
250
+ /** Applies `environment()` to this process. Returns what it set. */
251
+ materialize(only) {
252
+ const env = this.environment(only);
253
+ Object.assign(process.env, env);
254
+ return env;
255
+ }
256
+ #absorb(provider, name, raw) {
257
+ const source = resolve(raw);
258
+ if (!existsSync(source) || !statSync(source).isFile()) {
259
+ throw usageError(`${SHAPES[provider].label} credentials must be a file`, `no such file: ${source}`);
260
+ }
261
+ const target = `${provider}-${name}.json`;
262
+ ensureDir(paths.keyDir());
263
+ const path = join(paths.keyDir(), target);
264
+ copyFileSync(source, path);
265
+ // copyFile keeps the source's mode, which may well be group-readable.
266
+ chmodSync(path, 0o600);
267
+ return target;
268
+ }
269
+ }
270
+ // ---------------------------------------------------------------------------
271
+ // Display
272
+ // ---------------------------------------------------------------------------
273
+ /**
274
+ * Enough of a secret to recognise it, never enough to use it. Short values are
275
+ * hidden outright rather than half-shown — a twelve-character secret with eight
276
+ * characters visible is not masked, it is inconvenienced.
277
+ */
278
+ export function mask(secret) {
279
+ if (secret.length <= 12) {
280
+ return '•'.repeat(8);
281
+ }
282
+ return `${secret.slice(0, 4)}…${secret.slice(-4)}`;
283
+ }
284
+ export function describe(store, entry) {
285
+ return entry.holds === 'file' ? store.fileOf(entry) : mask(entry.value);
286
+ }
287
+ // ---------------------------------------------------------------------------
288
+ // Gate
289
+ // ---------------------------------------------------------------------------
290
+ /**
291
+ * Called before a run: says plainly that there is no way to reach a model,
292
+ * rather than letting the SDK raise it three frames deeper as a 401.
293
+ *
294
+ * Only model providers count. A keyring holding nothing but an Exa key can
295
+ * search the web and cannot think, and reporting that as usable would trade
296
+ * one clear error here for an obscure one on the first turn.
297
+ */
298
+ export function assertUsable(store) {
299
+ const reachable = PROVIDERS.filter((p) => process.env[SHAPES[p].env] || store.active(p) !== undefined);
300
+ if (reachable.length === 0) {
301
+ throw credentialError('no credentials for any provider', 'add one with: zen key add openai');
302
+ }
303
+ }
304
+ export function assertNotEmpty(store) {
305
+ if (store.entries.length === 0) {
306
+ throw new CliError('the keyring is empty', EXIT.credentials, 'add one: zen key add openai');
307
+ }
308
+ }
309
+ //# sourceMappingURL=keys.js.map
package/dist/lib.d.ts ADDED
@@ -0,0 +1,9 @@
1
+ export { extract, invokedAs, one, parse, split, type Parsed, type Split } from './args.ts';
2
+ export { printBanner, type BannerText } from './banner.ts';
3
+ export type { Command, Context } from './command.ts';
4
+ export { assertPrivate, ensureDir, ensureHome, home, paths, readJson, writeJson } from './home.ts';
5
+ export { assertNotEmpty, assertOwner, assertUsable, describe, isOwner, isProvider, keyId, KeyStore, mask, OWNERS, parseRef, PROVIDERS, SERVICES, SHAPES, type KeyCheck, type KeyEntry, type KeyOwner, type Liveness, type Provider, type Service, } from './keys.ts';
6
+ export { probe, probeAll } from './liveness.ts';
7
+ export { ensurePodmanReady, ownedContainers, podmanStatus, removeContainers, type OwnedContainer, type PodmanOptions, type PodmanStatus, } from './podman.ts';
8
+ export { ago, bold, CliError, count, credentialError, cyan, dim, EXIT, fail, green, invalidError, isInteractive, json, note, pad, red, table, usageError, warn, write, writeAll, yellow, type ExitCode, } from './term.ts';
9
+ //# sourceMappingURL=lib.d.ts.map
package/dist/lib.js ADDED
@@ -0,0 +1,31 @@
1
+ // ---------------------------------------------------------------------------
2
+ // The library face of the CLI
3
+ //
4
+ // `zen` is a program, not a library, and everything in `src/` is written that
5
+ // way: it prints, it prompts, it carries process exit codes. But the keyring is
6
+ // a machine-level thing rather than a command-level one — where credentials
7
+ // live, how they are stored 0600, how they reach `process.env` before a run —
8
+ // and a second front end on this machine has to agree with `zen` about all
9
+ // three or the two disagree about which key is active.
10
+ //
11
+ // `Command` is here for the other direction: a sibling package implements it
12
+ // and `zen` loads it by name, so a new package is a subcommand rather than a
13
+ // new binary.
14
+ //
15
+ // So this file, and only this file, is what another package may import. It is
16
+ // deliberately a hand-written list rather than a `export *`: every name here is
17
+ // public API of a published package and is bound by its version, which is a
18
+ // reason to add to it slowly.
19
+ //
20
+ // One caveat travels with `term.ts`: `CliError` carries an exit code and the
21
+ // writers go to stdout/stderr. That vocabulary belongs to startup. A server
22
+ // must not let a `CliError` escape into a request handler.
23
+ // ---------------------------------------------------------------------------
24
+ export { extract, invokedAs, one, parse, split } from "./args.js";
25
+ export { printBanner } from "./banner.js";
26
+ export { assertPrivate, ensureDir, ensureHome, home, paths, readJson, writeJson } from "./home.js";
27
+ export { assertNotEmpty, assertOwner, assertUsable, describe, isOwner, isProvider, keyId, KeyStore, mask, OWNERS, parseRef, PROVIDERS, SERVICES, SHAPES, } from "./keys.js";
28
+ export { probe, probeAll } from "./liveness.js";
29
+ export { ensurePodmanReady, ownedContainers, podmanStatus, removeContainers, } from "./podman.js";
30
+ export { ago, bold, CliError, count, credentialError, cyan, dim, EXIT, fail, green, invalidError, isInteractive, json, note, pad, red, table, usageError, warn, write, writeAll, yellow, } from "./term.js";
31
+ //# sourceMappingURL=lib.js.map
@@ -0,0 +1,23 @@
1
+ import { type KeyCheck, type KeyEntry, type KeyStore } from './keys.ts';
2
+ /**
3
+ * The cheapest authenticated call each SDK has. Nothing here reads a model or
4
+ * spends a token: the question is only whether the credential is accepted.
5
+ *
6
+ * The client is built through the library's own registry rather than by
7
+ * requiring the SDKs directly, so a missing optional dependency produces the
8
+ * library's "run: npm i openai" message instead of a raw MODULE_NOT_FOUND.
9
+ */
10
+ export declare function probe(store: KeyStore, entry: KeyEntry): Promise<KeyCheck>;
11
+ /**
12
+ * Probes many entries, one at a time. In flight together would be quicker, but
13
+ * `probe` reaches the SDKs the only way they can be reached — through
14
+ * `process.env` — and two probes sharing that variable would each read the
15
+ * other's key. One at a time is also what makes progress reportable: there is
16
+ * exactly one answer being waited on, and `onProbe` can name it.
17
+ *
18
+ * Pairs rather than a map, because the caller needs the entry itself to record
19
+ * the result against, and a map keyed by a string would only have to be
20
+ * un-joined again.
21
+ */
22
+ export declare function probeAll(store: KeyStore, entries: readonly KeyEntry[], onProbe?: (entry: KeyEntry, index: number, total: number) => void): Promise<[KeyEntry, KeyCheck][]>;
23
+ //# sourceMappingURL=liveness.d.ts.map