@netnodeag/kraftwerk 0.47.0 → 0.49.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 (50) hide show
  1. package/README.md +106 -5
  2. package/dist/cli/doctor.js +18 -1
  3. package/dist/cli/init.js +10 -8
  4. package/dist/cli/kraftwerk.js +16 -2
  5. package/dist/cli/projects.d.ts +0 -14
  6. package/dist/cli/projects.js +181 -107
  7. package/dist/cli/repos.js +1 -1
  8. package/dist/cli/ui.js +1 -1
  9. package/dist/cli/vibeables.js +1 -1
  10. package/dist/cli/workspaces.d.ts +16 -0
  11. package/dist/cli/workspaces.js +149 -0
  12. package/dist/config.d.ts +22 -1
  13. package/dist/config.js +19 -3
  14. package/dist/dotenv.d.ts +40 -0
  15. package/dist/dotenv.js +84 -0
  16. package/dist/inspector/agents.js +2 -2
  17. package/dist/inspector/channels.d.ts +10 -1
  18. package/dist/inspector/channels.js +11 -2
  19. package/dist/inspector/chat/sessions.d.ts +8 -3
  20. package/dist/inspector/chat/sessions.js +58 -19
  21. package/dist/inspector/chat/types.d.ts +11 -0
  22. package/dist/inspector/git.js +5 -3
  23. package/dist/inspector/instances.d.ts +14 -12
  24. package/dist/inspector/instances.js +71 -31
  25. package/dist/inspector/notifications.js +2 -0
  26. package/dist/inspector/projects.d.ts +167 -0
  27. package/dist/inspector/projects.js +520 -0
  28. package/dist/inspector/search.d.ts +1 -1
  29. package/dist/inspector/search.js +3 -3
  30. package/dist/inspector/server.js +111 -20
  31. package/dist/inspector/settings.d.ts +3 -1
  32. package/dist/inspector/settings.js +14 -3
  33. package/inspector/dist/assets/{dist-D8pjVMTl.js → dist-B-8ArbIm.js} +1 -1
  34. package/inspector/dist/assets/{dist-DfAFwiGQ.js → dist-BFJWVdBm.js} +1 -1
  35. package/inspector/dist/assets/{dist-C2Mu5bT-.js → dist-BIvcKBWo.js} +1 -1
  36. package/inspector/dist/assets/{dist-CdyUhzAt.js → dist-BgYnJawU.js} +1 -1
  37. package/inspector/dist/assets/dist-Bl1ogZZM.js +1 -0
  38. package/inspector/dist/assets/{dist-BNUGzZS7.js → dist-BrgwFqUR.js} +1 -1
  39. package/inspector/dist/assets/{dist-DnmDnMK2.js → dist-Bz0bwjja.js} +1 -1
  40. package/inspector/dist/assets/{dist-CjMETDfE.js → dist-CFvGR87v.js} +1 -1
  41. package/inspector/dist/assets/{dist-CUeUDZ9j.js → dist-CjtxxsZ2.js} +1 -1
  42. package/inspector/dist/assets/{dist-CmOlZvwa.js → dist-QwwTJ2wk.js} +1 -1
  43. package/inspector/dist/assets/{dist-BOQhQPN9.js → dist-tfmgozDB.js} +1 -1
  44. package/inspector/dist/assets/{editor-DJbeG376.js → editor-C6m40u-h.js} +3 -3
  45. package/inspector/dist/assets/{index-DgEUOA11.js → index-Cvq-RiV2.js} +14 -14
  46. package/inspector/dist/assets/{index-DQrsXEWt.css → index-DcQwGaV2.css} +1 -1
  47. package/inspector/dist/index.html +2 -2
  48. package/package.json +1 -1
  49. package/schema/kraftwerk.schema.json +18 -0
  50. package/inspector/dist/assets/dist-rrIMDK_-.js +0 -1
package/README.md CHANGED
@@ -94,7 +94,7 @@ newer version is on disk, right after "update now" or after a manual install.
94
94
 
95
95
  ```bash
96
96
  npm install -g @netnodeag/kraftwerk@latest
97
- kraftwerk projects # every workspace on this machine, running or not
97
+ kraftwerk workspaces # every workspace on this machine, running or not
98
98
  ```
99
99
 
100
100
  ## Consume
@@ -156,7 +156,8 @@ kraftwerk run # interactive: pick workflow, type the r
156
156
  kraftwerk runs # past runs from output/*/trace.jsonl; runs show <id> for detail
157
157
  kraftwerk knowledge # Knowledge: OKF bundles (list/get/put/verify/search/...)
158
158
  kraftwerk ui # inspector web UI on http://localhost:1981; --port, --output
