@netnodeag/kraftwerk 0.61.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 (75) 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 +22 -1
  46. package/dist/inspector/routines.js +32 -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 -1389
  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-B3L4wDm3.js → dist-9RESajfo.js} +1 -1
  58. package/inspector/dist/assets/{dist-Cwukoo61.js → dist-Bm5S1BoZ.js} +1 -1
  59. package/inspector/dist/assets/dist-C8BofvHx.js +1 -0
  60. package/inspector/dist/assets/{dist-wYE0FpHk.js → dist-CIs4wnTu.js} +1 -1
  61. package/inspector/dist/assets/{dist-De7-pK5i.js → dist-CjuETd-_.js} +1 -1
  62. package/inspector/dist/assets/{dist-B58fOC3R.js → dist-CmVK6rgN.js} +1 -1
  63. package/inspector/dist/assets/{dist-zk56dqRQ.js → dist-CryXvMV_.js} +1 -1
  64. package/inspector/dist/assets/{dist-BiWJ3XY1.js → dist-D1hk0iC5.js} +1 -1
  65. package/inspector/dist/assets/{dist-CH9pz3rR.js → dist-DvW9QE1d.js} +1 -1
  66. package/inspector/dist/assets/{dist-Dno2CmWo.js → dist-VD9jMxBv.js} +1 -1
  67. package/inspector/dist/assets/{dist-BKBuUSVQ.js → dist-VlAqWD8T.js} +1 -1
  68. package/inspector/dist/assets/{editor-CMQMyCOv.js → editor-DkWqwl89.js} +3 -3
  69. package/inspector/dist/assets/index-Bra74Nxe.css +2 -0
  70. package/inspector/dist/assets/index-DRJS2pGj.js +229 -0
  71. package/inspector/dist/index.html +2 -2
  72. package/package.json +7 -2
  73. package/inspector/dist/assets/dist-Bj93ipvc.js +0 -1
  74. package/inspector/dist/assets/index-1wfYRgHE.css +0 -2
  75. package/inspector/dist/assets/index-CtygNkZJ.js +0 -229
