@netnodeag/kraftwerk 0.32.0 → 0.33.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 (36) hide show
  1. package/README.md +22 -17
  2. package/dist/cli/doctor.js +2 -3
  3. package/dist/cli/kraftwerk.js +3 -0
  4. package/dist/cli/projects.d.ts +2 -0
  5. package/dist/cli/projects.js +126 -0
  6. package/dist/cli/ui.js +28 -11
  7. package/dist/config.d.ts +2 -0
  8. package/dist/config.js +2 -1
  9. package/dist/inspector/instances.d.ts +64 -1
  10. package/dist/inspector/instances.js +257 -15
  11. package/dist/inspector/self-command.d.ts +15 -0
  12. package/dist/inspector/self-command.js +18 -0
  13. package/dist/inspector/server.js +43 -7
  14. package/inspector/dist/assets/{editor-DhNlc5vd.js → editor-BlAeAqt0.js} +3 -3
  15. package/inspector/dist/assets/{index-CT73nPrH.js → index--9LQl_DL.js} +1 -1
  16. package/inspector/dist/assets/{index-xM_-dBjg.js → index--wwp-KD0.js} +1 -1
  17. package/inspector/dist/assets/{index-CWIXOY1D.js → index-9rElziKS.js} +1 -1
  18. package/inspector/dist/assets/{index-C22iDz8w.js → index-AR7uGZPa.js} +1 -1
  19. package/inspector/dist/assets/{index--vNjDAEm.js → index-B0qGMqfM.js} +1 -1
  20. package/inspector/dist/assets/index-B7W4QbA2.css +1 -0
  21. package/inspector/dist/assets/{index-DYnto4lP.js → index-BKnMNAOc.js} +1 -1
  22. package/inspector/dist/assets/{index-DmZE5psD.js → index-BvCAG0mQ.js} +1 -1
  23. package/inspector/dist/assets/{index-Hlokid_B.js → index-BvGhLr64.js} +1 -1
  24. package/inspector/dist/assets/{index-C9y82Xkr.js → index-BxPjb1aG.js} +1 -1
  25. package/inspector/dist/assets/{index-BWQugB-m.js → index-C3ugp_Wf.js} +1 -1
  26. package/inspector/dist/assets/{index-IK4Vsg-q.js → index-C5IBvIVa.js} +44 -44
  27. package/inspector/dist/assets/{index-Dx4HOh5k.js → index-CV4Aiss-.js} +1 -1
  28. package/inspector/dist/assets/{index-BanLa7PQ.js → index-CdkhUV5I.js} +1 -1
  29. package/inspector/dist/assets/{index-BvrVgIw_.js → index-D3O4auYk.js} +1 -1
  30. package/inspector/dist/assets/{index-BmdO9i46.js → index-DcXZX6dj.js} +1 -1
  31. package/inspector/dist/assets/{index-Dgi1UC16.js → index-Dnqjpy0h.js} +1 -1
  32. package/inspector/dist/assets/{index-CK6xHVuA.js → index-LBoNKP4N.js} +1 -1
  33. package/inspector/dist/assets/{index-2RQVAOu3.js → index-e-11MUIo.js} +1 -1
  34. package/inspector/dist/index.html +2 -2
  35. package/package.json +1 -1
  36. package/inspector/dist/assets/index-iGaBYIin.css +0 -1
package/README.md CHANGED
@@ -1,26 +1,31 @@
1
- # kraftwerk
1
+ # Kraftwerk
2
2
 
3
3
  **Agentic workspace.** Kraftwerk is an open-source agentic workspace where
4
- humans and persistent AI coworkers work on real tasks together.
5
-
6
- **Why it exists.** We think every team needs a place to bring persistent AI
7
- coworkers and humans together to get work done. Today, AI work mostly lives in
8
- individual silos: personal prompts, Claude Code sessions, scripts, and one-off
9
- automations. Kraftwerk turns individual AI use into shared, repeatable ways of
10
- working across a team or organisation.
11
-
12
- **How it works.** Kraftwerk brings together:
13
-
14
- - **Agents** — persistent AI coworkers, each with its own identity and memory,
15
- running on Claude Code, Codex, Pi, and others
4
+ humans and persistent AI coworkers work on real tasks together. Agents,
5
+ knowledge, skills and workflows belong to the team, not to the person who
6
+ happened to set them up, and every result is checked before anyone relies on
7
+ it.
8
+
9
+ **Why teams need it.** AI work today lives in individual silos: personal
10
+ prompts, Claude Code sessions, scripts, and one-off automations. Kraftwerk
11
+ gives a team or organisation one place where AI use becomes shared,
12
+ repeatable practice: agents that remember and are accountable, knowledge that
13
+ is curated and verified, workflows whose results are checked before the work
14
+ moves on.
15
+
16
+ **What's in the workspace.**
17
+
18
+ - **Agents** — persistent AI coworkers, each with its own identity, memory
19
+ and standing orders, running on Claude Code, Codex, Pi, and others
20
+ - **Knowledge** — shared organisational and project context, browsed like a
21
+ wiki, edited with autosave, verified by humans, exportable as PDF
16
22
  - **Skills** — reusable capabilities and ways of working
