@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.
Files changed (62) hide show
  1. package/README.md +40 -3
  2. package/dist/archive.d.ts +139 -0
  3. package/dist/archive.js +376 -0
  4. package/dist/banner.d.ts +33 -24
  5. package/dist/banner.js +25 -6
  6. package/dist/commands/check.js +1 -0
  7. package/dist/commands/export.d.ts +3 -0
  8. package/dist/commands/export.js +255 -0
  9. package/dist/commands/import.d.ts +3 -0
  10. package/dist/commands/import.js +173 -0
  11. package/dist/commands/index.d.ts +5 -1
  12. package/dist/commands/index.js +13 -1
  13. package/dist/commands/init.js +1 -0
  14. package/dist/commands/inspect.js +1 -0
  15. package/dist/commands/key.js +1 -0
  16. package/dist/commands/list.js +1 -0
  17. package/dist/commands/memory.js +118 -10
  18. package/dist/commands/open.js +1 -0
  19. package/dist/commands/run.js +3 -0
  20. package/dist/commands/sandbox.js +1 -0
  21. package/dist/commands/version.js +1 -0
  22. package/dist/lib.d.ts +2 -1
  23. package/dist/lib.js +2 -1
  24. package/dist/meta.d.ts +1 -1
  25. package/dist/meta.js +5 -28
  26. package/dist/resolve.js +19 -12
  27. package/dist/sandbox.js +1 -0
  28. package/dist/scaffold.d.ts +8 -0
  29. package/dist/scaffold.js +7 -2
  30. package/dist/term.d.ts +2 -0
  31. package/dist/term.js +2 -0
  32. package/dist/tui/markdown.d.ts +16 -0
  33. package/dist/tui/markdown.js +259 -0
  34. package/dist/tui/theme.js +1 -1
  35. package/dist/tui/wrap.d.ts +1 -1
  36. package/dist/validate.js +150 -4
  37. package/package.json +12 -6
  38. package/templates/editor/.github/copilot-instructions.md +62 -33
  39. package/templates/editor/.github/prompts/spec-sync-project.prompt.md +56 -3
  40. package/templates/editor/.github/skills/zen-cli/SKILL.md +21 -16
  41. package/templates/editor/.github/skills/zen-cli/references/check.md +6 -0
  42. package/templates/editor/.github/skills/zen-cli/references/export.md +107 -0
  43. package/templates/editor/.github/skills/zen-cli/references/memory.md +51 -13
  44. package/templates/editor/.github/skills/zen-cli/references/projects.md +1 -0
  45. package/templates/editor/.github/skills/zen-cli/references/rag.md +43 -3
  46. package/templates/editor/.github/skills/zen-cli/references/sandbox.md +30 -2
  47. package/templates/editor/.github/skills/zen-instructions/SKILL.md +232 -0
  48. package/templates/editor/.github/skills/zen-memory/SKILL.md +2 -1
  49. package/templates/editor/.github/skills/zen-memory-warmup/SKILL.md +684 -0
  50. package/templates/editor/.github/skills/zen-rag-docs/SKILL.md +56 -23
  51. package/templates/editor/.github/skills/zen-rag-schema/SKILL.md +55 -28
  52. package/templates/editor/.github/skills/zen-review/SKILL.md +17 -1
  53. package/templates/editor/.github/skills/zen-sandbox/SKILL.md +257 -0
  54. package/templates/editor/.github/skills/zen-spec-sync/SKILL.md +16 -3
  55. package/templates/editor/.vscode/agents.schema.json +8 -3
  56. package/templates/project/SPECIFICATION.md.tmpl +4 -1
  57. package/templates/project/agents/files-instructions.md +86 -0
  58. package/templates/project/agents/fork-instructions.md +17 -0
  59. package/templates/project/agents/memory-instructions.md +37 -0
  60. package/templates/project/agents.yaml.tmpl +3 -3
  61. package/templates/project/gitignore +12 -1
  62. package/templates/project/scripts/_setup.sh +68 -11
@@ -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
- ' --yes Do not ask before removing.',
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
- const opened = await open(ctx.cwd, values.project, values.dir);
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
@@ -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',
@@ -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',
@@ -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.',
@@ -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, fenced blocks kept as they were written.
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 { blocksOf, boxWidth, textOf, wrap } from "./tui/wrap.js";
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, fenced blocks kept as they were written.
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 { ago, bold, choose, confirm, cyan, dim, isInteractive, usageError, warn, yellow, } from "./term.js";
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
- warn(`the agent will be able to read and write ${at}`);
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
- return from === to || !relative(from, to).startsWith(`..${sep}`);
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
@@ -161,6 +161,7 @@ function toSpec(config, extra, secrets) {
161
161
  network: config.network,
162
162
  workdir: config.workdir,
163
163
  timeout: config.timeout,
164
+ hardening: config.hardening,
164
165
  user: config.user,
165
166
  persist: config.persist,
166
167
  env,
@@ -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