@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.
- package/README.md +126 -138
- package/dist/acp.js +3 -1
- package/dist/cli/daemon.d.ts +14 -0
- package/dist/cli/daemon.js +60 -0
- package/dist/cli/devices.d.ts +14 -0
- package/dist/cli/devices.js +53 -0
- package/dist/cli/doctor-legacy.js +9 -1
- package/dist/cli/doctor.js +17 -30
- package/dist/cli/init.js +1 -6
- package/dist/cli/kraftwerk.js +8 -3
- package/dist/cli/ui.d.ts +9 -0
- package/dist/cli/ui.js +47 -23
- package/dist/cli/workspaces.js +5 -3
- package/dist/client/index.d.ts +17 -0
- package/dist/client/index.js +10 -2
- package/dist/client/live.d.ts +1 -2
- package/dist/client/live.js +4 -2
- package/dist/client/routes.d.ts +8 -0
- package/dist/client/routes.js +8 -0
- package/dist/config.d.ts +18 -55
- package/dist/config.js +39 -92
- package/dist/core/chat/context.js +1 -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/devices.d.ts +35 -0
- package/dist/core/devices.js +156 -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 +32 -12
- package/dist/core/instances.js +136 -64
- 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/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/dotenv.d.ts +2 -2
- package/dist/dotenv.js +2 -2
- package/dist/server/api/devices.d.ts +20 -0
- package/dist/server/api/devices.js +55 -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 +42 -2
- package/dist/server/api/index.js +5 -1
- package/dist/server/api/router.d.ts +21 -6
- package/dist/server/api/router.js +37 -9
- package/dist/server/api/socket.d.ts +15 -1
- package/dist/server/api/socket.js +59 -8
- package/dist/server/api/workspace.d.ts +2 -0
- package/dist/server/api/workspace.js +4 -2
- package/dist/server/bind.d.ts +16 -0
- package/dist/server/bind.js +25 -0
- package/dist/server/cloud.d.ts +5 -1
- package/dist/server/cloud.js +89 -56
- 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 +58 -1
- package/dist/server/server.js +267 -103
- package/dist/server/target.d.ts +20 -0
- package/dist/server/target.js +18 -0
- package/dist/server/trust.d.ts +41 -0
- package/dist/server/trust.js +42 -0
- package/package.json +1 -1
- package/schema/kraftwerk.schema.json +7 -46
- package/web/dist/assets/{dist-Cgcwvf4j.js → dist-1bNkaWWA.js} +1 -1
- package/web/dist/assets/dist-BU1SOB5S.js +1 -0
- package/web/dist/assets/{dist-CboP0-T1.js → dist-Bk3f-J5U.js} +1 -1
- package/web/dist/assets/{dist-2kswkIH2.js → dist-Bx99Puvv.js} +1 -1
- package/web/dist/assets/{dist-D2PvEzo7.js → dist-C0M2aITo.js} +1 -1
- package/web/dist/assets/{dist-D_BcTzz-.js → dist-C0z19Zge.js} +1 -1
- package/web/dist/assets/{dist-36MjITWa.js → dist-COEgBKG6.js} +1 -1
- package/web/dist/assets/{dist-Jdx6DRrm.js → dist-CYRrdlae.js} +1 -1
- package/web/dist/assets/{dist-DDkd_zrP.js → dist-CbEFnSCD.js} +1 -1
- package/web/dist/assets/{dist-BsuA2pgO.js → dist-DCYVKU-H.js} +1 -1
- package/web/dist/assets/{dist-B8gPjMPP.js → dist-XyTHWtoN.js} +1 -1
- package/web/dist/assets/{editor-ByRUOfrX.js → editor-DPcGvSPy.js} +3 -3
- package/web/dist/assets/index-BAYs-2WC.css +2 -0
- package/web/dist/assets/index-DKLJdwYE.js +229 -0
- package/web/dist/index.html +2 -2
- package/dist/cli/tunnel.d.ts +0 -10
- package/dist/cli/tunnel.js +0 -280
- package/dist/server/access.d.ts +0 -32
- package/dist/server/access.js +0 -121
- package/web/dist/assets/dist-CVqmJrYd.js +0 -1
- package/web/dist/assets/index-BR7HvDzD.js +0 -229
- package/web/dist/assets/index-Bra74Nxe.css +0 -2
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,68 @@ 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.
|
|
91
|
+
`kraftwerk ui` hands the workspace to the daemon whenever one is running;
|
|
92
|
+
`kraftwerk ui --standalone` starts a server of its own anyway.
|
|
93
|
+
|
|
94
|
+
**From your phone or another computer.** Add `--lan` (`kraftwerk ui --lan`,
|
|
95
|
+
or `kraftwerk daemon --lan`) and kraftwerk also listens on your network.
|
|
96
|
+
Anyone who opens it there gets a pairing screen, nothing else, until you
|
|
97
|
+
pair that device: `kraftwerk devices pair` on this machine prints a
|
|
98
|
+
one-time code (see [Inspector on your phone](#inspector-on-your-phone-paired-devices)).
|
|
99
|
+
|
|
100
|
+
**What to do first in the inspector.** Talk to **Ralv**, the workspace's
|
|
101
|
+
general chat, in plain words: it knows what is in the workspace and helps
|
|
102
|
+
set up the rest. From there:
|
|
103
|
+
|
|
104
|
+
- **Agents** are persistent coworkers with a role, a memory and routines.
|
|
105
|
+
Open one and chat with it; its sessions continue where they ended.
|
|
106
|
+
- **Projects** gather a goal, a brief and links; every chat in a project
|
|
107
|
+
starts with that context.
|
|
108
|
+
- **Workflows** are repeatable processes with checked results; run one from
|
|
109
|
+
its page or with `kraftwerk run <workflow> "<request>"`.
|
|
110
|
+
- **Knowledge** is the shared, human-verified context agents read.
|
|
111
|
+
|
|
112
|
+
Everything you see in the inspector is files in the workspace folder, so
|
|
113
|
+
the CLI (`kraftwerk --help`) and git work on the same things.
|
|
114
|
+
|
|
51
115
|
## Under the hood
|
|
52
116
|
|
|
53
117
|
Deterministic workflow-as-code over headless agent harnesses, in the spirit of
|
|
@@ -112,7 +176,7 @@ cd your-project
|
|
|
112
176
|
kraftwerk init # scaffold kraftwerk.yml + kraftwerk-data/ (workflow, agent, knowledge)
|
|
113
177
|
kraftwerk doctor # preflight: harness CLIs, docker, workflows, declared env vars, legacy leftovers
|
|
114
178
|
kraftwerk run hello "Was ist kraftwerk?"
|
|
115
|
-
kraftwerk ui # inspector on http://localhost:1981
|
|
179
|
+
kraftwerk ui # inspector on http://localhost:1981 (see Start and use kraftwerk)
|
|
116
180
|
```
|
|
117
181
|
|
|
118
182
|
The same without a global install:
|
|
@@ -159,8 +223,10 @@ kraftwerk run tagline "https://..." # run; --yes, --verbose
|
|
|
159
223
|
kraftwerk run # interactive: pick workflow, type the request
|
|
160
224
|
kraftwerk runs # past runs from output/*/trace.jsonl; runs show <id> for detail
|
|
161
225
|
kraftwerk knowledge # Knowledge: OKF bundles (list/get/put/verify/search/...)
|
|
162
|
-
kraftwerk ui #
|
|
226
|
+
kraftwerk ui # start this workspace: http://localhost:1981 (or open it in the daemon); --port, --lan, --standalone
|
|
227
|
+
kraftwerk daemon # one kraftwerk for every open workspace: http://<slug>.localhost:1980; --port, --lan
|
|
163
228
|
kraftwerk workspaces # every workspace on this machine; workspaces start|stop|forget <ref>
|
|
229
|
+
kraftwerk devices # paired devices (a phone, another computer); devices pair|revoke
|
|
164
230
|
kraftwerk projects # goal-scoped project folders; projects create|show|link|log|remove
|
|
165
231
|
kraftwerk trash # what was deleted; trash restore|purge <id>, trash empty
|
|
166
232
|
kraftwerk journal <agent> ["<entry>"] # an agent's memory across sessions: print it, add a line (--kind)
|
|
@@ -171,8 +237,6 @@ kraftwerk create "was der Workflow tun soll" # for LLM agents: prints a build
|
|
|
171
237
|
kraftwerk runner build # build the Docker sandbox image (once)
|
|
172
238
|
kraftwerk run --sandbox website-check "https://..." # isolated container per run; --ssh forwards the agent
|
|
173
239
|
kraftwerk runner ps / stop <run-id> # see / stop running sandbox containers
|
|
174
|
-
kraftwerk tunnel setup kw.example.com # Cloudflare Tunnel to the inspector: login, create, route dns, kraftwerk.yml
|
|
175
|
-
kraftwerk tunnel # run that tunnel alone (the UI runs elsewhere)
|
|
176
240
|
```
|
|
177
241
|
|
|
178
242
|
Nothing is deleted at once. Removing an agent, a project, a workflow, a knowledge
|
|
@@ -184,20 +248,18 @@ The **trash** screen (`#/trash`) or `kraftwerk trash restore <id>` puts an entry
|
|
|
184
248
|
back where it came from. Deleting it there, or `kraftwerk trash empty`, makes it
|
|
185
249
|
final. A chat's agent transcripts are only removed then, so a restored chat can resume.
|
|
186
250
|
|
|
187
|
-
The inspector binds `127.0.0.1
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
127.0.0.1). Inside a
|
|
192
|
-
|
|
193
|
-
localhost, and its traefik
|
|
194
|
-
|
|
195
|
-
`X-Forwarded-Host` to the host
|
|
196
|
-
by default; nginx needs
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
requests are refused when `Origin` names a different host. See
|
|
200
|
-
[Inspector through a Cloudflare Tunnel](#inspector-through-a-cloudflare-tunnel).
|
|
251
|
+
The inspector binds `127.0.0.1`: its chat runs coding agents against the
|
|
252
|
+
repo, so it stays off the network unless you ask (`--lan`), and there only
|
|
253
|
+
paired devices get in. While bound to loopback it only answers requests
|
|
254
|
+
whose `Host` is a loopback name or `<slug>.localhost` (so a page on another
|
|
255
|
+
site cannot reach it by re-pointing its own DNS at 127.0.0.1). Inside a
|
|
256
|
+
container it binds all interfaces instead, because the port mapping is the
|
|
257
|
+
boundary there (`deploy-starter/` publishes to localhost, and its traefik
|
|
258
|
+
override adds basic-auth). `KRAFTWERK_UI_HOST` overrides the default either
|
|
259
|
+
way. A reverse proxy in front of it must set `X-Forwarded-Host` to the host
|
|
260
|
+
the browser addressed (Caddy and traefik do by default; nginx needs
|
|
261
|
+
`proxy_set_header X-Forwarded-Host $host;`), and its visitors pair like any
|
|
262
|
+
other device.
|
|
201
263
|
|
|
202
264
|
### The kraftwerk.yml project config
|
|
203
265
|
|
|
@@ -206,16 +268,19 @@ fields are optional. `workflows:` sets the workflows root, `output:` the
|
|
|
206
268
|
run-artifact directory (default `output/`), `knowledge:` the OKF bundle root
|
|
207
269
|
(default `knowledge/`), and `agents:` the agent-definition root (default
|
|
208
270
|
`agents/`). `repos:` turns on the [repositories](#repositories) folder.
|
|
209
|
-
`
|
|
210
|
-
[Cloudflare Tunnel](#inspector-through-a-cloudflare-tunnel). `cloud:`
|
|
211
|
-
tunes (or turns off) the registration with the
|
|
271
|
+
`cloud:` tunes (or turns off) the registration with the
|
|
212
272
|
[kraftwerk cloud](#inspector-in-the-kraftwerk-cloud), which is on by default.
|
|
213
273
|
|
|
274
|
+
`name:` is the title shown in the UI and can change freely. `slug:` is the
|
|
275
|
+
workspace's address on this machine. It defaults to the folder name
|
|
276
|
+
(`~/Sites/team-blau` becomes `team-blau`). It must be lowercase letters,
|
|
277
|
+
digits and dashes, and unique among the workspaces on the machine.
|
|
278
|
+
`kraftwerk ui` and `kraftwerk doctor` warn when two workspaces share one.
|
|
279
|
+
|
|
214
280
|
A `.env` next to kraftwerk.yml (`KEY=value` lines, `#` comments, quotes)
|
|
215
281
|
is loaded into every kraftwerk process at start: `kraftwerk ui` and the
|
|
216
282
|
server it supervises, so a restart from the UI re-reads the file; the chat
|
|
217
|
-
agents, routines and workflow runs the inspector spawns; the
|
|
218
|
-
(`TUNNEL_TOKEN`); `requires:` checks in `run` and `doctor`. A variable the
|
|
283
|
+
agents, routines and workflow runs the inspector spawns; `requires:` checks in `run` and `doctor`. A variable the
|
|
219
284
|
shell already sets wins over the file. A project started from another
|
|
220
285
|
workspace's inspector gets its own `.env`, not the other one's. The file is
|
|
221
286
|
never synced (it is on the workspace git's deny list and in the .gitignore
|
|
@@ -304,6 +369,33 @@ agent such as Claude Code or Codex. It prints a self-contained brief covering
|
|
|
304
369
|
the schema example, gates, harness rules and verify ladder, which the agent
|
|
305
370
|
follows to author the workflow folder and validate and smoke it with this CLI.
|
|
306
371
|
|
|
372
|
+
### Inspector on your phone (paired devices)
|
|
373
|
+
|
|
374
|
+
`kraftwerk ui --lan` also listens on your network and prints the addresses
|
|
375
|
+
(for example `http://192.168.1.20:1981`). Anyone who opens one gets a
|
|
376
|
+
pairing screen, nothing else. To let a device in, make a one-time code on
|
|
377
|
+
the machine kraftwerk runs on, with `kraftwerk devices pair` or settings →
|
|
378
|
+
devices → pair a device, and enter it on the device. The code works once,
|
|
379
|
+
for 10 minutes. The device stays paired until you unpair it in the same
|
|
380
|
+
place (`kraftwerk devices revoke <id>`), and its access ends at once.
|
|
381
|
+
|
|
382
|
+
One pairing covers every workspace on the machine. The tokens live in
|
|
383
|
+
`~/.kraftwerk/devices.json` as hashes only. A browser keeps its token as
|
|
384
|
+
an HttpOnly cookie. An app sends it as `Authorization: Bearer`, or as
|
|
385
|
+
`?token=` on the socket. A paired device can use everything except
|
|
386
|
+
managing devices, which stays on the machine itself. Requests from the
|
|
387
|
+
machine itself need no pairing.
|
|
388
|
+
Inside a container nothing changes: the port mapping is the boundary.
|
|
389
|
+
`KRAFTWERK_UI_TRUST_NETWORK=1` brings back the old behaviour of trusting
|
|
390
|
+
every network caller, and `kraftwerk doctor` warns while it is set.
|
|
391
|
+
Behind a reverse proxy on the same machine, the proxy must send
|
|
392
|
+
`X-Forwarded-For` (or `X-Forwarded-Host`), so its visitors pair like any
|
|
393
|
+
other device. If it cannot, set `KRAFTWERK_UI_REQUIRE_PAIRING=1`, which
|
|
394
|
+
makes every request pair, this machine's own included.
|
|
395
|
+
|
|
396
|
+
For a client of your own, `@netnodeag/kraftwerk/client` takes the token:
|
|
397
|
+
`createClient({ baseUrl, token })`.
|
|
398
|
+
|
|
307
399
|
### Inspector on a server
|
|
308
400
|
|
|
309
401
|
The repo root ships [`deploy-starter/`](../deploy-starter/). Copy it into your
|
|
@@ -311,117 +403,13 @@ consumer repo as `deploy/` and it builds a small image, this package from npm
|
|
|
311
403
|
plus the claude, codex and pi CLIs, that serves `kraftwerk ui` for that repo
|
|
312
404
|
bind-mounted at `/work`. `compose.yml` is localhost-only, reachable over an
|
|
313
405
|
SSH tunnel. `compose.traefik.yml` layers traefik routing and mandatory
|
|
314
|
-
basic-auth on top. Treat that auth as load-bearing.
|
|
315
|
-
|
|
406
|
+
basic-auth on top. Treat that auth as load-bearing. In a container the UI
|
|
407
|
+
trusts whoever reaches it (the port mapping is the boundary, so device
|
|
408
|
+
pairing does not apply), and its chat runs coding agents against the mounted
|
|
316
409
|
repo. Agent logins made inside the container persist in the `agent-home`
|
|
317
410
|
volume. This is a different image from the `kraftwerk-runner` sandbox
|
|
318
411
|
(`runner/Dockerfile`) used by `run --sandbox`.
|
|
319
412
|
|
|
320
|
-
### Inspector through a Cloudflare Tunnel
|
|
321
|
-
|
|
322
|
-
The other way to reach a laptop's or server's inspector from elsewhere: no
|
|
323
|
-
open port, no reverse proxy, no certificate. `kraftwerk ui` runs
|
|
324
|
-
[cloudflared](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/)
|
|
325
|
-
next to the inspector and the loopback bind becomes reachable at a hostname
|
|
326
|
-
on a domain you have on Cloudflare.
|
|
327
|
-
|
|
328
|
-
You need a Cloudflare account with a domain added to it (the free plan is
|
|
329
|
-
enough), and cloudflared on the machine:
|
|
330
|
-
|
|
331
|
-
```bash
|
|
332
|
-
brew install cloudflared # macOS; Linux packages: developers.cloudflare.com/cloudflare-one/connections/connect-networks/downloads/
|
|
333
|
-
```
|
|
334
|
-
|
|
335
|
-
**1. Create the tunnel and route a hostname to it.** From the project root:
|
|
336
|
-
|
|
337
|
-
```bash
|
|
338
|
-
kraftwerk tunnel setup kw.example.com
|
|
339
|
-
```
|
|
340
|
-
|
|
341
|
-
This opens the browser for `cloudflared tunnel login` when there is no
|
|
342
|
-
certificate yet (pick the zone `kw.example.com` belongs to), creates a
|
|
343
|
-
tunnel named `kraftwerk-<project name>` (or `--name`), adds the CNAME from
|
|
344
|
-
the hostname to it, and writes the result into kraftwerk.yml:
|
|
345
|
-
|
|
346
|
-
```yaml
|
|
347
|
-
public: https://kw.example.com # the hostname the tunnel routes to
|
|
348
|
-
tunnel:
|
|
349
|
-
name: kraftwerk-agent-playground
|
|
350
|
-
```
|
|
351
|
-
|
|
352
|
-
A second run reuses the login and the tunnel. If the hostname already has a
|
|
353
|
-
DNS record, setup says so and `--overwrite-dns` replaces it. If cloudflared
|
|
354
|
-
quietly routed `kw.example.com.other-zone.com` instead, because the hostname
|
|
355
|
-
is outside the zone you logged in to, setup refuses and names the stray
|
|
356
|
-
record to delete before you log in to the right zone and run it again.
|
|
357
|
-
|
|
358
|
-
**2. Put a Cloudflare Access policy on the hostname.** The UI has no login
|
|
359
|
-
of its own and its chat runs coding agents against the workspace, so the
|
|
360
|
-
tunnel must never be the only thing between the internet and it. In the
|
|
361
|
-
[Zero Trust dashboard](https://one.dash.cloudflare.com/) go to Access →
|
|
362
|
-
Applications → Add an application → Self-hosted, enter `kw.example.com` as
|
|
363
|
-
the domain, and add a policy: emails ending in your domain with a one-time
|
|
364
|
-
PIN, a Google or GitHub login, whatever fits. Access is free for up to 50
|
|
365
|
-
users. From then on Cloudflare shows a login page before anything reaches
|
|
366
|
-
the tunnel.
|
|
367
|
-
|
|
368
|
-
**3. Let the inspector verify that login.** On the application's overview
|
|
369
|
-
page copy the Application Audience (AUD) tag, and take your team name from
|
|
370
|
-
the team domain `https://<team>.cloudflareaccess.com`:
|
|
371
|
-
|
|
372
|
-
```yaml
|
|
373
|
-
tunnel:
|
|
374
|
-
name: kraftwerk-agent-playground
|
|
375
|
-
access:
|
|
376
|
-
team: my-team
|
|
377
|
-
aud: 4714c1358e65fe4b408ad6d432a5f878f08194bdb4752441fd56faefa9b2b6f2
|
|
378
|
-
```
|
|
379
|
-
|
|
380
|
-
With this block the inspector checks the `Cf-Access-Jwt-Assertion` token
|
|
381
|
-
Access adds to every request, against the team's public keys, the audience
|
|
382
|
-
tag and the clock. A removed or misconfigured policy then fails closed with
|
|
383
|
-
a 401 instead of exposing the UI. Skipping the block works, and
|
|
384
|
-
`kraftwerk doctor` warns about it every time.
|
|
385
|
-
|
|
386
|
-
**4. Start.** `kraftwerk ui` now starts cloudflared with the inspector,
|
|
387
|
-
keeps it across UI restarts and stops it with the UI:
|
|
388
|
-
|
|
389
|
-
```
|
|
390
|
-
✔ Kraftwerk UI: http://localhost:1981
|
|
391
|
-
✔ Public URL: https://kw.example.com
|
|
392
|
-
↗ tunnel: running kraftwerk-agent-playground → https://kw.example.com → http://127.0.0.1:1981
|
|
393
|
-
cloudflared │ ... Registered tunnel connection ...
|
|
394
|
-
```
|
|
395
|
-
|
|
396
|
-
Open `https://kw.example.com` from anywhere, log in through Access, and you
|
|
397
|
-
are in the same inspector. `localhost:1981` on the machine itself keeps
|
|
398
|
-
working without a login. `kraftwerk doctor` reports the tunnel, the Access
|
|
399
|
-
block and whether cloudflared is installed.
|
|
400
|
-
|
|
401
|
-
**Variants.** `kraftwerk tunnel` runs the configured tunnel alone, for an
|
|
402
|
-
inspector that already runs elsewhere (started by `kraftwerk workspaces
|
|
403
|
-
start`, or in a container). For a tunnel created in the Zero Trust
|
|
404
|
-
dashboard instead (Networks → Tunnels), leave `name` out, route the
|
|
405
|
-
hostname to `http://localhost:<port>` there and export its token as
|
|
406
|
-
`TUNNEL_TOKEN` before `kraftwerk ui`. `public:` on its own, without
|
|
407
|
-
`tunnel:`, is for any other proxy that forwards the browser's `Host`
|
|
408
|
-
without setting `X-Forwarded-Host`.
|
|
409
|
-
|
|
410
|
-
**Troubleshooting.** cloudflared's lines appear prefixed with
|
|
411
|
-
`cloudflared │`. "Cannot determine default origin certificate path" means no
|
|
412
|
-
login on this machine: run `cloudflared tunnel login` or setup again. "tunnel
|
|
413
|
-
not found" means the name in kraftwerk.yml does not exist in the account
|
|
414
|
-
that logged in; `cloudflared tunnel list` shows what does. A tunnel that
|
|
415
|
-
dies three times right after launch is given up and the UI stays local
|
|
416
|
-
until the next start. A 421 "unexpected Host header" in the browser means
|
|
417
|
-
`public:` does not match the hostname you opened. A 401 means Access is
|
|
418
|
-
configured in kraftwerk.yml but the token is missing or wrong: the
|
|
419
|
-
hostname has no Access application, or `team`/`aud` do not match it.
|
|
420
|
-
|
|
421
|
-
Quick tunnels (`trycloudflare.com`) are deliberately not supported: Access
|
|
422
|
-
cannot be attached to them, which would leave the UI open to anyone with
|
|
423
|
-
the URL.
|
|
424
|
-
|
|
425
413
|
### Inspector in the kraftwerk cloud
|
|
426
414
|
|
|
427
415
|
The kraftwerk cloud is the `~/.kraftwerk` registry made reachable over the
|
|
@@ -429,8 +417,8 @@ internet: every `kraftwerk ui` registers there when it starts and sends a
|
|
|
429
417
|
heartbeat while it runs, so you can see from anywhere which of your
|
|
430
418
|
workspaces are up, on which machine, with which agents — and an admin sees
|
|
431
419
|
every instance at once. **This is on by default.** The record carries the
|
|
432
|
-
workspace name, icon and colour, the machine, the root, the version,
|
|
433
|
-
|
|
420
|
+
workspace name, icon and colour, the machine, the root, the version, and
|
|
421
|
+
the agent and channel roster. Registration
|
|
434
422
|
is best-effort: a cloud that is down never affects the UI.
|
|
435
423
|
|
|
436
424
|
No key goes into kraftwerk.yml. The cloud hands the instance an identity
|
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
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { Command } from "commander";
|
|
2
|
+
/**
|
|
3
|
+
* `kraftwerk devices` — other machines (a phone, a second laptop, an app)
|
|
4
|
+
* that may use this machine's kraftwerk. One pairing covers every
|
|
5
|
+
* workspace here; the store is ~/.kraftwerk/devices.json.
|
|
6
|
+
*
|
|
7
|
+
* kraftwerk devices list paired devices
|
|
8
|
+
* kraftwerk devices pair a one-time code to pair one (10 minutes)
|
|
9
|
+
* kraftwerk devices revoke <id> unpair one: its token stops working at once
|
|
10
|
+
*
|
|
11
|
+
* A device reaches this machine when the UI listens on the network:
|
|
12
|
+
* `kraftwerk ui --lan` (or through a tunnel, see `kraftwerk tunnel`).
|
|
13
|
+
*/
|
|
14
|
+
export declare function registerDevicesCommands(program: Command): void;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import Table from "cli-table3";
|
|
3
|
+
import { createPairCode, listDevices, PAIR_CODE_TTL_MS, revokeDevice } from "../core/devices.js";
|
|
4
|
+
import { fmtAgo } from "./workspaces.js";
|
|
5
|
+
/**
|
|
6
|
+
* `kraftwerk devices` — other machines (a phone, a second laptop, an app)
|
|
7
|
+
* that may use this machine's kraftwerk. One pairing covers every
|
|
8
|
+
* workspace here; the store is ~/.kraftwerk/devices.json.
|
|
9
|
+
*
|
|
10
|
+
* kraftwerk devices list paired devices
|
|
11
|
+
* kraftwerk devices pair a one-time code to pair one (10 minutes)
|
|
12
|
+
* kraftwerk devices revoke <id> unpair one: its token stops working at once
|
|
13
|
+
*
|
|
14
|
+
* A device reaches this machine when the UI listens on the network:
|
|
15
|
+
* `kraftwerk ui --lan` (or through a tunnel, see `kraftwerk tunnel`).
|
|
16
|
+
*/
|
|
17
|
+
export function registerDevicesCommands(program) {
|
|
18
|
+
const devices = program.command("devices").description("Paired devices: list, pair one, revoke one");
|
|
19
|
+
devices
|
|
20
|
+
.command("list", { isDefault: true })
|
|
21
|
+
.description("List paired devices")
|
|
22
|
+
.option("--json", "Machine-readable output")
|
|
23
|
+
.action(async (opts) => {
|
|
24
|
+
const list = await listDevices();
|
|
25
|
+
if (opts.json)
|
|
26
|
+
return void console.log(JSON.stringify(list, null, 2));
|
|
27
|
+
if (list.length === 0)
|
|
28
|
+
return void console.log(chalk.dim("No paired devices. `kraftwerk devices pair` makes a code for one."));
|
|
29
|
+
const table = new Table({ head: ["id", "name", "paired", "last seen"].map((h) => chalk.bold(h)) });
|
|
30
|
+
for (const d of list)
|
|
31
|
+
table.push([chalk.cyan(d.id), d.name, chalk.dim(fmtAgo(d.createdAt)), chalk.dim(d.lastSeenAt ? fmtAgo(d.lastSeenAt) : "never")]);
|
|
32
|
+
console.log(table.toString());
|
|
33
|
+
});
|
|
34
|
+
devices
|
|
35
|
+
.command("pair")
|
|
36
|
+
.description(`A one-time pairing code, valid ${PAIR_CODE_TTL_MS / 60_000} minutes`)
|
|
37
|
+
.action(async () => {
|
|
38
|
+
const { code } = await createPairCode();
|
|
39
|
+
console.log(`${chalk.green("✔")} pairing code ${chalk.bold.cyan(code)} ${chalk.dim(`— valid ${PAIR_CODE_TTL_MS / 60_000} minutes, once`)}`);
|
|
40
|
+
console.log(chalk.dim("Open kraftwerk on the device (start the UI with `kraftwerk ui --lan` to reach it on your network) and enter the code."));
|
|
41
|
+
});
|
|
42
|
+
devices
|
|
43
|
+
.command("revoke")
|
|
44
|
+
.description("Unpair a device: its token stops working at once")
|
|
45
|
+
.argument("<id>", "Device id as `kraftwerk devices` lists it")
|
|
46
|
+
.action(async (id) => {
|
|
47
|
+
if (!(await revokeDevice(id))) {
|
|
48
|
+
console.error(chalk.red(`No paired device "${id}".`));
|
|
49
|
+
process.exit(1);
|
|
50
|
+
}
|
|
51
|
+
console.log(`${chalk.green("✔")} unpaired ${chalk.cyan(id)}`);
|
|
52
|
+
});
|
|
53
|
+
}
|
|
@@ -78,14 +78,22 @@ async function oldRunFolders(outputDir) {
|
|
|
78
78
|
? [{ level: "info", label: `${old.length} run(s) in the old location`, detail: `${tildify(outputDir)}/run-* — still listed; new runs go to runs/, move them there to tidy up` }]
|
|
79
79
|
: [];
|
|
80
80
|
}
|
|
81
|
+
/** kraftwerk.yml keys from the Cloudflare Tunnel (removed in 0.65): the loader ignores them (`legacyKeys`). */
|
|
82
|
+
function oldConfigKeys(ws) {
|
|
83
|
+
const keys = ws.legacyKeys ?? [];
|
|
84
|
+
return keys.length
|
|
85
|
+
? [{ level: "info", label: "kraftwerk.yml still has public:/tunnel:", detail: `${keys.map((k) => `${k}:`).join(", ")} — the Cloudflare Tunnel was removed in 0.65 and these keys are ignored; delete them` }]
|
|
86
|
+
: [];
|
|
87
|
+
}
|
|
81
88
|
export async function legacyFindings(ws, version) {
|
|
82
89
|
const output = existsSync(ws.outputDir) ? ws.outputDir : "";
|
|
83
90
|
const found = [
|
|
84
91
|
...(await olderInstances(version)),
|
|
85
92
|
...installsOnPath(),
|
|
86
93
|
...(await oldRegistry()),
|
|
94
|
+
...oldConfigKeys(ws),
|
|
87
95
|
...(output ? await oldChatScopes(output) : []),
|
|
88
96
|
...(output ? await oldRunFolders(output) : []),
|
|
89
97
|
];
|
|
90
|
-
return found.length ? found : [{ level: "ok", label: "no legacy leftovers", detail: "no older servers, installs, registry or data formats" }];
|
|
98
|
+
return found.length ? found : [{ level: "ok", label: "no legacy leftovers", detail: "no older servers, installs, registry, config keys or data formats" }];
|
|
91
99
|
}
|
package/dist/cli/doctor.js
CHANGED
|
@@ -2,11 +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,
|
|
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
|
+
import { listDevices } from "../core/devices.js";
|
|
11
|
+
import { slugClashes } from "../core/instances.js";
|
|
10
12
|
const ICONS = {
|
|
11
13
|
ok: chalk.green("✔"),
|
|
12
14
|
warn: chalk.yellow("⚠"),
|
|
@@ -71,6 +73,13 @@ export async function runDoctor(cwd) {
|
|
|
71
73
|
process.exit(1);
|
|
72
74
|
}
|
|
73
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");
|
|
74
83
|
// Project config: well-formed (guaranteed by the parse above) and the
|
|
75
84
|
// configured paths actually exist.
|
|
76
85
|
if (!project.configPath) {
|
|
@@ -79,7 +88,7 @@ export async function runDoctor(cwd) {
|
|
|
79
88
|
else {
|
|
80
89
|
const cfg = project.config;
|
|
81
90
|
const keys = Object.keys(cfg);
|
|
82
|
-
// Blocks (git, repos, vibeables, projects,
|
|
91
|
+
// Blocks (git, repos, vibeables, projects, cloud) print as their keys, not [object Object].
|
|
83
92
|
const show = (v) => typeof v === "object" && v !== null
|
|
84
93
|
? Array.isArray(v)
|
|
85
94
|
? `[${v.length}]`
|
|
@@ -112,7 +121,7 @@ export async function runDoctor(cwd) {
|
|
|
112
121
|
report("ok", `${DOTENV_FILE}: ${names.length} variable(s) loaded`, names.join(", ") || "empty");
|
|
113
122
|
}
|
|
114
123
|
else {
|
|
115
|
-
report("info", `no ${DOTENV_FILE}`, "variables for agents
|
|
124
|
+
report("info", `no ${DOTENV_FILE}`, "variables for agents and workflows can live there — loaded on every start and restart");
|
|
116
125
|
}
|
|
117
126
|
// Repositories: the clones root must stay out of the workspace git. git
|
|
118
127
|
// itself decides — that covers worktrees (.git is a file), a workspace
|
|
@@ -131,33 +140,6 @@ export async function runDoctor(cwd) {
|
|
|
131
140
|
else
|
|
132
141
|
report("ok", label, "not inside a git repository");
|
|
133
142
|
}
|
|
134
|
-
// Public hostname + Cloudflare Tunnel. The UI has no login of its own, so
|
|
135
|
-
// a tunnel without Access verification is worth a warning every time.
|
|
136
|
-
const publicHost = publicHostFor(project);
|
|
137
|
-
const tunnel = tunnelFor(project);
|
|
138
|
-
if (publicHost && !tunnel)
|
|
139
|
-
report("info", `public: ${publicHost}`, "served when a tunnel or reverse proxy delivers that Host");
|
|
140
|
-
if (tunnel) {
|
|
141
|
-
const cloudflared = cliVersion("cloudflared");
|
|
142
|
-
if (cloudflared)
|
|
143
|
-
report("ok", `cloudflared`, cloudflared);
|
|
144
|
-
else {
|
|
145
|
-
report("fail", "cloudflared missing", "needed by tunnel: — brew install cloudflared (or see developers.cloudflare.com)");
|
|
146
|
-
failures++;
|
|
147
|
-
}
|
|
148
|
-
if (tunnel.name)
|
|
149
|
-
report("ok", `tunnel: ${tunnel.name} → ${publicHost}`, `cloudflared tunnel run --url http://127.0.0.1:${project.config.port ?? 1981} ${tunnel.name}`);
|
|
150
|
-
else if (process.env.TUNNEL_TOKEN)
|
|
151
|
-
report("ok", `tunnel: dashboard-managed → ${publicHost}`, "TUNNEL_TOKEN is set");
|
|
152
|
-
else {
|
|
153
|
-
report("fail", "tunnel: nothing to run", "set tunnel.name (locally-managed) or the TUNNEL_TOKEN env var (dashboard-managed)");
|
|
154
|
-
failures++;
|
|
155
|
-
}
|
|
156
|
-
if (tunnel.access)
|
|
157
|
-
report("ok", "tunnel.access", `tokens verified against ${tunnel.access.team}.cloudflareaccess.com`);
|
|
158
|
-
else
|
|
159
|
-
report("warn", "tunnel without access", "the UI has no login of its own — put a Cloudflare Access policy on the hostname and set tunnel.access so a removed policy fails closed");
|
|
160
|
-
}
|
|
161
143
|
// Cloud manager: registration is best-effort at runtime, so doctor only says what would happen.
|
|
162
144
|
const cloud = cloudFor(project);
|
|
163
145
|
if (cloud) {
|
|
@@ -223,6 +205,11 @@ export async function runDoctor(cwd) {
|
|
|
223
205
|
else
|
|
224
206
|
report("warn", `${e.workflow.name}: env missing`, missing.join(", "));
|
|
225
207
|
}
|
|
208
|
+
// Devices: who may use this machine's kraftwerk from elsewhere.
|
|
209
|
+
const devices = await listDevices();
|
|
210
|
+
if (process.env.KRAFTWERK_UI_TRUST_NETWORK === "1")
|
|
211
|
+
report("warn", "KRAFTWERK_UI_TRUST_NETWORK=1", "anyone who reaches the UI over the network uses it without pairing — unset it and pair devices instead (`kraftwerk devices pair`)");
|
|
212
|
+
report("info", devices.length ? `${devices.length} paired device(s)` : "no paired devices", devices.length ? devices.map((d) => d.name).join(", ") : "a phone or another computer can use kraftwerk once paired: `kraftwerk ui --lan`, then `kraftwerk devices pair`");
|
|
226
213
|
// Legacy: what older versions left behind, and what still runs or is installed at one.
|
|
227
214
|
for (const f of await legacyFindings(project, pkg.version))
|
|
228
215
|
report(f.level, f.label, f.detail);
|
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
|
|
@@ -27,12 +28,6 @@ skills: ${DATA_DIR}/skills # workspace skills (shared instruction packag
|
|
|
27
28
|
# root: ${DATA_DIR}/repos # clones land here (git-ignored); a bare \`repos:\` uses repos/ instead
|
|
28
29
|
# vibeables: # small apps built live in a chat with a preview pane (uncomment to enable; one folder per app under ${DATA_DIR}/vibeables, versioned with the workspace)
|
|
29
30
|
# projects: # goal-scoped folders — brief, systems of record, links to knowledge/vibeables/repos/workflows/agents — that chats work in (uncomment to enable; one folder per project under ${DATA_DIR}/projects)
|
|
30
|
-
# public: https://kw.example.com # hostname the inspector is reached at through a tunnel or reverse proxy
|
|
31
|
-
# tunnel: # Cloudflare Tunnel run by \`kraftwerk ui\` (needs public:; put a Cloudflare Access policy on the hostname — the UI has no login of its own)
|
|
32
|
-
# name: kraftwerk # locally-managed tunnel (cloudflared tunnel create/route dns); omit and export TUNNEL_TOKEN for a dashboard-managed one
|
|
33
|
-
# access: # verify the Access login on every request via public: (team = Zero Trust team name, aud = the application's audience tag)
|
|
34
|
-
# team: my-team
|
|
35
|
-
# aud: 4714c1358e65fe4b408ad6d432a5f878f08194bdb4752441fd56faefa9b2b6f2
|
|
36
31
|
# cloud: # the kraftwerk cloud is ON BY DEFAULT: \`kraftwerk ui\` registers this workspace there and shows a claim code under Settings → Cloud
|
|
37
32
|
# enabled: false # uncomment both lines to keep this workspace out of the cloud (or set KRAFTWERK_CLOUD_URL=off for the whole machine)
|
|
38
33
|
`;
|