@zenera/cli 1.1.0

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 (78) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +239 -0
  3. package/dist/args.d.ts +40 -0
  4. package/dist/args.js +99 -0
  5. package/dist/audit.d.ts +53 -0
  6. package/dist/audit.js +144 -0
  7. package/dist/banner.d.ts +13 -0
  8. package/dist/banner.js +103 -0
  9. package/dist/command.d.ts +14 -0
  10. package/dist/command.js +12 -0
  11. package/dist/commands/check.d.ts +3 -0
  12. package/dist/commands/check.js +287 -0
  13. package/dist/commands/index.d.ts +22 -0
  14. package/dist/commands/index.js +56 -0
  15. package/dist/commands/init.d.ts +3 -0
  16. package/dist/commands/init.js +157 -0
  17. package/dist/commands/inspect.d.ts +3 -0
  18. package/dist/commands/inspect.js +158 -0
  19. package/dist/commands/key.d.ts +3 -0
  20. package/dist/commands/key.js +335 -0
  21. package/dist/commands/list.d.ts +3 -0
  22. package/dist/commands/list.js +101 -0
  23. package/dist/commands/models.d.ts +9 -0
  24. package/dist/commands/models.js +120 -0
  25. package/dist/commands/open.d.ts +9 -0
  26. package/dist/commands/open.js +270 -0
  27. package/dist/commands/run.d.ts +3 -0
  28. package/dist/commands/run.js +167 -0
  29. package/dist/commands/sandbox.d.ts +3 -0
  30. package/dist/commands/sandbox.js +112 -0
  31. package/dist/commands/version.d.ts +6 -0
  32. package/dist/commands/version.js +39 -0
  33. package/dist/engine.d.ts +49 -0
  34. package/dist/engine.js +208 -0
  35. package/dist/external.d.ts +10 -0
  36. package/dist/external.js +56 -0
  37. package/dist/home.d.ts +31 -0
  38. package/dist/home.js +108 -0
  39. package/dist/ids.d.ts +12 -0
  40. package/dist/ids.js +44 -0
  41. package/dist/keys.d.ts +124 -0
  42. package/dist/keys.js +309 -0
  43. package/dist/lib.d.ts +9 -0
  44. package/dist/lib.js +31 -0
  45. package/dist/liveness.d.ts +23 -0
  46. package/dist/liveness.js +221 -0
  47. package/dist/main.d.ts +3 -0
  48. package/dist/main.js +155 -0
  49. package/dist/narrate.d.ts +19 -0
  50. package/dist/narrate.js +124 -0
  51. package/dist/podman.d.ts +46 -0
  52. package/dist/podman.js +254 -0
  53. package/dist/projects.d.ts +70 -0
  54. package/dist/projects.js +232 -0
  55. package/dist/resolve.d.ts +27 -0
  56. package/dist/resolve.js +138 -0
  57. package/dist/sandbox.d.ts +36 -0
  58. package/dist/sandbox.js +104 -0
  59. package/dist/scaffold.d.ts +29 -0
  60. package/dist/scaffold.js +220 -0
  61. package/dist/session.d.ts +77 -0
  62. package/dist/session.js +156 -0
  63. package/dist/term.d.ts +69 -0
  64. package/dist/term.js +242 -0
  65. package/dist/tui/app.d.ts +8 -0
  66. package/dist/tui/app.js +257 -0
  67. package/dist/tui/theme.d.ts +23 -0
  68. package/dist/tui/theme.js +134 -0
  69. package/dist/tui/wrap.d.ts +12 -0
  70. package/dist/tui/wrap.js +62 -0
  71. package/dist/validate.d.ts +145 -0
  72. package/dist/validate.js +959 -0
  73. package/package.json +76 -0
  74. package/templates/.github/copilot-instructions.md +1579 -0
  75. package/templates/.github/prompts/new-agent.prompt.md +38 -0
  76. package/templates/.github/prompts/new-skill.prompt.md +37 -0
  77. package/templates/.github/prompts/review-project.prompt.md +31 -0
  78. package/templates/.github/skills/zen-cli/SKILL.md +110 -0
