@netnodeag/kraftwerk 0.75.1 → 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 (63) 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-control.js +4 -2
  5. package/dist/cli/daemon.js +9 -0
  6. package/dist/cli/doctor.d.ts +3 -1
  7. package/dist/cli/doctor.js +51 -9
  8. package/dist/cli/init.js +7 -4
  9. package/dist/cli/kraftwerk.js +13 -5
  10. package/dist/cli/routines.d.ts +2 -1
  11. package/dist/cli/routines.js +40 -4
  12. package/dist/cli/whoami.d.ts +9 -0
  13. package/dist/cli/whoami.js +48 -0
  14. package/dist/client/routes.d.ts +7 -0
  15. package/dist/client/routes.js +7 -0
  16. package/dist/config.d.ts +35 -28
  17. package/dist/config.js +43 -22
  18. package/dist/core/attention.d.ts +8 -1
  19. package/dist/core/attention.js +38 -2
  20. package/dist/core/cloud.d.ts +115 -0
  21. package/dist/core/cloud.js +94 -0
  22. package/dist/core/identity.d.ts +46 -0
  23. package/dist/core/identity.js +106 -0
  24. package/dist/core/routines.d.ts +81 -0
  25. package/dist/core/routines.js +174 -33
  26. package/dist/server/api/agents.d.ts +5 -1
  27. package/dist/server/api/agents.js +19 -2
  28. package/dist/server/api/cloud.d.ts +72 -0
  29. package/dist/server/api/cloud.js +38 -0
  30. package/dist/server/api/devices.d.ts +8 -8
  31. package/dist/server/api/index.d.ts +102 -13
  32. package/dist/server/api/index.js +2 -0
  33. package/dist/server/api/runs.d.ts +3 -1
  34. package/dist/server/api/runs.js +9 -1
  35. package/dist/server/api/workspace.d.ts +22 -2
  36. package/dist/server/api/workspace.js +7 -1
  37. package/dist/server/cloud.d.ts +65 -33
  38. package/dist/server/cloud.js +207 -188
  39. package/dist/server/hub-context.d.ts +1 -1
  40. package/dist/server/hub-context.js +3 -3
  41. package/dist/server/push.js +1 -1
  42. package/dist/server/relay.d.ts +13 -0
  43. package/dist/server/relay.js +29 -1
  44. package/dist/server/server.d.ts +3 -2
  45. package/dist/server/server.js +6 -6
  46. package/package.json +1 -1
  47. package/schema/kraftwerk.schema.json +9 -4
  48. package/web/dist/assets/{dist-CezfArgg.js → dist-BGHweZKi.js} +1 -1
  49. package/web/dist/assets/{dist-Y9aHncjU.js → dist-BVPxOqcx.js} +1 -1
  50. package/web/dist/assets/{dist-CLZjnhZZ.js → dist-CCNyRSAA.js} +1 -1
  51. package/web/dist/assets/{dist-QqFmuPda.js → dist-CXoAMU1b.js} +1 -1
  52. package/web/dist/assets/{dist-BI4umsk6.js → dist-DCFREMXk.js} +1 -1
  53. package/web/dist/assets/{dist-YNCNGGgK.js → dist-DT2Tjvwx.js} +1 -1
  54. package/web/dist/assets/{dist-CzM6tn0f.js → dist-DdA5PoVJ.js} +1 -1
  55. package/web/dist/assets/{dist-CYicby0c.js → dist-DoD3qX1P.js} +1 -1
  56. package/web/dist/assets/{dist-CODxhtAK.js → dist-FFA4-S6d.js} +1 -1
  57. package/web/dist/assets/{dist-DaGnyH_l.js → dist-yGx0qYbw.js} +1 -1
  58. package/web/dist/assets/dist-zjUUP5a9.js +1 -0
  59. package/web/dist/assets/{editor-DEb8z5-Q.js → editor-CyWapLQr.js} +3 -3
  60. package/web/dist/assets/index-CNXcNoKJ.js +231 -0
  61. package/web/dist/index.html +1 -1
  62. package/web/dist/assets/dist-DtktO5hc.js +0 -1
  63. package/web/dist/assets/index-kmyK4tFm.js +0 -231
@@ -1,11 +1,13 @@
1
- import { promises as fs } from "node:fs";
1
+ import { promises as fs, realpathSync } from "node:fs";
2
2
  import path from "node:path";