17
- - **Knowledge** — shared organisational and project context
18
23
  - **Workflows** — repeatable multi-step processes
19
24
  - **Verification** — deterministic gates that check the work
25
+ - **Inspector** — the web UI that brings it all together
20
26
 
21
- **The shift.** From everyone using AI individually → to the organisation
22
- working agentically. Kraftwerk is the shared place where that work lives,
23
- runs, and improves over time.
27
+ This README is the reference for the framework and CLI. For the overview,
28
+ start at the [project README](https://github.com/NETNODEAG/kraftwerk#readme).
24
29
 
25
30
  ## Under the hood
26
31
 
@@ -1,8 +1,8 @@
1
1
  import { spawnSync } from "node:child_process";
2
- import { readFile, stat } from "node:fs/promises";
2
+ import { readFile } from "node:fs/promises";
3
3
  import path from "node:path";
4
4
  import chalk from "chalk";
5
- import { resolveProject } from "../config.js";
5
+ import { isDir, resolveProject } from "../config.js";
6
6
  import { discoverWorkflows } from "../discover.js";
7
7
  import { missingEnv } from "../yaml.js";
8
8
  const ICONS = {
@@ -90,7 +90,6 @@ export async function runDoctor(cwd) {
90
90
  report("ok", `${path.basename(project.configPath)} well-formed`, keys.length ? keys.map((k) => `${k}: ${cfg[k]}`).join(", ") : "empty — defaults apply");
91
91
  if (!cfg.name)
92
92
  report("info", "name not set in kraftwerk.yml", "inspector header falls back to the folder name");
93
- const isDir = async (p) => (await stat(p).catch(() => null))?.isDirectory() ?? false;
94
93
  for (const key of ["workflows", "knowledge", "agents", "output"]) {
95
94
  const value = cfg[key];
96
95
  if (!value)
@@ -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 { registerKnowledgeCommands } from "./knowledge.js";
15
+ import { registerProjectCommands } from "./projects.js";
15
16
  import { registerRoutineCommands } from "./routines.js";
16
17
  import { listRuns, showRun } from "./runs.js";
17
18
  import { runUi } from "./ui.js";
@@ -25,6 +26,7 @@ import { runUi } from "./ui.js";
25
26
  * kraftwerk knowledge ... Context & Knowledge: OKF bundles (list/get/put/verify/...)
26
27
  * kraftwerk routines ... per-agent scheduled prompts (list/add/remove/enable/run)
27
28
  * kraftwerk ui start the inspector web UI (localhost:1981)
29
+ * kraftwerk projects ... known projects on this machine (list/start/forget)
28
30
  * kraftwerk doctor preflight: harness CLIs, docker, workflows, env
29
31
  * kraftwerk validate [paths...] validate without executing
30
32
  *
@@ -256,6 +258,7 @@ runs
256
258
  });
257
259
  registerKnowledgeCommands(program);
258
260
  registerRoutineCommands(program);
261
+ registerProjectCommands(program);
259
262
  program
260
263
  .command("ui")
261
264
  .description("Start the inspector web UI for this project's runs and workflows")
@@ -0,0 +1,2 @@
1
+ import type { Command } from "commander";
2
+ export declare function registerProjectCommands(program: Command): void;
@@ -0,0 +1,126 @@
1
+ import path from "node:path";
2
+ import chalk from "chalk";
3
+ import Table from "cli-table3";
4
+ import { discoverWorkspaces, forgetProject, startProject, tildify, } from "../inspector/instances.js";
5
+ /**
6
+ * `kraftwerk projects` — every project that ever ran the inspector on this
7
+ * machine (~/.kraftwerk/projects), joined with what is running right now.
8
+ * The answer to "where did I start that UI, and how do I get it back":
9
+ *
10
+ * kraftwerk projects list: name, root, running/stopped
11
+ * kraftwerk projects start <ref> relaunch `kraftwerk ui` in that root, detached
12
+ * kraftwerk projects forget <ref> drop the record (the folder stays)
13
+ *
14
+ * <ref> is a project name, the root's folder name, or the root path.
15
+ */
16
+ const fmtAgo = (iso) => {
17
+ if (!iso)
18
+ return "—";
19
+ const s = Math.max(0, (Date.now() - Date.parse(iso)) / 1000);
20
+ if (s < 90)
21
+ return "just now";
22
+ if (s < 5400)
23
+ return `${Math.round(s / 60)} min ago`;
24
+ if (s < 129600)
25
+ return `${Math.round(s / 3600)} h ago`;
26
+ return `${Math.round(s / 86400)} d ago`;
27
+ };
28
+ /**
29
+ * Running → its url. Not running: "stopped" after a clean shutdown, "died"
30
+ * when the last start has no matching stop (killed, crashed, terminal
31
+ * closed) — the case this command exists for.
32
+ */
33
+ const status = (e) => {
34
+ if (e.live)
35
+ return chalk.green(`● ${e.url.replace(/^https?:\/\//, "")}`);
36
+ if (e.exists === false)
37
+ return chalk.red("path missing");
38
+ const clean = !!e.lastStopped && !!e.lastStarted && e.lastStopped >= e.lastStarted;
39
+ return clean ? chalk.dim("stopped") : chalk.yellow("died");
40
+ };
41
+ async function resolveRef(ref) {
42
+ const all = (await discoverWorkspaces()).filter((e) => e.root);
43
+ const abs = path.resolve(ref);
44
+ const hits = all.filter((e) => e.root === abs ||
45
+ e.root === ref ||
46
+ e.name === ref ||
47
+ path.basename(e.root) === ref);
48
+ if (hits.length === 1)
49
+ return hits[0];
50
+ if (hits.length === 0) {
51
+ console.error(chalk.red(`No known project "${ref}". See \`kraftwerk projects\`.`));
52
+ process.exit(2);
53
+ }
54
+ console.error(chalk.red(`"${ref}" is ambiguous — use the root path:`));
55
+ for (const h of hits)
56
+ console.error(` ${h.root}`);
57
+ process.exit(2);
58
+ }
59
+ export function registerProjectCommands(program) {
60
+ const projects = program
61
+ .command("projects")
62
+ .description("Known kraftwerk projects on this machine: list, start a stopped UI, forget");
63
+ projects
64
+ .command("list", { isDefault: true })
65
+ .description("List known projects with their root and whether the UI is running")
66
+ .option("--json", "Machine-readable output")
67
+ .action(async (opts) => {
68
+ const entries = await discoverWorkspaces();
69
+ if (opts.json) {
70
+ console.log(JSON.stringify(entries, null, 2));
71
+ return;
72
+ }
73
+ if (entries.length === 0) {
74
+ console.log(chalk.dim("No projects known yet — `kraftwerk ui` registers the current one."));
75
+ return;
76
+ }
77
+ const table = new Table({
78
+ head: ["project", "root", "status", "last started"].map((h) => chalk.bold(h)),
79
+ wordWrap: true,
80
+ colWidths: [22, 48, 24, 14],
81
+ });
82
+ for (const e of entries) {
83
+ table.push([
84
+ `${e.icon ? e.icon + " " : ""}${chalk.cyan(e.name)}`,
85
+ e.rootLabel ?? chalk.dim("(no root recorded)"),
86
+ status(e),
87
+ fmtAgo(e.lastStarted),
88
+ ]);
89
+ }
90
+ console.log(table.toString());
91
+ });
92
+ projects
93
+ .command("start")
94
+ .description("Relaunch the UI of a stopped project (detached; logs in ~/.kraftwerk/logs)")
95
+ .argument("<ref>", "Project name, folder name, or root path")
96
+ .action(async (ref) => {
97
+ const entry = await resolveRef(ref);
98
+ if (entry.live) {
99
+ console.log(`${chalk.green("✔")} ${entry.name} already running: ${chalk.cyan(entry.url)}`);
100
+ return;
101
+ }
102
+ const r = await startProject(entry.root);
103
+ if (!r.ok) {
104
+ console.error(chalk.red(`Could not start ${entry.name}: ${r.error}`));
105
+ if (r.log)
106
+ console.error(chalk.dim(`log: ${tildify(r.log)}`));
107
+ process.exit(1);
108
+ }
109
+ console.log(`${chalk.green("✔")} ${entry.name} ${r.live ? "running" : "starting"}: ${chalk.cyan(r.url)}` +
110
+ chalk.dim(` (pid ${r.pid}${r.log ? `, log ${tildify(r.log)}` : ""})`));
111
+ });
112
+ projects
113
+ .command("forget")
114
+ .description("Remove a project from the registry (its folder is untouched)")
115
+ .argument("<ref>", "Project name, folder name, or root path")
116
+ .action(async (ref) => {
117
+ const entry = await resolveRef(ref);
118
+ if (await forgetProject(entry.root)) {
119
+ console.log(`${chalk.green("✔")} forgot ${entry.name} (${entry.rootLabel})`);
120
+ }
121
+ else {
122
+ console.error(chalk.red(`No record for ${entry.root}`));
123
+ process.exit(1);
124
+ }
125
+ });
126
+ }
package/dist/cli/ui.js CHANGED
@@ -1,9 +1,11 @@
1
1
  import { existsSync } from "node:fs";
2
+ import os from "node:os";
2
3
  import path from "node:path";
3
4
  import { fileURLToPath } from "node:url";
4
5
  import { spawn, spawnSync } from "node:child_process";
5
6
  import chalk from "chalk";
6
7
  import { resolveProject } from "../config.js";
8
+ import { selfCommand } from "../inspector/self-command.js";
7
9
  import { RESTART_EXIT_CODE, startInspector } from "../inspector/server.js";
8
10
  /**
9
11
  * `kraftwerk ui` — start the inspector web UI pointed at the current
@@ -59,27 +61,42 @@ export async function runUi(cwd, opts) {
59
61
  }
60
62
  /**
61
63
  * Respawn loop around the real server. Spawns this same bin script via the
62
- * current node — works identically for global installs, local node_modules,
63
- * npx, and a file: dev checkout. Any exit other than RESTART_EXIT_CODE
64
- * (including Ctrl-C, which signals the whole foreground group) ends the loop.
64
+ * current node (selfCommand) — works identically for global installs,
65
+ * local node_modules, npx, and a file: dev checkout. Any exit other than
66
+ * RESTART_EXIT_CODE ends the loop. A SIGTERM/SIGINT to the supervisor is
67
+ * forwarded to the server, so `kill <supervisor pid>` (what `kraftwerk
68
+ * projects start` reports) takes the whole UI down; Ctrl-C signals the
69
+ * foreground group and reaches both anyway.
65
70
  */
66
71
  async function superviseUi(opts) {
67
- const args = [
68
- process.argv[1],
72
+ const { cmd, args } = selfCommand([
69
73
  "ui",
70
74
  ...(opts.port ? ["--port", opts.port] : []),
71
75
  ...(opts.output ? ["--output", opts.output] : []),
72
- ];
76
+ ]);
73
77
  for (;;) {
74
- const child = spawn(process.execPath, args, {
78
+ const child = spawn(cmd, args, {
75
79
  stdio: "inherit",
76
80
  env: { ...process.env, KRAFTWERK_UI_SUPERVISED: "1" },
77
81
  });
78
- const code = await new Promise((resolve) => {
79
- child.once("exit", (c, signal) => resolve(signal ? null : c));
82
+ const forward = (sig) => () => {
83
+ child.kill(sig);
84
+ };
85
+ const handlers = { SIGTERM: forward("SIGTERM"), SIGINT: forward("SIGINT") };
86
+ process.on("SIGTERM", handlers.SIGTERM);
87
+ process.on("SIGINT", handlers.SIGINT);
88
+ const result = await new Promise((resolve) => {
89
+ child.once("exit", (code, signal) => resolve({ code, signal }));
80
90
  });
81
- if (code !== RESTART_EXIT_CODE)
82
- process.exit(code ?? 0);
91
+ process.off("SIGTERM", handlers.SIGTERM);
92
+ process.off("SIGINT", handlers.SIGINT);
93
+ if (result.code !== RESTART_EXIT_CODE) {
94
+ // A signal-killed server is not a clean exit — say so in the exit code
95
+ // (128 + signal, the shell convention) so callers can tell.
96
+ if (result.signal)
97
+ process.exit(128 + (os.constants.signals[result.signal] ?? 1));
98
+ process.exit(result.code ?? 1);
99
+ }
83
100
  console.log(chalk.dim("↻ relaunching the UI with the current install ..."));
84
101
  }
85
102
  }
package/dist/config.d.ts CHANGED
@@ -68,6 +68,8 @@ export interface Project {
68
68
  /** Absolute run-artifact directory (may not exist yet). */
69
69
  outputDir: string;
70
70
  }
71
+ /** True when the path exists and is a directory. */
72
+ export declare const isDir: (p: string) => Promise<boolean>;
71
73
  /**
72
74
  * Resolve the project for a cwd: walk up until a kraftwerk.yml or a
73
75
  * workflows root appears; a .git directory is the fallback root, the cwd
package/dist/config.js CHANGED
@@ -32,7 +32,8 @@ export const CONFIG_SCHEMA_URL = "https://raw.githubusercontent.com/NETNODEAG/kr
32
32
  export const CONFIG_FILENAMES = ["kraftwerk.yml", "kraftwerk.yaml"];
33
33
  const WORKFLOW_ROOT_CANDIDATES = ["src/workflows", "workflows"];
34
34
  const exists = async (p) => !!(await stat(p).catch(() => null));
35
- const isDir = async (p) => (await stat(p).catch(() => null))?.isDirectory() ?? false;
35
+ /** True when the path exists and is a directory. */
36
+ export const isDir = async (p) => (await stat(p).catch(() => null))?.isDirectory() ?? false;
36
37
  async function findConfigFile(dir) {
37
38
  for (const name of CONFIG_FILENAMES) {
38
39
  const candidate = path.join(dir, name);
@@ -1,13 +1,76 @@
1
+ /** Durable per-project record. The root is the key; everything else derives from it. */
2
+ export interface ProjectRecord {
3
+ root: string;
4
+ firstSeen: string;
5
+ lastStarted: string;
6
+ /** Set on clean shutdown; older than lastStarted means the last run died. */
7
+ lastStopped?: string;
8
+ startCount: number;
9
+ }
1
10
  /** A verified running instance, shaped like a switcher entry. */
2
11
  export interface DiscoveredInstance {
3
12
  name: string;
4
13
  url: string;
5
14
  icon?: string;
6
15
  live: true;
16
+ root?: string;
17
+ }
18
+ /**
19
+ * One workspace as the switcher shows it: a known project (running or
20
+ * not) or a running instance the projects registry doesn't know yet.
21
+ */
22
+ export interface WorkspaceEntry {
23
+ name: string;
24
+ /** Where the inspector answers (live) or would answer once started. */
25
+ url: string;
26
+ icon?: string;
27
+ live: boolean;
28
+ root?: string;
29
+ /** Root with ~ for the home dir — what the UI prints. */
30
+ rootLabel?: string;
31
+ /** False when the root directory is gone (or no longer a project of its own); start is impossible. */
32
+ exists?: boolean;
33
+ lastStarted?: string;
34
+ lastStopped?: string;
7
35
  }
36
+ /** ~/… for display. */
37
+ export declare const tildify: (p: string) => string;
8
38
  /** Write this instance's registry file (call once the server listens). */
9
- export declare function registerInstance(port: number): Promise<void>;
39
+ export declare function registerInstance(port: number, root: string): Promise<void>;
10
40
  /** Remove this instance's registry file. Sync so exit handlers can call it. */
11
41
  export declare function unregisterInstance(): void;
12
42
  /** All other running instances, verified live. Cached briefly — /api/meta is polled. */
13
43
  export declare function discoverInstances(): Promise<DiscoveredInstance[]>;
44
+ /** Upsert the project record for a root (call on every inspector start). */
45
+ export declare function registerProject(root: string): Promise<void>;
46
+ /** Stamp lastStopped on clean shutdown. Sync so exit handlers can call it. */
47
+ export declare function markProjectStopped(): void;
48
+ /** All known projects, most recently started first. */
49
+ export declare function listProjects(): Promise<ProjectRecord[]>;
50
+ /** Drop a project record (the root itself is untouched). */
51
+ export declare function forgetProject(root: string): Promise<boolean>;
52
+ /**
53
+ * The switcher's view: every known project joined with the live instances,
54
+ * plus running instances with no project record yet. The calling instance
55
+ * itself is left out. Live entries first, then by last start. Cached like
56
+ * discoverInstances — /api/meta is polled and this stats every root.
57
+ */
58
+ export declare function discoverWorkspaces(): Promise<WorkspaceEntry[]>;
59
+ export interface StartResult {
60
+ ok: boolean;
61
+ url?: string;
62
+ /** True once the new inspector answered its probe within the wait. */
63
+ live?: boolean;
64
+ pid?: number;
65
+ log?: string;
66
+ error?: string;
67
+ }
68
+ /**
69
+ * Launch `kraftwerk ui` for a root as a detached process (selfCommand:
70
+ * the bin this process runs from), logging to ~/.kraftwerk/logs/<key>.log.
71
+ * The reported pid is the `kraftwerk ui` supervisor, which forwards
72
+ * SIGTERM/SIGINT to its server child, so `kill <pid>` stops the whole UI.
73
+ * Waits up to ~5s for the new inspector to answer so callers can link
74
+ * straight to it.
75
+ */
76
+ export declare function startProject(root: string): Promise<StartResult>;