@netnodeag/kraftwerk 0.3.1 → 0.4.1

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 (64) hide show
  1. package/README.md +8 -6
  2. package/dist/cli/kraftwerk.js +2 -2
  3. package/dist/cli/ui.js +32 -55
  4. package/dist/inspector/chat/acp.d.ts +2 -0
  5. package/dist/inspector/chat/acp.js +122 -0
  6. package/dist/inspector/chat/backend.d.ts +28 -0
  7. package/dist/inspector/chat/backend.js +1 -0
  8. package/dist/inspector/chat/types.d.ts +70 -0
  9. package/dist/inspector/chat/types.js +8 -0
  10. package/dist/inspector/context.d.ts +5 -0
  11. package/dist/inspector/context.js +19 -0
  12. package/dist/inspector/runner.d.ts +13 -0
  13. package/dist/inspector/runner.js +66 -0
  14. package/dist/inspector/runs.d.ts +57 -0
  15. package/dist/inspector/runs.js +240 -0
  16. package/dist/inspector/server.d.ts +8 -0
  17. package/dist/inspector/server.js +186 -0
  18. package/dist/inspector/workflows.d.ts +45 -0
  19. package/dist/inspector/workflows.js +186 -0
  20. package/inspector/dist/assets/ibm-plex-mono-latin-400-normal-CvHOgSBP.woff +0 -0
  21. package/inspector/dist/assets/ibm-plex-mono-latin-400-normal-DMJ8VG8y.woff2 +0 -0
  22. package/inspector/dist/assets/ibm-plex-mono-latin-500-normal-CB9ihrfo.woff +0 -0
  23. package/inspector/dist/assets/ibm-plex-mono-latin-500-normal-DSY6xOcd.woff2 +0 -0
  24. package/inspector/dist/assets/ibm-plex-mono-latin-600-normal-BgSNZQsw.woff2 +0 -0
  25. package/inspector/dist/assets/ibm-plex-mono-latin-600-normal-DWFSQ4vo.woff +0 -0
  26. package/inspector/dist/assets/ibm-plex-mono-latin-700-normal-7sUh57Bg.woff2 +0 -0
  27. package/inspector/dist/assets/ibm-plex-mono-latin-700-normal-CNHXzs6v.woff +0 -0
  28. package/inspector/dist/assets/ibm-plex-sans-latin-400-normal-CDDApCn2.woff2 +0 -0
  29. package/inspector/dist/assets/ibm-plex-sans-latin-400-normal-CYLoc0-x.woff +0 -0
  30. package/inspector/dist/assets/ibm-plex-sans-latin-500-normal-6ng42L7E.woff2 +0 -0
  31. package/inspector/dist/assets/ibm-plex-sans-latin-500-normal-BgVn5rGT.woff +0 -0
  32. package/inspector/dist/assets/ibm-plex-sans-latin-600-normal-Cu4Hd6ag.woff +0 -0
  33. package/inspector/dist/assets/ibm-plex-sans-latin-600-normal-CuJfVYMP.woff2 +0 -0
  34. package/inspector/dist/assets/ibm-plex-sans-latin-700-normal-Bth3BMcD.woff +0 -0
  35. package/inspector/dist/assets/ibm-plex-sans-latin-700-normal-Bxkt5Cjx.woff2 +0 -0
  36. package/inspector/dist/assets/index-BCSWkslF.css +1 -0
  37. package/inspector/dist/assets/index-D6WuziFQ.js +9 -0
  38. package/inspector/dist/index.html +24 -0
  39. package/package.json +7 -10
  40. package/inspector/README.md +0 -67
  41. package/inspector/app/api/runs/[id]/file/route.ts +0 -56
  42. package/inspector/app/api/runs/[id]/route.ts +0 -15
  43. package/inspector/app/api/runs/[id]/stop/route.ts +0 -15
  44. package/inspector/app/api/runs/route.ts +0 -9
  45. package/inspector/app/api/workflows/[slug]/route.ts +0 -11
  46. package/inspector/app/api/workflows/[slug]/run/route.ts +0 -37
  47. package/inspector/app/api/workflows/route.ts +0 -8
  48. package/inspector/app/globals.css +0 -460
  49. package/inspector/app/layout.tsx +0 -54
  50. package/inspector/app/page.tsx +0 -16
  51. package/inspector/app/runs/[id]/page.tsx +0 -6
  52. package/inspector/app/runs/[id]/run-detail.tsx +0 -343
  53. package/inspector/app/shared.tsx +0 -81
  54. package/inspector/app/theme-toggle.tsx +0 -19
  55. package/inspector/app/workflows/[slug]/page.tsx +0 -6
  56. package/inspector/app/workflows/[slug]/workflow-view.tsx +0 -271
  57. package/inspector/app/workflows/page.tsx +0 -43
  58. package/inspector/lib/runner.ts +0 -78
  59. package/inspector/lib/runs.ts +0 -313
  60. package/inspector/lib/workflows.ts +0 -240
  61. package/inspector/next.config.ts +0 -5
  62. package/inspector/package-lock.json +0 -982
  63. package/inspector/package.json +0 -24
  64. package/inspector/tsconfig.json +0 -21
