@netnodeag/kraftwerk 0.72.0 → 0.74.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 (37) hide show
  1. package/README.md +6 -0
  2. package/dist/cli/daemon-control.d.ts +15 -0
  3. package/dist/cli/daemon-control.js +228 -0
  4. package/dist/cli/daemon-login.d.ts +2 -0
  5. package/dist/cli/daemon-login.js +9 -2
  6. package/dist/cli/daemon.js +48 -2
  7. package/dist/cli/doctor.js +3 -1
  8. package/dist/cli/ui.js +33 -11
  9. package/dist/client/routes.d.ts +1 -0
  10. package/dist/client/routes.js +1 -0
  11. package/dist/core/chat/sessions.d.ts +13 -0
  12. package/dist/core/chat/sessions.js +22 -0
  13. package/dist/core/daemon.d.ts +16 -0
  14. package/dist/core/daemon.js +18 -1
  15. package/dist/core/skills.js +4 -2
  16. package/dist/server/api/chats.d.ts +4 -1
  17. package/dist/server/api/chats.js +29 -3
  18. package/dist/server/api/index.d.ts +4 -1
  19. package/dist/server/live.d.ts +4 -2
  20. package/dist/server/live.js +3 -2
  21. package/dist/server/server.js +2 -1
  22. package/package.json +1 -1
  23. package/web/dist/assets/{dist-LUiVqSl6.js → dist-BJQuUDA0.js} +1 -1
  24. package/web/dist/assets/{dist-Bu0rxkzF.js → dist-CESw77ha.js} +1 -1
  25. package/web/dist/assets/dist-CMMQorId.js +1 -0
  26. package/web/dist/assets/{dist-CYnXUgv0.js → dist-CRrnyH8Z.js} +1 -1
  27. package/web/dist/assets/{dist-BLd_aA1g.js → dist-CaGkA8bH.js} +1 -1
  28. package/web/dist/assets/{dist-C8xoYPYE.js → dist-Cnm1S16W.js} +1 -1
  29. package/web/dist/assets/{dist-CVuec2Mm.js → dist-D746Qhyu.js} +1 -1
  30. package/web/dist/assets/{dist-H3OFzCSU.js → dist-Df4EJMUZ.js} +1 -1
  31. package/web/dist/assets/{dist-GJo81x50.js → dist-INPpBMuI.js} +1 -1
  32. package/web/dist/assets/{dist-WCCGMh5F.js → dist-XZFWBcL9.js} +1 -1
  33. package/web/dist/assets/{dist-DFZgINkL.js → dist-r61B6qLC.js} +1 -1
  34. package/web/dist/assets/{editor-6D-WNjZY.js → editor-ByOSpxTX.js} +3 -3
  35. package/web/dist/assets/{index-D8-2MY0Q.js → index-CJTkdJMe.js} +3 -3
  36. package/web/dist/index.html +1 -1
  37. package/web/dist/assets/dist-D-uSPljm.js +0 -1
@@ -33,6 +33,7 @@ export declare const chatRoutes: (import("./router.js").Route<"agents.sessions",
33
33
  slug: z.ZodOptional<z.ZodString>;
34
34
  }, z.core.$strip>>;
35
35
  resume: z.ZodOptional<z.ZodString>;
