@netnodeag/kraftwerk 0.62.0 → 0.63.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 (73) hide show
  1. package/dist/cli/routines.js +2 -3
  2. package/dist/client/index.d.ts +59 -0
  3. package/dist/client/index.js +63 -0
  4. package/dist/client/live.d.ts +34 -0
  5. package/dist/client/live.js +169 -0
  6. package/dist/client/routes.d.ts +125 -0
  7. package/dist/client/routes.js +126 -0
  8. package/dist/inspector/api/agents.d.ts +31 -0
  9. package/dist/inspector/api/agents.js +50 -0
  10. package/dist/inspector/api/attention.d.ts +10 -0
  11. package/dist/inspector/api/attention.js +23 -0
  12. package/dist/inspector/api/channels.d.ts +67 -0
  13. package/dist/inspector/api/channels.js +58 -0
  14. package/dist/inspector/api/chats.d.ts +55 -0
  15. package/dist/inspector/api/chats.js +154 -0
  16. package/dist/inspector/api/files.d.ts +15 -0
  17. package/dist/inspector/api/files.js +43 -0
  18. package/dist/inspector/api/git.d.ts +25 -0
  19. package/dist/inspector/api/git.js +56 -0
  20. package/dist/inspector/api/index.d.ts +334 -0
  21. package/dist/inspector/api/index.js +43 -0
  22. package/dist/inspector/api/knowledge.d.ts +13 -0
  23. package/dist/inspector/api/knowledge.js +56 -0
  24. package/dist/inspector/api/mime.d.ts +2 -0
  25. package/dist/inspector/api/mime.js +18 -0
  26. package/dist/inspector/api/projects.d.ts +11 -0
  27. package/dist/inspector/api/projects.js +32 -0
  28. package/dist/inspector/api/router.d.ts +75 -0
  29. package/dist/inspector/api/router.js +186 -0
  30. package/dist/inspector/api/runs.d.ts +33 -0
  31. package/dist/inspector/api/runs.js +105 -0
  32. package/dist/inspector/api/socket.d.ts +3 -0
  33. package/dist/inspector/api/socket.js +115 -0
  34. package/dist/inspector/api/vibeables.d.ts +4 -0
  35. package/dist/inspector/api/vibeables.js +31 -0
  36. package/dist/inspector/api/workspace.d.ts +55 -0
  37. package/dist/inspector/api/workspace.js +117 -0
  38. package/dist/inspector/chat/sessions.js +31 -19
  39. package/dist/inspector/context.d.ts +5 -7
  40. package/dist/inspector/context.js +6 -20
  41. package/dist/inspector/git.js +52 -43
  42. package/dist/inspector/live.d.ts +42 -0
  43. package/dist/inspector/live.js +91 -0
  44. package/dist/inspector/notifications.js +19 -13
  45. package/dist/inspector/routines.d.ts +1 -1
  46. package/dist/inspector/routines.js +8 -5
  47. package/dist/inspector/runner.js +5 -4
  48. package/dist/inspector/server.d.ts +2 -2
  49. package/dist/inspector/server.js +59 -1393
  50. package/dist/inspector/version.d.ts +9 -0
  51. package/dist/inspector/version.js +27 -0
  52. package/dist/inspector/vibeables.js +25 -23
  53. package/dist/inspector/workspace.d.ts +42 -0
  54. package/dist/inspector/workspace.js +63 -0
  55. package/dist/inspector/ws.d.ts +11 -0
  56. package/dist/inspector/ws.js +143 -0
  57. package/inspector/dist/assets/{dist-COwx246i.js → dist-9RESajfo.js} +1 -1
  58. package/inspector/dist/assets/{dist-OMwGczSn.js → dist-Bm5S1BoZ.js} +1 -1
  59. package/inspector/dist/assets/dist-C8BofvHx.js +1 -0
  60. package/inspector/dist/assets/{dist-jBYydzOe.js → dist-CIs4wnTu.js} +1 -1
  61. package/inspector/dist/assets/{dist-Crdu4xR8.js → dist-CjuETd-_.js} +1 -1
  62. package/inspector/dist/assets/{dist-ClyjyU-C.js → dist-CmVK6rgN.js} +1 -1
  63. package/inspector/dist/assets/{dist-CeGII4MF.js → dist-CryXvMV_.js} +1 -1
  64. package/inspector/dist/assets/{dist-DqU5Wgif.js → dist-D1hk0iC5.js} +1 -1
  65. package/inspector/dist/assets/{dist-BIHxko9t.js → dist-DvW9QE1d.js} +1 -1
  66. package/inspector/dist/assets/{dist-BMBdyFA5.js → dist-VD9jMxBv.js} +1 -1
  67. package/inspector/dist/assets/{dist-BnnTidDP.js → dist-VlAqWD8T.js} +1 -1
  68. package/inspector/dist/assets/{editor-Cmri6xKM.js → editor-DkWqwl89.js} +3 -3
  69. package/inspector/dist/assets/index-DRJS2pGj.js +229 -0
  70. package/inspector/dist/index.html +1 -1
  71. package/package.json +7 -2
  72. package/inspector/dist/assets/dist-wc-PzIzB.js +0 -1
  73. package/inspector/dist/assets/index-C1s81t9X.js +0 -229