3
3
  import { parse, stringify } from "yaml";
4
- import { getOutputDir } from "./context.js";
4
+ import { getOutputDir, workspaceRoot } from "./context.js";
5
5
  import { perWorkspace } from "./workspace.js";
6
6
  import { getAgent, listAgents, safeAgentSlug, agentsRoot } from "./agents.js";
7
7
  import { chatAwaitingApproval, createChat, postMessage } from "./chat/sessions.js";
8
8
  import { pushNotification } from "./notifications.js";
9
+ import { isIdentityString, localIdentity, readIdentity, identityString } from "./identity.js";
10
+ import { gitCommit, repoRootFor } from "./git.js";
9
11
  /* ---------- cron ---------- */
10
12
  const ALIASES = {
11
13
  "@hourly": "0 * * * *",
@@ -100,8 +102,26 @@ function normalizeRoutine(raw) {
100
102
  schedule: String(raw.schedule ?? ""),
101
103
  prompt: String(raw.prompt ?? ""),
102
104
  enabled: raw.enabled !== false,
105
+ ...(typeof raw.runs_on === "string" && raw.runs_on.trim() ? { runsOn: raw.runs_on.trim() } : {}),
106
+ ...(typeof raw.private === "boolean" ? { private: raw.private } : {}),
103
107
  };
104
108
  }
109
+ /** The file's own spelling: snake_case `runs_on`, optional keys only when set. */
110
+ function serializeRoutine(r) {
111
+ return {
112
+ id: r.id,
113
+ name: r.name,
114
+ schedule: r.schedule,
115
+ prompt: r.prompt,
116
+ enabled: r.enabled,
117
+ ...(r.runsOn ? { runs_on: r.runsOn } : {}),
118
+ ...(r.private !== undefined ? { private: r.private } : {}),
119
+ };
120
+ }
121
+ /** Why a runs_on value is refused, or undefined. */
122
+ function runsOnError(value) {
123
+ return isIdentityString(value) ? undefined : `runs_on must be <handle>@<machine> (lowercase letters, digits, ".", "_", "-"), got "${value}"`;
124
+ }
105
125
  export async function listRoutines(slug) {
106
126
  let raw;
107
127
  try {
@@ -117,7 +137,7 @@ export async function listRoutines(slug) {
117
137
  .filter(Boolean);
118
138
  }
119
139
  async function writeRoutines(slug, routines) {
120
- await fs.writeFile(await routinesFile(slug), stringify(routines));
140
+ await fs.writeFile(await routinesFile(slug), stringify(routines.map(serializeRoutine)));
121
141
  }
122
142
  export async function saveRoutine(slug, input) {
123
143
  const name = input.name?.trim();
@@ -134,15 +154,26 @@ export async function saveRoutine(slug, input) {
134
154
  .slice(0, 48);
135
155
  if (!id)
136
156
  throw new Error("name must contain at least one letter or digit");
157
+ const given = typeof input.runsOn === "string" ? input.runsOn.trim() : input.runsOn;
158
+ if (given) {
159
+ const err = runsOnError(given);
160
+ if (err)
161
+ throw new Error(err);
162
+ }
163
+ const routines = await listRoutines(slug);
164
+ const i = routines.findIndex((r) => r.id === id);
165
+ const existing = i >= 0 ? routines[i] : undefined;
166
+ const runsOn = given === undefined ? (existing ? existing.runsOn : await localIdentity()) : given || undefined;
167
+ const priv = input.private ?? existing?.private;
137
168
  const routine = {
138
169
  id,
139
170
  name,
140
171
  schedule: input.schedule.trim(),
141
172
  prompt: input.prompt,
142
173
  enabled: input.enabled !== false,
174
+ ...(runsOn ? { runsOn } : {}),
175
+ ...(priv !== undefined ? { private: priv } : {}),
143
176
  };
144
- const routines = await listRoutines(slug);
145
- const i = routines.findIndex((r) => r.id === id);
146
177
  if (i >= 0)
147
178
  routines[i] = routine;
148
179
  else
@@ -191,13 +222,19 @@ export async function allRoutines(workflows) {
191
222
  }))));
192
223
  return per.flat();
193
224
  }