36
+ project: z.ZodOptional<z.ZodString>;
36
37
  }, z.core.$strip>> | import("./router.js").Route<"chats.delete", import("./router.js").Reply<{
37
38
  error?: string;
38
39
  }>, "/api/chats/:id", undefined> | import("./router.js").Route<"chats.elicitation", import("./router.js").Reply<{
@@ -80,7 +81,9 @@ export declare const chatRoutes: (import("./router.js").Route<"agents.sessions",
80
81
  error?: string;
81
82
  }>, "/api/chats/:id", z.ZodObject<{
82
83
  title: z.ZodString;
83
- }, z.core.$strip>> | import("./router.js").Route<"chats.resetSession", import("../../core/chat/types.js").ChatMeta, "/api/chats/:id/reset-session", undefined> | import("./router.js").Route<"chats.setVibeable", import("../../core/chat/types.js").ChatMeta, "/api/chats/:id/vibeable", z.ZodObject<{
84
+ }, z.core.$strip>> | import("./router.js").Route<"chats.resetSession", import("../../core/chat/types.js").ChatMeta, "/api/chats/:id/reset-session", undefined> | import("./router.js").Route<"chats.setProject", import("../../core/chat/types.js").ChatMeta, "/api/chats/:id/project", z.ZodObject<{
85
+ project: z.ZodNullable<z.ZodString>;
86
+ }, z.core.$strip>> | import("./router.js").Route<"chats.setVibeable", import("../../core/chat/types.js").ChatMeta, "/api/chats/:id/vibeable", z.ZodObject<{
84
87
  slug: z.ZodOptional<z.ZodNullable<z.ZodString>>;
85
88
  }, z.core.$strip>> | import("./router.js").Route<"chats.steer", import("./router.js").Reply<{
86
89
  error: string;
@@ -2,7 +2,7 @@ import { promises as fs } from "node:fs";
2
2
  import path from "node:path";
3
3
  import * as z from "zod";
4
4
  import { getAgent } from "../../core/agents.js";
5
- import { answerElicitation, cancelChat, createChat, deleteChat, forkChat, getChat, listAgentSessionsFor, listChats, postMessage, renameChat, resetSession, resolvePermission, setChatConfig, setChatVibeable, steerChat, stopChatTask, subscribeChat, } from "../../core/chat/sessions.js";
5
+ import { answerElicitation, cancelChat, createChat, deleteChat, forkChat, getChat, listAgentSessionsFor, listChats, postMessage, renameChat, resetSession, resolvePermission, setChatConfig, setChatProject, setChatVibeable, steerChat, stopChatTask, subscribeChat, } from "../../core/chat/sessions.js";
6
6
  import { attachmentPath, saveAttachment } from "../../core/chat/store.js";
7
7
  import { getProject } from "../../core/projects.js";
8
8
  import { MIME } from "./mime.js";
@@ -25,6 +25,8 @@ const NewChat = z.object({
25
25
  agent: z.string().optional(),
26
26
  scope: z.object({ kind: z.string(), runId: z.string().optional(), bundle: z.string().optional(), slug: z.string().optional() }).optional(),
27
27
  resume: z.string().optional(),
28
+ /** An agent's chat started in a project: listed with the project (meta.project). */
29
+ project: z.string().optional(),
28
30
  });
29
31
  /** An action on a running chat: `{ok}` when it went through, 409 with the reason when the chat refused. */
30
32
  const act = (name, path, summary, body, run) => route({ name, method: "POST", path, summary, errors: 400, body }, async (c) => {
@@ -64,17 +66,29 @@ async function scopeOf(body) {
64
66
  return { agent, scope: { kind: "knowledge", ...(s.bundle ? { bundle: s.bundle } : {}) } };
65
67
  return { agent, scope: { kind: "general" } };
66
68
  }
69
+ /** The slug of an existing project, else a 404 (or the error reading it). */
70
+ async function projectSlug(slug) {
71
+ const found = await getProject(slug).catch((err) => {
72
+ throw new ApiError(projectErrors(err.message), err.message);
73
+ });
74
+ if (!found)
75
+ fail(404, "project not found");
76
+ return found.slug;
77
+ }
67
78
  /** Chats: ACP sessions with a coding agent, their event stream, and what a person does in them. */
68
79
  export const chatRoutes = [
69
80
  route({ name: "chats.list", method: "GET", path: "/api/chats", summary: "every chat with its live state" }, async () => ({ chats: await listChats() })),
70
- route({ name: "chats.create", method: "POST", path: "/api/chats", summary: "start a chat {agent, scope, resume?}", body: NewChat }, async (c) => {
81
+ route({ name: "chats.create", method: "POST", path: "/api/chats", summary: "start a chat {agent, scope, resume?, project?}", body: NewChat }, async (c) => {
71
82
  // `resume` continues one of the agent's own sessions (see agents.sessions).
72
83
  const body = await c.body();
73
84
  const { agent, scope } = await scopeOf(body);
74
85
  if (!AGENTS.includes(agent))
75
86
  fail(400, "agent must be claude, codex, or pi");
87
+ if (body.project && scope.kind !== "agent")
88
+ fail(400, "only an agent's chat takes a project");
89
+ const project = body.project ? await projectSlug(body.project) : undefined;
76
90
  try {
77
- return await createChat({ agent: agent, scope, ...(body.resume ? { resume: body.resume } : {}) });
91
+ return await createChat({ agent: agent, scope, ...(project ? { project } : {}), ...(body.resume ? { resume: body.resume } : {}) });
78
92
  }
79
93
  catch (err) {
80
94
  throw new ApiError(400, err.message);
@@ -96,6 +110,18 @@ export const chatRoutes = [
96
110
  const result = await validChat(renameChat(c.params.id, title), "invalid chat id or body");
97
111
  return reply(result.error ? (result.error === "not found" ? 404 : 400) : 200, result);
98
112
  }),
113
+ route({
114
+ name: "chats.setProject",
115
+ method: "POST",
116
+ path: "/api/chats/:id/project",
117
+ summary: "file an agent's chat under a project {project}, or under none {project: null}",
118
+ errors: 400,
119
+ body: z.object({ project: z.string().nullable() }),
120
+ }, async (c) => {
121
+ const { project } = await c.body();
122
+ const slug = project ? await projectSlug(project) : null;
123
+ return metaOf(await validChat(setChatProject(c.params.id, slug)));
124
+ }),
99
125
  // Replays from ?after=N, then streams new events as they happen.
100
126
  route({ name: "chats.events", method: "GET", path: "/api/chats/:id/events", summary: "SSE: the chat's events from ?after=N", raw: true }, async ({ req, res, params, query }) => {
101
127
  const afterSeq = Number(query.get("after") ?? "0") || 0;
@@ -218,6 +218,7 @@ export declare const routes: (Route<"agents.addJournal", {
218
218
  slug: z.ZodOptional<z.ZodString>;
219
219
  }, z.core.$strip>>;
220
220
  resume: z.ZodOptional<z.ZodString>;
221
+ project: z.ZodOptional<z.ZodString>;
221
222
  }, z.core.$strip>> | Route<"chats.delete", import("./router.js").Reply<{
222
223
  error?: string;
223
224
  }>, "/api/chats/:id", undefined> | Route<"chats.elicitation", import("./router.js").Reply<{
@@ -265,7 +266,9 @@ export declare const routes: (Route<"agents.addJournal", {
265
266
  error?: string;
266
267
  }>, "/api/chats/:id", z.ZodObject<{
267
268
  title: z.ZodString;
268
- }, z.core.$strip>> | Route<"chats.resetSession", import("../../core/chat/types.js").ChatMeta, "/api/chats/:id/reset-session", undefined> | Route<"chats.setVibeable", import("../../core/chat/types.js").ChatMeta, "/api/chats/:id/vibeable", z.ZodObject<{
269
+ }, z.core.$strip>> | Route<"chats.resetSession", import("../../core/chat/types.js").ChatMeta, "/api/chats/:id/reset-session", undefined> | Route<"chats.setProject", import("../../core/chat/types.js").ChatMeta, "/api/chats/:id/project", z.ZodObject<{
270
+ project: z.ZodNullable<z.ZodString>;
271
+ }, z.core.$strip>> | Route<"chats.setVibeable", import("../../core/chat/types.js").ChatMeta, "/api/chats/:id/vibeable", z.ZodObject<{
269
272
  slug: z.ZodOptional<z.ZodNullable<z.ZodString>>;
270
273
  }, z.core.$strip>> | Route<"chats.steer", import("./router.js").Reply<{
271
274
  error: string;
@@ -9,8 +9,10 @@
9
9
  * - and on the watch's interval, for changes nobody announces (an agent
10
10
  * writing files, a run progressing on disk).
11
11
  *
12
- * Evaluation runs in the watch's workspace. This module knows nothing about
13
- * routes: the socket hands it an `evaluate` closure.
12
+ * Evaluation runs in the watch's workspace — bound when the watch is made,
13
+ * so a re-evaluation whatever triggers it (a timer, a change elsewhere)
14
+ * never runs without one. This module knows nothing about routes: the
15
+ * socket hands it an `evaluate` closure.
14
16
  */
15
17
  interface Watcher {
16
18
  interval: number;
@@ -1,5 +1,5 @@
1
1
  import { onChanged } from "../core/changes.js";
2
- import { perWorkspace } from "../core/workspace.js";
2
+ import { currentWorkspace, perWorkspace } from "../core/workspace.js";
3
3
  const MIN_INTERVAL = 1000;
4
4
  const TOUCH_DEBOUNCE_MS = 60;
5
5
  const state = perWorkspace(() => ({ watches: new Map(), touchTimer: undefined }));
@@ -52,7 +52,8 @@ export function watch(key, evaluate, interval, push) {
52
52
  const { watches } = state();
53
53
  let w = watches.get(key);
54
54
  if (!w) {
55
- w = { evaluate, watchers: new Set() };
55
+ const ws = currentWorkspace();
56
+ w = { evaluate: () => ws.run(evaluate), watchers: new Set() };
56
57
  watches.set(key, w);
57
58
  }
58
59
  const watcher = { interval, push };
@@ -136,6 +136,7 @@ function installExitHandlers() {
136
136
  // Something still running for a workspace that was closed (a late callback) finds its state gone and throws
137
137
  // "workspace closed". That must not take the other workspaces down with it; anything else still crashes, as by default.
138
138
  const closedOnly = (err) => err instanceof Error && err.message === "workspace closed";
139
+ // Logged with the time, so `kraftwerk daemon logs` says when; the supervisor starts the server again.
139
140
  process.on("unhandledRejection", (err) => {
140
141
  if (closedOnly(err))
141
142
  return;
@@ -144,7 +145,7 @@ function installExitHandlers() {
144
145
  process.on("uncaughtException", (err) => {
145
146
  if (closedOnly(err))
146
147
  return;
147
- console.error(err);
148
+ console.error(`✖ ${new Date().toISOString()} crashed:`, err);
148
149
  process.exit(1);
149
150
  });
150
151
  // Chat agent subprocesses must die with the server — signals bypass "exit" handlers, so hook the signals themselves.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@netnodeag/kraftwerk",
3
- "version": "0.72.0",
3
+ "version": "0.74.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",