@netnodeag/kraftwerk 0.73.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.
@@ -40,6 +40,7 @@ export declare const ROUTES: {
40
40
  readonly "chats.permission": readonly ["POST", "/api/chats/:id/permission"];
41
41
  readonly "chats.rename": readonly ["PATCH", "/api/chats/:id"];
42
42
  readonly "chats.resetSession": readonly ["POST", "/api/chats/:id/reset-session"];
43
+ readonly "chats.setProject": readonly ["POST", "/api/chats/:id/project"];
43
44
  readonly "chats.setVibeable": readonly ["POST", "/api/chats/:id/vibeable"];
44
45
  readonly "chats.steer": readonly ["POST", "/api/chats/:id/steer"];
45
46
  readonly "chats.stopTask": readonly ["POST", "/api/chats/:id/task-stop"];
@@ -41,6 +41,7 @@ export const ROUTES = {
41
41
  "chats.permission": ["POST", "/api/chats/:id/permission"],
42
42
  "chats.rename": ["PATCH", "/api/chats/:id"],
43
43
  "chats.resetSession": ["POST", "/api/chats/:id/reset-session"],
44
+ "chats.setProject": ["POST", "/api/chats/:id/project"],
44
45
  "chats.setVibeable": ["POST", "/api/chats/:id/vibeable"],
45
46
  "chats.steer": ["POST", "/api/chats/:id/steer"],
46
47
  "chats.stopTask": ["POST", "/api/chats/:id/task-stop"],
@@ -16,6 +16,8 @@ export declare function createChat(opts: {
16
16
  title?: string;
17
17
  /** Continue one of the agent's own sessions (from listAgentSessionsFor) instead of starting fresh. */
18
18
  resume?: string;
19
+ /** The project an agent's chat works in (meta.project); the caller checks that it exists. */
20
+ project?: string;
19
21
  }): Promise<ChatMeta>;
20
22
  /** A permission request is waiting for a human in this chat (live state only — a restart drops it with the backend). */
21
23
  export declare function chatAwaitingApproval(id: string): boolean;
@@ -153,6 +155,17 @@ export declare function renameChat(id: string, title: unknown): Promise<{
153
155
  meta?: ChatMeta;
154
156
  error?: string;
155
157
  }>;
158
+ /**
159
+ * File an agent's chat under a project, or under none (`null`). The chat is
160
+ * then listed with the project, and the agent reads the project's context
161
+ * with its next message. Project chats and channels belong where
162
+ * their scope says and cannot move; the caller checks that the project exists.
163
+ */
164
+ export declare function setChatProject(id: string, project: string | null): Promise<{
165
+ meta?: ChatMeta;
166
+ error?: string;
167
+ status?: number;
168
+ }>;
156
169
  export declare function deleteChat(id: string): Promise<{
157
170
  error?: string;
158
171
  }>;
@@ -43,6 +43,7 @@ export async function createChat(opts) {
43
43
  title: opts.title ?? "",
44
44
  cwd,
45
45
  scope: opts.scope,
46
+ ...(opts.project && opts.scope.kind === "agent" ? { project: opts.project } : {}),
46
47
  ...(opts.resume && opts.agent !== "pi" ? { sessions: { [MAIN]: opts.resume } } : {}),
47
48
  createdAt: now,
48
49
  updatedAt: now,
@@ -478,6 +479,27 @@ export async function renameChat(id, title) {
478
479
  await writeMeta(state.meta);
479
480
  return { meta: state.meta };
480
481
  }
482
+ /**
483
+ * File an agent's chat under a project, or under none (`null`). The chat is
484
+ * then listed with the project, and the agent reads the project's context
485
+ * with its next message. Project chats and channels belong where
486
+ * their scope says and cannot move; the caller checks that the project exists.
487
+ */
488
+ export async function setChatProject(id, project) {
489
+ const state = await loadState(id);
490
+ if (!state)
491
+ return { error: "not found", status: 404 };
492
+ if (state.meta.scope.kind !== "agent")
493
+ return { error: "only an agent's chat can be filed under a project", status: 409 };
494
+ if (project)
495
+ state.meta.project = project;
496
+ else
497
+ delete state.meta.project;
498
+ for (const seat of state.seats.values())
499
+ seat.needsContext = true;
500
+ await writeMeta(state.meta);
501
+ return { meta: state.meta };
502
+ }
481
503
  export async function deleteChat(id) {
482
504
  const state = await loadState(id);
483
505
  if (!state)
@@ -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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@netnodeag/kraftwerk",
3
- "version": "0.73.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",