@netnodeag/kraftwerk 0.64.0 → 0.66.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 (97) hide show
  1. package/README.md +126 -138
  2. package/dist/acp.js +3 -1
  3. package/dist/cli/daemon.d.ts +14 -0
  4. package/dist/cli/daemon.js +60 -0
  5. package/dist/cli/devices.d.ts +14 -0
  6. package/dist/cli/devices.js +53 -0
  7. package/dist/cli/doctor-legacy.js +9 -1
  8. package/dist/cli/doctor.js +17 -30
  9. package/dist/cli/init.js +1 -6
  10. package/dist/cli/kraftwerk.js +8 -3
  11. package/dist/cli/ui.d.ts +9 -0
  12. package/dist/cli/ui.js +47 -23
  13. package/dist/cli/workspaces.js +5 -3
  14. package/dist/client/index.d.ts +17 -0
  15. package/dist/client/index.js +10 -2
  16. package/dist/client/live.d.ts +1 -2
  17. package/dist/client/live.js +4 -2
  18. package/dist/client/routes.d.ts +8 -0
  19. package/dist/client/routes.js +8 -0
  20. package/dist/config.d.ts +18 -55
  21. package/dist/config.js +39 -92
  22. package/dist/core/chat/context.js +1 -1
  23. package/dist/core/chat/pi.js +2 -1
  24. package/dist/core/chat/sessions.d.ts +1 -1
  25. package/dist/core/chat/sessions.js +1 -1
  26. package/dist/core/chat/state.d.ts +2 -0
  27. package/dist/core/chat/state.js +5 -0
  28. package/dist/core/daemon.d.ts +27 -0
  29. package/dist/core/daemon.js +61 -0
  30. package/dist/core/devices.d.ts +35 -0
  31. package/dist/core/devices.js +156 -0
  32. package/dist/core/env.d.ts +16 -0
  33. package/dist/core/env.js +45 -0
  34. package/dist/core/git.d.ts +2 -0
  35. package/dist/core/git.js +20 -4
  36. package/dist/core/instances.d.ts +32 -12
  37. package/dist/core/instances.js +136 -64
  38. package/dist/core/routines.d.ts +6 -0
  39. package/dist/core/routines.js +11 -0
  40. package/dist/core/runner.js +7 -3
  41. package/dist/core/vibeables.d.ts +2 -0
  42. package/dist/core/vibeables.js +17 -5
  43. package/dist/core/workspace.d.ts +13 -0
  44. package/dist/core/workspace.js +25 -0
  45. package/dist/dotenv.d.ts +2 -2
  46. package/dist/dotenv.js +2 -2
  47. package/dist/server/api/devices.d.ts +20 -0
  48. package/dist/server/api/devices.js +55 -0
  49. package/dist/server/api/hub.d.ts +25 -0
  50. package/dist/server/api/hub.js +32 -0
  51. package/dist/server/api/index.d.ts +42 -2
  52. package/dist/server/api/index.js +5 -1
  53. package/dist/server/api/router.d.ts +21 -6
  54. package/dist/server/api/router.js +37 -9
  55. package/dist/server/api/socket.d.ts +15 -1
  56. package/dist/server/api/socket.js +59 -8
  57. package/dist/server/api/workspace.d.ts +2 -0
  58. package/dist/server/api/workspace.js +4 -2
  59. package/dist/server/bind.d.ts +16 -0
  60. package/dist/server/bind.js +25 -0
  61. package/dist/server/cloud.d.ts +5 -1
  62. package/dist/server/cloud.js +89 -56
  63. package/dist/server/hub-context.d.ts +7 -0
  64. package/dist/server/hub-context.js +18 -0
  65. package/dist/server/live.d.ts +2 -0
  66. package/dist/server/live.js +9 -0
  67. package/dist/server/preview.js +2 -1
  68. package/dist/server/server.d.ts +58 -1
  69. package/dist/server/server.js +267 -103
  70. package/dist/server/target.d.ts +20 -0
  71. package/dist/server/target.js +18 -0
  72. package/dist/server/trust.d.ts +41 -0
  73. package/dist/server/trust.js +42 -0
  74. package/package.json +1 -1
  75. package/schema/kraftwerk.schema.json +7 -46
  76. package/web/dist/assets/{dist-Cgcwvf4j.js → dist-1bNkaWWA.js} +1 -1
  77. package/web/dist/assets/dist-BU1SOB5S.js +1 -0
  78. package/web/dist/assets/{dist-CboP0-T1.js → dist-Bk3f-J5U.js} +1 -1
  79. package/web/dist/assets/{dist-2kswkIH2.js → dist-Bx99Puvv.js} +1 -1
  80. package/web/dist/assets/{dist-D2PvEzo7.js → dist-C0M2aITo.js} +1 -1
  81. package/web/dist/assets/{dist-D_BcTzz-.js → dist-C0z19Zge.js} +1 -1
  82. package/web/dist/assets/{dist-36MjITWa.js → dist-COEgBKG6.js} +1 -1
  83. package/web/dist/assets/{dist-Jdx6DRrm.js → dist-CYRrdlae.js} +1 -1
  84. package/web/dist/assets/{dist-DDkd_zrP.js → dist-CbEFnSCD.js} +1 -1
  85. package/web/dist/assets/{dist-BsuA2pgO.js → dist-DCYVKU-H.js} +1 -1
  86. package/web/dist/assets/{dist-B8gPjMPP.js → dist-XyTHWtoN.js} +1 -1
  87. package/web/dist/assets/{editor-ByRUOfrX.js → editor-DPcGvSPy.js} +3 -3
  88. package/web/dist/assets/index-BAYs-2WC.css +2 -0
  89. package/web/dist/assets/index-DKLJdwYE.js +229 -0
  90. package/web/dist/index.html +2 -2
  91. package/dist/cli/tunnel.d.ts +0 -10
  92. package/dist/cli/tunnel.js +0 -280
  93. package/dist/server/access.d.ts +0 -32
  94. package/dist/server/access.js +0 -121
  95. package/web/dist/assets/dist-CVqmJrYd.js +0 -1
  96. package/web/dist/assets/index-BR7HvDzD.js +0 -229
  97. package/web/dist/assets/index-Bra74Nxe.css +0 -2