@@ -0,0 +1,186 @@
1
+ import { touch } from "../live.js";
2
+ /** A result with a status other than 200 (201 created, 409 with a body, …). */
3
+ export class Reply {
4
+ status;
5
+ body;
6
+ constructor(status, body) {
7
+ this.status = status;
8
+ this.body = body;
9
+ }
10
+ }
11
+ export const reply = (status, body) => new Reply(status, body);
12
+ /** A refusal with a status; the body is `{error}` unless given. */
13
+ export class ApiError extends Error {
14
+ status;
15
+ body;
16
+ constructor(status, message, body = { error: message }) {
17
+ super(message);
18
+ this.status = status;
19
+ this.body = body;
20
+ }
21
+ }
22
+ export function fail(status, message) {
23
+ throw new ApiError(status, message);
24
+ }
25
+ export function route(def, handle) {
26
+ return { ...def, handle };
27
+ }
28
+ /** Maps an error message to a status by the usual wording: "no <thing>" is 404, "… are off" 409, else 400. */
29
+ export const statusByMessage = (missing) => (message) => missing.test(message) ? 404 : /are off/.test(message) ? 409 : 400;
30
+ /** What a thrown error becomes: an ApiError's status and body, else the route's mapping. */
31
+ function failure(route, err) {
32
+ if (err instanceof ApiError)
33
+ return { status: err.status, data: err.body };
34
+ const message = err.message;
35
+ return { status: typeof route.errors === "function" ? route.errors(message) : (route.errors ?? 500), data: { error: message } };
36
+ }
37
+ export function json(res, body, status = 200) {
38
+ // A late error on an SSE response must not try to write headers again.
39
+ if (res.headersSent)
40
+ return void res.end();
41
+ res.writeHead(status, { "content-type": "application/json; charset=utf-8", "cache-control": "no-store" });
42
+ res.end(JSON.stringify(body));
43
+ }
44
+ export function readBody(req) {
45
+ return new Promise((resolve, reject) => {
46
+ let data = "";
47
+ req.on("data", (c) => {
48
+ data += c;
49
+ if (data.length > 1_000_000)
50
+ reject(new ApiError(413, "body too large"));
51
+ });
52
+ req.on("end", () => resolve(data));
53
+ req.on("error", reject);
54
+ });
55
+ }
56
+ /** A raw upload body (attachments), capped at `max` bytes. */
57
+ export function readRawBody(req, max) {
58
+ return new Promise((resolve, reject) => {
59
+ const chunks = [];
60
+ let size = 0;
61
+ req.on("data", (c) => {
62
+ size += c.length;
63
+ if (size > max) {
64
+ reject(new Error(`file too large (max ${Math.round(max / 1_000_000)} MB)`));
65
+ req.destroy();
66
+ return;
67
+ }
68
+ chunks.push(c);
69
+ });
70
+ req.on("end", () => resolve(Buffer.concat(chunks)));
71
+ req.on("error", reject);
72
+ });
73
+ }
74
+ function decode(segment) {
75
+ try {
76
+ return decodeURIComponent(segment);
77
+ }
78
+ catch {
79
+ throw new ApiError(400, "malformed path");
80
+ }
81
+ }
82
+ export function createRouter(routes) {
83
+ const names = new Set();
84
+ const compiled = routes.map((route) => {
85
+ if (names.has(route.name))
86
+ throw new Error(`duplicate route name ${route.name}`);
87
+ names.add(route.name);
88
+ const parts = route.path.split("/").filter(Boolean);
89
+ return { route, parts, rank: parts.reduce((n, p, i) => n + (p.startsWith(":") ? 0 : 2 ** (parts.length - i)), 0) };
90
+ });
91
+ compiled.sort((a, b) => b.rank - a.rank);
92
+ const byName = new Map(routes.map((r) => [r.name, r]));
93
+ const match = (method, seg) => {
94
+ for (const c of compiled) {
95
+ if (c.route.method !== method || c.parts.length !== seg.length)
96
+ continue;
97
+ const params = {};
98
+ let ok = true;
99
+ for (let i = 0; i < seg.length && ok; i++) {
100
+ if (c.parts[i].startsWith(":"))
101
+ params[c.parts[i].slice(1)] = seg[i];
102
+ else
103
+ ok = c.parts[i] === seg[i];
104
+ }
105
+ if (ok)
106
+ return { route: c.route, params };
107
+ }
108
+ return undefined;
109
+ };
110
+ return {
111
+ routes,
112
+ async handle(req, res, url) {
113
+ const seg = url.pathname.split("/").filter(Boolean);
114
+ const found = match(req.method ?? "GET", seg);
115
+ if (!found)
116
+ return json(res, { error: "not found" }, 404);
117
+ const { route } = found;
118
+ let bodyRead;
119
+ try {
120
+ const params = Object.fromEntries(Object.entries(found.params).map(([k, v]) => [k, decode(v)]));
121
+ const c = {
122
+ req,
123
+ res,
124
+ url,
125
+ params,
126
+ query: url.searchParams,
127
+ body: () => (bodyRead ??= readBody(req).then((raw) => {
128
+ if (!raw.trim())
129
+ return {};
130
+ try {
131
+ return JSON.parse(raw);
132
+ }
133
+ catch {
134
+ throw new ApiError(400, "invalid JSON body");
135
+ }
136
+ })),
137
+ };
138
+ const result = await route.handle(c);
139
+ if (route.method !== "GET")
140
+ touch();
141
+ if (route.raw)
142
+ return;
143
+ if (result instanceof Reply)
144
+ return json(res, result.body, result.status);
145
+ return json(res, result);
146
+ }
147
+ catch (err) {
148
+ const f = failure(route, err);
149
+ return json(res, f.data, f.status);
150
+ }
151
+ },
152
+ async invoke(name, input) {
153
+ const route = byName.get(name);
154
+ if (!route)
155
+ return { status: 404, data: { error: `no route ${name}` } };
156
+ if (route.raw || route.upload)
157
+ return { status: 400, data: { error: `${name} is HTTP only` } };
158
+ try {
159
+ const params = {};
160
+ for (const p of route.path.split("/"))
161
+ if (p.startsWith(":"))
162
+ params[p.slice(1)] = String(input[p.slice(1)] ?? "");
163
+ const query = new URLSearchParams();
164
+ for (const [k, v] of Object.entries(input.query ?? {}))
165
+ if (v !== undefined && v !== null && v !== false)
166
+ query.set(k, v === true ? "1" : String(v));
167
+ const c = {
168
+ // No HTTP request behind an invoke; only raw and upload routes read these.
169
+ req: undefined,
170
+ res: undefined,
171
+ url: new URL(`http://invoke${route.path}`),
172
+ params,
173
+ query,
174
+ body: async () => (input.body ?? {}),
175
+ };
176
+ const result = await route.handle(c);
177
+ if (route.method !== "GET")
178
+ touch();
179
+ return result instanceof Reply ? { status: result.status, data: result.body } : { status: 200, data: result };
180
+ }
181
+ catch (err) {
182
+ return failure(route, err);
183
+ }
184
+ },
185
+ };
186
+ }
@@ -0,0 +1,33 @@
1
+ /** Workflows, their runs, and the routines that start them. */
2
+ export declare const runRoutes: (import("./router.js").Route<"routines.list", {
3
+ routines: (import("../routines.js").RoutineStatus & {
4
+ agent: {
5
+ slug: string;
6
+ name: string;
7
+ emoji: string;
8
+ };
9
+ workflows: string[];
10
+ })[];
11
+ }, "/api/routines"> | import("./router.js").Route<"runs.decide", import("../decisions.js").DecisionAnswer, "/api/runs/:id/decision"> | import("./router.js").Route<"runs.delete", {
12
+ deleted: boolean;
13
+ }, "/api/runs/:id"> | import("./router.js").Route<"runs.file", undefined, "/api/runs/:id/file"> | import("./router.js").Route<"runs.fileText", {
14
+ name: string;
15
+ size: number;
16
+ truncated: boolean;
17
+ content: string;
18
+ }, "/api/runs/:id/text"> | import("./router.js").Route<"runs.get", import("../runs.js").RunDetail, "/api/runs/:id"> | import("./router.js").Route<"runs.list", {
19
+ outputDir: string;
20
+ runs: import("../runs.js").RunListItem[];
21
+ }, "/api/runs"> | import("./router.js").Route<"runs.stop", {
22
+ stopped: boolean;
23
+ }, "/api/runs/:id/stop"> | import("./router.js").Route<"workflows.delete", {
24
+ ok: boolean;
25
+ }, "/api/workflows/:slug"> | import("./router.js").Route<"workflows.get", import("../workflows.js").WorkflowDetail, "/api/workflows/:slug"> | import("./router.js").Route<"workflows.list", {
26
+ root?: string;
27
+ workflows: import("../workflows.js").WorkflowSummary[];
28
+ }, "/api/workflows"> | import("./router.js").Route<"workflows.run", {
29
+ runId: string;
30
+ }, "/api/workflows/:slug/run"> | import("./router.js").Route<"workflows.runner", {
31
+ available: boolean;
32
+ image: boolean;
33
+ }, "/api/workflows/:slug/run">)[];
@@ -0,0 +1,105 @@
1
+ import { promises as fs } from "node:fs";
2
+ import { getOutputDir } from "../context.js";
3
+ import { decide } from "../decisions.js";
4
+ import { allRoutines } from "../routines.js";
5
+ import { dockerStatus, stopRun, triggerRun } from "../runner.js";
6
+ import { deleteRun, getRun, listRuns, readRunFile, safeRunDir } from "../runs.js";
7
+ import { deleteWorkflow, getWorkflow, listWorkflows } from "../workflows.js";
8
+ import { MIME } from "./mime.js";
9
+ import { ApiError, fail, route } from "./router.js";
10
+ /** CSP for raw run files: same shape as the vibeable preview — scripts allowed, origin opaque. */
11
+ const RUN_FILE_CSP = "sandbox allow-scripts allow-forms allow-popups allow-modals allow-downloads";
12
+ /** Text preview payloads are capped; the tail matters most for logs. */
13
+ const MAX_TEXT = 400_000;
14
+ /** A run file's text, tail-capped: what the run page shows for logs and reports. */
15
+ async function fileText(name, absPath, size) {
16
+ let content = await fs.readFile(absPath, "utf8");
17
+ const truncated = content.length > MAX_TEXT;
18
+ if (truncated)
19
+ content = content.slice(-MAX_TEXT);
20
+ return { name, size, truncated, content };
21
+ }
22
+ /** A run id that does not name a run folder is refused as a 400. */
23
+ const validRun = (p, message = "invalid run id") => p.catch((err) => {
24
+ throw err instanceof ApiError ? err : new ApiError(400, message);
25
+ });
26
+ /** Workflows, their runs, and the routines that start them. */
27
+ export const runRoutes = [
28
+ route({ name: "workflows.list", method: "GET", path: "/api/workflows", summary: "every workflow, with validation errors" }, async () => listWorkflows()),
29
+ route({ name: "workflows.get", method: "GET", path: "/api/workflows/:slug", summary: "one workflow: agents, steps, gates" }, async (c) => (await getWorkflow(c.params.slug)) ?? fail(404, "not found")),
30
+ route({ name: "workflows.delete", method: "DELETE", path: "/api/workflows/:slug", summary: "move it to the trash; refused while a run is running" }, async (c) => {
31
+ const outcome = await deleteWorkflow(c.params.slug);
32
+ if (outcome === "missing")
33
+ fail(404, "not found");
34
+ if (outcome === "running")
35
+ fail(409, "a run of this workflow is still running — stop it first");
36
+ return { ok: true };
37
+ }),
38
+ route({ name: "workflows.runner", method: "GET", path: "/api/workflows/:slug/run", summary: "whether runs can start here (docker state)" }, async () => dockerStatus()),
39
+ route({ name: "workflows.run", method: "POST", path: "/api/workflows/:slug/run", summary: "start a run {request?, sandbox?, ssh?}" }, async (c) => {
40
+ const wf = await getWorkflow(c.params.slug);
41
+ if (!wf || wf.error || !wf.name)
42
+ fail(404, "workflow not found or broken");
43
+ const body = await c.body();
44
+ const request = (body.request ?? "").trim();
45
+ if (!request && wf.usesRequest)
46
+ fail(400, "this workflow needs a request");
47
+ try {
48
+ return { runId: triggerRun({ workflowName: wf.name, request, sandbox: body.sandbox ?? true, ssh: !!body.ssh }).runId };
49
+ }
50
+ catch (err) {
51
+ throw new ApiError(503, err.message);
52
+ }
53
+ }),
54
+ route({ name: "routines.list", method: "GET", path: "/api/routines", summary: "every agent's routines with run state and the workflows each one names" }, async () => ({
55
+ routines: await allRoutines((await listWorkflows()).workflows),
56
+ })),
57
+ route({ name: "runs.list", method: "GET", path: "/api/runs", summary: "every run in the output dir" }, async () => ({ outputDir: getOutputDir(), runs: await listRuns() })),
58
+ route({ name: "runs.get", method: "GET", path: "/api/runs/:id", summary: "one run: phases, gates, files" }, async (c) => (await validRun(getRun(c.params.id))) ?? fail(404, "not found")),
59
+ route({ name: "runs.fileText", method: "GET", path: "/api/runs/:id/text", summary: "one run file's text ?name=, tail-capped" }, async (c) => {
60
+ const name = c.query.get("name") ?? "";
61
+ const file = await validRun(readRunFile(c.params.id, name), "invalid request");
62
+ if (!file)
63
+ fail(404, "not found");
64
+ return fileText(name, file.absPath, file.size);
65
+ }),
66
+ // ?raw=1 serves the file itself (sandboxed); without it, the same JSON as runs.fileText (older clients).
67
+ route({ name: "runs.file", method: "GET", path: "/api/runs/:id/file", summary: "one run file ?name=[&raw=1]", raw: true }, async ({ res, params, query }) => {
68
+ const name = query.get("name") ?? "";
69
+ const file = await validRun(readRunFile(params.id, name), "invalid request");
70
+ if (!file)
71
+ fail(404, "not found");
72
+ if (query.get("raw") === "1") {
73
+ const ext = name.slice(name.lastIndexOf(".")).toLowerCase();
74
+ // Agent-written files must not run as the inspector's origin: the
75
+ // sandbox keeps scripts working (interactive reports) but makes the
76
+ // origin opaque, so no fetch() into /api and no cookies. PDFs are
77
+ // exempt — Chrome's viewer refuses sandboxed documents, and PDF
78
+ // scripting has no DOM or origin access.
79
+ res.writeHead(200, {
80
+ "content-type": MIME[ext] ?? "text/plain; charset=utf-8",
81
+ "cache-control": "no-store",
82
+ "x-content-type-options": "nosniff",
83
+ ...(ext === ".pdf" ? {} : { "content-security-policy": RUN_FILE_CSP }),
84
+ });
85
+ return void res.end(await fs.readFile(file.absPath));
86
+ }
87
+ res.writeHead(200, { "content-type": "application/json; charset=utf-8", "cache-control": "no-store" });
88
+ res.end(JSON.stringify(await fileText(name, file.absPath, file.size)));
89
+ }),
90
+ // A person answers the step that wrote decision-request.json; the answer is written once.
91
+ route({ name: "runs.decide", method: "POST", path: "/api/runs/:id/decision", summary: "answer a decision request {decision, note?}" }, async (c) => {
92
+ const runDir = await validRun(Promise.resolve().then(() => safeRunDir(c.params.id)), "invalid request");
93
+ const r = await decide(runDir, (await c.body()) ?? {});
94
+ return r.ok ? r.answer : fail(r.status, r.error);
95
+ }),
96
+ route({ name: "runs.stop", method: "POST", path: "/api/runs/:id/stop", summary: "stop a running run" }, async (c) => stopRun(c.params.id) ? { stopped: true } : fail(404, "nothing to stop: no launcher of this inspector and no sandbox container for this run")),
97
+ route({ name: "runs.delete", method: "DELETE", path: "/api/runs/:id", summary: "delete a run; refused while it is live" }, async (c) => {
98
+ const outcome = await validRun(deleteRun(c.params.id));
99
+ if (outcome === "missing")
100
+ fail(404, "not found");
101
+ if (outcome === "running")
102
+ fail(409, "run is still running — stop it first");
103
+ return { deleted: true };
104
+ }),
105
+ ];
@@ -0,0 +1,3 @@
1
+ import type { Workspace } from "../workspace.js";
2
+ import type { Socket } from "../ws.js";
3
+ export declare function serveSocket(conn: Socket, workspace: Workspace, version: string): void;
@@ -0,0 +1,115 @@
1
+ import { subscribeChat } from "../chat/sessions.js";
2
+ import { watch } from "../live.js";
3
+ import { resolveVibeable, subscribeVibeable } from "../vibeables.js";
4
+ import { apiRouter, PROTOCOL_VERSION, routes } from "./index.js";
5
+ /**
6
+ * The control plane over one WebSocket (`/api/ws`): every JSON route by
7
+ * name, live route results, and event streams — what a native app needs
8
+ * from one connection, and what the web UI uses instead of polling.
9
+ *
10
+ * Server → client, first: `{hello: {protocol, version}}`.
11
+ *
12
+ * Client → server, each a JSON text message:
13
+ * - `{id, call, input?}` runs route `call` (input = path params by name, `query`, `body`)
14
+ * → `{id, status, data}`. Raw and upload routes stay HTTP.
15
+ * - `{watch: key, name, input?, interval?}` keeps a GET route's result live
16
+ * → `{watch: key, data}` now and whenever it changes. `{unwatch: key}` stops.
17
+ * - `{sub: key, topic, after?}` follows an event stream: `chat.<id>` (events
18
+ * after seq `after`, then new ones) or `vibeable.<slug>` (file changes, dev
19
+ * server state) → `{event: key, data}` per event. `{unsub: key}` stops.
20
+ *
21
+ * `key`s are the client's own names, unique per connection. A refusal of a
22
+ * message is `{id?, watch?, sub?, error}`.
23
+ */
24
+ const DEFAULT_INTERVAL = 5000;
25
+ const GET_ROUTES = new Set(routes.filter((r) => r.method === "GET" && !r.raw).map((r) => r.name));
26
+ export function serveSocket(conn, workspace, version) {
27
+ const stops = new Map();
28
+ const send = (msg) => conn.send(JSON.stringify(msg));
29
+ const stop = (key) => {
30
+ stops.get(key)?.();
31
+ stops.delete(key);
32
+ };
33
+ const objectOf = (v) => (v && typeof v === "object" && !Array.isArray(v) ? v : {});
34
+ /** `placeholder` holds the key's place while the subscription is set up; a newer sub or unsub on the key replaces it. */
35
+ async function follow(key, topic, after, placeholder) {
36
+ const [kind, ...rest] = topic.split(".");
37
+ const name = rest.join(".");
38
+ let unsubscribe = null;
39
+ // Only the key's current subscription speaks: a superseded one may still be replaying.
40
+ const push = (data) => {
41
+ const current = stops.get(key);
42
+ if (current === placeholder || (current && current === unsubscribe))
43
+ send({ event: key, data });
44
+ };
45
+ if (kind === "chat" && name)
46
+ unsubscribe = await subscribeChat(name, after, push).catch(() => null);
47
+ else if (kind === "vibeable" && name) {
48
+ const dir = await resolveVibeable(name).then((v) => v.dir, () => null);
49
+ if (dir)
50
+ unsubscribe = subscribeVibeable(name, dir, push);
51
+ }
52
+ else {
53
+ if (stops.get(key) === placeholder)
54
+ stops.delete(key);
55
+ return send({ sub: key, error: `unknown topic ${topic}` });
56
+ }
57
+ if (!unsubscribe) {
58
+ if (stops.get(key) === placeholder)
59
+ stops.delete(key);
60
+ return send({ sub: key, error: `no such ${kind}: ${name}` });
61
+ }
62
+ // Unsubscribed (or closed) while subscribing: let go at once.
63
+ if (!conn.open || stops.get(key) !== placeholder)
64
+ return unsubscribe();
65
+ stops.set(key, unsubscribe);
66
+ }
67
+ async function handle(msg) {
68
+ if (typeof msg.call === "string") {
69
+ const r = await apiRouter.invoke(msg.call, objectOf(msg.input));
70
+ return send({ id: msg.id, status: r.status, data: r.data });
71
+ }
72
+ if (typeof msg.watch === "string") {
73
+ const key = msg.watch;
74
+ const name = String(msg.name ?? "");
75
+ if (!GET_ROUTES.has(name))
76
+ return send({ watch: key, error: `${name} cannot be watched` });
77
+ const input = objectOf(msg.input);
78
+ const interval = typeof msg.interval === "number" && msg.interval > 0 ? msg.interval : DEFAULT_INTERVAL;
79
+ stop(key);
80
+ stops.set(key, watch(`${name} ${JSON.stringify(input)}`, async () => {
81
+ const r = await apiRouter.invoke(name, input);
82
+ return { ok: r.status < 300, data: r.data };
83
+ }, interval, (data) => send({ watch: key, data })));
84
+ return;
85
+ }
86
+ if (typeof msg.unwatch === "string")
87
+ return stop(msg.unwatch);
88
+ if (typeof msg.sub === "string" && typeof msg.topic === "string") {
89
+ stop(msg.sub);
90
+ const placeholder = () => { };
91
+ stops.set(msg.sub, placeholder);
92
+ return follow(msg.sub, msg.topic, Number(msg.after) || 0, placeholder);
93
+ }
94
+ if (typeof msg.unsub === "string")
95
+ return stop(msg.unsub);
96
+ send({ id: msg.id, error: "unknown message" });
97
+ }
98
+ conn.onMessage((text) => {
99
+ let msg;
100
+ try {
101
+ msg = objectOf(JSON.parse(text));
102
+ }
103
+ catch {
104
+ return send({ error: "invalid JSON" });
105
+ }
106
+ // Every message runs in the connection's workspace, whatever started the read.
107
+ void workspace.run(() => handle(msg)).catch((err) => send({ id: msg.id, error: err.message }));
108
+ });
109
+ conn.onClose(() => {
110
+ for (const fn of stops.values())
111
+ fn();
112
+ stops.clear();
113
+ });
114
+ send({ hello: { protocol: PROTOCOL_VERSION, version } });
115
+ }
@@ -0,0 +1,4 @@
1
+ /** Apps (vibeables): folders under the vibeables root, previewed in the chat. */
2
+ export declare const vibeableRoutes: (import("./router.js").Route<"vibeables.create", import("./router.js").Reply<import("../vibeables.js").VibeableInfo>, "/api/vibeables"> | import("./router.js").Route<"vibeables.delete", {
3
+ ok: boolean;
4
+ }, "/api/vibeables/:slug"> | import("./router.js").Route<"vibeables.devStart", import("../vibeables.js").VibeableStatus, "/api/vibeables/:slug/dev/start"> | import("./router.js").Route<"vibeables.devStop", import("../vibeables.js").VibeableStatus, "/api/vibeables/:slug/dev/stop"> | import("./router.js").Route<"vibeables.events", void, "/api/vibeables/:slug/events"> | import("./router.js").Route<"vibeables.get", import("../vibeables.js").VibeableStatus, "/api/vibeables/:slug"> | import("./router.js").Route<"vibeables.list", import("../vibeables.js").VibeablesView, "/api/vibeables">)[];
@@ -0,0 +1,31 @@
1
+ import { createVibeable, deleteVibeable, listVibeables, openVibeables, resolveVibeable, startDev, stopDev, subscribeVibeable, vibeableStatus } from "../vibeables.js";
2
+ import { fail, reply, route, statusByMessage } from "./router.js";
3
+ const errors = statusByMessage(/^no vibeable/);
4
+ /** Apps (vibeables): folders under the vibeables root, previewed in the chat. */
5
+ export const vibeableRoutes = [
6
+ route({ name: "vibeables.list", method: "GET", path: "/api/vibeables", summary: "every app folder under the vibeables root" }, async () => listVibeables()),
7
+ route({ name: "vibeables.create", method: "POST", path: "/api/vibeables", summary: "create one {name} from the starter", errors: 400 }, async (c) => {
8
+ if ((await openVibeables()).off)
9
+ fail(409, "vibeables are off");
10
+ const body = await c.body();
11
+ return reply(201, await createVibeable(typeof body.name === "string" ? body.name : ""));
12
+ }),
13
+ route({ name: "vibeables.get", method: "GET", path: "/api/vibeables/:slug", summary: "how the app previews: mode, static url, dev server state", errors }, async (c) => vibeableStatus(c.params.slug)),
14
+ route({ name: "vibeables.delete", method: "DELETE", path: "/api/vibeables/:slug", summary: "remove the folder", errors }, async (c) => {
15
+ await deleteVibeable(c.params.slug);
16
+ return { ok: true };
17
+ }),
18
+ route({ name: "vibeables.devStart", method: "POST", path: "/api/vibeables/:slug/dev/start", summary: "start the app's dev command", errors }, async (c) => startDev(c.params.slug)),
19
+ route({ name: "vibeables.devStop", method: "POST", path: "/api/vibeables/:slug/dev/stop", summary: "stop the app's dev command", errors }, async (c) => stopDev(c.params.slug)),
20
+ route({ name: "vibeables.events", method: "GET", path: "/api/vibeables/:slug/events", summary: "SSE: file changes (debounced) and dev-server state", errors, raw: true }, async ({ req, res, params }) => {
21
+ const { dir } = await resolveVibeable(params.slug);
22
+ res.writeHead(200, { "content-type": "text/event-stream", "cache-control": "no-store", connection: "keep-alive" });
23
+ res.write(":ok\n\n");
24
+ const unsubscribe = subscribeVibeable(params.slug, dir, (ev) => res.write(`data: ${JSON.stringify(ev)}\n\n`));
25
+ const heartbeat = setInterval(() => res.write(":hb\n\n"), 25_000);
26
+ req.on("close", () => {
27
+ clearInterval(heartbeat);
28
+ unsubscribe();
29
+ });
30
+ }),
31
+ ];
@@ -0,0 +1,55 @@
1
+ /** The workspace itself: identity and features, settings, the other workspaces, updates. */
2
+ export declare const workspaceRoutes: (import("./router.js").Route<"meta.get", {
3
+ version: string;
4
+ diskVersion: string;
5
+ restartable: boolean;
6
+ projectName: string;
7
+ projectIcon: string;
8
+ projectColor: string;
9
+ projectNamed: boolean;
10
+ projectRoot: string;
11
+ projectRootLabel: string;
12
+ git: boolean;
13
+ repos: boolean;
14
+ vibeables: boolean;
15
+ projects: boolean;
16
+ publicUrl: string;
17
+ cloud: import("../cloud.js").CloudStatus;
18
+ switcher: import("../../config.js").SwitcherEntry[];
19
+ }, "/api/meta"> | import("./router.js").Route<"search.agents", import("../search.js").AgentSearch, "/api/search/agents"> | import("./router.js").Route<"server.restart", {
20
+ ok: boolean;
21
+ }, "/api/restart"> | import("./router.js").Route<"settings.get", import("../settings.js").SettingsView, "/api/settings"> | import("./router.js").Route<"settings.save", import("../settings.js").SettingsView, "/api/settings"> | import("./router.js").Route<"update.check", import("./router.js").Reply<{
22
+ name: string;
23
+ current: string;
24
+ latest: string;
25
+ error: string;
26
+ }> | {
27
+ name: string;
28
+ current: string;
29
+ latest: string;
30
+ error?: string;
31
+ }, "/api/update-check"> | import("./router.js").Route<"update.start", import("./router.js").Reply<{
32
+ state: "idle" | "running" | "done" | "failed";
33
+ version?: string;
34
+ log: string[];
35
+ startedAt?: string;
36
+ finishedAt?: string;
37
+ exitCode?: number | null;
38
+ name: string;
39
+ }>, "/api/update"> | import("./router.js").Route<"update.status", {
40
+ state: "idle" | "running" | "done" | "failed";
41
+ version?: string;
42
+ log: string[];
43
+ startedAt?: string;
44
+ finishedAt?: string;
45
+ exitCode?: number | null;
46
+ name: string;
47
+ available: boolean;
48
+ reason: string | undefined;
49
+ restartable: boolean;
50
+ }, "/api/update"> | import("./router.js").Route<"workspaces.forget", {
51
+ ok: boolean;
52
+ }, "/api/workspaces/forget"> | import("./router.js").Route<"workspaces.list", import("../instances.js").WorkspaceDetail[], "/api/workspaces"> | import("./router.js").Route<"workspaces.start", import("./router.js").Reply<import("../instances.js").StartResult>, "/api/workspaces/start"> | import("./router.js").Route<"workspaces.stop", import("./router.js").Reply<{
53
+ ok: boolean;
54
+ error?: string;
55
+ }>, "/api/workspaces/stop">)[];