@zenera/cli 1.1.29 → 1.1.31
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/dist/catalog.js +30 -18
- package/dist/commands/key.js +51 -18
- package/dist/keys.d.ts +2 -0
- package/dist/keys.js +5 -1
- package/package.json +3 -3
- package/templates/editor/.github/skills/zen-cli/references/keys.md +3 -1
package/dist/catalog.js
CHANGED
|
@@ -463,10 +463,6 @@ const store = () => new Cache(CATALOG_KIND, { mode: 0o644 });
|
|
|
463
463
|
* to both. A project left to the service-account file is not seen here.
|
|
464
464
|
*/
|
|
465
465
|
function scope(provider) {
|
|
466
|
-
const at = gatewayOf();
|
|
467
|
-
if (at) {
|
|
468
|
-
return cacheKey(GATEWAY_NAME, at.baseURL);
|
|
469
|
-
}
|
|
470
466
|
if (provider !== 'vertex') {
|
|
471
467
|
return undefined;
|
|
472
468
|
}
|
|
@@ -483,6 +479,29 @@ function readCache(provider) {
|
|
|
483
479
|
? file
|
|
484
480
|
: undefined;
|
|
485
481
|
}
|
|
482
|
+
/**
|
|
483
|
+
* Through a gateway nothing is cached on disk: the gateway caches its own
|
|
484
|
+
* listing, one request serves every provider, and a stale copy outlives the
|
|
485
|
+
* gateway's providers or the virtual key that was allowed to see them.
|
|
486
|
+
*/
|
|
487
|
+
async function loadGatewayCatalog(provider, at) {
|
|
488
|
+
const via = { via: { name: GATEWAY_NAME, url: at.baseURL } };
|
|
489
|
+
try {
|
|
490
|
+
const entries = await fetchGatewayCatalog(provider, at);
|
|
491
|
+
return { provider, entries, origin: 'live', fetchedAt: new Date().toISOString(), ...via };
|
|
492
|
+
}
|
|
493
|
+
catch (err) {
|
|
494
|
+
// No built-in list: it names vendor models the gateway may not serve at all.
|
|
495
|
+
return {
|
|
496
|
+
provider,
|
|
497
|
+
entries: [],
|
|
498
|
+
origin: 'curated',
|
|
499
|
+
fetchedAt: new Date(0).toISOString(),
|
|
500
|
+
problem: classify(err),
|
|
501
|
+
...via,
|
|
502
|
+
};
|
|
503
|
+
}
|
|
504
|
+
}
|
|
486
505
|
/**
|
|
487
506
|
* The listing for one provider, from the cheapest source that can answer.
|
|
488
507
|
*
|
|
@@ -494,9 +513,9 @@ function readCache(provider) {
|
|
|
494
513
|
*/
|
|
495
514
|
export async function loadCatalog(provider, opts = {}) {
|
|
496
515
|
const at = gatewayOf();
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
516
|
+
if (at) {
|
|
517
|
+
return loadGatewayCatalog(provider, at);
|
|
518
|
+
}
|
|
500
519
|
const cached = readCache(provider);
|
|
501
520
|
const fresh = cached && Date.now() - new Date(cached.fetchedAt).getTime() < CATALOG_TTL_MS
|
|
502
521
|
? cached
|
|
@@ -507,17 +526,14 @@ export async function loadCatalog(provider, opts = {}) {
|
|
|
507
526
|
entries: fresh.entries,
|
|
508
527
|
origin: 'cache',
|
|
509
528
|
fetchedAt: fresh.fetchedAt,
|
|
510
|
-
...via,
|
|
511
529
|
};
|
|
512
530
|
}
|
|
513
531
|
if (!opts.offline) {
|
|
514
532
|
try {
|
|
515
|
-
const entries =
|
|
516
|
-
? await fetchGatewayCatalog(provider, at)
|
|
517
|
-
: await fetchCatalog(provider);
|
|
533
|
+
const entries = await fetchCatalog(provider);
|
|
518
534
|
const fetchedAt = new Date().toISOString();
|
|
519
535
|
store().put(catalogKey(provider), { provider, fetchedAt, entries });
|
|
520
|
-
return { provider, entries, origin: 'live', fetchedAt
|
|
536
|
+
return { provider, entries, origin: 'live', fetchedAt };
|
|
521
537
|
}
|
|
522
538
|
catch (err) {
|
|
523
539
|
const problem = classify(err);
|
|
@@ -528,16 +544,14 @@ export async function loadCatalog(provider, opts = {}) {
|
|
|
528
544
|
origin: 'stale',
|
|
529
545
|
fetchedAt: cached.fetchedAt,
|
|
530
546
|
problem,
|
|
531
|
-
...via,
|
|
532
547
|
};
|
|
533
548
|
}
|
|
534
549
|
return {
|
|
535
550
|
provider,
|
|
536
|
-
entries:
|
|
551
|
+
entries: curated(provider),
|
|
537
552
|
origin: 'curated',
|
|
538
553
|
fetchedAt: new Date(0).toISOString(),
|
|
539
554
|
problem,
|
|
540
|
-
...via,
|
|
541
555
|
};
|
|
542
556
|
}
|
|
543
557
|
}
|
|
@@ -547,15 +561,13 @@ export async function loadCatalog(provider, opts = {}) {
|
|
|
547
561
|
entries: cached.entries,
|
|
548
562
|
origin: 'stale',
|
|
549
563
|
fetchedAt: cached.fetchedAt,
|
|
550
|
-
...via,
|
|
551
564
|
};
|
|
552
565
|
}
|
|
553
566
|
return {
|
|
554
567
|
provider,
|
|
555
|
-
entries:
|
|
568
|
+
entries: curated(provider),
|
|
556
569
|
origin: 'curated',
|
|
557
570
|
fetchedAt: new Date(0).toISOString(),
|
|
558
|
-
...via,
|
|
559
571
|
};
|
|
560
572
|
}
|
|
561
573
|
// ---------------------------------------------------------------------------
|
package/dist/commands/key.js
CHANGED
|
@@ -4,7 +4,7 @@ import { parse } from "../args.js";
|
|
|
4
4
|
import { ensureHome } from "../home.js";
|
|
5
5
|
import { ambient, ambientId, assertNotEmpty, checkRegion, describe, envNames, envOf, formOf, GATEWAYS, isGateway, keyId, KeyStore, mask, MODES, OWNERS, parseRef, SHAPES, } from "../keys.js";
|
|
6
6
|
import { probe, probeAll } from "../liveness.js";
|
|
7
|
-
import { ago, ask, askSecret, badge, bold, confirm, credentialError, cyan, dim, green, isInteractive, json, note, progress, readStdin, red, table, usageError, write, writeAll, yellow, } from "../term.js";
|
|
7
|
+
import { ago, ask, askSecret, badge, bold, confirm, credentialError, cyan, dim, green, isInteractive, json, note, progress, readStdin, red, table, usageError, warn, write, writeAll, yellow, } from "../term.js";
|
|
8
8
|
const USAGE = 'zen key <ls|add|use|mode|check|rm|show|env> [ref] [options]';
|
|
9
9
|
// ---------------------------------------------------------------------------
|
|
10
10
|
// Display
|
|
@@ -54,15 +54,28 @@ function placed(store, entry) {
|
|
|
54
54
|
* intent — it can be listed and probed, not chosen or forgotten.
|
|
55
55
|
*/
|
|
56
56
|
function asEntry(cred) {
|
|
57
|
+
const url = isGateway(cred.provider) ? envUrl() : undefined;
|
|
57
58
|
return {
|
|
58
59
|
provider: cred.provider,
|
|
59
60
|
name: cred.env ? `$${cred.env}` : 'adc',
|
|
60
61
|
holds: cred.holds,
|
|
61
62
|
value: cred.value,
|
|
62
63
|
env: cred.env,
|
|
64
|
+
...(url ? { url } : {}),
|
|
63
65
|
addedAt: new Date().toISOString(),
|
|
64
66
|
};
|
|
65
67
|
}
|
|
68
|
+
const envUrl = () => process.env[GATEWAY_URL_ENV]?.trim() || undefined;
|
|
69
|
+
/** Where a gateway mode sends runs, and with which key: the environment beats the keyring. */
|
|
70
|
+
function route(store, mode) {
|
|
71
|
+
const entry = store.active(mode);
|
|
72
|
+
const shell = envNames(mode).find((name) => process.env[name]);
|
|
73
|
+
return {
|
|
74
|
+
url: envUrl() ?? entry?.url,
|
|
75
|
+
key: shell ? `$${shell}` : entry ? keyId(entry) : undefined,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
const FROM_ENV = `set by $${GATEWAY_URL_ENV} in the environment`;
|
|
66
79
|
function rows(store, borrowed) {
|
|
67
80
|
const out = [
|
|
68
81
|
[bold(''), bold('KEY'), bold('VALUE'), bold('WHERE'), bold('STATE'), bold('CHECKED')],
|
|
@@ -131,10 +144,12 @@ function modeLine(store) {
|
|
|
131
144
|
}
|
|
132
145
|
return;
|
|
133
146
|
}
|
|
134
|
-
const
|
|
135
|
-
note(`${cyan(badge(`⇄ ${mode.toUpperCase()} MODE`))} ${bold(
|
|
136
|
-
note(dim(`every model goes through the gateway with ${
|
|
137
|
-
|
|
147
|
+
const { url, key } = route(store, mode);
|
|
148
|
+
note(`${cyan(badge(`⇄ ${mode.toUpperCase()} MODE`))} ${bold(url ?? 'no url stored')}`);
|
|
149
|
+
note(dim(`every model goes through the gateway with ${key ?? 'no key'}; vendor keys are not used — ` +
|
|
150
|
+
(store.modeFrom === 'env'
|
|
151
|
+
? `${FROM_ENV}, unset it to stop`
|
|
152
|
+
: 'zen key mode direct to stop')));
|
|
138
153
|
note('');
|
|
139
154
|
}
|
|
140
155
|
/**
|
|
@@ -189,7 +204,7 @@ const ls = async (ctx, args) => {
|
|
|
189
204
|
});
|
|
190
205
|
return;
|
|
191
206
|
}
|
|
192
|
-
if (store.entries.length === 0 && borrowed.length === 0) {
|
|
207
|
+
if (store.entries.length === 0 && borrowed.length === 0 && store.mode === 'direct') {
|
|
193
208
|
note('the keyring is empty');
|
|
194
209
|
note(dim('add one: zen key add openai'));
|
|
195
210
|
return;
|
|
@@ -392,27 +407,44 @@ const mode = async (ctx, args) => {
|
|
|
392
407
|
if (!MODES.includes(wanted)) {
|
|
393
408
|
throw usageError(`unknown mode "${wanted}"`, `one of: ${MODES.join(', ')}`);
|
|
394
409
|
}
|
|
395
|
-
|
|
396
|
-
store.
|
|
410
|
+
// Already on through the environment: storing it as well needs a gateway key of its own.
|
|
411
|
+
const viaEnv = store.modeFrom === 'env' && wanted === store.mode;
|
|
412
|
+
if (!viaEnv || store.active(store.mode)?.url) {
|
|
413
|
+
store.setMode(wanted);
|
|
414
|
+
store.save();
|
|
415
|
+
}
|
|
416
|
+
if (store.modeFrom === 'env' && wanted !== store.mode) {
|
|
417
|
+
warn(`saved, but ${FROM_ENV} wins: unset it to call vendors directly`);
|
|
418
|
+
}
|
|
397
419
|
}
|
|
398
420
|
const now = store.mode;
|
|
399
|
-
const
|
|
421
|
+
const from = store.modeFrom;
|
|
422
|
+
const { url, key } = now === 'direct' ? {} : route(store, now);
|
|
400
423
|
if (ctx.json) {
|
|
401
|
-
json({ mode: now, ...(
|
|
424
|
+
json({ mode: now, from, ...(key ? { key } : {}), ...(url ? { url } : {}) });
|
|
402
425
|
return;
|
|
403
426
|
}
|
|
404
|
-
if (
|
|
427
|
+
if (now === 'direct') {
|
|
405
428
|
note(`${cyan('direct')} ${dim('— each provider is called with its own key')}`);
|
|
406
429
|
return;
|
|
407
430
|
}
|
|
408
|
-
note(`${cyan(now)} ${dim('— every model goes through')} ${
|
|
409
|
-
dim(`with ${
|
|
431
|
+
note(`${cyan(now)} ${dim('— every model goes through')} ${url ?? 'no url stored'} ` +
|
|
432
|
+
dim(`with ${key ?? 'no key'}`));
|
|
433
|
+
if (from === 'env') {
|
|
434
|
+
note(dim(FROM_ENV));
|
|
435
|
+
}
|
|
410
436
|
};
|
|
411
437
|
const check = async (ctx, args) => {
|
|
412
438
|
const { positionals } = parse(args, {}, 'zen key check [provider[/name]]');
|
|
413
439
|
const store = await KeyStore.open();
|
|
414
|
-
|
|
415
|
-
const
|
|
440
|
+
// Checked like stored keys, never recorded: `store.record` finds no entry for them.
|
|
441
|
+
const borrowed = ambient(store).map(asEntry);
|
|
442
|
+
const targets = positionals[0]
|
|
443
|
+
? select(store, positionals[0], borrowed)
|
|
444
|
+
: [...store.entries, ...borrowed];
|
|
445
|
+
if (targets.length === 0) {
|
|
446
|
+
assertNotEmpty(store);
|
|
447
|
+
}
|
|
416
448
|
const checks = await checkAll(ctx, store, targets);
|
|
417
449
|
for (const [entry, result] of checks) {
|
|
418
450
|
store.record(entry, result);
|
|
@@ -441,12 +473,13 @@ const check = async (ctx, args) => {
|
|
|
441
473
|
throw credentialError('at least one key authenticated but cannot be used');
|
|
442
474
|
}
|
|
443
475
|
};
|
|
444
|
-
function select(store, ref) {
|
|
476
|
+
function select(store, ref, borrowed = []) {
|
|
445
477
|
const { provider, name } = parseRef(ref);
|
|
446
478
|
if (!name) {
|
|
447
|
-
return store.for(provider);
|
|
479
|
+
return [...store.for(provider), ...borrowed.filter((e) => e.provider === provider)];
|
|
448
480
|
}
|
|
449
|
-
const entry = store.find(provider, name)
|
|
481
|
+
const entry = store.find(provider, name) ??
|
|
482
|
+
borrowed.find((e) => e.provider === provider && e.name === name);
|
|
450
483
|
if (!entry) {
|
|
451
484
|
throw usageError(`no key ${ref}`, 'see: zen key ls');
|
|
452
485
|
}
|
package/dist/keys.d.ts
CHANGED
|
@@ -164,7 +164,9 @@ export declare class KeyStore {
|
|
|
164
164
|
remove(provider: KeyOwner, name: string): boolean;
|
|
165
165
|
use(provider: KeyOwner, name: string): KeyEntry;
|
|
166
166
|
record(entry: KeyEntry, check: KeyCheck): void;
|
|
167
|
+
/** A `BIFROST_BASE_URL` in the environment wins over the keyring, as it does for the runtime. */
|
|
167
168
|
get mode(): KeyMode;
|
|
169
|
+
get modeFrom(): 'env' | 'keyring';
|
|
168
170
|
/** Refuses a gateway mode with no gateway to send anything to. */
|
|
169
171
|
setMode(mode: KeyMode): void;
|
|
170
172
|
save(): void;
|
package/dist/keys.js
CHANGED
|
@@ -380,8 +380,12 @@ export class KeyStore {
|
|
|
380
380
|
hit.check = check;
|
|
381
381
|
}
|
|
382
382
|
}
|
|
383
|
+
/** A `BIFROST_BASE_URL` in the environment wins over the keyring, as it does for the runtime. */
|
|
383
384
|
get mode() {
|
|
384
|
-
return this.#file.mode ?? 'direct';
|
|
385
|
+
return this.modeFrom === 'env' ? 'bifrost' : (this.#file.mode ?? 'direct');
|
|
386
|
+
}
|
|
387
|
+
get modeFrom() {
|
|
388
|
+
return process.env[GATEWAY_URL_ENV]?.trim() ? 'env' : 'keyring';
|
|
385
389
|
}
|
|
386
390
|
/** Refuses a gateway mode with no gateway to send anything to. */
|
|
387
391
|
setMode(mode) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zenera/cli",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.31",
|
|
4
4
|
"description": "Command-line front end for @zenera/neo: agentic projects you can run, share and commit.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"agents",
|
|
@@ -54,8 +54,8 @@
|
|
|
54
54
|
"@google/genai": "^2.30.0",
|
|
55
55
|
"@inkjs/ui": "^2.0.0",
|
|
56
56
|
"@openrouter/sdk": "^1.4.25",
|
|
57
|
-
"@zenera/neo": "^1.1.
|
|
58
|
-
"@zenera/view": "^1.1.
|
|
57
|
+
"@zenera/neo": "^1.1.31",
|
|
58
|
+
"@zenera/view": "^1.1.31",
|
|
59
59
|
"google-auth-library": "^10.0.0",
|
|
60
60
|
"ink": "^7.1.1",
|
|
61
61
|
"openai": "^6.0.0",
|
|
@@ -73,7 +73,9 @@ Declared providers are rerouted too. `zen check` then reports
|
|
|
73
73
|
`BIFROST_API_KEY`, not the vendor variables, as the credential in use.
|
|
74
74
|
|
|
75
75
|
The switch is `BIFROST_BASE_URL` itself, so exporting it in a shell or a `.env`
|
|
76
|
-
turns gateway mode on without the keyring
|
|
76
|
+
turns gateway mode on without the keyring - and `zen key mode`/`ls`/`check`
|
|
77
|
+
report and probe it (`bifrost/$BIFROST_API_KEY`), with nothing stored. In this
|
|
78
|
+
mode `zen models` lists only
|
|
77
79
|
what the gateway serves - see `models.md`. The full guide is
|
|
78
80
|
[docs/gateway.md](https://github.com/andreyryabov/ZeneraNeo/blob/main/docs/gateway.md).
|
|
79
81
|
|