159
- kraftwerk projects # every workspace on this machine; projects start|stop|forget <ref>
159
+ kraftwerk workspaces # every workspace on this machine; workspaces start|stop|forget <ref>
160
+ kraftwerk projects # goal-scoped project folders; projects create|show|link|log|remove
160
161
  kraftwerk doctor # preflight: harness CLIs, docker, workflows, declared env vars
161
162
  kraftwerk validate # all discovered: schema + semantics + files, exit 1 on failure
162
163
  kraftwerk validate src/workflows/pitch # specific paths
@@ -193,6 +194,16 @@ run-artifact directory (default `output/`), `knowledge:` the OKF bundle root
193
194
  `public:` and `tunnel:` expose the inspector through a
194
195
  [Cloudflare Tunnel](#inspector-through-a-cloudflare-tunnel).
195
196
 
197
+ A `.env` next to kraftwerk.yml (`KEY=value` lines, `#` comments, quotes)
198
+ is loaded into every kraftwerk process at start: `kraftwerk ui` and the
199
+ server it supervises, so a restart from the UI re-reads the file; the chat
200
+ agents, routines and workflow runs the inspector spawns; the tunnel
201
+ (`TUNNEL_TOKEN`); `requires:` checks in `run` and `doctor`. A variable the
202
+ shell already sets wins over the file. A project started from another
203
+ workspace's inspector gets its own `.env`, not the other one's. The file is
204
+ never synced (it is on the workspace git's deny list and in the .gitignore
205
+ `kraftwerk init` writes); `kraftwerk doctor` lists the names it loaded.
206
+
196
207
  `switcher:` links other kraftwerk workspaces from the inspector header. The
197
208
  workspace name becomes a dropdown listing them:
198
209
 
@@ -207,8 +218,8 @@ switcher:
207
218
  letters of an agent's or channel's name, description or workspace and hit
208
219
  enter to jump to it. It lists the active agents and the channels of every
209
220
  workspace on this machine, grouped by workspace and running or not (a
210
- stopped one is started on the way), read from the project registry under
211
- `~/.kraftwerk/projects`, which every inspector keeps current with its
221
+ stopped one is started on the way), read from the workspace registry under
222
+ `~/.kraftwerk/workspaces`, which every inspector keeps current with its
212
223
  roster and channel list.
213
224
 
214
225
  ### Triggering from CI, cron, or webhooks
@@ -370,7 +381,7 @@ working without a login. `kraftwerk doctor` reports the tunnel, the Access
370
381
  block and whether cloudflared is installed.
371
382
 
372
383
  **Variants.** `kraftwerk tunnel` runs the configured tunnel alone, for an