@@ -17,8 +17,9 @@ import { registerWorkspaceCommands } from "./workspaces.js";
17
17
  import { registerRepoCommands } from "./repos.js";
18
18
  import { registerVibeableCommands } from "./vibeables.js";
19
19
  import { registerTrashCommands } from "./trash.js";
20
+ import { registerDevicesCommands } from "./devices.js";
21
+ import { registerDaemonCommand } from "./daemon.js";
20
22
  import { registerJournalCommands } from "./journal.js";
21
- import { registerTunnelCommands } from "./tunnel.js";
22
23
  import { applyDotenv } from "../dotenv.js";
23
24
  import { resolveWorkspace } from "../config.js";
24
25
  import { registerRoutineCommands } from "./routines.js";
@@ -40,7 +41,8 @@ import { runUi } from "./ui.js";
40
41
  * kraftwerk repos ... repositories the agents work on (list/add/update/remove)
41
42
  * kraftwerk journal <agent> [entry] an agent's memory across sessions: print it, add a line
42
43
  * kraftwerk trash ... what was deleted: list, restore, purge, empty
43
- * kraftwerk tunnel [setup <host>] Cloudflare Tunnel to the inspector: run it alone, or set one up
44
+ * kraftwerk devices ... paired devices: list, pair one, revoke one
45
+ * kraftwerk daemon the machine's kraftwerk: every open workspace at <slug>.localhost
44
46
  * kraftwerk doctor preflight: harness CLIs, docker, workflows, env
45
47
  * kraftwerk validate [paths...] validate without executing
46
48
  *
@@ -291,13 +293,16 @@ registerProjectCommands(program);
291
293
  registerRepoCommands(program);
292
294
  registerVibeableCommands(program);
293
295
  registerTrashCommands(program);
296
+ registerDevicesCommands(program);
297
+ registerDaemonCommand(program);
294
298
  registerJournalCommands(program);
295
- registerTunnelCommands(program);
296
299
  program
297
300
  .command("ui")
298
301
  .description("Start the inspector web UI for this project's runs and workflows")
299
302
  .option("--port <port>", "Port for the web UI (default: kraftwerk.yml `port`, else 1981)")
300
303
  .option("--output <dir>", "Output directory to inspect (default: the project's output dir)")
304
+ .option("--lan", "Listen on the network too, so paired devices (a phone) reach it — others get a pairing screen")
305
+ .option("--standalone", "Run a server of its own even when the kraftwerk daemon runs")
301
306
  .action(async (opts) => {
302
307
  await runUi(process.cwd(), opts);
303
308
  });
