@netnodeag/kraftwerk 0.76.0 → 0.77.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 (45) hide show
  1. package/README.md +33 -27
  2. package/dist/cli/cloud.d.ts +2 -0
  3. package/dist/cli/cloud.js +262 -0
  4. package/dist/cli/daemon.js +9 -0
  5. package/dist/cli/doctor.js +16 -8
  6. package/dist/cli/init.js +2 -2
  7. package/dist/cli/kraftwerk.js +4 -0
  8. package/dist/client/routes.d.ts +4 -0
  9. package/dist/client/routes.js +4 -0
  10. package/dist/config.d.ts +15 -28
  11. package/dist/config.js +5 -19
  12. package/dist/core/cloud.d.ts +115 -0
  13. package/dist/core/cloud.js +94 -0
  14. package/dist/server/api/cloud.d.ts +72 -0
  15. package/dist/server/api/cloud.js +38 -0
  16. package/dist/server/api/devices.d.ts +8 -8
  17. package/dist/server/api/index.d.ts +88 -10
  18. package/dist/server/api/index.js +2 -0
  19. package/dist/server/api/workspace.d.ts +16 -1
  20. package/dist/server/api/workspace.js +2 -1
  21. package/dist/server/cloud.d.ts +65 -33
  22. package/dist/server/cloud.js +207 -188
  23. package/dist/server/hub-context.d.ts +1 -1
  24. package/dist/server/hub-context.js +3 -3
  25. package/dist/server/relay.d.ts +13 -0
  26. package/dist/server/relay.js +29 -1
  27. package/dist/server/server.d.ts +3 -2
  28. package/dist/server/server.js +6 -6
  29. package/package.json +1 -1
  30. package/schema/kraftwerk.schema.json +4 -4
  31. package/web/dist/assets/{dist-T1sNLC62.js → dist-BGHweZKi.js} +1 -1
  32. package/web/dist/assets/{dist-DFXmaDc5.js → dist-BVPxOqcx.js} +1 -1
  33. package/web/dist/assets/{dist-BYkcY-Vl.js → dist-CCNyRSAA.js} +1 -1
  34. package/web/dist/assets/{dist-flnLXEzU.js → dist-CXoAMU1b.js} +1 -1
  35. package/web/dist/assets/{dist-l7foWlFx.js → dist-DCFREMXk.js} +1 -1
  36. package/web/dist/assets/{dist-BQpWYkxm.js → dist-DT2Tjvwx.js} +1 -1
  37. package/web/dist/assets/{dist-BwIca_Uj.js → dist-DdA5PoVJ.js} +1 -1
  38. package/web/dist/assets/{dist-fgB7OEJy.js → dist-DoD3qX1P.js} +1 -1
  39. package/web/dist/assets/{dist-vuciRCSz.js → dist-FFA4-S6d.js} +1 -1
  40. package/web/dist/assets/{dist-BTVP-6On.js → dist-yGx0qYbw.js} +1 -1
  41. package/web/dist/assets/dist-zjUUP5a9.js +1 -0
  42. package/web/dist/assets/{editor-C6Ok8NPo.js → editor-CyWapLQr.js} +3 -3
  43. package/web/dist/assets/{index-z4tpCCzk.js → index-CNXcNoKJ.js} +4 -4
  44. package/web/dist/index.html +1 -1
  45. package/web/dist/assets/dist-BURf1l-A.js +0 -1
@@ -3,7 +3,7 @@ import { promises as fs } from "node:fs";
3
3
  import http from "node:http";
4
4
  import os from "node:os";
5
5
  import path from "node:path";
6
- import { daemonHandshake, exportKeyPair, generateKeyPair, importKeyPair, relayId, toBase64, fromBase64, } from "../client/relay.js";
6
+ import { daemonHandshake, exportKeyPair, generateKeyPair, importKeyPair, relayId, toBase64, fromBase64, fromBase64Url, } from "../client/relay.js";
7
7
  import { CLOUD_DEFAULT_URL } from "../config.js";
