@zenera/cli 1.1.16 → 1.1.18

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 (71) hide show
  1. package/README.md +110 -19
  2. package/dist/banner.d.ts +19 -0
  3. package/dist/banner.js +12 -9
  4. package/dist/catalog.js +19 -20
  5. package/dist/command.d.ts +3 -0
  6. package/dist/commands/check.js +5 -1
  7. package/dist/commands/index.js +4 -1
  8. package/dist/commands/init.js +42 -7
  9. package/dist/commands/inspect.js +77 -13
  10. package/dist/commands/key.js +49 -1
  11. package/dist/commands/memory.js +16 -3
  12. package/dist/commands/meta.d.ts +3 -0
  13. package/dist/commands/meta.js +596 -0
  14. package/dist/commands/run.js +64 -35
  15. package/dist/engine.d.ts +15 -0
  16. package/dist/engine.js +26 -1
  17. package/dist/env.d.ts +20 -0
  18. package/dist/env.js +65 -0
  19. package/dist/home.d.ts +2 -0
  20. package/dist/home.js +2 -0
  21. package/dist/keys.d.ts +2 -0
  22. package/dist/keys.js +2 -0
  23. package/dist/main.js +1 -1
  24. package/dist/meta.d.ts +221 -0
  25. package/dist/meta.js +695 -0
  26. package/dist/podman.js +64 -2
  27. package/dist/projects.d.ts +9 -1
  28. package/dist/projects.js +19 -2
  29. package/dist/resolve.js +7 -1
  30. package/dist/sandbox.d.ts +11 -0
  31. package/dist/sandbox.js +24 -4
  32. package/dist/scaffold.d.ts +6 -0
  33. package/dist/scaffold.js +56 -15
  34. package/dist/session.d.ts +10 -0
  35. package/dist/session.js +19 -1
  36. package/dist/term.d.ts +7 -0
  37. package/dist/term.js +8 -1
  38. package/dist/tui/app.js +33 -5
  39. package/dist/tui/wrap.d.ts +1 -1
  40. package/dist/tui/wrap.js +1 -1
  41. package/dist/validate.js +57 -15
  42. package/package.json +3 -2
  43. package/templates/editor/.github/copilot-instructions.md +313 -94
  44. package/templates/editor/.github/prompts/project-review.prompt.md +74 -0
  45. package/templates/editor/.github/prompts/spec-apply-feedback.prompt.md +142 -0
  46. package/templates/editor/.github/prompts/spec-sync-project.prompt.md +392 -0
  47. package/templates/editor/.github/skills/zen-cli/references/check.md +6 -3
  48. package/templates/editor/.github/skills/zen-cli/references/frame.md +5 -3
  49. package/templates/editor/.github/skills/zen-cli/references/inspect.md +4 -3
  50. package/templates/editor/.github/skills/zen-cli/references/memory.md +14 -0
  51. package/templates/editor/.github/skills/zen-cli/references/projects.md +19 -2
  52. package/templates/editor/.github/skills/zen-cli/references/run.md +75 -16
  53. package/templates/editor/.github/skills/zen-cli/references/sandbox.md +6 -0
  54. package/templates/editor/.github/skills/zen-memory/SKILL.md +153 -33
  55. package/templates/editor/.github/skills/zen-memory/scripts/check-instructions.sh +115 -0
  56. package/templates/editor/.github/skills/zen-review/SKILL.md +123 -0
  57. package/templates/editor/.github/skills/zen-review/scripts/check-paths.sh +119 -0
  58. package/templates/editor/.github/skills/zen-review/scripts/review.sh +104 -0
  59. package/templates/editor/.github/skills/zen-spec-sync/SKILL.md +197 -0
  60. package/templates/editor/.github/skills/zen-spec-sync/scripts/snapshot.sh +200 -0
  61. package/templates/editor/.vscode/settings.json +44 -4
  62. package/templates/parts/memory-terms.yaml.tmpl +16 -0
  63. package/templates/parts/memory.yaml.tmpl +12 -0
  64. package/templates/project/SPECIFICATION.md.tmpl +32 -2
  65. package/templates/project/agents/memory-instructions.md +278 -0
  66. package/templates/project/agents.yaml.tmpl +12 -3
  67. package/templates/project/env +28 -0
  68. package/templates/project/gitignore +4 -4
  69. package/templates/editor/.github/prompts/review-project.prompt.md +0 -36
  70. package/templates/editor/.github/prompts/sync-with-spec.prompt.md +0 -205
  71. /package/templates/project/{INSTRUCTIONS.md → agents/instructions.md} +0 -0
