@zenera/cli 1.1.29 → 1.1.30

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.
@@ -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 gateway = store.active(mode);
135
- note(`${cyan(badge(`⇄ ${mode.toUpperCase()} MODE`))} ${bold(gateway?.url ?? 'no url stored')}`);
136
- note(dim(`every model goes through the gateway with ${gateway ? keyId(gateway) : 'no key'}; ` +
137
- 'vendor keys are not used — zen key mode direct to stop'));
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
- store.setMode(wanted);
396
- store.save();
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 gateway = now === 'direct' ? undefined : store.active(now);
421
+ const from = store.modeFrom;
422
+ const { url, key } = now === 'direct' ? {} : route(store, now);
400
423
  if (ctx.json) {
401
- json({ mode: now, ...(gateway ? { key: keyId(gateway), url: gateway.url } : {}) });
424
+ json({ mode: now, from, ...(key ? { key } : {}), ...(url ? { url } : {}) });
402
425
  return;
403
426
  }
404
- if (!gateway) {
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')} ${gateway.url} ` +
409
- dim(`with ${keyId(gateway)}`));
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
- assertNotEmpty(store);
415
- const targets = positionals[0] ? select(store, positionals[0]) : store.entries;
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.29",
3
+ "version": "1.1.30",
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.29",
58
- "@zenera/view": "^1.1.29",
57
+ "@zenera/neo": "^1.1.30",
58
+ "@zenera/view": "^1.1.30",
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. In this mode `zen models` lists only
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