@zenera/cli 1.1.25 → 1.1.27
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 +13 -1
- package/dist/catalog.d.ts +20 -0
- package/dist/catalog.js +60 -0
- package/dist/commands/index.js +2 -0
- package/dist/commands/inspect.d.ts +5 -1
- package/dist/commands/inspect.js +54 -14
- package/dist/commands/memory.js +28 -3
- package/dist/commands/meta.js +24 -5
- package/dist/commands/models.js +2 -1
- package/dist/commands/sandbox.js +73 -3
- package/dist/commands/view.d.ts +8 -0
- package/dist/commands/view.js +114 -0
- package/dist/engine.d.ts +3 -1
- package/dist/engine.js +6 -0
- package/dist/eventlog.d.ts +5 -0
- package/dist/eventlog.js +25 -1
- package/dist/liveness.js +20 -2
- package/dist/main.js +0 -0
- package/dist/meta/editor.d.ts +1 -1
- package/dist/meta/editor.js +4 -21
- package/dist/meta/finetune/analyze-brief.d.ts +26 -0
- package/dist/meta/finetune/analyze-brief.js +131 -0
- package/dist/meta/finetune/apply-brief.d.ts +23 -0
- package/dist/meta/finetune/apply-brief.js +247 -0
- package/dist/meta/finetune/brief.d.ts +45 -0
- package/dist/meta/finetune/brief.js +158 -0
- package/dist/meta/finetune/command.d.ts +1 -1
- package/dist/meta/finetune/command.js +224 -46
- package/dist/meta/finetune/cost.d.ts +46 -0
- package/dist/meta/finetune/cost.js +105 -0
- package/dist/meta/finetune/dataset/types.d.ts +3 -1
- package/dist/meta/finetune/feedback.d.ts +14 -2
- package/dist/meta/finetune/feedback.js +22 -3
- package/dist/meta/finetune/guard.d.ts +29 -0
- package/dist/meta/finetune/guard.js +104 -0
- package/dist/meta/finetune/improvements.d.ts +50 -3
- package/dist/meta/finetune/improvements.js +290 -33
- package/dist/meta/finetune/live.d.ts +17 -7
- package/dist/meta/finetune/live.js +56 -10
- package/dist/meta/finetune/loop.js +59 -10
- package/dist/meta/finetune/recheck.d.ts +58 -0
- package/dist/meta/finetune/recheck.js +311 -0
- package/dist/meta/finetune/report.d.ts +9 -2
- package/dist/meta/finetune/report.js +551 -438
- package/dist/meta/finetune/sampler.js +8 -2
- package/dist/meta/finetune/train.d.ts +6 -2
- package/dist/meta/finetune/train.js +55 -21
- package/dist/meta/finetune/tui.d.ts +3 -3
- package/dist/meta/finetune/tui.js +37 -12
- package/dist/meta/finetune/tuning.d.ts +27 -1
- package/dist/meta/finetune/tuning.js +21 -2
- package/dist/meta/finetune/usage.d.ts +4 -0
- package/dist/meta/finetune/usage.js +35 -0
- package/dist/meta/finetune/view.d.ts +9 -0
- package/dist/meta/finetune/view.js +27 -0
- package/dist/meta/host.d.ts +2 -1
- package/dist/meta/host.js +2 -1
- package/dist/meta.d.ts +291 -0
- package/dist/meta.js +981 -0
- package/dist/resources.d.ts +215 -0
- package/dist/resources.js +856 -0
- package/dist/scaffold.d.ts +3 -4
- package/dist/scaffold.js +21 -9
- package/dist/trace.d.ts +5 -0
- package/dist/trace.js +54 -5
- package/dist/tui/app.js +19 -5
- package/dist/tui/links.d.ts +27 -0
- package/dist/tui/links.js +142 -0
- package/dist/tui/models.d.ts +3 -1
- package/dist/tui/models.js +65 -7
- package/dist/tui/sandbox.d.ts +10 -0
- package/dist/tui/sandbox.js +131 -0
- package/package.json +3 -2
- package/templates/editor/.github/copilot-instructions.md +29 -43
- package/templates/editor/.github/prompts/analyze.prompt.md +3 -1
- package/templates/editor/.github/prompts/finetune-apply.prompt.md +221 -42
- package/templates/editor/.github/prompts/project-review.prompt.md +1 -1
- package/templates/editor/.github/skills/zen-analyze-run/SKILL.md +162 -57
- package/templates/editor/.github/skills/zen-cli/references/finetune.md +74 -14
- package/templates/editor/.github/skills/zen-cli/references/inspect.md +16 -2
- package/templates/editor/.github/skills/zen-cli/references/projects.md +4 -6
- package/templates/editor/.github/skills/zen-cli/references/sandbox.md +25 -13
- package/templates/editor/.github/skills/zen-finetune/SKILL.md +74 -33
- package/templates/editor/.github/skills/zen-inspect/SKILL.md +14 -1
- package/templates/editor/.github/skills/zen-inspect-ask/SKILL.md +4 -3
- package/templates/editor/.github/skills/zen-instructions/SKILL.md +4 -12
- package/templates/editor/.github/skills/zen-memory/SKILL.md +64 -39
- package/templates/editor/.github/skills/zen-review/SKILL.md +6 -31
- package/templates/editor/.github/skills/zen-review/scripts/check-paths.sh +1 -2
- package/templates/editor/.github/skills/zen-review/scripts/review.sh +3 -12
- package/templates/editor/.github/skills/zen-sandbox/SKILL.md +2 -1
- package/templates/editor/.github/skills/zen-spec-sync/SKILL.md +2 -4
- package/templates/project/agents/fork-instructions.md +78 -28
- package/templates/project/agents/memory-instructions.md +222 -493
- package/templates/project/agents/tools-instructions.md +7 -0
- package/templates/editor/.github/skills/zen-memory/scripts/check-instructions.sh +0 -116
package/README.md
CHANGED
|
@@ -494,6 +494,8 @@ you want to validate a project's execution environment before a run.
|
|
|
494
494
|
|
|
495
495
|
```sh
|
|
496
496
|
zen sandbox status # engine, project image and active containers
|
|
497
|
+
zen sandbox top # memory and cpu, live: this computer, the machine, each container
|
|
498
|
+
zen sandbox top --once # the same, printed once (also with --json or when piped)
|
|
497
499
|
zen sandbox up # prepare the execution environment for this project
|
|
498
500
|
zen sandbox pull # pull or build this project's image only
|
|
499
501
|
zen sandbox status --project my-app # inspect a named project from anywhere
|
|
@@ -507,6 +509,13 @@ zen sandbox clean --idle # only what no live run is using
|
|
|
507
509
|
`disk` are machine-wide operations, so use them when you mean to inspect or
|
|
508
510
|
remove resources beyond the current project.
|
|
509
511
|
|
|
512
|
+
`top` is machine-wide too. Next to every figure it says in a few words what the
|
|
513
|
+
figure means; `--brief` leaves those notes out, `--interval <seconds>` sets how
|
|
514
|
+
often the live screen refreshes. On macOS it answers the usual question - why
|
|
515
|
+
Activity Monitor shows "Virtual Machine Service for vfkit" holding many
|
|
516
|
+
gigabytes - by setting what macOS counts for the machine against what is in use
|
|
517
|
+
inside it, and naming the command that gives the difference back.
|
|
518
|
+
|
|
510
519
|
## Sharing a project
|
|
511
520
|
|
|
512
521
|
A project is a directory, so sending one somewhere else is a zip - except that
|
|
@@ -554,6 +563,7 @@ The commands to run next are printed for you to read and type.
|
|
|
554
563
|
| `run` | Runs the project - the TUI on a terminal, a single answer otherwise. |
|
|
555
564
|
| `meta` | Runs the meta agent over the project - the one that edits it. |
|
|
556
565
|
| `inspect` | Opens or rebuilds a run's `report.html`. |
|
|
566
|
+
| `view` | Shows a folder in the browser, redrawn live as its files change. |
|
|
557
567
|
| `memory` | What the agents remember - size, listing, one node, or a whole page. |
|
|
558
568
|
| `check` | Validates the project and every file it names, and asks the models. |
|
|
559
569
|
| `export` | Writes the project to a shareable zip archive. |
|
|
@@ -734,6 +744,7 @@ See the `zen-dataset` skill a project carries in `.github/skills/`.
|
|
|
734
744
|
zen meta finetune start -N 4 # 4 cases at a time; resumes if stopped
|
|
735
745
|
zen meta finetune # where it stands
|
|
736
746
|
zen meta finetune stop # finish the steps in flight, then stop
|
|
747
|
+
zen meta finetune serve # the pages live in a browser, while it is stopped
|
|
737
748
|
zen meta finetune session plan-day # resume that case's analyze session
|
|
738
749
|
```
|
|
739
750
|
|
|
@@ -743,7 +754,8 @@ applied in batches between runs, and every apply is a new, diffable version of
|
|
|
743
754
|
the prose. Follow it in `finetune/STATUS.md`, whose summary shows whether the
|
|
744
755
|
fixes and the memory are making runs cheaper; each case has its own
|
|
745
756
|
`finetune/cases/<id>/FEEDBACK.md` with its latest analysis and the metrics of
|
|
746
|
-
every try.
|
|
757
|
+
every try. `start` prints a local address that shows those pages in a browser,
|
|
758
|
+
with their diagrams drawn, updated in place as the tuning writes them.
|
|
747
759
|
|
|
748
760
|
## Credentials
|
|
749
761
|
|
package/dist/catalog.d.ts
CHANGED
|
@@ -103,6 +103,26 @@ export interface CatalogOptions {
|
|
|
103
103
|
* model list to four rows.
|
|
104
104
|
*/
|
|
105
105
|
export declare function loadCatalog(provider: Provider, opts?: CatalogOptions): Promise<Catalog>;
|
|
106
|
+
/** A per-token USD price as dollars per million tokens, the unit price lists use. */
|
|
107
|
+
export declare function perMillion(price: string | undefined): string;
|
|
108
|
+
/** What an OpenRouter account has left to spend, as far as its key may see. */
|
|
109
|
+
export interface Balance {
|
|
110
|
+
/** credits bought minus credits used; absent when the key may not read the account */
|
|
111
|
+
credits?: number;
|
|
112
|
+
/** the key's own spending cap, when one is set */
|
|
113
|
+
cap?: {
|
|
114
|
+
limit: number;
|
|
115
|
+
remaining: number;
|
|
116
|
+
reset?: string;
|
|
117
|
+
};
|
|
118
|
+
/** requests to `:free` models left today */
|
|
119
|
+
free?: {
|
|
120
|
+
used: number;
|
|
121
|
+
limit: number;
|
|
122
|
+
remaining: number;
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
export declare function routerBalance(): Promise<Balance>;
|
|
106
126
|
/** Every provider asked at once — they are independent and mostly latency. */
|
|
107
127
|
export declare function loadCatalogs(providers: readonly Provider[], opts?: CatalogOptions): Promise<Catalog[]>;
|
|
108
128
|
export interface Filters {
|
package/dist/catalog.js
CHANGED
|
@@ -477,6 +477,66 @@ export async function loadCatalog(provider, opts = {}) {
|
|
|
477
477
|
fetchedAt: new Date(0).toISOString(),
|
|
478
478
|
};
|
|
479
479
|
}
|
|
480
|
+
// ---------------------------------------------------------------------------
|
|
481
|
+
// Prices and balance
|
|
482
|
+
// ---------------------------------------------------------------------------
|
|
483
|
+
/** A per-token USD price as dollars per million tokens, the unit price lists use. */
|
|
484
|
+
export function perMillion(price) {
|
|
485
|
+
if (price === undefined || price === '') {
|
|
486
|
+
return '';
|
|
487
|
+
}
|
|
488
|
+
const n = Number(price) * 1_000_000;
|
|
489
|
+
if (!Number.isFinite(n)) {
|
|
490
|
+
return '';
|
|
491
|
+
}
|
|
492
|
+
// OpenRouter prices its routers (`openrouter/auto`) as "-1": it depends on the pick.
|
|
493
|
+
if (n < 0) {
|
|
494
|
+
return 'varies';
|
|
495
|
+
}
|
|
496
|
+
if (n === 0) {
|
|
497
|
+
return 'free';
|
|
498
|
+
}
|
|
499
|
+
if (n >= 1) {
|
|
500
|
+
return `$${n.toFixed(2)}`;
|
|
501
|
+
}
|
|
502
|
+
// Two significant digits for fractions of a dollar, but never fewer than cents.
|
|
503
|
+
const short = String(Number(n.toPrecision(2)));
|
|
504
|
+
return `$${(short.split('.')[1]?.length ?? 0) < 2 ? n.toFixed(2) : short}`;
|
|
505
|
+
}
|
|
506
|
+
export async function routerBalance() {
|
|
507
|
+
const base = (process.env.OPENROUTER_BASE_URL ?? 'https://openrouter.ai/api/v1').replace(/\/+$/, '');
|
|
508
|
+
const get = async (path) => {
|
|
509
|
+
const res = await fetch(`${base}${path}`, {
|
|
510
|
+
headers: { Authorization: `Bearer ${process.env.OPENROUTER_API_KEY ?? ''}` },
|
|
511
|
+
signal: AbortSignal.timeout(15_000),
|
|
512
|
+
});
|
|
513
|
+
if (!res.ok) {
|
|
514
|
+
throw Object.assign(new Error(`${path} answered ${res.status}`), {
|
|
515
|
+
status: res.status,
|
|
516
|
+
});
|
|
517
|
+
}
|
|
518
|
+
return (await res.json()).data;
|
|
519
|
+
};
|
|
520
|
+
// Raw rather than the SDK: its schema drops `free_model_daily_requests`.
|
|
521
|
+
const [key, credits] = await Promise.all([
|
|
522
|
+
get('/key'),
|
|
523
|
+
// Documented as management-key only, though ordinary keys are answered too.
|
|
524
|
+
get('/credits').catch(() => undefined),
|
|
525
|
+
]);
|
|
526
|
+
return {
|
|
527
|
+
...(credits ? { credits: credits.total_credits - credits.total_usage } : {}),
|
|
528
|
+
...(key.limit !== null && key.limit_remaining !== null
|
|
529
|
+
? {
|
|
530
|
+
cap: {
|
|
531
|
+
limit: key.limit,
|
|
532
|
+
remaining: key.limit_remaining,
|
|
533
|
+
...(key.limit_reset ? { reset: key.limit_reset } : {}),
|
|
534
|
+
},
|
|
535
|
+
}
|
|
536
|
+
: {}),
|
|
537
|
+
...(key.free_model_daily_requests ? { free: key.free_model_daily_requests } : {}),
|
|
538
|
+
};
|
|
539
|
+
}
|
|
480
540
|
/** Every provider asked at once — they are independent and mostly latency. */
|
|
481
541
|
export async function loadCatalogs(providers, opts = {}) {
|
|
482
542
|
return Promise.all(providers.map((p) => loadCatalog(p, opts)));
|
package/dist/commands/index.js
CHANGED
|
@@ -13,6 +13,7 @@ import { open } from "./open.js";
|
|
|
13
13
|
import { run } from "./run.js";
|
|
14
14
|
import { sandbox } from "./sandbox.js";
|
|
15
15
|
import { version } from "./version.js";
|
|
16
|
+
import { view } from "./view.js";
|
|
16
17
|
/**
|
|
17
18
|
* Insertion order is the order help prints in, and it is deliberate: the four
|
|
18
19
|
* a new user needs first, then the credentials-and-models pair, then the two
|
|
@@ -32,6 +33,7 @@ export const COMMANDS = {
|
|
|
32
33
|
models,
|
|
33
34
|
check,
|
|
34
35
|
inspect,
|
|
36
|
+
view,
|
|
35
37
|
export: pack,
|
|
36
38
|
import: unpack,
|
|
37
39
|
memory,
|
|
@@ -11,15 +11,19 @@ interface Flags {
|
|
|
11
11
|
part?: string[];
|
|
12
12
|
'question-file'?: string;
|
|
13
13
|
full?: boolean;
|
|
14
|
+
raw?: boolean;
|
|
14
15
|
open?: boolean;
|
|
15
16
|
rebuild?: boolean;
|
|
16
17
|
'no-timing'?: boolean;
|
|
17
18
|
'no-style'?: boolean;
|
|
18
19
|
}
|
|
19
20
|
export declare const inspect: Command;
|
|
21
|
+
/** The `--part` names that match no part of any of these nodes. */
|
|
22
|
+
export declare function missingParts(found: readonly NodeDetail[], want: readonly string[]): string[];
|
|
20
23
|
/**
|
|
21
24
|
* Which parts print in full: everything `--part` names, or everything but
|
|
22
|
-
*
|
|
25
|
+
* the bulky ones. A prefix matches, so `--part call` catches `call run_command (…)`.
|
|
26
|
+
* A name that matches nothing is an error only when no name matches at all.
|
|
23
27
|
*/
|
|
24
28
|
export declare function wholeParts(found: readonly NodeDetail[], values: {
|
|
25
29
|
part?: string[];
|
package/dist/commands/inspect.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import { assertState, buildRunReport, FilePayloadStore, memoryDir, MemoryStore, PayloadResolver, projectRegistry, readProjectConfig, renderReportHtml, text, } from '@zenera/neo';
|
|
1
|
+
import { assertState, buildRunReport, FilePayloadStore, memoryDir, MemoryStore, PayloadResolver, projectRegistry, readProjectConfig, renderReportHtml, SANDBOX_MOUNT, text, } from '@zenera/neo';
|
|
2
2
|
import { spawn } from 'node:child_process';
|
|
3
3
|
import { existsSync } from 'node:fs';
|
|
4
4
|
import { readFile, writeFile } from 'node:fs/promises';
|
|
5
5
|
import { join, resolve } from 'node:path';
|
|
6
|
+
import { pathToFileURL } from 'node:url';
|
|
6
7
|
import { parse } from "../args.js";
|
|
7
8
|
import { buildDiagnostic, parseRecordedRequest, pickModel } from "../ask.js";
|
|
8
9
|
import { loadProjectEnv } from "../env.js";
|
|
@@ -59,6 +60,7 @@ export const inspect = {
|
|
|
59
60
|
' --question-file <path> Read the `ask` question from a file; `-` is stdin.',
|
|
60
61
|
' --part <name> Print this part of a node in full. Repeatable.',
|
|
61
62
|
' --full Print every part, the recorded `request` included.',
|
|
63
|
+
' --raw With one node and one --part: that part alone, unframed.',
|
|
62
64
|
' --rebuild Build report.html again from the run state.',
|
|
63
65
|
' --open Open the report in a browser.',
|
|
64
66
|
' --no-timing Leave the clock off the graph.',
|
|
@@ -84,6 +86,14 @@ export const inspect = {
|
|
|
84
86
|
'',
|
|
85
87
|
' zen inspect node n11 --part thinking --part text --dir <run dir>',
|
|
86
88
|
'',
|
|
89
|
+
'An `llm_call` also carries two parts read out of its request, printed',
|
|
90
|
+
'only when named: `tools`, the tool names that call was offered, and',
|
|
91
|
+
'`messages`, the conversation it saw, one clipped line per message.',
|
|
92
|
+
'`--raw` prints one part with nothing around it, for a pipe:',
|
|
93
|
+
'',
|
|
94
|
+
' zen inspect node n11 --part tools --dir <run dir>',
|
|
95
|
+
" zen inspect node n11 --part request --raw --dir <run dir> | jq '.messages'",
|
|
96
|
+
'',
|
|
87
97
|
'`ask` replays one `llm_call` — its system prompt, its messages, its tool',
|
|
88
98
|
'schemas — to a model, with your question on the end and tool calling off.',
|
|
89
99
|
'It is told the run is over and that it may quote its own instructions, so',
|
|
@@ -122,6 +132,7 @@ export const inspect = {
|
|
|
122
132
|
part: { type: 'string', multiple: true },
|
|
123
133
|
'question-file': { type: 'string' },
|
|
124
134
|
full: { type: 'boolean' },
|
|
135
|
+
raw: { type: 'boolean' },
|
|
125
136
|
open: { type: 'boolean' },
|
|
126
137
|
rebuild: { type: 'boolean' },
|
|
127
138
|
'no-timing': { type: 'boolean' },
|
|
@@ -238,18 +249,42 @@ async function nodes(at, values, ids, asJson) {
|
|
|
238
249
|
return;
|
|
239
250
|
}
|
|
240
251
|
const whole = wholeParts(found, values);
|
|
241
|
-
|
|
252
|
+
if (values.raw) {
|
|
253
|
+
const [node] = found;
|
|
254
|
+
const hits = node?.parts.filter((p) => whole(p.name)) ?? [];
|
|
255
|
+
if (found.length !== 1 || values.part?.length !== 1 || hits.length !== 1) {
|
|
256
|
+
throw usageError('--raw prints one part of one node', hits.length > 1
|
|
257
|
+
? `name one: ${hits.map((p) => `--part ${flag(p.name)}`).join(', ')}`
|
|
258
|
+
: 'zen inspect node n11 --part request --raw --dir <run dir>');
|
|
259
|
+
}
|
|
260
|
+
const { text } = hits[0];
|
|
261
|
+
process.stdout.write(text.endsWith('\n') ? text : `${text}\n`);
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
writeAll([
|
|
265
|
+
...preamble(run.id, found, whole, trace.entries.length),
|
|
266
|
+
...missingParts(found, values.part ?? []).map((name) => `# no part starts with "${name}" in these nodes`),
|
|
267
|
+
]);
|
|
242
268
|
for (const node of found) {
|
|
243
269
|
write(renderNode(node, whole));
|
|
244
270
|
}
|
|
245
|
-
|
|
246
|
-
|
|
271
|
+
// A model reading this already has the commands; only a person needs reminding.
|
|
272
|
+
if (process.stderr.isTTY) {
|
|
273
|
+
note(dim(`the rest of the run: ${cyan(`zen inspect graph --dir ${run.dir}`)}`));
|
|
274
|
+
note(dim(`why it did this: ${cyan(`zen inspect ask ${found[0]?.id ?? 'n1'} "…" --dir ${run.dir}`)}`));
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
/** Parts `node` names with their size instead of printing, unless asked for. */
|
|
278
|
+
const BULKY = new Set(['request', 'messages', 'tools']);
|
|
279
|
+
/** The `--part` names that match no part of any of these nodes. */
|
|
280
|
+
export function missingParts(found, want) {
|
|
281
|
+
const names = found.flatMap((n) => n.parts.map((p) => p.name));
|
|
282
|
+
return want.filter((asked) => !names.some((name) => name.startsWith(asked)));
|
|
247
283
|
}
|
|
248
|
-
/** The part `--part` leaves out unless it is asked for by name. */
|
|
249
|
-
const BULKY = 'request';
|
|
250
284
|
/**
|
|
251
285
|
* Which parts print in full: everything `--part` names, or everything but
|
|
252
|
-
*
|
|
286
|
+
* the bulky ones. A prefix matches, so `--part call` catches `call run_command (…)`.
|
|
287
|
+
* A name that matches nothing is an error only when no name matches at all.
|
|
253
288
|
*/
|
|
254
289
|
export function wholeParts(found, values) {
|
|
255
290
|
if (values.full) {
|
|
@@ -257,13 +292,12 @@ export function wholeParts(found, values) {
|
|
|
257
292
|
}
|
|
258
293
|
const want = values.part ?? [];
|
|
259
294
|
if (!want.length) {
|
|
260
|
-
return (name) => name
|
|
295
|
+
return (name) => !BULKY.has(name);
|
|
261
296
|
}
|
|
262
|
-
const
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
}
|
|
297
|
+
const missing = missingParts(found, want);
|
|
298
|
+
if (missing.length === want.length) {
|
|
299
|
+
const names = [...new Set(found.flatMap((n) => n.parts.map((p) => p.name)))];
|
|
300
|
+
throw usageError(`no part starts with "${want[0]}" in ${found.length === 1 ? found[0]?.id : 'these nodes'}`, names.length ? `they carry: ${names.join(', ')}` : 'they carry no payloads');
|
|
267
301
|
}
|
|
268
302
|
return (name) => want.some((asked) => name.startsWith(asked));
|
|
269
303
|
}
|
|
@@ -618,7 +652,13 @@ async function pickRun(session, asked, asking) {
|
|
|
618
652
|
async function rebuild(session, run, store) {
|
|
619
653
|
const state = await readState(run);
|
|
620
654
|
const payloads = new PayloadResolver(new FilePayloadStore({ dir: session.blobs, id: 'file' }));
|
|
621
|
-
const
|
|
655
|
+
const { workspace } = await readRunMeta(run);
|
|
656
|
+
const report = await buildRunReport(state, payloads, {
|
|
657
|
+
title: run.id,
|
|
658
|
+
memory: store,
|
|
659
|
+
// The run never recorded a `workdir:`, so the default name is the guess.
|
|
660
|
+
mounts: workspace ? [{ at: SANDBOX_MOUNT, href: pathToFileURL(workspace).href }] : [],
|
|
661
|
+
});
|
|
622
662
|
await writeFile(run.report, renderReportHtml(report), 'utf8');
|
|
623
663
|
}
|
|
624
664
|
/**
|
package/dist/commands/memory.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { ALL_AGENTS, buildMemoryReport, createEmbedder, diffMemories, GREP_FIELDS, grepMemory, hostPath, memoryDir, MemoryError, MemoryIndex, MemoryStore, MergeConflicts, mergeMemories, readProjectConfig, renderMemoryHtml, renderRecollection, } from '@zenera/neo';
|
|
2
2
|
import { spawn } from 'node:child_process';
|
|
3
|
-
import { existsSync } from 'node:fs';
|
|
3
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
4
4
|
import { writeFile } from 'node:fs/promises';
|
|
5
5
|
import { basename, isAbsolute, join, resolve } from 'node:path';
|
|
6
6
|
import { parse } from "../args.js";
|
|
@@ -49,6 +49,7 @@ export const memory = {
|
|
|
49
49
|
'',
|
|
50
50
|
' --project <name|dir> Which project. Defaults to the one you are in.',
|
|
51
51
|
' --dir <dir> Read this memory directory instead of the project’s.',
|
|
52
|
+
' Run from inside a memory directory, that one is the default.',
|
|
52
53
|
' --kind <name> Only this kind of node.',
|
|
53
54
|
' --audience <name> Only nodes committed under this label.',
|
|
54
55
|
' --files Only nodes that remember a file.',
|
|
@@ -198,6 +199,11 @@ export const memory = {
|
|
|
198
199
|
*/
|
|
199
200
|
async function open(cwd, want, at, opts = {}) {
|
|
200
201
|
const { create = false, lock = true } = opts;
|
|
202
|
+
// Standing inside a memory (a merged or batch copy) means that memory, not
|
|
203
|
+
// the project's own one a few directories up.
|
|
204
|
+
if (!at && !want && isMemoryDir(cwd)) {
|
|
205
|
+
at = cwd;
|
|
206
|
+
}
|
|
201
207
|
if (at) {
|
|
202
208
|
const dir = resolve(cwd, at);
|
|
203
209
|
if (!create && !existsSync(join(dir, 'manifest.json'))) {
|
|
@@ -221,6 +227,19 @@ async function open(cwd, want, at, opts = {}) {
|
|
|
221
227
|
at: { root: project.dir, config },
|
|
222
228
|
};
|
|
223
229
|
}
|
|
230
|
+
/** A rag index also has a manifest and a graph.json; only a memory's manifest counts nodes and edges. */
|
|
231
|
+
function isMemoryDir(dir) {
|
|
232
|
+
if (!existsSync(join(dir, 'graph.json'))) {
|
|
233
|
+
return false;
|
|
234
|
+
}
|
|
235
|
+
try {
|
|
236
|
+
const m = JSON.parse(readFileSync(join(dir, 'manifest.json'), 'utf8'));
|
|
237
|
+
return typeof m?.nodes === 'number' && typeof m?.edges === 'number';
|
|
238
|
+
}
|
|
239
|
+
catch {
|
|
240
|
+
return false;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
224
243
|
// ---------------------------------------------------------------------------
|
|
225
244
|
// stats
|
|
226
245
|
// ---------------------------------------------------------------------------
|
|
@@ -753,10 +772,16 @@ async function write_(o, rest, flags, cwd, asJson) {
|
|
|
753
772
|
const report = { ...built, nodes: built.nodes.map((n) => onHost(o, n)) };
|
|
754
773
|
await writeFile(target, renderMemoryHtml(report), 'utf8');
|
|
755
774
|
if (asJson) {
|
|
756
|
-
json({
|
|
775
|
+
json({
|
|
776
|
+
path: target,
|
|
777
|
+
from: o.dir,
|
|
778
|
+
nodes: report.nodes.length,
|
|
779
|
+
edges: report.edges.length,
|
|
780
|
+
});
|
|
757
781
|
}
|
|
758
782
|
else {
|
|
759
|
-
note(`${green('wrote')} ${target} ${dim(`· ${count(report.nodes.length, 'node')}, ${count(report.edges.length, 'edge')}`
|
|
783
|
+
note(`${green('wrote')} ${target} ${dim(`· ${count(report.nodes.length, 'node')}, ${count(report.edges.length, 'edge')}` +
|
|
784
|
+
` · from ${o.dir}`)}`);
|
|
760
785
|
}
|
|
761
786
|
if (flags.open) {
|
|
762
787
|
reveal(target);
|
package/dist/commands/meta.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { readProjectConfig } from '@zenera/neo';
|
|
2
2
|
import { spawn } from 'node:child_process';
|
|
3
3
|
import { randomUUID } from 'node:crypto';
|
|
4
|
-
import { existsSync } from 'node:fs';
|
|
4
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
5
5
|
import { join, resolve } from 'node:path';
|
|
6
6
|
import { setTimeout as sleep } from 'node:timers/promises';
|
|
7
7
|
import { parse } from "../args.js";
|
|
@@ -83,6 +83,7 @@ export const meta = {
|
|
|
83
83
|
' --project <name|dir> Which project to work in. The agent is rooted there.',
|
|
84
84
|
' --model <ref> Model for this run, e.g. vertex/gemini-3.8-flash.',
|
|
85
85
|
' --provider <name> Force the key to use. Default: the model ref says.',
|
|
86
|
+
' --prompt-file <file> Append this file to the prompt, after any words.',
|
|
86
87
|
' --agent <name> A .github/agents/<name>.agent.md.',
|
|
87
88
|
' --effort <level> none, minimal, low, medium, high, xhigh or max.',
|
|
88
89
|
' --allow-tool <list> Only these: read, shell(git:*), write(README.md).',
|
|
@@ -164,6 +165,7 @@ export const meta = {
|
|
|
164
165
|
provider: { type: 'string' },
|
|
165
166
|
model: { type: 'string' },
|
|
166
167
|
prompt: { type: 'string', short: 'p' },
|
|
168
|
+
'prompt-file': { type: 'string' },
|
|
167
169
|
agent: { type: 'string' },
|
|
168
170
|
effort: { type: 'string' },
|
|
169
171
|
share: { type: 'string' },
|
|
@@ -243,7 +245,9 @@ async function where(ctx, flag, positionals) {
|
|
|
243
245
|
function refresh(project) {
|
|
244
246
|
try {
|
|
245
247
|
const written = editorFiles(project.dir);
|
|
246
|
-
|
|
248
|
+
if (written.length) {
|
|
249
|
+
note(dim(`refreshed ${written.length} editor files in ${project.name}`));
|
|
250
|
+
}
|
|
247
251
|
}
|
|
248
252
|
catch (err) {
|
|
249
253
|
warn(`could not refresh the editor files — ${err.message}`);
|
|
@@ -282,7 +286,7 @@ async function stored(ctx, values, args) {
|
|
|
282
286
|
if (name) {
|
|
283
287
|
return await runPrompt(ctx, values, project, name, rest);
|
|
284
288
|
}
|
|
285
|
-
const question = (values.prompt ?? rest.join(' ')).trim() || (await readStdin());
|
|
289
|
+
const question = withFile(values, (values.prompt ?? rest.join(' ')).trim()) || (await readStdin());
|
|
286
290
|
if (question) {
|
|
287
291
|
return await go(ctx, values, project, question);
|
|
288
292
|
}
|
|
@@ -294,9 +298,24 @@ async function runPrompt(ctx, values, project, name, extra) {
|
|
|
294
298
|
if (found.description) {
|
|
295
299
|
note(dim(found.description));
|
|
296
300
|
}
|
|
297
|
-
const prompt = extra.length > 0 ? `${found.body}\n\n${extra.join(' ')}` : found.body;
|
|
301
|
+
const prompt = withFile(values, extra.length > 0 ? `${found.body}\n\n${extra.join(' ')}` : found.body);
|
|
298
302
|
await go(ctx, values, project, prompt, name.replace(/^\//, ''));
|
|
299
303
|
}
|
|
304
|
+
/** The prompt with `--prompt-file` after it: text too long, or too awkward to quote, for a word. */
|
|
305
|
+
function withFile(values, prompt) {
|
|
306
|
+
const file = values['prompt-file'];
|
|
307
|
+
if (!file) {
|
|
308
|
+
return prompt;
|
|
309
|
+
}
|
|
310
|
+
let text;
|
|
311
|
+
try {
|
|
312
|
+
text = readFileSync(resolve(file), 'utf8').trim();
|
|
313
|
+
}
|
|
314
|
+
catch {
|
|
315
|
+
throw usageError(`no prompt file at ${file}`, 'name a file that exists');
|
|
316
|
+
}
|
|
317
|
+
return [prompt, text].filter(Boolean).join('\n\n');
|
|
318
|
+
}
|
|
300
319
|
async function pickPrompt(ctx, project) {
|
|
301
320
|
const names = await listPrompts(project.dir);
|
|
302
321
|
if (names.length === 0) {
|
|
@@ -348,7 +367,7 @@ async function resumeRun(ctx, values, args) {
|
|
|
348
367
|
if (!values['no-refresh']) {
|
|
349
368
|
refresh(project);
|
|
350
369
|
}
|
|
351
|
-
const message = (values.prompt ?? words.join(' ')).trim() || RESUME_PROMPT;
|
|
370
|
+
const message = withFile(values, (values.prompt ?? words.join(' ')).trim()) || RESUME_PROMPT;
|
|
352
371
|
await go(ctx, { ...values, resume: session, continue: false }, project, message);
|
|
353
372
|
}
|
|
354
373
|
// ---------------------------------------------------------------------------
|
package/dist/commands/models.js
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
// ---------------------------------------------------------------------------
|
|
14
14
|
import { createEmbedder, ModelRegistry } from '@zenera/neo';
|
|
15
15
|
import { parse } from "../args.js";
|
|
16
|
-
import { byGroup, groupOf, isPartner, loadCatalog, loadCatalogs, matches, PREFERRED, } from "../catalog.js";
|
|
16
|
+
import { byGroup, groupOf, isPartner, loadCatalog, loadCatalogs, matches, PREFERRED, routerBalance, } from "../catalog.js";
|
|
17
17
|
import { ensureHome } from "../home.js";
|
|
18
18
|
import { envNames, form, isProvider, KeyStore, PROVIDERS } from "../keys.js";
|
|
19
19
|
import { probeChat, probeModel } from "../liveness.js";
|
|
@@ -589,6 +589,7 @@ const browse = async (ctx, args) => {
|
|
|
589
589
|
initial,
|
|
590
590
|
theme: values.theme,
|
|
591
591
|
load: (provider, refresh) => loadCatalog(provider, { refresh: refresh || values.refresh }),
|
|
592
|
+
balance: async (provider) => provider === 'openrouter' && source(where, provider) ? routerBalance() : undefined,
|
|
592
593
|
test: async (entry) => {
|
|
593
594
|
const role = entry.roles.includes('chat')
|
|
594
595
|
? 'chat'
|
package/dist/commands/sandbox.js
CHANGED
|
@@ -3,8 +3,14 @@ import { parse } from "../args.js";
|
|
|
3
3
|
import { resolveBuild } from "../image.js";
|
|
4
4
|
import { engineDisk, ensurePodmanReady, idleContainers, ownedContainers, podmanStatus, removeContainers, sessionOwners, } from "../podman.js";
|
|
5
5
|
import { current as currentProject, dirSize, isProjectDir, open as openProject, Registry, sessionIds, } from "../projects.js";
|
|
6
|
-
import {
|
|
7
|
-
|
|
6
|
+
import { layout, ResourceSampler } from "../resources.js";
|
|
7
|
+
import { ago, bold, bytes, count, cyan, dim, green, json, note, red, table, usageError, write, writeAll, yellow, } from "../term.js";
|
|
8
|
+
const USAGE = 'zen sandbox [status|top|up|pull|clean|disk] [options]';
|
|
9
|
+
const SUBCOMMANDS = ['status', 'top', 'up', 'pull', 'clean', 'disk'];
|
|
10
|
+
/** `top` refreshes this often unless told otherwise. */
|
|
11
|
+
const DEFAULT_INTERVAL_S = 2;
|
|
12
|
+
/** A one-shot report still needs two readings for a cpu figure. */
|
|
13
|
+
const ONCE_INTERVAL_MS = 1000;
|
|
8
14
|
/** Enough to see the pattern; the rest are a number. */
|
|
9
15
|
const LISTED = 6;
|
|
10
16
|
/** Under the labels, which is where the eye already is. */
|
|
@@ -28,6 +34,8 @@ export const sandbox = {
|
|
|
28
34
|
banner: { head: 'Zenera', accent: 'Sandbox', subtitle: 'Container Isolation', hue: 'azure' },
|
|
29
35
|
details: [
|
|
30
36
|
' status What is installed, running and pulled. Changes nothing.',
|
|
37
|
+
' top Memory and cpu, live: this computer, the machine, each',
|
|
38
|
+
' container — with a note on what every figure means.',
|
|
31
39
|
' up Install if asked, start the machine, pull or build the image.',
|
|
32
40
|
' pull Just the image: pulled, or built from the project\u2019s Dockerfile.',
|
|
33
41
|
' clean Remove every container this CLI created.',
|
|
@@ -37,6 +45,10 @@ export const sandbox = {
|
|
|
37
45
|
' --image <ref> Use this image instead of the project\u2019s.',
|
|
38
46
|
' --idle With clean: only what no live run is using — stopped ones,',
|
|
39
47
|
' and running ones whose run was killed before it cleaned up.',
|
|
48
|
+
' --once With top: measure once and print, no live screen. Also',
|
|
49
|
+
' what happens with --json or when output is not a terminal.',
|
|
50
|
+
' --interval <seconds> With top: how often the live screen refreshes (default 2).',
|
|
51
|
+
' --brief With top: the figures without the notes explaining them.',
|
|
40
52
|
'',
|
|
41
53
|
'Without --project this is the project you are standing in, and standing',
|
|
42
54
|
'nowhere is an answer: `status` then reports the engine and every container',
|
|
@@ -51,9 +63,13 @@ export const sandbox = {
|
|
|
51
63
|
image: { type: 'string' },
|
|
52
64
|
stopped: { type: 'boolean' },
|
|
53
65
|
idle: { type: 'boolean' },
|
|
66
|
+
once: { type: 'boolean' },
|
|
67
|
+
interval: { type: 'string' },
|
|
68
|
+
brief: { type: 'boolean' },
|
|
69
|
+
theme: { type: 'string' },
|
|
54
70
|
}, USAGE);
|
|
55
71
|
const what = positionals[0] ?? 'status';
|
|
56
|
-
if (!
|
|
72
|
+
if (!SUBCOMMANDS.includes(what)) {
|
|
57
73
|
throw usageError(`unknown subcommand: ${what}`, USAGE);
|
|
58
74
|
}
|
|
59
75
|
if (positionals.length > 1) {
|
|
@@ -75,6 +91,8 @@ export const sandbox = {
|
|
|
75
91
|
throw usageError('no project here, so there is no image to pull', 'name one with --project, or an image with --image');
|
|
76
92
|
}
|
|
77
93
|
return up(image, build, true, ctx.json, true);
|
|
94
|
+
case 'top':
|
|
95
|
+
return top(values, ctx.json);
|
|
78
96
|
case 'clean':
|
|
79
97
|
return clean(ctx.json, Boolean(values.idle || values.stopped));
|
|
80
98
|
case 'disk':
|
|
@@ -167,6 +185,58 @@ async function status(project, image, asJson) {
|
|
|
167
185
|
note('');
|
|
168
186
|
note(dim('run `zen sandbox up` to fix what can be fixed.'));
|
|
169
187
|
}
|
|
188
|
+
else {
|
|
189
|
+
note('');
|
|
190
|
+
note(dim('memory and cpu, live: zen sandbox top'));
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
// ---------------------------------------------------------------------------
|
|
194
|
+
// Memory and cpu
|
|
195
|
+
//
|
|
196
|
+
// A live screen when a person is watching, one report when not: the same
|
|
197
|
+
// layout either way, so what a script reads is what the screen showed.
|
|
198
|
+
// ---------------------------------------------------------------------------
|
|
199
|
+
async function top(values, asJson) {
|
|
200
|
+
const interval = values.interval === undefined ? DEFAULT_INTERVAL_S : Number(values.interval);
|
|
201
|
+
if (!Number.isFinite(interval) || interval < 0.5 || interval > 3600) {
|
|
202
|
+
throw usageError(`--interval must be a number of seconds from 0.5 to 3600, not ${values.interval}`, USAGE);
|
|
203
|
+
}
|
|
204
|
+
const sampler = new ResourceSampler();
|
|
205
|
+
const live = !asJson && !values.once && Boolean(process.stdout.isTTY) && Boolean(process.stdin.isTTY);
|
|
206
|
+
if (live) {
|
|
207
|
+
const { watch } = await import("../tui/sandbox.js");
|
|
208
|
+
await watch({
|
|
209
|
+
sampler,
|
|
210
|
+
intervalMs: interval * 1000,
|
|
211
|
+
explain: !values.brief,
|
|
212
|
+
theme: values.theme,
|
|
213
|
+
});
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
// The first reading has nothing to compare against, so its cpu figures are
|
|
217
|
+
// lifetime averages; the second one is over a second of real time.
|
|
218
|
+
await sampler.next();
|
|
219
|
+
await new Promise((done) => setTimeout(done, ONCE_INTERVAL_MS));
|
|
220
|
+
const view = await sampler.next();
|
|
221
|
+
if (asJson) {
|
|
222
|
+
json(view);
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
const width = process.stdout.columns || Number(process.env.COLUMNS) || 100;
|
|
226
|
+
writeAll(layout(view, { width, explain: !values.brief, sort: 'memory', wrapNotes: true }).map(ansi));
|
|
227
|
+
}
|
|
228
|
+
const PAINT = {
|
|
229
|
+
plain: (s) => s,
|
|
230
|
+
strong: bold,
|
|
231
|
+
heading: (s) => bold(cyan(s)),
|
|
232
|
+
dim,
|
|
233
|
+
good: green,
|
|
234
|
+
warn: yellow,
|
|
235
|
+
bad: red,
|
|
236
|
+
accent: cyan,
|
|
237
|
+
};
|
|
238
|
+
function ansi(line) {
|
|
239
|
+
return line.map((s) => PAINT[s.tone ?? 'plain'](s.text)).join('');
|
|
170
240
|
}
|
|
171
241
|
/**
|
|
172
242
|
* The hungriest container this project can start. An agent may override the
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Command } from '../command.ts';
|
|
2
|
+
/** A file inside the cwd is served from the cwd, so its links up the tree still resolve. */
|
|
3
|
+
export declare function placeOf(cwd: string, target: string): {
|
|
4
|
+
root: string;
|
|
5
|
+
start: string;
|
|
6
|
+
};
|
|
7
|
+
export declare const view: Command;
|
|
8
|
+
//# sourceMappingURL=view.d.ts.map
|