373
- inspector that already runs elsewhere (started by `kraftwerk projects
384
+ inspector that already runs elsewhere (started by `kraftwerk workspaces
374
385
  start`, or in a container). For a tunnel created in the Zero Trust
375
386
  dashboard instead (Networks → Tunnels), leave `name` out, route the
376
387
  hostname to `http://localhost:<port>` there and export its token as
@@ -393,6 +404,96 @@ Quick tunnels (`trycloudflare.com`) are deliberately not supported: Access
393
404
  cannot be attached to them, which would leave the UI open to anyone with
394
405
  the URL.
395
406
 
407
+ ## Projects
408
+
409
+ Turn on `projects:` in `kraftwerk.yml` (or the checkbox in settings) and
410
+ the workspace gets one folder per project — a goal with everything the
411
+ agents need to reach it:
412
+
413
+ ```yaml
414
+ projects:
415
+ root: kraftwerk-data/projects # default; part of the workspace, synced like agents and knowledge
416
+ ```
417
+
418
+ ```
419
+ kraftwerk-data/projects/<slug>/
420
+ project.yml # title, status, goal, records, links
421
+ brief.md # the goal in full: what done looks like, constraints, stakeholders
422
+ state.md # current state, rewritten at the end of a session
423
+ log.md # append-only, newest first: decisions and milestones, dated and attributed
424
+ ```
425
+
426
+ `project.yml` holds the one-line goal, a status (`active`, `paused`, `done`,
427
+ `archived`), the harness, model and effort its chats run on (the same three
428
+ fields an agent has; the harness decides, not the caller), the **systems of
429
+ record** and the **links**:
430
+
431
+ ```yaml
432
+ title: Relaunch netnode.ch
433
+ status: active
434
+ goal: Ship the new site on NodeHive by 2026-11-30
435
+ harness: claude # claude | codex | pi — every chat in the project runs on it
436
+ model: sonnet # optional, like an agent's
437
+ effort: high # optional: low | medium | high | xhigh | max
438
+ records:
439
+ - kind: my-netnode # my-netnode | google-drive | github | bitbucket | notion | slack | url | anything
440
+ workspace: 22
441
+ url: https://my.netnode.ch/workspace/22
442
+ note: tickets, roadmap and meetings
443
+ - kind: google-drive
444
+ title: Contracts and briefs
445
+ url: https://drive.google.com/drive/folders/…
446
+ knowledge: [netnode-helpdesk] # OKF bundle names
447
+ vibeables: [launch-tracker] # folders under the vibeables root
448
+ repos: [netnode-frontend] # folders under the repos root
449
+ workflows: [website-check] # workflow slugs
450
+ agents: [max] # agent slugs
451
+ ```
452
+
453
+ A system of record says where the truth of the project is managed outside
454
+ kraftwerk and what usually lives there. It is context, not a credential and
455
+ not a grant: the agent reaches it through the tools its harness already has
456
+ (a CLI, an MCP server, the browser), and the harness decides what it may
457
+ call. Kraftwerk knows a few kinds only to label them and phrase the context
458
+ better; any other kind passes through as a link with a note.
459
+
460
+ Links are one-directional lists of slugs. A target that does not exist is
461
+ shown as "not found" on the project page and told to the agent, never an
462
+ error — the same rule an agent's knowledge list follows.
463
+
464
+ **Working in a project is chat.** The Projects screen lists the projects,
465
+ the chats of the selected one, and the thread; opening a project opens its
466
+ latest chat. A session scoped to a project starts with the brief, the
467
+ current state, the records, every link with how to reach it, and the rule
468
+ for keeping the project current: `state.md` is rewritten at the end of a
469
+ session that changed something, so the next session (by anyone) continues
470
+ without the transcript; `log.md` is appended through
471
+ `kraftwerk projects log <slug> "<line>" --actor <who>` so every line is
472
+ dated and attributed; `brief.md` belongs to the user. The working
473
+ directory stays the workspace root, so linked repositories and knowledge
474
+ are reachable by path; opening a vibeable moves it into the app folder as
475
+ in any chat.
476
+
477
+ **Coworkers.** "Add coworker" on a project chat works as on an agent
478
+ session: the chat becomes a channel whose members are the agents you pick,
479
+ with everything said so far and the project's brief, state, records and
480
+ links as context for every member (`project:` in `channel.yml`). The
481
+ channel lives on the channels screen and stays listed under the project.
482
+ The first agent picked answers messages that mention nobody.
483
+
484
+ ```bash
485
+ kraftwerk projects # list: title, status, goal, links
486
+ kraftwerk projects create "Relaunch netnode.ch" --goal "Ship by November"
487
+ kraftwerk projects show relaunch-netnode-ch # definition, records, link states, state, log
488
+ kraftwerk projects set relaunch-netnode-ch --harness codex --model gpt-5.6-sol --effort high
489
+ kraftwerk projects link relaunch-netnode-ch workflows website-check
490
+ kraftwerk projects log relaunch-netnode-ch "Decided on NodeHive." --actor human:lukas
491
+ kraftwerk projects remove relaunch-netnode-ch
492
+ ```
493
+
494
+ The registry of workspaces on this machine, which answered to
495
+ `kraftwerk projects` until 0.48, is `kraftwerk workspaces` now.
496
+
396
497
  ## Persistent agents
397
498
 
398
499
  The inspector's "agents" screen turns chat agents into persistent teammates. An
@@ -5,6 +5,7 @@ import chalk from "chalk";
5
5
  import { ignoreEntryFor, isDir, publicHostFor, reposRootFor, resolveProject, tunnelFor } from "../config.js";
6
6
  import { discoverWorkflows } from "../discover.js";
7
7
  import { missingEnv } from "../yaml.js";
8
+ import { applyDotenv, DOTENV_FILE } from "../dotenv.js";
8
9
  const ICONS = {
9
10
  ok: chalk.green("✔"),
10
11
  warn: chalk.yellow("⚠"),
@@ -87,7 +88,13 @@ export async function runDoctor(cwd) {
87
88
  else {
88
89
  const cfg = project.config;
89
90
  const keys = Object.keys(cfg);
90
- report("ok", `${path.basename(project.configPath)} well-formed`, keys.length ? keys.map((k) => `${k}: ${cfg[k]}`).join(", ") : "empty — defaults apply");
91
+ // Blocks (git, repos, vibeables, projects, tunnel) print as their keys, not [object Object].
92
+ const show = (v) => typeof v === "object" && v !== null
93
+ ? Array.isArray(v)
94
+ ? `[${v.length}]`
95
+ : `{${Object.entries(v).map(([k, x]) => `${k}: ${show(x)}`).join(", ") || " "}}`
96
+ : String(v);
97
+ report("ok", `${path.basename(project.configPath)} well-formed`, keys.length ? keys.map((k) => `${k}: ${show(cfg[k])}`).join(", ") : "empty — defaults apply");
91
98
  if (!cfg.name)
92
99
  report("info", "name not set in kraftwerk.yml", "inspector header falls back to the folder name");
93
100
  for (const key of ["workflows", "knowledge", "agents", "output"]) {
@@ -106,6 +113,16 @@ export async function runDoctor(cwd) {
106
113
  }
107
114
  }
108
115
  }
116
+ // .env next to kraftwerk.yml: already applied by the CLI hook; this call
117
+ // only reports what it holds (names, never values).
118
+ const dotenv = await applyDotenv(project.root);
119
+ if (dotenv.file) {
120
+ const names = [...dotenv.applied, ...dotenv.kept.map((k) => `${k} (shell wins)`)];
121
+ report("ok", `${DOTENV_FILE}: ${names.length} variable(s) loaded`, names.join(", ") || "empty");
122
+ }
123
+ else {
124
+ report("info", `no ${DOTENV_FILE}`, "variables for agents, workflows and the tunnel can live there — loaded on every start and restart");
125
+ }
109
126
  // Repositories: the clones root must stay out of the workspace git. git
110
127
  // itself decides — that covers worktrees (.git is a file), a workspace
111
128
  // nested in a larger repo, a .gitignore at the toplevel, global excludes.
package/dist/cli/init.js CHANGED
@@ -2,6 +2,7 @@ import { appendFile, mkdir, readFile, stat, writeFile } from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  import chalk from "chalk";
4
4
  import { CONFIG_SCHEMA_URL, gitignoreHas, SCHEMA_URL } from "../config.js";
5
+ import { DOTENV_FILE } from "../dotenv.js";
5
6
  import { initBundle, writeConcept } from "../okf.js";
6
7
  /**
7
8
  * `kraftwerk init` — make any repository a kraftwerk consumer in one
@@ -25,6 +26,7 @@ skills: ${DATA_DIR}/skills # workspace skills (shared instruction packag
25
26
  # repos: # git repositories the agents work on (uncomment both lines to enable)
26
27
  # root: ${DATA_DIR}/repos # clones land here (git-ignored); a bare \`repos:\` uses repos/ instead
27
28
  # 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
+ # 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)
28
30
  # public: https://kw.example.com # hostname the inspector is reached at through a tunnel or reverse proxy
29
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)
30
32
  # name: kraftwerk # locally-managed tunnel (cloudflared tunnel create/route dns); omit and export TUNNEL_TOKEN for a dashboard-managed one
@@ -133,24 +135,24 @@ export async function runInit(cwd) {
133
135
  await writeConcept(knowledgeRoot, DEMO_BUNDLE, "playbooks/refunds", DEMO_CONCEPT, "kraftwerk-init");
134
136
  created.push(bundleRel);
135
137
  }
136
- // .gitignore: the output dir and the repos root (clones must never become
137
- // gitlinks of the workspace repo). A missing file gets both in one write;
138
- // an existing one only the entries it lacks.
138
+ // .gitignore: the output dir, the repos root (clones must never become
139
+ // gitlinks of the workspace repo) and .env (secrets). A missing file gets
140
+ // all in one write; an existing one only the entries it lacks.
139
141
  const gitignorePath = path.join(cwd, ".gitignore");
140
142
  const gitignore = (await readFile(gitignorePath, "utf8").catch(() => null)) ?? null;
141
- const entries = [`${DATA_DIR}/output`, `${DATA_DIR}/repos`];
143
+ const entries = [`${DATA_DIR}/output/`, `${DATA_DIR}/repos/`, DOTENV_FILE];
142
144
  if (gitignore === null) {
143
- await writeFile(gitignorePath, entries.map((e) => `${e}/\n`).join(""));
145
+ await writeFile(gitignorePath, entries.map((e) => `${e}\n`).join(""));
144
146
  created.push(".gitignore");
145
147
  }
146
148
  else {
147
- const missing = entries.filter((e) => !gitignoreHas(gitignore, e));
149
+ const missing = entries.filter((e) => !gitignoreHas(gitignore, e.replace(/\/$/, "")));
148
150
  if (missing.length === 0) {
149
151
  skipped.push(".gitignore");
150
152
  }
151
153
  else {
152
- await appendFile(gitignorePath, `${gitignore.endsWith("\n") ? "" : "\n"}${missing.map((e) => `${e}/\n`).join("")}`);
153
- created.push(`.gitignore (${missing.map((e) => `${e}/`).join(", ")} added)`);
154
+ await appendFile(gitignorePath, `${gitignore.endsWith("\n") ? "" : "\n"}${missing.map((e) => `${e}\n`).join("")}`);
155
+ created.push(`.gitignore (${missing.join(", ")} added)`);
154
156
  }
155
157
  }
156
158
  for (const f of created)
@@ -13,9 +13,12 @@ import { runDoctor } from "./doctor.js";
13
13
  import { runInit } from "./init.js";
14
14
  import { registerKnowledgeCommands } from "./knowledge.js";
15
15
  import { registerProjectCommands } from "./projects.js";
16
+ import { registerWorkspaceCommands } from "./workspaces.js";
16
17
  import { registerRepoCommands } from "./repos.js";
17
18
  import { registerVibeableCommands } from "./vibeables.js";
18
19
  import { registerTunnelCommands } from "./tunnel.js";
20
+ import { applyDotenv } from "../dotenv.js";
21
+ import { resolveProject } from "../config.js";
19
22
  import { registerRoutineCommands } from "./routines.js";
20
23
  import { listRuns, showRun } from "./runs.js";
21
24
  import { runUi } from "./ui.js";
@@ -29,7 +32,9 @@ import { runUi } from "./ui.js";
29
32
  * kraftwerk knowledge ... Knowledge: OKF bundles (list/get/put/verify/...)
30
33
  * kraftwerk routines ... per-agent scheduled prompts (list/add/remove/enable/run)
31
34
  * kraftwerk ui start the inspector web UI (localhost:1981)
32
- * kraftwerk projects ... known projects on this machine (list/start/forget)
35
+ * kraftwerk workspaces ... known workspaces on this machine (list/start/stop/forget)
36
+ * kraftwerk projects ... goal-scoped project folders (list/create/show/link/log/remove)
37
+ * kraftwerk vibeables ... small apps built live in a chat (list/create/remove)
33
38
  * kraftwerk repos ... repositories the agents work on (list/add/update/remove)
34
39
  * kraftwerk tunnel [setup <host>] Cloudflare Tunnel to the inspector: run it alone, or set one up
35
40
  * kraftwerk doctor preflight: harness CLIs, docker, workflows, env
@@ -61,7 +66,15 @@ const pkg = JSON.parse(await readFile(new URL("../../package.json", import.meta.
61
66
  const program = new Command()
62
67
  .name("kraftwerk")
63
68
  .description("kraftwerk — agentic workspace for teams: agents, skills, knowledge, workflows, and the inspector UI")
64
- .version(pkg.version);
69
+ .version(pkg.version)
70
+ // The project's .env, before any command runs (and so before `kraftwerk
71
+ // ui` spawns its server, and again in that server on every restart). A
72
+ // broken kraftwerk.yml is the command's own error to report, not the hook's.
73
+ .hook("preAction", async () => {
74
+ const project = await resolveProject(process.cwd()).catch(() => null);
75
+ if (project)
76
+ await applyDotenv(project.root);
77
+ });
65
78
  const agentLabel = (workflow) => workflow.meta.agents
66
79
  .map((a) => {
67
80
  const where = [a.harness && a.harness !== "claude" ? a.harness : "", a.protocol === "acp" ? "acp" : ""]
@@ -269,6 +282,7 @@ runs
269
282
  });
270
283
  registerKnowledgeCommands(program);
271
284
  registerRoutineCommands(program);
285
+ registerWorkspaceCommands(program);
272
286
  registerProjectCommands(program);
273
287
  registerRepoCommands(program);
274
288
  registerVibeableCommands(program);
@@ -1,16 +1,2 @@
1
1
  import type { Command } from "commander";
2
- /**
3
- * `kraftwerk projects` — every project that ever ran the inspector on this
4
- * machine (~/.kraftwerk/projects), joined with what is running right now.
5
- * The answer to "where did I start that UI, and how do I get it back":
6
- *
7
- * kraftwerk projects list: name, root, running/stopped
8
- * kraftwerk projects start <ref> relaunch `kraftwerk ui` in that root, detached
9
- * kraftwerk projects stop <ref> SIGTERM a running UI (also ones started in a terminal)
10
- * kraftwerk projects forget <ref> drop the record (the folder stays)
11
- *
12
- * <ref> is a project name, the root's folder name, the root path, or a
13
- * port / localhost:port for running instances that recorded no root.
14
- */
15
- export declare const fmtAgo: (iso?: string) => string;
16
2
  export declare function registerProjectCommands(program: Command): void;