@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.
- package/README.md +8 -6
- package/dist/cli/kraftwerk.js +2 -2
- package/dist/cli/ui.js +32 -55
- package/dist/inspector/chat/acp.d.ts +2 -0
- package/dist/inspector/chat/acp.js +122 -0
- package/dist/inspector/chat/backend.d.ts +28 -0
- package/dist/inspector/chat/backend.js +1 -0
- package/dist/inspector/chat/types.d.ts +70 -0
- package/dist/inspector/chat/types.js +8 -0
- package/dist/inspector/context.d.ts +5 -0
- package/dist/inspector/context.js +19 -0
- package/dist/inspector/runner.d.ts +13 -0
- package/dist/inspector/runner.js +66 -0
- package/dist/inspector/runs.d.ts +57 -0
- package/dist/inspector/runs.js +240 -0
- package/dist/inspector/server.d.ts +8 -0
- package/dist/inspector/server.js +186 -0
- package/dist/inspector/workflows.d.ts +45 -0
- package/dist/inspector/workflows.js +186 -0
- package/inspector/dist/assets/ibm-plex-mono-latin-400-normal-CvHOgSBP.woff +0 -0
- package/inspector/dist/assets/ibm-plex-mono-latin-400-normal-DMJ8VG8y.woff2 +0 -0
- package/inspector/dist/assets/ibm-plex-mono-latin-500-normal-CB9ihrfo.woff +0 -0
- package/inspector/dist/assets/ibm-plex-mono-latin-500-normal-DSY6xOcd.woff2 +0 -0
- package/inspector/dist/assets/ibm-plex-mono-latin-600-normal-BgSNZQsw.woff2 +0 -0
- package/inspector/dist/assets/ibm-plex-mono-latin-600-normal-DWFSQ4vo.woff +0 -0
- package/inspector/dist/assets/ibm-plex-mono-latin-700-normal-7sUh57Bg.woff2 +0 -0
- package/inspector/dist/assets/ibm-plex-mono-latin-700-normal-CNHXzs6v.woff +0 -0
- package/inspector/dist/assets/ibm-plex-sans-latin-400-normal-CDDApCn2.woff2 +0 -0
- package/inspector/dist/assets/ibm-plex-sans-latin-400-normal-CYLoc0-x.woff +0 -0
- package/inspector/dist/assets/ibm-plex-sans-latin-500-normal-6ng42L7E.woff2 +0 -0
- package/inspector/dist/assets/ibm-plex-sans-latin-500-normal-BgVn5rGT.woff +0 -0
- package/inspector/dist/assets/ibm-plex-sans-latin-600-normal-Cu4Hd6ag.woff +0 -0
- package/inspector/dist/assets/ibm-plex-sans-latin-600-normal-CuJfVYMP.woff2 +0 -0
- package/inspector/dist/assets/ibm-plex-sans-latin-700-normal-Bth3BMcD.woff +0 -0
- package/inspector/dist/assets/ibm-plex-sans-latin-700-normal-Bxkt5Cjx.woff2 +0 -0
- package/inspector/dist/assets/index-BCSWkslF.css +1 -0
- package/inspector/dist/assets/index-D6WuziFQ.js +9 -0
- package/inspector/dist/index.html +24 -0
- package/package.json +7 -10
- package/inspector/README.md +0 -67
- package/inspector/app/api/runs/[id]/file/route.ts +0 -56
- package/inspector/app/api/runs/[id]/route.ts +0 -15
- package/inspector/app/api/runs/[id]/stop/route.ts +0 -15
- package/inspector/app/api/runs/route.ts +0 -9
- package/inspector/app/api/workflows/[slug]/route.ts +0 -11
- package/inspector/app/api/workflows/[slug]/run/route.ts +0 -37
- package/inspector/app/api/workflows/route.ts +0 -8
- package/inspector/app/globals.css +0 -460
- package/inspector/app/layout.tsx +0 -54
- package/inspector/app/page.tsx +0 -16
- package/inspector/app/runs/[id]/page.tsx +0 -6
- package/inspector/app/runs/[id]/run-detail.tsx +0 -343
- package/inspector/app/shared.tsx +0 -81
- package/inspector/app/theme-toggle.tsx +0 -19
- package/inspector/app/workflows/[slug]/page.tsx +0 -6
- package/inspector/app/workflows/[slug]/workflow-view.tsx +0 -271
- package/inspector/app/workflows/page.tsx +0 -43
- package/inspector/lib/runner.ts +0 -78
- package/inspector/lib/runs.ts +0 -313
- package/inspector/lib/workflows.ts +0 -240
- package/inspector/next.config.ts +0 -5
- package/inspector/package-lock.json +0 -982
- package/inspector/package.json +0 -24
- 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:
|
|
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
|
|
344
|
-
inspector
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
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.
|
package/dist/cli/kraftwerk.js
CHANGED
|
@@ -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:
|
|
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", "
|
|
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 {
|
|
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
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
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
|
-
*
|
|
20
|
-
*
|
|
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
|
-
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 ?? "
|
|
52
|
-
|
|
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,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>;
|