@netnodeag/kraftwerk 0.2.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 (53) hide show
  1. package/README.md +339 -0
  2. package/bin/kraftwerk.js +24 -0
  3. package/dist/agent.d.ts +49 -0
  4. package/dist/agent.js +3 -0
  5. package/dist/cli/create-brief.d.ts +4 -0
  6. package/dist/cli/create-brief.js +146 -0
  7. package/dist/cli/doctor.d.ts +1 -0
  8. package/dist/cli/doctor.js +87 -0
  9. package/dist/cli/init.d.ts +1 -0
  10. package/dist/cli/init.js +81 -0
  11. package/dist/cli/kraftwerk.d.ts +1 -0
  12. package/dist/cli/kraftwerk.js +342 -0
  13. package/dist/cli/runs.d.ts +6 -0
  14. package/dist/cli/runs.js +120 -0
  15. package/dist/cli.d.ts +13 -0
  16. package/dist/cli.js +47 -0
  17. package/dist/config.d.ts +41 -0
  18. package/dist/config.js +97 -0
  19. package/dist/discover.d.ts +21 -0
  20. package/dist/discover.js +38 -0
  21. package/dist/envelope.d.ts +21 -0
  22. package/dist/envelope.js +61 -0
  23. package/dist/gates.d.ts +18 -0
  24. package/dist/gates.js +34 -0
  25. package/dist/harness.d.ts +66 -0
  26. package/dist/harness.js +14 -0
  27. package/dist/harnesses/claude.d.ts +2 -0
  28. package/dist/harnesses/claude.js +117 -0
  29. package/dist/harnesses/codex.d.ts +2 -0
  30. package/dist/harnesses/codex.js +158 -0
  31. package/dist/harnesses/pi.d.ts +2 -0
  32. package/dist/harnesses/pi.js +151 -0
  33. package/dist/harnesses/registry.d.ts +2 -0
  34. package/dist/harnesses/registry.js +19 -0
  35. package/dist/index.d.ts +23 -0
  36. package/dist/index.js +22 -0
  37. package/dist/remote.d.ts +23 -0
  38. package/dist/remote.js +57 -0
  39. package/dist/run.d.ts +66 -0
  40. package/dist/run.js +278 -0
  41. package/dist/runner/docker.d.ts +38 -0
  42. package/dist/runner/docker.js +166 -0
  43. package/dist/stats.d.ts +46 -0
  44. package/dist/stats.js +65 -0
  45. package/dist/validate.d.ts +8 -0
  46. package/dist/validate.js +33 -0
  47. package/dist/workflow.d.ts +24 -0
  48. package/dist/workflow.js +11 -0
  49. package/dist/yaml.d.ts +21 -0
  50. package/dist/yaml.js +324 -0
  51. package/package.json +52 -0
  52. package/runner/Dockerfile +43 -0
  53. package/schema/workflow.schema.json +244 -0