8
8
  import { dropPushToken } from "../core/devices.js";
9
9
  const settingsFile = () => path.join(os.homedir(), ".kraftwerk", "relay.json");
@@ -36,6 +36,34 @@ export async function setRelay(enabled, url) {
36
36
  await writeRelaySettings(s);
37
37
  return s;
38
38
  }
39
+ /**
40
+ * The machine's key pair and secret, created (with the relay staying off)
41
+ * when relay.json is missing: the daemon's first start, `kraftwerk login`.
42
+ * The cloud knows the machine by the same id and secret the relay does; a
43
+ * damaged file is refused, never overwritten (it holds the id).
44
+ */
45
+ export async function ensureRelayKeys() {
46
+ const s = await readRelaySettings();
47
+ if (s)
48
+ return s;
49
+ try {
50
+ await fs.access(settingsFile());
51
+ throw new Error(`${settingsFile()} is damaged — fix it or remove it (removing gives this machine a new id)`);
52
+ }
53
+ catch (err) {
54
+ if (err.code !== "ENOENT")
55
+ throw err;
56
+ }
57
+ const pair = await exportKeyPair(await generateKeyPair());
58
+ const fresh = { enabled: false, url: CLOUD_DEFAULT_URL, ...pair, secret: randomBytes(32).toString("base64url") };
59
+ await writeRelaySettings(fresh);
60
+ return fresh;
61
+ }
62
+ /** The machine's id (its relay id), public key and secret: what the cloud knows it by. */
63
+ export async function machineCredentials() {
64
+ const s = await ensureRelayKeys();
65
+ return { id: await relayId(fromBase64Url(s.publicKey)), key: s.publicKey, secret: s.secret };
66
+ }
39
67
  /** wss://host/api/relay/<role> for a cloud URL. */
40
68
  export const relayEndpoint = (cloudUrl, role) => `${cloudUrl.replace(/\/+$/, "").replace(/^http/, "ws")}/api/relay/${role}`;
41
69
  /**
@@ -34,8 +34,9 @@ export interface OpenWorkspace {
34
34
  * a standalone `kraftwerk ui` exactly one — its default, also reached
35
35
  * without a slug, as before. Each open workspace runs in its own Workspace
36
36
  * (core/workspace.ts) with its own .env, background jobs (routines, git
37
- * sync, cloud), registry entry and live state; closing one ends all of that
38
- * and leaves the others running.
37
+ * sync), registry entry and live state; closing one ends all of that
38
+ * and leaves the others running. The daemon's hub also carries the
39
+ * machine's cloud record (server/cloud.ts), which lists the open workspaces.
39
40
  */
40
41
  export interface Hub {
41
42
  readonly server: http.Server;
@@ -3,7 +3,7 @@ import http from "node:http";
3
3
  import net from "node:net";
4
4
  import path from "node:path";
5
5
  import { setDefaultWorkspace, Workspace } from "../core/workspace.js";
6
- import { cloudGoodbye, cloudGoodbyeAll, startCloudSync, stopCloudSync } from "./cloud.js";
6
+ import { stopMachineCloud } from "./cloud.js";
7
7
  import { disposeAllBackends, disposeWorkspaceBackends } from "../core/chat/sessions.js";
8
8
  import { markWorkspaceStopped, registerInstance, registerWorkspace, unregisterInstance } from "../core/instances.js";
9
9
  import { startGitSync, stopGitSync } from "../core/git.js";
@@ -156,8 +156,8 @@ function installExitHandlers() {
156
156
  // Stamp first: unregistering forgets which workspaces this process served.
157
157
  markWorkspaceStopped();
158
158
  unregisterInstance();
159
- // The cloud goodbye is bounded and a no-op without cloud:, so the exit stays prompt.
160
- void cloudGoodbyeAll().finally(() => process.exit(sig === "SIGINT" ? 130 : 143));
159
+ // The machine's cloud goodbye is bounded and a no-op outside the daemon, so the exit stays prompt.
160
+ void stopMachineCloud().finally(() => process.exit(sig === "SIGINT" ? 130 : 143));
161
161
  });
