@zenera/cli 1.1.2 → 1.1.4
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 +228 -31
- package/dist/audit.d.ts +13 -8
- package/dist/audit.js +21 -24
- package/dist/catalog.d.ts +111 -0
- package/dist/catalog.js +439 -0
- package/dist/commands/check.js +72 -17
- package/dist/commands/index.d.ts +2 -2
- package/dist/commands/index.js +3 -2
- package/dist/commands/init.js +71 -11
- package/dist/commands/key.js +144 -36
- package/dist/commands/models.d.ts +0 -6
- package/dist/commands/models.js +546 -101
- package/dist/commands/open.js +2 -2
- package/dist/commands/run.js +3 -0
- package/dist/engine.d.ts +2 -0
- package/dist/engine.js +1 -0
- package/dist/home.d.ts +2 -0
- package/dist/home.js +2 -0
- package/dist/keys.d.ts +104 -13
- package/dist/keys.js +175 -34
- package/dist/lib.d.ts +2 -1
- package/dist/lib.js +2 -1
- package/dist/liveness.d.ts +48 -6
- package/dist/liveness.js +268 -28
- package/dist/sandbox.d.ts +2 -0
- package/dist/sandbox.js +58 -7
- package/dist/scaffold.d.ts +21 -21
- package/dist/scaffold.js +132 -204
- package/dist/validate.d.ts +17 -1
- package/dist/validate.js +100 -10
- package/package.json +2 -18
- package/templates/{.github → editor/.github}/copilot-instructions.md +37 -9
- package/templates/editor/.github/skills/api-schema-index/SKILL.md +292 -0
- package/templates/editor/.github/skills/zen-cli/SKILL.md +77 -0
- package/templates/editor/.github/skills/zen-cli/references/check.md +88 -0
- package/templates/editor/.github/skills/zen-cli/references/faker.md +111 -0
- package/templates/editor/.github/skills/zen-cli/references/frame.md +119 -0
- package/templates/editor/.github/skills/zen-cli/references/inspect.md +61 -0
- package/templates/editor/.github/skills/zen-cli/references/keys.md +119 -0
- package/templates/editor/.github/skills/zen-cli/references/models.md +108 -0
- package/templates/editor/.github/skills/zen-cli/references/projects.md +99 -0
- package/templates/editor/.github/skills/zen-cli/references/rag.md +159 -0
- package/templates/editor/.github/skills/zen-cli/references/run.md +104 -0
- package/templates/editor/.github/skills/zen-cli/references/sandbox.md +91 -0
- package/templates/editor/.vscode/settings.json +6 -0
- package/templates/parts/exa.yaml.tmpl +5 -0
- package/templates/parts/model.yaml.tmpl +4 -0
- package/templates/parts/models.yaml.tmpl +10 -0
- package/templates/project/INSTRUCTIONS.md +7 -0
- package/templates/project/SPECIFICATION.md +6 -0
- package/templates/project/agents/prompts/default.md +15 -0
- package/templates/project/agents.yaml.tmpl +44 -0
- package/templates/project/assets/README.md +12 -0
- package/templates/project/gitignore +9 -0
- package/templates/{sandbox → project/sandbox}/Dockerfile +2 -0
- package/templates/.github/skills/zen-cli/SKILL.md +0 -110
- /package/templates/{.github → editor/.github}/prompts/new-agent.prompt.md +0 -0
- /package/templates/{.github → editor/.github}/prompts/new-skill.prompt.md +0 -0
- /package/templates/{.github → editor/.github}/prompts/review-project.prompt.md +0 -0
package/dist/commands/open.js
CHANGED
|
@@ -4,7 +4,7 @@ import { homedir } from 'node:os';
|
|
|
4
4
|
import { delimiter, join, resolve, sep } from 'node:path';
|
|
5
5
|
import { one, parse } from "../args.js";
|
|
6
6
|
import { project as resolveProject } from "../resolve.js";
|
|
7
|
-
import {
|
|
7
|
+
import { editorFiles } from "../scaffold.js";
|
|
8
8
|
import { CliError, cyan, dim, EXIT, json, note, usageError } from "../term.js";
|
|
9
9
|
const USAGE = 'zen open [project] [--editor <cmd>] [--wait]';
|
|
10
10
|
/**
|
|
@@ -42,7 +42,7 @@ export const open = {
|
|
|
42
42
|
// opened on, and a project may predate either of them — or the version
|
|
43
43
|
// this `zen` writes. They are ours, so they are written fresh before
|
|
44
44
|
// the window is there to read them.
|
|
45
|
-
const written =
|
|
45
|
+
const written = editorFiles(dir);
|
|
46
46
|
if (ctx.json) {
|
|
47
47
|
json({
|
|
48
48
|
name: found.name,
|
package/dist/commands/run.js
CHANGED
|
@@ -19,6 +19,7 @@ export const run = {
|
|
|
19
19
|
' --model <ref> Override the default model.',
|
|
20
20
|
' --image <ref> Override the sandbox image commands run in.',
|
|
21
21
|
' --read-only Give the agent no way to write.',
|
|
22
|
+
' --no-keys Keep the API keys out of the sandbox.',
|
|
22
23
|
' --quiet Answer only; no narration.',
|
|
23
24
|
' --plain One shot, even on a terminal.',
|
|
24
25
|
' --theme <dark|light> Force the palette. Detected otherwise; $ZENERA_THEME.',
|
|
@@ -43,6 +44,7 @@ export const run = {
|
|
|
43
44
|
workspace: { type: 'string' },
|
|
44
45
|
model: { type: 'string' },
|
|
45
46
|
image: { type: 'string' },
|
|
47
|
+
'no-keys': { type: 'boolean' },
|
|
46
48
|
'read-only': { type: 'boolean' },
|
|
47
49
|
yes: { type: 'boolean' },
|
|
48
50
|
quiet: { type: 'boolean' },
|
|
@@ -84,6 +86,7 @@ export const run = {
|
|
|
84
86
|
readOnly: values['read-only'],
|
|
85
87
|
model: values.model,
|
|
86
88
|
image: values.image,
|
|
89
|
+
keys: values['no-keys'] ? false : undefined,
|
|
87
90
|
yes: values.yes || ctx.json,
|
|
88
91
|
});
|
|
89
92
|
try {
|
package/dist/engine.d.ts
CHANGED
|
@@ -10,6 +10,8 @@ export interface EngineOptions {
|
|
|
10
10
|
model?: string;
|
|
11
11
|
/** sandbox image override — `--image` */
|
|
12
12
|
image?: string;
|
|
13
|
+
/** whether credentials reach the sandbox — `--no-keys` sets this false */
|
|
14
|
+
keys?: boolean;
|
|
13
15
|
/** answer the sandbox's install question without asking — `--yes` */
|
|
14
16
|
yes?: boolean;
|
|
15
17
|
}
|
package/dist/engine.js
CHANGED
package/dist/home.d.ts
CHANGED
|
@@ -5,6 +5,8 @@ export declare const paths: {
|
|
|
5
5
|
keys: () => string;
|
|
6
6
|
keyDir: () => string;
|
|
7
7
|
faker: () => string;
|
|
8
|
+
/** cached model listings, one file per provider — public data, not secrets */
|
|
9
|
+
catalog: () => string;
|
|
8
10
|
};
|
|
9
11
|
/** Creates a directory owner-only, and leaves an existing one's mode alone. */
|
|
10
12
|
export declare function ensureDir(dir: string, mode?: number): string;
|
package/dist/home.js
CHANGED
|
@@ -26,6 +26,8 @@ export const paths = {
|
|
|
26
26
|
keys: () => join(home(), 'keys.json'),
|
|
27
27
|
keyDir: () => join(home(), 'keys'),
|
|
28
28
|
faker: () => join(home(), 'faker'),
|
|
29
|
+
/** cached model listings, one file per provider — public data, not secrets */
|
|
30
|
+
catalog: () => join(home(), 'catalog'),
|
|
29
31
|
};
|
|
30
32
|
/** Creates a directory owner-only, and leaves an existing one's mode alone. */
|
|
31
33
|
export function ensureDir(dir, mode = DIR_MODE) {
|
package/dist/keys.d.ts
CHANGED
|
@@ -17,33 +17,66 @@ export type Service = (typeof SERVICES)[number];
|
|
|
17
17
|
/** Anything the keyring can hold a credential for. */
|
|
18
18
|
export declare const OWNERS: readonly ["openai", "anthropic", "google", "vertex", "openrouter", "exa"];
|
|
19
19
|
export type KeyOwner = Provider | Service;
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
20
|
+
/**
|
|
21
|
+
* One way a credential can arrive.
|
|
22
|
+
*
|
|
23
|
+
* A provider that accepts two accepts two of these, and they agree about
|
|
24
|
+
* nothing: not the variable, not whether the value is the secret or a path to
|
|
25
|
+
* it, not where you go to get one. So the alternative is a whole form rather
|
|
26
|
+
* than a wider `holds`.
|
|
27
|
+
*/
|
|
28
|
+
export interface CredentialForm {
|
|
25
29
|
/** what the value is: a secret string, or a path to a credentials file */
|
|
26
30
|
holds: 'secret' | 'file';
|
|
27
|
-
|
|
31
|
+
/** environment variable the library reads */
|
|
32
|
+
env: string;
|
|
28
33
|
/** where to get one, printed when there is none */
|
|
29
34
|
where: string;
|
|
30
35
|
}
|
|
36
|
+
interface ProviderShape {
|
|
37
|
+
/** whether this is somewhere a model lives, or something a tool calls */
|
|
38
|
+
kind: 'model' | 'service';
|
|
39
|
+
label: string;
|
|
40
|
+
/** the ways in, first being the one this provider is usually reached by */
|
|
41
|
+
forms: [CredentialForm, ...CredentialForm[]];
|
|
42
|
+
}
|
|
31
43
|
/**
|
|
32
|
-
* Vertex is the odd one
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
44
|
+
* Vertex is the odd one, and it is odd twice.
|
|
45
|
+
*
|
|
46
|
+
* Its usual credential is not a key at all: the GenAI SDK resolves Application
|
|
47
|
+
* Default Credentials itself, so what is stored is a service-account *file* and
|
|
48
|
+
* what is exported is a path. But it also accepts an express-mode api key,
|
|
49
|
+
* which is an ordinary secret under an entirely different variable. The two are
|
|
50
|
+
* alternatives — express mode addresses no project — so which form a credential
|
|
51
|
+
* is gets decided per entry rather than per provider.
|
|
36
52
|
*/
|
|
37
53
|
export declare const SHAPES: Record<KeyOwner, ProviderShape>;
|
|
54
|
+
/**
|
|
55
|
+
* The form a provider is usually reached by — for the questions that have to
|
|
56
|
+
* have one answer, like which variable to name when nothing is set yet.
|
|
57
|
+
*/
|
|
58
|
+
export declare function form(provider: KeyOwner): CredentialForm;
|
|
59
|
+
/** Every variable a provider's credential could arrive in, usual one first. */
|
|
60
|
+
export declare function envNames(provider: KeyOwner): string[];
|
|
61
|
+
/** The variable this particular credential occupies. */
|
|
62
|
+
export declare function envOf(entry: Pick<KeyEntry, 'provider' | 'holds' | 'env'>): string;
|
|
38
63
|
export declare function isProvider(name: string): name is Provider;
|
|
39
64
|
export declare function isOwner(name: string): name is KeyOwner;
|
|
40
65
|
export declare function assertOwner(name: string): KeyOwner;
|
|
41
|
-
|
|
66
|
+
/**
|
|
67
|
+
* `blocked` is the credential being fine and the *account* not: an api switched
|
|
68
|
+
* off in the project, a spent balance, a model this key was never granted. It
|
|
69
|
+
* is split out of `dead` because the two want opposite actions — rotating a key
|
|
70
|
+
* that authenticated perfectly is the wrong afternoon.
|
|
71
|
+
*/
|
|
72
|
+
export type Liveness = 'live' | 'dead' | 'blocked' | 'unknown';
|
|
42
73
|
export interface KeyCheck {
|
|
43
74
|
state: Liveness;
|
|
44
75
|
at: string;
|
|
45
76
|
/** the provider's own words when it said no, or ours when we could not ask */
|
|
46
77
|
detail?: string;
|
|
78
|
+
/** what to actually do about it, when the refusal implies something specific */
|
|
79
|
+
fix?: string;
|
|
47
80
|
}
|
|
48
81
|
export interface KeyEntry {
|
|
49
82
|
provider: KeyOwner;
|
|
@@ -52,6 +85,11 @@ export interface KeyEntry {
|
|
|
52
85
|
holds: 'secret' | 'file';
|
|
53
86
|
/** the secret itself, or a path relative to the key directory */
|
|
54
87
|
value: string;
|
|
88
|
+
/** the variable it is exported as; absent on entries written before providers had two */
|
|
89
|
+
env?: string;
|
|
90
|
+
/** vertex, file-shaped only: what the library would otherwise have to be told twice */
|
|
91
|
+
project?: string;
|
|
92
|
+
location?: string;
|
|
55
93
|
addedAt: string;
|
|
56
94
|
check?: KeyCheck;
|
|
57
95
|
}
|
|
@@ -65,6 +103,13 @@ export declare function parseRef(ref: string): {
|
|
|
65
103
|
provider: KeyOwner;
|
|
66
104
|
name?: string;
|
|
67
105
|
};
|
|
106
|
+
/** What a credential cannot say about itself. */
|
|
107
|
+
export interface KeyMeta {
|
|
108
|
+
/** GCP project id, for a Vertex service account */
|
|
109
|
+
project?: string;
|
|
110
|
+
/** GCP region, or `global` */
|
|
111
|
+
location?: string;
|
|
112
|
+
}
|
|
68
113
|
export declare class KeyStore {
|
|
69
114
|
#private;
|
|
70
115
|
private constructor();
|
|
@@ -81,13 +126,21 @@ export declare class KeyStore {
|
|
|
81
126
|
* directory: the point of a store is that the credential survives the
|
|
82
127
|
* original being moved, renamed or cleaned up, and a stored path that
|
|
83
128
|
* silently stops resolving is worse than no store at all.
|
|
129
|
+
*
|
|
130
|
+
* Which form a value is, when the provider accepts two, is read off the
|
|
131
|
+
* value: a path that is there is a credentials file, and anything else is a
|
|
132
|
+
* secret. Asking would be a flag to get wrong, and a service-account key
|
|
133
|
+
* and an api key are not mistakable for one another.
|
|
84
134
|
*/
|
|
85
|
-
add(provider: KeyOwner, name: string, raw: string): KeyEntry;
|
|
135
|
+
add(provider: KeyOwner, name: string, raw: string, meta?: KeyMeta): KeyEntry;
|
|
86
136
|
remove(provider: KeyOwner, name: string): boolean;
|
|
87
137
|
use(provider: KeyOwner, name: string): KeyEntry;
|
|
88
138
|
record(entry: KeyEntry, check: KeyCheck): void;
|
|
89
139
|
save(): void;
|
|
90
|
-
/**
|
|
140
|
+
/**
|
|
141
|
+
* Absolute path behind a file-shaped entry. Stored entries name a file in
|
|
142
|
+
* the key directory; an ambient one already knows where it is.
|
|
143
|
+
*/
|
|
91
144
|
fileOf(entry: KeyEntry): string;
|
|
92
145
|
/** The plaintext an entry stands for — the only way out of the store. */
|
|
93
146
|
reveal(entry: KeyEntry): string;
|
|
@@ -110,6 +163,44 @@ export declare class KeyStore {
|
|
|
110
163
|
*/
|
|
111
164
|
export declare function mask(secret: string): string;
|
|
112
165
|
export declare function describe(store: KeyStore, entry: KeyEntry): string;
|
|
166
|
+
/**
|
|
167
|
+
* A credential the keyring does not hold but the libraries will nonetheless
|
|
168
|
+
* find: a variable already in the environment, or the file `gcloud auth
|
|
169
|
+
* application-default login` writes.
|
|
170
|
+
*
|
|
171
|
+
* These have to be listed, because they are the reason a provider works when
|
|
172
|
+
* `zen key ls` says there is nothing for it — and the reason one keeps working
|
|
173
|
+
* after its entry is removed.
|
|
174
|
+
*/
|
|
175
|
+
export interface Ambient {
|
|
176
|
+
provider: KeyOwner;
|
|
177
|
+
/** the variable it arrived in; absent when it was found where the SDK looks */
|
|
178
|
+
env?: string;
|
|
179
|
+
holds: 'secret' | 'file';
|
|
180
|
+
value: string;
|
|
181
|
+
}
|
|
182
|
+
/** How an ambient credential is named on the command line: it has no key name. */
|
|
183
|
+
export declare function ambientId(cred: Ambient): string;
|
|
184
|
+
/**
|
|
185
|
+
* Where `gcloud auth application-default login` leaves its credentials. The
|
|
186
|
+
* GenAI SDK reads this without being told to, so it counts even though nothing
|
|
187
|
+
* in the environment mentions it.
|
|
188
|
+
*/
|
|
189
|
+
export declare function gcloudAdc(): string | undefined;
|
|
190
|
+
export declare function ambient(store: KeyStore, only?: KeyOwner[]): Ambient[];
|
|
191
|
+
/**
|
|
192
|
+
* Every credential variable this process is carrying, whatever put it there.
|
|
193
|
+
*
|
|
194
|
+
* Read off the environment rather than off the store, and deliberately: by the
|
|
195
|
+
* time anyone asks, `materialize()` has already run, so the environment is the
|
|
196
|
+
* union of the keyring and whatever the shell brought — which is exactly the
|
|
197
|
+
* set of credentials the run is actually using.
|
|
198
|
+
*/
|
|
199
|
+
export declare function credentials(): {
|
|
200
|
+
env: string;
|
|
201
|
+
holds: 'secret' | 'file';
|
|
202
|
+
value: string;
|
|
203
|
+
}[];
|
|
113
204
|
/**
|
|
114
205
|
* Called before a run: says plainly that there is no way to reach a model,
|
|
115
206
|
* rather than letting the SDK raise it three frames deeper as a 401.
|
package/dist/keys.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { chmodSync, copyFileSync, existsSync, statSync } from 'node:fs';
|
|
2
|
-
import { join, resolve } from 'node:path';
|
|
3
1
|
import { EXA_API_KEY_ENV } from '@zenera/neo';
|
|
2
|
+
import { chmodSync, copyFileSync, existsSync, statSync } from 'node:fs';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
import { isAbsolute, join, resolve } from 'node:path';
|
|
4
5
|
import { assertPrivate, ensureDir, paths, readJson, writeJson } from "./home.js";
|
|
5
6
|
import { CliError, EXIT, credentialError, usageError } from "./term.js";
|
|
6
7
|
// ---------------------------------------------------------------------------
|
|
@@ -33,55 +34,108 @@ export const SERVICES = ['exa'];
|
|
|
33
34
|
/** Anything the keyring can hold a credential for. */
|
|
34
35
|
export const OWNERS = [...PROVIDERS, ...SERVICES];
|
|
35
36
|
/**
|
|
36
|
-
* Vertex is the odd one
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
37
|
+
* Vertex is the odd one, and it is odd twice.
|
|
38
|
+
*
|
|
39
|
+
* Its usual credential is not a key at all: the GenAI SDK resolves Application
|
|
40
|
+
* Default Credentials itself, so what is stored is a service-account *file* and
|
|
41
|
+
* what is exported is a path. But it also accepts an express-mode api key,
|
|
42
|
+
* which is an ordinary secret under an entirely different variable. The two are
|
|
43
|
+
* alternatives — express mode addresses no project — so which form a credential
|
|
44
|
+
* is gets decided per entry rather than per provider.
|
|
40
45
|
*/
|
|
41
46
|
export const SHAPES = {
|
|
42
47
|
openai: {
|
|
43
48
|
kind: 'model',
|
|
44
|
-
env: 'OPENAI_API_KEY',
|
|
45
|
-
holds: 'secret',
|
|
46
49
|
label: 'OpenAI',
|
|
47
|
-
|
|
50
|
+
forms: [
|
|
51
|
+
{
|
|
52
|
+
holds: 'secret',
|
|
53
|
+
env: 'OPENAI_API_KEY',
|
|
54
|
+
where: 'https://platform.openai.com/api-keys',
|
|
55
|
+
},
|
|
56
|
+
],
|
|
48
57
|
},
|
|
49
58
|
anthropic: {
|
|
50
59
|
kind: 'model',
|
|
51
|
-
env: 'ANTHROPIC_API_KEY',
|
|
52
|
-
holds: 'secret',
|
|
53
60
|
label: 'Anthropic',
|
|
54
|
-
|
|
61
|
+
forms: [
|
|
62
|
+
{
|
|
63
|
+
holds: 'secret',
|
|
64
|
+
env: 'ANTHROPIC_API_KEY',
|
|
65
|
+
where: 'https://console.anthropic.com/settings/keys',
|
|
66
|
+
},
|
|
67
|
+
],
|
|
55
68
|
},
|
|
56
69
|
google: {
|
|
57
70
|
kind: 'model',
|
|
58
|
-
env: 'GEMINI_API_KEY',
|
|
59
|
-
holds: 'secret',
|
|
60
71
|
label: 'Google AI Studio',
|
|
61
|
-
|
|
72
|
+
forms: [
|
|
73
|
+
{
|
|
74
|
+
holds: 'secret',
|
|
75
|
+
env: 'GEMINI_API_KEY',
|
|
76
|
+
where: 'https://aistudio.google.com/apikey',
|
|
77
|
+
},
|
|
78
|
+
],
|
|
62
79
|
},
|
|
63
80
|
vertex: {
|
|
64
81
|
kind: 'model',
|
|
65
|
-
env: 'GOOGLE_APPLICATION_CREDENTIALS',
|
|
66
|
-
holds: 'file',
|
|
67
82
|
label: 'Vertex AI',
|
|
68
|
-
|
|
83
|
+
forms: [
|
|
84
|
+
{
|
|
85
|
+
holds: 'file',
|
|
86
|
+
env: 'GOOGLE_APPLICATION_CREDENTIALS',
|
|
87
|
+
where: 'a service-account JSON key from the GCP console',
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
holds: 'secret',
|
|
91
|
+
env: 'VERTEX_API_KEY',
|
|
92
|
+
where: 'an express-mode key from https://console.cloud.google.com/vertex-ai',
|
|
93
|
+
},
|
|
94
|
+
],
|
|
69
95
|
},
|
|
70
96
|
openrouter: {
|
|
71
97
|
kind: 'model',
|
|
72
|
-
env: 'OPENROUTER_API_KEY',
|
|
73
|
-
holds: 'secret',
|
|
74
98
|
label: 'OpenRouter',
|
|
75
|
-
|
|
99
|
+
forms: [
|
|
100
|
+
{
|
|
101
|
+
holds: 'secret',
|
|
102
|
+
env: 'OPENROUTER_API_KEY',
|
|
103
|
+
where: 'https://openrouter.ai/settings/keys',
|
|
104
|
+
},
|
|
105
|
+
],
|
|
76
106
|
},
|
|
77
107
|
exa: {
|
|
78
108
|
kind: 'service',
|
|
79
|
-
env: EXA_API_KEY_ENV,
|
|
80
|
-
holds: 'secret',
|
|
81
109
|
label: 'Exa',
|
|
82
|
-
|
|
110
|
+
forms: [
|
|
111
|
+
{
|
|
112
|
+
holds: 'secret',
|
|
113
|
+
env: EXA_API_KEY_ENV,
|
|
114
|
+
where: 'https://dashboard.exa.ai/api-keys',
|
|
115
|
+
},
|
|
116
|
+
],
|
|
83
117
|
},
|
|
84
118
|
};
|
|
119
|
+
/**
|
|
120
|
+
* The form a provider is usually reached by — for the questions that have to
|
|
121
|
+
* have one answer, like which variable to name when nothing is set yet.
|
|
122
|
+
*/
|
|
123
|
+
export function form(provider) {
|
|
124
|
+
return SHAPES[provider].forms[0];
|
|
125
|
+
}
|
|
126
|
+
/** Every variable a provider's credential could arrive in, usual one first. */
|
|
127
|
+
export function envNames(provider) {
|
|
128
|
+
return SHAPES[provider].forms.map((f) => f.env);
|
|
129
|
+
}
|
|
130
|
+
/** The variable this particular credential occupies. */
|
|
131
|
+
export function envOf(entry) {
|
|
132
|
+
if (entry.env) {
|
|
133
|
+
return entry.env;
|
|
134
|
+
}
|
|
135
|
+
// Entries written before a provider had two forms carry no `env` of their own.
|
|
136
|
+
const forms = SHAPES[entry.provider].forms;
|
|
137
|
+
return (forms.find((f) => f.holds === entry.holds) ?? forms[0]).env;
|
|
138
|
+
}
|
|
85
139
|
export function isProvider(name) {
|
|
86
140
|
return PROVIDERS.includes(name);
|
|
87
141
|
}
|
|
@@ -114,6 +168,15 @@ export function parseRef(ref) {
|
|
|
114
168
|
}
|
|
115
169
|
return { provider, name };
|
|
116
170
|
}
|
|
171
|
+
/** Which of a provider's forms a raw value is, decided by the value itself. */
|
|
172
|
+
function formOf(provider, raw) {
|
|
173
|
+
const { forms } = SHAPES[provider];
|
|
174
|
+
if (forms.length === 1) {
|
|
175
|
+
return forms[0];
|
|
176
|
+
}
|
|
177
|
+
const wanted = raw.length < 4096 && existsSync(resolve(raw)) ? 'file' : 'secret';
|
|
178
|
+
return forms.find((f) => f.holds === wanted) ?? forms[0];
|
|
179
|
+
}
|
|
117
180
|
// ---------------------------------------------------------------------------
|
|
118
181
|
// Store
|
|
119
182
|
// ---------------------------------------------------------------------------
|
|
@@ -160,17 +223,27 @@ export class KeyStore {
|
|
|
160
223
|
* directory: the point of a store is that the credential survives the
|
|
161
224
|
* original being moved, renamed or cleaned up, and a stored path that
|
|
162
225
|
* silently stops resolving is worse than no store at all.
|
|
226
|
+
*
|
|
227
|
+
* Which form a value is, when the provider accepts two, is read off the
|
|
228
|
+
* value: a path that is there is a credentials file, and anything else is a
|
|
229
|
+
* secret. Asking would be a flag to get wrong, and a service-account key
|
|
230
|
+
* and an api key are not mistakable for one another.
|
|
163
231
|
*/
|
|
164
|
-
add(provider, name, raw) {
|
|
232
|
+
add(provider, name, raw, meta = {}) {
|
|
165
233
|
if (!NAME.test(name)) {
|
|
166
234
|
throw usageError(`"${name}" is not a usable key name`, 'letters, digits, - and _ only');
|
|
167
235
|
}
|
|
168
|
-
const
|
|
236
|
+
const form = formOf(provider, raw);
|
|
169
237
|
const entry = {
|
|
170
238
|
provider,
|
|
171
239
|
name,
|
|
172
|
-
holds:
|
|
173
|
-
value:
|
|
240
|
+
holds: form.holds,
|
|
241
|
+
value: form.holds === 'file' ? this.#absorb(provider, name, raw) : raw,
|
|
242
|
+
env: form.env,
|
|
243
|
+
// Express mode addresses no project, so carrying one would only ever
|
|
244
|
+
// be a way to build the combination the service refuses.
|
|
245
|
+
...(form.holds === 'file' && meta.project ? { project: meta.project } : {}),
|
|
246
|
+
...(form.holds === 'file' && meta.location ? { location: meta.location } : {}),
|
|
174
247
|
addedAt: new Date().toISOString(),
|
|
175
248
|
};
|
|
176
249
|
const at = this.#file.entries.findIndex((e) => e.provider === provider && e.name === name);
|
|
@@ -216,9 +289,12 @@ export class KeyStore {
|
|
|
216
289
|
ensureDir(paths.home());
|
|
217
290
|
writeJson(paths.keys(), this.#file);
|
|
218
291
|
}
|
|
219
|
-
/**
|
|
292
|
+
/**
|
|
293
|
+
* Absolute path behind a file-shaped entry. Stored entries name a file in
|
|
294
|
+
* the key directory; an ambient one already knows where it is.
|
|
295
|
+
*/
|
|
220
296
|
fileOf(entry) {
|
|
221
|
-
return join(paths.keyDir(), entry.value);
|
|
297
|
+
return isAbsolute(entry.value) ? entry.value : join(paths.keyDir(), entry.value);
|
|
222
298
|
}
|
|
223
299
|
/** The plaintext an entry stands for — the only way out of the store. */
|
|
224
300
|
reveal(entry) {
|
|
@@ -239,11 +315,18 @@ export class KeyStore {
|
|
|
239
315
|
if (!entry) {
|
|
240
316
|
continue;
|
|
241
317
|
}
|
|
242
|
-
|
|
243
|
-
|
|
318
|
+
// Either variable being set means this provider is already answered
|
|
319
|
+
// for, so the stored alternative must not be exported alongside it.
|
|
320
|
+
if (envNames(provider).some((name) => process.env[name])) {
|
|
244
321
|
continue;
|
|
245
322
|
}
|
|
246
|
-
env[
|
|
323
|
+
env[envOf(entry)] = this.reveal(entry);
|
|
324
|
+
if (entry.project && !process.env.GOOGLE_CLOUD_PROJECT) {
|
|
325
|
+
env.GOOGLE_CLOUD_PROJECT = entry.project;
|
|
326
|
+
}
|
|
327
|
+
if (entry.location && !process.env.GOOGLE_CLOUD_LOCATION) {
|
|
328
|
+
env.GOOGLE_CLOUD_LOCATION = entry.location;
|
|
329
|
+
}
|
|
247
330
|
}
|
|
248
331
|
return env;
|
|
249
332
|
}
|
|
@@ -284,6 +367,64 @@ export function mask(secret) {
|
|
|
284
367
|
export function describe(store, entry) {
|
|
285
368
|
return entry.holds === 'file' ? store.fileOf(entry) : mask(entry.value);
|
|
286
369
|
}
|
|
370
|
+
/** How an ambient credential is named on the command line: it has no key name. */
|
|
371
|
+
export function ambientId(cred) {
|
|
372
|
+
return cred.env ? `${cred.provider}/$${cred.env}` : `${cred.provider}/adc`;
|
|
373
|
+
}
|
|
374
|
+
/**
|
|
375
|
+
* Where `gcloud auth application-default login` leaves its credentials. The
|
|
376
|
+
* GenAI SDK reads this without being told to, so it counts even though nothing
|
|
377
|
+
* in the environment mentions it.
|
|
378
|
+
*/
|
|
379
|
+
export function gcloudAdc() {
|
|
380
|
+
const dir = process.env.CLOUDSDK_CONFIG ?? join(homedir(), '.config', 'gcloud');
|
|
381
|
+
const path = join(dir, 'application_default_credentials.json');
|
|
382
|
+
return existsSync(path) ? path : undefined;
|
|
383
|
+
}
|
|
384
|
+
export function ambient(store, only) {
|
|
385
|
+
const found = [];
|
|
386
|
+
for (const provider of only ?? OWNERS) {
|
|
387
|
+
for (const form of SHAPES[provider].forms) {
|
|
388
|
+
const value = process.env[form.env];
|
|
389
|
+
if (!value) {
|
|
390
|
+
continue;
|
|
391
|
+
}
|
|
392
|
+
// `materialize()` puts the store's own entries here. Reporting one
|
|
393
|
+
// of those as ambient would double-count it, and would claim the
|
|
394
|
+
// environment as a source that would survive removing the entry.
|
|
395
|
+
const active = store.active(provider);
|
|
396
|
+
if (active && envOf(active) === form.env && store.reveal(active) === value) {
|
|
397
|
+
continue;
|
|
398
|
+
}
|
|
399
|
+
found.push({ provider, env: form.env, holds: form.holds, value });
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
const adc = (only ?? OWNERS).includes('vertex') ? gcloudAdc() : undefined;
|
|
403
|
+
if (adc && !process.env.GOOGLE_APPLICATION_CREDENTIALS) {
|
|
404
|
+
found.push({ provider: 'vertex', holds: 'file', value: adc });
|
|
405
|
+
}
|
|
406
|
+
return found;
|
|
407
|
+
}
|
|
408
|
+
/**
|
|
409
|
+
* Every credential variable this process is carrying, whatever put it there.
|
|
410
|
+
*
|
|
411
|
+
* Read off the environment rather than off the store, and deliberately: by the
|
|
412
|
+
* time anyone asks, `materialize()` has already run, so the environment is the
|
|
413
|
+
* union of the keyring and whatever the shell brought — which is exactly the
|
|
414
|
+
* set of credentials the run is actually using.
|
|
415
|
+
*/
|
|
416
|
+
export function credentials() {
|
|
417
|
+
const found = [];
|
|
418
|
+
for (const provider of OWNERS) {
|
|
419
|
+
for (const form of SHAPES[provider].forms) {
|
|
420
|
+
const value = process.env[form.env];
|
|
421
|
+
if (value) {
|
|
422
|
+
found.push({ env: form.env, holds: form.holds, value });
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
return found;
|
|
427
|
+
}
|
|
287
428
|
// ---------------------------------------------------------------------------
|
|
288
429
|
// Gate
|
|
289
430
|
// ---------------------------------------------------------------------------
|
|
@@ -296,7 +437,7 @@ export function describe(store, entry) {
|
|
|
296
437
|
* one clear error here for an obscure one on the first turn.
|
|
297
438
|
*/
|
|
298
439
|
export function assertUsable(store) {
|
|
299
|
-
const reachable = PROVIDERS.filter((p) => process.env[
|
|
440
|
+
const reachable = PROVIDERS.filter((p) => envNames(p).some((name) => process.env[name]) || store.active(p) !== undefined);
|
|
300
441
|
if (reachable.length === 0) {
|
|
301
442
|
throw credentialError('no credentials for any provider', 'add one with: zen key add openai');
|
|
302
443
|
}
|
package/dist/lib.d.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
export { extract, invokedAs, one, parse, split, type Parsed, type Split } from './args.ts';
|
|
2
2
|
export { printBanner, type BannerText } from './banner.ts';
|
|
3
|
+
export { CATALOG_TTL_MS, CURATED, fetchCatalog, loadCatalog, loadCatalogs, matches, PREFERRED, type Catalog, type CatalogEntry, type CatalogOptions, type Filters, type Role, } from './catalog.ts';
|
|
3
4
|
export type { Command, Context } from './command.ts';
|
|
4
5
|
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 { assertNotEmpty, assertOwner, assertUsable, describe, envNames, envOf, form, isOwner, isProvider, keyId, KeyStore, mask, OWNERS, parseRef, PROVIDERS, SERVICES, SHAPES, type CredentialForm, type KeyCheck, type KeyEntry, type KeyOwner, type Liveness, type Provider, type Service, } from './keys.ts';
|
|
6
7
|
export { probe, probeAll } from './liveness.ts';
|
|
7
8
|
export { engineDisk, ensurePodmanReady, ownedContainers, podmanStatus, removeContainers, type DiskLine, type EngineDisk, type OwnedContainer, type PodmanOptions, type PodmanStatus, } from './podman.ts';
|
|
8
9
|
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';
|
package/dist/lib.js
CHANGED
|
@@ -23,8 +23,9 @@
|
|
|
23
23
|
// ---------------------------------------------------------------------------
|
|
24
24
|
export { extract, invokedAs, one, parse, split } from "./args.js";
|
|
25
25
|
export { printBanner } from "./banner.js";
|
|
26
|
+
export { CATALOG_TTL_MS, CURATED, fetchCatalog, loadCatalog, loadCatalogs, matches, PREFERRED, } from "./catalog.js";
|
|
26
27
|
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 { assertNotEmpty, assertOwner, assertUsable, describe, envNames, envOf, form, isOwner, isProvider, keyId, KeyStore, mask, OWNERS, parseRef, PROVIDERS, SERVICES, SHAPES, } from "./keys.js";
|
|
28
29
|
export { probe, probeAll } from "./liveness.js";
|
|
29
30
|
export { engineDisk, ensurePodmanReady, ownedContainers, podmanStatus, removeContainers, } from "./podman.js";
|
|
30
31
|
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";
|
package/dist/liveness.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import { type Embedder, type Model } from '@zenera/neo';
|
|
1
2
|
import { type KeyCheck, type KeyEntry, type KeyStore } from './keys.ts';
|
|
3
|
+
export declare function classify(err: unknown): KeyCheck;
|
|
2
4
|
/**
|
|
3
5
|
* The cheapest authenticated call each SDK has. Nothing here reads a model or
|
|
4
6
|
* spends a token: the question is only whether the credential is accepted.
|
|
@@ -6,18 +8,58 @@ import { type KeyCheck, type KeyEntry, type KeyStore } from './keys.ts';
|
|
|
6
8
|
* The client is built through the library's own registry rather than by
|
|
7
9
|
* requiring the SDKs directly, so a missing optional dependency produces the
|
|
8
10
|
* library's "run: npm i openai" message instead of a raw MODULE_NOT_FOUND.
|
|
11
|
+
*
|
|
12
|
+
* A secret is handed to the registry as a value rather than exported first,
|
|
13
|
+
* which is what lets several of these run at once: two probes sharing
|
|
14
|
+
* `process.env` would each read the other's key.
|
|
9
15
|
*/
|
|
10
16
|
export declare function probe(store: KeyStore, entry: KeyEntry): Promise<KeyCheck>;
|
|
11
17
|
/**
|
|
12
|
-
* Probes many entries,
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
18
|
+
* Probes many entries together, because they are independent questions asked
|
|
19
|
+
* of different vendors and the answer is a round trip apiece: in sequence,
|
|
20
|
+
* five keys is five deadlines end to end, and `zen init` spends them all
|
|
21
|
+
* before it writes a file.
|
|
22
|
+
*
|
|
23
|
+
* The exception is a credential the SDK can only be given through the
|
|
24
|
+
* environment — the vertex service-account file — since two of those in flight
|
|
25
|
+
* would each read the other's path. Those go one at a time, after the rest.
|
|
26
|
+
*
|
|
27
|
+
* `onProbe` therefore reports what has *finished* rather than what is being
|
|
28
|
+
* waited on; with several in the air there is no single one to name.
|
|
17
29
|
*
|
|
18
30
|
* Pairs rather than a map, because the caller needs the entry itself to record
|
|
19
31
|
* the result against, and a map keyed by a string would only have to be
|
|
20
32
|
* un-joined again.
|
|
21
33
|
*/
|
|
22
|
-
export declare function probeAll(store: KeyStore, entries: readonly KeyEntry[], onProbe?: (entry: KeyEntry,
|
|
34
|
+
export declare function probeAll(store: KeyStore, entries: readonly KeyEntry[], onProbe?: (entry: KeyEntry, done: number, total: number) => void): Promise<[KeyEntry, KeyCheck][]>;
|
|
35
|
+
/** What the project calls a model, and the thing that call resolved to. */
|
|
36
|
+
export type ModelTarget = {
|
|
37
|
+
ref: string;
|
|
38
|
+
kind: 'model';
|
|
39
|
+
model: Model;
|
|
40
|
+
} | {
|
|
41
|
+
ref: string;
|
|
42
|
+
kind: 'embedding';
|
|
43
|
+
embedder: Embedder;
|
|
44
|
+
};
|
|
45
|
+
export interface ModelProbe {
|
|
46
|
+
/** the reference as the config writes it — an alias, or a full ref */
|
|
47
|
+
ref: string;
|
|
48
|
+
/** the id that goes on the wire */
|
|
49
|
+
id: string;
|
|
50
|
+
kind: 'model' | 'embedding';
|
|
51
|
+
check: KeyCheck;
|
|
52
|
+
/** how long the round trip took, for the one that is merely slow */
|
|
53
|
+
ms: number;
|
|
54
|
+
/** embeddings only: the width the model actually returned */
|
|
55
|
+
dimensions?: number;
|
|
56
|
+
}
|
|
57
|
+
export declare function probeModel(target: ModelTarget): Promise<ModelProbe>;
|
|
58
|
+
/**
|
|
59
|
+
* One round trip per model, run together — they are independent questions, and
|
|
60
|
+
* a project with four models should not take four deadlines to answer. Nothing
|
|
61
|
+
* here touches `process.env`, so unlike the credential probes there is no case
|
|
62
|
+
* that has to go alone.
|
|
63
|
+
*/
|
|
64
|
+
export declare function probeModels(targets: readonly ModelTarget[], onProbe?: (target: ModelTarget, done: number, total: number) => void): Promise<ModelProbe[]>;
|
|
23
65
|
//# sourceMappingURL=liveness.d.ts.map
|