package/dist/cli/ui.d.ts CHANGED
@@ -1,4 +1,13 @@
1
+ /**
2
+ * Published packages ship web/dist. A dev checkout builds it on
3
+ * first use (and after frontend edits: `npm run build` in web/).
4
+ */
5
+ export declare function ensureBuilt(): string;
1
6
  export declare function runUi(cwd: string, opts: {
2
7
  port?: string;
3
8
  output?: string;
9
+ lan?: boolean;
10
+ standalone?: boolean;
4
11
  }): Promise<void>;
12
+ /** The respawn loop for any long-running kraftwerk process (`ui`, `daemon`): see superviseUi. */
13
+ export declare function supervise(argv: string[], what: string): Promise<void>;
package/dist/cli/ui.js CHANGED
@@ -4,10 +4,12 @@ import path from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
5
  import { spawn, spawnSync } from "node:child_process";
6
6
  import chalk from "chalk";
7
- import { absolutePath, publicUrlFor, resolveWorkspace } from "../config.js";
8
- import { startTunnel } from "./tunnel.js";
7
+ import { absolutePath, resolveWorkspace, workspaceSlug } from "../config.js";
8
+ import { slugClashes } from "../core/instances.js";
9
+ import { daemonCall, findDaemon } from "../core/daemon.js";
9
10
  import { selfCommand } from "../core/self-command.js";
10
11
  import { RESTART_EXIT_CODE, startInspector } from "../server/server.js";
12
+ import { networkUrls } from "../server/bind.js";
11
13
  /**
12
14
  * `kraftwerk ui` — start the inspector web UI pointed at the current
13
15
  * project's output dir. The server is dependency-free node:http (part of
@@ -26,7 +28,10 @@ const packageRoot = () => path.join(path.dirname(fileURLToPath(import.meta.url))
26
28
  * Published packages ship web/dist. A dev checkout builds it on
27
29
  * first use (and after frontend edits: `npm run build` in web/).
28
30
  */