162
162
  }
163
163
  process.once("exit", (code) => {
@@ -345,6 +345,9 @@ export async function startHub(opts) {
345
345
  await hub.close(o.root);
346
346
  opts.onChange = notify;
347
347
  hubs.delete(hub);
348
+ // The daemon's machine client (started by `kraftwerk daemon`) says goodbye with the hub it reports.
349
+ if (opts.daemon)
350
+ await stopMachineCloud(hub);
348
351
  // A connected browser tab (its socket, a keep-alive connection) must not hold the stop open.
349
352
  for (const s of [...sockets])
350
353
  s.close();
@@ -408,7 +411,6 @@ export async function startHub(opts) {
408
411
  // Best-effort, in the background: none of them may hold up (or, failing, break) the open.
409
412
  entry.registered = registerInstance(entry.aliasPort ?? port, abs, opts.daemon ? { hub: true, slug, url: entry.url } : {}).catch(() => { });
410
413
  void registerWorkspace(abs).catch(() => { });
411
- void startCloudSync(entry.url, version).catch(() => { });
412
414
  }));
413
415
  await noteChange();
414
416
  return entry;
@@ -434,8 +436,6 @@ export async function startHub(opts) {
434
436
  closeWatches();
435
437
  disposeWorkspaceBackends();
436
438
  disposeWorkspaceDevs();
437
- await cloudGoodbye();
438
- stopCloudSync();
439
439
  }));
440
440
  await entry.registered;
441
441
  unregisterInstance(abs);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@netnodeag/kraftwerk",
3
- "version": "0.76.0",
3
+ "version": "0.77.0",
4
4
  "description": "Open-source agentic workspace for teams — where people and AI agents get work done together: agents (Claude Code, Codex, Pi), skills, shared knowledge, repeatable workflows, and deterministic verification",
5
5
  "keywords": [
6
6
  "agents",
@@ -177,21 +177,21 @@
177
177
  "null"
178
178
  ],
179
179
  "additionalProperties": false,
180
- "description": "kraftwerk cloud manager — ~/.kraftwerk made reachable over the internet. ON BY DEFAULT: every `kraftwerk ui` registers on start and heartbeats while it runs (name, icon, colour, root, machine, version, agent and channel roster). `enabled: false` opts out. No credential belongs here: the cloud hands the instance its identity and a claim code (Settings → Cloud), and an unattended server uses KRAFTWERK_CLOUD_TOKEN in .env.",
180
+ "description": "kraftwerk cloud: the daemon registers this MACHINE (its relay id) once, and the workspaces it serves ride along. This block only vetoes: `enabled: false` leaves this workspace out of the machine's record. The machine's switch and the cloud's origin are ~/.kraftwerk/cloud.json (`kraftwerk cloud on|off`) and KRAFTWERK_CLOUD_URL; `kraftwerk login` claims the machine.",
181
181
  "properties": {
182
182
  "enabled": {
183
183
  "type": "boolean",
184
- "description": "false opts out of the cloud. Default: true"
184
+ "description": "false leaves this workspace out of the machine's cloud record. Default: true"
185
185
  },
186
186
  "url": {
187
187
  "type": "string",
188
188
  "pattern": "^https?://",
189
- "description": "Origin of the cloud manager. Default: https://srv.kraftwerk-cloud.netnode.cloud (the KRAFTWERK_CLOUD_URL environment variable wins; \"off\" disables)"
189
+ "description": "Deprecated: the cloud's origin is the machine's (~/.kraftwerk/cloud.json, KRAFTWERK_CLOUD_URL). Still used, with a warning, when the machine names none."
190
190
  },
191
191
  "interval": {
192
192
  "type": "integer",
193
193
  "minimum": 1,
194
- "description": "Seconds between heartbeats. Default: 60 (the cloud may raise it)"
194
+ "description": "Deprecated and ignored: the cloud sets the heartbeat interval."
195
195
  }
196
196
  }
197
197
  }