@zenera/cli 1.1.36 → 1.1.39
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 +158 -900
- package/dist/archive.js +1 -1
- package/dist/batch.js +2 -3
- package/dist/cache.js +1 -1
- package/dist/candidates.js +1 -1
- package/dist/catalog.d.ts +1 -1
- package/dist/catalog.js +1 -1
- package/dist/commands/cache.js +1 -1
- package/dist/commands/check.js +1 -3
- package/dist/commands/export.js +1 -3
- package/dist/commands/import.js +1 -2
- package/dist/commands/index.js +6 -7
- package/dist/commands/init.js +1 -4
- package/dist/commands/inspect.d.ts +1 -1
- package/dist/commands/inspect.js +1 -4
- package/dist/commands/key.js +1 -2
- package/dist/commands/list.js +4 -5
- package/dist/commands/memory.js +1 -2
- package/dist/commands/meta.js +2 -4
- package/dist/commands/models.d.ts +1 -1
- package/dist/commands/models.js +1 -2
- package/dist/commands/run.js +2 -2
- package/dist/commands/sandbox.js +6 -7
- package/dist/engine.d.ts +3 -90
- package/dist/engine.js +16 -302
- package/dist/external.js +1 -1
- package/dist/history.js +1 -1
- package/dist/lib.d.ts +1 -2
- package/dist/lib.js +1 -2
- package/dist/liveness.d.ts +1 -1
- package/dist/liveness.js +1 -1
- package/dist/meta/host.d.ts +1 -2
- package/dist/meta/host.js +1 -2
- package/dist/modelref.d.ts +1 -1
- package/dist/podman.d.ts +1 -1
- package/dist/podman.js +4 -4
- package/dist/resolve.d.ts +2 -2
- package/dist/resolve.js +2 -3
- package/dist/resources.d.ts +2 -2
- package/dist/resources.js +6 -6
- package/dist/sandbox.d.ts +2 -60
- package/dist/sandbox.js +1 -200
- package/dist/term.d.ts +1 -22
- package/dist/term.js +3 -26
- package/dist/tui/app.js +1 -1
- package/dist/tui/models.d.ts +1 -1
- package/dist/validate.d.ts +1 -2
- package/dist/validate.js +1 -3
- package/package.json +7 -6
- package/templates/editor/.github/skills/zen-cli/SKILL.md +3 -3
- package/templates/editor/.github/skills/zen-cli/references/frame.md +2 -2
- package/templates/editor/.github/skills/zen-cli/references/{faker.md → gym.md} +45 -40
- package/templates/editor/.github/skills/zen-cli/references/keys.md +2 -2
- package/templates/editor/.github/skills/zen-cli/references/models.md +1 -1
- package/templates/editor/.github/skills/zen-cli/references/rag.md +1 -1
- package/templates/editor/.vscode/agents.schema.json +1 -1
- package/dist/audit.d.ts +0 -58
- package/dist/audit.js +0 -148
- package/dist/env.d.ts +0 -20
- package/dist/env.js +0 -65
- package/dist/home.d.ts +0 -35
- package/dist/home.js +0 -112
- package/dist/ids.d.ts +0 -12
- package/dist/ids.js +0 -44
- package/dist/image.d.ts +0 -16
- package/dist/image.js +0 -85
- package/dist/keys.d.ts +0 -269
- package/dist/keys.js +0 -624
- package/dist/projects.d.ts +0 -91
- package/dist/projects.js +0 -303
- package/dist/session.d.ts +0 -112
- package/dist/session.js +0 -188
package/dist/sandbox.js
CHANGED
|
@@ -1,202 +1,12 @@
|
|
|
1
|
-
import { DEFAULT_SANDBOX_IMAGE, GATEWAY_URL_ENV, SANDBOX_GROUP, SandboxPool, } from '@zenera/neo';
|
|
2
|
-
import { mkdirSync } from 'node:fs';
|
|
3
|
-
import { basename, join } from 'node:path';
|
|
4
|
-
import { resolveBuild } from "./image.js";
|
|
5
|
-
import { credentials } from "./keys.js";
|
|
6
1
|
import { assertLocks, ensurePodmanReady } from "./podman.js";
|
|
7
|
-
import { warn } from "./term.js";
|
|
8
|
-
// ---------------------------------------------------------------------------
|
|
9
|
-
// The sandbox, as this CLI wires it
|
|
10
|
-
//
|
|
11
|
-
// The library takes resolved values; the config states names and numbers. This
|
|
12
|
-
// is the layer between the two, and it owns the two decisions the library has
|
|
13
|
-
// no business making: which host variables are worth forwarding, and where the
|
|
14
|
-
// container's home directory lives.
|
|
15
|
-
//
|
|
16
|
-
// It lives in the session directory, which is the whole point. A session is
|
|
17
|
-
// already a self-contained thing — copy the directory and its conversation,
|
|
18
|
-
// memory and blobs travel with it — and a `pip install --user` that vanished
|
|
19
|
-
// on close would be the only part of a session that did not. So `$HOME` is a
|
|
20
|
-
// bind mount into `.data/sandbox/home`, and what the agent installs for itself
|
|
21
|
-
// is still there when the session is opened again. Everything *outside* the
|
|
22
|
-
// mounts is throwaway, which is what keeps a stale container from becoming a
|
|
23
|
-
// second, invisible configuration.
|
|
24
|
-
// ---------------------------------------------------------------------------
|
|
25
|
-
/** Where the persistent home lives, inside the container. */
|
|
26
|
-
const HOME = '/home/agent';
|
|
27
|
-
/** Where a credential *file* is mounted, inside the container. */
|
|
28
|
-
const KEY_MOUNT = '/run/zenera/keys';
|
|
29
|
-
/**
|
|
30
|
-
* The credentials the run is using, in the form a container takes.
|
|
31
|
-
*
|
|
32
|
-
* Two shapes, two mechanisms. A secret is forwarded by *name* — podman reads
|
|
33
|
-
* the value from its own environment, so nothing is written into an argv that
|
|
34
|
-
* `ps` will show. A service-account file has to be present as a file, so the
|
|
35
|
-
* one file is bind-mounted read-only and the variable is rewritten to where it
|
|
36
|
-
* landed; mounting the directory it came from would drag in whatever else the
|
|
37
|
-
* user keeps beside it.
|
|
38
|
-
*
|
|
39
|
-
* This is the part of the design that gives something away, and it should be
|
|
40
|
-
* said plainly: the sandbox runs code the model wrote, and a key it can read
|
|
41
|
-
* is a key it can send. `keys: false`, or `network: none`, is the answer for a
|
|
42
|
-
* project where that is not an acceptable trade.
|
|
43
|
-
*/
|
|
44
|
-
function forwarded() {
|
|
45
|
-
const secrets = [];
|
|
46
|
-
const env = {};
|
|
47
|
-
const mounts = [];
|
|
48
|
-
for (const cred of credentials()) {
|
|
49
|
-
if (cred.holds === 'file') {
|
|
50
|
-
const at = `${KEY_MOUNT}/${basename(cred.value)}`;
|
|
51
|
-
mounts.push({ host: cred.value, at, readOnly: true });
|
|
52
|
-
env[cred.env] = at;
|
|
53
|
-
continue;
|
|
54
|
-
}
|
|
55
|
-
secrets.push(cred.env);
|
|
56
|
-
}
|
|
57
|
-
// Not secrets, and useless without one: a service account says which
|
|
58
|
-
// project it belongs to but never which region to call, and a gateway key
|
|
59
|
-
// says nothing of where the gateway is.
|
|
60
|
-
for (const name of ['GOOGLE_CLOUD_PROJECT', 'GOOGLE_CLOUD_LOCATION', GATEWAY_URL_ENV]) {
|
|
61
|
-
const value = process.env[name];
|
|
62
|
-
if (value) {
|
|
63
|
-
env[name] = value;
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
return { secrets, env, mounts };
|
|
67
|
-
}
|
|
68
|
-
export function buildSandbox(opts) {
|
|
69
|
-
// An explicit --image is an answer, so there is nothing left to build.
|
|
70
|
-
const build = opts.image ? undefined : resolveBuild(opts.root, opts.config.sandbox);
|
|
71
|
-
const base = {
|
|
72
|
-
...(opts.config.sandbox ?? {}),
|
|
73
|
-
...((opts.image ?? build?.tag) ? { image: opts.image ?? build?.tag } : {}),
|
|
74
|
-
...(opts.keys === false ? { keys: false } : {}),
|
|
75
|
-
...(opts.persist === false ? { persist: false } : {}),
|
|
76
|
-
};
|
|
77
|
-
const home = join(opts.session.data, 'sandbox', 'home');
|
|
78
|
-
const mounts = [{ host: home, at: HOME }, ...(opts.mounts ?? [])];
|
|
79
|
-
const keys = base.keys === false ? undefined : forwarded();
|
|
80
|
-
if (keys) {
|
|
81
|
-
mounts.push(...keys.mounts);
|
|
82
|
-
}
|
|
83
|
-
// Skills and assets are mounted read-only, and a python script run from a
|
|
84
|
-
// read-only directory fails on writing its own `__pycache__` — a confusing
|
|
85
|
-
// error about a file nobody asked for.
|
|
86
|
-
// `ZENERA_AGENT` tells a `zen` run by `run_command` that no person is there.
|
|
87
|
-
const extra = { HOME, PYTHONDONTWRITEBYTECODE: '1', ZENERA_AGENT: '1', ...(keys?.env ?? {}) };
|
|
88
|
-
const spec = toSpec(base, extra, secretsOf(keys?.secrets, opts.env));
|
|
89
|
-
const agents = {};
|
|
90
|
-
for (const agent of opts.config.agents) {
|
|
91
|
-
if (agent.sandbox) {
|
|
92
|
-
const merged = merge(base, agent.sandbox);
|
|
93
|
-
if (opts.persist === false) {
|
|
94
|
-
merged.persist = false;
|
|
95
|
-
}
|
|
96
|
-
// An agent that opts out gets neither the variables nor the mount,
|
|
97
|
-
// but the mount is on the pool and cannot be taken back per agent —
|
|
98
|
-
// so the variable is what actually decides, and without it the file
|
|
99
|
-
// sitting there is not a credential anything will look for.
|
|
100
|
-
const own = merged.keys === false ? undefined : keys;
|
|
101
|
-
agents[agent.name] = toSpec(merged, { HOME, PYTHONDONTWRITEBYTECODE: '1', ZENERA_AGENT: '1', ...(own?.env ?? {}) }, own && secretsOf(own.secrets, opts.env));
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
const pool = new SandboxPool({
|
|
105
|
-
...spec,
|
|
106
|
-
agents,
|
|
107
|
-
root: opts.workspace,
|
|
108
|
-
key: opts.session.id,
|
|
109
|
-
readOnly: opts.readOnly,
|
|
110
|
-
mounts,
|
|
111
|
-
});
|
|
112
|
-
return { pool, spec, image: spec.image ?? DEFAULT_SANDBOX_IMAGE, build, home, mounts };
|
|
113
|
-
}
|
|
114
|
-
/**
|
|
115
|
-
* An agent's overrides on the project's block. `image` and `build` answer the
|
|
116
|
-
* same question, so naming either one drops the other: a plain spread would
|
|
117
|
-
* leave an agent's `image` sitting next to the project's `build`, and the
|
|
118
|
-
* schema forbids exactly that combination when it is written down.
|
|
119
|
-
*/
|
|
120
|
-
function merge(base, agent) {
|
|
121
|
-
const merged = { ...base, ...agent };
|
|
122
|
-
if (agent.image !== undefined) {
|
|
123
|
-
delete merged.build;
|
|
124
|
-
}
|
|
125
|
-
if (agent.build !== undefined) {
|
|
126
|
-
delete merged.image;
|
|
127
|
-
}
|
|
128
|
-
return merged;
|
|
129
|
-
}
|
|
130
|
-
/**
|
|
131
|
-
* The two sources of forwarded names, as one list: the credentials the keyring
|
|
132
|
-
* selected, and whatever the project's `.env` declares. Deduped, because the
|
|
133
|
-
* same variable named twice is the same `--env` twice.
|
|
134
|
-
*
|
|
135
|
-
* `undefined` in means keys are withheld, and the file goes with them.
|
|
136
|
-
*/
|
|
137
|
-
function secretsOf(secrets, env) {
|
|
138
|
-
if (!secrets) {
|
|
139
|
-
return undefined;
|
|
140
|
-
}
|
|
141
|
-
return [...new Set([...secrets, ...(env ?? [])])];
|
|
142
|
-
}
|
|
143
|
-
/**
|
|
144
|
-
* Config names a variable; this reads it. A name that is not set on this host
|
|
145
|
-
* is simply not forwarded — an empty string in the container is a different
|
|
146
|
-
* thing from an absent one, and tools test for absence.
|
|
147
|
-
*
|
|
148
|
-
* A name that has already been answered with a value drops off the forwarded
|
|
149
|
-
* list: `--env NAME=value` and `--env NAME` are the same variable twice, and
|
|
150
|
-
* the one that carries a value is the one that was resolved on purpose. That
|
|
151
|
-
* matters for `GOOGLE_APPLICATION_CREDENTIALS`, which is rewritten here to
|
|
152
|
-
* where the key file was mounted — a `.env` naming it must not put the host's
|
|
153
|
-
* path back, since that path does not exist inside the container.
|
|
154
|
-
*/
|
|
155
|
-
function toSpec(config, extra, secrets) {
|
|
156
|
-
const env = { ...extra };
|
|
157
|
-
for (const name of config.env ?? []) {
|
|
158
|
-
const value = process.env[name];
|
|
159
|
-
if (value !== undefined && value !== '') {
|
|
160
|
-
env[name] = value;
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
return {
|
|
164
|
-
image: config.image,
|
|
165
|
-
cpus: config.cpus,
|
|
166
|
-
memory: config.memory,
|
|
167
|
-
network: config.network,
|
|
168
|
-
workdir: config.workdir,
|
|
169
|
-
timeout: config.timeout,
|
|
170
|
-
hardening: config.hardening,
|
|
171
|
-
user: config.user,
|
|
172
|
-
persist: config.persist,
|
|
173
|
-
env,
|
|
174
|
-
secrets: secrets?.filter((name) => !(name in env)),
|
|
175
|
-
};
|
|
176
|
-
}
|
|
177
|
-
/**
|
|
178
|
-
* Whether anything in this project can reach a shell.
|
|
179
|
-
*
|
|
180
|
-
* Read off the *resolved* tool lists rather than off the config's selectors,
|
|
181
|
-
* because `sandbox:*`, `*` and a bare name all mean the same thing by the time
|
|
182
|
-
* the loader is done, and only one of those three is greppable.
|
|
183
|
-
*/
|
|
184
|
-
export function usesSandbox(project) {
|
|
185
|
-
return project.agents.some((a) => a.tools.some((t) => t.group === SANDBOX_GROUP));
|
|
186
|
-
}
|
|
187
2
|
/**
|
|
188
3
|
* Images this process has already settled. A batch opens the same project
|
|
189
4
|
* dozens of times at once, and without this each one shells out to podman and
|
|
190
5
|
* the concurrent copies race each other to pull the very same image.
|
|
191
6
|
*/
|
|
192
7
|
const READY = new Set();
|
|
193
|
-
/**
|
|
194
|
-
* Asked before the first turn rather than at the first tool call, so a missing
|
|
195
|
-
* container engine costs nothing instead of costing a round trip and half a
|
|
196
|
-
* plan. A project that never shells out never gets here at all.
|
|
197
|
-
*/
|
|
8
|
+
/** The engine's preflight on a terminal: podman is offered for install and started if needed. */
|
|
198
9
|
export async function preflight(setup, yes) {
|
|
199
|
-
mkdirSync(setup.home, { recursive: true });
|
|
200
10
|
if (READY.has(setup.image)) {
|
|
201
11
|
return;
|
|
202
12
|
}
|
|
@@ -211,13 +21,4 @@ export async function preflight(setup, yes) {
|
|
|
211
21
|
await assertLocks(1, { reuses: setup.pool.for().name });
|
|
212
22
|
READY.add(setup.image);
|
|
213
23
|
}
|
|
214
|
-
/** Best-effort teardown: losing a container must never lose a run. */
|
|
215
|
-
export async function teardown(pool) {
|
|
216
|
-
try {
|
|
217
|
-
await pool.dispose();
|
|
218
|
-
}
|
|
219
|
-
catch (err) {
|
|
220
|
-
warn(`could not clean up the sandbox: ${err instanceof Error ? err.message : String(err)}`);
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
24
|
//# sourceMappingURL=sandbox.js.map
|
package/dist/term.d.ts
CHANGED
|
@@ -1,25 +1,4 @@
|
|
|
1
|
-
export
|
|
2
|
-
readonly ok: 0;
|
|
3
|
-
readonly failed: 1;
|
|
4
|
-
readonly usage: 2;
|
|
5
|
-
readonly invalid: 3;
|
|
6
|
-
readonly credentials: 4;
|
|
7
|
-
readonly sandbox: 5;
|
|
8
|
-
};
|
|
9
|
-
export type ExitCode = (typeof EXIT)[keyof typeof EXIT];
|
|
10
|
-
/**
|
|
11
|
-
* An error that already knows what it means. Anything else reaching the top is
|
|
12
|
-
* a bug and exits `failed`, which is the distinction a script needs: a wrong
|
|
13
|
-
* invocation and a wrong answer are not the same failure.
|
|
14
|
-
*/
|
|
15
|
-
export declare class CliError extends Error {
|
|
16
|
-
readonly code: ExitCode;
|
|
17
|
-
readonly hint?: string;
|
|
18
|
-
constructor(message: string, code?: ExitCode, hint?: string);
|
|
19
|
-
}
|
|
20
|
-
export declare const usageError: (m: string, hint?: string) => CliError;
|
|
21
|
-
export declare const invalidError: (m: string, hint?: string) => CliError;
|
|
22
|
-
export declare const credentialError: (m: string, hint?: string) => CliError;
|
|
1
|
+
export { EngineError as CliError, credentialError, EXIT, invalidError, usageError, type ExitCode, } from '@zenera/engine';
|
|
23
2
|
export type Appearance = 'dark' | 'light';
|
|
24
3
|
/** One grey per background. The only 256-colour indices in the CLI. */
|
|
25
4
|
export declare const CHROME: Record<Appearance, number>;
|
package/dist/term.js
CHANGED
|
@@ -11,32 +11,9 @@ import { styleText } from 'node:util';
|
|
|
11
11
|
// `styleText` no-ops when the stream is not a tty and honours NO_COLOR itself,
|
|
12
12
|
// so there is no flag to thread through and no piped output to garble.
|
|
13
13
|
// ---------------------------------------------------------------------------
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
usage: 2,
|
|
18
|
-
invalid: 3,
|
|
19
|
-
credentials: 4,
|
|
20
|
-
sandbox: 5,
|
|
21
|
-
};
|
|
22
|
-
/**
|
|
23
|
-
* An error that already knows what it means. Anything else reaching the top is
|
|
24
|
-
* a bug and exits `failed`, which is the distinction a script needs: a wrong
|
|
25
|
-
* invocation and a wrong answer are not the same failure.
|
|
26
|
-
*/
|
|
27
|
-
export class CliError extends Error {
|
|
28
|
-
code;
|
|
29
|
-
hint;
|
|
30
|
-
constructor(message, code = EXIT.failed, hint) {
|
|
31
|
-
super(message);
|
|
32
|
-
this.name = 'CliError';
|
|
33
|
-
this.code = code;
|
|
34
|
-
this.hint = hint;
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
export const usageError = (m, hint) => new CliError(m, EXIT.usage, hint);
|
|
38
|
-
export const invalidError = (m, hint) => new CliError(m, EXIT.invalid, hint);
|
|
39
|
-
export const credentialError = (m, hint) => new CliError(m, EXIT.credentials, hint);
|
|
14
|
+
// The engine raises these too, so `instanceof CliError` holds for its errors.
|
|
15
|
+
import { usageError } from '@zenera/engine';
|
|
16
|
+
export { EngineError as CliError, credentialError, EXIT, invalidError, usageError, } from '@zenera/engine';
|
|
40
17
|
/** One grey per background. The only 256-colour indices in the CLI. */
|
|
41
18
|
export const CHROME = { dark: 245, light: 242 };
|
|
42
19
|
let sniffed;
|
package/dist/tui/app.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import { TextInput } from '@inkjs/ui';
|
|
3
|
+
import { display } from '@zenera/engine';
|
|
3
4
|
import { addUsage, isCheckpoint, turns, zeroUsage, } from '@zenera/neo';
|
|
4
5
|
import { Box, Static, Text, useApp, useInput, useStdout } from 'ink';
|
|
5
6
|
import { pathToFileURL } from 'node:url';
|
|
@@ -7,7 +8,6 @@ import React, { useCallback, useContext, useEffect, useRef, useState } from 'rea
|
|
|
7
8
|
import * as Engine from "../engine.js";
|
|
8
9
|
import { History } from "../history.js";
|
|
9
10
|
import { format } from "../narrate.js";
|
|
10
|
-
import { display } from "../session.js";
|
|
11
11
|
import { CliError } from "../term.js";
|
|
12
12
|
import { hostUrls, hyperlink, hyperlinks, linkBlock, mountsOf } from "./links.js";
|
|
13
13
|
import { resolveTheme, THEMES } from "./theme.js";
|
package/dist/tui/models.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
+
import type { Provider } from '@zenera/engine';
|
|
1
2
|
import { type Balance, type Catalog, type CatalogEntry, type Role } from '../catalog.ts';
|
|
2
|
-
import type { Provider } from '../keys.ts';
|
|
3
3
|
export interface ProviderRow {
|
|
4
4
|
provider: Provider;
|
|
5
5
|
/** where its credential came from — `environment`, `keyring` — or '' for none */
|
package/dist/validate.d.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
|
+
import { type DeclaredRole, type KeyStore, type Liveness } from '@zenera/engine';
|
|
1
2
|
import { type AnyTool, type ProjectConfig, type Runner } from '@zenera/neo';
|
|
2
|
-
import { type DeclaredRole } from './audit.ts';
|
|
3
|
-
import { type KeyStore, type Liveness } from './keys.ts';
|
|
4
3
|
/**
|
|
5
4
|
* `error` — the project will not load, or will not run.
|
|
6
5
|
* `warning` — it loads, and something about it is probably not what was meant.
|
package/dist/validate.js
CHANGED
|
@@ -1,10 +1,8 @@
|
|
|
1
|
+
import { SHAPES, auditModels, credentialFor, envNames, form, resolveBuild, } from '@zenera/engine';
|
|
1
2
|
import { EXA_GROUP, FileSkillProvider, SANDBOX_MOUNT, Sandbox, exaTools, fileTools, frontmatter, projectRegistry, readProjectConfig, sandboxTools, selectTools, toList, } from '@zenera/neo';
|
|
2
3
|
import { existsSync, mkdtempSync, readFileSync, readdirSync, realpathSync, rmSync, statSync, } from 'node:fs';
|
|
3
4
|
import { tmpdir } from 'node:os';
|
|
4
5
|
import { basename, dirname, isAbsolute, join, relative, resolve } from 'node:path';
|
|
5
|
-
import { auditModels, credentialFor } from "./audit.js";
|
|
6
|
-
import { resolveBuild } from "./image.js";
|
|
7
|
-
import { SHAPES, envNames, form } from "./keys.js";
|
|
8
6
|
import { probeModels } from "./liveness.js";
|
|
9
7
|
import { BuildError, ensurePodmanReady } from "./podman.js";
|
|
10
8
|
import { ownVersion, staleShared } from "./scaffold.js";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zenera/cli",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.39",
|
|
4
4
|
"description": "Command-line front end for @zenera/neo: agentic projects you can run, share and commit.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"agents",
|
|
@@ -15,13 +15,13 @@
|
|
|
15
15
|
"access": "public"
|
|
16
16
|
},
|
|
17
17
|
"author": "Andrey Ryabov",
|
|
18
|
-
"homepage": "https://github.com/
|
|
18
|
+
"homepage": "https://github.com/zenera-ai/ZeneraNeo#readme",
|
|
19
19
|
"repository": {
|
|
20
20
|
"type": "git",
|
|
21
|
-
"url": "git+https://github.com/
|
|
21
|
+
"url": "git+https://github.com/zenera-ai/ZeneraNeo.git",
|
|
22
22
|
"directory": "packages/cli"
|
|
23
23
|
},
|
|
24
|
-
"bugs": "https://github.com/
|
|
24
|
+
"bugs": "https://github.com/zenera-ai/ZeneraNeo/issues",
|
|
25
25
|
"type": "module",
|
|
26
26
|
"bin": {
|
|
27
27
|
"zen": "./dist/main.js",
|
|
@@ -54,8 +54,9 @@
|
|
|
54
54
|
"@google/genai": "^2.30.0",
|
|
55
55
|
"@inkjs/ui": "^2.0.0",
|
|
56
56
|
"@openrouter/sdk": "^1.4.25",
|
|
57
|
-
"@zenera/
|
|
58
|
-
"@zenera/
|
|
57
|
+
"@zenera/engine": "^1.1.39",
|
|
58
|
+
"@zenera/neo": "^1.1.39",
|
|
59
|
+
"@zenera/view": "^1.1.39",
|
|
59
60
|
"google-auth-library": "^10.0.0",
|
|
60
61
|
"ink": "^7.1.1",
|
|
61
62
|
"openai": "^6.0.0",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: zen-cli
|
|
3
|
-
description: How to drive this project from the terminal with `zen` - running it, validating it, credentials, the sandbox, run reports, and the `
|
|
3
|
+
description: How to drive this project from the terminal with `zen` - running it, validating it, credentials, the sandbox, run reports, and the `gym` and `rag` modules.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# The `zen` command line
|
|
@@ -33,7 +33,7 @@ zen version
|
|
|
33
33
|
|
|
34
34
|
zen meta <run|prompts|resume|model|dataset> [project] [args]
|
|
35
35
|
|
|
36
|
-
zen
|
|
36
|
+
zen gym <serve|build|cache|model> [spec...]
|
|
37
37
|
zen rag schema <index|restore|ready|search|list|grep|trace|show|stats> [spec...]
|
|
38
38
|
zen rag docs <index|restore|ready|search|list|grep|show|stats> [path...]
|
|
39
39
|
```
|
|
@@ -61,7 +61,7 @@ guess a flag, and do not read them all.
|
|
|
61
61
|
| The evaluation dataset: cases, drift, notes, drawing a sample | [dataset.md](./references/dataset.md) (and `zen-dataset` skill) |
|
|
62
62
|
| Tuning the prose on the dataset: workers, applies, STATUS.md | [finetune.md](./references/finetune.md) |
|
|
63
63
|
| Sharing a project: archives, what travels, importing one safely | [export.md](./references/export.md) |
|
|
64
|
-
| `zen
|
|
64
|
+
| `zen gym` - a mock API from an OpenAPI/Swagger document | [gym.md](./references/gym.md) |
|
|
65
65
|
| `zen rag` - searching an API description, or a pile of markdown | [rag.md](./references/rag.md) |
|
|
66
66
|
|
|
67
67
|
## The short version
|
|
@@ -37,7 +37,7 @@ Not listed in help, but they work.
|
|
|
37
37
|
| `validate`, `doctor` | `check` |
|
|
38
38
|
| `report` | `inspect` |
|
|
39
39
|
| `edit`, `code` | `open` |
|
|
40
|
-
| `mock` | `
|
|
40
|
+
| `mock` | `gym` |
|
|
41
41
|
|
|
42
42
|
## stdout is the answer, stderr is the narration
|
|
43
43
|
|
|
@@ -103,7 +103,7 @@ A real environment variable always beats the keyring.
|
|
|
103
103
|
projects.json the registry: name -> directory
|
|
104
104
|
keys.json credentials (0600)
|
|
105
105
|
keys/ key files, for providers that hold a file
|
|
106
|
-
|
|
106
|
+
gym/ generator cache for `zen gym`
|
|
107
107
|
|
|
108
108
|
<project>/
|
|
109
109
|
zenera.json { version, name }
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Agent API environments - `zen gym`
|
|
2
2
|
|
|
3
3
|
```
|
|
4
|
-
zen
|
|
4
|
+
zen gym <serve|build|cache|model> [spec...]
|
|
5
5
|
```
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
`zen
|
|
7
|
+
Provided by `@zenera/gym` - `npm i -g @zenera/gym` if
|
|
8
|
+
`zen gym` says it is not installed.
|
|
9
9
|
|
|
10
10
|
Serves a mock API from one or more OpenAPI/Swagger documents. For each
|
|
11
11
|
operation a **model writes a Python generator**, which is self-tested inside a
|
|
@@ -14,17 +14,18 @@ plausible, not `"string"` repeated.
|
|
|
14
14
|
|
|
15
15
|
## Subcommands
|
|
16
16
|
|
|
17
|
-
| Command | What it does
|
|
18
|
-
| ----------------- |
|
|
19
|
-
| `serve <spec...>` | Serve the documents. Generators are written on demand
|
|
20
|
-
| `build <spec...>` | Write every generator now and exit
|
|
21
|
-
| `cache ls\|clear` | What has been generated, or throw it away
|
|
17
|
+
| Command | What it does |
|
|
18
|
+
| ----------------- | ---------------------------------------------------------- |
|
|
19
|
+
| `serve <spec...>` | Serve the documents. Generators are written on demand |
|
|
20
|
+
| `build <spec...>` | Write every generator now and exit |
|
|
21
|
+
| `cache ls\|clear` | What has been generated, or throw it away |
|
|
22
|
+
| `model [ref]` | Show or set the model; `--clear` forgets the stored choice |
|
|
22
23
|
|
|
23
24
|
```
|
|
24
|
-
zen
|
|
25
|
-
zen
|
|
26
|
-
zen
|
|
27
|
-
zen
|
|
25
|
+
zen gym serve openapi.yaml
|
|
26
|
+
zen gym serve api/*.yaml --port 9000 --seed 7
|
|
27
|
+
zen gym build openapi.yaml --concurrency 8
|
|
28
|
+
zen gym cache clear
|
|
28
29
|
```
|
|
29
30
|
|
|
30
31
|
`build` is the one to run in CI or before a demo: it pays for every generator up
|
|
@@ -32,23 +33,25 @@ front and exits non-zero, with a table, if any could not be written.
|
|
|
32
33
|
|
|
33
34
|
## Options
|
|
34
35
|
|
|
35
|
-
| Flag | Default
|
|
36
|
-
| ------------------- |
|
|
37
|
-
| `--port <n>` | `8787`
|
|
38
|
-
| `--host <h>` | `127.0.0.1`
|
|
39
|
-
| `--model <ref>` | project default
|
|
40
|
-
| `--image <ref>` | a baked image
|
|
41
|
-
| `--cache <dir>` | `~/.zenera/neo/
|
|
42
|
-
| `--seed <n>` | -
|
|
43
|
-
| `--attempts <n>` | `3`
|
|
44
|
-
| `--concurrency <n>` | `4`
|
|
45
|
-
| `--timeout <s>` | `30`
|
|
46
|
-
| `--max-body <n>` | 1 MB
|
|
47
|
-
| `--rebuild` | -
|
|
48
|
-
| `--no-cache` | -
|
|
49
|
-
| `--quiet` | -
|
|
36
|
+
| Flag | Default | Meaning |
|
|
37
|
+
| ------------------- | ------------------- | ----------------------------------------------- |
|
|
38
|
+
| `--port <n>` | `8787` | Port to listen on |
|
|
39
|
+
| `--host <h>` | `127.0.0.1` | Anything else is reachable off-machine |
|
|
40
|
+
| `--model <ref>` | project default | Which model writes the generators |
|
|
41
|
+
| `--image <ref>` | a baked image | Skip the baked image and use this one |
|
|
42
|
+
| `--cache <dir>` | `~/.zenera/neo/gym` | Where generators live |
|
|
43
|
+
| `--seed <n>` | - | Answer the same request the same way every time |
|
|
44
|
+
| `--attempts <n>` | `3` | Tries per generator before giving up |
|
|
45
|
+
| `--concurrency <n>` | `4` | Generators written at once |
|
|
46
|
+
| `--timeout <s>` | `30` | Seconds one generator may take |
|
|
47
|
+
| `--max-body <n>` | 1 MB | Largest request body accepted, in bytes |
|
|
48
|
+
| `--rebuild` | - | Ignore what is cached and write it again |
|
|
49
|
+
| `--no-cache` | - | Do not record what is written |
|
|
50
|
+
| `--quiet` | - | No narration |
|
|
50
51
|
|
|
51
52
|
Credentials come from the `zen` keyring - see [keys.md](keys.md).
|
|
53
|
+
Model selection: `--model`, `ZENERA_GYM_MODEL`, `~/.zenera/neo/gym.json`,
|
|
54
|
+
then the best model the available keys support.
|
|
52
55
|
|
|
53
56
|
## Serving
|
|
54
57
|
|
|
@@ -56,17 +59,19 @@ Binds the loopback address by default. On start it prints a table per document:
|
|
|
56
59
|
paths, methods, and how many operations have a response schema and therefore get
|
|
57
60
|
a generator.
|
|
58
61
|
|
|
59
|
-
| Endpoint
|
|
60
|
-
|
|
|
61
|
-
| `/
|
|
62
|
-
| `/
|
|
62
|
+
| Endpoint | Answers |
|
|
63
|
+
| --------------- | ------------------------- |
|
|
64
|
+
| `/__gym/routes` | Every route it will serve |
|
|
65
|
+
| `/__gym/health` | Whether it is up |
|
|
63
66
|
|
|
64
67
|
Response headers:
|
|
65
68
|
|
|
66
|
-
| Header
|
|
67
|
-
|
|
|
68
|
-
| `x-
|
|
69
|
-
| `x-
|
|
69
|
+
| Header | Meaning |
|
|
70
|
+
| ----------------- | ----------------------------------------------------- |
|
|
71
|
+
| `x-gym-operation` | The `operationId` that answered |
|
|
72
|
+
| `x-gym-cache` | `hit` or `miss` - whether this call cost a model turn |
|
|
73
|
+
|
|
74
|
+
Gym provides simulation, not task scoring, rewards, or a training loop.
|
|
70
75
|
|
|
71
76
|
Incoming headers are filtered: `authorization`, `cookie` and anything matching
|
|
72
77
|
`key|token|secret|password|credential` never reach a generator.
|
|
@@ -91,7 +96,7 @@ Probes are synthetic on purpose - real request bodies never reach a prompt.
|
|
|
91
96
|
|
|
92
97
|
## The cache
|
|
93
98
|
|
|
94
|
-
Generators live in the machine's shared cache, `~/.zenera/neo/cache/
|
|
99
|
+
Generators live in the machine's shared cache, `~/.zenera/neo/cache/gym-generator/`,
|
|
95
100
|
keyed by the operation's shape - so the same spec served from two directories is
|
|
96
101
|
written once, and a spec edit produces a new key rather than a stale answer. A
|
|
97
102
|
generator that a model gave up on is remembered for the life of the process, so
|
|
@@ -99,17 +104,17 @@ a hopeless operation is not re-asked on every request; a _transient_ failure - a
|
|
|
99
104
|
429, a dropped socket - is not, because it is about this minute rather than this
|
|
100
105
|
operation.
|
|
101
106
|
|
|
102
|
-
`~/.zenera/neo/
|
|
107
|
+
`~/.zenera/neo/gym/` is the container's workspace, and scratch: a cache hit is
|
|
103
108
|
copied into it, because the container can only run what is under its mount.
|
|
104
109
|
|
|
105
|
-
`zen
|
|
110
|
+
`zen gym cache ls` lists what has been generated. `zen gym cache clear`
|
|
106
111
|
removes the generators, the workspace and the container together. They have to
|
|
107
112
|
go together: the container's name is a hash of its configuration, so deleting
|
|
108
113
|
the workspace alone would leave a stopped container bind-mounted onto a
|
|
109
114
|
directory that no longer exists, and every generator would fail with
|
|
110
115
|
`python3: can't open file '/workspace/generators/…/gen.py'`.
|
|
111
116
|
|
|
112
|
-
`zen cache ls --kind
|
|
117
|
+
`zen cache ls --kind gym-generator` reaches the same entries from the other
|
|
113
118
|
side, along with everything else this machine has cached.
|
|
114
119
|
|
|
115
120
|
## Documents it accepts
|
|
@@ -77,7 +77,7 @@ turns gateway mode on without the keyring - and `zen key mode`/`ls`/`check`
|
|
|
77
77
|
report and probe it (`bifrost/$BIFROST_API_KEY`), with nothing stored. In this
|
|
78
78
|
mode `zen models` lists only
|
|
79
79
|
what the gateway serves - see `models.md`. The full guide is
|
|
80
|
-
[docs/gateway.md](https://github.com/
|
|
80
|
+
[docs/gateway.md](https://github.com/zenera-ai/ZeneraNeo/blob/main/docs/gateway.md).
|
|
81
81
|
|
|
82
82
|
### Vertex takes either shape
|
|
83
83
|
|
|
@@ -141,6 +141,6 @@ The frame does **not** materialise the keyring for you - each command that needs
|
|
|
141
141
|
a credential opens the store itself. If `zen key ls` shows a key as live and a
|
|
142
142
|
command still says `provider "openai": no api key - set OPENAI_API_KEY`, the
|
|
143
143
|
credential is fine and the command is at fault. Everything that runs a model
|
|
144
|
-
(`run`, `check`, `models`, `
|
|
144
|
+
(`run`, `check`, `models`, `gym`, `rag`) already does this.
|
|
145
145
|
|
|
146
146
|
Exit code `4` means no usable credential for what was asked.
|
|
@@ -58,7 +58,7 @@ stderr with a `⇄ bifrost mode` line, `browse` carries the same badge on every
|
|
|
58
58
|
screen, and `--json` adds a `gateway` field. `zen models` then does go to the
|
|
59
59
|
network, since one request covers every provider; `test` asks through the
|
|
60
60
|
gateway too. The full guide is
|
|
61
|
-
[docs/gateway.md](https://github.com/
|
|
61
|
+
[docs/gateway.md](https://github.com/zenera-ai/ZeneraNeo/blob/main/docs/gateway.md).
|
|
62
62
|
|
|
63
63
|
## Testing a model
|
|
64
64
|
|
|
@@ -34,7 +34,7 @@ zen rag schema index <spec...> [--embedding <ref>] [-o <dir>] [--batch <n>]
|
|
|
34
34
|
zen rag schema index openapi.yaml --embedding openai:text-embedding-3-small
|
|
35
35
|
```
|
|
36
36
|
|
|
37
|
-
Unlike the
|
|
37
|
+
Unlike the gym, `$ref`s are **bundled, not dereferenced**: component names are
|
|
38
38
|
the node ids and the cycles between them are the edges. `discriminator` is kept -
|
|
39
39
|
it is what turns a `oneOf` into a tagged union a compiler can narrow.
|
|
40
40
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
|
-
"$id": "https://raw.githubusercontent.com/
|
|
3
|
+
"$id": "https://raw.githubusercontent.com/zenera-ai/ZeneraNeo/main/packages/cli/templates/editor/.vscode/agents.schema.json",
|
|
4
4
|
"title": "Zenera Neo agents.yaml",
|
|
5
5
|
"description": "Who exists in this project, what they may reach for, and which model answers for them. Written by `zen init` and replaced by every `zen init` and `zen open`, so it describes the version of `zen` in hand. The runtime is the authority: `zen check` reads the project the way a run does.",
|
|
6
6
|
"type": "object",
|