@netnodeag/kraftwerk 0.65.0 → 0.67.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +112 -27
- package/dist/acp.js +3 -1
- package/dist/cli/daemon.d.ts +14 -0
- package/dist/cli/daemon.js +60 -0
- package/dist/cli/doctor.js +9 -1
- package/dist/cli/init.js +1 -0
- package/dist/cli/kraftwerk.js +4 -0
- package/dist/cli/ui.d.ts +8 -0
- package/dist/cli/ui.js +35 -8
- package/dist/cli/workspaces.js +5 -3
- package/dist/client/index.d.ts +8 -0
- package/dist/client/index.js +2 -1
- package/dist/client/live.d.ts +1 -2
- package/dist/client/live.js +2 -1
- package/dist/client/routes.d.ts +3 -0
- package/dist/client/routes.js +3 -0
- package/dist/config.d.ts +16 -0
- package/dist/config.js +23 -1
- package/dist/core/chat/pi.js +2 -1
- package/dist/core/chat/sessions.d.ts +1 -1
- package/dist/core/chat/sessions.js +1 -1
- package/dist/core/chat/state.d.ts +2 -0
- package/dist/core/chat/state.js +5 -0
- package/dist/core/daemon.d.ts +27 -0
- package/dist/core/daemon.js +61 -0
- package/dist/core/env.d.ts +16 -0
- package/dist/core/env.js +45 -0
- package/dist/core/git.d.ts +2 -0
- package/dist/core/git.js +20 -4
- package/dist/core/instances.d.ts +34 -12
- package/dist/core/instances.js +142 -66
- package/dist/core/routines.d.ts +6 -0
- package/dist/core/routines.js +11 -0
- package/dist/core/runner.js +7 -3
- package/dist/core/search.d.ts +2 -0
- package/dist/core/search.js +1 -0
- package/dist/core/vibeables.d.ts +2 -0
- package/dist/core/vibeables.js +17 -5
- package/dist/core/workspace.d.ts +13 -0
- package/dist/core/workspace.js +25 -0
- package/dist/server/api/hub.d.ts +25 -0
- package/dist/server/api/hub.js +32 -0
- package/dist/server/api/index.d.ts +35 -3
- package/dist/server/api/index.js +2 -0
- package/dist/server/api/socket.d.ts +9 -1
- package/dist/server/api/socket.js +27 -7
- package/dist/server/api/workspace.d.ts +16 -2
- package/dist/server/api/workspace.js +44 -4
- package/dist/server/cloud.d.ts +5 -1
- package/dist/server/cloud.js +86 -54
- package/dist/server/hub-context.d.ts +7 -0
- package/dist/server/hub-context.js +18 -0
- package/dist/server/live.d.ts +2 -0
- package/dist/server/live.js +9 -0
- package/dist/server/preview.js +2 -1
- package/dist/server/server.d.ts +52 -1
- package/dist/server/server.js +283 -52
- package/dist/server/target.d.ts +20 -0
- package/dist/server/target.js +18 -0
- package/package.json +1 -1
- package/schema/kraftwerk.schema.json +5 -0
- package/web/dist/assets/{dist-10uPVFvu.js → dist-BMZBs9W4.js} +1 -1
- package/web/dist/assets/{dist-v6-49xpW.js → dist-BkZKygT6.js} +1 -1
- package/web/dist/assets/{dist-Bnxg6V5L.js → dist-CZjwFXw-.js} +1 -1
- package/web/dist/assets/{dist-DS1R2xKJ.js → dist-D7BYSDIr.js} +1 -1
- package/web/dist/assets/dist-DZLlNafq.js +1 -0
- package/web/dist/assets/{dist-BCfXUgtf.js → dist-D_nqqdyh.js} +1 -1
- package/web/dist/assets/{dist-nA7q2-h7.js → dist-IO8GeoYh.js} +1 -1
- package/web/dist/assets/{dist-hevbMnoj.js → dist-LUKHNpC7.js} +1 -1
- package/web/dist/assets/{dist-DnMgiPT1.js → dist-Y0KLvK_g.js} +1 -1
- package/web/dist/assets/{dist-C-CclD1E.js → dist-aMqhK50K.js} +1 -1
- package/web/dist/assets/{dist-CAWmDGJC.js → dist-jyZ9dxfk.js} +1 -1
- package/web/dist/assets/{editor-C8yy8YS9.js → editor-DQouCIUq.js} +3 -3
- package/web/dist/assets/index-D57IrV_V.js +229 -0
- package/web/dist/index.html +1 -1
- package/web/dist/assets/dist-yoh47aAL.js +0 -1
- package/web/dist/assets/index-DWS1u71Y.js +0 -229
package/README.md
CHANGED
|
@@ -7,16 +7,18 @@ happened to set them up, and every result is checked before anyone relies on
|
|
|
7
7
|
it.
|
|
8
8
|
|
|
9
9
|
```bash
|
|
10
|
-
npm install -g @netnodeag/kraftwerk
|
|
10
|
+
npm install -g @netnodeag/kraftwerk # once per machine
|
|
11
11
|
|
|
12
|
-
cd your-project
|
|
13
|
-
kraftwerk init #
|
|
14
|
-
kraftwerk doctor # check
|
|
15
|
-
kraftwerk ui #
|
|
12
|
+
cd your-project # any folder: a repo, a team folder, an empty one
|
|
13
|
+
kraftwerk init # make it a workspace: kraftwerk.yml + kraftwerk-data/
|
|
14
|
+
kraftwerk doctor # check that the agents (Claude Code, Codex, Pi) are there
|
|
15
|
+
kraftwerk ui # start it, then open http://localhost:1981
|
|
16
16
|
```
|
|
17
17
|
|
|
18
18
|
Needs Node 20+ and at least one agent harness (Claude Code, Codex, or Pi).
|
|
19
|
-
|
|
19
|
+
[Start and use kraftwerk](#start-and-use-kraftwerk) explains the ways to
|
|
20
|
+
run it (one workspace, several at once, from your phone);
|
|
21
|
+
[Install](#install) has the details.
|
|
20
22
|
|
|
21
23
|
**Why teams need it.** AI work today lives in individual silos. Personal
|
|
22
24
|
prompts, Claude Code sessions, one-off scripts. What one person figures out
|
|
@@ -48,6 +50,73 @@ becomes shared practice.
|
|
|
48
50
|
This README is the reference for the framework and CLI. For the overview,
|
|
49
51
|
start at the [project README](https://github.com/NETNODEAG/kraftwerk#readme).
|
|
50
52
|
|
|
53
|
+
## Start and use kraftwerk
|
|
54
|
+
|
|
55
|
+
A **workspace** is a folder with a `kraftwerk.yml`: its agents, knowledge,
|
|
56
|
+
workflows, projects and chats live in it (under `kraftwerk-data/`), and git
|
|
57
|
+
can share it with your team. `kraftwerk init` makes any folder one.
|
|
58
|
+
kraftwerk is then used in the browser, the **inspector**, which a kraftwerk
|
|
59
|
+
process serves from your machine. There are three ways to run that process.
|
|
60
|
+
|
|
61
|
+
**One workspace: `kraftwerk ui`.** In the workspace folder, `kraftwerk ui`
|
|
62
|
+
starts a server for it and prints the address, `http://localhost:1981`
|
|
63
|
+
(`port:` in kraftwerk.yml, or `--port`, sets another). It runs in the
|
|
64
|
+
foreground; Ctrl-C stops it. That is all most people need.
|
|
65
|
+
|
|
66
|
+
**Several workspaces: `kraftwerk daemon`.** With more than one workspace,
|
|
67
|
+
run one kraftwerk for all of them instead of a server and a port each.
|
|
68
|
+
Start the daemon once and leave it running (a terminal tab, or a login
|
|
69
|
+
item):
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
kraftwerk daemon # the machine's kraftwerk, on http://localhost:1980
|
|
73
|
+
|
|
74
|
+
cd ~/work/team-blau && kraftwerk ui # opens this workspace in the daemon:
|
|
75
|
+
# http://team-blau.localhost:1980
|
|
76
|
+
cd ~/work/relaunch && kraftwerk ui # http://relaunch.localhost:1980
|
|
77
|
+
kraftwerk workspaces # every workspace on this machine, running or not
|
|
78
|
+
kraftwerk workspaces stop team-blau # close one; the daemon and the others keep running
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Each workspace is addressed by its **slug**: the folder name, or `slug:` in
|
|
82
|
+
kraftwerk.yml (the `name:` is only the title and can change freely).
|
|
83
|
+
Browsers resolve `<slug>.localhost` to your machine by themselves, so there
|
|
84
|
+
is nothing to set up, and each workspace keeps its own browser origin.
|
|
85
|
+
Where a host name per workspace is not possible, `http://localhost:1980/w/<slug>/`
|
|
86
|
+
reaches the same workspace. `http://localhost:1980` lists what is open.
|
|
87
|
+
A workspace with a `port:` of its own is served there too, so old links
|
|
88
|
+
keep working. The daemon remembers what was open and opens it again when it
|
|
89
|
+
starts (after a reboot or an update). Each workspace still runs on its own
|
|
90
|
+
inside it, with its own `.env`, routines, git sync and chats. The
|
|
91
|
+
workspace menu in the header switches between them, and starts a stopped
|
|
92
|
+
one right in the daemon. The Kraftwerk desktop app starts the daemon for
|
|
93
|
+
you when it opens.
|
|
94
|
+
`kraftwerk ui` hands the workspace to the daemon whenever one is running;
|
|
95
|
+
`kraftwerk ui --standalone` starts a server of its own anyway.
|
|
96
|
+
|
|
97
|
+
**From your phone or another computer.** Add `--lan` (`kraftwerk ui --lan`,
|
|
98
|
+
or `kraftwerk daemon --lan`) and kraftwerk also listens on your network.
|
|
99
|
+
Anyone who opens it there gets a pairing screen, nothing else, until you
|
|
100
|
+
pair that device: `kraftwerk devices pair` on this machine prints a
|
|
101
|
+
one-time code. A paired device reaches the daemon's workspaces at
|
|
102
|
+
`/w/<slug>/` on the address it used, and the workspace menu links them
|
|
103
|
+
that way (see [Inspector on your phone](#inspector-on-your-phone-paired-devices)).
|
|
104
|
+
|
|
105
|
+
**What to do first in the inspector.** Talk to **Ralv**, the workspace's
|
|
106
|
+
general chat, in plain words: it knows what is in the workspace and helps
|
|
107
|
+
set up the rest. From there:
|
|
108
|
+
|
|
109
|
+
- **Agents** are persistent coworkers with a role, a memory and routines.
|
|
110
|
+
Open one and chat with it; its sessions continue where they ended.
|
|
111
|
+
- **Projects** gather a goal, a brief and links; every chat in a project
|
|
112
|
+
starts with that context.
|
|
113
|
+
- **Workflows** are repeatable processes with checked results; run one from
|
|
114
|
+
its page or with `kraftwerk run <workflow> "<request>"`.
|
|
115
|
+
- **Knowledge** is the shared, human-verified context agents read.
|
|
116
|
+
|
|
117
|
+
Everything you see in the inspector is files in the workspace folder, so
|
|
118
|
+
the CLI (`kraftwerk --help`) and git work on the same things.
|
|
119
|
+
|
|
51
120
|
## Under the hood
|
|
52
121
|
|
|
53
122
|
Deterministic workflow-as-code over headless agent harnesses, in the spirit of
|
|
@@ -112,7 +181,7 @@ cd your-project
|
|
|
112
181
|
kraftwerk init # scaffold kraftwerk.yml + kraftwerk-data/ (workflow, agent, knowledge)
|
|
113
182
|
kraftwerk doctor # preflight: harness CLIs, docker, workflows, declared env vars, legacy leftovers
|
|
114
183
|
kraftwerk run hello "Was ist kraftwerk?"
|
|
115
|
-
kraftwerk ui # inspector on http://localhost:1981
|
|
184
|
+
kraftwerk ui # inspector on http://localhost:1981 (see Start and use kraftwerk)
|
|
116
185
|
```
|
|
117
186
|
|
|
118
187
|
The same without a global install:
|
|
@@ -159,8 +228,10 @@ kraftwerk run tagline "https://..." # run; --yes, --verbose
|
|
|
159
228
|
kraftwerk run # interactive: pick workflow, type the request
|
|
160
229
|
kraftwerk runs # past runs from output/*/trace.jsonl; runs show <id> for detail
|
|
161
230
|
kraftwerk knowledge # Knowledge: OKF bundles (list/get/put/verify/search/...)
|
|
162
|
-
kraftwerk ui #
|
|
231
|
+
kraftwerk ui # start this workspace: http://localhost:1981 (or open it in the daemon); --port, --lan, --standalone
|
|
232
|
+
kraftwerk daemon # one kraftwerk for every open workspace: http://<slug>.localhost:1980; --port, --lan
|
|
163
233
|
kraftwerk workspaces # every workspace on this machine; workspaces start|stop|forget <ref>
|
|
234
|
+
kraftwerk devices # paired devices (a phone, another computer); devices pair|revoke
|
|
164
235
|
kraftwerk projects # goal-scoped project folders; projects create|show|link|log|remove
|
|
165
236
|
kraftwerk trash # what was deleted; trash restore|purge <id>, trash empty
|
|
166
237
|
kraftwerk journal <agent> ["<entry>"] # an agent's memory across sessions: print it, add a line (--kind)
|
|
@@ -182,19 +253,18 @@ The **trash** screen (`#/trash`) or `kraftwerk trash restore <id>` puts an entry
|
|
|
182
253
|
back where it came from. Deleting it there, or `kraftwerk trash empty`, makes it
|
|
183
254
|
final. A chat's agent transcripts are only removed then, so a restored chat can resume.
|
|
184
255
|
|
|
185
|
-
The inspector binds `127.0.0.1
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
127.0.0.1). Inside a
|
|
190
|
-
|
|
191
|
-
localhost, and its traefik
|
|
192
|
-
|
|
193
|
-
`X-Forwarded-Host` to the host
|
|
194
|
-
by default; nginx needs
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
different host.
|
|
256
|
+
The inspector binds `127.0.0.1`: its chat runs coding agents against the
|
|
257
|
+
repo, so it stays off the network unless you ask (`--lan`), and there only
|
|
258
|
+
paired devices get in. While bound to loopback it only answers requests
|
|
259
|
+
whose `Host` is a loopback name or `<slug>.localhost` (so a page on another
|
|
260
|
+
site cannot reach it by re-pointing its own DNS at 127.0.0.1). Inside a
|
|
261
|
+
container it binds all interfaces instead, because the port mapping is the
|
|
262
|
+
boundary there (`deploy-starter/` publishes to localhost, and its traefik
|
|
263
|
+
override adds basic-auth). `KRAFTWERK_UI_HOST` overrides the default either
|
|
264
|
+
way. A reverse proxy in front of it must set `X-Forwarded-Host` to the host
|
|
265
|
+
the browser addressed (Caddy and traefik do by default; nginx needs
|
|
266
|
+
`proxy_set_header X-Forwarded-Host $host;`), and its visitors pair like any
|
|
267
|
+
other device.
|
|
198
268
|
|
|
199
269
|
### The kraftwerk.yml project config
|
|
200
270
|
|
|
@@ -203,10 +273,15 @@ fields are optional. `workflows:` sets the workflows root, `output:` the
|
|
|
203
273
|
run-artifact directory (default `output/`), `knowledge:` the OKF bundle root
|
|
204
274
|
(default `knowledge/`), and `agents:` the agent-definition root (default
|
|
205
275
|
`agents/`). `repos:` turns on the [repositories](#repositories) folder.
|
|
206
|
-
`cloud:`
|
|
207
|
-
tunes (or turns off) the registration with the
|
|
276
|
+
`cloud:` tunes (or turns off) the registration with the
|
|
208
277
|
[kraftwerk cloud](#inspector-in-the-kraftwerk-cloud), which is on by default.
|
|
209
278
|
|
|
279
|
+
`name:` is the title shown in the UI and can change freely. `slug:` is the
|
|
280
|
+
workspace's address on this machine. It defaults to the folder name
|
|
281
|
+
(`~/Sites/team-blau` becomes `team-blau`). It must be lowercase letters,
|
|
282
|
+
digits and dashes, and unique among the workspaces on the machine.
|
|
283
|
+
`kraftwerk ui` and `kraftwerk doctor` warn when two workspaces share one.
|
|
284
|
+
|
|
210
285
|
A `.env` next to kraftwerk.yml (`KEY=value` lines, `#` comments, quotes)
|
|
211
286
|
is loaded into every kraftwerk process at start: `kraftwerk ui` and the
|
|
212
287
|
server it supervises, so a restart from the UI re-reads the file; the chat
|
|
@@ -216,8 +291,14 @@ workspace's inspector gets its own `.env`, not the other one's. The file is
|
|
|
216
291
|
never synced (it is on the workspace git's deny list and in the .gitignore
|
|
217
292
|
`kraftwerk init` writes); `kraftwerk doctor` lists the names it loaded.
|
|
218
293
|
|
|
219
|
-
|
|
220
|
-
workspace
|
|
294
|
+
The workspace name in the inspector header is the **workspace menu**. It
|
|
295
|
+
lists every workspace this machine knows. Running ones open with a click,
|
|
296
|
+
and a stopped one starts from its row (in the daemon, when one runs). Each
|
|
297
|
+
link uses the address you opened this page with: `<slug>.localhost` on this
|
|
298
|
+
machine, `/w/<slug>/` from a paired device on the network, the path form
|
|
299
|
+
when you are in it.
|
|
300
|
+
`switcher:` adds workspaces kraftwerk cannot discover, such as one on
|
|
301
|
+
another machine:
|
|
221
302
|
|
|
222
303
|
```yaml
|
|
223
304
|
switcher:
|
|
@@ -301,8 +382,12 @@ follows to author the workflow folder and validate and smoke it with this CLI.
|
|
|
301
382
|
|
|
302
383
|
### Inspector on your phone (paired devices)
|
|
303
384
|
|
|
304
|
-
`kraftwerk ui --lan` also listens on your
|
|
305
|
-
(for example `http://192.168.1.20:1981`).
|
|
385
|
+
`kraftwerk ui --lan` (or `kraftwerk daemon --lan`) also listens on your
|
|
386
|
+
network and prints the addresses (for example `http://192.168.1.20:1981`).
|
|
387
|
+
On the daemon, a device opens a workspace at `/w/<slug>/` on that address
|
|
388
|
+
(`http://192.168.1.20:1980/w/team-blau/`), because `<slug>.localhost` only
|
|
389
|
+
works on this machine. The daemon's own address lists them, and the
|
|
390
|
+
workspace menu links the others the same way. Anyone who opens one gets a
|
|
306
391
|
pairing screen, nothing else. To let a device in, make a one-time code on
|
|
307
392
|
the machine kraftwerk runs on, with `kraftwerk devices pair` or settings →
|
|
308
393
|
devices → pair a device, and enter it on the device. The code works once,
|
package/dist/acp.js
CHANGED
|
@@ -2,6 +2,7 @@ import { spawn } from "node:child_process";
|
|
|
2
2
|
import { Readable, Writable } from "node:stream";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
4
|
import { ClientSideConnection, ndJsonStream, PROTOCOL_VERSION, } from "@agentclientprotocol/sdk";
|
|
5
|
+
import { workspaceEnv } from "./core/env.js";
|
|
5
6
|
const ADAPTERS = {
|
|
6
7
|
claude: "@agentclientprotocol/claude-agent-acp/dist/index.js",
|
|
7
8
|
codex: "@agentclientprotocol/codex-acp/dist/index.js",
|
|
@@ -21,7 +22,8 @@ const CLAUDE_THINKING_BUDGET = {
|
|
|
21
22
|
* the session config it hands to `codex app-server`.
|
|
22
23
|
*/
|
|
23
24
|
export function adapterEnv(agent, tuning) {
|
|
24
|
-
|
|
25
|
+
// The workspace's environment when a server runs this (one per workspace, see core/env.ts); a CLI run has none and gets process.env.
|
|
26
|
+
const env = workspaceEnv();
|
|
25
27
|
if (agent === "claude" && tuning.effort && CLAUDE_THINKING_BUDGET[tuning.effort]) {
|
|
26
28
|
env.MAX_THINKING_TOKENS = String(CLAUDE_THINKING_BUDGET[tuning.effort]);
|
|
27
29
|
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { Command } from "commander";
|
|
2
|
+
/**
|
|
3
|
+
* `kraftwerk daemon` — the machine's kraftwerk: one process serving every
|
|
4
|
+
* open workspace, each at `<slug>.localhost:<port>` (and its own port, when
|
|
5
|
+
* free). `kraftwerk ui` in a workspace folder opens it there; `kraftwerk
|
|
6
|
+
* workspaces stop <root>` closes it; the daemon reopens what was open when it
|
|
7
|
+
* starts again. Runs in the foreground, restarted by a supervisor after a
|
|
8
|
+
* self-update like `kraftwerk ui`.
|
|
9
|
+
*/
|
|
10
|
+
export declare function runDaemon(opts: {
|
|
11
|
+
port?: string;
|
|
12
|
+
lan?: boolean;
|
|
13
|
+
}): Promise<void>;
|
|
14
|
+
export declare function registerDaemonCommand(program: Command): void;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import { DAEMON_PORT, findDaemon, lastOpenRoots, writeDaemonFile } from "../core/daemon.js";
|
|
3
|
+
import { tildify } from "../core/instances.js";
|
|
4
|
+
import { networkUrls } from "../server/bind.js";
|
|
5
|
+
import { startHub } from "../server/server.js";
|
|
6
|
+
import { ensureBuilt, supervise } from "./ui.js";
|
|
7
|
+
/**
|
|
8
|
+
* `kraftwerk daemon` — the machine's kraftwerk: one process serving every
|
|
9
|
+
* open workspace, each at `<slug>.localhost:<port>` (and its own port, when
|
|
10
|
+
* free). `kraftwerk ui` in a workspace folder opens it there; `kraftwerk
|
|
11
|
+
* workspaces stop <root>` closes it; the daemon reopens what was open when it
|
|
12
|
+
* starts again. Runs in the foreground, restarted by a supervisor after a
|
|
13
|
+
* self-update like `kraftwerk ui`.
|
|
14
|
+
*/
|
|
15
|
+
export async function runDaemon(opts) {
|
|
16
|
+
if (opts.lan)
|
|
17
|
+
process.env.KRAFTWERK_UI_HOST ||= "0.0.0.0";
|
|
18
|
+
if (process.env.KRAFTWERK_UI_SUPERVISED !== "1")
|
|
19
|
+
return supervise(["daemon", ...(opts.port ? ["--port", opts.port] : [])], "the daemon");
|
|
20
|
+
const running = await findDaemon();
|
|
21
|
+
if (running && running.pid !== process.pid) {
|
|
22
|
+
console.error(chalk.red(`✖ a kraftwerk daemon already runs (pid ${running.pid}, port ${running.port})`));
|
|
23
|
+
process.exit(1);
|
|
24
|
+
}
|
|
25
|
+
const port = Number(opts.port ?? process.env.KRAFTWERK_DAEMON_PORT ?? DAEMON_PORT);
|
|
26
|
+
const reopen = await lastOpenRoots();
|
|
27
|
+
const hub = await startHub({
|
|
28
|
+
port,
|
|
29
|
+
staticDir: ensureBuilt(),
|
|
30
|
+
daemon: true,
|
|
31
|
+
onChange: (h) => writeDaemonFile(h.port, h.list().map((o) => o.root)),
|
|
32
|
+
});
|
|
33
|
+
await writeDaemonFile(hub.port, []);
|
|
34
|
+
for (const root of reopen) {
|
|
35
|
+
try {
|
|
36
|
+
await hub.open(root);
|
|
37
|
+
}
|
|
38
|
+
catch (err) {
|
|
39
|
+
console.log(`${chalk.yellow("⚠")} not reopened: ${tildify(root)} — ${err.message}`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
console.log(`${chalk.green("✔")} kraftwerk daemon on ${chalk.cyan(`http://localhost:${hub.port}`)} ${chalk.dim(`(pid ${process.pid})`)}`);
|
|
43
|
+
for (const o of hub.list())
|
|
44
|
+
console.log(` ${chalk.cyan(o.url)} ${chalk.dim(o.name)}${o.aliasPort ? chalk.dim(` · also :${o.aliasPort}`) : ""}`);
|
|
45
|
+
if (!hub.list().length)
|
|
46
|
+
console.log(chalk.dim(" no workspace open yet — `kraftwerk ui` in a workspace folder opens it here"));
|
|
47
|
+
const lan = networkUrls(hub.port);
|
|
48
|
+
if (lan.length)
|
|
49
|
+
console.log(`${chalk.green("✔")} On your network: ${lan.map((u) => chalk.cyan(u)).join(", ")} ${chalk.dim("(add /w/<slug>/; devices pair first)")}`);
|
|
50
|
+
}
|
|
51
|
+
export function registerDaemonCommand(program) {
|
|
52
|
+
program
|
|
53
|
+
.command("daemon")
|
|
54
|
+
.description("Run the machine's kraftwerk: one process serving every open workspace at <slug>.localhost")
|
|
55
|
+
.option("--port <port>", `Port (default: KRAFTWERK_DAEMON_PORT, else ${DAEMON_PORT})`)
|
|
56
|
+
.option("--lan", "Listen on the network too, for paired devices")
|
|
57
|
+
.action(async (opts) => {
|
|
58
|
+
await runDaemon(opts);
|
|
59
|
+
});
|
|
60
|
+
}
|
package/dist/cli/doctor.js
CHANGED
|
@@ -2,12 +2,13 @@ import { spawnSync } from "node:child_process";
|
|
|
2
2
|
import { readFile } from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import chalk from "chalk";
|
|
5
|
-
import { cloudFor, ignoreEntryFor, isDir, reposRootFor, resolveWorkspace } from "../config.js";
|
|
5
|
+
import { cloudFor, ignoreEntryFor, isDir, reposRootFor, resolveWorkspace, workspaceSlug } from "../config.js";
|
|
6
6
|
import { discoverWorkflows } from "../discover.js";
|
|
7
7
|
import { missingEnv } from "../yaml.js";
|
|
8
8
|
import { applyDotenv, DOTENV_FILE } from "../dotenv.js";
|
|
9
9
|
import { legacyFindings, versionLt } from "./doctor-legacy.js";
|
|
10
10
|
import { listDevices } from "../core/devices.js";
|
|
11
|
+
import { slugClashes } from "../core/instances.js";
|
|
11
12
|
const ICONS = {
|
|
12
13
|
ok: chalk.green("✔"),
|
|
13
14
|
warn: chalk.yellow("⚠"),
|
|
@@ -72,6 +73,13 @@ export async function runDoctor(cwd) {
|
|
|
72
73
|
process.exit(1);
|
|
73
74
|
}
|
|
74
75
|
report("info", `Project root: ${project.root}`, project.configPath ? path.basename(project.configPath) : "no kraftwerk.yml (fallback: workflows folder or .git)");
|
|
76
|
+
// The slug is the workspace's address on this machine; two workspaces cannot share one.
|
|
77
|
+
const slug = workspaceSlug(project);
|
|
78
|
+
const clash = await slugClashes(slug, project.root);
|
|
79
|
+
if (clash.length)
|
|
80
|
+
report("warn", `slug: ${slug}`, `also used by ${clash.map((r) => r.root).join(", ")} — set \`slug:\` in one kraftwerk.yml`);
|
|
81
|
+
else
|
|
82
|
+
report("ok", `slug: ${slug}`, project.config.slug ? "from kraftwerk.yml" : "the folder name — `slug:` in kraftwerk.yml sets another");
|
|
75
83
|
// Project config: well-formed (guaranteed by the parse above) and the
|
|
76
84
|
// configured paths actually exist.
|
|
77
85
|
if (!project.configPath) {
|
package/dist/cli/init.js
CHANGED
|
@@ -16,6 +16,7 @@ const configTemplate = (name) => `# yaml-language-server: $schema=${CONFIG_SCHEM
|
|
|
16
16
|
# kraftwerk project config — also marks the project root for the CLI.
|
|
17
17
|
# All fields optional. Docs: https://github.com/NETNODEAG/kraftwerk
|
|
18
18
|
name: ${JSON.stringify(name)} # display name, shown in the inspector header + browser tab
|
|
19
|
+
# slug: my-team # this workspace's address on this machine (my-team.localhost); default: the folder name
|
|
19
20
|
icon: "⚡" # emoji shown as the inspector favicon
|
|
20
21
|
port: 1981 # port \`kraftwerk ui\` listens on
|
|
21
22
|
workflows: ${DATA_DIR}/workflows # where workflows live
|
package/dist/cli/kraftwerk.js
CHANGED
|
@@ -18,6 +18,7 @@ import { registerRepoCommands } from "./repos.js";
|
|
|
18
18
|
import { registerVibeableCommands } from "./vibeables.js";
|
|
19
19
|
import { registerTrashCommands } from "./trash.js";
|
|
20
20
|
import { registerDevicesCommands } from "./devices.js";
|
|
21
|
+
import { registerDaemonCommand } from "./daemon.js";
|
|
21
22
|
import { registerJournalCommands } from "./journal.js";
|
|
22
23
|
import { applyDotenv } from "../dotenv.js";
|
|
23
24
|
import { resolveWorkspace } from "../config.js";
|
|
@@ -41,6 +42,7 @@ import { runUi } from "./ui.js";
|
|
|
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
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
|
*
|
|
@@ -292,6 +294,7 @@ registerRepoCommands(program);
|
|
|
292
294
|
registerVibeableCommands(program);
|
|
293
295
|
registerTrashCommands(program);
|
|
294
296
|
registerDevicesCommands(program);
|
|
297
|
+
registerDaemonCommand(program);
|
|
295
298
|
registerJournalCommands(program);
|
|
296
299
|
program
|
|
297
300
|
.command("ui")
|
|
@@ -299,6 +302,7 @@ program
|
|
|
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)")
|
|
301
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")
|
|
302
306
|
.action(async (opts) => {
|
|
303
307
|
await runUi(process.cwd(), opts);
|
|
304
308
|
});
|
package/dist/cli/ui.d.ts
CHANGED
|
@@ -1,5 +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;
|
|
4
9
|
lan?: boolean;
|
|
10
|
+
standalone?: boolean;
|
|
5
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,7 +4,9 @@ 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, resolveWorkspace } from "../config.js";
|
|
7
|
+
import { absolutePath, resolveWorkspace, workspaceSlug } from "../config.js";
|
|
8
|
+
import { slugClashes } from "../core/instances.js";
|
|
9
|
+
import { daemonCall, findDaemon } from "../core/daemon.js";
|
|
8
10
|
import { selfCommand } from "../core/self-command.js";
|
|
9
11
|
import { RESTART_EXIT_CODE, startInspector } from "../server/server.js";
|
|
10
12
|
import { networkUrls } from "../server/bind.js";
|
|
@@ -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,6 +54,23 @@ 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
|
+
}
|
|
52
74
|
// The server reads its bind address when it starts — in the supervised child, which inherits this.
|
|
53
75
|
if (opts.lan)
|
|
54
76
|
process.env.KRAFTWERK_UI_HOST ||= "0.0.0.0";
|
|
@@ -60,6 +82,11 @@ export async function runUi(cwd, opts) {
|
|
|
60
82
|
// Port precedence: --port flag > kraftwerk.yml `port` > 1981.
|
|
61
83
|
const port = opts.port ? Number(opts.port) : (project.config.port ?? 1981);
|
|
62
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`);
|
|
63
90
|
console.log(`${chalk.green("✔")} Kraftwerk UI: ${chalk.cyan(`http://localhost:${port}`)} ` +
|
|
64
91
|
chalk.dim(`(output: ${outputDir})`));
|
|
65
92
|
const lan = networkUrls(port);
|
|
@@ -78,11 +105,11 @@ export async function runUi(cwd, opts) {
|
|
|
78
105
|
* foreground group and reaches both anyway.
|
|
79
106
|
*/
|
|
80
107
|
async function superviseUi(opts) {
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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);
|
|
86
113
|
for (;;) {
|
|
87
114
|
const child = spawn(cmd, args, {
|
|
88
115
|
stdio: "inherit",
|
|
@@ -104,6 +131,6 @@ async function superviseUi(opts) {
|
|
|
104
131
|
process.exit(128 + (os.constants.signals[result.signal] ?? 1));
|
|
105
132
|
process.exit(result.code ?? 1);
|
|
106
133
|
}
|
|
107
|
-
console.log(chalk.dim(
|
|
134
|
+
console.log(chalk.dim(`↻ relaunching ${what} with the current install ...`));
|
|
108
135
|
}
|
|
109
136
|
}
|
package/dist/cli/workspaces.js
CHANGED
|
@@ -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
|
|
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) {
|
package/dist/client/index.d.ts
CHANGED
|
@@ -63,6 +63,12 @@ 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;
|
|
67
73
|
/**
|
|
68
74
|
* A paired device's token (from `devices.pair`), sent as `Authorization:
|
|
@@ -81,5 +87,7 @@ export interface Client {
|
|
|
81
87
|
onChange(fn: () => void): () => void;
|
|
82
88
|
/** The token the client was created with, if any (the socket sends it too). */
|
|
83
89
|
readonly token?: string;
|
|
90
|
+
/** Where routes live: baseUrl plus the workspace's `/w/<slug>` prefix. */
|
|
91
|
+
readonly baseUrl: string;
|
|
84
92
|
}
|
|
85
93
|
export declare function createClient(opts?: ClientOptions): Client;
|
package/dist/client/index.js
CHANGED
|
@@ -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 = {}) => {
|
|
@@ -66,6 +66,7 @@ export function createClient(opts = {}) {
|
|
|
66
66
|
};
|
|
67
67
|
return {
|
|
68
68
|
token: opts.token,
|
|
69
|
+
baseUrl: base,
|
|
69
70
|
onChange(fn) {
|
|
70
71
|
changeFns.add(fn);
|
|
71
72
|
return () => changeFns.delete(fn);
|
package/dist/client/live.d.ts
CHANGED
|
@@ -25,9 +25,8 @@ export interface Live {
|
|
|
25
25
|
close(): void;
|
|
26
26
|
}
|
|
27
27
|
export interface LiveOptions {
|
|
28
|
-
/** The HTTP client
|
|
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
|
}
|
package/dist/client/live.js
CHANGED
|
@@ -12,7 +12,8 @@ export function createLive(opts) {
|
|
|
12
12
|
let retry;
|
|
13
13
|
let counter = 0;
|
|
14
14
|
const socketUrl = () => {
|
|
15
|
-
|
|
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;
|
|
16
17
|
// A browser's socket carries the device cookie; other runtimes cannot send headers on a WebSocket, so the token rides in the query.
|
|
17
18
|
return `${base.replace(/^http/, "ws").replace(/\/+$/, "")}/api/ws${client.token ? `?token=${encodeURIComponent(client.token)}` : ""}`;
|
|
18
19
|
};
|
package/dist/client/routes.d.ts
CHANGED
|
@@ -62,6 +62,9 @@ export declare const ROUTES: {
|
|
|
62
62
|
readonly "git.pull": readonly ["POST", "/api/git/pull"];
|
|
63
63
|
readonly "git.push": readonly ["POST", "/api/git/push"];
|
|
64
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"];
|
|
65
68
|
readonly "knowledge.concept": readonly ["GET", "/api/knowledge/:bundle/concept"];
|
|
66
69
|
readonly "knowledge.create": readonly ["POST", "/api/knowledge"];
|
|
67
70
|
readonly "knowledge.delete": readonly ["DELETE", "/api/knowledge/:bundle"];
|
package/dist/client/routes.js
CHANGED
|
@@ -63,6 +63,9 @@ export const ROUTES = {
|
|
|
63
63
|
"git.pull": ["POST", "/api/git/pull"],
|
|
64
64
|
"git.push": ["POST", "/api/git/push"],
|
|
65
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"],
|
|
66
69
|
"knowledge.concept": ["GET", "/api/knowledge/:bundle/concept"],
|
|
67
70
|
"knowledge.create": ["POST", "/api/knowledge"],
|
|
68
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
|
|
@@ -166,6 +167,12 @@ export interface SwitcherEntry {
|
|
|
166
167
|
export interface WorkspaceConfig {
|
|
167
168
|
/** Display name of the project ("environment"), shown in the inspector header. */
|
|
168
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;
|
|
169
176
|
/** Emoji used as the inspector favicon (browser-tab icon). */
|
|
170
177
|
icon?: string;
|
|
171
178
|
/** Accent colour (#rgb / #rrggbb) for this workspace in the switcher. Default: derived from the root path. */
|
|
@@ -234,3 +241,12 @@ export declare const resolveProject: typeof resolveWorkspace;
|
|
|
234
241
|
export type Project = ResolvedWorkspace;
|
|
235
242
|
/** @deprecated Use `WorkspaceConfig`. */
|
|
236
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;
|