@zenera/cli 1.1.31 → 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.
@@ -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.31",
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.31",
58
- "@zenera/view": "^1.1.31",
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",