@netnodeag/kraftwerk 0.2.0 → 0.3.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.
- package/README.md +10 -2
- package/dist/cli/kraftwerk.js +10 -0
- package/dist/cli/ui.d.ts +4 -0
- package/dist/cli/ui.js +48 -0
- package/inspector/README.md +67 -0
- package/inspector/app/api/runs/[id]/file/route.ts +56 -0
- package/inspector/app/api/runs/[id]/route.ts +15 -0
- package/inspector/app/api/runs/[id]/stop/route.ts +15 -0
- package/inspector/app/api/runs/route.ts +9 -0
- package/inspector/app/api/workflows/[slug]/route.ts +11 -0
- package/inspector/app/api/workflows/[slug]/run/route.ts +37 -0
- package/inspector/app/api/workflows/route.ts +8 -0
- package/inspector/app/globals.css +460 -0
- package/inspector/app/layout.tsx +54 -0
- package/inspector/app/page.tsx +16 -0
- package/inspector/app/runs/[id]/page.tsx +6 -0
- package/inspector/app/runs/[id]/run-detail.tsx +343 -0
- package/inspector/app/shared.tsx +81 -0
- package/inspector/app/theme-toggle.tsx +19 -0
- package/inspector/app/workflows/[slug]/page.tsx +6 -0
- package/inspector/app/workflows/[slug]/workflow-view.tsx +271 -0
- package/inspector/app/workflows/page.tsx +43 -0
- package/inspector/lib/runner.ts +78 -0
- package/inspector/lib/runs.ts +313 -0
- package/inspector/lib/workflows.ts +240 -0
- package/inspector/next.config.ts +5 -0
- package/inspector/package-lock.json +982 -0
- package/inspector/package.json +24 -0
- package/inspector/tsconfig.json +21 -0
- package/package.json +29 -6
package/README.md
CHANGED
|
@@ -22,6 +22,12 @@ npx @netnodeag/kraftwerk init # scaffold kraftwerk.yml + workflows/ + examp
|
|
|
22
22
|
npx @netnodeag/kraftwerk run hello "Was ist kraftwerk?"
|
|
23
23
|
```
|
|
24
24
|
|
|
25
|
+
Your own workflows are just more folders under `workflows/` — a
|
|
26
|
+
`workflow.yml` plus prompt files, discovered automatically (see
|
|
27
|
+
[YAML workflows](#yaml-workflows)). Or let a coding agent build one:
|
|
28
|
+
`npx @netnodeag/kraftwerk create "<what it should do>"` prints a
|
|
29
|
+
self-contained brief that Claude Code / Codex follows end to end.
|
|
30
|
+
|
|
25
31
|
For a local checkout / programmatic consumer (TS workflows, custom gates,
|
|
26
32
|
approval loops), add the dependency (the `kraftwerk` alias keeps imports
|
|
27
33
|
short):
|
|
@@ -50,6 +56,7 @@ kraftwerk list # table: workflows, steps, agents (with
|
|
|
50
56
|
kraftwerk run tagline "https://..." # run; --yes, --verbose
|
|
51
57
|
kraftwerk run # interactive: pick workflow, type the request
|
|
52
58
|
kraftwerk runs # past runs from output/*/trace.jsonl; runs show <id> for detail
|
|
59
|
+
kraftwerk ui # inspector web UI on http://localhost:4499; --port, --output
|
|
53
60
|
kraftwerk doctor # preflight: harness CLIs, docker, workflows, declared env vars
|
|
54
61
|
kraftwerk validate # all discovered — schema + semantics + files, exit 1 on failure
|
|
55
62
|
kraftwerk validate src/workflows/pitch # specific paths
|
|
@@ -333,7 +340,8 @@ npm publish # runs npm run build first via prepublishOnly
|
|
|
333
340
|
```
|
|
334
341
|
|
|
335
342
|
The tarball is whitelisted via `files`: `bin/`, `dist/`, `runner/`
|
|
336
|
-
(Dockerfile for sandboxed runs), `schema/` (workflow JSON schema)
|
|
337
|
-
|
|
343
|
+
(Dockerfile for sandboxed runs), `schema/` (workflow JSON schema), and the
|
|
344
|
+
inspector sources (for `kraftwerk ui`; its deps install on first launch) —
|
|
345
|
+
no `src/` or examples. Check with `npm pack --dry-run` before a
|
|
338
346
|
release. Runtime deps stay regular `dependencies`; `tsx` and `typescript`
|
|
339
347
|
are dev-only, so consumers install neither.
|
package/dist/cli/kraftwerk.js
CHANGED
|
@@ -12,6 +12,7 @@ import { renderCreateBrief } from "./create-brief.js";
|
|
|
12
12
|
import { runDoctor } from "./doctor.js";
|
|
13
13
|
import { runInit } from "./init.js";
|
|
14
14
|
import { listRuns, showRun } from "./runs.js";
|
|
15
|
+
import { runUi } from "./ui.js";
|
|
15
16
|
/**
|
|
16
17
|
* kraftwerk — the kraftwerk CLI.
|
|
17
18
|
*
|
|
@@ -19,6 +20,7 @@ import { listRuns, showRun } from "./runs.js";
|
|
|
19
20
|
* kraftwerk list discover + list workflows (--json, --from)
|
|
20
21
|
* kraftwerk run [workflow] [text] run one (prompts interactively if omitted)
|
|
21
22
|
* kraftwerk runs [show <id>] inspect past runs from their traces
|
|
23
|
+
* kraftwerk ui start the inspector web UI (localhost:4499)
|
|
22
24
|
* kraftwerk doctor preflight: harness CLIs, docker, workflows, env
|
|
23
25
|
* kraftwerk validate [paths...] validate without executing
|
|
24
26
|
*
|
|
@@ -249,6 +251,14 @@ runs
|
|
|
249
251
|
.action(async (runId, opts) => {
|
|
250
252
|
await showRun(process.cwd(), runId, opts);
|
|
251
253
|
});
|
|
254
|
+
program
|
|
255
|
+
.command("ui")
|
|
256
|
+
.description("Start the inspector web UI for this project's runs and workflows")
|
|
257
|
+
.option("--port <port>", "Port for the web UI", "4499")
|
|
258
|
+
.option("--output <dir>", "Output directory to inspect (default: the project's output dir)")
|
|
259
|
+
.action(async (opts) => {
|
|
260
|
+
await runUi(process.cwd(), opts);
|
|
261
|
+
});
|
|
252
262
|
const runner = program
|
|
253
263
|
.command("runner")
|
|
254
264
|
.description("Manage the Docker sandbox runner (build the image, see/stop running runs)");
|
package/dist/cli/ui.d.ts
ADDED
package/dist/cli/ui.js
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
5
|
+
import chalk from "chalk";
|
|
6
|
+
import { resolveProject } from "../config.js";
|
|
7
|
+
/**
|
|
8
|
+
* `kraftwerk ui` — start the inspector web UI (Next.js app shipped in the
|
|
9
|
+
* package under inspector/) pointed at the current project's output dir.
|
|
10
|
+
* First launch installs the inspector's dependencies; the server then runs
|
|
11
|
+
* in the foreground until Ctrl-C.
|
|
12
|
+
*/
|
|
13
|
+
/** Package root is two levels up from this file (src/cli/ or dist/cli/). */
|
|
14
|
+
const inspectorDir = () => path.join(path.dirname(fileURLToPath(import.meta.url)), "../../inspector");
|
|
15
|
+
export async function runUi(cwd, opts) {
|
|
16
|
+
const dir = inspectorDir();
|
|
17
|
+
if (!existsSync(path.join(dir, "package.json"))) {
|
|
18
|
+
console.error(chalk.red(`Inspector not found at ${dir} — broken install?`));
|
|
19
|
+
process.exit(1);
|
|
20
|
+
}
|
|
21
|
+
const outputDir = opts.output
|
|
22
|
+
? path.resolve(cwd, opts.output)
|
|
23
|
+
: (await resolveProject(cwd)).outputDir;
|
|
24
|
+
const port = opts.port ?? "4499";
|
|
25
|
+
if (!existsSync(path.join(dir, "node_modules"))) {
|
|
26
|
+
console.log(chalk.dim("First launch — installing inspector dependencies ..."));
|
|
27
|
+
const install = spawnSync("npm", ["install", "--no-fund", "--no-audit"], {
|
|
28
|
+
cwd: dir,
|
|
29
|
+
stdio: "inherit",
|
|
30
|
+
});
|
|
31
|
+
if (install.status !== 0) {
|
|
32
|
+
console.error(chalk.red("npm install failed in the inspector directory."));
|
|
33
|
+
process.exit(1);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
console.log(`${chalk.green("✔")} Inspector: ${chalk.cyan(`http://localhost:${port}`)} ` +
|
|
37
|
+
chalk.dim(`(output: ${outputDir})`));
|
|
38
|
+
const nextBin = path.join(dir, "node_modules", "next", "dist", "bin", "next");
|
|
39
|
+
const child = spawn(process.execPath, [nextBin, "dev", "-p", port], {
|
|
40
|
+
cwd: dir,
|
|
41
|
+
stdio: "inherit",
|
|
42
|
+
env: { ...process.env, KRAFTWERK_OUTPUT: outputDir },
|
|
43
|
+
});
|
|
44
|
+
for (const signal of ["SIGINT", "SIGTERM"]) {
|
|
45
|
+
process.on(signal, () => child.kill(signal));
|
|
46
|
+
}
|
|
47
|
+
child.on("exit", (code) => process.exit(code ?? 0));
|
|
48
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# kraftwerk inspector
|
|
2
|
+
|
|
3
|
+
Web UI to look into the `output/` folder of a kraftwerk consumer project:
|
|
4
|
+
all runs at a glance, live phase timelines while a run executes, and every
|
|
5
|
+
file per run with inline preview (HTML reports render in place, text tails
|
|
6
|
+
live, images display).
|
|
7
|
+
|
|
8
|
+
## Run
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
kraftwerk ui # from any consumer project — installs deps on first launch,
|
|
12
|
+
# points at the project's output dir; --port, --output
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Or directly from a checkout:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
cd inspector
|
|
19
|
+
npm install
|
|
20
|
+
npm run dev # http://localhost:4499
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
By default it inspects `../../agent-playground/output` (the playground next
|
|
24
|
+
to this repo). Point it anywhere else with:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
KRAFTWERK_OUTPUT=/path/to/your-project/output npm run dev
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## What it shows
|
|
31
|
+
|
|
32
|
+
- **Run index** — every `run-*` folder: workflow, request, status lamp
|
|
33
|
+
(ok / running / failed / aborted), phase progress, duration, cost.
|
|
34
|
+
Running runs tick live.
|
|
35
|
+
- **Run detail** — the phase timeline parsed from `trace.jsonl`: agent +
|
|
36
|
+
model chips, attempts, duration, token and cost figures, gate results
|
|
37
|
+
(including failure messages), envelope summaries, and for the running
|
|
38
|
+
phase the last tool activity. Steps that have not started yet appear as
|
|
39
|
+
pending — the `run_start` trace event declares them.
|
|
40
|
+
- **Files** — all files of the run dir with size; click to view. `.html`
|
|
41
|
+
renders in a sandboxed iframe (reports look like reports), images render
|
|
42
|
+
inline, everything else is text with live tail while the run is active.
|
|
43
|
+
- **Workflows** — browses `src/workflows/` (or `workflows/`) of the same
|
|
44
|
+
project: every workflow as a card, and per workflow a visualization of
|
|
45
|
+
how it is built — agent cards (model, tools, persona) with identity
|
|
46
|
+
colors, the step pipeline in order with each step's resolved prompt or
|
|
47
|
+
script (file references are inlined), the gates per step, the workflow
|
|
48
|
+
folder contents, and links to recent runs of that workflow. Broken
|
|
49
|
+
YAML still shows up, flagged with its parse error.
|
|
50
|
+
|
|
51
|
+
- **Trigger runs** — every workflow page has a "trigger run" panel: enter a
|
|
52
|
+
request and launch. Default is the **Docker sandbox** (one
|
|
53
|
+
`kraftwerk-runner` container per run, workflow mounted read-only, run dir
|
|
54
|
+
bind-mounted back into `output/` so the live timeline works unchanged;
|
|
55
|
+
build the image once with `kraftwerk runner build`). Optional: forward
|
|
56
|
+
the SSH agent, or run locally instead. Sandboxed runs show a "sandbox"
|
|
57
|
+
chip and a stop button while running (`docker stop` under the hood).
|
|
58
|
+
Env vars for sandboxed runs go into `<project>/runner.env`.
|
|
59
|
+
|
|
60
|
+
Realtime is plain polling (1.5 s while something runs, 6 s otherwise) —
|
|
61
|
+
no daemon, no socket, works on a plain filesystem.
|
|
62
|
+
|
|
63
|
+
## Requirements
|
|
64
|
+
|
|
65
|
+
Traces written by kraftwerk ≥ the `run_start` event carry workflow
|
|
66
|
+
name, request, and the declared step list; older traces still render, only
|
|
67
|
+
without those labels.
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { promises as fs } from "node:fs";
|
|
2
|
+
import { NextResponse } from "next/server";
|
|
3
|
+
import { readRunFile } from "@/lib/runs";
|
|
4
|
+
|
|
5
|
+
export const dynamic = "force-dynamic";
|
|
6
|
+
|
|
7
|
+
const TYPES: Record<string, string> = {
|
|
8
|
+
".html": "text/html; charset=utf-8",
|
|
9
|
+
".svg": "image/svg+xml",
|
|
10
|
+
".png": "image/png",
|
|
11
|
+
".jpg": "image/jpeg",
|
|
12
|
+
".jpeg": "image/jpeg",
|
|
13
|
+
".gif": "image/gif",
|
|
14
|
+
".webp": "image/webp",
|
|
15
|
+
".pdf": "application/pdf",
|
|
16
|
+
".json": "application/json; charset=utf-8",
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
/** Text preview payloads are capped; the tail matters most for logs. */
|
|
20
|
+
const MAX_TEXT = 400_000;
|
|
21
|
+
|
|
22
|
+
export async function GET(req: Request, ctx: { params: Promise<{ id: string }> }) {
|
|
23
|
+
const { id } = await ctx.params;
|
|
24
|
+
const url = new URL(req.url);
|
|
25
|
+
const name = url.searchParams.get("name") ?? "";
|
|
26
|
+
const raw = url.searchParams.get("raw") === "1";
|
|
27
|
+
|
|
28
|
+
let file;
|
|
29
|
+
try {
|
|
30
|
+
file = await readRunFile(id, name);
|
|
31
|
+
} catch {
|
|
32
|
+
return NextResponse.json({ error: "invalid request" }, { status: 400 });
|
|
33
|
+
}
|
|
34
|
+
if (!file) return NextResponse.json({ error: "not found" }, { status: 404 });
|
|
35
|
+
|
|
36
|
+
const ext = name.slice(name.lastIndexOf(".")).toLowerCase();
|
|
37
|
+
|
|
38
|
+
if (raw) {
|
|
39
|
+
const buf = await fs.readFile(file.absPath);
|
|
40
|
+
return new NextResponse(new Uint8Array(buf), {
|
|
41
|
+
headers: {
|
|
42
|
+
"content-type": TYPES[ext] ?? "text/plain; charset=utf-8",
|
|
43
|
+
"cache-control": "no-store",
|
|
44
|
+
},
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const buf = await fs.readFile(file.absPath);
|
|
49
|
+
let text = buf.toString("utf8");
|
|
50
|
+
let truncated = false;
|
|
51
|
+
if (text.length > MAX_TEXT) {
|
|
52
|
+
text = text.slice(-MAX_TEXT);
|
|
53
|
+
truncated = true;
|
|
54
|
+
}
|
|
55
|
+
return NextResponse.json({ name, size: file.size, truncated, content: text });
|
|
56
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { NextResponse } from "next/server";
|
|
2
|
+
import { getRun } from "@/lib/runs";
|
|
3
|
+
|
|
4
|
+
export const dynamic = "force-dynamic";
|
|
5
|
+
|
|
6
|
+
export async function GET(_req: Request, ctx: { params: Promise<{ id: string }> }) {
|
|
7
|
+
const { id } = await ctx.params;
|
|
8
|
+
try {
|
|
9
|
+
const run = await getRun(id);
|
|
10
|
+
if (!run) return NextResponse.json({ error: "not found" }, { status: 404 });
|
|
11
|
+
return NextResponse.json(run);
|
|
12
|
+
} catch {
|
|
13
|
+
return NextResponse.json({ error: "invalid run id" }, { status: 400 });
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { NextResponse } from "next/server";
|
|
2
|
+
import { stopRun } from "@/lib/runner";
|
|
3
|
+
|
|
4
|
+
export const dynamic = "force-dynamic";
|
|
5
|
+
|
|
6
|
+
export async function POST(_req: Request, ctx: { params: Promise<{ id: string }> }) {
|
|
7
|
+
const { id } = await ctx.params;
|
|
8
|
+
const stopped = stopRun(id);
|
|
9
|
+
return stopped
|
|
10
|
+
? NextResponse.json({ stopped: true })
|
|
11
|
+
: NextResponse.json(
|
|
12
|
+
{ error: "no running sandbox container for this run (local runs cannot be stopped here)" },
|
|
13
|
+
{ status: 404 }
|
|
14
|
+
);
|
|
15
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { NextResponse } from "next/server";
|
|
2
|
+
import { listRuns, OUTPUT_DIR } from "@/lib/runs";
|
|
3
|
+
|
|
4
|
+
export const dynamic = "force-dynamic";
|
|
5
|
+
|
|
6
|
+
export async function GET() {
|
|
7
|
+
const runs = await listRuns();
|
|
8
|
+
return NextResponse.json({ outputDir: OUTPUT_DIR, runs });
|
|
9
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { NextResponse } from "next/server";
|
|
2
|
+
import { getWorkflow } from "@/lib/workflows";
|
|
3
|
+
|
|
4
|
+
export const dynamic = "force-dynamic";
|
|
5
|
+
|
|
6
|
+
export async function GET(_req: Request, ctx: { params: Promise<{ slug: string }> }) {
|
|
7
|
+
const { slug } = await ctx.params;
|
|
8
|
+
const wf = await getWorkflow(slug);
|
|
9
|
+
if (!wf) return NextResponse.json({ error: "not found" }, { status: 404 });
|
|
10
|
+
return NextResponse.json(wf);
|
|
11
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { NextResponse } from "next/server";
|
|
2
|
+
import { getWorkflow } from "@/lib/workflows";
|
|
3
|
+
import { triggerRun, dockerStatus } from "@/lib/runner";
|
|
4
|
+
|
|
5
|
+
export const dynamic = "force-dynamic";
|
|
6
|
+
|
|
7
|
+
export async function GET() {
|
|
8
|
+
return NextResponse.json(dockerStatus());
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export async function POST(req: Request, ctx: { params: Promise<{ slug: string }> }) {
|
|
12
|
+
const { slug } = await ctx.params;
|
|
13
|
+
const wf = await getWorkflow(decodeURIComponent(slug));
|
|
14
|
+
if (!wf || wf.error || !wf.name) {
|
|
15
|
+
return NextResponse.json({ error: "workflow not found or broken" }, { status: 404 });
|
|
16
|
+
}
|
|
17
|
+
let body: { request?: string; sandbox?: boolean; ssh?: boolean };
|
|
18
|
+
try {
|
|
19
|
+
body = await req.json();
|
|
20
|
+
} catch {
|
|
21
|
+
return NextResponse.json({ error: "invalid JSON body" }, { status: 400 });
|
|
22
|
+
}
|
|
23
|
+
const request = (body.request ?? "").trim();
|
|
24
|
+
if (!request) return NextResponse.json({ error: "request text is required" }, { status: 400 });
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
const { runId } = triggerRun({
|
|
28
|
+
workflowName: wf.name,
|
|
29
|
+
request,
|
|
30
|
+
sandbox: body.sandbox ?? true,
|
|
31
|
+
ssh: !!body.ssh,
|
|
32
|
+
});
|
|
33
|
+
return NextResponse.json({ runId });
|
|
34
|
+
} catch (err) {
|
|
35
|
+
return NextResponse.json({ error: (err as Error).message }, { status: 503 });
|
|
36
|
+
}
|
|
37
|
+
}
|