@zenera/cli 1.1.20 → 1.1.22

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 (50) hide show
  1. package/README.md +3 -3
  2. package/dist/banner.d.ts +41 -20
  3. package/dist/banner.js +32 -5
  4. package/dist/commands/check.js +1 -0
  5. package/dist/commands/init.js +1 -0
  6. package/dist/commands/inspect.js +1 -0
  7. package/dist/commands/key.js +1 -0
  8. package/dist/commands/list.js +1 -0
  9. package/dist/commands/memory.js +1 -0
  10. package/dist/commands/models.js +1 -0
  11. package/dist/commands/open.js +1 -0
  12. package/dist/commands/run.js +3 -0
  13. package/dist/commands/sandbox.js +1 -0
  14. package/dist/commands/version.js +1 -0
  15. package/dist/lib.d.ts +3 -2
  16. package/dist/lib.js +3 -2
  17. package/dist/meta.d.ts +1 -1
  18. package/dist/meta.js +5 -28
  19. package/dist/resolve.js +19 -12
  20. package/dist/sandbox.js +1 -0
  21. package/dist/scaffold.js +3 -1
  22. package/dist/term.d.ts +2 -0
  23. package/dist/term.js +2 -0
  24. package/dist/tui/markdown.d.ts +16 -0
  25. package/dist/tui/markdown.js +259 -0
  26. package/dist/tui/theme.js +1 -1
  27. package/dist/tui/wrap.d.ts +1 -1
  28. package/dist/validate.js +82 -3
  29. package/package.json +2 -2
  30. package/templates/editor/.github/copilot-instructions.md +62 -33
  31. package/templates/editor/.github/prompts/spec-sync-project.prompt.md +31 -1
  32. package/templates/editor/.github/skills/zen-cli/SKILL.md +15 -15
  33. package/templates/editor/.github/skills/zen-cli/references/projects.md +1 -0
  34. package/templates/editor/.github/skills/zen-cli/references/rag.md +37 -3
  35. package/templates/editor/.github/skills/zen-cli/references/sandbox.md +30 -2
  36. package/templates/editor/.github/skills/zen-instructions/SKILL.md +232 -0
  37. package/templates/editor/.github/skills/zen-memory-warmup/SKILL.md +554 -0
  38. package/templates/editor/.github/skills/zen-rag-docs/SKILL.md +56 -23
  39. package/templates/editor/.github/skills/zen-rag-schema/SKILL.md +55 -28
  40. package/templates/editor/.github/skills/zen-review/SKILL.md +17 -1
  41. package/templates/editor/.github/skills/zen-sandbox/SKILL.md +257 -0
  42. package/templates/editor/.github/skills/zen-spec-sync/SKILL.md +15 -3
  43. package/templates/editor/.vscode/agents.schema.json +8 -3
  44. package/templates/project/SPECIFICATION.md.tmpl +2 -1
  45. package/templates/project/agents/files-instructions.md +86 -0
  46. package/templates/project/agents/fork-instructions.md +17 -0
  47. package/templates/project/agents/memory-instructions.md +37 -0
  48. package/templates/project/agents.yaml.tmpl +3 -3
  49. package/templates/project/gitignore +12 -1
  50. package/templates/project/scripts/_setup.sh +19 -4
package/README.md CHANGED
@@ -310,13 +310,13 @@ agents:
310
310
  - name: reader
311
311
  description: Reads the workspace and summarises what is in it.
312
312
  system: agents/prompts/reader.md
313
- tools: [workspace:read_file, workspace:list_dir, workspace:find_files]
313
+ tools: [files:read_file, files:list_dir, files:find_files]
314
314
  handoffs: [writer]
315
315
 
316
316
  - name: writer
317
317
  description: Turns a summary into a file on disk.
318
318
  system: agents/prompts/writer.md
319
- tools: [workspace:*]
319
+ tools: [files:*]
320
320
  handoffs: [reader] # a hand-off does not return by itself; give it a way back