@@ -5,31 +5,39 @@ import { readFile, writeFile } from 'node:fs/promises';
5
5
  import { createServer } from 'node:http';
6
6
  import { extname, join, normalize, resolve, sep } from 'node:path';
7
7
  import { parse } from "../args.js";
8
+ import { sessionIds } from "../projects.js";
8
9
  import { project as resolveProject } from "../resolve.js";
9
- import { display, newestRun, newestSession, requireSession, runPaths, sessionPaths, } from "../session.js";
10
- import { bold, cyan, dim, invalidError, json, note, write } from "../term.js";
10
+ import { display, listRuns, listSessions, newestRun, requireSession, runPaths, sessionPaths, } from "../session.js";
11
+ import { ago, bold, choose, cyan, dim, invalidError, isInteractive, json, note, write, } from "../term.js";
11
12
  const USAGE = 'zen inspect [run] [--session <id>] [--open] [--rebuild] [--serve [port]]';
12
13
  export const inspect = {
13
14
  summary: "Open or rebuild a run's report.html.",
14
15
  usage: USAGE,
15
16
  details: [
16
- 'With no arguments: the newest run of the newest session.',
17
+ 'With no arguments: asks which session and run, or takes the newest of',
18
+ 'each when there is nothing to ask on.',
17
19
  '--serve starts a local server, which the report needs for its assets.',
20
+ '--memory <dir> reads that memory instead of the project’s, for a run',
21
+ 'that was given `zen run --memory`.',
18
22
  ],
19
23
  run: async (ctx) => {
20
24
  const { values, positionals } = parse(ctx.args, {
21
25
  project: { type: 'string' },
22
26
  session: { type: 'string' },
27
+ memory: { type: 'string' },
23
28
  open: { type: 'boolean' },
24
29
  rebuild: { type: 'boolean' },
25
30
  serve: { type: 'string' },
26
31
  }, USAGE);
27
32
  const found = await resolveProject({ cwd: ctx.cwd, project: values.project });
28
33
  const dir = found.dir;
29
- const session = pickSession(dir, values.session);
30
- const run = pickRun(session, positionals[0]);
34
+ // Asking is only possible at a terminal, and only honest when the
35
+ // answer is not being parsed by something.
36
+ const asking = isInteractive() && !ctx.json;
37
+ const session = await pickSession(dir, values.session, positionals[0], asking);
38
+ const run = await pickRun(session, positionals[0], asking);
31
39
  if (values.rebuild || !existsSync(run.report)) {
32
- await rebuild(session, run, await memory(dir));
40
+ await rebuild(session, run, await memory(dir, values.memory, ctx.cwd));
33
41
  }
34
42
  if (ctx.json) {
35
43
  json({ session: session.id, run: run.id, report: run.report });
@@ -52,17 +60,73 @@ export const inspect = {
52
60
  // ---------------------------------------------------------------------------
53
61
  // Choosing what to show
54
62
  // ---------------------------------------------------------------------------
55
- function pickSession(dir, asked) {
63
+ /**
64
+ * Which session to read. With nothing named and a terminal to ask on, the
65
+ * person picks: "the newest" is a guess about which of a dozen runs they meant,
66
+ * and a wrong guess looks the same as a broken command.
67
+ */
68
+ async function pickSession(dir, asked, run, asking) {
56
69
  if (asked) {
57
70
  return requireSession(dir, asked);
58
71
  }
59
- const newest = newestSession(dir);
60
- if (!newest) {
72
+ if (sessionIds(dir).length === 0) {
61
73
  throw invalidError('nothing has been run here yet', 'start one: zen run');
62
74
  }
63
- return sessionPaths(dir, newest);
75
+ // A named run answers the question itself: it is in whichever session holds it.
76
+ if (!asking || run) {
77
+ return newestWorked(dir, run);
78
+ }
79
+ const worked = (await listSessions(dir)).filter((s) => s.runs > 0);
80
+ if (worked.length === 0) {
81
+ throw invalidError('no runs yet — every session is empty', 'start one: zen run');
82
+ }
83
+ return await choose('Which session?', worked.map((s) => ({
84
+ label: s.id,
85
+ detail: [
86
+ s.title,
87
+ `${s.runs} run${s.runs === 1 ? '' : 's'}`,
88
+ ago(s.lastRunAt ?? s.createdAt),
89
+ s.busy ? 'running' : '',
90
+ ]
91
+ .filter(Boolean)
92
+ .join(' '),
93
+ value: sessionPaths(dir, s.id),
94
+ })));
64
95
  }
65
- function pickRun(session, asked) {
96
+ /**
97
+ * The newest session that has a report to show — not simply the newest. A
98
+ * session exists before its first run and outlives one that never recorded
99
+ * anything, so the newest is routinely empty and picking it blindly answers
100
+ * "has no runs" about a project full of them.
101
+ */
102
+ function newestWorked(dir, run) {
103
+ for (const id of sessionIds(dir).reverse()) {
104
+ const session = sessionPaths(dir, id);
105
+ if (run ? existsSync(runPaths(session, run).dir) : newestRun(session)) {
106
+ return session;
107
+ }
108
+ }
109
+ throw invalidError(run ? `no run ${run} in any session` : 'no runs yet — every session is empty', run ? 'see: zen list --sessions' : 'start one: zen run');
110
+ }
111
+ async function pickRun(session, asked, asking) {
112
+ if (!asked && asking) {
113
+ const runs = await listRuns(session);
114
+ if (runs.length === 0) {
115
+ throw invalidError(`session ${session.id} has no runs`);
116
+ }
117
+ // One run is not a question.
118
+ return await choose(`Which run of ${session.id}?`, runs.map((r) => ({
119
+ label: r.id,
120
+ detail: [
121
+ r.agent,
122
+ r.error ? 'failed' : r.stopReason,
123
+ ago(r.finishedAt ?? r.startedAt),
124
+ ]
125
+ .filter(Boolean)
126
+ .join(' '),
127
+ value: runPaths(session, r.id),
128
+ })));
129
+ }
66
130
  const id = asked ?? newestRun(session);
67
131
  if (!id) {
68
132
  throw invalidError(`session ${session.id} has no runs`);
@@ -98,9 +162,9 @@ async function rebuild(session, run, store) {
98
162
  * has the shape of what the run recalled but not a word of it; a run that
99
163
  * never touched memory pays nothing, because the report asks for no node.
100
164
  */
101
- async function memory(dir) {
165
+ async function memory(dir, override, cwd) {
102
166
  const { config } = readProjectConfig(dir);
103
- const at = memoryDir(dir, config);
167
+ const at = memoryDir(dir, config, override && resolve(cwd, override));
104
168
  if (!at || !existsSync(join(at, 'manifest.json'))) {
105
169
  return undefined;
106
170
  }
@@ -454,10 +454,57 @@ const env = async (ctx, args) => {
454
454
  function shellQuote(value) {
455
455
  return `'${value.replaceAll("'", `'\\''`)}'`;
456
456
  }
457
+ /**
458
+ * One credential on stdout, alone, for a tool that wants a fresh one before
459
+ * every request rather than an export that goes stale — `zen meta` hands this
460
+ * to copilot as `COPILOT_PROVIDER_API_KEY_COMMAND`.
461
+ *
462
+ * A stored secret is simply printed. A service-account file is exchanged for
463
+ * an OAuth access token, which is the part worth having a command for: those
464
+ * last an hour, and a session does not.
465
+ */
466
+ const token = async (ctx, args) => {
467
+ const { positionals } = parse(args, {}, 'zen key token <provider>[/name]');
468
+ const { provider, name } = parseRef(positionals[0] ?? 'vertex');
469
+ const store = await KeyStore.open();
470
+ const entry = name ? store.find(provider, name) : store.active(provider);
471
+ if (!entry) {
472
+ throw credentialError(`no ${provider} key`, `add one: zen key add ${provider}`);
473
+ }
474
+ if (entry.holds === 'secret') {
475
+ write(store.reveal(entry));
476
+ return;
477
+ }
478
+ const { GoogleAuth } = await import('google-auth-library');
479
+ const auth = new GoogleAuth({
480
+ keyFile: store.fileOf(entry),
481
+ scopes: ['https://www.googleapis.com/auth/cloud-platform'],
482
+ });
483
+ const access = await (await auth.getClient()).getAccessToken();
484
+ if (!access.token) {
485
+ throw credentialError(`${keyId(entry)} minted no token`, 'check the service account is still enabled');
486
+ }
487
+ if (ctx.json) {
488
+ json({ key: keyId(entry), token: access.token });
489
+ return;
490
+ }
491
+ write(access.token);
492
+ };
457
493
  // ---------------------------------------------------------------------------
458
494
  // Dispatch
459
495
  // ---------------------------------------------------------------------------
460
- const SUBS = { ls, list: ls, add, use, check, rm, remove: rm, show, env };
496
+ const SUBS = {
497
+ ls,
498
+ list: ls,
499
+ add,
500
+ use,
501
+ check,
502
+ rm,
503
+ remove: rm,
504
+ show,
505
+ env,
506
+ token,
507
+ };
461
508
  export const key = {
462
509
  summary: 'The credential store: add, choose, verify and export API keys.',
463
510
  usage: USAGE,
@@ -480,6 +527,7 @@ export const key = {
480
527
  ' zen key rm <provider>/<name> Forget one.',
481
528
  ' zen key show <ref> [--reveal] Masked by default.',
482
529
  ' zen key env [provider …] Shell exports, for other tools.',
530
+ ' zen key token [ref] One credential on stdout, minted now.',
483
531
  ],
484
532
  run: async (ctx) => {
485
533
  const [name, ...rest] = ctx.args;
@@ -2,7 +2,7 @@ import { ALL_AGENTS, buildMemoryReport, hostPath, memoryDir, MemoryIndex, Memory
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 { isAbsolute, join, resolve } from 'node:path';
5
+ import { basename, isAbsolute, join, resolve } from 'node:path';
6
6
  import { parse } from "../args.js";
7
7
  import { project as resolveProject } from "../resolve.js";
8
8
  import { ago, bold, bytes, confirm, count, cyan, dim, green, isInteractive, json, note, table, usageError, write, writeAll, yellow, } from "../term.js";
@@ -39,6 +39,7 @@ export const memory = {
39
39
  ' forget <id...> Remove nodes, their vectors and their files.',
40
40
  '',
41
41
  ' --project <name|dir> Which project. Defaults to the one you are in.',
42
+ ' --dir <dir> Read this memory directory instead of the project’s.',
42
43
  ' --kind <name> Only this kind of node.',
43
44
  ' --audience <name> Only nodes committed under this label.',
44
45
  ' --files Only nodes that remember a file.',
@@ -59,6 +60,7 @@ export const memory = {
59
60
  run: async (ctx) => {
60
61
  const { values, positionals } = parse(ctx.args, {
61
62
  project: { type: 'string' },
63
+ dir: { type: 'string' },
62
64
  kind: { type: 'string' },
63
65
  audience: { type: 'string' },
64
66
  files: { type: 'boolean' },
@@ -73,7 +75,7 @@ export const memory = {
73
75
  throw usageError(`unknown subcommand: ${what}`, USAGE);
74
76
  }
75
77
  const rest = positionals.slice(1);
76
- const opened = await open(ctx.cwd, values.project);
78
+ const opened = await open(ctx.cwd, values.project, values.dir);
77
79
  try {
78
80
  switch (what) {
79
81
  case 'stats':
@@ -98,8 +100,19 @@ export const memory = {
98
100
  * project would resolve models and read every prompt file to inspect a graph
99
101
  * that needs none of it — and would fail on a project whose credentials are
100
102
  * missing, which is exactly when someone is debugging.
103
+ *
104
+ * A named directory is opened as it stands, project or no project: a graph
105
+ * copied out of a running session is the one most worth looking at, and it
106
+ * should not have to be given an `agents.yaml` first.
101
107
  */
102
- async function open(cwd, want) {
108
+ async function open(cwd, want, at) {
109
+ if (at) {
110
+ const dir = resolve(cwd, at);
111
+ if (!existsSync(join(dir, 'manifest.json'))) {
112
+ throw usageError(`${dir} is not a memory`, 'no manifest.json in it');
113
+ }
114
+ return { store: await MemoryStore.open(dir), dir, project: basename(dir) };
115
+ }
103
116
  const project = await resolveProject({ cwd, project: want });
104
117
  const { config } = readProjectConfig(project.dir);
105
118
  const dir = memoryDir(project.dir, config);
@@ -0,0 +1,3 @@
1
+ import type { Command } from '../command.ts';
2
+ export declare const meta: Command;
3
+ //# sourceMappingURL=meta.d.ts.map