@zenera/cli 1.1.17 → 1.1.19
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 +99 -16
- package/dist/banner.d.ts +19 -0
- package/dist/banner.js +17 -10
- package/dist/command.d.ts +3 -0
- package/dist/commands/index.js +4 -1
- package/dist/commands/init.js +1 -1
- package/dist/commands/inspect.js +77 -13
- package/dist/commands/key.js +49 -1
- package/dist/commands/memory.js +16 -3
- package/dist/commands/meta.d.ts +3 -0
- package/dist/commands/meta.js +596 -0
- package/dist/commands/run.js +63 -38
- package/dist/engine.d.ts +15 -0
- package/dist/engine.js +20 -1
- package/dist/home.d.ts +2 -0
- package/dist/home.js +2 -0
- package/dist/main.js +1 -1
- package/dist/meta.d.ts +221 -0
- package/dist/meta.js +695 -0
- package/dist/podman.js +64 -2
- package/dist/projects.d.ts +9 -1
- package/dist/projects.js +19 -2
- package/dist/resolve.js +7 -1
- package/dist/sandbox.d.ts +2 -0
- package/dist/sandbox.js +1 -1
- package/dist/scaffold.js +1 -1
- package/dist/session.d.ts +10 -0
- package/dist/session.js +19 -1
- package/dist/term.d.ts +21 -0
- package/dist/term.js +74 -2
- package/dist/tui/app.js +313 -150
- package/dist/tui/theme.d.ts +26 -7
- package/dist/tui/theme.js +62 -36
- package/dist/tui/wrap.d.ts +41 -16
- package/dist/tui/wrap.js +191 -18
- package/dist/validate.js +3 -16
- package/package.json +3 -2
- package/templates/editor/.github/copilot-instructions.md +61 -7
- package/templates/editor/.github/prompts/{review-project.prompt.md → project-review.prompt.md} +0 -1
- package/templates/editor/.github/prompts/{update-spec-from-feedback.prompt.md → spec-apply-feedback.prompt.md} +5 -5
- package/templates/editor/.github/prompts/{sync-with-spec.prompt.md → spec-sync-project.prompt.md} +4 -4
- package/templates/editor/.github/skills/zen-cli/references/check.md +0 -2
- package/templates/editor/.github/skills/zen-cli/references/inspect.md +4 -3
- package/templates/editor/.github/skills/zen-cli/references/memory.md +14 -0
- package/templates/editor/.github/skills/zen-cli/references/projects.md +1 -0
- package/templates/editor/.github/skills/zen-cli/references/run.md +75 -16
- package/templates/editor/.github/skills/zen-memory/SKILL.md +52 -4
- package/templates/editor/.github/skills/zen-review/SKILL.md +2 -2
- package/templates/editor/.github/skills/zen-review/scripts/review.sh +1 -1
- package/templates/editor/.github/skills/zen-spec-sync/SKILL.md +2 -2
- package/templates/editor/.github/skills/zen-spec-sync/scripts/snapshot.sh +1 -1
- package/templates/project/SPECIFICATION.md.tmpl +25 -2
- package/templates/project/agents/tools-instructions.md +8 -0
- package/templates/project/agents.yaml.tmpl +3 -3
- package/templates/project/env +7 -1
- package/templates/project/gitignore +4 -4
package/README.md
CHANGED
|
@@ -125,7 +125,7 @@ In the editor, describe the job in `SPECIFICATION.md`, then send this in the
|
|
|
125
125
|
agent chat:
|
|
126
126
|
|
|
127
127
|
```
|
|
128
|
-
/sync-
|
|
128
|
+
/spec-sync-project
|
|
129
129
|
```
|
|
130
130
|
|
|
131
131
|
It creates the specialists, responsibilities, tool access and handoffs the job
|
|
@@ -145,7 +145,7 @@ zen run my-project # no prompt given: opens a full-screen terminal
|
|
|
145
145
|
zen check my-project # validate the project and every file it names
|
|
146
146
|
zen inspect # open the last run's report.html
|
|
147
147
|
zen list --sessions # every project, its sessions and last run
|
|
148
|
-
echo "triage this" | zen run my-project --
|
|
148
|
+
echo "triage this" | zen run my-project --json | jq
|
|
149
149
|
```
|
|
150
150
|
|
|
151
151
|
Standing inside the project, the name is optional: a bare `zen run`, `zen check`
|
|
@@ -156,7 +156,7 @@ session and uses the current directory as the workspace, with write access.
|
|
|
156
156
|
`--session`, `--workspace` and `--read-only` override that.
|
|
157
157
|
|
|
158
158
|
To change what the system does, update `SPECIFICATION.md` and send
|
|
159
|
-
`/sync-
|
|
159
|
+
`/spec-sync-project` again. The next section explains that workflow in detail.
|
|
160
160
|
|
|
161
161
|
## You write the specification; a coding agent writes the system
|
|
162
162
|
|
|
@@ -202,15 +202,15 @@ In the window that opens:
|
|
|
202
202
|
|
|
203
203
|
1. **Edit `SPECIFICATION.md`** - what the system is for, which agents exist,
|
|
204
204
|
what each may reach for, and what _done_ means.
|
|
205
|
-
2. **Open the chat panel and send `/sync-
|
|
205
|
+
2. **Open the chat panel and send `/spec-sync-project`.** It reads the
|
|
206
206
|
specification and every file implementing it, works out the difference in
|
|
207
207
|
both directions, and changes the smallest thing that closes each gap.
|
|
208
208
|
3. **Answer `SPECIFICATION-FEEDBACK.md`**, which it writes for anything it could
|
|
209
209
|
not do without guessing. Every question carries two to four written-out
|
|
210
210
|
answers, each with a `[ ]` box: tick one, delete the ones you do not want, or
|
|
211
211
|
write your own in their place.
|
|
212
|
-
4. **Send `/
|
|
213
|
-
`SPECIFICATION.md` - never into a prompt - and then `/sync-
|
|
212
|
+
4. **Send `/spec-apply-feedback`.** It folds the answers you chose into
|
|
213
|
+
`SPECIFICATION.md` - never into a prompt - and then `/spec-sync-project` again.
|
|
214
214
|
|
|
215
215
|
Back in the terminal: `zen check`, then `zen run`.
|
|
216
216
|
|
|
@@ -221,16 +221,16 @@ They are prompt files under `.github/prompts/`, which VS Code and its forks
|
|
|
221
221
|
offer as chat slash-commands. Both `zen init` and `zen open` write them fresh,
|
|
222
222
|
so they never go stale - and edits to them do not survive.
|
|
223
223
|
|
|
224
|
-
| In chat
|
|
225
|
-
|
|
|
226
|
-
| `/sync-
|
|
227
|
-
| `/
|
|
228
|
-
| `/review
|
|
229
|
-
| `/new-agent`
|
|
230
|
-
| `/new-skill`
|
|
224
|
+
| In chat | Does |
|
|
225
|
+
| ---------------------- | ---------------------------------------------------------------- |
|
|
226
|
+
| `/spec-sync-project` | Makes every file match `SPECIFICATION.md`, both directions. |
|
|
227
|
+
| `/spec-apply-feedback` | Folds your answers in `SPECIFICATION-FEEDBACK.md` into the spec. |
|
|
228
|
+
| `/project-review` | Reads the project as a reviewer would, and reports. |
|
|
229
|
+
| `/new-agent` | Adds an agent - prompt, wiring and hand-offs. |
|
|
230
|
+
| `/new-skill` | Adds a skill under `agents/skills/`. |
|
|
231
231
|
|
|
232
232
|
In an editor that does not support prompt files, paste the contents of
|
|
233
|
-
`.github/prompts/sync-
|
|
233
|
+
`.github/prompts/spec-sync-project.prompt.md` into its chat instead - it is only a
|
|
234
234
|
prompt. Alongside them, `.github/copilot-instructions.md` is the standing brief
|
|
235
235
|
that explains this runtime to whatever agent is reading, and
|
|
236
236
|
`.github/skills/zen-cli/` is the CLI's own reference for it.
|
|
@@ -240,7 +240,7 @@ that explains this runtime to whatever agent is reading, and
|
|
|
240
240
|
### The self-improving loop
|
|
241
241
|
|
|
242
242
|
```
|
|
243
|
-
edit SPECIFICATION.md → /sync-
|
|
243
|
+
edit SPECIFICATION.md → /spec-sync-project → zen check → zen run
|
|
244
244
|
↑ ↓
|
|
245
245
|
└───── refine the spec <-──── zen inspect <-───────┘
|
|
246
246
|
```
|
|
@@ -299,7 +299,7 @@ Read a codebase and leave a short written note about it in the workspace.
|
|
|
299
299
|
- Every claim in it comes from a file that was actually read.
|
|
300
300
|
```
|
|
301
301
|
|
|
302
|
-
Send `/sync-
|
|
302
|
+
Send `/spec-sync-project` in the editor's chat. It writes `agents.yaml` - who
|
|
303
303
|
exists, and what each may reach for:
|
|
304
304
|
|
|
305
305
|
```yaml
|
|
@@ -465,6 +465,7 @@ remove resources beyond the current project.
|
|
|
465
465
|
| `key` | The credential keyring - add, check, switch, remove. |
|
|
466
466
|
| `models` | What this machine can use - list, search, test, pick. |
|
|
467
467
|
| `run` | Runs the project - the TUI on a terminal, a single answer otherwise. |
|
|
468
|
+
| `meta` | Runs the meta agent over the project - the one that edits it. |
|
|
468
469
|
| `inspect` | Opens or rebuilds a run's `report.html`. |
|
|
469
470
|
| `memory` | What the agents remember - size, listing, one node, or a whole page. |
|
|
470
471
|
| `check` | Validates the project and every file it names, and asks the models. |
|
|
@@ -539,6 +540,88 @@ an accident.
|
|
|
539
540
|
| `4` | no usable credential |
|
|
540
541
|
| `5` | sandbox unavailable |
|
|
541
542
|
|
|
543
|
+
## The meta agent - the agent that works on the project
|
|
544
|
+
|
|
545
|
+
`zen run` runs the agents your project describes. `zen meta` runs an agent _over_
|
|
546
|
+
the project: a coding agent rooted at the project directory, which reads
|
|
547
|
+
`SPECIFICATION.md`, changes `agents.yaml`, the prompts and the skills, and runs
|
|
548
|
+
whatever commands it needs to check itself - on the keys already in your
|
|
549
|
+
keyring, and not on anybody's subscription. It is the loop your editor's chat
|
|
550
|
+
panel drives, as a command you can put in a script.
|
|
551
|
+
|
|
552
|
+
```sh
|
|
553
|
+
zen meta run "what does this project do?"
|
|
554
|
+
zen meta run acme "review the last commit"
|
|
555
|
+
zen meta prompts # which /<name> prompts this project has
|
|
556
|
+
zen meta run /project-review # .github/prompts/project-review.prompt.md
|
|
557
|
+
zen meta run acme /spec-sync-project agents/triage.md
|
|
558
|
+
git diff | zen meta run "what broke?" --allow-tool read
|
|
559
|
+
zen meta run --dry-run "hello" # what would run, secrets masked
|
|
560
|
+
```
|
|
561
|
+
|
|
562
|
+
Every prompt goes through `run`, whether you typed it or it is a file: the
|
|
563
|
+
alternative was a verb that appeared only sometimes, and a bare word after
|
|
564
|
+
`zen meta` that was a subcommand on Tuesdays and a question on Wednesdays.
|
|
565
|
+
|
|
566
|
+
It uses every tool without asking. The prompts it runs were written for an
|
|
567
|
+
editor, where reading files, writing them and running commands is the whole
|
|
568
|
+
point, and a `-p` run has nobody sitting there to answer the question - so the
|
|
569
|
+
alternative is not a safer run but a stalled one. `--allow-tool read`,
|
|
570
|
+
`--allow-tool 'shell(git:*)'` narrow it to what a particular run needs, and
|
|
571
|
+
`--ask` puts the question back. It is your own checkout and your own key either
|
|
572
|
+
way, so treat it as you would `git push`: fine on a project you own, worth
|
|
573
|
+
narrowing on one you do not.
|
|
574
|
+
|
|
575
|
+
The answer goes to stdout and the progress to stderr, like everything else here,
|
|
576
|
+
so `zen meta run "…" > out.md` keeps the answer alone.
|
|
577
|
+
|
|
578
|
+
Progress is a small window that rewrites itself rather than a transcript
|
|
579
|
+
scrolling past, because only the last few steps say what it is doing now. The
|
|
580
|
+
whole of it - the prompt, every tool call, the answer - is written as it happens
|
|
581
|
+
to `.tmp/logs/meta.<when>.log`, whose path is printed before the run starts, so
|
|
582
|
+
`tail -f` in another terminal follows a run already going. The answer alone is
|
|
583
|
+
kept beside it as `.tmp/logs/meta.<when>.md`, named again once it has printed -
|
|
584
|
+
a terminal turns that into something you click.
|
|
585
|
+
|
|
586
|
+
`zen meta run /<name>` is the part an editor cannot do for you from a script.
|
|
587
|
+
The prompts under `.github/prompts/` - `/spec-sync-project`, `/project-review` -
|
|
588
|
+
are what a chat panel offers as slash commands, and nothing outside an editor
|
|
589
|
+
reads them. zen reads one, drops the frontmatter and sends the body - so the
|
|
590
|
+
same prompt runs from the editor, from a terminal and from CI.
|
|
591
|
+
|
|
592
|
+
A terminal has no menu dropping down as you type a slash, so
|
|
593
|
+
`zen meta prompts` lists what this project holds, each with its description.
|
|
594
|
+
`zen meta run` with nothing to say asks the same question interactively.
|
|
595
|
+
|
|
596
|
+
### Which model it runs on
|
|
597
|
+
|
|
598
|
+
```sh
|
|
599
|
+
zen meta model # the whole chain, with the winner marked
|
|
600
|
+
zen meta model vertex/gemini-3.8-flash # for every project on this machine
|
|
601
|
+
zen meta model --local openai/gpt-5.6-sol # for this project only, via its .env
|
|
602
|
+
zen meta model --pick # choose from providers you hold a key for
|
|
603
|
+
```
|
|
604
|
+
|
|
605
|
+
Highest first: `--model`, `ZENERA_META_MODEL` in the shell, the same variable in
|
|
606
|
+
the project's `.env`, `zen meta model`, then the project's `agents.yaml`
|
|
607
|
+
`model:`. When none of them answers, a key does: the best model zen knows of
|
|
608
|
+
for the first provider you hold one for, named on stderr as it starts, so a
|
|
609
|
+
fresh machine needs a key and nothing else.
|
|
610
|
+
The store is separate from `agents.yaml` on purpose - the model a coding agent
|
|
611
|
+
runs on is a personal choice about a tool, like a key, where `agents.yaml` is a
|
|
612
|
+
committed decision about the project's own agents.
|
|
613
|
+
|
|
614
|
+
Not every model works. The meta agent declares its tools in a form only the
|
|
615
|
+
reasoning APIs accept, so use a reasoning model - `gpt-5.6-sol`,
|
|
616
|
+
`claude-opus-5`, `gemini-3.8-flash`. Those three are also what it falls back
|
|
617
|
+
to, and what `zen meta model --pick` offers.
|
|
618
|
+
|
|
619
|
+
Setting one asks it a one-word question before storing it, so a model that will
|
|
620
|
+
not answer is refused with the provider's own words rather than at the start of
|
|
621
|
+
the next run. A typed prefix that names no provider - `vertes/...` - is caught
|
|
622
|
+
before that, because it would otherwise be sent whole to a vendor you never
|
|
623
|
+
named. `--force` stores a ref without asking.
|
|
624
|
+
|
|
542
625
|
## Credentials
|
|
543
626
|
|
|
544
627
|
One keyring serves every provider, and a key goes in the same way whatever it
|
package/dist/banner.d.ts
CHANGED
|
@@ -1,3 +1,19 @@
|
|
|
1
|
+
/** One per sub-brand, so two of them are told apart before the word is read. */
|
|
2
|
+
declare const ACCENTS: {
|
|
3
|
+
readonly orange: {
|
|
4
|
+
readonly face: "\u001B[1;38;5;208m";
|
|
5
|
+
readonly shade: "\u001B[0;38;5;130m";
|
|
6
|
+
};
|
|
7
|
+
readonly cyan: {
|
|
8
|
+
readonly face: "\u001B[1;38;5;51m";
|
|
9
|
+
readonly shade: "\u001B[0;38;5;37m";
|
|
10
|
+
};
|
|
11
|
+
readonly mint: {
|
|
12
|
+
readonly face: "\u001B[1;38;5;121m";
|
|
13
|
+
readonly shade: "\u001B[0;38;5;78m";
|
|
14
|
+
};
|
|
15
|
+
};
|
|
16
|
+
export type Hue = keyof typeof ACCENTS;
|
|
1
17
|
export interface BannerText {
|
|
2
18
|
/** drawn white */
|
|
3
19
|
head: string;
|
|
@@ -5,6 +21,8 @@ export interface BannerText {
|
|
|
5
21
|
accent: string;
|
|
6
22
|
/** the line underneath, dim */
|
|
7
23
|
subtitle: string;
|
|
24
|
+
/** which neon to use. Default: orange, the `zen` mark. */
|
|
25
|
+
hue?: Hue;
|
|
8
26
|
}
|
|
9
27
|
export interface PrintBannerOptions {
|
|
10
28
|
/** Render the block face even when the terminal is narrower than it. */
|
|
@@ -14,4 +32,5 @@ export declare const NEO_BANNER: BannerText;
|
|
|
14
32
|
export declare function bannerLines(text: BannerText, columns?: number): string[];
|
|
15
33
|
/** Narration, and only for someone watching. */
|
|
16
34
|
export declare function printBanner(text: BannerText, options?: PrintBannerOptions): void;
|
|
35
|
+
export {};
|
|
17
36
|
//# sourceMappingURL=banner.d.ts.map
|
package/dist/banner.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { dim, note } from "./term.js";
|
|
1
|
+
import { appearance, dim, note, styled } from "./term.js";
|
|
3
2
|
// ---------------------------------------------------------------------------
|
|
4
3
|
// The banner
|
|
5
4
|
//
|
|
@@ -65,11 +64,17 @@ function big(word) {
|
|
|
65
64
|
}
|
|
66
65
|
/** A wordmark rather than a sentence, so the letters are set apart. */
|
|
67
66
|
const spaced = (s) => [...s.toUpperCase()].join(' ');
|
|
68
|
-
/** Whether anything should be coloured at all — `styleText`'s own answer. */
|
|
69
|
-
const styling = () => styleText('dim', '.') !== '.';
|
|
70
67
|
const RESET = '\u001b[0m';
|
|
71
|
-
const
|
|
72
|
-
|
|
68
|
+
const HEADS = {
|
|
69
|
+
dark: { face: '\u001b[1;38;5;231m', shade: '\u001b[0;38;5;244m' },
|
|
70
|
+
light: { face: '\u001b[1;38;5;235m', shade: '\u001b[0;38;5;245m' },
|
|
71
|
+
};
|
|
72
|
+
/** One per sub-brand, so two of them are told apart before the word is read. */
|
|
73
|
+
const ACCENTS = {
|
|
74
|
+
orange: { face: '\u001b[1;38;5;208m', shade: '\u001b[0;38;5;130m' },
|
|
75
|
+
cyan: { face: '\u001b[1;38;5;51m', shade: '\u001b[0;38;5;37m' },
|
|
76
|
+
mint: { face: '\u001b[1;38;5;121m', shade: '\u001b[0;38;5;78m' },
|
|
77
|
+
};
|
|
73
78
|
/** Runs of stroke and runs of bevel, alternating; spaces stay unstyled. */
|
|
74
79
|
const RUNS = /[█▀▄]+|[^█▀▄ ]+/g;
|
|
75
80
|
const isStroke = (run) => /[█▀▄]/.test(run);
|
|
@@ -77,11 +82,11 @@ const isStroke = (run) => /[█▀▄]/.test(run);
|
|
|
77
82
|
* Two tones per word — without the darker bevel the letter is a flat outline
|
|
78
83
|
* and the shadow reads as part of the stroke.
|
|
79
84
|
*/
|
|
80
|
-
const paint = (text, tone) =>
|
|
85
|
+
const paint = (text, tone) => styled()
|
|
81
86
|
? text.replace(RUNS, (run) => `${isStroke(run) ? tone.face : tone.shade}${run}${RESET}`)
|
|
82
87
|
: text;
|
|
83
88
|
/** The wordmark has no bevel to shade, so it is all face. */
|
|
84
|
-
const flat = (text, tone) =>
|
|
89
|
+
const flat = (text, tone) => styled() ? `${tone.face}${text}${RESET}` : text;
|
|
85
90
|
export const NEO_BANNER = {
|
|
86
91
|
head: 'Zenera',
|
|
87
92
|
accent: 'Neo',
|
|
@@ -92,17 +97,19 @@ export function bannerLines(text, columns = process.stderr.columns || 80) {
|
|
|
92
97
|
// Trimmed: the widest accent row is the banner's right edge, and a pad left
|
|
93
98
|
// inside a styled string cannot be trimmed away later.
|
|
94
99
|
const accent = big(text.accent).map((row) => row.trimEnd());
|
|
100
|
+
const tone = ACCENTS[text.hue ?? 'orange'];
|
|
101
|
+
const ink = HEADS[appearance()];
|
|
95
102
|
const margin = ' '.repeat(INDENT);
|
|
96
103
|
const width = INDENT + head[0].length + GAP + Math.max(...accent.map((row) => row.length));
|
|
97
104
|
if (width > columns) {
|
|
98
105
|
return [
|
|
99
|
-
`${margin}${flat(text.head.toUpperCase(),
|
|
106
|
+
`${margin}${flat(text.head.toUpperCase(), ink)} ${flat(text.accent.toUpperCase(), tone)}`,
|
|
100
107
|
`${margin}${dim(spaced(text.subtitle))}`,
|
|
101
108
|
];
|
|
102
109
|
}
|
|
103
110
|
const lines = [];
|
|
104
111
|
for (let r = 0; r < HEIGHT; r++) {
|
|
105
|
-
lines.push(`${margin}${paint(head[r],
|
|
112
|
+
lines.push(`${margin}${paint(head[r], ink)}${' '.repeat(GAP)}${paint(accent[r], tone)}`);
|
|
106
113
|
}
|
|
107
114
|
// Rules on both sides, centred under the art: the subtitle is a caption,
|
|
108
115
|
// and without them a short line of spaced capitals floats.
|
package/dist/command.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { BannerText } from './banner.ts';
|
|
1
2
|
export interface Context {
|
|
2
3
|
readonly args: readonly string[];
|
|
3
4
|
readonly json: boolean;
|
|
@@ -9,6 +10,8 @@ export interface Command {
|
|
|
9
10
|
readonly usage: string;
|
|
10
11
|
/** lines printed under the usage line by `zen help <command>` */
|
|
11
12
|
readonly details?: readonly string[];
|
|
13
|
+
/** printed instead of the `zen` banner, so the sub-brand shows through */
|
|
14
|
+
readonly banner?: BannerText;
|
|
12
15
|
/**
|
|
13
16
|
* Answers `--help` itself, for a command whose arguments select a page the
|
|
14
17
|
* frame cannot see. Without it the frame prints this command's own page.
|
package/dist/commands/index.js
CHANGED
|
@@ -5,6 +5,7 @@ import { inspect } from "./inspect.js";
|
|
|
5
5
|
import { key } from "./key.js";
|
|
6
6
|
import { list } from "./list.js";
|
|
7
7
|
import { memory } from "./memory.js";
|
|
8
|
+
import { meta } from "./meta.js";
|
|
8
9
|
import { models } from "./models.js";
|
|
9
10
|
import { open } from "./open.js";
|
|
10
11
|
import { run } from "./run.js";
|
|
@@ -19,6 +20,7 @@ export const COMMANDS = {
|
|
|
19
20
|
init,
|
|
20
21
|
list,
|
|
21
22
|
run,
|
|
23
|
+
meta,
|
|
22
24
|
open,
|
|
23
25
|
key,
|
|
24
26
|
models,
|
|
@@ -37,6 +39,7 @@ export const ALIASES = {
|
|
|
37
39
|
model: 'models',
|
|
38
40
|
validate: 'check',
|
|
39
41
|
doctor: 'check',
|
|
42
|
+
copilot: 'meta',
|
|
40
43
|
report: 'inspect',
|
|
41
44
|
mem: 'memory',
|
|
42
45
|
edit: 'open',
|
|
@@ -49,7 +52,7 @@ export const EXTERNAL = {
|
|
|
49
52
|
summary: 'A mock API from an openapi/swagger document.',
|
|
50
53
|
usage: 'zen faker <serve|build|cache> [spec...]',
|
|
51
54
|
install: 'npm i -g @zenera/faker',
|
|
52
|
-
banner: { head: 'Zenera', accent: 'Faker', subtitle: 'Mock API Server' },
|
|
55
|
+
banner: { head: 'Zenera', accent: 'Faker', subtitle: 'Mock API Server', hue: 'mint' },
|
|
53
56
|
},
|
|
54
57
|
rag: {
|
|
55
58
|
package: '@zenera/rag',
|
package/dist/commands/init.js
CHANGED
|
@@ -140,7 +140,7 @@ export const init = {
|
|
|
140
140
|
'',
|
|
141
141
|
'SPECIFICATION.md states what the project is for and what it may do,',
|
|
142
142
|
'and describes the one that was just written. Say what you are building',
|
|
143
|
-
'there, then run /sync-
|
|
143
|
+
'there, then run /spec-sync-project in your editor to make the rest match.',
|
|
144
144
|
'',
|
|
145
145
|
'The default agent gets the file tools and a sandboxed shell, plus',
|
|
146
146
|
'`exa:*` when the keyring holds an Exa key. It keeps a memory of its',
|
package/dist/commands/inspect.js
CHANGED
|
@@ -5,31 +5,39 @@ import { readFile, writeFile } from 'node:fs/promises';
|
|
|
5
5
|
import { createServer } from 'node:http';
|
|
6
6
|
import { extname, join, normalize, resolve, sep } from 'node:path';
|
|
7
7
|
import { parse } from "../args.js";
|
|
8
|
+
import { sessionIds } from "../projects.js";
|
|
8
9
|
import { project as resolveProject } from "../resolve.js";
|
|
9
|
-
import { display,
|
|
10
|
-
import { bold, cyan, dim, invalidError, json, note, write } from "../term.js";
|
|
10
|
+
import { display, listRuns, listSessions, newestRun, requireSession, runPaths, sessionPaths, } from "../session.js";
|
|
11
|
+
import { ago, bold, choose, cyan, dim, invalidError, isInteractive, json, note, write, } from "../term.js";
|
|
11
12
|
const USAGE = 'zen inspect [run] [--session <id>] [--open] [--rebuild] [--serve [port]]';
|
|
12
13
|
export const inspect = {
|
|
13
14
|
summary: "Open or rebuild a run's report.html.",
|
|
14
15
|
usage: USAGE,
|
|
15
16
|
details: [
|
|
16
|
-
'With no arguments:
|
|
17
|
+
'With no arguments: asks which session and run, or takes the newest of',
|
|
18
|
+
'each when there is nothing to ask on.',
|
|
17
19
|
'--serve starts a local server, which the report needs for its assets.',
|
|
20
|
+
'--memory <dir> reads that memory instead of the project’s, for a run',
|
|
21
|
+
'that was given `zen run --memory`.',
|
|
18
22
|
],
|
|
19
23
|
run: async (ctx) => {
|
|
20
24
|
const { values, positionals } = parse(ctx.args, {
|
|
21
25
|
project: { type: 'string' },
|
|
22
26
|
session: { type: 'string' },
|
|
27
|
+
memory: { type: 'string' },
|
|
23
28
|
open: { type: 'boolean' },
|
|
24
29
|
rebuild: { type: 'boolean' },
|
|
25
30
|
serve: { type: 'string' },
|
|
26
31
|
}, USAGE);
|
|
27
32
|
const found = await resolveProject({ cwd: ctx.cwd, project: values.project });
|
|
28
33
|
const dir = found.dir;
|
|
29
|
-
|
|
30
|
-
|
|
34
|
+
// Asking is only possible at a terminal, and only honest when the
|
|
35
|
+
// answer is not being parsed by something.
|
|
36
|
+
const asking = isInteractive() && !ctx.json;
|
|
37
|
+
const session = await pickSession(dir, values.session, positionals[0], asking);
|
|
38
|
+
const run = await pickRun(session, positionals[0], asking);
|
|
31
39
|
if (values.rebuild || !existsSync(run.report)) {
|
|
32
|
-
await rebuild(session, run, await memory(dir));
|
|
40
|
+
await rebuild(session, run, await memory(dir, values.memory, ctx.cwd));
|
|
33
41
|
}
|
|
34
42
|
if (ctx.json) {
|
|
35
43
|
json({ session: session.id, run: run.id, report: run.report });
|
|
@@ -52,17 +60,73 @@ export const inspect = {
|
|
|
52
60
|
// ---------------------------------------------------------------------------
|
|
53
61
|
// Choosing what to show
|
|
54
62
|
// ---------------------------------------------------------------------------
|
|
55
|
-
|
|
63
|
+
/**
|
|
64
|
+
* Which session to read. With nothing named and a terminal to ask on, the
|
|
65
|
+
* person picks: "the newest" is a guess about which of a dozen runs they meant,
|
|
66
|
+
* and a wrong guess looks the same as a broken command.
|
|
67
|
+
*/
|
|
68
|
+
async function pickSession(dir, asked, run, asking) {
|
|
56
69
|
if (asked) {
|
|
57
70
|
return requireSession(dir, asked);
|
|
58
71
|
}
|
|
59
|
-
|
|
60
|
-
if (!newest) {
|
|
72
|
+
if (sessionIds(dir).length === 0) {
|
|
61
73
|
throw invalidError('nothing has been run here yet', 'start one: zen run');
|
|
62
74
|
}
|
|
63
|
-
|
|
75
|
+
// A named run answers the question itself: it is in whichever session holds it.
|
|
76
|
+
if (!asking || run) {
|
|
77
|
+
return newestWorked(dir, run);
|
|
78
|
+
}
|
|
79
|
+
const worked = (await listSessions(dir)).filter((s) => s.runs > 0);
|
|
80
|
+
if (worked.length === 0) {
|
|
81
|
+
throw invalidError('no runs yet — every session is empty', 'start one: zen run');
|
|
82
|
+
}
|
|
83
|
+
return await choose('Which session?', worked.map((s) => ({
|
|
84
|
+
label: s.id,
|
|
85
|
+
detail: [
|
|
86
|
+
s.title,
|
|
87
|
+
`${s.runs} run${s.runs === 1 ? '' : 's'}`,
|
|
88
|
+
ago(s.lastRunAt ?? s.createdAt),
|
|
89
|
+
s.busy ? 'running' : '',
|
|
90
|
+
]
|
|
91
|
+
.filter(Boolean)
|
|
92
|
+
.join(' '),
|
|
93
|
+
value: sessionPaths(dir, s.id),
|
|
94
|
+
})));
|
|
64
95
|
}
|
|
65
|
-
|
|
96
|
+
/**
|
|
97
|
+
* The newest session that has a report to show — not simply the newest. A
|
|
98
|
+
* session exists before its first run and outlives one that never recorded
|
|
99
|
+
* anything, so the newest is routinely empty and picking it blindly answers
|
|
100
|
+
* "has no runs" about a project full of them.
|
|
101
|
+
*/
|
|
102
|
+
function newestWorked(dir, run) {
|
|
103
|
+
for (const id of sessionIds(dir).reverse()) {
|
|
104
|
+
const session = sessionPaths(dir, id);
|
|
105
|
+
if (run ? existsSync(runPaths(session, run).dir) : newestRun(session)) {
|
|
106
|
+
return session;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
throw invalidError(run ? `no run ${run} in any session` : 'no runs yet — every session is empty', run ? 'see: zen list --sessions' : 'start one: zen run');
|
|
110
|
+
}
|
|
111
|
+
async function pickRun(session, asked, asking) {
|
|
112
|
+
if (!asked && asking) {
|
|
113
|
+
const runs = await listRuns(session);
|
|
114
|
+
if (runs.length === 0) {
|
|
115
|
+
throw invalidError(`session ${session.id} has no runs`);
|
|
116
|
+
}
|
|
117
|
+
// One run is not a question.
|
|
118
|
+
return await choose(`Which run of ${session.id}?`, runs.map((r) => ({
|
|
119
|
+
label: r.id,
|
|
120
|
+
detail: [
|
|
121
|
+
r.agent,
|
|
122
|
+
r.error ? 'failed' : r.stopReason,
|
|
123
|
+
ago(r.finishedAt ?? r.startedAt),
|
|
124
|
+
]
|
|
125
|
+
.filter(Boolean)
|
|
126
|
+
.join(' '),
|
|
127
|
+
value: runPaths(session, r.id),
|
|
128
|
+
})));
|
|
129
|
+
}
|
|
66
130
|
const id = asked ?? newestRun(session);
|
|
67
131
|
if (!id) {
|
|
68
132
|
throw invalidError(`session ${session.id} has no runs`);
|
|
@@ -98,9 +162,9 @@ async function rebuild(session, run, store) {
|
|
|
98
162
|
* has the shape of what the run recalled but not a word of it; a run that
|
|
99
163
|
* never touched memory pays nothing, because the report asks for no node.
|
|
100
164
|
*/
|
|
101
|
-
async function memory(dir) {
|
|
165
|
+
async function memory(dir, override, cwd) {
|
|
102
166
|
const { config } = readProjectConfig(dir);
|
|
103
|
-
const at = memoryDir(dir, config);
|
|
167
|
+
const at = memoryDir(dir, config, override && resolve(cwd, override));
|
|
104
168
|
if (!at || !existsSync(join(at, 'manifest.json'))) {
|
|
105
169
|
return undefined;
|
|
106
170
|
}
|
package/dist/commands/key.js
CHANGED
|
@@ -454,10 +454,57 @@ const env = async (ctx, args) => {
|
|
|
454
454
|
function shellQuote(value) {
|
|
455
455
|
return `'${value.replaceAll("'", `'\\''`)}'`;
|
|
456
456
|
}
|
|
457
|
+
/**
|
|
458
|
+
* One credential on stdout, alone, for a tool that wants a fresh one before
|
|
459
|
+
* every request rather than an export that goes stale — `zen meta` hands this
|
|
460
|
+
* to copilot as `COPILOT_PROVIDER_API_KEY_COMMAND`.
|
|
461
|
+
*
|
|
462
|
+
* A stored secret is simply printed. A service-account file is exchanged for
|
|
463
|
+
* an OAuth access token, which is the part worth having a command for: those
|
|
464
|
+
* last an hour, and a session does not.
|
|
465
|
+
*/
|
|
466
|
+
const token = async (ctx, args) => {
|
|
467
|
+
const { positionals } = parse(args, {}, 'zen key token <provider>[/name]');
|
|
468
|
+
const { provider, name } = parseRef(positionals[0] ?? 'vertex');
|
|
469
|
+
const store = await KeyStore.open();
|
|
470
|
+
const entry = name ? store.find(provider, name) : store.active(provider);
|
|
471
|
+
if (!entry) {
|
|
472
|
+
throw credentialError(`no ${provider} key`, `add one: zen key add ${provider}`);
|
|
473
|
+
}
|
|
474
|
+
if (entry.holds === 'secret') {
|
|
475
|
+
write(store.reveal(entry));
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
478
|
+
const { GoogleAuth } = await import('google-auth-library');
|
|
479
|
+
const auth = new GoogleAuth({
|
|
480
|
+
keyFile: store.fileOf(entry),
|
|
481
|
+
scopes: ['https://www.googleapis.com/auth/cloud-platform'],
|
|
482
|
+
});
|
|
483
|
+
const access = await (await auth.getClient()).getAccessToken();
|
|
484
|
+
if (!access.token) {
|
|
485
|
+
throw credentialError(`${keyId(entry)} minted no token`, 'check the service account is still enabled');
|
|
486
|
+
}
|
|
487
|
+
if (ctx.json) {
|
|
488
|
+
json({ key: keyId(entry), token: access.token });
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
write(access.token);
|
|
492
|
+
};
|
|
457
493
|
// ---------------------------------------------------------------------------
|
|
458
494
|
// Dispatch
|
|
459
495
|
// ---------------------------------------------------------------------------
|
|
460
|
-
const SUBS = {
|
|
496
|
+
const SUBS = {
|
|
497
|
+
ls,
|
|
498
|
+
list: ls,
|
|
499
|
+
add,
|
|
500
|
+
use,
|
|
501
|
+
check,
|
|
502
|
+
rm,
|
|
503
|
+
remove: rm,
|
|
504
|
+
show,
|
|
505
|
+
env,
|
|
506
|
+
token,
|
|
507
|
+
};
|
|
461
508
|
export const key = {
|
|
462
509
|
summary: 'The credential store: add, choose, verify and export API keys.',
|
|
463
510
|
usage: USAGE,
|
|
@@ -480,6 +527,7 @@ export const key = {
|
|
|
480
527
|
' zen key rm <provider>/<name> Forget one.',
|
|
481
528
|
' zen key show <ref> [--reveal] Masked by default.',
|
|
482
529
|
' zen key env [provider …] Shell exports, for other tools.',
|
|
530
|
+
' zen key token [ref] One credential on stdout, minted now.',
|
|
483
531
|
],
|
|
484
532
|
run: async (ctx) => {
|
|
485
533
|
const [name, ...rest] = ctx.args;
|
package/dist/commands/memory.js
CHANGED
|
@@ -2,7 +2,7 @@ import { ALL_AGENTS, buildMemoryReport, hostPath, memoryDir, MemoryIndex, Memory
|
|
|
2
2
|
import { spawn } from 'node:child_process';
|
|
3
3
|
import { existsSync } from 'node:fs';
|
|
4
4
|
import { writeFile } from 'node:fs/promises';
|
|
5
|
-
import { isAbsolute, join, resolve } from 'node:path';
|
|
5
|
+
import { basename, isAbsolute, join, resolve } from 'node:path';
|
|
6
6
|
import { parse } from "../args.js";
|
|
7
7
|
import { project as resolveProject } from "../resolve.js";
|
|
8
8
|
import { ago, bold, bytes, confirm, count, cyan, dim, green, isInteractive, json, note, table, usageError, write, writeAll, yellow, } from "../term.js";
|
|
@@ -39,6 +39,7 @@ export const memory = {
|
|
|
39
39
|
' forget <id...> Remove nodes, their vectors and their files.',
|
|
40
40
|
'',
|
|
41
41
|
' --project <name|dir> Which project. Defaults to the one you are in.',
|
|
42
|
+
' --dir <dir> Read this memory directory instead of the project’s.',
|
|
42
43
|
' --kind <name> Only this kind of node.',
|
|
43
44
|
' --audience <name> Only nodes committed under this label.',
|
|
44
45
|
' --files Only nodes that remember a file.',
|
|
@@ -59,6 +60,7 @@ export const memory = {
|
|
|
59
60
|
run: async (ctx) => {
|
|
60
61
|
const { values, positionals } = parse(ctx.args, {
|
|
61
62
|
project: { type: 'string' },
|
|
63
|
+
dir: { type: 'string' },
|
|
62
64
|
kind: { type: 'string' },
|
|
63
65
|
audience: { type: 'string' },
|
|
64
66
|
files: { type: 'boolean' },
|
|
@@ -73,7 +75,7 @@ export const memory = {
|
|
|
73
75
|
throw usageError(`unknown subcommand: ${what}`, USAGE);
|
|
74
76
|
}
|
|
75
77
|
const rest = positionals.slice(1);
|
|
76
|
-
const opened = await open(ctx.cwd, values.project);
|
|
78
|
+
const opened = await open(ctx.cwd, values.project, values.dir);
|
|
77
79
|
try {
|
|
78
80
|
switch (what) {
|
|
79
81
|
case 'stats':
|
|
@@ -98,8 +100,19 @@ export const memory = {
|
|
|
98
100
|
* project would resolve models and read every prompt file to inspect a graph
|
|
99
101
|
* that needs none of it — and would fail on a project whose credentials are
|
|
100
102
|
* missing, which is exactly when someone is debugging.
|
|
103
|
+
*
|
|
104
|
+
* A named directory is opened as it stands, project or no project: a graph
|
|
105
|
+
* copied out of a running session is the one most worth looking at, and it
|
|
106
|
+
* should not have to be given an `agents.yaml` first.
|
|
101
107
|
*/
|
|
102
|
-
async function open(cwd, want) {
|
|
108
|
+
async function open(cwd, want, at) {
|
|
109
|
+
if (at) {
|
|
110
|
+
const dir = resolve(cwd, at);
|
|
111
|
+
if (!existsSync(join(dir, 'manifest.json'))) {
|
|
112
|
+
throw usageError(`${dir} is not a memory`, 'no manifest.json in it');
|
|
113
|
+
}
|
|
114
|
+
return { store: await MemoryStore.open(dir), dir, project: basename(dir) };
|
|
115
|
+
}
|
|
103
116
|
const project = await resolveProject({ cwd, project: want });
|
|
104
117
|
const { config } = readProjectConfig(project.dir);
|
|
105
118
|
const dir = memoryDir(project.dir, config);
|