@zenera/cli 1.1.30 → 1.1.32

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 CHANGED
@@ -14,6 +14,9 @@
14
14
  [![npm](https://img.shields.io/npm/v/@zenera/cli.svg)](https://www.npmjs.com/package/@zenera/cli)
15
15
  [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/andreyryabov/ZeneraNeo/blob/main/LICENSE)
16
16
  [![Node](https://img.shields.io/badge/node-%E2%89%A524-brightgreen.svg)](https://nodejs.org)
17
+ [![CI](https://github.com/andreyryabov/ZeneraNeo/actions/workflows/ci.yml/badge.svg)](https://github.com/andreyryabov/ZeneraNeo/actions/workflows/ci.yml)
18
+
19
+ [Install](#install) · [Quickstart](#quickstart) · [Commands](#commands) · [Documentation](#documentation) · [Family](#the-rest-of-the-family) · [License](#license)
17
20
 
18
21
  </div>
19
22
 
@@ -1125,6 +1128,16 @@ terminal:
1125
1128
  - [docs/gateway.md](https://github.com/andreyryabov/ZeneraNeo/blob/main/docs/gateway.md)
1126
1129
  - every model through a Bifrost gateway, with one virtual key.
1127
1130
 
1131
+ ## The rest of the family
1132
+
1133
+ | Package | What it is |
1134
+ | ----------------------------------------------------------------------------------------------- | ------------------------------------------------------------- |
1135
+ | **`@zenera/cli`** | `zen` - agent projects on the command line |
1136
+ | [`@zenera/neo`](https://github.com/andreyryabov/ZeneraNeo/blob/main/packages/neo/README.md) | the runtime - agents, models, tools, skills, memory |
1137
+ | [`@zenera/faker`](https://github.com/andreyryabov/ZeneraNeo/blob/main/packages/faker/README.md) | `zen faker` - a mock API from an openapi/swagger document |
1138
+ | [`@zenera/rag`](https://github.com/andreyryabov/ZeneraNeo/blob/main/packages/rag/README.md) | `zen rag` - indexing and search for documents and API schemas |
1139
+ | [`@zenera/view`](https://github.com/andreyryabov/ZeneraNeo/blob/main/packages/view/README.md) | `zen view` - a live local viewer for a folder |
1140
+
1128
1141
  ## License
1129
1142
 
1130
1143
  Early days and moving fast - issues, questions and pull requests are welcome.
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
- const via = at ? { via: { name: GATEWAY_NAME, url: at.baseURL } } : {};
498
- // The built-in list names vendor models the gateway may not serve at all.
499
- const fallback = () => (at ? [] : curated(provider));
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 = at
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, ...via };
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: fallback(),
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: fallback(),
568
+ entries: curated(provider),
556
569
  origin: 'curated',
557
570
  fetchedAt: new Date(0).toISOString(),
558
- ...via,
559
571
  };
560
572
  }
561
573
  // ---------------------------------------------------------------------------
@@ -1,4 +1,4 @@
1
- import { readProjectConfig } from '@zenera/neo';
1
+ import { gatewayOf, readProjectConfig } from '@zenera/neo';
2
2
  import { spawn } from 'node:child_process';
3
3
  import { randomUUID } from 'node:crypto';
4
4
  import { existsSync, readFileSync } from 'node:fs';
@@ -11,7 +11,7 @@ import { copilotEventLog } from "../eventlog.js";
11
11
  import { ensureHome } from "../home.js";
12
12
  import { assertOwner, isProvider, KeyStore, PROVIDERS } from "../keys.js";
13
13
  import { probeChat, probeModel } from "../liveness.js";
14
- import { answerBox, chooseModel, DATASET_DETAILS, DATASET_SUMMARY, DATASET_USAGE, datasetProjectFlag, DEFAULT_RESUMES, defaultRef, editorFiles, FINETUNE_DETAILS, FINETUNE_SUMMARY, FINETUNE_USAGE, finetuneProjectFlag, lastSession, launch, listPrompts, loadPrompt, locate, masked, misspelledProvider, MODEL_ENV, openLog, ORDER, PROMPT_DIR, providersWarning, readMeta, recordSession, RESUME_PROMPT, resumeDelayMs, runDataset, runFinetune, sameModel, sessionModel, sessionTotals, SOURCE_LABELS, startVertexRelay, tailSpans, Tally, terminalSink, transient, wire, writeMeta, } from "../meta/index.js";
14
+ import { answerBox, chooseModel, DATASET_DETAILS, DATASET_SUMMARY, DATASET_USAGE, datasetProjectFlag, DEFAULT_RESUMES, defaultRef, editorFiles, FINETUNE_DETAILS, FINETUNE_SUMMARY, FINETUNE_USAGE, finetuneProjectFlag, lastSession, launch, listPrompts, loadPrompt, locate, masked, misspelledProvider, MODEL_ENV, openLog, ORDER, PROMPT_DIR, providersWarning, readMeta, recordSession, RESUME_PROMPT, resumeDelayMs, runDataset, runFinetune, sameModel, sessionModel, sessionTotals, SOURCE_LABELS, startVertexRelay, tailSpans, Tally, terminalSink, transient, wire, wireGateway, writeMeta, } from "../meta/index.js";
15
15
  import { splitRef } from "../modelref.js";
16
16
  import { duration } from "../narrate.js";
17
17
  import * as Projects from "../projects.js";
@@ -554,6 +554,8 @@ async function go(ctx, values, project, prompt, promptName) {
554
554
  ensureHome();
555
555
  const store = await KeyStore.open();
556
556
  store.materialize();
557
+ // After materialize: a keyring in bifrost mode is what sets the url.
558
+ const gateway = gatewayOf();
557
559
  const only = values.provider ? asProvider(values.provider) : undefined;
558
560
  const chosen = chooseModel({
559
561
  flag: values.model,
@@ -561,7 +563,7 @@ async function go(ctx, values, project, prompt, promptName) {
561
563
  env: process.env[MODEL_ENV],
562
564
  stored: (await readMeta()).model,
563
565
  project: projectModel(project.dir),
564
- fallback: defaultRef(store, only),
566
+ fallback: defaultRef(store, only, gateway !== undefined),
565
567
  });
566
568
  if (!chosen) {
567
569
  if (only) {
@@ -577,11 +579,11 @@ async function go(ctx, values, project, prompt, promptName) {
577
579
  if (meant && !only) {
578
580
  warn(`${chosen.ref} names no provider — did you mean ${meant}/${id}?`);
579
581
  }
580
- const entry = store.active(owner);
581
- if (!entry) {
582
+ const entry = gateway ? undefined : store.active(owner);
583
+ if (!gateway && !entry) {
582
584
  throw credentialError(`no ${owner} key`, `add one: zen key add ${owner}`);
583
585
  }
584
- const wiring = wire(store, entry, id);
586
+ const wiring = gateway ? wireGateway(gateway, owner, id) : wire(store, entry, id);
585
587
  const binary = locate();
586
588
  // Copilot cannot carry a session to another model: the resume fails on the first call.
587
589
  const ran = values.resume ? sessionModel(values.resume) : undefined;
@@ -999,10 +1001,10 @@ async function vet(ctx, ref) {
999
1001
  const { provider, id } = splitRef(ref);
1000
1002
  const owner = provider ?? 'openai';
1001
1003
  const store = await KeyStore.open();
1002
- if (!store.active(owner)) {
1004
+ store.materialize();
1005
+ if (!gatewayOf() && !store.active(owner)) {
1003
1006
  throw credentialError(`no ${owner} key to try ${id} with`, `add one: zen key add ${owner}`);
1004
1007
  }
1005
- store.materialize();
1006
1008
  const bar = ctx.json ? undefined : progress();
1007
1009
  bar?.update(dim(`asking ${owner} about ${id} …`));
1008
1010
  const probe = await probeModel({
@@ -1058,13 +1060,12 @@ async function pickModel(ctx) {
1058
1060
  throw usageError('--pick needs a terminal', 'name the model instead');
1059
1061
  }
1060
1062
  const store = await KeyStore.open();
1061
- const known = store.entries
1062
- .filter((e) => e.provider !== 'exa')
1063
- .map((e) => e.provider);
1064
- if (known.length === 0) {
1063
+ store.materialize();
1064
+ const known = store.entries.map((e) => e.provider).filter(isProvider);
1065
+ if (known.length === 0 && !gatewayOf()) {
1065
1066
  throw credentialError('no keys to choose from', 'add one: zen key add openai');
1066
1067
  }
1067
- const held = [...new Set(known)];
1068
+ const held = gatewayOf() ? [] : [...new Set(known)];
1068
1069
  const { browseModels } = await import("./models.js");
1069
1070
  const chosen = await browseModels({
1070
1071
  initial: held.length === 1 ? held[0] : undefined,
package/dist/keys.d.ts CHANGED
@@ -202,7 +202,12 @@ export declare class KeyStore {
202
202
  * since its url alone is what switches the library over.
203
203
  */
204
204
  environment(only?: KeyOwner[]): Record<string, string>;
205
- /** Applies `environment()` to this process. Returns what it set. */
205
+ /**
206
+ * Copies the keyring's active credentials into `process.env`, the only
207
+ * place the library looks. Must run before anything that reads them —
208
+ * in `bifrost` mode this is what sets `BIFROST_BASE_URL`, so `gatewayOf()`
209
+ * returns undefined until it has. Returns what it set.
210
+ */
206
211
  materialize(only?: KeyOwner[]): Record<string, string>;
207
212
  }
208
213
  /**
package/dist/keys.js CHANGED
@@ -482,7 +482,12 @@ export class KeyStore {
482
482
  }
483
483
  return env;
484
484
  }
485
- /** Applies `environment()` to this process. Returns what it set. */
485
+ /**
486
+ * Copies the keyring's active credentials into `process.env`, the only
487
+ * place the library looks. Must run before anything that reads them —
488
+ * in `bifrost` mode this is what sets `BIFROST_BASE_URL`, so `gatewayOf()`
489
+ * returns undefined until it has. Returns what it set.
490
+ */
486
491
  materialize(only) {
487
492
  const env = this.environment(only);
488
493
  Object.assign(process.env, env);
@@ -21,12 +21,21 @@ export interface ModelSources {
21
21
  }
22
22
  /** Where the answer is looked for, best first. */
23
23
  export declare const ORDER: readonly ModelSource[];
24
+ /**
25
+ * Decides which model `zen meta` runs copilot on, when it can be named in six
26
+ * places at once. The source comes back too, so a surprising pick can be traced
27
+ * ("from agents.yaml model:") instead of guessed at.
28
+ */
24
29
  export declare function chooseModel(sources: ModelSources): ModelChoice | undefined;
25
30
  export declare const SOURCE_LABELS: Record<ModelSource, string>;
26
31
  /** First of each row is the default; the rest are the next best. */
27
32
  export declare const RECOMMENDED: Partial<Record<Provider, readonly string[]>>;
28
- /** The recommended ref for the first provider with a key, or for `only`. */
29
- export declare function defaultRef(store: KeyStore, only?: Provider): string | undefined;
33
+ /**
34
+ * The model `zen meta` falls back to when nothing names one, so a machine with
35
+ * just a key (or just a gateway, which reaches every provider) still runs.
36
+ * `only` is `--provider`: the recommendation for that provider, if it is held.
37
+ */
38
+ export declare function defaultRef(store: KeyStore, only?: Provider, gateway?: boolean): string | undefined;
30
39
  export interface MetaFile {
31
40
  version: 1;
32
41
  model?: string;
@@ -13,6 +13,11 @@ export const ORDER = [
13
13
  'project',
14
14
  'default',
15
15
  ];
16
+ /**
17
+ * Decides which model `zen meta` runs copilot on, when it can be named in six
18
+ * places at once. The source comes back too, so a surprising pick can be traced
19
+ * ("from agents.yaml model:") instead of guessed at.
20
+ */
16
21
  export function chooseModel(sources) {
17
22
  const at = {
18
23
  flag: sources.flag,
@@ -57,11 +62,15 @@ export const RECOMMENDED = {
57
62
  };
58
63
  /** Which provider is tried first when several are held. */
59
64
  const PREFERENCE = ['anthropic', 'openai', 'vertex', 'google', 'openrouter'];
60
- /** The recommended ref for the first provider with a key, or for `only`. */
61
- export function defaultRef(store, only) {
65
+ /**
66
+ * The model `zen meta` falls back to when nothing names one, so a machine with
67
+ * just a key (or just a gateway, which reaches every provider) still runs.
68
+ * `only` is `--provider`: the recommendation for that provider, if it is held.
69
+ */
70
+ export function defaultRef(store, only, gateway = false) {
62
71
  for (const provider of only ? [only] : PREFERENCE) {
63
72
  const id = RECOMMENDED[provider]?.[0];
64
- if (id && store.active(provider)) {
73
+ if (id && (gateway || store.active(provider))) {
65
74
  return `${provider}/${id}`;
66
75
  }
67
76
  }
@@ -1,3 +1,4 @@
1
+ import { type Gateway } from '@zenera/neo';
1
2
  import { type KeyEntry, type KeyStore, type Provider } from './host.ts';
2
3
  /**
3
4
  * Copilot offers its tools as OpenAI *custom* tools, which the completions API
@@ -35,6 +36,12 @@ export declare const RELAY_URL = "http://127.0.0.1:<relay>";
35
36
  * through a GitHub account we are deliberately not using.
36
37
  */
37
38
  export declare function wire(store: KeyStore, entry: KeyEntry, id: string): Wiring;
39
+ /**
40
+ * Through a gateway: one virtual key, and Bifrost's OpenAI or Anthropic drop-in,
41
+ * which routes on an `<upstream>/<id>` wire model. Claude speaks Anthropic
42
+ * wherever it is hosted; everything else goes through the OpenAI one.
43
+ */
44
+ export declare function wireGateway(at: Gateway, provider: Provider, id: string): Wiring;
38
45
  /** Values a `--dry-run` may print. A key is four characters and an apology. */
39
46
  export declare function masked(env: Record<string, string>, secret: readonly string[]): string[];
40
47
  /** Non-fatal, said once, because a competing file is a silent override. */
package/dist/meta/wire.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { GATEWAY_UPSTREAMS } from '@zenera/neo';
1
2
  import { randomBytes } from 'node:crypto';
2
3
  import { existsSync } from 'node:fs';
3
4
  import { credentialError, envOf, invokedAs, yellow, } from "./host.js";
@@ -112,6 +113,31 @@ export function wire(store, entry, id) {
112
113
  }
113
114
  return { provider, model: env.COPILOT_PROVIDER_WIRE_MODEL ?? id, env, secret, warnings };
114
115
  }
116
+ /**
117
+ * Through a gateway: one virtual key, and Bifrost's OpenAI or Anthropic drop-in,
118
+ * which routes on an `<upstream>/<id>` wire model. Claude speaks Anthropic
119
+ * wherever it is hosted; everything else goes through the OpenAI one.
120
+ */
121
+ export function wireGateway(at, provider, id) {
122
+ const claude = provider === 'anthropic' || (provider === 'vertex' && vertexClaude(id));
123
+ const model = provider === 'vertex' ? id.replace(/^anthropic\//, '') : id;
124
+ const env = {
125
+ COPILOT_PROVIDER_TYPE: claude ? 'anthropic' : 'openai',
126
+ COPILOT_PROVIDER_BASE_URL: `${at.baseURL}${claude ? '/anthropic' : '/openai/v1'}`,
127
+ COPILOT_PROVIDER_MODEL_ID: model.replace(/^google\//, ''),
128
+ COPILOT_PROVIDER_WIRE_MODEL: `${GATEWAY_UPSTREAMS[provider]}/${model}`,
129
+ };
130
+ const api = wireApi(provider, model);
131
+ if (api) {
132
+ env.COPILOT_PROVIDER_WIRE_API = api;
133
+ }
134
+ const secret = [];
135
+ if (at.apiKey) {
136
+ env.COPILOT_PROVIDER_API_KEY = at.apiKey;
137
+ secret.push('COPILOT_PROVIDER_API_KEY');
138
+ }
139
+ return { provider, model: env.COPILOT_PROVIDER_WIRE_MODEL, env, secret, warnings: [] };
140
+ }
115
141
  /** Values a `--dry-run` may print. A key is four characters and an apology. */
116
142
  export function masked(env, secret) {
117
143
  return Object.entries(env)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zenera/cli",
3
- "version": "1.1.30",
3
+ "version": "1.1.32",
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.30",
58
- "@zenera/view": "^1.1.30",
57
+ "@zenera/neo": "^1.1.32",
58
+ "@zenera/view": "^1.1.32",
59
59
  "google-auth-library": "^10.0.0",
60
60
  "ink": "^7.1.1",
61
61
  "openai": "^6.0.0",