29
- function ensureBuilt() {
31
+ export function ensureBuilt() {
32
+ // Another build of the web UI (tests serve a stub; a dev setup may point at its own).
33
+ if (process.env.KRAFTWERK_WEB_DIR)
34
+ return path.resolve(process.env.KRAFTWERK_WEB_DIR);
30
35
  const web = path.join(packageRoot(), "web");
31
36
  const dist = path.join(web, "dist");
32
37
  if (existsSync(path.join(dist, "index.html")))
@@ -49,19 +54,46 @@ function ensureBuilt() {
49
54
  return dist;
50
55
  }
51
56
  export async function runUi(cwd, opts) {
57
+ // The machine runs a daemon: hand it this workspace, it serves it next to the others.
58
+ if (!opts.standalone && !opts.port && !opts.output && !opts.lan && process.env.KRAFTWERK_UI_SUPERVISED !== "1") {
59
+ const daemon = await findDaemon();
60
+ if (daemon) {
61
+ const project = await resolveWorkspace(cwd);
62
+ const r = await daemonCall(daemon.port, "open", project.root);
63
+ if (!r.ok) {
64
+ console.error(chalk.red(`✖ the kraftwerk daemon refused: ${r.data.error ?? r.status}`));
65
+ process.exit(1);
66
+ }
67
+ console.log(`${chalk.green("✔")} Kraftwerk UI: ${chalk.cyan(r.data.url)} ${chalk.dim(`(served by the kraftwerk daemon, pid ${daemon.pid})`)}`);
68
+ if (r.data.aliasPort)
69
+ console.log(chalk.dim(` also at http://localhost:${r.data.aliasPort}`));
70
+ console.log(chalk.dim(" `kraftwerk workspaces stop .` closes it there; `kraftwerk ui --standalone` runs a server of its own instead."));
71
+ return;
72
+ }
73
+ }
74
+ // The server reads its bind address when it starts — in the supervised child, which inherits this.
75
+ if (opts.lan)
76
+ process.env.KRAFTWERK_UI_HOST ||= "0.0.0.0";
52
77
  if (process.env.KRAFTWERK_UI_SUPERVISED !== "1")
53
- return superviseUi(cwd, opts);
78
+ return superviseUi(opts);
54
79
  const staticDir = ensureBuilt();
55
80
  const project = await resolveWorkspace(cwd);
56
81
  const outputDir = opts.output ? absolutePath(opts.output, cwd) : project.outputDir;
57
82
  // Port precedence: --port flag > kraftwerk.yml `port` > 1981.
58
83
  const port = opts.port ? Number(opts.port) : (project.config.port ?? 1981);
59
84
  await startInspector({ outputDir, staticDir, port, root: project.root });
85
+ // Two workspaces with one slug would share an address once one kraftwerk serves them all.
86
+ const slug = workspaceSlug(project);
87
+ const clash = await slugClashes(slug, project.root);
88
+ if (clash.length)
89
+ console.log(`${chalk.yellow("⚠")} slug "${slug}" is also used by ${clash.map((r) => r.root).join(", ")} — set \`slug:\` in one kraftwerk.yml so each workspace keeps its own address`);
60
90
  console.log(`${chalk.green("✔")} Kraftwerk UI: ${chalk.cyan(`http://localhost:${port}`)} ` +
61
91
  chalk.dim(`(output: ${outputDir})`));
62
- const publicUrl = publicUrlFor(project);
63
- if (publicUrl)
64
- console.log(`${chalk.green("✔")} Public URL: ${chalk.cyan(publicUrl)}`);
92
+ const lan = networkUrls(port);
93
+ if (lan.length) {
94
+ console.log(`${chalk.green("✔")} On your network: ${lan.map((u) => chalk.cyan(u)).join(", ")}`);
95
+ console.log(chalk.dim(" Devices there get a pairing screen — `kraftwerk devices pair` (or settings → devices) makes the code."));
96
+ }
65
97
  }
66
98
  /**
67
99
  * Respawn loop around the real server. Spawns this same bin script via the
@@ -72,25 +104,18 @@ export async function runUi(cwd, opts) {
72
104
  * workspaces start` reports) takes the whole UI down; Ctrl-C signals the
73
105
  * foreground group and reaches both anyway.
74
106
  */
75
- async function superviseUi(cwd, opts) {
76
- const { cmd, args } = selfCommand([
77
- "ui",
78
- ...(opts.port ? ["--port", opts.port] : []),
79
- ...(opts.output ? ["--output", opts.output] : []),
80
- ]);
81
- // The tunnel lives with the supervisor, not the server: a self-restart
82
- // (new version) swaps the server process and the tunnel stays up.
83
- const project = await resolveWorkspace(cwd).catch(() => null);
84
- const tunnel = project ? startTunnel(project, opts.port ? Number(opts.port) : (project.config.port ?? 1981)) : undefined;
107
+ async function superviseUi(opts) {
108
+ return supervise(["ui", "--standalone", ...(opts.port ? ["--port", opts.port] : []), ...(opts.output ? ["--output", opts.output] : [])], "the UI");
109
+ }
110
+ /** The respawn loop for any long-running kraftwerk process (`ui`, `daemon`): see superviseUi. */
111
+ export async function supervise(argv, what) {
112
+ const { cmd, args } = selfCommand(argv);
85
113
  for (;;) {
86
114
  const child = spawn(cmd, args, {
87
115
  stdio: "inherit",
88
116
  env: { ...process.env, KRAFTWERK_UI_SUPERVISED: "1" },
89
117
  });
90
- const forward = (sig) => () => {
91
- tunnel?.stop();
92
- child.kill(sig);
93
- };
118
+ const forward = (sig) => () => child.kill(sig);
94
119
  const handlers = { SIGTERM: forward("SIGTERM"), SIGINT: forward("SIGINT") };
95
120
  process.on("SIGTERM", handlers.SIGTERM);
96
121
  process.on("SIGINT", handlers.SIGINT);
@@ -100,13 +125,12 @@ async function superviseUi(cwd, opts) {
100
125
  process.off("SIGTERM", handlers.SIGTERM);
101
126
  process.off("SIGINT", handlers.SIGINT);
102
127
  if (result.code !== RESTART_EXIT_CODE) {
103
- tunnel?.stop();
104
128
  // A signal-killed server is not a clean exit — say so in the exit code
105
129
  // (128 + signal, the shell convention) so callers can tell.
106
130
  if (result.signal)
107
131
  process.exit(128 + (os.constants.signals[result.signal] ?? 1));
108
132
  process.exit(result.code ?? 1);
109
133
  }
110
- console.log(chalk.dim("↻ relaunching the UI with the current install ..."));
134
+ console.log(chalk.dim(`↻ relaunching ${what} with the current install ...`));
111
135
  }
112
136
  }
@@ -50,7 +50,9 @@ async function resolveRef(ref, needRoot = true) {
50
50
  e.root === ref ||
51
51
  e.name === ref ||
52
52
  (e.root && path.basename(e.root) === ref) ||
53
- e.url === port);
53
+ e.url === port ||
54
+ // A daemon's workspace by its slug: <slug>.localhost.
55
+ e.url.startsWith(`http://${ref}.localhost:`));
54
56
  if (hits.length === 1)
55
57
  return hits[0];
56
58
  if (hits.length === 0) {
@@ -117,8 +119,8 @@ export function registerWorkspaceCommands(program) {
117
119
  });
118
120
  workspaces
119
121
  .command("stop")
120
- .description("Stop a running UI (SIGTERM to its server; works for terminal-started ones too)")
121
- .argument("<ref>", "Workspace name, folder name, root path, or port")
122
+ .description("Stop a running UI (closes it in the kraftwerk daemon, or SIGTERM to a server of its own)")
123
+ .argument("<ref>", "Workspace name, slug, folder name, root path, or port")
122
124
  .action(async (ref) => {
123
125
  const entry = await resolveRef(ref, false);
124
126
  if (!entry.live) {
@@ -63,7 +63,20 @@ export declare function errorOf(body: unknown): string | undefined;
63
63
  export interface ClientOptions {
64
64
  /** Where the server is; "" (the default) is the page's own origin. */
65
65
  baseUrl?: string;
66
+ /**
67
+ * The workspace (its slug) to talk to, through the `/w/<slug>` path form.
68
+ * Leave it out when the address already names one (`<slug>.localhost`, or
69
+ * a server's own port).
70
+ */
71
+ workspace?: string;
66
72
  fetch?: typeof fetch;
73
+ /**
74
+ * A paired device's token (from `devices.pair`), sent as `Authorization:
75
+ * Bearer`. A browser needs none: pairing sets an HttpOnly cookie.
76
+ */
77
+ token?: string;
78
+ /** Called when the server says this device is not paired (401 `{pair: true}`) — show the pairing screen. */
79
+ onUnauthorized?: () => void;
67
80
  }
68
81
  export interface Client {
69
82
  call<N extends ApiName>(name: N, ...input: InputArg<N>): Promise<ApiResult<N>>;
@@ -72,5 +85,9 @@ export interface Client {
72
85
  url<N extends ApiName>(name: N, ...input: InputArg<N>): string;
73
86
  /** Called after every change this client made (a non-GET route that went through). */
74
87
  onChange(fn: () => void): () => void;
88
+ /** The token the client was created with, if any (the socket sends it too). */
89
+ readonly token?: string;
90
+ /** Where routes live: baseUrl plus the workspace's `/w/<slug>` prefix. */
91
+ readonly baseUrl: string;
75
92
  }
76
93
  export declare function createClient(opts?: ClientOptions): Client;
@@ -17,7 +17,7 @@ export function errorOf(body) {
17
17
  return typeof error === "string" && error ? error : undefined;
18
18
  }
19
19
  export function createClient(opts = {}) {
20
- const base = (opts.baseUrl ?? "").replace(/\/+$/, "");
20
+ const base = (opts.baseUrl ?? "").replace(/\/+$/, "") + (opts.workspace ? `/w/${encodeURIComponent(opts.workspace)}` : "");
21
21
  const doFetch = opts.fetch ?? ((...a) => fetch(...a));
22
22
  const changeFns = new Set();
23
23
  const url = (name, input = {}) => {
@@ -44,7 +44,11 @@ export function createClient(opts = {}) {
44
44
  cache: "no-store",
45
45
  signal,
46
46
  keepalive,
47
- headers: { ...(body !== undefined && !raw ? { "content-type": "application/json" } : {}), ...headers },
47
+ headers: {
48
+ ...(body !== undefined && !raw ? { "content-type": "application/json" } : {}),
49
+ ...(opts.token ? { authorization: `Bearer ${opts.token}` } : {}),
50
+ ...headers,
51
+ },
48
52
  body: body === undefined ? undefined : raw ? body : JSON.stringify(body),
49
53
  });
50
54
  const text = await res.text();
@@ -56,9 +60,13 @@ export function createClient(opts = {}) {
56
60
  if (res.ok && method !== "GET")
57
61
  for (const fn of changeFns)
58
62
  fn();
63
+ if (res.status === 401 && data?.pair === true)
64
+ opts.onUnauthorized?.();
59
65
  return res.ok ? { ok: true, status: res.status, data } : { ok: false, status: res.status, data, error: errorOf(data) };
60
66
  };
61
67
  return {
68
+ token: opts.token,
69
+ baseUrl: base,
62
70
  onChange(fn) {
63
71
  changeFns.add(fn);
64
72
  return () => changeFns.delete(fn);
@@ -25,9 +25,8 @@ export interface Live {
25
25
  close(): void;
26
26
  }
27
27
  export interface LiveOptions {
28
- /** The HTTP client for fallbacks; its baseUrl is the server. */
28
+ /** The HTTP client: its baseUrl (workspace prefix included) is where the socket connects too. */
29
29
  client: Client;
30
- baseUrl?: string;
31
30
  WebSocket?: typeof WebSocket;
32
31
  EventSource?: typeof EventSource;
33
32
  }
@@ -12,8 +12,10 @@ export function createLive(opts) {
12
12
  let retry;
13
13
  let counter = 0;
14
14
  const socketUrl = () => {
15
- const base = opts.baseUrl ?? (typeof location === "undefined" ? "" : location.origin);
16
- return `${base.replace(/^http/, "ws").replace(/\/+$/, "")}/api/ws`;
15
+ // The client's base carries the workspace prefix; a relative one is on this page's origin.
16
+ const base = /^https?:/.test(client.baseUrl) ? client.baseUrl : (typeof location === "undefined" ? "" : location.origin) + client.baseUrl;
17
+ // A browser's socket carries the device cookie; other runtimes cannot send headers on a WebSocket, so the token rides in the query.
18
+ return `${base.replace(/^http/, "ws").replace(/\/+$/, "")}/api/ws${client.token ? `?token=${encodeURIComponent(client.token)}` : ""}`;
17
19
  };
18
20
  const send = (msg) => {
19
21
  if (connected)
@@ -43,6 +43,11 @@ export declare const ROUTES: {
43
43
  readonly "chats.setVibeable": readonly ["POST", "/api/chats/:id/vibeable"];
44
44
  readonly "chats.steer": readonly ["POST", "/api/chats/:id/steer"];
45
45
  readonly "chats.stopTask": readonly ["POST", "/api/chats/:id/task-stop"];
46
+ readonly "devices.code": readonly ["POST", "/api/devices/code"];
47
+ readonly "devices.list": readonly ["GET", "/api/devices"];
48
+ readonly "devices.pair": readonly ["POST", "/api/pair"];
49
+ readonly "devices.revoke": readonly ["DELETE", "/api/devices/:id"];
50
+ readonly "devices.self": readonly ["GET", "/api/devices/self"];
46
51
  readonly "files.delete": readonly ["DELETE", "/api/files"];
47
52
  readonly "files.list": readonly ["GET", "/api/files/list"];
48
53
  readonly "files.mkdir": readonly ["POST", "/api/files/folder"];
@@ -57,6 +62,9 @@ export declare const ROUTES: {
57
62
  readonly "git.pull": readonly ["POST", "/api/git/pull"];
58
63
  readonly "git.push": readonly ["POST", "/api/git/push"];
59
64
  readonly "git.status": readonly ["GET", "/api/git"];
65
+ readonly "hub.close": readonly ["POST", "/api/hub/close"];
66
+ readonly "hub.list": readonly ["GET", "/api/hub"];
67
+ readonly "hub.open": readonly ["POST", "/api/hub/open"];
60
68
  readonly "knowledge.concept": readonly ["GET", "/api/knowledge/:bundle/concept"];
61
69
  readonly "knowledge.create": readonly ["POST", "/api/knowledge"];
62
70
  readonly "knowledge.delete": readonly ["DELETE", "/api/knowledge/:bundle"];
@@ -44,6 +44,11 @@ export const ROUTES = {
44
44
  "chats.setVibeable": ["POST", "/api/chats/:id/vibeable"],
45
45
  "chats.steer": ["POST", "/api/chats/:id/steer"],
46
46
  "chats.stopTask": ["POST", "/api/chats/:id/task-stop"],
47
+ "devices.code": ["POST", "/api/devices/code"],
48
+ "devices.list": ["GET", "/api/devices"],
49
+ "devices.pair": ["POST", "/api/pair"],
50
+ "devices.revoke": ["DELETE", "/api/devices/:id"],
51
+ "devices.self": ["GET", "/api/devices/self"],
47
52
  "files.delete": ["DELETE", "/api/files"],
48
53
  "files.list": ["GET", "/api/files/list"],
49
54
  "files.mkdir": ["POST", "/api/files/folder"],
@@ -58,6 +63,9 @@ export const ROUTES = {
58
63
  "git.pull": ["POST", "/api/git/pull"],
59
64
  "git.push": ["POST", "/api/git/push"],
60
65
  "git.status": ["GET", "/api/git"],
66
+ "hub.close": ["POST", "/api/hub/close"],
67
+ "hub.list": ["GET", "/api/hub"],
68
+ "hub.open": ["POST", "/api/hub/open"],
61
69
  "knowledge.concept": ["GET", "/api/knowledge/:bundle/concept"],
62
70
  "knowledge.create": ["POST", "/api/knowledge"],
63
71
  "knowledge.delete": ["DELETE", "/api/knowledge/:bundle"],
package/dist/config.d.ts CHANGED
@@ -12,6 +12,7 @@
12
12
  * name: my-project # display name (inspector header, "environment")
13
13
  * icon: "⚡" # emoji shown as inspector favicon
14
14
  * color: "#c2410c" # accent for this workspace in the switcher (default: derived from the root)
15
+ * slug: team-blau # this workspace's address (team-blau.localhost); default: the folder name
15
16
  * port: 1981 # port `kraftwerk ui` listens on
16
17
  * workflows: src/workflows # workflows root, relative to the file
17
18
  * output: output # run-artifact directory, relative to the file
@@ -32,12 +33,6 @@
32
33
  * root: apps # one folder per app, part of the workspace. Default: kraftwerk-data/vibeables
33
34
  * projects: # goal-scoped folders (brief, systems of record, links) that chats work in (absent = off, bare key = on)
34
35
  * root: kraftwerk-data/projects # one folder per project, part of the workspace. Default: kraftwerk-data/projects
35
- * public: https://kw.example.com # hostname the inspector is reached at through a tunnel or reverse proxy
36
- * tunnel: # Cloudflare Tunnel run by `kraftwerk ui` (absent = off, bare key = on)
37
- * name: kraftwerk # locally-managed tunnel (cloudflared tunnel create); absent: TUNNEL_TOKEN env, dashboard-managed
38
- * access: # verify the Cloudflare Access login on every request that arrives via `public`
39
- * team: acme # Zero Trust team name (https://<team>.cloudflareaccess.com)
40
- * aud: 4714c135… # Application Audience tag of the Access application
41
36
  * cloud: # kraftwerk cloud manager — ON BY DEFAULT, `enabled: false` opts out
42
37
  * url: https://srv.kraftwerk-cloud.netnode.cloud # where the cloud answers (default). KRAFTWERK_CLOUD_URL env wins; "off" disables
43
38
  * interval: 60 # seconds between heartbeats. Default: 60
@@ -126,39 +121,6 @@ export interface ProjectsConfig {
126
121
  export declare const PROJECTS_DEFAULT_ROOT = "kraftwerk-data/projects";
127
122
  /** Absolute projects root when the feature is on, undefined otherwise. */
128
123
  export declare function projectsRootFor(project: ResolvedWorkspace): string | undefined;
129
- /**
130
- * Cloudflare Access in front of the public hostname. Access puts a login
131
- * page on the hostname at Cloudflare's edge and hands the origin a signed
132
- * JWT per request (Cf-Access-Jwt-Assertion). With this block the inspector
133
- * verifies that token itself, so a removed or misconfigured Access policy
134
- * fails closed instead of exposing the UI, which has no login of its own.
135
- */
136
- export interface AccessConfig {
137
- /** Zero Trust team name: the subdomain of https://<team>.cloudflareaccess.com */
138
- team: string;
139
- /** Application Audience (AUD) tag of the Access application, from its overview page. */
140
- aud: string;
141
- }
142
- /**
143
- * Cloudflare Tunnel: `kraftwerk ui` runs cloudflared next to the inspector
144
- * so the loopback bind is reachable at `public` without an open port. The
145
- * tunnel itself is created once with cloudflared (or in the Zero Trust
146
- * dashboard); this block only says which one to run.
147
- */
148
- export interface TunnelConfig {
149
- /** false keeps the block but turns the feature off. Default: true. */
150
- enabled?: boolean;
151
- /**
152
- * Name of a locally-managed tunnel (`cloudflared tunnel create <name>`,
153
- * `cloudflared tunnel route dns <name> <public host>`); cloudflared routes
154
- * everything to the inspector port. Absent: a dashboard-managed tunnel,
155
- * whose token comes from the TUNNEL_TOKEN environment variable and whose
156
- * route to http://localhost:<port> is configured in the dashboard.
157
- */
158
- name?: string;
159
- /** Verify the Cloudflare Access token on every request that arrives via `public`. */
160
- access?: AccessConfig;
161
- }
162
124
  /**
163
125
  * Cloud manager: the kraftwerk cloud is ~/.kraftwerk made reachable over
164
126
  * the internet — a registry of instances across machines. Every inspector
@@ -189,14 +151,6 @@ export interface CloudSettings {
189
151
  }
190
152
  /** The effective cloud settings when the feature is on; undefined when kraftwerk.yml or the environment turned it off. */
191
153
  export declare function cloudFor(project: ResolvedWorkspace): CloudSettings | undefined;
192
- /** The public hostname (lowercase, no port) when `public` is set, undefined otherwise. */
193
- export declare function publicHostFor(project: ResolvedWorkspace): string | undefined;
194
- /** The public origin ("https://kw.example.com") when `public` is set, undefined otherwise. */
195
- export declare function publicUrlFor(project: ResolvedWorkspace): string | undefined;
196
- /** The tunnel block when the feature is on, undefined otherwise. */
197
- export declare function tunnelFor(project: ResolvedWorkspace): TunnelConfig | undefined;
198
- /** "kw.example.com" or "https://kw.example.com[:port]" → its URL; undefined when it is neither. */
199
- export declare function parsePublic(value: string): URL | undefined;
200
154
  /** A directory as a .gitignore entry: relative, forward slashes, no trailing slash; undefined outside the root. */
201
155
  export declare function ignoreEntryFor(projectRoot: string, dir: string): string | undefined;
202
156
  /** True when .gitignore text already covers the entry (with or without a leading or trailing slash). */
@@ -213,6 +167,12 @@ export interface SwitcherEntry {
213
167
  export interface WorkspaceConfig {
214
168
  /** Display name of the project ("environment"), shown in the inspector header. */
215
169
  name?: string;
170
+ /**
171
+ * The workspace's address on this machine: `<slug>.localhost`, `/w/<slug>/`.
172
+ * Unique per machine. Default: the folder name (see workspaceSlug). The
173
+ * name above may change freely; the slug only when you mean to move it.
174
+ */
175
+ slug?: string;
216
176
  /** Emoji used as the inspector favicon (browser-tab icon). */
217
177
  icon?: string;
218
178
  /** Accent colour (#rgb / #rrggbb) for this workspace in the switcher. Default: derived from the root path. */
@@ -239,14 +199,6 @@ export interface WorkspaceConfig {
239
199
  vibeables?: VibeablesConfig;
240
200
  /** Projects: goal-scoped folders the chats work in. Absent = off. */
241
201
  projects?: ProjectsConfig;
242
- /**
243
- * Hostname the inspector is reached at through a tunnel or reverse proxy,
244
- * e.g. "https://kw.example.com". The loopback bind then answers requests
245
- * carrying that Host even without X-Forwarded-Host (cloudflared sends none).
246
- */
247
- public?: string;
248
- /** Cloudflare Tunnel run alongside the inspector. Absent = off. */
249
- tunnel?: TunnelConfig;
250
202
  /** kraftwerk cloud manager this instance registers with. Absent = on with defaults; `enabled: false` opts out. */
251
203
  cloud?: CloudConfig;
252
204
  }
@@ -261,6 +213,8 @@ export interface ResolvedWorkspace {
261
213
  workflowsRoot?: string;
262
214
  /** Absolute run-artifact directory (may not exist yet). */
263
215
  outputDir: string;
216
+ /** Keys in kraftwerk.yml that an older kraftwerk read and this one ignores (`public`, `tunnel`), for doctor. Absent when none. */
217
+ legacyKeys?: string[];
264
218
  }
265
219
  /** True when the path exists and is a directory. */
266
220
  /**
@@ -287,3 +241,12 @@ export declare const resolveProject: typeof resolveWorkspace;
287
241
  export type Project = ResolvedWorkspace;
288
242
  /** @deprecated Use `WorkspaceConfig`. */
289
243
  export type ProjectConfig = WorkspaceConfig;
244
+ /** A folder name as a slug: "Team Blau (2024)" → "team-blau-2024". */
245
+ export declare function slugFromFolder(folder: string): string;
246
+ /** The workspace's slug: kraftwerk.yml `slug`, else its folder name made into one. */
247
+ export declare function workspaceSlug(ws: {
248
+ root: string;
249
+ config: {
250
+ slug?: string;
251
+ };
252
+ }): string;