package/README.md CHANGED
@@ -56,7 +56,7 @@ kraftwerk list # table: workflows, steps, agents (with
56
56
  kraftwerk run tagline "https://..." # run; --yes, --verbose
57
57
  kraftwerk run # interactive: pick workflow, type the request
58
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
59
+ kraftwerk ui # inspector web UI on http://localhost:1981; --port, --output
60
60
  kraftwerk doctor # preflight: harness CLIs, docker, workflows, declared env vars
61
61
  kraftwerk validate # all discovered — schema + semantics + files, exit 1 on failure
62
62
  kraftwerk validate src/workflows/pitch # specific paths
@@ -340,8 +340,10 @@ npm publish # runs npm run build first via prepublishOnly
340
340
  ```
341
341
 
342
342
  The tarball is whitelisted via `files`: `bin/`, `dist/`, `runner/`
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
346
- release. Runtime deps stay regular `dependencies`; `tsx` and `typescript`
347
- are dev-only, so consumers install neither.
343
+ (Dockerfile for sandboxed runs), `schema/` (workflow JSON schema), and
344
+ `inspector/dist/` (the prebuilt web UI for `kraftwerk ui` built by
345
+ `prepublishOnly`, served by the dependency-free server compiled into
346
+ `dist/inspector/`) no `src/` or examples. Check with `npm pack
347
+ --dry-run` before a release. Runtime deps stay regular `dependencies`;
348
+ `tsx`, `typescript`, and the inspector's Vite/React toolchain are
349
+ dev-only, so consumers install none of them.
@@ -20,7 +20,7 @@ import { runUi } from "./ui.js";
20
20
  * kraftwerk list discover + list workflows (--json, --from)
21
21
  * kraftwerk run [workflow] [text] run one (prompts interactively if omitted)
22
22
  * kraftwerk runs [show <id>] inspect past runs from their traces
23
- * kraftwerk ui start the inspector web UI (localhost:4499)
23
+ * kraftwerk ui start the inspector web UI (localhost:1981)
24
24
  * kraftwerk doctor preflight: harness CLIs, docker, workflows, env
25
25
  * kraftwerk validate [paths...] validate without executing
26
26
  *
@@ -254,7 +254,7 @@ runs
254
254
  program
255
255
  .command("ui")
256
256
  .description("Start the inspector web UI for this project's runs and workflows")
257
- .option("--port <port>", "Port for the web UI", "4499")
257
+ .option("--port <port>", "Port for the web UI", "1981")
258
258
  .option("--output <dir>", "Output directory to inspect (default: the project's output dir)")
259
259
  .action(async (opts) => {
260
260
  await runUi(process.cwd(), opts);
package/dist/cli/ui.js CHANGED
@@ -1,75 +1,52 @@
1
1
  import { existsSync } from "node:fs";
2
- import { cp, readFile } from "node:fs/promises";
3
- import os from "node:os";
4
2
  import path from "node:path";
5
3
  import { fileURLToPath } from "node:url";
6
- import { spawn, spawnSync } from "node:child_process";
4
+ import { spawnSync } from "node:child_process";
7
5
  import chalk from "chalk";
8
6
  import { resolveProject } from "../config.js";
7
+ import { startInspector } from "../inspector/server.js";
9
8
  /**
10
- * `kraftwerk ui` — start the inspector web UI (Next.js app shipped in the
11
- * package under inspector/) pointed at the current project's output dir.
12
- * First launch installs the inspector's dependencies; the server then runs
13
- * in the foreground until Ctrl-C.
9
+ * `kraftwerk ui` — start the inspector web UI pointed at the current
10
+ * project's output dir. The server is dependency-free node:http (part of
11
+ * this package); the frontend is a prebuilt Vite bundle shipped in
12
+ * inspector/dist. Nothing to install at runtime — the server starts
13
+ * instantly and runs in the foreground until Ctrl-C.
14
14
  */
15
15
  /** Package root is two levels up from this file (src/cli/ or dist/cli/). */
16
16
  const packageRoot = () => path.join(path.dirname(fileURLToPath(import.meta.url)), "../..");
17
- const inNodeModules = (p) => p.split(path.sep).includes("node_modules");
18
17
  /**
19
- * The inspector cannot run in place from an installed package: Next.js
20
- * excludes everything under node_modules/ from compilation, so its .ts/.tsx
21
- * sources would be served raw. Installed copies are materialized (sources
22
- * only) into ~/.cache/kraftwerk/inspector-<version>/ and run from there;
23
- * dev checkouts run in place.
18
+ * Published packages ship inspector/dist. A dev checkout builds it on
19
+ * first use (and after frontend edits: `npm run build` in inspector/).
24
20
  */
25
- async function materializeInspector() {
26
- const src = path.join(packageRoot(), "inspector");
27
- if (!inNodeModules(src))
28
- return src;
29
- const pkg = JSON.parse(await readFile(path.join(packageRoot(), "package.json"), "utf8"));
30
- const dest = path.join(os.homedir(), ".cache", "kraftwerk", `inspector-${pkg.version}`);
31
- if (!existsSync(path.join(dest, "package.json"))) {
32
- await cp(src, dest, {
33
- recursive: true,
34
- filter: (s) => {
35
- const parts = path.relative(src, s).split(path.sep);
36
- return !parts.includes("node_modules") && !parts.includes(".next");
37
- },
38
- });
21
+ function ensureBuilt() {
22
+ const inspector = path.join(packageRoot(), "inspector");
23
+ const dist = path.join(inspector, "dist");
24
+ if (existsSync(path.join(dist, "index.html")))
25
+ return dist;
26
+ if (!existsSync(path.join(inspector, "src"))) {
27
+ console.error(chalk.red(`Inspector assets missing at ${dist} — broken install?`));
28
+ process.exit(1);
39
29
  }
40
- return dest;
30
+ console.log(chalk.dim("Building the inspector frontend (first use in this checkout) ..."));
31
+ for (const args of [
32
+ ...(existsSync(path.join(inspector, "node_modules")) ? [] : [["install", "--no-fund", "--no-audit"]]),
33
+ ["run", "build"],
34
+ ]) {
35
+ const r = spawnSync("npm", args, { cwd: inspector, stdio: "inherit" });
36
+ if (r.status !== 0) {
37
+ console.error(chalk.red(`npm ${args.join(" ")} failed in inspector/.`));
38
+ process.exit(1);
39
+ }
40
+ }
41
+ return dist;
41
42
  }
42
43
  export async function runUi(cwd, opts) {
43
- if (!existsSync(path.join(packageRoot(), "inspector", "package.json"))) {
44
- console.error(chalk.red(`Inspector not found at ${path.join(packageRoot(), "inspector")} — broken install?`));
45
- process.exit(1);
46
- }
47
- const dir = await materializeInspector();
44
+ const staticDir = ensureBuilt();
48
45
  const outputDir = opts.output
49
46
  ? path.resolve(cwd, opts.output)
50
47
  : (await resolveProject(cwd)).outputDir;
51
- const port = opts.port ?? "4499";
52
- if (!existsSync(path.join(dir, "node_modules"))) {
53
- console.log(chalk.dim("First launch — installing inspector dependencies ..."));
54
- const install = spawnSync("npm", ["install", "--no-fund", "--no-audit"], {
55
- cwd: dir,
56
- stdio: "inherit",
57
- });
58
- if (install.status !== 0) {
59
- console.error(chalk.red("npm install failed in the inspector directory."));
60
- process.exit(1);
61
- }
62
- }
48
+ const port = Number(opts.port ?? "1981");
49
+ await startInspector({ outputDir, staticDir, port });
63
50
  console.log(`${chalk.green("✔")} Inspector: ${chalk.cyan(`http://localhost:${port}`)} ` +
64
51
  chalk.dim(`(output: ${outputDir})`));
65
- const nextBin = path.join(dir, "node_modules", "next", "dist", "bin", "next");
66
- const child = spawn(process.execPath, [nextBin, "dev", "-p", port], {
67
- cwd: dir,
68
- stdio: "inherit",
69
- env: { ...process.env, KRAFTWERK_OUTPUT: outputDir },
70
- });
71
- for (const signal of ["SIGINT", "SIGTERM"]) {
72
- process.on(signal, () => child.kill(signal));
73
- }
74
- child.on("exit", (code) => process.exit(code ?? 0));
75
52
  }
@@ -0,0 +1,2 @@
1
+ import type { BackendHooks, ChatBackend } from "./backend.js";
2
+ export declare function startAcpBackend(agent: "claude" | "codex", cwd: string, hooks: BackendHooks): Promise<ChatBackend>;
@@ -0,0 +1,122 @@
1
+ import { spawn } from "node:child_process";
2
+ import { Readable, Writable } from "node:stream";
3
+ import { fileURLToPath } from "node:url";
4
+ import { ClientSideConnection, ndJsonStream, PROTOCOL_VERSION, RequestError, } from "@agentclientprotocol/sdk";
5
+ /**
6
+ * ACP-backed chat: spawn an adapter (claude-agent-acp / codex-acp) as a
7
+ * subprocess, speak Agent Client Protocol over its stdio, and translate
8
+ * session/update notifications into chat events. One subprocess lives for
9
+ * the whole chat; the ACP session id carries the conversation.
10
+ *
11
+ * Auth rides on the local CLI logins (Claude Code / Codex) via the
12
+ * inherited environment — same story as the kraftwerk harnesses.
13
+ */
14
+ const ADAPTERS = {
15
+ claude: "@agentclientprotocol/claude-agent-acp/dist/index.js",
16
+ codex: "@agentclientprotocol/codex-acp/dist/index.js",
17
+ };
18
+ function contentText(content) {
19
+ return content.type === "text" ? content.text : "";
20
+ }
21
+ export async function startAcpBackend(agent, cwd, hooks) {
22
+ const entry = fileURLToPath(import.meta.resolve(ADAPTERS[agent]));
23
+ const child = spawn(process.execPath, [entry], {
24
+ cwd,
25
+ stdio: ["pipe", "pipe", "pipe"],
26
+ env: process.env,
27
+ });
28
+ let stderr = "";
29
+ child.stderr.on("data", (c) => {
30
+ stderr += c.toString("utf8");
31
+ if (stderr.length > 20_000)
32
+ stderr = stderr.slice(-20_000);
33
+ });
34
+ let dead = false;
35
+ child.on("close", (code) => {
36
+ dead = true;
37
+ if (code !== 0 && code !== null) {
38
+ hooks.emit({
39
+ type: "error",
40
+ message: `${agent} agent exited (code ${code})${stderr.trim() ? `: ${stderr.trim().slice(-500)}` : ""}`,
41
+ });
42
+ }
43
+ });
44
+ const client = {
45
+ sessionUpdate(params) {
46
+ const u = params.update;
47
+ switch (u.sessionUpdate) {
48
+ case "agent_message_chunk": {
49
+ const text = contentText(u.content);
50
+ if (text)
51
+ hooks.emit({ type: "text", text });
52
+ break;
53
+ }
54
+ case "agent_thought_chunk": {
55
+ const text = contentText(u.content);
56
+ if (text)
57
+ hooks.emit({ type: "thought", text });
58
+ break;
59
+ }
60
+ case "tool_call":
61
+ hooks.emit({
62
+ type: "tool_call",
63
+ callId: u.toolCallId,
64
+ title: u.title,
65
+ kind: u.kind ?? undefined,
66
+ status: u.status ?? undefined,
67
+ });
68
+ break;
69
+ case "tool_call_update":
70
+ hooks.emit({
71
+ type: "tool_update",
72
+ callId: u.toolCallId,
73
+ title: u.title ?? undefined,
74
+ status: u.status ?? undefined,
75
+ });
76
+ break;
77
+ // plans, mode/config updates etc. carry no thread content — skip.
78
+ }
79
+ },
80
+ async requestPermission(params) {
81
+ const optionId = await hooks.askPermission(params.toolCall.title ?? params.toolCall.toolCallId, params.options.map((o) => ({ optionId: o.optionId, name: o.name, kind: o.kind })));
82
+ return optionId
83
+ ? { outcome: { outcome: "selected", optionId } }
84
+ : { outcome: { outcome: "cancelled" } };
85
+ },
86
+ };
87
+ const stream = ndJsonStream(Writable.toWeb(child.stdin), Readable.toWeb(child.stdout));
88
+ const conn = new ClientSideConnection(() => client, stream);
89
+ await conn.initialize({
90
+ protocolVersion: PROTOCOL_VERSION,
91
+ clientCapabilities: { fs: { readTextFile: false, writeTextFile: false } },
92
+ clientInfo: { name: "kraftwerk-inspector", version: "1.0.0" },
93
+ });
94
+ const session = await conn.newSession({ cwd, mcpServers: [] });
95
+ const sessionId = session.sessionId;
96
+ return {
97
+ async prompt(text) {
98
+ if (dead)
99
+ throw new Error(`${agent} agent process is gone — start a new chat`);
100
+ try {
101
+ const res = await conn.prompt({
102
+ sessionId,
103
+ prompt: [{ type: "text", text }],
104
+ });
105
+ return res.stopReason;
106
+ }
107
+ catch (err) {
108
+ if (err instanceof RequestError)
109
+ throw new Error(`${agent} agent: ${err.message}`);
110
+ throw err;
111
+ }
112
+ },
113
+ cancel() {
114
+ if (!dead)
115
+ void conn.cancel({ sessionId }).catch(() => { });
116
+ },
117
+ dispose() {
118
+ if (!dead)
119
+ child.kill("SIGTERM");
120
+ },
121
+ };
122
+ }
@@ -0,0 +1,28 @@
1
+ import type { ChatEvent } from "./types.js";
2
+ /**
3
+ * A chat backend is one live agent conversation: prompt() runs a full turn
4
+ * (resolves when the agent finishes), streaming intermediate activity
5
+ * through the hooks. ACP agents (claude, codex) keep a subprocess alive for
6
+ * the whole chat; pi respawns per message and resumes via --session-id.
7
+ */
8
+ export interface BackendHooks {
9
+ /** Stream a thread event (text chunk, tool call, ...) to the session. */
10
+ emit(ev: ChatEvent): void;
11
+ /**
12
+ * Surface a permission request to the user; resolves with the chosen
13
+ * optionId, or null when the user (or a cancel) dismissed it.
14
+ */
15
+ askPermission(title: string, options: Array<{
16
+ optionId: string;
17
+ name: string;
18
+ kind?: string;
19
+ }>): Promise<string | null>;
20
+ }
21
+ export interface ChatBackend {
22
+ /** Send one user message; resolves with the stop reason at turn end. */
23
+ prompt(text: string): Promise<string>;
24
+ /** Interrupt the current turn (the running prompt() still resolves). */
25
+ cancel(): void;
26
+ /** Kill the agent subprocess, if any. */
27
+ dispose(): void;
28
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Chat data model shared across the chat backend modules. Every chat is a
3
+ * folder under <output>/chats/ holding meta.json plus events.jsonl — the
4
+ * same files-on-disk philosophy as runs. Events are the single source of
5
+ * truth: the UI replays them to render the thread, and the SSE stream just
6
+ * appends live ones.
7
+ */
8
+ export type ChatAgentId = "claude" | "codex" | "pi";
9
+ export type ChatScope = {
10
+ kind: "general";
11
+ } | {
12
+ kind: "kraftwerk";
13
+ } | {
14
+ kind: "run";
15
+ runId: string;
16
+ };
17
+ export interface ChatMeta {
18
+ id: string;
19
+ agent: ChatAgentId;
20
+ title: string;
21
+ cwd: string;
22
+ scope: ChatScope;
23
+ createdAt: string;
24
+ updatedAt: string;
25
+ }
26
+ /** One thread event; `seq` orders them and drives SSE resume (?after=seq). */
27
+ export type ChatEvent = {
28
+ type: "user_message";
29
+ text: string;
30
+ } | {
31
+ type: "text";
32
+ text: string;
33
+ } | {
34
+ type: "thought";
35
+ text: string;
36
+ } | {
37
+ type: "tool_call";
38
+ callId: string;
39
+ title: string;
40
+ kind?: string;
41
+ status?: string;
42
+ } | {
43
+ type: "tool_update";
44
+ callId: string;
45
+ title?: string;
46
+ status?: string;
47
+ } | {
48
+ type: "permission_request";
49
+ requestId: string;
50
+ title: string;
51
+ options: Array<{
52
+ optionId: string;
53
+ name: string;
54
+ kind?: string;
55
+ }>;
56
+ } | {
57
+ type: "permission_resolved";
58
+ requestId: string;
59
+ optionId: string | null;
60
+ } | {
61
+ type: "turn_end";
62
+ stopReason: string;
63
+ } | {
64
+ type: "error";
65
+ message: string;
66
+ };
67
+ export type StoredChatEvent = ChatEvent & {
68
+ seq: number;
69
+ ts: string;
70
+ };
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Chat data model shared across the chat backend modules. Every chat is a
3
+ * folder under <output>/chats/ holding meta.json plus events.jsonl — the
4
+ * same files-on-disk philosophy as runs. Events are the single source of
5
+ * truth: the UI replays them to render the thread, and the SSE stream just
6
+ * appends live ones.
7
+ */
8
+ export {};
@@ -0,0 +1,5 @@
1
+ export declare function setOutputDir(dir: string): void;
2
+ /** Absolute output directory (one run-* folder per run). */
3
+ export declare function getOutputDir(): string;
4
+ /** The consumer project root is the parent of the output dir. */
5
+ export declare function getProjectRoot(): string;
@@ -0,0 +1,19 @@
1
+ import path from "node:path";
2
+ /**
3
+ * Server-side context for the inspector: which consumer project it looks at.
4
+ * Set once by startInspector() before any request is served.
5
+ */
6
+ let outputDir = "";
7
+ export function setOutputDir(dir) {
8
+ outputDir = path.resolve(dir);
9
+ }
10
+ /** Absolute output directory (one run-* folder per run). */
11
+ export function getOutputDir() {
12
+ if (!outputDir)
13
+ throw new Error("inspector context not initialized");
14
+ return outputDir;
15
+ }
16
+ /** The consumer project root is the parent of the output dir. */
17
+ export function getProjectRoot() {
18
+ return path.dirname(getOutputDir());
19
+ }
@@ -0,0 +1,13 @@
1
+ export declare function dockerStatus(): {
2
+ available: boolean;
3
+ image: boolean;
4
+ };
5
+ export declare function triggerRun(opts: {
6
+ workflowName: string;
7
+ request: string;
8
+ sandbox: boolean;
9
+ ssh: boolean;
10
+ }): {
11
+ runId: string;
12
+ };
13
+ export declare function stopRun(runId: string): boolean;
@@ -0,0 +1,66 @@
1
+ import { spawn, spawnSync } from "node:child_process";
2
+ import { mkdirSync, openSync, closeSync } from "node:fs";
3
+ import path from "node:path";
4
+ import { getProjectRoot } from "./context.js";
5
+ /**
6
+ * Workflow trigger for the web UI. The inspector stays decoupled from the
7
+ * framework runtime: it shells out to `npx kraftwerk run` in the consumer
8
+ * project, detached, with a pre-chosen --run-id so the browser can jump to
9
+ * /runs/<id> immediately and watch the (already polling) live timeline.
10
+ *
11
+ * Sandbox mode adds --sandbox: one Docker container per run, workflow
12
+ * mounted read-only, run dir bind-mounted back into output/ (see
13
+ * kraftwerk/src/runner/docker.ts).
14
+ */
15
+ function stamp(d = new Date()) {
16
+ const p = (n) => String(n).padStart(2, "0");
17
+ return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())}-${p(d.getHours())}${p(d.getMinutes())}-${p(d.getSeconds())}`;
18
+ }
19
+ export function dockerStatus() {
20
+ const daemon = spawnSync("docker", ["version", "--format", "ok"], {
21
+ stdio: "ignore",
22
+ timeout: 8000,
23
+ });
24
+ if (daemon.status !== 0)
25
+ return { available: false, image: false };
26
+ const image = spawnSync("docker", ["image", "inspect", "kraftwerk-runner"], {
27
+ stdio: "ignore",
28
+ timeout: 8000,
29
+ });
30
+ return { available: true, image: image.status === 0 };
31
+ }
32
+ export function triggerRun(opts) {
33
+ if (opts.sandbox) {
34
+ const docker = dockerStatus();
35
+ if (!docker.available)
36
+ throw new Error("Docker daemon not reachable — start Docker first.");
37
+ if (!docker.image)
38
+ throw new Error('Image "kraftwerk-runner" missing — run `kraftwerk runner build`.');
39
+ }
40
+ const runId = `run-${stamp()}`;
41
+ // Matches the CLI's --run-id resolution: output/<id> under the project root.
42
+ const runDir = path.join(getProjectRoot(), "output", runId);
43
+ mkdirSync(runDir, { recursive: true });
44
+ const log = openSync(path.join(runDir, "trigger.log"), "a");
45
+ const args = ["kraftwerk", "run", "--yes", "--run-id", runId];
46
+ if (opts.sandbox)
47
+ args.push("--sandbox");
48
+ if (opts.ssh)
49
+ args.push("--ssh");
50
+ args.push(opts.workflowName, opts.request);
51
+ const child = spawn("npx", args, {
52
+ cwd: getProjectRoot(),
53
+ detached: true,
54
+ stdio: ["ignore", log, log],
55
+ env: { ...process.env, FORCE_COLOR: "0" },
56
+ });
57
+ child.unref();
58
+ closeSync(log);
59
+ return { runId };
60
+ }
61
+ export function stopRun(runId) {
62
+ // Sandboxed runs run in container kw-<runId>; docker stop ends them.
63
+ if (!/^run-[0-9-]+$/.test(runId))
64
+ return false;
65
+ return (spawnSync("docker", ["stop", `kw-${runId}`], { stdio: "ignore", timeout: 30_000 }).status === 0);
66
+ }
@@ -0,0 +1,57 @@
1
+ export type RunStatus = "running" | "ok" | "failed" | "aborted";
2
+ export interface GateView {
3
+ gate: string;
4
+ passed: boolean;
5
+ failure: string | null;
6
+ }
7
+ export interface PhaseView {
8
+ phase: string;
9
+ kind: "agent" | "script";
10
+ agent?: string;
11
+ model?: string;
12
+ harness?: string;
13
+ status: "running" | "ok" | "failed" | "blocked" | "pending";
14
+ attempts: number;
15
+ startedAt?: string;
16
+ endedAt?: string;
17
+ durationMs?: number;
18
+ costUsd?: number;
19
+ tokensIn?: number;
20
+ tokensOut?: number;
21
+ gates: GateView[];
22
+ summary?: string;
23
+ lastActivity?: string;
24
+ stdout?: string;
25
+ stderr?: string;
26
+ }
27
+ export interface FileView {
28
+ name: string;
29
+ size: number;
30
+ mtime: string;
31
+ }
32
+ export interface RunListItem {
33
+ id: string;
34
+ workflow?: string;
35
+ request?: string;
36
+ status: RunStatus;
37
+ startedAt?: string;
38
+ updatedAt: string;
39
+ phasesDone: number;
40
+ phasesTotal?: number;
41
+ currentPhase?: string;
42
+ durationMs?: number;
43
+ costUsd?: number;
44
+ }
45
+ export interface RunDetail extends RunListItem {
46
+ description?: string;
47
+ steps?: string[];
48
+ phases: PhaseView[];
49
+ files: FileView[];
50
+ }
51
+ export declare function listRuns(): Promise<RunListItem[]>;
52
+ export declare function safeRunDir(id: string): string;
53
+ export declare function getRun(id: string): Promise<RunDetail | null>;
54
+ export declare function readRunFile(id: string, name: string): Promise<{
55
+ absPath: string;
56
+ size: number;
57
+ } | null>;