@@ -0,0 +1,138 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { relative, resolve, sep } from 'node:path';
3
+ import { stamp } from "./ids.js";
4
+ import * as Projects from "./projects.js";
5
+ import { createSession, listSessions, requireSession, sessionPaths, } from "./session.js";
6
+ import { ago, choose, confirm, dim, isInteractive, usageError, warn, yellow } from "./term.js";
7
+ // ---------------------------------------------------------------------------
8
+ // Project
9
+ // ---------------------------------------------------------------------------
10
+ export async function project(want) {
11
+ if (want.project) {
12
+ return Projects.open(want.project);
13
+ }
14
+ const here = await Projects.current(want.cwd);
15
+ if (here) {
16
+ return here;
17
+ }
18
+ const registry = await Projects.Registry.open();
19
+ const known = registry.entries.filter((e) => existsSync(e.path));
20
+ if (known.length === 0) {
21
+ throw usageError('not inside a project, and none are registered', 'create one: zen init');
22
+ }
23
+ if (!isInteractive()) {
24
+ throw usageError('not inside a project', 'name one with --project');
25
+ }
26
+ const chosen = await choose('Which project?', known.map((e) => ({ label: e.name, detail: e.path, value: e })));
27
+ return Projects.openDir(chosen.path);
28
+ }
29
+ // ---------------------------------------------------------------------------
30
+ // Session
31
+ // ---------------------------------------------------------------------------
32
+ export async function target(want) {
33
+ const found = await project(want);
34
+ const dir = found.dir;
35
+ if (want.session && want.fresh) {
36
+ throw usageError('--session and --new contradict each other');
37
+ }
38
+ if (want.session) {
39
+ return {
40
+ project: found,
41
+ session: requireSession(dir, want.session),
42
+ created: false,
43
+ };
44
+ }
45
+ if (!want.fresh) {
46
+ const existing = await pickExisting(dir);
47
+ if (existing) {
48
+ return { project: found, session: existing, created: false };
49
+ }
50
+ }
51
+ return { project: found, session: await create(dir, want), created: true };
52
+ }
53
+ /**
54
+ * Resuming is the default: the common case is continuing what you were doing.
55
+ * With one session it is taken without asking; with several, the others are
56
+ * offered, because "the most recent" is only usually what you meant.
57
+ *
58
+ * A session that has never run has nothing to continue — it is indistinguishable
59
+ * from a fresh one, so it is left out rather than offered as a choice.
60
+ */
61
+ async function pickExisting(projectDir) {
62
+ const sessions = (await listSessions(projectDir)).filter((s) => s.runs > 0 || s.busy);
63
+ if (sessions.length === 0) {
64
+ return undefined;
65
+ }
66
+ if (!isInteractive()) {
67
+ return sessionPaths(projectDir, sessions[0].id);
68
+ }
69
+ const choice = await choose('Session', [
70
+ ...sessions.map((s) => ({
71
+ label: s.title ?? s.id,
72
+ detail: `${s.id} ${ago(s.lastRunAt ?? s.createdAt)} ` +
73
+ `${s.runs} run${s.runs === 1 ? '' : 's'}${s.busy ? yellow(' running') : ''}`,
74
+ value: s.id,
75
+ })),
76
+ { label: dim('New session…'), value: undefined },
77
+ ]);
78
+ return choice ? sessionPaths(projectDir, choice) : undefined;
79
+ }
80
+ /**
81
+ * The id is minted before anything is written, so the workspace question can be
82
+ * asked against the real paths — and so a cancelled answer leaves no directory
83
+ * behind.
84
+ */
85
+ async function create(projectDir, want) {
86
+ const id = stamp();
87
+ const planned = sessionPaths(projectDir, id);
88
+ const workspace = await chooseWorkspace(planned, want);
89
+ return createSession(projectDir, id, workspace);
90
+ }
91
+ // ---------------------------------------------------------------------------
92
+ // Workspace
93
+ // ---------------------------------------------------------------------------
94
+ /**
95
+ * The workspace is what the agent can read and write, so pointing it outside
96
+ * the session is the useful case and the dangerous one. It is confirmed once,
97
+ * explicitly, naming the path — and a script has to say `--yes` to skip that.
98
+ * An agent with file tools rooted at `$HOME` should take more than one
99
+ * keystroke to arrange.
100
+ */
101
+ export async function chooseWorkspace(session, want) {
102
+ const own = resolve(session.workspace);
103
+ if (want.workspace) {
104
+ const at = resolve(want.cwd, want.workspace);
105
+ await approve(at, session, want);
106
+ return at;
107
+ }
108
+ if (!isInteractive()) {
109
+ return own;
110
+ }
111
+ const chosen = await choose('Workspace — what the agent can read and write', [
112
+ { label: 'A fresh, empty directory', detail: own, value: own },
113
+ { label: 'The directory you started in', detail: want.cwd, value: want.cwd },
114
+ ]);
115
+ await approve(chosen, session, want);
116
+ return chosen;
117
+ }
118
+ async function approve(at, session, want) {
119
+ if (contains(session.dir, at)) {
120
+ return;
121
+ }
122
+ warn(`the agent will be able to read and write ${at}`);
123
+ if (want.yes) {
124
+ return;
125
+ }
126
+ if (!isInteractive()) {
127
+ throw usageError('refusing a workspace outside the session without confirmation', 'pass --yes if that is what you meant');
128
+ }
129
+ if (!(await confirm('Continue?'))) {
130
+ throw usageError('cancelled');
131
+ }
132
+ }
133
+ function contains(parent, child) {
134
+ const from = resolve(parent);
135
+ const to = resolve(child);
136
+ return from === to || !relative(from, to).startsWith(`..${sep}`);
137
+ }
138
+ //# sourceMappingURL=resolve.js.map
@@ -0,0 +1,36 @@
1
+ import { SandboxPool, type AgentProject, type ProjectConfig, type SandboxSpec } from '@zenera/neo';
2
+ import type { SessionPaths } from './session.ts';
3
+ export interface SandboxSetup {
4
+ pool: SandboxPool;
5
+ /** the resolved base spec, for `zn sandbox status` and the image pre-warm */
6
+ spec: SandboxSpec;
7
+ image: string;
8
+ /** host side of the container's `$HOME`, created only if it is ever needed */
9
+ home: string;
10
+ }
11
+ export interface SandboxInputs {
12
+ config: ProjectConfig;
13
+ session: SessionPaths;
14
+ workspace: string;
15
+ readOnly?: boolean;
16
+ /** `--image` */
17
+ image?: string;
18
+ }
19
+ export declare function buildSandbox(opts: SandboxInputs): SandboxSetup;
20
+ /**
21
+ * Whether anything in this project can reach a shell.
22
+ *
23
+ * Read off the *resolved* tool lists rather than off the config's selectors,
24
+ * because `sandbox:*`, `*` and a bare name all mean the same thing by the time
25
+ * the loader is done, and only one of those three is greppable.
26
+ */
27
+ export declare function usesSandbox(project: AgentProject): boolean;
28
+ /**
29
+ * Asked before the first turn rather than at the first tool call, so a missing
30
+ * container engine costs nothing instead of costing a round trip and half a
31
+ * plan. A project that never shells out never gets here at all.
32
+ */
33
+ export declare function preflight(setup: SandboxSetup, yes?: boolean): Promise<void>;
34
+ /** Best-effort teardown: losing a container must never lose a run. */
35
+ export declare function teardown(pool: SandboxPool): Promise<void>;
36
+ //# sourceMappingURL=sandbox.d.ts.map
@@ -0,0 +1,104 @@
1
+ import { mkdirSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { DEFAULT_SANDBOX_IMAGE, SANDBOX_GROUP, SandboxPool, } from '@zenera/neo';
4
+ import { ensurePodmanReady } from "./podman.js";
5
+ import { warn } from "./term.js";
6
+ // ---------------------------------------------------------------------------
7
+ // The sandbox, as this CLI wires it
8
+ //
9
+ // The library takes resolved values; the config states names and numbers. This
10
+ // is the layer between the two, and it owns the two decisions the library has
11
+ // no business making: which host variables are worth forwarding, and where the
12
+ // container's home directory lives.
13
+ //
14
+ // It lives in the session directory, which is the whole point. A session is
15
+ // already a self-contained thing — copy the directory and its conversation,
16
+ // memory and blobs travel with it — and a `pip install --user` that vanished
17
+ // on close would be the only part of a session that did not. So `$HOME` is a
18
+ // bind mount into `.data/sandbox/home`, and what the agent installs for itself
19
+ // is still there when the session is opened again. Everything *outside* the
20
+ // mounts is throwaway, which is what keeps a stale container from becoming a
21
+ // second, invisible configuration.
22
+ // ---------------------------------------------------------------------------
23
+ /** Where the persistent home lives, inside the container. */
24
+ const HOME = '/home/agent';
25
+ export function buildSandbox(opts) {
26
+ const base = { ...(opts.config.sandbox ?? {}), ...(opts.image ? { image: opts.image } : {}) };
27
+ const home = join(opts.session.data, 'sandbox', 'home');
28
+ const mounts = [{ host: home, at: HOME }];
29
+ const spec = toSpec(base, { HOME });
30
+ const agents = {};
31
+ for (const agent of opts.config.agents) {
32
+ if (agent.sandbox) {
33
+ agents[agent.name] = toSpec({ ...base, ...agent.sandbox }, { HOME });
34
+ }
35
+ }
36
+ const pool = new SandboxPool({
37
+ ...spec,
38
+ agents,
39
+ root: opts.workspace,
40
+ key: opts.session.id,
41
+ readOnly: opts.readOnly,
42
+ mounts,
43
+ });
44
+ return { pool, spec, image: spec.image ?? DEFAULT_SANDBOX_IMAGE, home };
45
+ }
46
+ /**
47
+ * Config names a variable; this reads it. A name that is not set on this host
48
+ * is simply not forwarded — an empty string in the container is a different
49
+ * thing from an absent one, and tools test for absence.
50
+ */
51
+ function toSpec(config, extra) {
52
+ const env = { ...extra };
53
+ for (const name of config.env ?? []) {
54
+ const value = process.env[name];
55
+ if (value !== undefined && value !== '') {
56
+ env[name] = value;
57
+ }
58
+ }
59
+ return {
60
+ image: config.image,
61
+ cpus: config.cpus,
62
+ memory: config.memory,
63
+ network: config.network,
64
+ workdir: config.workdir,
65
+ timeout: config.timeout,
66
+ user: config.user,
67
+ persist: config.persist,
68
+ env,
69
+ };
70
+ }
71
+ /**
72
+ * Whether anything in this project can reach a shell.
73
+ *
74
+ * Read off the *resolved* tool lists rather than off the config's selectors,
75
+ * because `sandbox:*`, `*` and a bare name all mean the same thing by the time
76
+ * the loader is done, and only one of those three is greppable.
77
+ */
78
+ export function usesSandbox(project) {
79
+ return project.agents.some((a) => a.tools.some((t) => t.group === SANDBOX_GROUP));
80
+ }
81
+ /**
82
+ * Asked before the first turn rather than at the first tool call, so a missing
83
+ * container engine costs nothing instead of costing a round trip and half a
84
+ * plan. A project that never shells out never gets here at all.
85
+ */
86
+ export async function preflight(setup, yes) {
87
+ mkdirSync(setup.home, { recursive: true });
88
+ await ensurePodmanReady({
89
+ image: setup.image,
90
+ cpus: setup.spec.cpus,
91
+ memory: setup.spec.memory,
92
+ yes,
93
+ });
94
+ }
95
+ /** Best-effort teardown: losing a container must never lose a run. */
96
+ export async function teardown(pool) {
97
+ try {
98
+ await pool.dispose();
99
+ }
100
+ catch (err) {
101
+ warn(`could not clean up the sandbox: ${err instanceof Error ? err.message : String(err)}`);
102
+ }
103
+ }
104
+ //# sourceMappingURL=sandbox.js.map
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Writes `.vscode/settings.json` under `dir`, replacing what is there. The file
3
+ * is ours: it says how the editor is to treat a directory the agents write
4
+ * into, and a stale copy of that answer is worse than none. Returns the
5
+ * relative path.
6
+ */
7
+ export declare function editorSettings(dir: string): string;
8
+ /**
9
+ * Writes the `.github/` tree under `dir`, replacing what is there — it
10
+ * describes the file formats of the version of `zen` in hand, so the current
11
+ * one is the only one worth having. Returns the relative paths written.
12
+ */
13
+ export declare function copilotInstructions(dir: string): string[];
14
+ export interface ScaffoldOptions {
15
+ /** the project directory */
16
+ dir: string;
17
+ model: string;
18
+ /** extra lines for the model's configuration; their presence picks the object form */
19
+ modelOptions?: string;
20
+ /** give the default agent `exa:*` — set when a key for it is on hand */
21
+ web?: boolean;
22
+ }
23
+ /**
24
+ * Writes a project. Never overwrites the project's own files: the caller
25
+ * decides whether it may. The editor files are the exception — they are ours,
26
+ * and are replaced.
27
+ */
28
+ export declare function scaffold(opts: ScaffoldOptions): string[];
29
+ //# sourceMappingURL=scaffold.d.ts.map
@@ -0,0 +1,220 @@
1
+ import { mkdirSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ // ---------------------------------------------------------------------------
5
+ // Scaffolding
6
+ //
7
+ // What `zen init` writes. Deliberately close to empty: a template full of
8
+ // commented-out options is a template nobody reads and everybody deletes. The
9
+ // one agent here works as written, and every other knob is in `docs/`.
10
+ // ---------------------------------------------------------------------------
11
+ const INSTRUCTIONS_MD = `# House rules
12
+
13
+ Everything in this file is prepended to every agent's prompt, so it is the
14
+ place for the things that are true regardless of who is answering: tone,
15
+ constraints, what to do when the answer is not knowable.
16
+
17
+ Replace this with yours.
18
+ `;
19
+ /**
20
+ * The `model:` section, which is one line until it has to say more.
21
+ *
22
+ * A shorthand cannot carry options, and the object form cannot carry a
23
+ * shorthand — its `model:` is the bare id the API is sent — so asking for
24
+ * reasoning means splitting the ref back into the two fields and giving the
25
+ * configuration a name to be referred to by.
26
+ */
27
+ const MODEL_SECTION = (ref, options) => {
28
+ const colon = ref.indexOf(':');
29
+ if (!options || colon < 0) {
30
+ return ('# The model an agent uses when it does not pin its own. Change it here and the\n' +
31
+ '# whole project moves. The prefix is the *provider* name, not the vendor — drop\n' +
32
+ '# it and the id goes to the default provider, whatever the id looks like.\n' +
33
+ `model: ${ref}`);
34
+ }
35
+ const indented = options
36
+ .trimEnd()
37
+ .split('\n')
38
+ .map((line) => (line ? ` ${line}` : ''))
39
+ .join('\n');
40
+ return ('# The model an agent uses when it does not pin its own. Change it here and the\n' +
41
+ '# whole project moves. A named configuration is what gives the knobs below\n' +
42
+ '# somewhere to live; a bare `model: <provider>:<id>` works when there are none.\n' +
43
+ 'models:\n' +
44
+ ' main:\n' +
45
+ ` provider: ${ref.slice(0, colon)}\n` +
46
+ ` model: ${ref.slice(colon + 1)}\n` +
47
+ `${indented}\n` +
48
+ '\n' +
49
+ 'model: main');
50
+ };
51
+ /**
52
+ * Added above the tool list when the project is scaffolded with web access.
53
+ * The group is registered whether or not a key exists, so this only ever
54
+ * changes what the agent is allowed to reach for.
55
+ */
56
+ const EXA_NOTE = `
57
+ #
58
+ # exa:* is web search and page reading, here because this machine has an
59
+ # Exa key. The key is read from the environment when a tool is called,
60
+ # so a clone of this project without one still loads and only the call
61
+ # fails.`;
62
+ const AGENTS_YAML = (model, options, web) => `# Who exists, and what they may reach for.
63
+ #
64
+ version: 1
65
+
66
+ ${MODEL_SECTION(model, options)}
67
+
68
+ # The container \`sandbox:*\` commands run in. \`persist: true\` keeps it between
69
+ # runs instead of throwing it away, so what the agent installs is still there
70
+ # next time — otherwise only /workspace and its home directory survive, and an
71
+ # \`apt-get\` or a root \`pip install\` is repeated on every run. \`zen sandbox
72
+ # clean\` removes the ones left behind. Everything else has a default; see the
73
+ # sandbox: block in docs/agents-yaml.md to size or pin the image.
74
+ sandbox:
75
+ persist: true
76
+
77
+ agents:
78
+ - name: default
79
+ description: The entry point.
80
+ # Instructions live in agents/prompts/<name>.md and are picked up by
81
+ # convention — no need to name the file here.
82
+ #
83
+ # workspace:* is every file tool at once, sandbox:* is the shell. Name
84
+ # them one by one to be narrower, or subtract: [workspace:*, -delete_file]
85
+ #
86
+ # sandbox:* runs commands in a container, not on this machine, so it
87
+ # needs podman — \`zen run\` installs and starts what it can on its own,
88
+ # and \`zen sandbox status\` says where that got to. Drop the line if you
89
+ # would rather this agent never reached a shell.${web ? EXA_NOTE : ''}
90
+ tools:
91
+ - workspace:*
92
+ - sandbox:*${web ? '\n - exa:*' : ''}
93
+ `;
94
+ const PROMPT = `You are a helpful assistant working inside a project workspace.
95
+
96
+ You have tools to read, search and edit files. The workspace is the only
97
+ place you can see; paths are relative to its root.
98
+
99
+ You can also run shell commands. They run in a container over the same
100
+ workspace, not on the user's machine, so a command that fails there has cost
101
+ them nothing — but it is still their work in the directory, so read before you
102
+ overwrite and say what you ran.
103
+
104
+ Read a file before you change it: \`apply_patch\` matches the surrounding text
105
+ exactly, so a patch written from memory will not apply. Use \`apply_patch\` to
106
+ change part of a file and \`write_file\` only for a new one.
107
+
108
+ Say what you changed.
109
+ `;
110
+ const GITIGNORE = `# Sessions hold run state, memory, blobs and whatever the agent wrote.
111
+ # None of it is source.
112
+ sessions/
113
+ `;
114
+ // ---------------------------------------------------------------------------
115
+ // Telling the editor which instructions are not for it
116
+ //
117
+ // The project's house rules are `INSTRUCTIONS.md`, deliberately not
118
+ // `AGENTS.md`: every coding assistant now reads that name out of an open
119
+ // folder and feeds it to itself as always-on instructions, and `zen open`
120
+ // opens exactly this directory. A name nobody else claims means the two are
121
+ // never confused, and `chat.useAgentsMdFile` no longer has to be switched off
122
+ // to keep them apart.
123
+ //
124
+ // `chat.useNestedAgentsMdFiles` is still written. It is already false by
125
+ // default, but it is opt-in globally, and this is a directory the agent itself
126
+ // writes into — someone who turned it on would otherwise have the editor pick
127
+ // up whatever `AGENTS.md` a run happened to leave behind. It is a *restricted*
128
+ // setting, so it applies only in a trusted workspace; that is the right way
129
+ // round, since an untrusted folder is not one to run agents in either.
130
+ // ---------------------------------------------------------------------------
131
+ const VSCODE_SETTINGS = `{
132
+ "chat.useNestedAgentsMdFiles": false,
133
+ "chat.tools.terminal.autoApprove": {
134
+ "zen": true
135
+ }
136
+ }
137
+ `;
138
+ /**
139
+ * Writes `.vscode/settings.json` under `dir`, replacing what is there. The file
140
+ * is ours: it says how the editor is to treat a directory the agents write
141
+ * into, and a stale copy of that answer is worse than none. Returns the
142
+ * relative path.
143
+ */
144
+ export function editorSettings(dir) {
145
+ const rel = join('.vscode', 'settings.json');
146
+ mkdirSync(join(dir, '.vscode'), { recursive: true });
147
+ writeFileSync(join(dir, rel), VSCODE_SETTINGS);
148
+ return rel;
149
+ }
150
+ // ---------------------------------------------------------------------------
151
+ // The other half of the editor story
152
+ //
153
+ // `INSTRUCTIONS.md` addresses the *project's* agents. The editor's assistant
154
+ // still needs a brief of its own, and what it needs to know is how this kind
155
+ // of project is put together — the file formats, how a prompt is written, when
156
+ // to add a skill rather than an agent. That is a whole `.github/` tree — the
157
+ // standing brief, plus the prompt files and skills the editor picks up from
158
+ // the same place — kept as files rather than template literals in here: they
159
+ // are full of backticks and `${...}` examples, which a TS template literal
160
+ // cannot hold without escaping every one of them into illegibility.
161
+ //
162
+ // `templates/.github/` mirrors what lands in the project one for one, so
163
+ // adding a skill or a prompt file is adding a file there and nothing else.
164
+ // ---------------------------------------------------------------------------
165
+ const GITHUB_TEMPLATE = fileURLToPath(new URL('../templates/.github', import.meta.url));
166
+ /**
167
+ * Writes the `.github/` tree under `dir`, replacing what is there — it
168
+ * describes the file formats of the version of `zen` in hand, so the current
169
+ * one is the only one worth having. Returns the relative paths written.
170
+ */
171
+ export function copilotInstructions(dir) {
172
+ return copyTree(GITHUB_TEMPLATE, dir, '.github');
173
+ }
174
+ /**
175
+ * Copies one template directory into `dir` at `rel`, depth first, sorted so
176
+ * the list it returns is the same on every machine.
177
+ */
178
+ function copyTree(from, dir, rel) {
179
+ const written = [];
180
+ mkdirSync(join(dir, rel), { recursive: true });
181
+ const entries = readdirSync(from, { withFileTypes: true });
182
+ entries.sort((a, b) => a.name.localeCompare(b.name));
183
+ for (const entry of entries) {
184
+ const child = join(rel, entry.name);
185
+ if (entry.isDirectory()) {
186
+ written.push(...copyTree(join(from, entry.name), dir, child));
187
+ }
188
+ else {
189
+ writeFileSync(join(dir, child), readFileSync(join(from, entry.name)));
190
+ written.push(child);
191
+ }
192
+ }
193
+ return written;
194
+ }
195
+ /**
196
+ * Writes a project. Never overwrites the project's own files: the caller
197
+ * decides whether it may. The editor files are the exception — they are ours,
198
+ * and are replaced.
199
+ */
200
+ export function scaffold(opts) {
201
+ const written = [];
202
+ const put = (rel, body) => {
203
+ const path = join(opts.dir, rel);
204
+ mkdirSync(join(path, '..'), { recursive: true });
205
+ writeFileSync(path, body, { flag: 'wx' });
206
+ written.push(rel);
207
+ };
208
+ mkdirSync(join(opts.dir, 'agents', 'prompts'), { recursive: true });
209
+ mkdirSync(join(opts.dir, 'agents', 'skills'), { recursive: true });
210
+ mkdirSync(join(opts.dir, 'sessions'), { recursive: true });
211
+ put('INSTRUCTIONS.md', INSTRUCTIONS_MD);
212
+ put('agents.yaml', AGENTS_YAML(opts.model, opts.modelOptions, opts.web));
213
+ put(join('agents', 'prompts', 'default.md'), PROMPT);
214
+ put('.gitignore', GITIGNORE);
215
+ // The project directory is what `zen open` opens, so this is where the
216
+ // editor actually reads them.
217
+ written.push(editorSettings(opts.dir), ...copilotInstructions(opts.dir));
218
+ return written;
219
+ }
220
+ //# sourceMappingURL=scaffold.js.map
@@ -0,0 +1,77 @@
1
+ export interface SessionPaths {
2
+ id: string;
3
+ dir: string;
4
+ /** what the agent can see and write */
5
+ workspace: string;
6
+ data: string;
7
+ /** the live, resumable state — rewritten after every run */
8
+ state: string;
9
+ memory: string;
10
+ blobs: string;
11
+ runs: string;
12
+ lock: string;
13
+ meta: string;
14
+ }
15
+ export declare function sessionPaths(projectDir: string, id: string): SessionPaths;
16
+ export interface SessionMeta {
17
+ version: 1;
18
+ id: string;
19
+ createdAt: string;
20
+ /**
21
+ * Absolute path the agent's file tools are rooted at. Recorded so resuming
22
+ * never re-asks and never silently moves — a session that quietly changed
23
+ * what "the workspace" meant between turns would be unexplainable.
24
+ */
25
+ workspace: string;
26
+ lastRunAt?: string;
27
+ title?: string;
28
+ }
29
+ export declare function createSession(projectDir: string, id: string, workspace: string): SessionPaths;
30
+ export declare function readSessionMeta(p: SessionPaths): Promise<SessionMeta>;
31
+ export declare function writeSessionMeta(p: SessionPaths, meta: SessionMeta): void;
32
+ export interface SessionSummary {
33
+ id: string;
34
+ createdAt?: string;
35
+ runs: number;
36
+ lastRunAt?: string;
37
+ busy: boolean;
38
+ title?: string;
39
+ }
40
+ /** Newest first — the order a picker wants. */
41
+ export declare function listSessions(projectDir: string): Promise<SessionSummary[]>;
42
+ export declare function newestSession(projectDir: string): string | undefined;
43
+ export declare function requireSession(projectDir: string, id: string): SessionPaths;
44
+ export interface Held {
45
+ release(): void;
46
+ }
47
+ export declare function acquire(p: SessionPaths): Held;
48
+ export interface RunPaths {
49
+ id: string;
50
+ dir: string;
51
+ input: string;
52
+ output: string;
53
+ state: string;
54
+ report: string;
55
+ meta: string;
56
+ }
57
+ export declare function runPaths(session: SessionPaths, id: string): RunPaths;
58
+ export declare function createRun(session: SessionPaths): RunPaths;
59
+ export interface RunMeta {
60
+ version: 1;
61
+ id: string;
62
+ session: string;
63
+ startedAt: string;
64
+ finishedAt: string;
65
+ durationMs: number;
66
+ agent: string;
67
+ stopReason: string;
68
+ turns: number;
69
+ usage: unknown;
70
+ workspace: string;
71
+ error?: string;
72
+ }
73
+ export declare function writeRunMeta(p: RunPaths, meta: RunMeta): void;
74
+ export declare function newestRun(session: SessionPaths): string | undefined;
75
+ /** A path to show a human: relative when that is shorter, absolute otherwise. */
76
+ export declare function display(path: string, from?: string): string;
77
+ //# sourceMappingURL=session.d.ts.map