321
321
  ```
322
322
 
@@ -351,7 +351,7 @@ lines. Then say where you put it and stop.
351
351
  | `description` | what it is for - read by the _other_ agents when deciding to hand off |
352
352
  | `system` | its own brief, in prose, at `agents/prompts/<name>.md` |
353
353
  | `model` | this agent's model; the top-level one otherwise |
354
- | `tools` | what it may reach for - `workspace:*`, `sandbox:*`, a named tool |
354
+ | `tools` | what it may reach for - `files:*` (or `workspace:*`), `sandbox:*` |
355
355
  | `handoffs` | who it may pass the work to |
356
356
  | `skills` | knowledge pulled in mid-run instead of carried in every prompt |
357
357
 
package/dist/banner.d.ts CHANGED
@@ -1,25 +1,38 @@
1
+ /**
2
+ * A stroke and the bevel that shades it, one step darker in the same hue.
3
+ *
4
+ * The head word is the terminal's own ink, so it has one per background: near
5
+ * white raised off grey, or near black embossed into it. The accent does not
6
+ * — it is the brand, it is the same orange on both, and it is the only word
7
+ * here that is allowed not to care what it is printed on.
8
+ */
9
+ interface Tone {
10
+ face: string;
11
+ shade: string;
12
+ }
1
13
  /** One per sub-brand, so two of them are told apart before the word is read. */
2
14
  declare const ACCENTS: {
3
- readonly orange: {
4
- readonly face: "\u001B[1;38;5;208m";
5
- readonly shade: "\u001B[0;38;5;130m";
6
- };
7
- readonly green: {
8
- readonly face: "\u001B[1;38;5;41m";
9
- readonly shade: "\u001B[0;38;5;29m";
10
- };
11
- readonly cyan: {
12
- readonly face: "\u001B[1;38;5;51m";
13
- readonly shade: "\u001B[0;38;5;37m";
14
- };
15
- readonly mint: {
16
- readonly face: "\u001B[1;38;5;121m";
17
- readonly shade: "\u001B[0;38;5;78m";
18
- };
19
- readonly gold: {
20
- readonly face: "\u001B[1;38;5;220m";
21
- readonly shade: "\u001B[0;38;5;178m";
22
- };
15
+ readonly orange: Tone;
16
+ readonly amber: Tone;
17
+ readonly gold: Tone;
18
+ readonly lime: Tone;
19
+ readonly green: Tone;
20
+ readonly emerald: Tone;
21
+ readonly mint: Tone;
22
+ readonly teal: Tone;
23
+ readonly cyan: Tone;
24
+ readonly sky: Tone;
25
+ readonly azure: Tone;
26
+ readonly steel: Tone;
27
+ readonly indigo: Tone;
28
+ readonly violet: Tone;
29
+ readonly purple: Tone;
30
+ readonly magenta: Tone;
31
+ readonly pink: Tone;
32
+ readonly rose: Tone;
33
+ readonly crimson: Tone;
34
+ readonly red: Tone;
35
+ readonly dusk: Tone;
23
36
  };
24
37
  export type Hue = keyof typeof ACCENTS;
25
38
  export interface BannerText {
@@ -38,6 +51,14 @@ export interface PrintBannerOptions {
38
51
  }
39
52
  export declare const NEO_BANNER: BannerText;
40
53
  export declare function bannerLines(text: BannerText, columns?: number): string[];
54
+ /**
55
+ * The same letters, unpainted and still split by word, for a renderer that is
56
+ * not a terminal and colours the two halves its own way.
57
+ */
58
+ export declare function bannerArt(text: BannerText): {
59
+ head: string[];
60
+ accent: string[];
61
+ };
41
62
  /** Narration, and only for someone watching. */
42
63
  export declare function printBanner(text: BannerText, options?: PrintBannerOptions): void;
43
64
  export {};
package/dist/banner.js CHANGED
@@ -69,13 +69,33 @@ const HEADS = {
69
69
  dark: { face: '\u001b[1;38;5;231m', shade: '\u001b[0;38;5;244m' },
70
70
  light: { face: '\u001b[1;38;5;235m', shade: '\u001b[0;38;5;245m' },
71
71
  };
72
+ const tone = (face, shade) => ({
73
+ face: `\u001b[1;38;5;${face}m`,
74
+ shade: `\u001b[0;38;5;${shade}m`,
75
+ });
72
76
  /** One per sub-brand, so two of them are told apart before the word is read. */
73
77
  const ACCENTS = {
74
- orange: { face: '\u001b[1;38;5;208m', shade: '\u001b[0;38;5;130m' },
75
- green: { face: '\u001b[1;38;5;41m', shade: '\u001b[0;38;5;29m' },
76
- cyan: { face: '\u001b[1;38;5;51m', shade: '\u001b[0;38;5;37m' },
77
- mint: { face: '\u001b[1;38;5;121m', shade: '\u001b[0;38;5;78m' },
78
- gold: { face: '\u001b[1;38;5;220m', shade: '\u001b[0;38;5;178m' },
78
+ orange: tone(208, 130),
79
+ amber: tone(214, 172),
80
+ gold: tone(220, 178),
81
+ lime: tone(154, 106),
82
+ green: tone(41, 29),
83
+ emerald: tone(48, 29),
84
+ mint: tone(121, 78),
85
+ teal: tone(44, 30),
86
+ cyan: tone(51, 37),
87
+ sky: tone(39, 26),
88
+ azure: tone(33, 25),
89
+ steel: tone(110, 67),
90
+ indigo: tone(105, 61),
91
+ violet: tone(141, 97),
92
+ purple: tone(171, 90),
93
+ magenta: tone(201, 127),
94
+ pink: tone(212, 169),
95
+ rose: tone(204, 125),
96
+ crimson: tone(197, 124),
97
+ red: tone(196, 88),
98
+ dusk: tone(141, 97),
79
99
  };
80
100
  /** Runs of stroke and runs of bevel, alternating; spaces stay unstyled. */
81
101
  const RUNS = /[█▀▄]+|[^█▀▄ ]+/g;
@@ -120,6 +140,13 @@ export function bannerLines(text, columns = process.stderr.columns || 80) {
120
140
  lines.push(`${' '.repeat(Math.max(INDENT, Math.round((width - caption.length) / 2)))}${dim(caption)}`);
121
141
  return lines;
122
142
  }
143
+ /**
144
+ * The same letters, unpainted and still split by word, for a renderer that is
145
+ * not a terminal and colours the two halves its own way.
146
+ */
147
+ export function bannerArt(text) {
148
+ return { head: big(text.head), accent: big(text.accent).map((row) => row.trimEnd()) };
149
+ }
123
150
  /** Narration, and only for someone watching. */
124
151
  export function printBanner(text, options = {}) {
125
152
  if (!process.stderr.isTTY) {
@@ -34,6 +34,7 @@ const USAGE = 'zen check [name|dir] [--project <name|dir>] [--fix] [--no-sandbox
34
34
  export const check = {
35
35
  summary: 'Validate agents.yaml and every file it names, and ask its models.',
36
36
  usage: USAGE,
37
+ banner: { head: 'Zenera', accent: 'Check', subtitle: 'Project Health', hue: 'lime' },
37
38
  details: [
38
39
  'Checks the whole project: the configuration parses and satisfies the',
39
40
  'schema, every prompt, skill and catalog it names is on disk, hand-offs',
@@ -126,6 +126,7 @@ function hasExa(store) {
126
126
  export const init = {
127
127
  summary: 'Create a project here, or in <dir>, and register it.',
128
128
  usage: USAGE,
129
+ banner: { head: 'Zenera', accent: 'Init', subtitle: 'Project Scaffolding', hue: 'rose' },
129
130
  details: [
130
131
  'Writes SPECIFICATION.md, agents.yaml, agents/ (house rules, the memory',
131
132
  'rules and one role prompt) and scripts/, then records the directory so',
@@ -13,6 +13,7 @@ const USAGE = 'zen inspect [run] [--session <id>] [--open] [--rebuild] [--serve
13
13
  export const inspect = {
14
14
  summary: "Open or rebuild a run's report.html.",
15
15
  usage: USAGE,
16
+ banner: { head: 'Zenera', accent: 'Inspect', subtitle: 'Run Trajectory', hue: 'indigo' },
16
17
  details: [
17
18
  'With no arguments: asks which session and run, or takes the newest of',
18
19
  'each when there is nothing to ask on.',
@@ -508,6 +508,7 @@ const SUBS = {
508
508
  export const key = {
509
509
  summary: 'The credential store: add, choose, verify and export API keys.',
510
510
  usage: USAGE,
511
+ banner: { head: 'Zenera', accent: 'Keys', subtitle: 'Credential Keyring', hue: 'amber' },
511
512
  details: [
512
513
  'Keys live in ~/.zenera/neo/keys.json (0600) and are materialised into',
513
514
  'the environment before a run. A real environment variable always wins,',
@@ -9,6 +9,7 @@ const USAGE = 'zen list [--sessions] [--prune]';
9
9
  export const list = {
10
10
  summary: 'Every known project: sessions, last run, whether one is live.',
11
11
  usage: USAGE,
12
+ banner: { head: 'Zenera', accent: 'Projects', subtitle: 'Project Registry', hue: 'sky' },
12
13
  details: [
13
14
  'The registry is an index, not the truth. An entry whose directory has',
14
15
  'gone away is shown dimmed rather than hidden; --prune forgets them.',
@@ -31,6 +31,7 @@ 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.',
@@ -540,6 +540,7 @@ const SUBS = {
540
540
  export const models = {
541
541
  summary: 'What this machine can use: list, search, test and pick models.',
542
542
  usage: USAGE,
543
+ banner: { head: 'Zenera', accent: 'Models', subtitle: 'Model Catalog', hue: 'dusk' },
543
544
  details: [
544
545
  'Answers "what can I use", using the credentials already on this',
545
546
  'machine. `zen check` answers the other question — whether one',
@@ -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
@@ -1,5 +1,5 @@
1
1
  export { extract, invokedAs, one, parse, split, type Parsed, type Split } from './args.ts';
2
- export { printBanner, type BannerText } from './banner.ts';
2
+ export { bannerArt, printBanner, type BannerText } from './banner.ts';
3
3
  export { Cache, items as cacheItems, cacheKey, kinds as cacheKinds, clear as clearCache, NO_CACHE, sweep as sweepCache, type CacheEntry, type CacheItem, type CacheKind, type CacheOptions, type CacheStore, type Swept, } from './cache.ts';
4
4
  export { CATALOG_TTL_MS, CURATED, fetchCatalog, loadCatalog, loadCatalogs, matches, PREFERRED, type Catalog, type CatalogEntry, type CatalogOptions, type Filters, type Role, } from './catalog.ts';
5
5
  export type { Command, Context } from './command.ts';
@@ -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
@@ -22,12 +22,13 @@
22
22
  // must not let a `CliError` escape into a request handler.
23
23
  // ---------------------------------------------------------------------------
24
24
  export { extract, invokedAs, one, parse, split } from "./args.js";
25
- export { printBanner } from "./banner.js";
25
+ export { bannerArt, printBanner } from "./banner.js";
26
26
  export { Cache, items as cacheItems, cacheKey, kinds as cacheKinds, clear as clearCache, NO_CACHE, sweep as sweepCache, } from "./cache.js";
27
27
  export { CATALOG_TTL_MS, CURATED, fetchCatalog, loadCatalog, loadCatalogs, matches, PREFERRED, } from "./catalog.js";
28
28
  export { assertPrivate, ensureDir, ensureHome, home, paths, readJson, writeJson } from "./home.js";
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,
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,7 +53,7 @@ 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
  /**
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