@zenera/cli 1.1.42 → 1.1.43

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
@@ -115,7 +115,7 @@ in line with it, validate the setup, and run it:
115
115
  ```sh
116
116
  zen meta run my-project /spec-sync-project
117
117
  zen check my-project
118
- zen run my-project "Summarize this repository and write NOTES.md"
118
+ zen run my-project --here "Summarize this repository and write NOTES.md"
119
119
  zen inspect
120
120
  ```
121
121
 
@@ -127,13 +127,15 @@ than silently guessing. After you answer, apply that feedback with
127
127
  ## Run it your way
128
128
 
129
129
  With no prompt, `zen run` opens the terminal UI. Provide a prompt for a
130
- one-shot run, or use JSON and batch mode to integrate with other tools:
130
+ one-shot run, or use JSON and batch mode to integrate with other tools. A
131
+ one-shot run works in a fresh directory inside its session; `--here` points it
132
+ at the directory you are in instead:
131
133
 
132
134
  ```sh
133
135
  zen run my-project # interactive terminal UI
134
- zen run my-project "Review the latest changes"
136
+ zen run my-project --here "Review the latest changes"
135
137
  git diff | zen run my-project
136
- zen run my-project "Summarize the changes" --read-only
138
+ zen run my-project --here "Summarize the changes" --read-only
137
139
  zen run my-project "Triage these alerts" --json | jq
138
140
  zen run batch --input cases.json # run a set of requests
139
141
  ```
@@ -6,7 +6,7 @@ import { duration } from "../narrate.js";
6
6
  import { project as resolveProject } from "../resolve.js";
7
7
  import { refreshShared } from "../scaffold.js";
8
8
  import { bold, count, cyan, dim, green, invalidError, json, progress, red, table, usageError, write, writeAll, yellow, } from "../term.js";
9
- import { validateProject, } from "../validate.js";
9
+ import { repinDockerfile, validateProject, } from "../validate.js";
10
10
  const USAGE = 'zen check [name|dir] [--project <name|dir>] [--fix] [--no-sandbox] [--no-models] [--strict] [--quiet]';
11
11
  // ---------------------------------------------------------------------------
12
12
  // zen check
