@zenera/cli 1.1.21 → 1.1.23
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 +40 -3
- package/dist/archive.d.ts +139 -0
- package/dist/archive.js +376 -0
- package/dist/banner.d.ts +33 -24
- package/dist/banner.js +25 -6
- package/dist/commands/check.js +1 -0
- package/dist/commands/export.d.ts +3 -0
- package/dist/commands/export.js +255 -0
- package/dist/commands/import.d.ts +3 -0
- package/dist/commands/import.js +173 -0
- package/dist/commands/index.d.ts +5 -1
- package/dist/commands/index.js +13 -1
- package/dist/commands/init.js +1 -0
- package/dist/commands/inspect.js +1 -0
- package/dist/commands/key.js +1 -0
- package/dist/commands/list.js +1 -0
- package/dist/commands/memory.js +118 -10
- package/dist/commands/open.js +1 -0
- package/dist/commands/run.js +3 -0
- package/dist/commands/sandbox.js +1 -0
- package/dist/commands/version.js +1 -0
- package/dist/lib.d.ts +2 -1
- package/dist/lib.js +2 -1
- package/dist/meta.d.ts +1 -1
- package/dist/meta.js +5 -28
- package/dist/resolve.js +19 -12
- package/dist/sandbox.js +1 -0
- package/dist/scaffold.d.ts +8 -0
- package/dist/scaffold.js +7 -2
- package/dist/term.d.ts +2 -0
- package/dist/term.js +2 -0
- package/dist/tui/markdown.d.ts +16 -0
- package/dist/tui/markdown.js +259 -0
- package/dist/tui/theme.js +1 -1
- package/dist/tui/wrap.d.ts +1 -1
- package/dist/validate.js +150 -4
- package/package.json +12 -6
- package/templates/editor/.github/copilot-instructions.md +62 -33
- package/templates/editor/.github/prompts/spec-sync-project.prompt.md +56 -3
- package/templates/editor/.github/skills/zen-cli/SKILL.md +21 -16
- package/templates/editor/.github/skills/zen-cli/references/check.md +6 -0
- package/templates/editor/.github/skills/zen-cli/references/export.md +107 -0
- package/templates/editor/.github/skills/zen-cli/references/memory.md +51 -13
- package/templates/editor/.github/skills/zen-cli/references/projects.md +1 -0
- package/templates/editor/.github/skills/zen-cli/references/rag.md +43 -3
- package/templates/editor/.github/skills/zen-cli/references/sandbox.md +30 -2
- package/templates/editor/.github/skills/zen-instructions/SKILL.md +232 -0
- package/templates/editor/.github/skills/zen-memory/SKILL.md +2 -1
- package/templates/editor/.github/skills/zen-memory-warmup/SKILL.md +684 -0
- package/templates/editor/.github/skills/zen-rag-docs/SKILL.md +56 -23
- package/templates/editor/.github/skills/zen-rag-schema/SKILL.md +55 -28
- package/templates/editor/.github/skills/zen-review/SKILL.md +17 -1
- package/templates/editor/.github/skills/zen-sandbox/SKILL.md +257 -0
- package/templates/editor/.github/skills/zen-spec-sync/SKILL.md +16 -3
- package/templates/editor/.vscode/agents.schema.json +8 -3
- package/templates/project/SPECIFICATION.md.tmpl +4 -1
- package/templates/project/agents/files-instructions.md +86 -0
- package/templates/project/agents/fork-instructions.md +17 -0
- package/templates/project/agents/memory-instructions.md +37 -0
- package/templates/project/agents.yaml.tmpl +3 -3
- package/templates/project/gitignore +12 -1
- package/templates/project/scripts/_setup.sh +68 -11
package/dist/commands/memory.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
import { ALL_AGENTS, buildMemoryReport, hostPath, memoryDir, MemoryIndex, MemoryStore, readProjectConfig, renderMemoryHtml, } from '@zenera/neo';
|
|
1
|
+
import { ALL_AGENTS, buildMemoryReport, hostPath, memoryDir, MemoryIndex, MemoryStore, MergeConflicts, mergeMemories, readProjectConfig, renderMemoryHtml, } from '@zenera/neo';
|
|
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 { basename, isAbsolute, join, resolve } from 'node:path';
|
|
5
|
+
import { basename, isAbsolute, join, relative, resolve } from 'node:path';
|
|
6
6
|
import { parse } from "../args.js";
|
|
7
7
|
import { project as resolveProject } from "../resolve.js";
|
|
8
|
-
import { ago, bold, bytes, confirm, count, cyan, dim, green, isInteractive, json, note, table, usageError, write, writeAll, yellow, } from "../term.js";
|
|
9
|
-
const USAGE = 'zen memory [stats|ls|show|export|forget] [args] [options]';
|
|
10
|
-
const SUBCOMMANDS = ['stats', 'ls', 'show', 'export', 'forget'];
|
|
8
|
+
import { ago, bold, bytes, CliError, confirm, count, cyan, dim, EXIT, green, isInteractive, json, note, table, usageError, write, writeAll, yellow, } from "../term.js";
|
|
9
|
+
const USAGE = 'zen memory [stats|ls|show|export|merge|forget] [args] [options]';
|
|
10
|
+
const SUBCOMMANDS = ['stats', 'ls', 'show', 'export', 'merge', 'forget'];
|
|
11
11
|
/** Enough to see the shape of it; the rest is a number. */
|
|
12
12
|
const LISTED = 30;
|
|
13
13
|
// ---------------------------------------------------------------------------
|
|
@@ -31,11 +31,13 @@ const LISTED = 30;
|
|
|
31
31
|
export const memory = {
|
|
32
32
|
summary: 'What the agents remember, and getting rid of it.',
|
|
33
33
|
usage: USAGE,
|
|
34
|
+
banner: { head: 'Zenera', accent: 'Memory', subtitle: 'Knowledge Graph', hue: 'purple' },
|
|
34
35
|
details: [
|
|
35
36
|
' stats Size, vocabulary, and whether it is embedded.',
|
|
36
37
|
' ls Nodes, newest first. Changes nothing.',
|
|
37
38
|
' show <id> One node in full, with what it links to.',
|
|
38
39
|
' export [file] The whole graph as one HTML page.',
|
|
40
|
+
' merge <dir...> Fold other memories into this one.',
|
|
39
41
|
' forget <id...> Remove nodes, their vectors and their files.',
|
|
40
42
|
'',
|
|
41
43
|
' --project <name|dir> Which project. Defaults to the one you are in.',
|
|
@@ -47,7 +49,10 @@ export const memory = {
|
|
|
47
49
|
' --limit <n> Rows to list. Default 30.',
|
|
48
50
|
' --out <file> Where `export` writes. Default memory.html.',
|
|
49
51
|
' --open Open the exported page.',
|
|
50
|
-
' --
|
|
52
|
+
' --dry-run Say what `merge` would do, and stop.',
|
|
53
|
+
' --no-dedupe Keep memories `merge` would otherwise fold together.',
|
|
54
|
+
' --force Let `merge` pick a winner where two copies disagree.',
|
|
55
|
+
' --yes Do not ask before removing or merging.',
|
|
51
56
|
'',
|
|
52
57
|
'Everything here reads the graph *unmasked* — every audience, including',
|
|
53
58
|
'what no agent can see. That is the point: when a mask is the thing',
|
|
@@ -56,6 +61,11 @@ export const memory = {
|
|
|
56
61
|
'`export` is the one to reach for. It writes a single self-contained',
|
|
57
62
|
'page: the node list on the left, the graph in the middle, and whatever',
|
|
58
63
|
'you click on the right, file contents and all.',
|
|
64
|
+
'',
|
|
65
|
+
'`merge` is for warming a memory in parallel. The lock is per directory,',
|
|
66
|
+
'so N runs write N memories; this folds them back into one:',
|
|
67
|
+
'',
|
|
68
|
+
' zen memory merge .tmp/warmup-*/memory',
|
|
59
69
|
],
|
|
60
70
|
run: async (ctx) => {
|
|
61
71
|
const { values, positionals } = parse(ctx.args, {
|
|
@@ -69,13 +79,18 @@ export const memory = {
|
|
|
69
79
|
out: { type: 'string' },
|
|
70
80
|
open: { type: 'boolean' },
|
|
71
81
|
yes: { type: 'boolean' },
|
|
82
|
+
force: { type: 'boolean' },
|
|
83
|
+
'dry-run': { type: 'boolean' },
|
|
84
|
+
'no-dedupe': { type: 'boolean' },
|
|
72
85
|
}, USAGE);
|
|
73
86
|
const what = positionals[0] ?? 'stats';
|
|
74
87
|
if (!SUBCOMMANDS.includes(what)) {
|
|
75
88
|
throw usageError(`unknown subcommand: ${what}`, USAGE);
|
|
76
89
|
}
|
|
77
90
|
const rest = positionals.slice(1);
|
|
78
|
-
|
|
91
|
+
// `merge` is the one subcommand that may write a memory into existence;
|
|
92
|
+
// every other one is an inspector and a missing manifest is a mistake.
|
|
93
|
+
const opened = await open(ctx.cwd, values.project, values.dir, what === 'merge');
|
|
79
94
|
try {
|
|
80
95
|
switch (what) {
|
|
81
96
|
case 'stats':
|
|
@@ -86,6 +101,8 @@ export const memory = {
|
|
|
86
101
|
return show(opened, rest, ctx.json);
|
|
87
102
|
case 'export':
|
|
88
103
|
return await write_(opened, rest, values, ctx.cwd, ctx.json);
|
|
104
|
+
case 'merge':
|
|
105
|
+
return await merge(opened, rest, values, ctx.cwd, ctx.json);
|
|
89
106
|
default:
|
|
90
107
|
return await forget(opened, rest, values, ctx.json);
|
|
91
108
|
}
|
|
@@ -104,11 +121,15 @@ export const memory = {
|
|
|
104
121
|
* A named directory is opened as it stands, project or no project: a graph
|
|
105
122
|
* copied out of a running session is the one most worth looking at, and it
|
|
106
123
|
* should not have to be given an `agents.yaml` first.
|
|
124
|
+
*
|
|
125
|
+
* `create` drops the requirement that anything be there yet, for `merge`,
|
|
126
|
+
* which has somewhere to put what it reads even when the destination is a name
|
|
127
|
+
* nothing has written to.
|
|
107
128
|
*/
|
|
108
|
-
async function open(cwd, want, at) {
|
|
129
|
+
async function open(cwd, want, at, create = false) {
|
|
109
130
|
if (at) {
|
|
110
131
|
const dir = resolve(cwd, at);
|
|
111
|
-
if (!existsSync(join(dir, 'manifest.json'))) {
|
|
132
|
+
if (!create && !existsSync(join(dir, 'manifest.json'))) {
|
|
112
133
|
throw usageError(`${dir} is not a memory`, 'no manifest.json in it');
|
|
113
134
|
}
|
|
114
135
|
return { store: await MemoryStore.open(dir), dir, project: basename(dir) };
|
|
@@ -119,7 +140,7 @@ async function open(cwd, want, at) {
|
|
|
119
140
|
if (!dir) {
|
|
120
141
|
throw usageError(`${project.name} has no memory`, 'give an agent `memory: true` in agents.yaml, or add a `memory:` block');
|
|
121
142
|
}
|
|
122
|
-
if (!existsSync(join(dir, 'manifest.json'))) {
|
|
143
|
+
if (!create && !existsSync(join(dir, 'manifest.json'))) {
|
|
123
144
|
throw usageError(`${project.name} has a memory configured but nothing in it yet`, 'it is written the first time an agent commits something');
|
|
124
145
|
}
|
|
125
146
|
return { store: await MemoryStore.open(dir), dir, project: project.name };
|
|
@@ -335,6 +356,88 @@ function reveal(target) {
|
|
|
335
356
|
spawn(command, [target], { stdio: 'ignore', detached: true }).unref();
|
|
336
357
|
}
|
|
337
358
|
// ---------------------------------------------------------------------------
|
|
359
|
+
// merge
|
|
360
|
+
// ---------------------------------------------------------------------------
|
|
361
|
+
/**
|
|
362
|
+
* Warming a memory is parallel work, and the lock is per directory, so N runs
|
|
363
|
+
* produce N memories rather than one. This is the other half of that: it folds
|
|
364
|
+
* them back together, offline and without a model.
|
|
365
|
+
*
|
|
366
|
+
* Divergence refuses rather than guesses. Two copies of the same memory that
|
|
367
|
+
* disagree are a question about which piece of work was right, and answering
|
|
368
|
+
* it silently is how a merge loses the answer.
|
|
369
|
+
*/
|
|
370
|
+
async function merge(o, dirs, flags, cwd, asJson) {
|
|
371
|
+
if (!dirs.length) {
|
|
372
|
+
throw usageError('merge takes at least one memory directory', 'zen memory merge <dir...> [--dir <into>]');
|
|
373
|
+
}
|
|
374
|
+
const dryRun = flags['dry-run'] === true;
|
|
375
|
+
const held = o.store.graph.order;
|
|
376
|
+
if (held && !dryRun && !flags.yes) {
|
|
377
|
+
if (!isInteractive()) {
|
|
378
|
+
throw usageError(`${o.project} already remembers ${count(held, 'node')}`, 'pass --yes, or --dry-run to see what would change first');
|
|
379
|
+
}
|
|
380
|
+
note(`${count(dirs.length, 'memory', 'memories')} into ${bold(o.dir)} ` +
|
|
381
|
+
dim(`· ${count(held, 'node')} already there`));
|
|
382
|
+
if (!(await confirm('Merge?'))) {
|
|
383
|
+
note('nothing merged');
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
let report;
|
|
388
|
+
try {
|
|
389
|
+
report = await mergeMemories(o.store, dirs.map((d) => resolve(cwd, d)), { dedupe: !flags['no-dedupe'], force: flags.force, dryRun });
|
|
390
|
+
}
|
|
391
|
+
catch (err) {
|
|
392
|
+
if (err instanceof MergeConflicts) {
|
|
393
|
+
throw diverged(err, cwd, asJson);
|
|
394
|
+
}
|
|
395
|
+
throw err;
|
|
396
|
+
}
|
|
397
|
+
if (asJson) {
|
|
398
|
+
json(report);
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
writeAll(table([
|
|
402
|
+
[dim('memory'), dim('nodes'), dim('new'), dim('shared'), dim('folded'), dim('files')],
|
|
403
|
+
...report.sources.map((s) => [
|
|
404
|
+
near(s.dir, cwd),
|
|
405
|
+
String(s.nodes),
|
|
406
|
+
s.added ? green(String(s.added)) : '0',
|
|
407
|
+
String(s.shared),
|
|
408
|
+
s.twins ? yellow(String(s.twins)) : '0',
|
|
409
|
+
String(s.files),
|
|
410
|
+
]),
|
|
411
|
+
]));
|
|
412
|
+
write();
|
|
413
|
+
const summary = [
|
|
414
|
+
count(report.added, 'node'),
|
|
415
|
+
`${count(report.edges, 'edge')}`,
|
|
416
|
+
`${report.twins} folded`,
|
|
417
|
+
].join(', ');
|
|
418
|
+
note(dryRun ? `${yellow('would add')} ${summary}` : `${green('merged')} ${summary}`);
|
|
419
|
+
}
|
|
420
|
+
/**
|
|
421
|
+
* A refusal with the ids to look at. It is deliberately not a summary: the
|
|
422
|
+
* point of stopping is that a person decides, and they cannot decide from a
|
|
423
|
+
* count.
|
|
424
|
+
*/
|
|
425
|
+
function diverged(err, cwd, asJson) {
|
|
426
|
+
if (asJson) {
|
|
427
|
+
json({ error: err.message, conflicts: err.conflicts });
|
|
428
|
+
}
|
|
429
|
+
else {
|
|
430
|
+
writeAll(table(err.conflicts.map((c) => [
|
|
431
|
+
cyan(c.id),
|
|
432
|
+
dim(`r${c.mine} ↔ r${c.theirs}`),
|
|
433
|
+
clip(c.text, 48),
|
|
434
|
+
dim(near(c.dir, cwd)),
|
|
435
|
+
])));
|
|
436
|
+
write();
|
|
437
|
+
}
|
|
438
|
+
return new CliError(err.message, EXIT.failed, err.hint);
|
|
439
|
+
}
|
|
440
|
+
// ---------------------------------------------------------------------------
|
|
338
441
|
// forget
|
|
339
442
|
// ---------------------------------------------------------------------------
|
|
340
443
|
/**
|
|
@@ -385,4 +488,9 @@ function clip(text, n) {
|
|
|
385
488
|
const one = text.replace(/\s+/g, ' ').trim();
|
|
386
489
|
return one.length > n ? one.slice(0, n - 1) + '…' : one;
|
|
387
490
|
}
|
|
491
|
+
/** A path as it was probably typed: relative when it is below here, absolute when it is not. */
|
|
492
|
+
function near(dir, cwd) {
|
|
493
|
+
const rel = relative(cwd, dir);
|
|
494
|
+
return rel && !rel.startsWith('..') ? rel : dir;
|
|
495
|
+
}
|
|
388
496
|
//# sourceMappingURL=memory.js.map
|
package/dist/commands/open.js
CHANGED
|
@@ -16,6 +16,7 @@ const USAGE = 'zen open [project] [--editor <cmd>] [--wait]';
|
|
|
16
16
|
export const open = {
|
|
17
17
|
summary: 'Open a project in your editor.',
|
|
18
18
|
usage: USAGE,
|
|
19
|
+
banner: { head: 'Zenera', accent: 'Open', subtitle: 'Workspace Studio', hue: 'teal' },
|
|
19
20
|
details: [
|
|
20
21
|
'Editor: --editor, then $ZENERA_EDITOR, then the editor this terminal',
|
|
21
22
|
'belongs to, then $VISUAL or $EDITOR, then a known editor on PATH or',
|
package/dist/commands/run.js
CHANGED
|
@@ -12,6 +12,7 @@ const USAGE = 'zen run [project] [prompt] [options]';
|
|
|
12
12
|
export const run = {
|
|
13
13
|
summary: 'Run the project — the TUI on a terminal, one shot otherwise.',
|
|
14
14
|
usage: USAGE,
|
|
15
|
+
banner: { head: 'Zenera', accent: 'Neo', subtitle: 'Agentic Runtime', hue: 'orange' },
|
|
15
16
|
details: [
|
|
16
17
|
'Arguments:',
|
|
17
18
|
' [project] Name of a project. Default: the one you are in.',
|
|
@@ -31,6 +32,7 @@ export const run = {
|
|
|
31
32
|
' --theme <dark|light> Colours for the TUI. Default: auto.',
|
|
32
33
|
' --out <file> Put the answer in this file instead of on screen.',
|
|
33
34
|
' --yes Answer yes to every question.',
|
|
35
|
+
' --json Print machine-readable JSON (session, run, mounts, output, etc...).',
|
|
34
36
|
'',
|
|
35
37
|
'The first word is the project when it names one, otherwise the prompt.',
|
|
36
38
|
'The answer goes to stdout and the progress to stderr, so `> out.md` keeps',
|
|
@@ -46,6 +48,7 @@ export const run = {
|
|
|
46
48
|
' git diff | zen run take the prompt from stdin',
|
|
47
49
|
' zen run "what changed?" > out.md redirect the answer into a file',
|
|
48
50
|
' zen run --out out.md "what changed?" write the answer to out.md only',
|
|
51
|
+
' zen run --json "what changed?" output JSON with workspace, run paths, state, etc...',
|
|
49
52
|
' zen run --read-only "what changed?" let it read but not write',
|
|
50
53
|
' zen run --memory ./mem remember into ./mem, not the project',
|
|
51
54
|
' zen run --new open the TUI in a new session',
|
package/dist/commands/sandbox.js
CHANGED
|
@@ -21,6 +21,7 @@ const INDENT = ' '.repeat(11);
|
|
|
21
21
|
export const sandbox = {
|
|
22
22
|
summary: 'Check and prepare the container command-line tools run in.',
|
|
23
23
|
usage: USAGE,
|
|
24
|
+
banner: { head: 'Zenera', accent: 'Sandbox', subtitle: 'Container Isolation', hue: 'azure' },
|
|
24
25
|
details: [
|
|
25
26
|
' status What is installed, running and pulled. Changes nothing.',
|
|
26
27
|
' up Install if asked, start the machine, pull or build the image.',
|
package/dist/commands/version.js
CHANGED
|
@@ -20,6 +20,7 @@ function libraryManifest() {
|
|
|
20
20
|
export const version = {
|
|
21
21
|
summary: 'Print the CLI, library and Node versions.',
|
|
22
22
|
usage: 'zen version',
|
|
23
|
+
banner: { head: 'Zenera', accent: 'Version', subtitle: 'Runtime & Build', hue: 'steel' },
|
|
23
24
|
run: async (ctx) => {
|
|
24
25
|
const versions = {
|
|
25
26
|
cli: await versionOf(cliManifest),
|
package/dist/lib.d.ts
CHANGED
|
@@ -7,5 +7,6 @@ export { assertPrivate, ensureDir, ensureHome, home, paths, readJson, writeJson
|
|
|
7
7
|
export { assertNotEmpty, assertOwner, assertUsable, describe, envNames, envOf, form, isOwner, isProvider, keyId, KeyStore, mask, OWNERS, parseRef, PROVIDERS, SERVICES, SHAPES, type CredentialForm, type KeyCheck, type KeyEntry, type KeyOwner, type Liveness, type Provider, type Service, } from './keys.ts';
|
|
8
8
|
export { probe, probeAll } from './liveness.ts';
|
|
9
9
|
export { engineDisk, ensurePodmanReady, ownedContainers, podmanStatus, removeContainers, type DiskLine, type EngineDisk, type OwnedContainer, type PodmanOptions, type PodmanStatus, } from './podman.ts';
|
|
10
|
-
export { ago, bold, CliError, count, credentialError, cyan, dim, EXIT, fail, green, invalidError, isInteractive, json, note, pad, red, table, usageError, warn, write, writeAll, yellow, type ExitCode, } from './term.ts';
|
|
10
|
+
export { ago, bold, CliError, count, credentialError, cyan, dim, EXIT, fail, green, invalidError, isInteractive, italic, json, note, pad, red, table, underline, usageError, warn, write, writeAll, yellow, type ExitCode, } from './term.ts';
|
|
11
|
+
export { formatBlocks, formatInline, formatMarkdown, wrapSpans } from './tui/markdown.ts';
|
|
11
12
|
//# sourceMappingURL=lib.d.ts.map
|
package/dist/lib.js
CHANGED
|
@@ -29,5 +29,6 @@ export { assertPrivate, ensureDir, ensureHome, home, paths, readJson, writeJson
|
|
|
29
29
|
export { assertNotEmpty, assertOwner, assertUsable, describe, envNames, envOf, form, isOwner, isProvider, keyId, KeyStore, mask, OWNERS, parseRef, PROVIDERS, SERVICES, SHAPES, } from "./keys.js";
|
|
30
30
|
export { probe, probeAll } from "./liveness.js";
|
|
31
31
|
export { engineDisk, ensurePodmanReady, ownedContainers, podmanStatus, removeContainers, } from "./podman.js";
|
|
32
|
-
export { ago, bold, CliError, count, credentialError, cyan, dim, EXIT, fail, green, invalidError, isInteractive, json, note, pad, red, table, usageError, warn, write, writeAll, yellow, } from "./term.js";
|
|
32
|
+
export { ago, bold, CliError, count, credentialError, cyan, dim, EXIT, fail, green, invalidError, isInteractive, italic, json, note, pad, red, table, underline, usageError, warn, write, writeAll, yellow, } from "./term.js";
|
|
33
|
+
export { formatBlocks, formatInline, formatMarkdown, wrapSpans } from "./tui/markdown.js";
|
|
33
34
|
//# sourceMappingURL=lib.js.map
|
package/dist/meta.d.ts
CHANGED
|
@@ -179,7 +179,7 @@ export declare function footerCells(label: string, width: number): string[];
|
|
|
179
179
|
export declare function windowSink(rows?: number): Sink;
|
|
180
180
|
/**
|
|
181
181
|
* The answer in the same box `zen run` draws it in: bounded width, rounded
|
|
182
|
-
* rule,
|
|
182
|
+
* rule, formatted with full markdown styling (bold, italics, code, lists, quotes, tables).
|
|
183
183
|
*
|
|
184
184
|
* Redirected output gets the text and nothing else — `zen meta run ... > out.md`
|
|
185
185
|
* is meant to produce a file you can read, not one with a border down its side.
|
package/dist/meta.js
CHANGED
|
@@ -6,7 +6,8 @@ import { invokedAs } from "./args.js";
|
|
|
6
6
|
import { paths, readJson, writeJson } from "./home.js";
|
|
7
7
|
import { envOf } from "./keys.js";
|
|
8
8
|
import { credentialError, cut, cyan, dim, note, pad, plain, red, styled, usageError, write, yellow, } from "./term.js";
|
|
9
|
-
import {
|
|
9
|
+
import { formatInline, formatMarkdown } from "./tui/markdown.js";
|
|
10
|
+
import { boxWidth } from "./tui/wrap.js";
|
|
10
11
|
export const PROMPT_DIR = '.github/prompts';
|
|
11
12
|
const PROMPT_SUFFIX = '.prompt.md';
|
|
12
13
|
// ---------------------------------------------------------------------------
|
|
@@ -483,7 +484,7 @@ export function windowSink(rows = WINDOW_ROWS) {
|
|
|
483
484
|
}
|
|
484
485
|
/**
|
|
485
486
|
* The answer in the same box `zen run` draws it in: bounded width, rounded
|
|
486
|
-
* rule,
|
|
487
|
+
* rule, formatted with full markdown styling (bold, italics, code, lists, quotes, tables).
|
|
487
488
|
*
|
|
488
489
|
* Redirected output gets the text and nothing else — `zen meta run ... > out.md`
|
|
489
490
|
* is meant to produce a file you can read, not one with a border down its side.
|
|
@@ -494,31 +495,7 @@ export function answerBox(text, columns = process.stdout.columns ?? 80) {
|
|
|
494
495
|
}
|
|
495
496
|
const outer = boxWidth(text, columns);
|
|
496
497
|
const inner = outer - 4;
|
|
497
|
-
const body =
|
|
498
|
-
let previous;
|
|
499
|
-
for (const block of blocksOf(text)) {
|
|
500
|
-
if (previous && !(block.kind === 'item' && previous.kind === 'item')) {
|
|
501
|
-
body.push('');
|
|
502
|
-
}
|
|
503
|
-
previous = block;
|
|
504
|
-
if (block.kind === 'code') {
|
|
505
|
-
// Indentation is the meaning of a fenced block, so it is cut rather
|
|
506
|
-
// than reflowed.
|
|
507
|
-
body.push(dim(`\u250c\u2500${block.title ? ` ${block.title}` : ''}`));
|
|
508
|
-
body.push(...(block.lines ?? []).map((l) => `${dim('\u2502')} ${cut(l, inner - 2)}`));
|
|
509
|
-
body.push(dim('\u2514\u2500'));
|
|
510
|
-
}
|
|
511
|
-
else if (block.lines || block.kind === 'table') {
|
|
512
|
-
body.push(...textOf(block)
|
|
513
|
-
.split('\n')
|
|
514
|
-
.map((l) => cut(l, inner)));
|
|
515
|
-
}
|
|
516
|
-
else {
|
|
517
|
-
// Plain, not styled: `wrap` counts characters, and a bold run
|
|
518
|
-
// measured with its escape codes in it folds the line early.
|
|
519
|
-
body.push(...wrap(textOf(block, inner), inner));
|
|
520
|
-
}
|
|
521
|
-
}
|
|
498
|
+
const body = formatMarkdown(text, inner);
|
|
522
499
|
const rule = BOX.h.repeat(outer - 2);
|
|
523
500
|
return [
|
|
524
501
|
'',
|
|
@@ -586,7 +563,7 @@ export function absorb(event, out, sink, width) {
|
|
|
586
563
|
out.answer = content;
|
|
587
564
|
}
|
|
588
565
|
else if (content) {
|
|
589
|
-
sink.narrate(cut(content.replace(/\s+/g, ' '), width));
|
|
566
|
+
sink.narrate(cut(formatInline(content.replace(/\s+/g, ' ')), width));
|
|
590
567
|
}
|
|
591
568
|
break;
|
|
592
569
|
}
|
package/dist/resolve.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { existsSync } from 'node:fs';
|
|
2
|
-
import { relative, resolve, sep } from 'node:path';
|
|
2
|
+
import { isAbsolute, relative, resolve, sep } from 'node:path';
|
|
3
3
|
import { stamp } from "./ids.js";
|
|
4
4
|
import * as Projects from "./projects.js";
|
|
5
5
|
import { createSession, listSessions, readSessionMeta, requireSession, sessionPaths, } from "./session.js";
|
|
6
|
-
import
|
|
6
|
+
import * as term from "./term.js";
|
|
7
|
+
import { ago, bold, cyan, dim, usageError, yellow } from "./term.js";
|
|
7
8
|
// ---------------------------------------------------------------------------
|
|
8
9
|
// Project
|
|
9
10
|
// ---------------------------------------------------------------------------
|
|
@@ -20,7 +21,7 @@ export async function project(want) {
|
|
|
20
21
|
if (known.length === 0) {
|
|
21
22
|
throw usageError('not inside a project, and none are registered', 'create one: zen init');
|
|
22
23
|
}
|
|
23
|
-
if (!isInteractive()) {
|
|
24
|
+
if (!term.isInteractive()) {
|
|
24
25
|
throw usageError('not inside a project', 'name one with --project');
|
|
25
26
|
}
|
|
26
27
|
// Most recently worked in first: the answer to "which project" is nearly
|
|
@@ -29,7 +30,7 @@ export async function project(want) {
|
|
|
29
30
|
const ranked = known
|
|
30
31
|
.map((entry) => ({ entry, used: Projects.lastUsedAt(entry.path) ?? '' }))
|
|
31
32
|
.sort((a, b) => b.used.localeCompare(a.used) || a.entry.name.localeCompare(b.entry.name));
|
|
32
|
-
const chosen = await choose('Which project?', ranked.map(({ entry }) => ({ label: entry.name, detail: entry.path, value: entry })));
|
|
33
|
+
const chosen = await term.choose('Which project?', ranked.map(({ entry }) => ({ label: entry.name, detail: entry.path, value: entry })));
|
|
33
34
|
return Projects.openDir(chosen.path);
|
|
34
35
|
}
|
|
35
36
|
// ---------------------------------------------------------------------------
|
|
@@ -89,11 +90,11 @@ async function pickExisting(projectDir) {
|
|
|
89
90
|
if (all.length === 0) {
|
|
90
91
|
return undefined;
|
|
91
92
|
}
|
|
92
|
-
if (!isInteractive()) {
|
|
93
|
+
if (!term.isInteractive()) {
|
|
93
94
|
return sessionPaths(projectDir, all[0].id);
|
|
94
95
|
}
|
|
95
96
|
const sessions = all.slice(0, MAX_SESSIONS);
|
|
96
|
-
const choice = await choose('Session', [
|
|
97
|
+
const choice = await term.choose('Session', [
|
|
97
98
|
{
|
|
98
99
|
key: '0',
|
|
99
100
|
label: bold(cyan('New session…')),
|
|
@@ -137,10 +138,10 @@ export async function chooseWorkspace(session, want) {
|
|
|
137
138
|
await approve(at, session, want);
|
|
138
139
|
return at;
|
|
139
140
|
}
|
|
140
|
-
if (!isInteractive()) {
|
|
141
|
+
if (!term.isInteractive()) {
|
|
141
142
|
return own;
|
|
142
143
|
}
|
|
143
|
-
const chosen = await choose('Workspace — what the agent can read and write', [
|
|
144
|
+
const chosen = await term.choose('Workspace — what the agent can read and write', [
|
|
144
145
|
{ label: 'A fresh, empty directory', detail: own, value: own },
|
|
145
146
|
{ label: 'The directory you started in', detail: want.cwd, value: want.cwd },
|
|
146
147
|
]);
|
|
@@ -151,20 +152,26 @@ async function approve(at, session, want) {
|
|
|
151
152
|
if (contains(session.dir, at)) {
|
|
152
153
|
return;
|
|
153
154
|
}
|
|
154
|
-
|
|
155
|
+
if (!want.workspace) {
|
|
156
|
+
term.warn(`the agent will be able to read and write ${at}`);
|
|
157
|
+
}
|
|
155
158
|
if (want.yes) {
|
|
156
159
|
return;
|
|
157
160
|
}
|
|
158
|
-
if (!isInteractive()) {
|
|
161
|
+
if (!term.isInteractive()) {
|
|
159
162
|
throw usageError('refusing a workspace outside the session without confirmation', 'pass --yes if that is what you meant');
|
|
160
163
|
}
|
|
161
|
-
if (!(await confirm('Continue?'))) {
|
|
164
|
+
if (!(await term.confirm('Continue?'))) {
|
|
162
165
|
throw usageError('cancelled');
|
|
163
166
|
}
|
|
164
167
|
}
|
|
165
168
|
function contains(parent, child) {
|
|
166
169
|
const from = resolve(parent);
|
|
167
170
|
const to = resolve(child);
|
|
168
|
-
|
|
171
|
+
if (from === to) {
|
|
172
|
+
return true;
|
|
173
|
+
}
|
|
174
|
+
const rel = relative(from, to);
|
|
175
|
+
return !rel.startsWith(`..${sep}`) && rel !== '..' && !isAbsolute(rel);
|
|
169
176
|
}
|
|
170
177
|
//# sourceMappingURL=resolve.js.map
|
package/dist/sandbox.js
CHANGED
package/dist/scaffold.d.ts
CHANGED
|
@@ -11,6 +11,14 @@
|
|
|
11
11
|
* own beside them.
|
|
12
12
|
*/
|
|
13
13
|
export declare const SHARED_RULES: readonly string[];
|
|
14
|
+
/**
|
|
15
|
+
* This `zen`'s own version, which the scaffold pins the sandbox's tools to.
|
|
16
|
+
* The publishable packages move in lockstep, so one number covers them all.
|
|
17
|
+
*
|
|
18
|
+
* Exported so the check that reports a drifted pin reads the same number the
|
|
19
|
+
* scaffold wrote, rather than a second definition of "this version".
|
|
20
|
+
*/
|
|
21
|
+
export declare function ownVersion(): string;
|
|
14
22
|
/**
|
|
15
23
|
* Writes the editor's files under `dir`, replacing what is there. They are
|
|
16
24
|
* ours: they say how the editor is to treat a directory the agents write into,
|
package/dist/scaffold.js
CHANGED
|
@@ -33,6 +33,8 @@ import { fileURLToPath } from 'node:url';
|
|
|
33
33
|
const TEMPLATES = fileURLToPath(new URL('../templates', import.meta.url));
|
|
34
34
|
/** The suffix on a file with `{{...}}` in it, dropped when the file lands. */
|
|
35
35
|
const TEMPLATE = '.tmpl';
|
|
36
|
+
/** How to use file tools: a house rules file, landing under `agents/`. */
|
|
37
|
+
const FILE_RULES = join('agents', 'files-instructions.md');
|
|
36
38
|
/** How to use the memory graph: a house rules file, landing under `agents/`. */
|
|
37
39
|
const MEMORY_RULES = join('agents', 'memory-instructions.md');
|
|
38
40
|
/** How tools are called: the same, and equally not the project's to maintain. */
|
|
@@ -51,14 +53,17 @@ const FORK_RULES = join('agents', 'fork-instructions.md');
|
|
|
51
53
|
* place: project policy on any of those subjects goes in a topic file of its
|
|
52
54
|
* own beside them.
|
|
53
55
|
*/
|
|
54
|
-
export const SHARED_RULES = [FORK_RULES, MEMORY_RULES, TOOL_RULES];
|
|
56
|
+
export const SHARED_RULES = [FILE_RULES, FORK_RULES, MEMORY_RULES, TOOL_RULES];
|
|
55
57
|
/** The same bytes, kept in the `zen-memory` skill to restore or diff against. */
|
|
56
58
|
const MEMORY_REFERENCE = join('.github', 'skills', 'zen-memory', 'references', 'memory-instructions.md');
|
|
57
59
|
/**
|
|
58
60
|
* This `zen`'s own version, which the scaffold pins the sandbox's tools to.
|
|
59
61
|
* The publishable packages move in lockstep, so one number covers them all.
|
|
62
|
+
*
|
|
63
|
+
* Exported so the check that reports a drifted pin reads the same number the
|
|
64
|
+
* scaffold wrote, rather than a second definition of "this version".
|
|
60
65
|
*/
|
|
61
|
-
function ownVersion() {
|
|
66
|
+
export function ownVersion() {
|
|
62
67
|
const manifest = fileURLToPath(new URL('../package.json', import.meta.url));
|
|
63
68
|
return JSON.parse(readFileSync(manifest, 'utf8')).version;
|
|
64
69
|
}
|
package/dist/term.d.ts
CHANGED
|
@@ -26,6 +26,8 @@ export declare const CHROME: Record<Appearance, number>;
|
|
|
26
26
|
/** What the terminal looks like, as far as can be known without asking it. */
|
|
27
27
|
export declare function appearance(): Appearance;
|
|
28
28
|
export declare const bold: (s: string) => string;
|
|
29
|
+
export declare const italic: (s: string) => string;
|
|
30
|
+
export declare const underline: (s: string) => string;
|
|
29
31
|
/**
|
|
30
32
|
* One step quieter, which is half of everything this tool prints: paths,
|
|
31
33
|
* counts, hints, the second column of every table.
|
package/dist/term.js
CHANGED
|
@@ -88,6 +88,8 @@ function fromAppleTerminal() {
|
|
|
88
88
|
// Styling
|
|
89
89
|
// ---------------------------------------------------------------------------
|
|
90
90
|
export const bold = (s) => styleText('bold', s);
|
|
91
|
+
export const italic = (s) => styleText('italic', s);
|
|
92
|
+
export const underline = (s) => styleText('underline', s);
|
|
91
93
|
/**
|
|
92
94
|
* One step quieter, which is half of everything this tool prints: paths,
|
|
93
95
|
* counts, hints, the second column of every table.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { type Block, type Span } from './wrap.ts';
|
|
2
|
+
export declare function wrapSpans(spans: readonly Span[], width: number, prefix?: string, subsequentPrefix?: string): string[];
|
|
3
|
+
/**
|
|
4
|
+
* Render a sequence of AST `Block`s into styled terminal lines wrapped to `width`.
|
|
5
|
+
*/
|
|
6
|
+
export declare function formatBlocks(blocks: readonly Block[], width?: number): string[];
|
|
7
|
+
/**
|
|
8
|
+
* Format markdown text into styled ANSI terminal lines wrapped to `width`.
|
|
9
|
+
*/
|
|
10
|
+
export declare function formatMarkdown(text: string, width?: number): string[];
|
|
11
|
+
/**
|
|
12
|
+
* Format inline markdown text (bold, italic, code, links) into a styled ANSI string.
|
|
13
|
+
* Markers like backticks and asterisks are resolved and removed.
|
|
14
|
+
*/
|
|
15
|
+
export declare function formatInline(text: string): string;
|
|
16
|
+
//# sourceMappingURL=markdown.d.ts.map
|