@@ -0,0 +1,81 @@
1
+ import { appendFile, mkdir, readFile, stat, writeFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import chalk from "chalk";
4
+ import { SCHEMA_URL } from "../config.js";
5
+ /**
6
+ * `kraftwerk init` — make any repository a kraftwerk consumer in one
7
+ * command: kraftwerk.yml (root marker + config), workflows/ with a runnable
8
+ * example, and an output/ ignore entry. Everything is idempotent: existing
9
+ * files are left untouched and reported.
10
+ */
11
+ const CONFIG_TEMPLATE = `# kraftwerk project config — also marks the project root for the CLI.
12
+ # All fields optional. Docs: https://github.com/NETNODEAG/kraftwerk
13
+ workflows: workflows # where workflows live
14
+ output: output # where run artifacts land (git-ignored)
15
+ `;
16
+ const WORKFLOW_TEMPLATE = `# yaml-language-server: $schema=${SCHEMA_URL}
17
+ name: hello
18
+ description: "Example workflow: one agent answers the request in a file"
19
+ workspace: |
20
+ Files: answer.md (the result).
21
+ agents:
22
+ assistant:
23
+ model: haiku
24
+ tools: [Read, Write, Edit]
25
+ persona: prompts/assistant.md
26
+ steps:
27
+ - name: answer
28
+ agent: assistant
29
+ prompt: prompts/answer.md
30
+ gates:
31
+ - file_non_empty: answer.md
32
+ `;
33
+ const PERSONA_TEMPLATE = `You are a precise assistant. You answer requests concisely and
34
+ concretely, and store the result as a file.
35
+ `;
36
+ const PROMPT_TEMPLATE = `Request: \${{ request }}
37
+
38
+ Answer the request and write the answer to answer.md
39
+ (Markdown, with a short heading).
40
+ `;
41
+ export async function runInit(cwd) {
42
+ const exists = async (p) => !!(await stat(p).catch(() => null));
43
+ const created = [];
44
+ const skipped = [];
45
+ const put = async (rel, content) => {
46
+ const abs = path.join(cwd, rel);
47
+ if (await exists(abs)) {
48
+ skipped.push(rel);
49
+ return;
50
+ }
51
+ await mkdir(path.dirname(abs), { recursive: true });
52
+ await writeFile(abs, content);
53
+ created.push(rel);
54
+ };
55
+ await put("kraftwerk.yml", CONFIG_TEMPLATE);
56
+ await put("workflows/hello/workflow.yml", WORKFLOW_TEMPLATE);
57
+ await put("workflows/hello/prompts/assistant.md", PERSONA_TEMPLATE);
58
+ await put("workflows/hello/prompts/answer.md", PROMPT_TEMPLATE);
59
+ // .gitignore: append output/ if it's not covered yet.
60
+ const gitignorePath = path.join(cwd, ".gitignore");
61
+ const gitignore = (await readFile(gitignorePath, "utf8").catch(() => null)) ?? null;
62
+ if (gitignore === null) {
63
+ await writeFile(gitignorePath, "output/\n");
64
+ created.push(".gitignore");
65
+ }
66
+ else if (!gitignore.split("\n").some((l) => l.trim().replace(/\/$/, "") === "output")) {
67
+ await appendFile(gitignorePath, `${gitignore.endsWith("\n") ? "" : "\n"}output/\n`);
68
+ created.push(".gitignore (output/ added)");
69
+ }
70
+ else {
71
+ skipped.push(".gitignore");
72
+ }
73
+ for (const f of created)
74
+ console.log(`${chalk.green("✔")} ${f}`);
75
+ for (const f of skipped)
76
+ console.log(`${chalk.dim("• skipped (exists):")} ${chalk.dim(f)}`);
77
+ console.log(`\nNext steps:\n` +
78
+ ` kraftwerk list\n` +
79
+ ` kraftwerk run hello "What is kraftwerk?"\n` +
80
+ chalk.dim(` (editor validation comes from the $schema line in workflow.yml)`));
81
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,342 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { input, select } from "@inquirer/prompts";
4
+ import chalk from "chalk";
5
+ import Table from "cli-table3";
6
+ import { Command } from "commander";
7
+ import ora from "ora";
8
+ import { discoverWorkflows, findWorkflowsRoot } from "../discover.js";
9
+ import { isRemoteSpec, resolveRemote } from "../remote.js";
10
+ import { loadWorkflow, missingEnv } from "../yaml.js";
11
+ import { renderCreateBrief } from "./create-brief.js";
12
+ import { runDoctor } from "./doctor.js";
13
+ import { runInit } from "./init.js";
14
+ import { listRuns, showRun } from "./runs.js";
15
+ /**
16
+ * kraftwerk — the kraftwerk CLI.
17
+ *
18
+ * kraftwerk init scaffold kraftwerk.yml + workflows/ + example
19
+ * kraftwerk list discover + list workflows (--json, --from)
20
+ * kraftwerk run [workflow] [text] run one (prompts interactively if omitted)
21
+ * kraftwerk runs [show <id>] inspect past runs from their traces
22
+ * kraftwerk doctor preflight: harness CLIs, docker, workflows, env
23
+ * kraftwerk validate [paths...] validate without executing
24
+ *
25
+ * Workflows are auto-discovered under src/workflows/ (or workflows/), from
26
+ * any subdirectory (walk-up to kraftwerk.yml / workflows root / .git). No
27
+ * entry file, no registration, no local install needed — a repo with
28
+ * workflow folders plus `npx kraftwerk` is a complete consumer.
29
+ *
30
+ * Machine use (CI, cron, webhooks): `run --json` prints one JSON result on
31
+ * stdout and moves all narration to stderr; KRAFTWERK_YES=1 = --yes.
32
+ * Exit codes: 0 ok, 2 usage/config error, 3 run failed, 1 unexpected.
33
+ *
34
+ * `--from github:org/repo[@ref]` on list/run executes workflows straight
35
+ * from a git remote (shallow clone cache in ~/.cache/kraftwerk).
36
+ */
37
+ /** Working directory for list/run: local cwd or the --from remote clone. */
38
+ async function resolveBaseDir(from) {
39
+ if (!from)
40
+ return process.cwd();
41
+ if (!isRemoteSpec(from)) {
42
+ console.error(chalk.red(`--from "${from}" not recognized — expected github:org/repo[@ref] or a git URL.`));
43
+ process.exit(2);
44
+ }
45
+ return (await resolveRemote(from)).dir;
46
+ }
47
+ const pkg = JSON.parse(await readFile(new URL("../../package.json", import.meta.url), "utf8"));
48
+ const program = new Command()
49
+ .name("kraftwerk")
50
+ .description("kraftwerk CLI: discover, validate, and run YAML workflows")
51
+ .version(pkg.version);
52
+ const agentLabel = (workflow) => workflow.meta.agents
53
+ .map((a) => {
54
+ const harness = a.harness && a.harness !== "claude" ? `${a.harness}:` : "";
55
+ const effort = a.effort ? `, ${a.effort}` : "";
56
+ return `${a.id} ${chalk.dim(`(${harness}${a.model}${effort})`)}`;
57
+ })
58
+ .join("\n");
59
+ program
60
+ .command("list")
61
+ .description("Discover and list the workflows in the current project")
62
+ .option("--json", "Machine-readable output (JSON on stdout)")
63
+ .option("--from <source>", "Workflows from a git remote: github:org/repo[@ref] or git URL")
64
+ .action(async (opts) => {
65
+ const baseDir = await resolveBaseDir(opts.from);
66
+ const spinner = opts.json ? undefined : ora("Discovering workflows ...").start();
67
+ const found = await discoverWorkflows(baseDir);
68
+ spinner?.stop();
69
+ if (opts.json) {
70
+ console.log(JSON.stringify(found.map((e) => ({
71
+ path: e.path,
72
+ name: e.workflow?.name,
73
+ description: e.workflow?.description,
74
+ steps: e.workflow?.meta.steps,
75
+ requires: e.workflow?.meta.requires,
76
+ agents: e.workflow?.meta.agents.map((a) => ({
77
+ id: a.id,
78
+ model: a.model,
79
+ harness: a.harness ?? "claude",
80
+ })),
81
+ error: e.error,
82
+ })), null, 2));
83
+ return;
84
+ }
85
+ if (found.length === 0) {
86
+ console.log(chalk.yellow("No workflows found (expected: src/workflows/ or workflows/ — `kraftwerk init` scaffolds a project)."));
87
+ return;
88
+ }
89
+ const table = new Table({
90
+ head: ["workflow", "description", "steps", "agents"].map((h) => chalk.bold(h)),
91
+ wordWrap: true,
92
+ colWidths: [16, 40, 7, 36],
93
+ });
94
+ for (const entry of found) {
95
+ if (entry.workflow) {
96
+ const requires = entry.workflow.meta.requires;
97
+ table.push([
98
+ chalk.cyan(entry.workflow.name),
99
+ entry.workflow.description +
100
+ (requires.length ? chalk.dim(`\nrequires: ${requires.join(", ")}`) : ""),
101
+ String(entry.workflow.meta.steps.length),
102
+ agentLabel(entry.workflow),
103
+ ]);
104
+ }
105
+ else {
106
+ table.push([
107
+ chalk.red(path.basename(entry.path)),
108
+ chalk.red(entry.error ?? "unknown error"),
109
+ "—",
110
+ "—",
111
+ ]);
112
+ }
113
+ }
114
+ console.log(table.toString());
115
+ });
116
+ program
117
+ .command("run")
118
+ .description("Run a workflow (prompts interactively for anything missing)")
119
+ .argument("[workflow]", "Workflow name from `kraftwerk list`")
120
+ .argument("[request...]", "The request: topic, URL, project idea, ...")
121
+ .option("--yes", "Auto-confirm approval gates (also: KRAFTWERK_YES=1)")
122
+ .option("--verbose", "Stream the agent narration")
123
+ .option("--json", "Non-interactive: JSON result on stdout, narration on stderr")
124
+ .option("--quiet", "Suppress narration (result/errors only)")
125
+ .option("--from <source>", "Workflows from a git remote: github:org/repo[@ref] or git URL")
126
+ .option("--sandbox", "Run inside the Docker sandbox container (image: kraftwerk-runner)")
127
+ .option("--ssh", "Forward the SSH agent + known_hosts into the sandbox (only with --sandbox)")
128
+ .option("--run-id <id>", "Pin the run folder name (output/<id>), e.g. for external triggers")
129
+ .action(async (name, requestParts, opts) => {
130
+ const machine = !!opts.json;
131
+ const fail = (code, message) => {
132
+ if (machine)
133
+ console.log(JSON.stringify({ ok: false, error: message }));
134
+ console.error(chalk.red(message));
135
+ process.exit(code);
136
+ };
137
+ const baseDir = await resolveBaseDir(opts.from);
138
+ const spinner = machine ? undefined : ora("Loading workflows ...").start();
139
+ const found = await discoverWorkflows(baseDir);
140
+ spinner?.stop();
141
+ const valid = found.filter((e) => e.workflow);
142
+ if (valid.length === 0)
143
+ fail(2, "No valid workflows found.");
144
+ let entry = name ? valid.find((e) => e.workflow.name === name) : undefined;
145
+ if (name && !entry) {
146
+ fail(2, `Workflow "${name}" not found. Available: ${valid.map((e) => e.workflow.name).join(", ")}`);
147
+ }
148
+ if (!entry && machine)
149
+ fail(2, "No workflow given (--json is non-interactive).");
150
+ entry ??= await select({
151
+ message: "Which workflow?",
152
+ choices: valid.map((e) => ({
153
+ name: `${e.workflow.name} — ${e.workflow.description}`,
154
+ value: e,
155
+ })),
156
+ });
157
+ const workflow = entry.workflow;
158
+ let request = (requestParts ?? []).join(" ").trim();
159
+ if (!request && machine)
160
+ fail(2, "No request given (--json is non-interactive).");
161
+ if (!request)
162
+ request = (await input({ message: "Request (topic, URL, ...):" })).trim();
163
+ if (!request)
164
+ fail(2, "No request given.");
165
+ const missing = missingEnv(workflow.meta.requires);
166
+ if (missing.length > 0) {
167
+ fail(2, `Workflow "${workflow.name}" needs environment variables that are missing: ${missing.join(", ")}`);
168
+ }
169
+ if (opts.sandbox) {
170
+ const { runSandboxed } = await import("../runner/docker.js");
171
+ const handle = await runSandboxed({
172
+ projectRoot: baseDir,
173
+ workflowPath: entry.path,
174
+ workflowName: workflow.name,
175
+ request,
176
+ runId: opts.runId,
177
+ ssh: !!opts.ssh,
178
+ mode: "attach",
179
+ });
180
+ console.log(chalk.dim(`sandbox ${handle.containerName} → ${handle.runDir}`));
181
+ process.exit(await handle.finished);
182
+ }
183
+ if (opts.runId) {
184
+ process.env.KRAFTWERK_RUN_DIR = path.resolve("output", opts.runId);
185
+ }
186
+ else if (!process.env.KRAFTWERK_RUN_DIR) {
187
+ // Root the run dir explicitly: at the project's output dir (honors
188
+ // kraftwerk.yml `output:` and works from subdirs); for remote runs at
189
+ // the caller's cwd — never inside the clone cache.
190
+ const { runStamp } = await import("../workflow.js");
191
+ const { resolveProject } = await import("../config.js");
192
+ const outBase = opts.from ? path.resolve("output") : (await resolveProject(baseDir)).outputDir;
193
+ process.env.KRAFTWERK_RUN_DIR = path.join(outBase, `run-${runStamp()}`);
194
+ }
195
+ // Machine/quiet mode: workflow narration must not pollute stdout — the
196
+ // engine logs via console.log, so reroute it for the duration of the run.
197
+ const realLog = console.log;
198
+ if (machine)
199
+ console.log = (...args) => console.error(...args);
200
+ else if (opts.quiet)
201
+ console.log = () => { };
202
+ const autoApprove = !!opts.yes || process.env.KRAFTWERK_YES === "1";
203
+ try {
204
+ const result = await workflow.run({ request, autoApprove, verbose: !!opts.verbose });
205
+ console.log = realLog;
206
+ if (machine) {
207
+ console.log(JSON.stringify({ ok: true, workflow: workflow.name, request, ...result }, null, 2));
208
+ }
209
+ else if (opts.quiet && result) {
210
+ console.log(`ok ${workflow.name} → ${result.runDir}`);
211
+ }
212
+ }
213
+ catch (err) {
214
+ console.log = realLog;
215
+ const message = err.message;
216
+ if (machine)
217
+ console.log(JSON.stringify({ ok: false, workflow: workflow.name, request, error: message }));
218
+ console.error(chalk.red(message));
219
+ process.exit(3);
220
+ }
221
+ });
222
+ program
223
+ .command("init")
224
+ .description("Scaffold the project: kraftwerk.yml, workflows/ with an example, .gitignore")
225
+ .action(async () => {
226
+ await runInit(process.cwd());
227
+ });
228
+ program
229
+ .command("doctor")
230
+ .description("Preflight: harness CLIs, docker, workflows, declared environment variables")
231
+ .action(async () => {
232
+ await runDoctor(process.cwd());
233
+ });
234
+ const runs = program
235
+ .command("runs")
236
+ .description("Inspect past runs (from output/*/trace.jsonl)");
237
+ runs
238
+ .command("list", { isDefault: true })
239
+ .description("List runs (newest first)")
240
+ .option("--json", "Machine-readable output")
241
+ .action(async (opts) => {
242
+ await listRuns(process.cwd(), opts);
243
+ });
244
+ runs
245
+ .command("show")
246
+ .description("Show one run in detail: phases, gates, cost")
247
+ .argument("<runId>", "Folder name under output/, see `kraftwerk runs`")
248
+ .option("--json", "Machine-readable output (all trace events)")
249
+ .action(async (runId, opts) => {
250
+ await showRun(process.cwd(), runId, opts);
251
+ });
252
+ const runner = program
253
+ .command("runner")
254
+ .description("Manage the Docker sandbox runner (build the image, see/stop running runs)");
255
+ runner
256
+ .command("build")
257
+ .description("Build/update the kraftwerk-runner image")
258
+ .action(async () => {
259
+ const { buildImage, dockerAvailable } = await import("../runner/docker.js");
260
+ if (!dockerAvailable()) {
261
+ console.error(chalk.red("Docker daemon not reachable — is Docker running?"));
262
+ process.exit(1);
263
+ }
264
+ await buildImage();
265
+ console.log(chalk.green("✔ Image kraftwerk-runner built"));
266
+ });
267
+ runner
268
+ .command("ps")
269
+ .description("List running sandbox runs")
270
+ .action(async () => {
271
+ const { listSandboxes } = await import("../runner/docker.js");
272
+ const rows = listSandboxes();
273
+ if (rows.length === 0) {
274
+ console.log(chalk.dim("No running sandbox runs."));
275
+ return;
276
+ }
277
+ for (const r of rows) {
278
+ console.log(`${chalk.cyan(r.runId)} ${r.workflow} ${chalk.dim(r.status)}`);
279
+ }
280
+ });
281
+ runner
282
+ .command("stop")
283
+ .description("Stop a running sandbox run")
284
+ .argument("<runId>", "Run-Id (run-...)")
285
+ .action(async (runId) => {
286
+ const { stopSandbox } = await import("../runner/docker.js");
287
+ if (stopSandbox(runId.replace(/^kw-/, ""))) {
288
+ console.log(chalk.green(`✔ ${runId} stopped`));
289
+ }
290
+ else {
291
+ console.error(chalk.red(`No running container for ${runId}.`));
292
+ process.exit(1);
293
+ }
294
+ });
295
+ // LLM-facing, veloop-style: prints a self-contained brief for the agent
296
+ // that then authors the workflow folder with this CLI.
297
+ program
298
+ .command("create")
299
+ .description("Print a brief for an LLM agent that builds a workflow from the description")
300
+ .argument("<spec...>", "What the workflow should do (free text)")
301
+ .action(async (specParts) => {
302
+ const spec = specParts.join(" ").trim();
303
+ if (!spec) {
304
+ console.error(chalk.red('Description missing. Example: kraftwerk create "A workflow that writes and reviews release notes"'));
305
+ process.exit(1);
306
+ }
307
+ const root = await findWorkflowsRoot(process.cwd());
308
+ console.log(renderCreateBrief({
309
+ spec,
310
+ workflowsRoot: root ? path.relative(process.cwd(), root) : undefined,
311
+ }));
312
+ });
313
+ program
314
+ .command("validate")
315
+ .description("Validate workflows without executing them (schema + semantics + files)")
316
+ .argument("[paths...]", "workflow.yml files or workflow folders; without paths: all discovered")
317
+ .action(async (paths) => {
318
+ let targets = paths;
319
+ if (targets.length === 0) {
320
+ const spinner = ora("Discovering workflows ...").start();
321
+ targets = (await discoverWorkflows(process.cwd())).map((e) => e.path);
322
+ spinner.stop();
323
+ if (targets.length === 0) {
324
+ console.log(chalk.yellow("No workflows found."));
325
+ return;
326
+ }
327
+ }
328
+ let failures = 0;
329
+ for (const target of targets) {
330
+ try {
331
+ const workflow = await loadWorkflow(target);
332
+ console.log(`${chalk.green("✔")} ${target} — ${chalk.cyan(workflow.name)} ` +
333
+ chalk.dim(`(${workflow.meta.steps.length} steps, ${workflow.meta.agents.length} agents)`));
334
+ }
335
+ catch (err) {
336
+ failures += 1;
337
+ console.error(`${chalk.red("✖")} ${target}\n${chalk.red(err.message)}`);
338
+ }
339
+ }
340
+ process.exit(failures > 0 ? 1 : 0);
341
+ });
342
+ await program.parseAsync(process.argv);
@@ -0,0 +1,6 @@
1
+ export declare function listRuns(cwd: string, opts?: {
2
+ json?: boolean;
3
+ }): Promise<void>;
4
+ export declare function showRun(cwd: string, id: string, opts?: {
5
+ json?: boolean;
6
+ }): Promise<void>;
@@ -0,0 +1,120 @@
1
+ import { readdir, readFile, stat } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import chalk from "chalk";
4
+ import Table from "cli-table3";
5
+ import { resolveProject } from "../config.js";
6
+ import { fmtDuration, fmtTokens } from "../stats.js";
7
+ async function readRun(dir) {
8
+ const tracePath = path.join(dir, "trace.jsonl");
9
+ const raw = await readFile(tracePath, "utf8").catch(() => null);
10
+ if (raw === null)
11
+ return undefined;
12
+ const events = raw
13
+ .split("\n")
14
+ .filter((l) => l.trim())
15
+ .flatMap((l) => {
16
+ try {
17
+ return [JSON.parse(l)];
18
+ }
19
+ catch {
20
+ return [];
21
+ }
22
+ });
23
+ const start = events.find((e) => e.event === "run_start");
24
+ const summary = events.find((e) => e.event === "run_summary");
25
+ const failed = events.some((e) => e.event === "phase_end" && (e.status === "failed" || e.status === "blocked"));
26
+ return {
27
+ id: path.basename(dir),
28
+ dir,
29
+ workflow: start?.workflow,
30
+ request: start?.request,
31
+ startedAt: start?.ts,
32
+ status: summary ? "ok" : failed ? "failed" : "incomplete",
33
+ durationMs: summary?.total?.durationMs,
34
+ costUsd: summary?.total?.costUsd,
35
+ events,
36
+ };
37
+ }
38
+ async function collectRuns(cwd) {
39
+ const { outputDir } = await resolveProject(cwd);
40
+ const entries = await readdir(outputDir, { withFileTypes: true }).catch(() => []);
41
+ const runs = [];
42
+ for (const entry of entries) {
43
+ if (!entry.isDirectory())
44
+ continue;
45
+ const info = await readRun(path.join(outputDir, entry.name));
46
+ if (info)
47
+ runs.push(info);
48
+ }
49
+ runs.sort((a, b) => (b.startedAt ?? b.id).localeCompare(a.startedAt ?? a.id));
50
+ return { outputDir, runs };
51
+ }
52
+ const STATUS_LABEL = {
53
+ ok: chalk.green("ok"),
54
+ failed: chalk.red("failed"),
55
+ incomplete: chalk.yellow("incomplete"),
56
+ };
57
+ export async function listRuns(cwd, opts = {}) {
58
+ const { outputDir, runs } = await collectRuns(cwd);
59
+ if (opts.json) {
60
+ console.log(JSON.stringify(runs.map(({ events: _events, ...rest }) => rest), null, 2));
61
+ return;
62
+ }
63
+ if (runs.length === 0) {
64
+ console.log(chalk.dim(`No runs under ${outputDir}.`));
65
+ return;
66
+ }
67
+ const table = new Table({
68
+ head: ["run", "workflow", "status", "duration", "cost", "request"].map((h) => chalk.bold(h)),
69
+ wordWrap: true,
70
+ colWidths: [26, 14, 12, 8, 9, 30],
71
+ });
72
+ for (const r of runs) {
73
+ table.push([
74
+ r.id,
75
+ r.workflow ?? "—",
76
+ STATUS_LABEL[r.status],
77
+ r.durationMs !== undefined ? fmtDuration(r.durationMs) : "—",
78
+ r.costUsd !== undefined ? `$${r.costUsd.toFixed(2)}` : "—",
79
+ r.request ?? "—",
80
+ ]);
81
+ }
82
+ console.log(table.toString());
83
+ }
84
+ export async function showRun(cwd, id, opts = {}) {
85
+ const { outputDir } = await resolveProject(cwd);
86
+ const dir = path.join(outputDir, id);
87
+ if (!(await stat(dir).catch(() => null))) {
88
+ console.error(chalk.red(`Run "${id}" not found under ${outputDir}.`));
89
+ process.exit(2);
90
+ }
91
+ const info = await readRun(dir);
92
+ if (!info) {
93
+ console.error(chalk.red(`${id}: no trace.jsonl — not a kraftwerk run?`));
94
+ process.exit(2);
95
+ }
96
+ if (opts.json) {
97
+ console.log(JSON.stringify(info, null, 2));
98
+ return;
99
+ }
100
+ console.log(`${chalk.cyan(info.workflow ?? "?")} ${STATUS_LABEL[info.status]} ${chalk.dim(info.startedAt ?? "")}`);
101
+ if (info.request)
102
+ console.log(chalk.dim(`request: ${info.request}`));
103
+ console.log(chalk.dim(`dir: ${info.dir}\n`));
104
+ for (const e of info.events) {
105
+ if (e.event === "phase_end") {
106
+ const icon = e.status === "ok" ? chalk.green("✔") : chalk.red("✖");
107
+ const s = e.stats;
108
+ const detail = s
109
+ ? ` ${chalk.dim(`${fmtDuration(s.durationMs)} | ${fmtTokens(s.inputTokens + s.cacheReadTokens + s.cacheCreationTokens)} in / ${fmtTokens(s.outputTokens)} out | $${s.costUsd.toFixed(4)} | ${s.attempts} attempt(s)`)}`
110
+ : "";
111
+ console.log(`${icon} ${e.phase}${detail}`);
112
+ }
113
+ if (e.event === "gate_result" && e.passed === false) {
114
+ console.log(` ${chalk.red("gate")} ${e.gate}: ${e.failure}`);
115
+ }
116
+ }
117
+ if (info.durationMs !== undefined) {
118
+ console.log(chalk.dim(`\ntotal: ${fmtDuration(info.durationMs)} | $${(info.costUsd ?? 0).toFixed(4)}`));
119
+ }
120
+ }
package/dist/cli.d.ts ADDED
@@ -0,0 +1,13 @@
1
+ import type { WorkflowDefinition } from "./workflow.js";
2
+ /**
3
+ * CLI registry dispatch: one entry point, one name per workflow. A consumer's
4
+ * whole entry file is one call:
5
+ *
6
+ * runCli({ [contentWorkflow.name]: contentWorkflow });
7
+ *
8
+ * npm start -- <workflow> "topic or source URL"
9
+ * npm start -- <workflow> --yes "..." # auto-approve
10
+ * npm start -- <workflow> --verbose "..." # + agent narration
11
+ * npm start -- validate <path> ... # validate YAML workflows
12
+ */
13
+ export declare function runCli(workflows: Record<string, WorkflowDefinition>): void;
package/dist/cli.js ADDED
@@ -0,0 +1,47 @@
1
+ import { validateWorkflows } from "./validate.js";
2
+ /**
3
+ * CLI registry dispatch: one entry point, one name per workflow. A consumer's
4
+ * whole entry file is one call:
5
+ *
6
+ * runCli({ [contentWorkflow.name]: contentWorkflow });
7
+ *
8
+ * npm start -- <workflow> "topic or source URL"
9
+ * npm start -- <workflow> --yes "..." # auto-approve
10
+ * npm start -- <workflow> --verbose "..." # + agent narration
11
+ * npm start -- validate <path> ... # validate YAML workflows
12
+ */
13
+ export function runCli(workflows) {
14
+ const usage = () => {
15
+ console.error('Usage: npm start -- <workflow> [--yes] [--verbose] "<topic or source URL>"');
16
+ console.error(' npm start -- validate <workflow.yml | workflow-folder> ...');
17
+ console.error("\nAvailable workflows:");
18
+ for (const wf of Object.values(workflows)) {
19
+ console.error(` ${wf.name.padEnd(12)} ${wf.description}`);
20
+ }
21
+ process.exit(1);
22
+ };
23
+ const main = async () => {
24
+ const args = process.argv.slice(2);
25
+ const autoApprove = args.includes("--yes");
26
+ const verbose = args.includes("--verbose") || args.includes("-v");
27
+ const positional = args.filter((a) => !["--yes", "--verbose", "-v"].includes(a));
28
+ const [name, ...rest] = positional;
29
+ // Reserved subcommand: validate YAML workflows without executing them.
30
+ if (name === "validate") {
31
+ if (rest.length === 0)
32
+ usage();
33
+ process.exit((await validateWorkflows(rest)) > 0 ? 1 : 0);
34
+ }
35
+ const workflow = name ? workflows[name] : undefined;
36
+ if (!workflow)
37
+ usage();
38
+ const request = rest.join(" ").trim();
39
+ if (!request)
40
+ usage();
41
+ await workflow.run({ request, autoApprove, verbose });
42
+ };
43
+ main().catch((err) => {
44
+ console.error(err);
45
+ process.exit(1);
46
+ });
47
+ }
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Project resolution + optional project config.
3
+ *
4
+ * `kraftwerk.yml` at the project root is both the config file and the
5
+ * root marker, so every CLI command works from any subdirectory. Without
6
+ * it, the walk-up falls back to the first ancestor containing a workflows
7
+ * root (src/workflows/ or workflows/), then to the first .git directory,
8
+ * then to the starting cwd.
9
+ *
10
+ * All fields are optional — a valid kraftwerk.yml may be empty:
11
+ *
12
+ * workflows: src/workflows # workflows root, relative to the file
13
+ * output: output # run-artifact directory, relative to the file
14
+ */
15
+ /** Stable, versionless URL of the workflow JSON schema (editor validation). */
16
+ export declare const SCHEMA_URL = "https://raw.githubusercontent.com/NETNODEAG/kraftwerk/main/kraftwerk/schema/workflow.schema.json";
17
+ export declare const CONFIG_FILENAMES: string[];
18
+ export interface ProjectConfig {
19
+ /** Workflows root relative to the project root. */
20
+ workflows?: string;
21
+ /** Run-artifact directory relative to the project root. Default: output */
22
+ output?: string;
23
+ }
24
+ export interface Project {
25
+ /** Absolute project root the CLI operates on. */
26
+ root: string;
27
+ /** Parsed kraftwerk.yml, {} if none exists. */
28
+ config: ProjectConfig;
29
+ /** Absolute path of the config file, if one exists. */
30
+ configPath?: string;
31
+ /** Absolute workflows root, if one exists. */
32
+ workflowsRoot?: string;
33
+ /** Absolute run-artifact directory (may not exist yet). */
34
+ outputDir: string;
35
+ }
36
+ /**
37
+ * Resolve the project for a cwd: walk up until a kraftwerk.yml or a
38
+ * workflows root appears; a .git directory is the fallback root, the cwd
39
+ * itself the last resort.
40
+ */
41
+ export declare function resolveProject(cwd: string): Promise<Project>;