@@ -57,8 +57,10 @@ export const check = {
57
57
  '(.vscode/ and .github/). They restate how this version of zen behaves,',
58
58
  'so a copy left behind by an upgrade is worse than none — they are',
59
59
  'replaced whether or not they were edited, and the report that follows is',
60
- 'of the project as repaired. Nothing else is touched: agents.yaml, the',
61
- 'prompts, the specification and agents/instructions.md are yours.',
60
+ 'of the project as repaired. It also pins the @zenera/* installs in the',
61
+ 'sandbox Dockerfile to this zen\u2019s version, changing nothing else in it.',
62
+ 'Nothing else is touched: agents.yaml, the prompts, the specification and',
63
+ 'agents/instructions.md are yours.',
62
64
  '',
63
65
  'The argument is a directory if one is there and a registered project',
64
66
  'name otherwise; with neither, the project you are standing in.',
@@ -87,7 +89,7 @@ export const check = {
87
89
  // the files that are there now and the exit code answers the repaired
88
90
  // project. A fix that left an error behind in the output would be read
89
91
  // as a fix that failed.
90
- const fixed = values.fix ? refreshShared(dir) : [];
92
+ const fixed = values.fix ? [...refreshShared(dir), ...repinDockerfile(dir)] : [];
91
93
  // Materialised first, so the credential verdicts are the ones a run
92
94
  // would reach: a key in the environment and a key in the keyring are
93
95
  // the same key by the time the library asks. The project's own `.env`
@@ -329,7 +331,7 @@ function fixedLines(fixed) {
329
331
  return [];
330
332
  }
331
333
  return [
332
- `${bold('Replaced')} ${dim(`${count(fixed.length, 'file')} \u2014 ours, not the project's`)}`,
334
+ `${bold('Fixed')} ${dim(count(fixed.length, 'file'))}`,
333
335
  ...fixed.map((f) => ` ${green('wrote')} ${f}`),
334
336
  '',
335
337
  ];
@@ -33,6 +33,8 @@ export const run = {
33
33
  ' --new Start a new session without asking which one.',
34
34
  ' --input <file> Read the whole request from JSON. `-` is stdin.',
35
35
  ' --workspace <dir> Directory the agent can read and write.',
36
+ ' Default: a fresh one inside the new session.',
37
+ ' --here Same as --workspace with the directory you are in.',
36
38
  ' --memory <dir> Directory the agents remember into.',
37
39
  ' --memory-read-only Recall from it, write nothing back.',
38
40
  ' --model <ref> Use this model instead of the default.',
@@ -59,7 +61,9 @@ export const run = {
59
61
  'the answer alone and `2>/dev/null` hides the progress.',
60
62
  '',
61
63
  'A prompt always starts a new session, so --new is for the TUI, where the',
62
- 'alternative is being asked which session to continue.',
64
+ 'alternative is being asked which session to continue. Its workspace is a',
65
+ 'fresh, empty directory inside that session; to ask about the files where',
66
+ 'you are, say --here.',
63
67
  '',
64
68
  '--input names a file holding the whole request - project, input, workspace,',
65
69
  'memory - and what it says wins over the same flag on the line. Its paths are',
@@ -98,6 +102,7 @@ export const run = {
98
102
  ' zen run open the TUI in this project',
99
103
  ' zen run acme open the TUI in the acme project',
100
104
  ' zen run "what changed?" ask that, print the answer, exit',
105
+ ' zen run --here "what changed?" ... about the files in this directory',
101
106
  ' git diff | zen run take the prompt from stdin',
102
107
  ' zen run "what changed?" > out.md redirect the answer into a file',
103
108
  ' zen run --out out.md "what changed?" write the answer to out.md only',
@@ -118,6 +123,7 @@ export const run = {
118
123
  new: { type: 'boolean' },
119
124
  input: { type: 'string' },
120
125
  workspace: { type: 'string' },
126
+ here: { type: 'boolean' },
121
127
  memory: { type: 'string' },
122
128
  'memory-read-only': { type: 'boolean' },
123
129
  model: { type: 'string' },
@@ -155,16 +161,20 @@ export const run = {
155
161
  if (values.theme !== undefined && !parseChoice(values.theme)) {
156
162
  throw usageError(`unknown theme: ${values.theme}`, 'dark, light or auto');
157
163
  }
164
+ if (values.here && values.workspace !== undefined) {
165
+ throw usageError('--here and --workspace both name the workspace', 'keep one');
166
+ }
158
167
  // With `--input -` the pipe carries the request, not the prompt, so it
159
168
  // is read here and never looked at again below.
160
169
  const request = values.input ? await readRequest(values.input, ctx.cwd, piped) : undefined;
161
170
  const prompt = typed || (request ? '' : (piped ?? ''));
162
171
  // A prompt on the command line is a request for an answer, not a
163
172
  // conversation to pick up. So it answers the three questions itself:
164
- // a fresh session, the directory it was typed in as the workspace,
165
- // and no confirmation for it — `zen run acme "what changed?"` should
166
- // read the code that is right there. Every flag still wins, and the
167
- // TUI, where there is someone to ask, still asks.
173
+ // a fresh session, a fresh directory inside it as the workspace, and
174
+ // no confirmation for any of it. Reading the files where it was typed
175
+ // takes --here: an agent that can write should not land in whatever
176
+ // directory the shell happened to be in. Every flag still wins, and
177
+ // the TUI, where there is someone to ask, still asks.
168
178
  const input = request ? request.input : prompt;
169
179
  const shot = Boolean(prompt) || Boolean(request);
170
180
  if (values.events && !shot) {
@@ -175,7 +185,8 @@ export const run = {
175
185
  project: request?.project ?? values.project ?? (named ? head : undefined),
176
186
  session: values.session,
177
187
  fresh: values.new || (shot && !values.session),
178
- workspace: request?.workspace ?? values.workspace ?? (shot ? ctx.cwd : undefined),
188
+ workspace: request?.workspace ?? values.workspace ?? (values.here ? ctx.cwd : undefined),
189
+ ask: !shot,
179
190
  yes: values.yes || shot,
180
191
  });
181
192
  const engine = await Engine.open({
@@ -234,6 +245,7 @@ const NOT_IN_A_BATCH = [
234
245
  ['session', 'every item is a new session of its own'],
235
246
  ['new', 'every item is new already'],
236
247
  ['workspace', 'they cannot share one; each item gets its own, or names one in the file'],
248
+ ['here', 'they cannot share one; each item gets its own, or names one in the file'],
237
249
  ['plain', 'a batch never draws the TUI'],
238
250
  ['theme', 'a batch never draws the TUI'],
239
251
  ['events', 'each item already has its own output.json'],
package/dist/resolve.d.ts CHANGED
@@ -5,6 +5,8 @@ export interface Wanted {
5
5
  session?: string;
6
6
  fresh?: boolean;
7
7
  workspace?: string;
8
+ /** false to take the session's own workspace without offering a choice */
9
+ ask?: boolean;
8
10
  yes?: boolean;
9
11
  cwd: string;
10
12
  }
package/dist/resolve.js CHANGED
@@ -137,7 +137,7 @@ export async function chooseWorkspace(session, want) {
137
137
  await approve(at, session, want);
138
138
  return at;
139
139
  }
140
- if (!term.isInteractive()) {
140
+ if (want.ask === false || !term.isInteractive()) {
141
141
  return own;
142
142
  }
143
143
  const chosen = await term.choose('Workspace — what the agent can read and write', [
@@ -176,4 +176,15 @@ export interface ValidateOptions {
176
176
  }
177
177
  export declare function validateProject(opts: ValidateOptions): Promise<Report>;
178
178
  export declare function availableTools(root: string, config: ProjectConfig): AnyTool<unknown>[];
179
+ /**
180
+ * Pins every `@zenera/*` install in the project's Dockerfile to this `zen`,
181
+ * which is what `zen check --fix` does about `sandbox.pin.stale` and
182
+ * `sandbox.pin.absent`. Only the version after each name is touched, and only
183
+ * on lines `checkPin` reads, so whatever else the file holds is left alone.
184
+ *
185
+ * Returns the project-relative path when the file was rewritten, and nothing
186
+ * when there is no Dockerfile, it resolves outside the project, or it already
187
+ * pins this version — a project that does not load is the report's to explain.
188
+ */
189
+ export declare function repinDockerfile(root: string): string[];
179
190
  //# sourceMappingURL=validate.d.ts.map
package/dist/validate.js CHANGED
@@ -1,8 +1,8 @@
1
1
  import { SHAPES, auditModels, credentialFor, envNames, form, resolveBuild, } from '@zenera/engine';
2
2
  import { EXA_GROUP, FileSkillProvider, SANDBOX_MOUNT, Sandbox, exaTools, fileTools, frontmatter, projectRegistry, readProjectConfig, sandboxTools, selectTools, toList, } from '@zenera/neo';
3
- import { existsSync, mkdtempSync, readFileSync, readdirSync, realpathSync, rmSync, statSync, } from 'node:fs';
3
+ import { existsSync, mkdtempSync, readFileSync, readdirSync, realpathSync, rmSync, statSync, writeFileSync, } from 'node:fs';
4
4
  import { tmpdir } from 'node:os';
5
- import { basename, dirname, isAbsolute, join, relative, resolve } from 'node:path';
5
+ import { basename, dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
6
6
  import { probeModels } from "./liveness.js";
7
7
  import { BuildError, ensurePodmanReady } from "./podman.js";
8
8
  import { ownVersion, staleShared } from "./scaffold.js";
@@ -1315,7 +1315,7 @@ function checkPin(path, rel, add) {
1315
1315
  }
1316
1316
  }
1317
1317
  }
1318
- const fix = `pin them to ${mine}, the version \`zen --version\` reports`;
1318
+ const fix = `pin them to ${mine}, the version \`zen --version\` reports — \`zen check --fix\` does it`;
1319
1319
  if (stale.size > 0) {
1320
1320
  add({
1321
1321
  severity: 'warning',
@@ -1337,6 +1337,49 @@ function checkPin(path, rel, add) {
1337
1337
  });
1338
1338
  }
1339
1339
  }
1340
+ /**
1341
+ * Pins every `@zenera/*` install in the project's Dockerfile to this `zen`,
1342
+ * which is what `zen check --fix` does about `sandbox.pin.stale` and
1343
+ * `sandbox.pin.absent`. Only the version after each name is touched, and only
1344
+ * on lines `checkPin` reads, so whatever else the file holds is left alone.
1345
+ *
1346
+ * Returns the project-relative path when the file was rewritten, and nothing
1347
+ * when there is no Dockerfile, it resolves outside the project, or it already
1348
+ * pins this version — a project that does not load is the report's to explain.
1349
+ */
1350
+ export function repinDockerfile(root) {
1351
+ let rel;
1352
+ try {
1353
+ const ref = readProjectConfig(root).config.sandbox?.build?.dockerfile;
1354
+ rel = ref === undefined ? undefined : normalise(root, ref);
1355
+ }
1356
+ catch {
1357
+ return [];
1358
+ }
1359
+ if (rel === undefined) {
1360
+ return [];
1361
+ }
1362
+ const path = join(root, rel);
1363
+ let text;
1364
+ try {
1365
+ text = readFileSync(path, 'utf8');
1366
+ }
1367
+ catch {
1368
+ return [];
1369
+ }
1370
+ const mine = ownVersion();
1371
+ const next = text
1372
+ .split('\n')
1373
+ .map((line) => /^\s*#/.test(line)
1374
+ ? line
1375
+ : line.replace(ZENERA_INSTALL, (_, name) => `@zenera/${name}@${mine}`))
1376
+ .join('\n');
1377
+ if (next === text) {
1378
+ return [];
1379
+ }
1380
+ writeFileSync(path, next);
1381
+ return [rel.split(sep).join('/')];
1382
+ }
1340
1383
  /** Written by the smoke test and read back, to prove the mount is real. */
1341
1384
  const SMOKE = 'zen-check';
1342
1385
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zenera/cli",
3
- "version": "1.1.42",
3
+ "version": "1.1.43",
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,9 +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/engine": "^1.1.42",
58
- "@zenera/neo": "^1.1.42",
59
- "@zenera/view": "^1.1.42",
57
+ "@zenera/engine": "^1.1.43",
58
+ "@zenera/neo": "^1.1.43",
59
+ "@zenera/view": "^1.1.43",
60
60
  "google-auth-library": "^10.0.0",
61
61
  "ink": "^7.1.1",
62
62
  "openai": "^6.0.0",
@@ -127,8 +127,8 @@ the model to compute or remember. Specifically:
127
127
  the container is the same one that built it. An unpinned name drifts away from
128
128
  the host on the next image build; a stale pin leaves the container holding an
129
129
  older `zen` than the session outside it. `zen check` reports both, as
130
- `sandbox.pin.absent` and `sandbox.pin.stale`, but will not repair either - the
131
- Dockerfile is the project's file, so this edit is yours to make. The pin
130
+ `sandbox.pin.absent` and `sandbox.pin.stale`, and `zen check --fix` repairs
131
+ both by rewriting only the version after each `@zenera/*` name. The pin
132
132
  follows `zen --version` even when `SPECIFICATION.md` names another number.
133
133
  The `zen-spec-sync` skill gives the one question to raise in that case.
134
134
 
@@ -70,7 +70,8 @@ guess a flag, and do not read them all.
70
70
  zen init scaffold a project here, pick a model
71
71
  zen check validate everything before spending a turn
72
72
  zen run the TUI
73
- zen run "what changed?" one answer, this directory as the workspace
73
+ zen run "what changed?" one answer, in a fresh session directory
74
+ zen run --here "what changed?" ... about the files in this directory
74
75
  zen run batch --input cases.json many questions at once, each isolated
75
76
  zen inspect --open what the model was actually given
76
77
  zen inspect graph --dir <run dir> the same run as a graph you can read
@@ -55,7 +55,8 @@ checked too. A word that is neither is a usage error (exit 2), not a report.
55
55
  is written once, by `zen init`, and nothing rewrites it afterwards, so it falls
56
56
  behind the host on the next upgrade and the container ends up holding a
57
57
  different `zen` than the session outside it. Read rather than run, so this one
58
- is reported with `--no-sandbox` too.
58
+ is reported with `--no-sandbox` too. `--fix` rewrites the pins to this `zen`'s
59
+ version and touches nothing else in the Dockerfile.
59
60
 
60
61
  ### Findings
61
62
 
@@ -16,6 +16,7 @@ answers once on stdout and exits.
16
16
  | `--new` | Start a new session rather than continuing one |
17
17
  | `--input <file>` | Read the whole request from JSON; `-` is stdin |
18
18
  | `--workspace <dir>` | What the agent may read and write |
19
+ | `--here` | `--workspace` with the directory you are in |
19
20
  | `--memory <dir>` | Where the agents remember into, instead of the project's |
20
21
  | `--memory-read-only` | Recall from it; write nothing back |
21
22
  | `--model <ref>` | Override the default model for this run |
@@ -270,7 +271,7 @@ question of what the agents know without changing what they know.
270
271
 
271
272
  ### What means nothing in a batch
272
273
 
273
- `--session`, `--new`, `--workspace`, `--plain` and `--theme` are refused by
274
+ `--session`, `--new`, `--workspace`, `--here`, `--plain` and `--theme` are refused by
274
275
  name rather than ignored. Every item is a new session of its own, they cannot
275
276
  share a workspace, and a batch never draws the TUI.
276
277
 
@@ -297,11 +298,18 @@ git diff | zen run "summarise this diff"
297
298
  ## What a prompt on the command line implies
298
299
 
299
300
  A prompt is a request for an answer, not a conversation to pick up, so it
300
- answers the three questions itself: **a fresh session**, **the directory you are
301
- in** as the workspace, and **no confirmation**. Every flag still wins -
302
- `--session`, `--workspace` and `--read-only` override it - and the TUI, where
301
+ answers the three questions itself: **a fresh session**, **a fresh, empty
302
+ directory inside it** (`<session>/workspace/`) as the workspace, and **no
303
+ confirmation**. To have it read and write the directory you are in, say
304
+ `--here` (the same as `--workspace .`). Every flag still wins - `--session`,
305
+ `--workspace`, `--here` and `--read-only` override it - and the TUI, where
303
306
  there is someone to ask, still asks.
304
307
 
308
+ ```
309
+ zen run "draft a release note" works in sessions/<id>/workspace/
310
+ zen run --here "what changed?" reads and writes the directory you are in
311
+ ```
312
+
305
313
  There is no `resume`. A session continues itself, because its state is what it
306
314
  is; `--session <id>` picks which one.
307
315