@@ -0,0 +1,50 @@
1
+ import { agentsRoot, deleteAgent, getAgent, listAgents, saveAgent, setAgentArchived } from "../agents.js";
2
+ import { appendJournal, readJournal } from "../journal.js";
3
+ import { UI_ACTOR } from "../knowledge.js";
4
+ import { deleteRoutine, routineStatuses, runRoutineNow, saveRoutine } from "../routines.js";
5
+ import { deleteAgentSkill, listAgentSkills, readSkill, saveAgentSkill } from "../skills.js";
6
+ import { fail, route } from "./router.js";
7
+ /** Agents (team members): definition, own skills, journal, routines. */
8
+ export const agentRoutes = [
9
+ route({ name: "agents.list", method: "GET", path: "/api/agents", summary: "every member" }, async () => ({ root: await agentsRoot(), agents: await listAgents() })),
10
+ route({ name: "agents.create", method: "POST", path: "/api/agents", summary: "create a member", errors: 400 }, async (c) => saveAgent({ ...(await c.body()), slug: undefined })),
11
+ route({ name: "agents.get", method: "GET", path: "/api/agents/:slug", summary: "one member", errors: 400 }, async (c) => (await getAgent(c.params.slug)) ?? fail(404, "not found")),
12
+ route({ name: "agents.save", method: "PUT", path: "/api/agents/:slug", summary: "save a member", errors: 400 }, async (c) => saveAgent({ ...(await c.body()), slug: c.params.slug })),
13
+ route({ name: "agents.delete", method: "DELETE", path: "/api/agents/:slug", summary: "move a member to the trash", errors: 400 }, async (c) => {
14
+ await deleteAgent(c.params.slug);
15
+ return { ok: true };
16
+ }),
17
+ route({ name: "agents.archive", method: "POST", path: "/api/agents/:slug/archive", summary: "archive or unarchive {archived}", errors: 400 }, async (c) => setAgentArchived(c.params.slug, (await c.body()).archived === true)),
18
+ route({ name: "agents.skills", method: "GET", path: "/api/agents/:slug/skills", summary: "the agent's own skills", errors: 400 }, async (c) => ({
19
+ skills: await listAgentSkills(c.params.slug),
20
+ })),
21
+ route({ name: "agents.saveSkill", method: "POST", path: "/api/agents/:slug/skills", summary: "create or update one {name, content}", errors: 400 }, async (c) => {
22
+ const body = await c.body();
23
+ return saveAgentSkill(c.params.slug, String(body.name ?? ""), String(body.content ?? ""));
24
+ }),
25
+ route({ name: "agents.skill", method: "GET", path: "/api/agents/:slug/skills/:name", summary: "one agent skill with its content", errors: 400 }, async (c) => {
26
+ const name = c.params.name.toLowerCase();
27
+ const skill = (await listAgentSkills(c.params.slug)).find((s) => s.name.toLowerCase() === name);
28
+ if (!skill)
29
+ fail(404, "not found");
30
+ return { ...skill, content: await readSkill(skill) };
31
+ }),
32
+ route({ name: "agents.deleteSkill", method: "DELETE", path: "/api/agents/:slug/skills/:name", summary: "delete one agent skill", errors: 400 }, async (c) => {
33
+ await deleteAgentSkill(c.params.slug, c.params.name);
34
+ return { ok: true };
35
+ }),
36
+ route({ name: "agents.journal", method: "GET", path: "/api/agents/:slug/journal", summary: "the agent's journal", errors: 400 }, async (c) => ({ journal: await readJournal(c.params.slug) })),
37
+ route({ name: "agents.addJournal", method: "POST", path: "/api/agents/:slug/journal", summary: "a person adds a line {entry, kind?}", errors: 400 }, async (c) => {
38
+ const body = await c.body();
39
+ return { journal: await appendJournal(c.params.slug, String(body.entry ?? ""), { kind: body.kind, actor: UI_ACTOR }) };
40
+ }),
41
+ route({ name: "agents.routines", method: "GET", path: "/api/agents/:slug/routines", summary: "the agent's routines with run state", errors: 400 }, async (c) => ({
42
+ routines: await routineStatuses(c.params.slug),
43
+ })),
44
+ route({ name: "agents.saveRoutine", method: "POST", path: "/api/agents/:slug/routines", summary: "create or update a routine", errors: 400 }, async (c) => saveRoutine(c.params.slug, await c.body())),
45
+ route({ name: "agents.deleteRoutine", method: "DELETE", path: "/api/agents/:slug/routines/:id", summary: "delete a routine", errors: 400 }, async (c) => {
46
+ await deleteRoutine(c.params.slug, c.params.id);
47
+ return { ok: true };
48
+ }),
49
+ 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)),
50
+ ];
@@ -0,0 +1,10 @@
1
+ /** What needs a person: the bell, the attention list, the rail's per-agent status. */
2
+ export declare const attentionRoutes: (import("./router.js").Route<"agents.status", Record<string, import("../agent-status.js").AgentStatus>, "/api/agent-status"> | import("./router.js").Route<"attention.list", import("../attention.js").AttentionView, "/api/attention"> | import("./router.js").Route<"notifications.clear", {
3
+ ok: boolean;
4
+ }, "/api/notifications"> | import("./router.js").Route<"notifications.diagnose", import("./router.js").Reply<{
5
+ error: string;
6
+ status: number;
7
+ }> | {
8
+ chatId: string;
9
+ href: string;
10
+ }, "/api/notifications/:id/diagnose"> | import("./router.js").Route<"notifications.list", import("../notifications.js").NotificationsView, "/api/notifications"> | import("./router.js").Route<"notifications.read", import("../notifications.js").NotificationsView, "/api/notifications/read">)[];
@@ -0,0 +1,23 @@
1
+ import { agentStatuses } from "../agent-status.js";
2
+ import { listAttention } from "../attention.js";
3
+ import { startDiagnosis } from "../diagnose.js";
4
+ import { clearNotifications, listNotifications, markNotificationsRead } from "../notifications.js";
5
+ import { reply, route } from "./router.js";
6
+ /** What needs a person: the bell, the attention list, the rail's per-agent status. */
7
+ export const attentionRoutes = [
8
+ route({ name: "notifications.list", method: "GET", path: "/api/notifications", summary: "the bell: attention items, newest first, plus the unread count" }, async () => listNotifications()),
9
+ route({ name: "notifications.clear", method: "DELETE", path: "/api/notifications", summary: "clear the list" }, async () => {
10
+ await clearNotifications();
11
+ return { ok: true };
12
+ }),
13
+ route({ name: "notifications.read", method: "POST", path: "/api/notifications/read", summary: "mark some {ids?} or all read" }, async (c) => {
14
+ const body = await c.body();
15
+ return markNotificationsRead(Array.isArray(body.ids) ? body.ids.map(String) : "all");
16
+ }),
17
+ route({ name: "notifications.diagnose", method: "POST", path: "/api/notifications/:id/diagnose", summary: "open a chat that investigates the failure behind an item" }, async (c) => {
18
+ const result = await startDiagnosis(c.params.id);
19
+ return "error" in result ? reply(result.status, result) : result;
20
+ }),
21
+ route({ name: "attention.list", method: "GET", path: "/api/attention", summary: "waiting approvals and questions, unread failures, with their owner" }, async () => listAttention()),
22
+ route({ name: "agents.status", method: "GET", path: "/api/agent-status", summary: "per agent: waiting, working, next routine, last active" }, async () => agentStatuses()),
23
+ ];
@@ -0,0 +1,67 @@
1
+ /** Channels: one shared transcript (channels/<slug>/channel.yml + one chat) with several agents. */
2
+ export declare const channelRoutes: (import("./router.js").Route<"channels.create", {
3
+ slug: string;
4
+ name: string;
5
+ purpose?: string;
6
+ members: string[];
7
+ responder?: string;
8
+ maxHops: number;
9
+ project?: string;
10
+ chatId: string;
11
+ busy: boolean;
12
+ awaitingApproval: boolean;
13
+ updatedAt: string;
14
+ }, "/api/channels"> | import("./router.js").Route<"channels.delete", {
15
+ ok: boolean;
16
+ }, "/api/channels/:slug"> | import("./router.js").Route<"channels.fromChat", {
17
+ slug: string;
18
+ name: string;
19
+ purpose?: string;
20
+ members: string[];
21
+ responder?: string;
22
+ maxHops: number;
23
+ project?: string;
24
+ chatId: string;
25
+ busy: boolean;
26
+ awaitingApproval: boolean;
27
+ updatedAt: string;
28
+ }, "/api/channels/from-chat"> | import("./router.js").Route<"channels.get", {
29
+ slug: string;
30
+ name: string;
31
+ purpose?: string;
32
+ members: string[];
33
+ responder?: string;
34
+ maxHops: number;
35
+ project?: string;
36
+ chatId: string;
37
+ busy: boolean;
38
+ awaitingApproval: boolean;
39
+ updatedAt: string;
40
+ }, "/api/channels/:slug"> | import("./router.js").Route<"channels.list", {
41
+ root: string;
42
+ channels: {
43
+ slug: string;
44
+ name: string;
45
+ purpose?: string;
46
+ members: string[];
47
+ responder?: string;
48
+ maxHops: number;
49
+ project?: string;
50
+ chatId: string;
51
+ busy: boolean;
52
+ awaitingApproval: boolean;
53
+ updatedAt: string;
54
+ }[];
55
+ }, "/api/channels"> | import("./router.js").Route<"channels.save", {
56
+ slug: string;
57
+ name: string;
58
+ purpose?: string;
59
+ members: string[];
60
+ responder?: string;
61
+ maxHops: number;
62
+ project?: string;
63
+ chatId: string;
64
+ busy: boolean;
65
+ awaitingApproval: boolean;
66
+ updatedAt: string;
67
+ }, "/api/channels/:slug">)[];
@@ -0,0 +1,58 @@
1
+ import { channelsRoot, deleteChannel, getChannel, listChannels, saveChannel } from "../channels.js";
2
+ import { convertChatToChannel, deleteChat, dropChannelSeats, ensureChannelChat, listChats } from "../chat/sessions.js";
3
+ import { readMeta } from "../chat/store.js";
4
+ import { fail, route } from "./router.js";
5
+ /** A channel with its chat and that chat's live state. */
6
+ async function view(c) {
7
+ const meta = await ensureChannelChat(c);
8
+ const live = (await listChats()).find((x) => x.id === meta.id);
9
+ return { ...c, chatId: meta.id, busy: live?.busy ?? false, awaitingApproval: live?.awaitingApproval ?? false, updatedAt: meta.updatedAt };
10
+ }
11
+ async function existing(slug) {
12
+ return (await getChannel(slug)) ?? fail(404, "channel not found");
13
+ }
14
+ /** Channels: one shared transcript (channels/<slug>/channel.yml + one chat) with several agents. */
15
+ export const channelRoutes = [
16
+ route({ name: "channels.list", method: "GET", path: "/api/channels", summary: "every channel with its chat's state", errors: 400 }, async () => ({
17
+ root: await channelsRoot(),
18
+ channels: await Promise.all((await listChannels()).map(view)),
19
+ })),
20
+ route({ name: "channels.create", method: "POST", path: "/api/channels", summary: "create one {name, purpose?, members, responder?}", errors: 400 }, async (c) => {
21
+ const { chatId: _c, slug: _s, ...input } = await c.body();
22
+ return view(await saveChannel(input));
23
+ }),
24
+ // An agent session becomes a channel; a project chat's coworkers work in that project — the chat decides, not the body.
25
+ route({ name: "channels.fromChat", method: "POST", path: "/api/channels/from-chat", summary: "turn a chat into a channel {chatId, name, members, …}", errors: 400 }, async (c) => {
26
+ const { chatId, slug: _s, project: _p, ...input } = await c.body();
27
+ if (!chatId)
28
+ fail(400, "chatId is required");
29
+ const source = await readMeta(chatId).catch(() => null);
30
+ if (!source)
31
+ fail(404, "chat not found");
32
+ const channel = await saveChannel({ ...input, ...(source.scope.kind === "project" ? { project: source.scope.slug } : {}) });
33
+ const converted = await convertChatToChannel(chatId, channel);
34
+ if (converted.error) {
35
+ await deleteChannel(channel.slug).catch(() => { });
36
+ fail(409, converted.error);
37
+ }
38
+ return view(channel);
39
+ }),
40
+ route({ name: "channels.get", method: "GET", path: "/api/channels/:slug", summary: "one channel with its chat's state", errors: 400 }, async (c) => view(await existing(c.params.slug))),
41
+ route({ name: "channels.save", method: "PUT", path: "/api/channels/:slug", summary: "save a channel; members who left lose their seat", errors: 400 }, async (c) => {
42
+ const slug = c.params.slug;
43
+ await existing(slug);
44
+ const { chatId: _c, slug: _s, ...input } = await c.body();
45
+ const saved = await saveChannel({ ...input, slug });
46
+ await dropChannelSeats((await ensureChannelChat(saved)).id, saved.members);
47
+ return view(saved);
48
+ }),
49
+ route({ name: "channels.delete", method: "DELETE", path: "/api/channels/:slug", summary: "delete a channel and its chat", errors: 400 }, async (c) => {
50
+ const slug = c.params.slug;
51
+ await existing(slug);
52
+ const meta = (await listChats()).find((x) => x.scope.kind === "channel" && x.scope.slug === slug);
53
+ if (meta)
54
+ await deleteChat(meta.id);
55
+ await deleteChannel(slug);
56
+ return { ok: true };
57
+ }),
58
+ ];
@@ -0,0 +1,55 @@
1
+ /** Chats: ACP sessions with a coding agent, their event stream, and what a person does in them. */
2
+ export declare const chatRoutes: (import("./router.js").Route<"agents.sessions", {
3
+ sessions: {
4
+ sessionId: string;
5
+ title?: string;
6
+ updatedAt?: string;
7
+ cwd: string;
8
+ }[];
9
+ }, "/api/agent-sessions"> | import("./router.js").Route<"chats.attach", {
10
+ name: string;
11
+ mimeType: string;
12
+ size: number;
13
+ }, "/api/chats/:id/attachments"> | import("./router.js").Route<"chats.attachment", undefined, "/api/chats/:id/attachments/:name"> | import("./router.js").Route<"chats.cancel", import("./router.js").Reply<{
14
+ error: string;
15
+ }> | {
16
+ ok: true;
17
+ }, "/api/chats/:id/cancel"> | import("./router.js").Route<"chats.config", import("./router.js").Reply<{
18
+ error: string;
19
+ }> | {
20
+ ok: true;
21
+ }, "/api/chats/:id/config"> | import("./router.js").Route<"chats.create", import("../chat/types.js").ChatMeta, "/api/chats"> | import("./router.js").Route<"chats.delete", import("./router.js").Reply<{
22
+ error?: string;
23
+ }>, "/api/chats/:id"> | import("./router.js").Route<"chats.elicitation", import("./router.js").Reply<{
24
+ error: string;
25
+ }> | {
26
+ ok: true;
27
+ }, "/api/chats/:id/elicitation"> | import("./router.js").Route<"chats.events", undefined, "/api/chats/:id/events"> | import("./router.js").Route<"chats.fork", import("../chat/types.js").ChatMeta, "/api/chats/:id/fork"> | import("./router.js").Route<"chats.get", {
28
+ meta: import("../chat/types.js").ChatMeta;
29
+ events: import("../chat/types.js").StoredChatEvent[];
30
+ busy: boolean;
31
+ }, "/api/chats/:id"> | import("./router.js").Route<"chats.list", {
32
+ chats: (import("../chat/types.js").ChatMeta & {
33
+ busy: boolean;
34
+ awaitingApproval: boolean;
35
+ })[];
36
+ }, "/api/chats"> | import("./router.js").Route<"chats.message", import("./router.js").Reply<{
37
+ error: string;
38
+ }> | {
39
+ ok: true;
40
+ }, "/api/chats/:id/message"> | import("./router.js").Route<"chats.permission", import("./router.js").Reply<{
41
+ error: string;
42
+ }> | {
43
+ ok: true;
44
+ }, "/api/chats/:id/permission"> | import("./router.js").Route<"chats.rename", import("./router.js").Reply<{
45
+ meta?: import("../chat/types.js").ChatMeta;
46
+ error?: string;
47
+ }>, "/api/chats/:id"> | import("./router.js").Route<"chats.resetSession", import("../chat/types.js").ChatMeta, "/api/chats/:id/reset-session"> | import("./router.js").Route<"chats.setVibeable", import("../chat/types.js").ChatMeta, "/api/chats/:id/vibeable"> | import("./router.js").Route<"chats.steer", import("./router.js").Reply<{
48
+ error: string;
49
+ }> | {
50
+ ok: true;
51
+ }, "/api/chats/:id/steer"> | import("./router.js").Route<"chats.stopTask", import("./router.js").Reply<{
52
+ error: string;
53
+ }> | {
54
+ ok: true;
55
+ }, "/api/chats/:id/task-stop">)[];
@@ -0,0 +1,154 @@
1
+ import { promises as fs } from "node:fs";
2
+ import path from "node:path";
3
+ import { getAgent } from "../agents.js";
4
+ import { answerElicitation, cancelChat, createChat, deleteChat, forkChat, getChat, listAgentSessionsFor, listChats, postMessage, renameChat, resetSession, resolvePermission, setChatConfig, setChatVibeable, steerChat, stopChatTask, subscribeChat, } from "../chat/sessions.js";
5
+ import { attachmentPath, saveAttachment } from "../chat/store.js";
6
+ import { getProject } from "../projects.js";
7
+ import { MIME } from "./mime.js";
8
+ import { projectErrors } from "./projects.js";
9
+ import { ApiError, fail, readRawBody, reply, route } from "./router.js";
10
+ const AGENTS = ["claude", "codex", "pi"];
11
+ /** A chat id that names no chat folder (or a malformed one) is a 400. */
12
+ const validChat = (p, message = "invalid chat id") => p.catch((err) => {
13
+ throw err instanceof ApiError ? err : new ApiError(400, message);
14
+ });
15
+ /** Sessions report refusals as `{error, status?}`; the success value is the chat's meta. */
16
+ const metaOf = (result) => (result.error ? fail(result.status ?? 400, result.error) : result.meta);
17
+ /** The attachments a message or steer carries: references to files already uploaded. */
18
+ const attachmentsOf = (body) => Array.isArray(body.attachments)
19
+ ? body.attachments
20
+ .filter((a) => a && typeof a.name === "string" && typeof a.mimeType === "string")
21
+ .map((a) => ({ name: String(a.name), mimeType: String(a.mimeType), size: Number(a.size) || 0 }))
22
+ : [];
23
+ /** An action on a running chat: `{ok}` when it went through, 409 with the reason when the chat refused. */
24
+ const act = (name, path, summary, run) => route({ name, method: "POST", path, summary, errors: 400 }, async (c) => {
25
+ const result = await run(c, await c.body());
26
+ return result.error ? reply(409, result) : { ok: true };
27
+ });
28
+ const textOf = (body) => {
29
+ const text = String(body.text ?? "").trim();
30
+ if (!text && attachmentsOf(body).length === 0)
31
+ fail(400, "text is required");
32
+ return text || "(see attached)";
33
+ };
34
+ /** The scope a new chat runs in; agent and project chats take their harness from the definition, not the caller. */
35
+ async function scopeOf(body) {
36
+ const s = body.scope;
37
+ if (s?.kind === "agent" && s.slug) {
38
+ const def = await getAgent(s.slug).catch(() => null);
39
+ if (!def)
40
+ fail(404, "agent not found");
41
+ return { agent: def.harness, scope: { kind: "agent", slug: def.slug } };
42
+ }
43
+ if (s?.kind === "project" && s.slug) {
44
+ // A project chat needs its folder; a missing one is a 404 here, not a chat that apologizes.
45
+ const found = await getProject(s.slug).catch((err) => {
46
+ throw new ApiError(projectErrors(err.message), err.message);
47
+ });
48
+ if (!found)
49
+ fail(404, "project not found");
50
+ return { agent: found.harness, scope: { kind: "project", slug: found.slug } };
51
+ }
52
+ const agent = body.agent ?? "";
53
+ if (s?.kind === "run" && s.runId)
54
+ return { agent, scope: { kind: "run", runId: s.runId } };
55
+ if (s?.kind === "kraftwerk")
56
+ return { agent, scope: { kind: "kraftwerk" } };
57
+ if (s?.kind === "knowledge")
58
+ return { agent, scope: { kind: "knowledge", ...(s.bundle ? { bundle: s.bundle } : {}) } };
59
+ return { agent, scope: { kind: "general" } };
60
+ }
61
+ /** Chats: ACP sessions with a coding agent, their event stream, and what a person does in them. */
62
+ export const chatRoutes = [
63
+ route({ name: "chats.list", method: "GET", path: "/api/chats", summary: "every chat with its live state" }, async () => ({ chats: await listChats() })),
64
+ route({ name: "chats.create", method: "POST", path: "/api/chats", summary: "start a chat {agent, scope, resume?}" }, async (c) => {
65
+ // `resume` continues one of the agent's own sessions (see agents.sessions).
66
+ const body = await c.body();
67
+ const { agent, scope } = await scopeOf(body);
68
+ if (!AGENTS.includes(agent))
69
+ fail(400, "agent must be claude, codex, or pi");
70
+ try {
71
+ return await createChat({ agent: agent, scope, ...(typeof body.resume === "string" && body.resume ? { resume: body.resume } : {}) });
72
+ }
73
+ catch (err) {
74
+ throw new ApiError(400, err.message);
75
+ }
76
+ }),
77
+ route({ name: "agents.sessions", method: "GET", path: "/api/agent-sessions", summary: "the agent's own sessions in the workspace ?agent=claude|codex|pi", errors: 502 }, async (c) => {
78
+ const agent = c.query.get("agent") ?? "claude";
79
+ if (!AGENTS.includes(agent))
80
+ fail(400, "agent must be claude, codex, or pi");
81
+ return { sessions: await listAgentSessionsFor(agent) };
82
+ }),
83
+ route({ name: "chats.get", method: "GET", path: "/api/chats/:id", summary: "the chat's meta and events" }, async (c) => (await validChat(getChat(c.params.id))) ?? fail(404, "not found")),
84
+ route({ name: "chats.delete", method: "DELETE", path: "/api/chats/:id", summary: "delete the chat" }, async (c) => {
85
+ const result = await validChat(deleteChat(c.params.id));
86
+ return reply(result.error ? 404 : 200, result);
87
+ }),
88
+ route({ name: "chats.rename", method: "PATCH", path: "/api/chats/:id", summary: "rename the chat {title}" }, async (c) => {
89
+ const body = await c.body();
90
+ const result = await validChat(renameChat(c.params.id, body.title), "invalid chat id or body");
91
+ return reply(result.error ? (result.error === "not found" ? 404 : 400) : 200, result);
92
+ }),
93
+ // Replays from ?after=N, then streams new events as they happen.
94
+ 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 }) => {
95
+ const afterSeq = Number(query.get("after") ?? "0") || 0;
96
+ res.writeHead(200, { "content-type": "text/event-stream", "cache-control": "no-store", connection: "keep-alive" });
97
+ const unsubscribe = await subscribeChat(params.id, afterSeq, (ev) => res.write(`id: ${ev.seq}\ndata: ${JSON.stringify(ev)}\n\n`)).catch(() => null);
98
+ if (!unsubscribe)
99
+ return void res.end();
100
+ const heartbeat = setInterval(() => res.write(":hb\n\n"), 25_000);
101
+ req.on("close", () => {
102
+ clearInterval(heartbeat);
103
+ unsubscribe();
104
+ });
105
+ }),
106
+ route({ name: "chats.setVibeable", method: "POST", path: "/api/chats/:id/vibeable", summary: "open an app {slug} in the chat's preview pane (cwd follows), or close it {slug: null}", errors: 400 }, async (c) => {
107
+ const { slug } = await c.body();
108
+ return metaOf(await setChatVibeable(c.params.id, slug == null || slug === "" ? null : String(slug)));
109
+ }),
110
+ route({ name: "chats.fork", method: "POST", path: "/api/chats/:id/fork", summary: "a new chat continuing a copy of this one's session", errors: 400 }, async (c) => metaOf(await forkChat(c.params.id))),
111
+ route({ name: "chats.resetSession", method: "POST", path: "/api/chats/:id/reset-session", summary: "forget the agent's session; the next message starts fresh", errors: 400 }, async (c) => metaOf(await resetSession(c.params.id))),
112
+ // The body is the file itself; x-file-name names it, content-type types it.
113
+ route({ name: "chats.attach", method: "POST", path: "/api/chats/:id/attachments", summary: "upload an attachment for the next message", errors: 400, upload: true }, async (c) => {
114
+ if (!(await getChat(c.params.id)))
115
+ fail(404, "not found");
116
+ const data = await readRawBody(c.req, 25_000_000);
117
+ if (data.length === 0)
118
+ fail(400, "empty file");
119
+ const mimeType = (c.req.headers["content-type"] ?? "application/octet-stream").split(";")[0].trim();
120
+ const name = await saveAttachment(c.params.id, decodeURIComponent(String(c.req.headers["x-file-name"] ?? "file")), data);
121
+ return { name, mimeType, size: data.length };
122
+ }),
123
+ route({ name: "chats.attachment", method: "GET", path: "/api/chats/:id/attachments/:name", summary: "a stored attachment", raw: true }, async ({ res, params }) => {
124
+ let data;
125
+ let file;
126
+ try {
127
+ file = attachmentPath(params.id, params.name);
128
+ data = await fs.readFile(file);
129
+ }
130
+ catch {
131
+ return fail(404, "not found");
132
+ }
133
+ res.writeHead(200, {
134
+ "content-type": MIME[path.extname(file).toLowerCase()] ?? "application/octet-stream",
135
+ "content-length": data.length,
136
+ "cache-control": "private, max-age=86400",
137
+ "x-content-type-options": "nosniff",
138
+ });
139
+ res.end(data);
140
+ }),
141
+ // Channels: `from` is the poster's display name, which signs the message.
142
+ act("chats.message", "/api/chats/:id/message", "send a message {text, attachments?, from?}", (c, body) => postMessage(c.params.id, textOf(body), { from: typeof body.from === "string" ? body.from : undefined, attachments: attachmentsOf(body) })),
143
+ act("chats.steer", "/api/chats/:id/steer", "add to the running turn {text, attachments?}", (c, body) => steerChat(c.params.id, textOf(body), attachmentsOf(body))),
144
+ act("chats.permission", "/api/chats/:id/permission", "answer a permission request {requestId, optionId | null}", (c, body) => resolvePermission(c.params.id, String(body.requestId ?? ""), body.optionId == null ? null : String(body.optionId))),
145
+ // Channels: `agent` stops one member; without it every agent in the chat.
146
+ act("chats.cancel", "/api/chats/:id/cancel", "stop the running turn {agent?}", (c, body) => cancelChat(c.params.id, typeof body.agent === "string" && body.agent ? body.agent : undefined)),
147
+ act("chats.elicitation", "/api/chats/:id/elicitation", "answer the agent's question {requestId, action, content?}", (c, body) => {
148
+ const action = body.action === "accept" || body.action === "decline" || body.action === "cancel" ? body.action : fail(400, "action must be accept, decline, or cancel");
149
+ const content = body.content && typeof body.content === "object" ? body.content : {};
150
+ return answerElicitation(c.params.id, String(body.requestId ?? ""), action === "accept" ? { action, content } : { action });
151
+ }),
152
+ act("chats.stopTask", "/api/chats/:id/task-stop", "stop one background task {taskId}", (c, body) => stopChatTask(c.params.id, String(body.taskId ?? ""))),
153
+ act("chats.config", "/api/chats/:id/config", "set a session option {configId, value}", (c, body) => setChatConfig(c.params.id, String(body.configId ?? ""), typeof body.value === "boolean" ? body.value : String(body.value ?? ""))),
154
+ ];
@@ -0,0 +1,15 @@
1
+ /** Files: the workspace's folder and one per project, browsed by scope and path. */
2
+ export declare const fileRoutes: (import("./router.js").Route<"files.delete", {
3
+ ok: boolean;
4
+ }, "/api/files"> | import("./router.js").Route<"files.list", import("../files.js").FilesListing, "/api/files/list"> | import("./router.js").Route<"files.mkdir", import("./router.js").Reply<import("../files.js").FileEntry>, "/api/files/folder"> | import("./router.js").Route<"files.move", {
5
+ ok: boolean;
6
+ }, "/api/files/move"> | import("./router.js").Route<"files.raw", void, "/api/files/raw"> | import("./router.js").Route<"files.recent", {
7
+ root: string;
8
+ count: number;
9
+ recent: {
10
+ path: string;
11
+ at: string;
12
+ }[];
13
+ }, "/api/files/recent"> | import("./router.js").Route<"files.scopes", {
14
+ scopes: import("../files.js").FilesScopeInfo[];
15
+ }, "/api/files"> | import("./router.js").Route<"files.upload", import("./router.js").Reply<import("../files.js").FileEntry>, "/api/files/upload">)[];
@@ -0,0 +1,43 @@
1
+ import { deleteFile, filesSummary, listFiles, listScopes, makeFolder, moveFile, openFile, saveUpload } from "../files.js";
2
+ import { reply, route } from "./router.js";
3
+ const errors = (message) => (/^no (file|folder|project)/.test(message) ? 404 : 400);
4
+ /** The scope (workspace, or a project's folder) and the path inside it, from the query. */
5
+ const where = (c) => [c.query.get("scope") ?? "workspace", c.query.get("path") ?? ""];
6
+ /** Files: the workspace's folder and one per project, browsed by scope and path. */
7
+ export const fileRoutes = [
8
+ route({ name: "files.scopes", method: "GET", path: "/api/files", summary: "the roots: the workspace and one per project", errors }, async () => ({ scopes: await listScopes() })),
9
+ route({ name: "files.list", method: "GET", path: "/api/files/list", summary: "a folder's entries ?scope&path", errors }, async (c) => listFiles(...where(c))),
10
+ route({ name: "files.recent", method: "GET", path: "/api/files/recent", summary: "the scope's file count and latest files ?scope", errors }, async (c) => (await filesSummary(where(c)[0], 5)) ?? { root: "", count: 0, recent: [] }),
11
+ route({ name: "files.raw", method: "GET", path: "/api/files/raw", summary: "the file itself ?scope&path[&download=1]", errors, raw: true }, async (c) => {
12
+ const f = await openFile(...where(c));
13
+ c.res.writeHead(200, {
14
+ "content-type": f.mime,
15
+ "content-length": f.size,
16
+ "cache-control": "no-store",
17
+ "x-content-type-options": "nosniff",
18
+ // Anything in here may come from outside: a page, an SVG or XML renders without scripts, in an origin of its own.
19
+ // (Not on PDFs and media: a sandboxed document cannot show the browser's PDF viewer.)
20
+ ...(/^(text\/html|image\/svg|text\/xml|application\/xml)/.test(f.mime) ? { "content-security-policy": "sandbox" } : {}),
21
+ "content-disposition": `${c.query.get("download") ? "attachment" : "inline"}; filename*=UTF-8''${encodeURIComponent(f.name)}`,
22
+ });
23
+ f.stream.pipe(c.res);
24
+ }),
25
+ // The body is the file itself; x-file-name names it.
26
+ route({ name: "files.upload", method: "POST", path: "/api/files/upload", summary: "upload one file ?scope&path", errors, upload: true }, async (c) => {
27
+ const name = decodeURIComponent(String(c.req.headers["x-file-name"] ?? "file"));
28
+ return reply(201, await saveUpload(...where(c), name, c.req));
29
+ }),
30
+ route({ name: "files.mkdir", method: "POST", path: "/api/files/folder", summary: "create a folder {scope, path}", errors }, async (c) => {
31
+ const body = await c.body();
32
+ return reply(201, await makeFolder(body.scope ?? where(c)[0], String(body.path ?? "")));
33
+ }),
34
+ route({ name: "files.move", method: "POST", path: "/api/files/move", summary: "move or rename {scope, from, to}", errors }, async (c) => {
35
+ const body = await c.body();
36
+ await moveFile(body.scope ?? where(c)[0], String(body.from ?? ""), String(body.to ?? ""));
37
+ return { ok: true };
38
+ }),
39
+ route({ name: "files.delete", method: "DELETE", path: "/api/files", summary: "move a file or folder ?scope&path to the trash", errors }, async (c) => {
40
+ await deleteFile(...where(c));
41
+ return { ok: true };
42
+ }),
43
+ ];
@@ -0,0 +1,25 @@
1
+ /** The workspace's own git, and the repositories cloned under its repos root. */
2
+ export declare const gitRoutes: (import("./router.js").Route<"git.commit", import("./router.js").Reply<{
3
+ ok: boolean;
4
+ error?: string;
5
+ committed?: number;
6
+ }>, "/api/git/commit"> | import("./router.js").Route<"git.diff", import("../git.js").GitDiff, "/api/git/diff"> | import("./router.js").Route<"git.fetch", import("./router.js").Reply<{
7
+ ok: boolean;
8
+ error?: string;
9
+ }>, "/api/git/fetch"> | import("./router.js").Route<"git.pull", import("./router.js").Reply<{
10
+ ok: boolean;
11
+ error?: string;
12
+ }>, "/api/git/pull"> | import("./router.js").Route<"git.push", import("./router.js").Reply<{
13
+ ok: boolean;
14
+ error?: string;
15
+ }>, "/api/git/push"> | import("./router.js").Route<"git.status", import("../git.js").GitStatus, "/api/git"> | import("./router.js").Route<"repos.add", import("./router.js").Reply<import("../repos.js").RepoInfo>, "/api/repos"> | import("./router.js").Route<"repos.commit", import("./router.js").Reply<import("../git.js").GitDiff>, "/api/repos/:slug/commits/:hash"> | import("./router.js").Route<"repos.diff", import("./router.js").Reply<import("../git.js").GitDiff>, "/api/repos/:slug/diff"> | import("./router.js").Route<"repos.get", import("../repos.js").RepoDetail, "/api/repos/:slug"> | import("./router.js").Route<"repos.list", import("../repos.js").ReposView, "/api/repos"> | import("./router.js").Route<"repos.remove", import("./router.js").Reply<{
16
+ ok: boolean;
17
+ error?: string;
18
+ off?: boolean;
19
+ conflict?: boolean;
20
+ }>, "/api/repos/:slug"> | import("./router.js").Route<"repos.update", import("./router.js").Reply<{
21
+ ok: boolean;
22
+ error?: string;
23
+ off?: boolean;
24
+ repo?: import("../repos.js").RepoInfo;
25
+ }>, "/api/repos/:slug/update">)[];
@@ -0,0 +1,56 @@
1
+ import { gitCommit, gitDiff, gitFetch, gitPull, gitPush, gitStatus } from "../git.js";
2
+ import { addRepo, listRepos, openRepos, removeRepo, repoCommitDiff, repoDetail, repoDiff, updateRepo } from "../repos.js";
3
+ import { fail, reply, route, statusByMessage } from "./router.js";
4
+ const repoErrors = statusByMessage(/^no repository/);
5
+ const conflictUnlessOk = (result) => reply(result.ok ? 200 : 409, result);
6
+ /** The workspace's own git, and the repositories cloned under its repos root. */
7
+ export const gitRoutes = [
8
+ // The git screen polls this, so it is cached for a beat; ?fresh=1 bypasses the cache.
9
+ route({ name: "git.status", method: "GET", path: "/api/git", summary: "branch, ahead/behind, changed files" }, async (c) => gitStatus(c.query.has("fresh"))),
10
+ route({ name: "git.diff", method: "GET", path: "/api/git/diff", summary: "unified diff for one file ?path=" }, async (c) => {
11
+ const file = c.query.get("path");
12
+ if (!file)
13
+ fail(400, "path required");
14
+ return gitDiff(file);
15
+ }),
16
+ // Commit and push stay manual; the background timer never writes history.
17
+ route({ name: "git.commit", method: "POST", path: "/api/git/commit", summary: "stage and commit {paths, message}", errors: 400 }, async (c) => {
18
+ const body = await c.body();
19
+ const paths = Array.isArray(body.paths) ? body.paths.filter((p) => typeof p === "string") : [];
20
+ const result = await gitCommit(paths, typeof body.message === "string" ? body.message : "");
21
+ return reply(result.ok ? 200 : 409, result);
22
+ }),
23
+ route({ name: "git.fetch", method: "POST", path: "/api/git/fetch", summary: "git fetch" }, async () => conflictUnlessOk(await gitFetch())),
24
+ route({ name: "git.pull", method: "POST", path: "/api/git/pull", summary: "git pull --ff-only" }, async () => conflictUnlessOk(await gitPull())),
25
+ route({ name: "git.push", method: "POST", path: "/api/git/push", summary: "git push" }, async () => conflictUnlessOk(await gitPush())),
26
+ route({ name: "repos.list", method: "GET", path: "/api/repos", summary: "every clone under the repos root, read live from git" }, async () => listRepos()),
27
+ route({ name: "repos.add", method: "POST", path: "/api/repos", summary: "clone {url, name?, branch?, depth?} into the root", errors: 400 }, async (c) => {
28
+ if ((await openRepos()).off)
29
+ fail(409, "repositories are off");
30
+ const body = await c.body();
31
+ const str = (v) => (typeof v === "string" ? v : undefined);
32
+ const depth = body.depth === undefined ? undefined : typeof body.depth === "number" ? body.depth : Number.NaN;
33
+ return reply(201, await addRepo({ url: str(body.url) ?? "", name: str(body.name), branch: str(body.branch), depth }));
34
+ }),
35
+ route({ name: "repos.get", method: "GET", path: "/api/repos/:slug", summary: "changed files, line counts, recent commits (unpushed marked)", errors: repoErrors }, async (c) => {
36
+ const detail = await repoDetail(c.params.slug);
37
+ return detail ?? fail(404, `no repository "${c.params.slug}"`);
38
+ }),
39
+ route({ name: "repos.diff", method: "GET", path: "/api/repos/:slug/diff", summary: "unified diff of one changed file ?path= against HEAD", errors: repoErrors }, async (c) => {
40
+ const d = await repoDiff(c.params.slug, c.query.get("path") ?? "");
41
+ return reply(d.error ? (/^not shown/.test(d.error) ? 404 : 400) : 200, d);
42
+ }),
43
+ route({ name: "repos.commit", method: "GET", path: "/api/repos/:slug/commits/:hash", summary: "one commit as a patch", errors: repoErrors }, async (c) => {
44
+ const d = await repoCommitDiff(c.params.slug, c.params.hash);
45
+ return reply(d.error ? (/^no such commit/.test(d.error) ? 404 : 400) : 200, d);
46
+ }),
47
+ route({ name: "repos.update", method: "POST", path: "/api/repos/:slug/update", summary: "fetch, fast-forward when clean", errors: 400 }, async (c) => {
48
+ const result = await updateRepo(c.params.slug);
49
+ return reply(result.ok ? 200 : result.repo || result.off ? 409 : 404, result);
50
+ }),
51
+ // 409 while the clone holds unpushed or uncommitted work, unless ?force=1.
52
+ route({ name: "repos.remove", method: "DELETE", path: "/api/repos/:slug", summary: "remove the clone", errors: 400 }, async (c) => {
53
+ const result = await removeRepo(c.params.slug, ["1", "true"].includes(c.query.get("force") ?? ""));
54
+ return reply(result.ok ? 200 : result.conflict || result.off ? 409 : 404, result);
55
+ }),
56
+ ];