225
+ /** Whether a routine fires on the machine whose identity is `me`: enabled and runs_on exactly that. */
226
+ export const firesHere = (r, me) => r.enabled && r.runsOn === me;
194
227
  export async function routineStatuses(slug) {
195
- const [routines, state] = await Promise.all([listRoutines(slug), readState()]);
228
+ const [routines, state, id] = await Promise.all([listRoutines(slug), readState(), readIdentity()]);
229
+ const me = identityString(id);
196
230
  return routines.map((r) => {
197
231
  const s = state[`${slug}/${r.id}`] ?? {};
232
+ const runner = !r.runsOn ? "unassigned" : r.runsOn === me ? "here" : "elsewhere";
198
233
  return {
199
234
  ...r,
200
- ...(r.enabled ? { nextRunAt: nextRun(r.schedule) } : {}),
235
+ runner,
236
+ ...(r.runsOn && id.previous.includes(r.runsOn) ? { retired: true } : {}),
237
+ ...(firesHere(r, me) ? { nextRunAt: nextRun(r.schedule) } : {}),
201
238
  ...(s.lastRunAt ? { lastRunAt: s.lastRunAt } : {}),
202
239
  ...(s.lastChatId ? { lastChatId: s.lastChatId } : {}),
203
240
  ...(s.lastChatId && chatAwaitingApproval(s.lastChatId) ? { awaitingApproval: true } : {}),
@@ -254,48 +291,152 @@ export async function runRoutineNow(slug, id) {
254
291
  /* ---------- scheduler ---------- */
255
292
  /** The scheduler's timer, one per workspace. */
256
293
  const scheduler = perWorkspace(() => ({ timer: null }));
294
+ /**
295
+ * The routines due at `now` that fire on this machine: enabled, runs_on
296
+ * exactly this identity, cron matching. Everything else — unassigned,
297
+ * another machine's, a retired identity's — is never due here.
298
+ */
299
+ export async function dueRoutines(now, me) {
300
+ const identity = me ?? (await localIdentity());
301
+ const out = [];
302
+ for (const m of await listAgents()) {
303
+ for (const r of await listRoutines(m.slug).catch(() => [])) {
304
+ if (!firesHere(r, identity))
305
+ continue;
306
+ try {
307
+ if (cronMatches(parseCron(r.schedule), now))
308
+ out.push({ slug: m.slug, routine: r });
309
+ }
310
+ catch {
311
+ // invalid schedule — surfaced by validation on save, skip here
312
+ }
313
+ }
314
+ }
315
+ return out;
316
+ }
257
317
  async function tick() {
258
318
  const now = new Date();
259
319
  const p = (n) => String(n).padStart(2, "0");
260
320
  const minuteKey = `${now.getFullYear()}-${p(now.getMonth() + 1)}-${p(now.getDate())}T${p(now.getHours())}:${p(now.getMinutes())}`;
261
- let agents;
321
+ let due;
262
322
  try {
263
- agents = await listAgents();
323
+ due = await dueRoutines(now);
264
324
  }
265
325
  catch {
266
326
  return;
267
327
  }
328
+ if (!due.length)
329
+ return;
268
330
  const state = await readState();
269
331
  let dirty = false;
270
- for (const m of agents) {
271
- for (const r of await listRoutines(m.slug).catch(() => [])) {
272
- if (!r.enabled)
273
- continue;
274
- let due = false;
275
- try {
276
- due = cronMatches(parseCron(r.schedule), now);
332
+ for (const { slug, routine: r } of due) {
333
+ const key = `${slug}/${r.id}`;
334
+ if (state[key]?.lastMinute === minuteKey)
335
+ continue;
336
+ state[key] = { ...state[key], lastMinute: minuteKey };
337
+ dirty = true;
338
+ try {
339
+ const chatId = await fireRoutine(slug, r);
340
+ state[key] = { ...state[key], lastRunAt: now.toISOString(), lastChatId: chatId };
341
+ delete state[key].lastError;
342
+ }
343
+ catch (err) {
344
+ state[key] = { ...state[key], lastError: err.message };
345
+ void notifyRoutineFailed(slug, r, err.message);
346
+ }
347
+ }
348
+ if (dirty)
349
+ await writeState(state);
350
+ }
351
+ /** Every routine in the workspace that has no runner, or names a retired identity of this machine. */
352
+ export async function runnerFindings() {
353
+ const id = await readIdentity();
354
+ const out = { unassigned: [], retired: [] };
355
+ let since = 0;
356
+ // Archived agents included: the scheduler fires their routines too.
357
+ for (const a of await listAgents()) {
358
+ const routines = await listRoutines(a.slug).catch(() => []);
359
+ let hit = false;
360
+ for (const r of routines) {
361
+ if (!r.runsOn && r.enabled) {
362
+ out.unassigned.push({ agent: a.slug, id: r.id, name: r.name });
363
+ hit = true;
277
364
  }
278
- catch {
279
- continue; // invalid schedule — surfaced by validation on save, skip here
365
+ else if (r.runsOn && id.previous.includes(r.runsOn)) {
366
+ out.retired.push({ agent: a.slug, id: r.id, name: r.name, from: r.runsOn });
367
+ hit = true;
280
368
  }
281
- const key = `${m.slug}/${r.id}`;
282
- if (!due || state[key]?.lastMinute === minuteKey)
369
+ }
370
+ if (hit) {
371
+ const st = await fs.stat(await routinesFile(a.slug)).catch(() => null);
372
+ if (st && st.mtimeMs > since)
373
+ since = st.mtimeMs;
374
+ }
375
+ }
376
+ if (since)
377
+ out.since = new Date(since).toISOString();
378
+ return out;
379
+ }
380
+ /**
381
+ * "Run these here": make this machine the runner of the routines `pick`
382
+ * selects, write routines.yml and commit just the files it touched — the
383
+ * commit is what tells the other clones who runs them. A workspace without
384
+ * git sync, or a commit that fails, keeps the written files and says why.
385
+ */
386
+ export async function adoptRoutines(pick, message) {
387
+ const id = await readIdentity();
388
+ const me = identityString(id);
389
+ const adopted = [];
390
+ const written = [];
391
+ for (const a of await listAgents()) {
392
+ const routines = await listRoutines(a.slug).catch(() => []);
393
+ let changed = false;
394
+ for (const r of routines) {
395
+ if (r.runsOn === me || !pick(a.slug, r, id.previous))
283
396
  continue;
284
- state[key] = { ...state[key], lastMinute: minuteKey };
285
- dirty = true;
286
- try {
287
- const chatId = await fireRoutine(m.slug, r);
288
- state[key] = { ...state[key], lastRunAt: now.toISOString(), lastChatId: chatId };
289
- delete state[key].lastError;
290
- }
291
- catch (err) {
292
- state[key] = { ...state[key], lastError: err.message };
293
- void notifyRoutineFailed(m.slug, r, err.message);
294
- }
397
+ adopted.push({ agent: a.slug, id: r.id, name: r.name, ...(r.runsOn ? { from: r.runsOn } : {}) });
398
+ r.runsOn = me;
399
+ changed = true;
400
+ }
401
+ if (changed) {
402
+ await writeRoutines(a.slug, routines);
403
+ written.push(await routinesFile(a.slug));
295
404
  }
296
405
  }
297
- if (dirty)
298
- await writeState(state);
406
+ const root = workspaceRoot();
407
+ const files = written.map((f) => path.relative(root, f).split(path.sep).join("/"));
408
+ if (!written.length)
409
+ return { runsOn: me, adopted, files, commit: null };
410
+ return { runsOn: me, adopted, files, commit: await commitFiles(written, message ?? `Routines run on ${me}: ${adopted.map((x) => `${x.agent}/${x.id}`).join(", ")}`) };
411
+ }
412
+ async function commitFiles(abs, message) {
413
+ const repoRoot = await repoRootFor(path.dirname(abs[0]));
414
+ if (!repoRoot)
415
+ return { ok: false, error: "the workspace is not a git repository" };
416
+ // rev-parse answers with symlinks resolved (/var → /private/var on macOS); the files must be spelled the same way.
417
+ const real = (p) => {
418
+ try {
419
+ return realpathSync.native(p);
420
+ }
421
+ catch {
422
+ return path.resolve(p);
423
+ }
424
+ };
425
+ const rel = abs.map((f) => path.relative(repoRoot, real(f)).split(path.sep).join("/"));
426
+ return gitCommit(rel, message);
427
+ }
428
+ /** Adopt one routine (whatever its runs_on said). */
429
+ export async function adoptRoutine(slug, id) {
430
+ const safe = safeAgentSlug(slug);
431
+ if (!(await listRoutines(safe)).some((r) => r.id === id))
432
+ throw new Error("routine not found");
433
+ const result = await adoptRoutines((agent, r) => agent === safe && r.id === id);
434
+ const routine = (await routineStatuses(safe)).find((r) => r.id === id);
435
+ return { ...result, routine };
436
+ }
437
+ /** Adopt every enabled routine without runs_on — and, with `retired`, every one naming a retired identity of this machine. */
438
+ export function adoptUnassigned(opts = {}) {
439
+ return adoptRoutines((_agent, r, previous) => (!r.runsOn && r.enabled) || (!!opts.retired && !!r.runsOn && previous.includes(r.runsOn)));
299
440
  }
300
441
  /** Start the current workspace's scheduler; safe to call more than once. Ticks run in that workspace. */
301
442
  export function startRoutineScheduler() {
@@ -5,7 +5,9 @@ export declare const agentRoutes: (import("./router.js").Route<"agents.addJourna
5
5
  }, "/api/agents/:slug/journal", z.ZodObject<{
6
6
  entry: z.ZodDefault<z.ZodString>;
7
7
  kind: z.ZodOptional<z.ZodString>;
8
- }, z.core.$strip>> | import("./router.js").Route<"agents.archive", import("../../core/agents.js").AgentDetail, "/api/agents/:slug/archive", z.ZodObject<{
8
+ }, z.core.$strip>> | import("./router.js").Route<"agents.adoptRoutine", import("../../core/routines.js").AdoptResult & {
9
+ routine: import("../../core/routines.js").RoutineStatus;
10
+ }, "/api/agents/:slug/routines/:id/adopt", undefined> | import("./router.js").Route<"agents.archive", import("../../core/agents.js").AgentDetail, "/api/agents/:slug/archive", z.ZodObject<{
9
11
  archived: z.ZodDefault<z.ZodBoolean>;
10
12
  }, z.core.$strip>> | import("./router.js").Route<"agents.create", import("../../core/agents.js").AgentDetail, "/api/agents", z.ZodObject<{
11
13
  name: z.ZodOptional<z.ZodString>;
@@ -56,6 +58,8 @@ export declare const agentRoutes: (import("./router.js").Route<"agents.addJourna
56
58
  schedule: z.ZodOptional<z.ZodString>;
57
59
  prompt: z.ZodOptional<z.ZodString>;
58
60
  enabled: z.ZodOptional<z.ZodBoolean>;
61
+ runsOn: z.ZodOptional<z.ZodNullable<z.ZodString>>;
62
+ private: z.ZodOptional<z.ZodBoolean>;
59
63
  }, z.core.$strip>> | import("./router.js").Route<"agents.saveSkill", import("../../core/skills.js").SkillInfo & {
60
64
  content: string;
61
65
  }, "/api/agents/:slug/skills", z.ZodObject<{
@@ -2,7 +2,7 @@ import * as z from "zod";
2
2
  import { agentsRoot, deleteAgent, getAgent, listAgents, saveAgent, setAgentArchived } from "../../core/agents.js";
3
3
  import { appendJournal, readJournal } from "../../core/journal.js";
4
4
  import { UI_ACTOR } from "../../core/knowledge.js";
5
- import { deleteRoutine, routineStatuses, runRoutineNow, saveRoutine } from "../../core/routines.js";
5
+ import { adoptRoutine, deleteRoutine, listRoutines, routineStatuses, runRoutineNow, saveRoutine } from "../../core/routines.js";
6
6
  import { deleteAgentSkill, listAgentSkills, readSkill, saveAgentSkill } from "../../core/skills.js";
7
7
  import { fail, route } from "./router.js";
8
8
  /** A member's editable fields. Loose: saveAgent checks values (name, harness, effort) and keeps what it knows. */
@@ -21,13 +21,19 @@ const AgentFields = z.looseObject({
21
21
  skills: z.array(z.string()).optional(),
22
22
  system: z.string().optional(),
23
23
  });
24
- /** A routine: every field saveRoutine stores (it builds the routine from these alone), so a strict object loses nothing. saveRoutine checks name, prompt and the cron schedule. */
24
+ /**
25
+ * A routine: every field saveRoutine stores (it builds the routine from these alone), so a strict object loses nothing.
26
+ * saveRoutine checks name, prompt, the cron schedule and runsOn. runsOn absent keeps the stored runner (a new routine:
27
+ * this machine); null or "" unassigns it.
28
+ */
25
29
  const RoutineFields = z.object({
26
30
  id: z.string().optional(),
27
31
  name: z.string().optional(),
28
32
  schedule: z.string().optional(),
29
33
  prompt: z.string().optional(),
30
34
  enabled: z.boolean().optional(),
35
+ runsOn: z.string().nullable().optional(),
36
+ private: z.boolean().optional(),
31
37
  });
32
38
  /** Agents (team members): definition, own skills, journal, routines. */
33
39
  export const agentRoutes = [
@@ -92,5 +98,16 @@ export const agentRoutes = [
92
98
  await deleteRoutine(c.params.slug, c.params.id);
93
99
  return { ok: true };
94
100
  }),
101
+ route({
102
+ name: "agents.adoptRoutine",
103
+ method: "POST",
104
+ path: "/api/agents/:slug/routines/:id/adopt",
105
+ summary: "run this routine here: runs_on = this machine's identity, routines.yml committed",
106
+ errors: 400,
107
+ }, async (c) => {
108
+ if (!(await listRoutines(c.params.slug)).some((r) => r.id === c.params.id))
109
+ fail(404, "routine not found");
110
+ return adoptRoutine(c.params.slug, c.params.id);
111
+ }),
95
112
  route({ name: "agents.runRoutine", method: "POST", path: "/api/agents/:slug/routines/:id/run", summary: "run a routine now", errors: 400 }, async (c) => runRoutineNow(c.params.slug, c.params.id)),
96
113
  ];
@@ -0,0 +1,72 @@
1
+ import * as z from "zod";
2
+ /**
3
+ * The machine in the kraftwerk cloud (server/cloud.ts): its record, turning
4
+ * it on or off, a round now, releasing the claim. What `kraftwerk cloud`,
5
+ * `login` and `logout` ask the running daemon; a standalone server answers
6
+ * "off" (only the daemon registers). This machine only: the claim code
7
+ * claims the machine.
8
+ */
9
+ export declare const cloudRoutes: (import("./router.js").Route<"cloud.set", {
10
+ daemon: boolean;
11
+ url: string;
12
+ state: import("../cloud.js").CloudState;
13
+ reason?: string;
14
+ source?: import("../../core/cloud.js").CloudUrlSource;
15
+ error?: string;
16
+ id?: string;
17
+ machine?: string;
18
+ account?: string | null;
19
+ interval?: number;
20
+ lastSeen?: string;
21
+ claimCode?: string | null;
22
+ claimUrl?: string | null;
23
+ workspaces?: number;
24
+ }, "/api/cloud", z.ZodObject<{
25
+ enabled: z.ZodBoolean;
26
+ url: z.ZodOptional<z.ZodString>;
27
+ }, z.core.$strip>> | import("./router.js").Route<"cloud.status", {
28
+ daemon: boolean;
29
+ url: string;
30
+ state: import("../cloud.js").CloudState;
31
+ reason?: string;
32
+ source?: import("../../core/cloud.js").CloudUrlSource;
33
+ error?: string;
34
+ id?: string;
35
+ machine?: string;
36
+ account?: string | null;
37
+ interval?: number;
38
+ lastSeen?: string;
39
+ claimCode?: string | null;
40
+ claimUrl?: string | null;
41
+ workspaces?: number;
42
+ }, "/api/cloud", undefined> | import("./router.js").Route<"cloud.sync", {
43
+ daemon: boolean;
44
+ url: string;
45
+ state: import("../cloud.js").CloudState;
46
+ reason?: string;
47
+ source?: import("../../core/cloud.js").CloudUrlSource;
48
+ error?: string;
49
+ id?: string;
50
+ machine?: string;
51
+ account?: string | null;
52
+ interval?: number;
53
+ lastSeen?: string;
54
+ claimCode?: string | null;
55
+ claimUrl?: string | null;
56
+ workspaces?: number;
57
+ }, "/api/cloud/sync", undefined> | import("./router.js").Route<"cloud.unlink", {
58
+ daemon: boolean;
59
+ url: string;
60
+ state: import("../cloud.js").CloudState;
61
+ reason?: string;
62
+ source?: import("../../core/cloud.js").CloudUrlSource;
63
+ error?: string;
64
+ id?: string;
65
+ machine?: string;
66
+ account?: string | null;
67
+ interval?: number;
68
+ lastSeen?: string;
69
+ claimCode?: string | null;
70
+ claimUrl?: string | null;
71
+ workspaces?: number;
72
+ }, "/api/cloud/unlink", undefined>)[];
@@ -0,0 +1,38 @@
1
+ import * as z from "zod";
2
+ import { writeCloudFile } from "../../core/cloud.js";
3
+ import { applyMachineCloud, cloudStatus, syncMachineCloud, unlinkMachineCloud } from "../cloud.js";
4
+ import { currentHub } from "../hub-context.js";
5
+ import { route } from "./router.js";
6
+ /**
7
+ * The machine in the kraftwerk cloud (server/cloud.ts): its record, turning
8
+ * it on or off, a round now, releasing the claim. What `kraftwerk cloud`,
9
+ * `login` and `logout` ask the running daemon; a standalone server answers
10
+ * "off" (only the daemon registers). This machine only: the claim code
11
+ * claims the machine.
12
+ */
13
+ export const cloudRoutes = [
14
+ route({ name: "cloud.status", method: "GET", path: "/api/cloud", summary: "the machine's cloud record: state, claim code, account, last heartbeat", access: "local" }, async () => ({
15
+ daemon: currentHub().daemon === true,
16
+ ...cloudStatus(),
17
+ })),
18
+ route({
19
+ name: "cloud.set",
20
+ method: "POST",
21
+ path: "/api/cloud",
22
+ summary: "turn the machine's cloud on or off {enabled, url?} (~/.kraftwerk/cloud.json); off says goodbye",
23
+ access: "local",
24
+ body: z.object({ enabled: z.boolean(), url: z.string().url().optional() }),
25
+ }, async (c) => {
26
+ const { enabled, url } = await c.body();
27
+ await writeCloudFile({ enabled, url });
28
+ return { daemon: currentHub().daemon === true, ...(await applyMachineCloud()) };
29
+ }),
30
+ route({ name: "cloud.sync", method: "POST", path: "/api/cloud/sync", summary: "register or heartbeat now; answers the record (a claim made in the cloud shows here)", access: "local" }, async () => ({
31
+ daemon: currentHub().daemon === true,
32
+ ...(await syncMachineCloud()),
33
+ })),
34
+ route({ name: "cloud.unlink", method: "POST", path: "/api/cloud/unlink", summary: "release the machine from its account; the cloud hands out a fresh claim code", access: "local", errors: 400 }, async () => ({
35
+ daemon: currentHub().daemon === true,
36
+ ...(await unlinkMachineCloud()),
37
+ })),
38
+ ];
@@ -16,16 +16,16 @@ export declare const deviceRoutes: (import("./router.js").Route<"devices.code",
16
16
  code: z.ZodString;
17
17
  name: z.ZodDefault<z.ZodString>;
18
18
  }, z.core.$strip>> | import("./router.js").Route<"devices.relay", {
19
+ enabled: boolean;
20
+ url: string | null;
21
+ fingerprint: string | null;
22
+ daemon: boolean;
19
23
  state: import("../relay.js").RelayState;
20
24
  id?: string;
21
25
  key?: string;
22
26
  error?: string;
23
27
  clients: number;
24
28
  since?: string;
25
- enabled: boolean;
26
- url: string | null;
27
- fingerprint: string | null;
28
- daemon: boolean;
29
29
  }, "/api/devices/relay", undefined> | import("./router.js").Route<"devices.revoke", {
30
30
  ok: boolean;
31
31
  }, "/api/devices/:id", undefined> | import("./router.js").Route<"devices.self", {
@@ -41,16 +41,16 @@ export declare const deviceRoutes: (import("./router.js").Route<"devices.code",
41
41
  }>;
42
42
  key: z.ZodString;
43
43
  }, z.core.$strip>> | import("./router.js").Route<"devices.setRelay", {
44
+ enabled: boolean;
45
+ url: string;
46
+ fingerprint: string;
47
+ daemon: boolean;
44
48
  state: import("../relay.js").RelayState;
45
49
  id?: string;
46
50
  key?: string;
47
51
  error?: string;
48
52
  clients: number;
49
53
  since?: string;
50
- enabled: boolean;
51
- url: string;
52
- fingerprint: string;
53
- daemon: boolean;
54
54
  }, "/api/devices/relay", z.ZodObject<{
55
55
  enabled: z.ZodBoolean;
56
56
  url: z.ZodOptional<z.ZodString>;