@bpmnkit/proxy 0.0.16 → 0.0.21

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.
package/dist/index.js CHANGED
@@ -1,16 +1,20 @@
1
1
  #!/usr/bin/env node
2
- import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
2
+ import { existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, renameSync, rmSync, statSync, unlinkSync, writeFileSync, } from "node:fs";
3
3
  import http from "node:http";
4
- import { tmpdir } from "node:os";
5
- import { dirname, join } from "node:path";
4
+ import { homedir, tmpdir } from "node:os";
5
+ import { basename, dirname, extname, join, relative, sep } from "node:path";
6
6
  import { fileURLToPath } from "node:url";
7
- import { Bpmn, expand, optimize } from "@bpmnkit/core";
7
+ import { Bpmn, applyOperations, compactify, expand, optimize } from "@bpmnkit/core";
8
8
  import { createClientFromProfile } from "@bpmnkit/profiles";
9
9
  import { getActiveName, getActiveProfile, getAuthHeader, getProfile, listProfiles, } from "@bpmnkit/profiles";
10
10
  import * as claude from "./adapters/claude.js";
11
11
  import * as copilot from "./adapters/copilot.js";
12
12
  import * as gemini from "./adapters/gemini.js";
13
- import { buildIncidentSystemPrompt, buildIncidentUserMessage, buildMcpExplainPrompt, buildMcpImprovePrompt, buildMcpSystemPrompt, buildOperateChatSystemPrompt, buildSearchSystemPrompt, buildSystemPrompt, } from "./prompt.js";
13
+ import { buildImproveSystemPrompt, buildImproveUserMessage, buildIncidentSystemPrompt, buildIncidentUserMessage, buildMcpExplainPrompt, buildMcpImprovePrompt, buildMcpSystemPrompt, buildOperateChatSystemPrompt, buildSearchSystemPrompt, buildSystemPrompt, } from "./prompt.js";
14
+ import { handleDeleteRunHistory, handleGetRunHistory, handleGetRunHistoryDetail, handleRerunHistory, matchRerunHistoryRoute, matchRunHistoryRoute, } from "./routes/run-history.js";
15
+ import { handleWebhook, matchWebhookRoute, startTriggers } from "./triggers/index.js";
16
+ import { WORKER_TEMPLATES } from "./worker-templates.js";
17
+ import { startWorkerDaemon, workerState } from "./worker.js";
14
18
  const PORT = process.env.AI_SERVER_PORT ? Number(process.env.AI_SERVER_PORT) : 3033;
15
19
  // Resolve the compiled mcp-server entry point relative to this file.
16
20
  // When bundled as bundle.cjs, import.meta.url ends with .cjs → use mcp-server.cjs.
@@ -62,9 +66,132 @@ function extractCompactDiagram(text) {
62
66
  }
63
67
  return null;
64
68
  }
69
+ /**
70
+ * Extract a BpmnOperation[] from LLM text output.
71
+ * Looks for the first ```json block containing a JSON array.
72
+ */
73
+ function extractOperations(text) {
74
+ const match = /```json\s*\n([\s\S]*?)\n```/.exec(text);
75
+ if (!match?.[1])
76
+ return null;
77
+ try {
78
+ const parsed = JSON.parse(match[1]);
79
+ if (Array.isArray(parsed))
80
+ return parsed;
81
+ }
82
+ catch {
83
+ /* invalid JSON */
84
+ }
85
+ return null;
86
+ }
87
+ // ── File System helpers ───────────────────────────────────────────────────────
88
+ const SUPPORTED_EXTS = new Set([".bpmn", ".dmn", ".form", ".md"]);
89
+ /** Expand a leading `~` to the user's home directory. */
90
+ function expandHome(p) {
91
+ if (p === "~" || p.startsWith("~/"))
92
+ return homedir() + p.slice(1);
93
+ return p;
94
+ }
95
+ /** Reject any path that escapes the root via `..` or is outside it. */
96
+ function fsValidate(root, target) {
97
+ const normRoot = root.endsWith(sep) ? root : root + sep;
98
+ const normTarget = target + (statSync(target, { throwIfNoEntry: false })?.isDirectory() ? sep : "");
99
+ return (!target.includes("..") &&
100
+ (target === root || target.startsWith(normRoot) || normTarget.startsWith(normRoot)));
101
+ }
102
+ function sidecarPath(filePath) {
103
+ return join(dirname(filePath), ".bpmnkit", `${basename(filePath)}.meta.json`);
104
+ }
105
+ function readMeta(filePath) {
106
+ const sp = sidecarPath(filePath);
107
+ try {
108
+ if (!existsSync(sp))
109
+ return null;
110
+ return JSON.parse(readFileSync(sp, "utf8"));
111
+ }
112
+ catch {
113
+ return null;
114
+ }
115
+ }
116
+ function writeMeta(filePath, meta) {
117
+ const sp = sidecarPath(filePath);
118
+ const dir = dirname(sp);
119
+ if (!existsSync(dir))
120
+ mkdirSync(dir, { recursive: true });
121
+ writeFileSync(sp, JSON.stringify(meta, null, 2), "utf8");
122
+ }
123
+ function buildTree(root, dir) {
124
+ let entries;
125
+ try {
126
+ entries = readdirSync(dir, { withFileTypes: true, encoding: "utf8" });
127
+ }
128
+ catch {
129
+ return [];
130
+ }
131
+ const result = [];
132
+ for (const entry of entries) {
133
+ if (entry.name.startsWith("."))
134
+ continue;
135
+ const abs = join(dir, entry.name);
136
+ const rel = relative(root, abs);
137
+ if (entry.isDirectory()) {
138
+ result.push({
139
+ name: entry.name,
140
+ relativePath: rel,
141
+ type: "dir",
142
+ children: buildTree(root, abs),
143
+ });
144
+ }
145
+ else if (entry.isFile()) {
146
+ const ext = extname(entry.name).toLowerCase();
147
+ if (!SUPPORTED_EXTS.has(ext))
148
+ continue;
149
+ const fileType = ext.slice(1);
150
+ result.push({ name: entry.name, relativePath: rel, type: "file", fileType });
151
+ }
152
+ }
153
+ return result;
154
+ }
155
+ function collectFiles(root, dir) {
156
+ const results = [];
157
+ let entries;
158
+ try {
159
+ entries = readdirSync(dir, { withFileTypes: true, encoding: "utf8" });
160
+ }
161
+ catch {
162
+ return results;
163
+ }
164
+ for (const entry of entries) {
165
+ if (entry.name.startsWith("."))
166
+ continue;
167
+ const abs = join(dir, entry.name);
168
+ if (entry.isDirectory()) {
169
+ results.push(...collectFiles(root, abs));
170
+ }
171
+ else if (entry.isFile()) {
172
+ const ext = extname(entry.name).toLowerCase();
173
+ if (!SUPPORTED_EXTS.has(ext))
174
+ continue;
175
+ const rel = relative(root, abs);
176
+ const fileType = ext.slice(1);
177
+ let content = "";
178
+ try {
179
+ content = readFileSync(abs, "utf8");
180
+ }
181
+ catch {
182
+ /* skip unreadable files */
183
+ }
184
+ const meta = readMeta(abs);
185
+ const nameNoExt = basename(entry.name, extname(entry.name));
186
+ results.push({ relativePath: rel, name: nameNoExt, absPath: abs, fileType, content, meta });
187
+ }
188
+ }
189
+ return results;
190
+ }
191
+ // ─────────────────────────────────────────────────────────────────────────────
65
192
  const server = http.createServer(async (req, res) => {
66
193
  res.setHeader("Access-Control-Allow-Origin", "*");
67
- res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
194
+ res.setHeader("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS");
68
195
  res.setHeader("Access-Control-Allow-Headers", "Content-Type, X-Profile");
69
196
  if (req.method === "OPTIONS") {
70
197
  res.writeHead(204);
@@ -78,7 +205,48 @@ const server = http.createServer(async (req, res) => {
78
205
  const names = available.map((a) => a.name);
79
206
  console.log(`[server] /status → available: [${names.join(", ")}]`);
80
207
  res.writeHead(200, { "Content-Type": "application/json" });
81
- res.end(JSON.stringify({ ready: available.length > 0, backend: names[0] ?? null, available: names }));
208
+ res.end(JSON.stringify({
209
+ ready: available.length > 0,
210
+ backend: names[0] ?? null,
211
+ available: names,
212
+ workers: {
213
+ active: workerState.active,
214
+ jobTypes: workerState.jobTypes,
215
+ pollCount: workerState.pollCount,
216
+ lastError: workerState.lastError,
217
+ },
218
+ }));
219
+ return;
220
+ }
221
+ // ── GET /worker-templates — built-in element templates for Studio ────────
222
+ if (url.pathname === "/worker-templates" && req.method === "GET") {
223
+ res.writeHead(200, { "Content-Type": "application/json" });
224
+ res.end(JSON.stringify(WORKER_TEMPLATES));
225
+ return;
226
+ }
227
+ // ── Run history routes ────────────────────────────────────────────────────
228
+ if (url.pathname === "/run-history" && req.method === "GET") {
229
+ handleGetRunHistory(req, res);
230
+ return;
231
+ }
232
+ if (url.pathname === "/run-history" && req.method === "DELETE") {
233
+ handleDeleteRunHistory(req, res);
234
+ return;
235
+ }
236
+ const rerunMatch = matchRerunHistoryRoute(req);
237
+ if (rerunMatch) {
238
+ await handleRerunHistory(req, res, rerunMatch.id);
239
+ return;
240
+ }
241
+ const runHistoryMatch = matchRunHistoryRoute(req);
242
+ if (runHistoryMatch && req.method === "GET") {
243
+ handleGetRunHistoryDetail(req, res, runHistoryMatch.id);
244
+ return;
245
+ }
246
+ // ── POST /webhooks/:processId — webhook trigger ───────────────────────────
247
+ const webhookMatch = matchWebhookRoute(req);
248
+ if (webhookMatch) {
249
+ await handleWebhook(req, res, webhookMatch.processId);
82
250
  return;
83
251
  }
84
252
  if (url.pathname === "/chat" && req.method === "POST") {
@@ -260,6 +428,8 @@ const server = http.createServer(async (req, res) => {
260
428
  apiType: p.apiType,
261
429
  baseUrl: p.config.baseUrl ?? null,
262
430
  authType: p.config.auth?.type ?? "none",
431
+ description: p.description,
432
+ tags: p.tags,
263
433
  }));
264
434
  res.writeHead(200, { "Content-Type": "application/json" });
265
435
  res.end(JSON.stringify(payload));
@@ -731,15 +901,136 @@ const server = http.createServer(async (req, res) => {
731
901
  res.end(JSON.stringify({ endpoint: finalSpec.endpoint, filter: finalSpec.filter, items, total }));
732
902
  return;
733
903
  }
904
+ // ── POST /improve — structured AI-assisted BPMN improvement ─────────────────
905
+ // Token-efficient alternative to /chat?action=improve.
906
+ // Phase 1: optimize() auto-fix (no AI). Phase 2: AI outputs BpmnOperation[].
907
+ // Emits SSE: tokens (explanation) + ops event + xml event + done.
908
+ if (url.pathname === "/improve" && req.method === "POST") {
909
+ const body = await readBody(req);
910
+ let context;
911
+ let instruction;
912
+ let backend;
913
+ try {
914
+ const parsed = JSON.parse(body);
915
+ context = parsed.context;
916
+ instruction = parsed.instruction ?? null;
917
+ backend = parsed.backend ?? null;
918
+ }
919
+ catch {
920
+ res.writeHead(400);
921
+ res.end("Bad Request");
922
+ return;
923
+ }
924
+ const available = await detectAll();
925
+ const detected = backend
926
+ ? (available.find((a) => a.name === backend) ?? available[0])
927
+ : available[0];
928
+ if (!detected) {
929
+ res.writeHead(503);
930
+ res.end("No AI CLI available. Install claude, copilot, or gemini.");
931
+ return;
932
+ }
933
+ // ── Phase 1: auto-fix ─────────────────────────────────────────────────
934
+ let fixedCompact = context;
935
+ let autoFixCount = 0;
936
+ try {
937
+ const defs = expand(context);
938
+ const report = optimize(defs);
939
+ const fixable = report.findings
940
+ .filter((f) => f.applyFix)
941
+ .sort((a, b) => {
942
+ const ord = { error: 0, warning: 1, info: 2 };
943
+ return (ord[a.severity] ?? 2) - (ord[b.severity] ?? 2);
944
+ });
945
+ for (const f of fixable)
946
+ f.applyFix?.(defs);
947
+ autoFixCount = fixable.length;
948
+ if (autoFixCount > 0) {
949
+ fixedCompact = compactify(defs);
950
+ console.log(`[server] /improve → auto-fixed ${autoFixCount} issue(s)`);
951
+ }
952
+ }
953
+ catch (err) {
954
+ console.error("[server] /improve auto-fix failed:", String(err));
955
+ }
956
+ // ── Phase 2: collect remaining findings ───────────────────────────────
957
+ const findings = [];
958
+ try {
959
+ const remaining = optimize(expand(fixedCompact));
960
+ for (const f of remaining.findings) {
961
+ findings.push({
962
+ category: f.category,
963
+ severity: f.severity,
964
+ message: f.message,
965
+ suggestion: f.suggestion,
966
+ elementIds: f.elementIds,
967
+ });
968
+ }
969
+ }
970
+ catch {
971
+ /* non-fatal */
972
+ }
973
+ console.log(`[server] /improve → adapter: ${detected.name}, findings: ${findings.length}, autoFix: ${autoFixCount}`);
974
+ res.writeHead(200, {
975
+ "Content-Type": "text/event-stream",
976
+ "Cache-Control": "no-cache",
977
+ Connection: "keep-alive",
978
+ });
979
+ // ── Phase 3: AI call — outputs explanation + ```json operations block ─
980
+ const systemPrompt = buildImproveSystemPrompt();
981
+ const improveCtx = {
982
+ compact: fixedCompact,
983
+ findings,
984
+ autoFixCount,
985
+ instruction,
986
+ };
987
+ const userMessage = buildImproveUserMessage(improveCtx);
988
+ const accumulated = [];
989
+ try {
990
+ await detected.adapter.stream([{ role: "user", content: userMessage }], systemPrompt, null, (token) => {
991
+ accumulated.push(token);
992
+ res.write(`data: ${JSON.stringify({ type: "token", text: token })}\n\n`);
993
+ });
994
+ }
995
+ catch (err) {
996
+ const msg = err instanceof Error ? err.message : String(err);
997
+ console.error(`[server] /improve adapter error: ${msg}`);
998
+ res.write(`data: ${JSON.stringify({ type: "error", message: msg })}\n\n`);
999
+ res.write(`data: ${JSON.stringify({ type: "done" })}\n\n`);
1000
+ res.end();
1001
+ return;
1002
+ }
1003
+ // ── Phase 4: parse ops, apply, expand, emit ───────────────────────────
1004
+ const fullText = accumulated.join("");
1005
+ const ops = extractOperations(fullText) ?? [];
1006
+ res.write(`data: ${JSON.stringify({ type: "ops", ops, autoFixCount })}\n\n`);
1007
+ if (ops.length > 0 || autoFixCount > 0) {
1008
+ try {
1009
+ const finalCompact = ops.length > 0 ? applyOperations(fixedCompact, ops) : fixedCompact;
1010
+ const xml = Bpmn.export(expand(finalCompact));
1011
+ res.write(`data: ${JSON.stringify({ type: "xml", xml })}\n\n`);
1012
+ console.log(`[server] /improve → ${ops.length} ops applied, XML emitted`);
1013
+ }
1014
+ catch (err) {
1015
+ console.error("[server] /improve expand failed:", String(err));
1016
+ res.write(`data: ${JSON.stringify({ type: "error", message: `Failed to apply operations: ${String(err)}` })}\n\n`);
1017
+ }
1018
+ }
1019
+ res.write(`data: ${JSON.stringify({ type: "done" })}\n\n`);
1020
+ res.end();
1021
+ return;
1022
+ }
734
1023
  // ── POST /operate/chat — operations-context AI chat ──────────────────────────
735
1024
  if (url.pathname === "/operate/chat" && req.method === "POST") {
736
1025
  const body = await readBody(req);
737
1026
  let messages;
738
1027
  let stats;
1028
+ let backend;
739
1029
  try {
740
1030
  const parsed = JSON.parse(body);
741
1031
  messages = parsed.messages;
742
1032
  stats = parsed.stats ?? null;
1033
+ backend = parsed.backend ?? null;
743
1034
  }
744
1035
  catch {
745
1036
  res.writeHead(400);
@@ -747,7 +1038,9 @@ const server = http.createServer(async (req, res) => {
747
1038
  return;
748
1039
  }
749
1040
  const available = await detectAll();
750
- const detected = available[0];
1041
+ const detected = backend
1042
+ ? (available.find((a) => a.name === backend) ?? available[0])
1043
+ : available[0];
751
1044
  if (!detected) {
752
1045
  res.writeHead(503);
753
1046
  res.end("No AI adapter available. Install claude, copilot, or gemini.");
@@ -772,6 +1065,245 @@ const server = http.createServer(async (req, res) => {
772
1065
  res.end();
773
1066
  return;
774
1067
  }
1068
+ // ── File System API (/fs/*) ───────────────────────────────────────────────
1069
+ if (url.pathname.startsWith("/fs/")) {
1070
+ const fsPath = url.pathname.slice("/fs".length); // e.g. "/tree", "/list", "/read"
1071
+ // GET /fs/tree?root=<abs> — lightweight directory tree
1072
+ if (fsPath === "/tree" && req.method === "GET") {
1073
+ const rawRoot = url.searchParams.get("root") ?? "";
1074
+ const root = expandHome(rawRoot);
1075
+ const rootExists = root !== "" && existsSync(root);
1076
+ console.log(`[fs/tree] raw param: ${JSON.stringify(rawRoot)}`);
1077
+ console.log(`[fs/tree] expanded: ${JSON.stringify(root)}`);
1078
+ console.log(`[fs/tree] existsSync: ${rootExists}`);
1079
+ if (!rootExists) {
1080
+ console.log("[fs/tree] → 404 not found");
1081
+ res.writeHead(404, { "Content-Type": "application/json" });
1082
+ res.end(JSON.stringify({ error: "Project root not found" }));
1083
+ return;
1084
+ }
1085
+ const tree = buildTree(root, root);
1086
+ console.log(`[fs/tree] → 200 ok, ${tree.length} entries`);
1087
+ res.writeHead(200, { "Content-Type": "application/json" });
1088
+ res.end(JSON.stringify(tree));
1089
+ return;
1090
+ }
1091
+ // GET /fs/list?root=<abs> — all files with content + metadata
1092
+ if (fsPath === "/list" && req.method === "GET") {
1093
+ const root = expandHome(url.searchParams.get("root") ?? "");
1094
+ if (!root || !existsSync(root)) {
1095
+ res.writeHead(404, { "Content-Type": "application/json" });
1096
+ res.end(JSON.stringify({ error: "Project root not found" }));
1097
+ return;
1098
+ }
1099
+ const files = collectFiles(root, root);
1100
+ res.writeHead(200, { "Content-Type": "application/json" });
1101
+ res.end(JSON.stringify(files));
1102
+ return;
1103
+ }
1104
+ // GET /fs/read?path=<abs> — read single file content
1105
+ if (fsPath === "/read" && req.method === "GET") {
1106
+ const filePath = expandHome(url.searchParams.get("path") ?? "");
1107
+ if (!filePath || !existsSync(filePath)) {
1108
+ res.writeHead(404, { "Content-Type": "application/json" });
1109
+ res.end(JSON.stringify({ error: "File not found" }));
1110
+ return;
1111
+ }
1112
+ let content;
1113
+ try {
1114
+ content = readFileSync(filePath, "utf8");
1115
+ }
1116
+ catch (err) {
1117
+ res.writeHead(500, { "Content-Type": "application/json" });
1118
+ res.end(JSON.stringify({ error: String(err) }));
1119
+ return;
1120
+ }
1121
+ res.writeHead(200, { "Content-Type": "application/json" });
1122
+ res.end(JSON.stringify({ content }));
1123
+ return;
1124
+ }
1125
+ // POST /fs/write — write file (creates parent directories)
1126
+ if (fsPath === "/write" && req.method === "POST") {
1127
+ const body = await readBody(req);
1128
+ let filePath;
1129
+ let content;
1130
+ try {
1131
+ const parsed = JSON.parse(body);
1132
+ filePath = expandHome(parsed.path);
1133
+ content = parsed.content;
1134
+ }
1135
+ catch {
1136
+ res.writeHead(400, { "Content-Type": "application/json" });
1137
+ res.end(JSON.stringify({ error: "Invalid JSON body" }));
1138
+ return;
1139
+ }
1140
+ if (!filePath) {
1141
+ res.writeHead(400, { "Content-Type": "application/json" });
1142
+ res.end(JSON.stringify({ error: "Missing path" }));
1143
+ return;
1144
+ }
1145
+ try {
1146
+ const dir = dirname(filePath);
1147
+ if (!existsSync(dir))
1148
+ mkdirSync(dir, { recursive: true });
1149
+ writeFileSync(filePath, content, "utf8");
1150
+ res.writeHead(200, { "Content-Type": "application/json" });
1151
+ res.end(JSON.stringify({ ok: true }));
1152
+ }
1153
+ catch (err) {
1154
+ res.writeHead(500, { "Content-Type": "application/json" });
1155
+ res.end(JSON.stringify({ error: String(err) }));
1156
+ }
1157
+ return;
1158
+ }
1159
+ // DELETE /fs/file?path=<abs> — delete file and its sidecar
1160
+ if (fsPath === "/file" && req.method === "DELETE") {
1161
+ const filePath = expandHome(url.searchParams.get("path") ?? "");
1162
+ if (!filePath) {
1163
+ res.writeHead(400, { "Content-Type": "application/json" });
1164
+ res.end(JSON.stringify({ error: "Missing path" }));
1165
+ return;
1166
+ }
1167
+ try {
1168
+ if (existsSync(filePath))
1169
+ unlinkSync(filePath);
1170
+ const sp = sidecarPath(filePath);
1171
+ if (existsSync(sp))
1172
+ unlinkSync(sp);
1173
+ res.writeHead(200, { "Content-Type": "application/json" });
1174
+ res.end(JSON.stringify({ ok: true }));
1175
+ }
1176
+ catch (err) {
1177
+ res.writeHead(500, { "Content-Type": "application/json" });
1178
+ res.end(JSON.stringify({ error: String(err) }));
1179
+ }
1180
+ return;
1181
+ }
1182
+ // POST /fs/move — rename/move file and its sidecar
1183
+ if (fsPath === "/move" && req.method === "POST") {
1184
+ const body = await readBody(req);
1185
+ let from;
1186
+ let to;
1187
+ try {
1188
+ const parsed = JSON.parse(body);
1189
+ from = expandHome(parsed.from);
1190
+ to = expandHome(parsed.to);
1191
+ }
1192
+ catch {
1193
+ res.writeHead(400, { "Content-Type": "application/json" });
1194
+ res.end(JSON.stringify({ error: "Invalid JSON body" }));
1195
+ return;
1196
+ }
1197
+ if (!from || !to) {
1198
+ res.writeHead(400, { "Content-Type": "application/json" });
1199
+ res.end(JSON.stringify({ error: "Missing from/to" }));
1200
+ return;
1201
+ }
1202
+ try {
1203
+ const toDir = dirname(to);
1204
+ if (!existsSync(toDir))
1205
+ mkdirSync(toDir, { recursive: true });
1206
+ renameSync(from, to);
1207
+ // Move sidecar if it exists
1208
+ const fromSidecar = sidecarPath(from);
1209
+ const toSidecar = sidecarPath(to);
1210
+ if (existsSync(fromSidecar)) {
1211
+ const toSidecarDir = dirname(toSidecar);
1212
+ if (!existsSync(toSidecarDir))
1213
+ mkdirSync(toSidecarDir, { recursive: true });
1214
+ renameSync(fromSidecar, toSidecar);
1215
+ }
1216
+ res.writeHead(200, { "Content-Type": "application/json" });
1217
+ res.end(JSON.stringify({ ok: true }));
1218
+ }
1219
+ catch (err) {
1220
+ res.writeHead(500, { "Content-Type": "application/json" });
1221
+ res.end(JSON.stringify({ error: String(err) }));
1222
+ }
1223
+ return;
1224
+ }
1225
+ // POST /fs/mkdir — create directory
1226
+ if (fsPath === "/mkdir" && req.method === "POST") {
1227
+ const body = await readBody(req);
1228
+ let dirPath;
1229
+ try {
1230
+ const parsed = JSON.parse(body);
1231
+ dirPath = expandHome(parsed.path);
1232
+ }
1233
+ catch {
1234
+ res.writeHead(400, { "Content-Type": "application/json" });
1235
+ res.end(JSON.stringify({ error: "Invalid JSON body" }));
1236
+ return;
1237
+ }
1238
+ if (!dirPath) {
1239
+ res.writeHead(400, { "Content-Type": "application/json" });
1240
+ res.end(JSON.stringify({ error: "Missing path" }));
1241
+ return;
1242
+ }
1243
+ try {
1244
+ mkdirSync(dirPath, { recursive: true });
1245
+ res.writeHead(200, { "Content-Type": "application/json" });
1246
+ res.end(JSON.stringify({ ok: true }));
1247
+ }
1248
+ catch (err) {
1249
+ res.writeHead(500, { "Content-Type": "application/json" });
1250
+ res.end(JSON.stringify({ error: String(err) }));
1251
+ }
1252
+ return;
1253
+ }
1254
+ // GET /fs/meta?path=<abs> — read sidecar metadata
1255
+ if (fsPath === "/meta" && req.method === "GET") {
1256
+ const filePath = expandHome(url.searchParams.get("path") ?? "");
1257
+ if (!filePath) {
1258
+ res.writeHead(400, { "Content-Type": "application/json" });
1259
+ res.end(JSON.stringify({ error: "Missing path" }));
1260
+ return;
1261
+ }
1262
+ const meta = readMeta(filePath);
1263
+ if (!meta) {
1264
+ res.writeHead(404, { "Content-Type": "application/json" });
1265
+ res.end(JSON.stringify({ error: "No metadata found" }));
1266
+ return;
1267
+ }
1268
+ res.writeHead(200, { "Content-Type": "application/json" });
1269
+ res.end(JSON.stringify(meta));
1270
+ return;
1271
+ }
1272
+ // POST /fs/meta — write sidecar metadata
1273
+ if (fsPath === "/meta" && req.method === "POST") {
1274
+ const body = await readBody(req);
1275
+ let filePath;
1276
+ let meta;
1277
+ try {
1278
+ const parsed = JSON.parse(body);
1279
+ filePath = expandHome(parsed.path);
1280
+ meta = parsed.meta;
1281
+ }
1282
+ catch {
1283
+ res.writeHead(400, { "Content-Type": "application/json" });
1284
+ res.end(JSON.stringify({ error: "Invalid JSON body" }));
1285
+ return;
1286
+ }
1287
+ if (!filePath || !meta) {
1288
+ res.writeHead(400, { "Content-Type": "application/json" });
1289
+ res.end(JSON.stringify({ error: "Missing path or meta" }));
1290
+ return;
1291
+ }
1292
+ try {
1293
+ writeMeta(filePath, meta);
1294
+ res.writeHead(200, { "Content-Type": "application/json" });
1295
+ res.end(JSON.stringify({ ok: true }));
1296
+ }
1297
+ catch (err) {
1298
+ res.writeHead(500, { "Content-Type": "application/json" });
1299
+ res.end(JSON.stringify({ error: String(err) }));
1300
+ }
1301
+ return;
1302
+ }
1303
+ res.writeHead(404);
1304
+ res.end("FS route not found");
1305
+ return;
1306
+ }
775
1307
  // ── ALL /api/* — transparent Camunda API proxy ─────────────────────────────
776
1308
  if (url.pathname.startsWith("/api/")) {
777
1309
  const profileName = req.headers["x-profile"];
@@ -823,11 +1355,116 @@ const server = http.createServer(async (req, res) => {
823
1355
  res.end(await upstream.text());
824
1356
  return;
825
1357
  }
1358
+ // ── POST /secrets/check — bulk existence check for secret names ───────────
1359
+ if (url.pathname === "/secrets/check" && req.method === "POST") {
1360
+ const body = await readBody(req);
1361
+ let names = [];
1362
+ try {
1363
+ names = (JSON.parse(body).names ?? []);
1364
+ }
1365
+ catch {
1366
+ /* treat as empty list */
1367
+ }
1368
+ const result = {};
1369
+ for (const name of names) {
1370
+ result[name] = process.env[name] !== undefined;
1371
+ }
1372
+ res.writeHead(200, { "Content-Type": "application/json" });
1373
+ res.end(JSON.stringify(result));
1374
+ return;
1375
+ }
1376
+ // ── POST /secrets/:name — resolve and encrypt a secret for the client ─────
1377
+ if (url.pathname.startsWith("/secrets/") && req.method === "POST") {
1378
+ const name = url.pathname.slice("/secrets/".length);
1379
+ if (!name) {
1380
+ res.writeHead(400, { "Content-Type": "application/json" });
1381
+ res.end(JSON.stringify({ error: "Secret name required" }));
1382
+ return;
1383
+ }
1384
+ const secretValue = process.env[name];
1385
+ if (secretValue === undefined) {
1386
+ res.writeHead(404, { "Content-Type": "application/json" });
1387
+ res.end(JSON.stringify({ error: `Secret "${name}" is not configured` }));
1388
+ return;
1389
+ }
1390
+ const body = await readBody(req);
1391
+ let keyBase64;
1392
+ try {
1393
+ const parsed = JSON.parse(body);
1394
+ if (typeof parsed.key !== "string" || !parsed.key)
1395
+ throw new Error("missing key");
1396
+ keyBase64 = parsed.key;
1397
+ }
1398
+ catch {
1399
+ res.writeHead(400, { "Content-Type": "application/json" });
1400
+ res.end(JSON.stringify({ error: "Body must be { key: string } with a base64 AES-256 key" }));
1401
+ return;
1402
+ }
1403
+ try {
1404
+ const rawKey = Buffer.from(keyBase64, "base64");
1405
+ const cryptoKey = await globalThis.crypto.subtle.importKey("raw", rawKey, { name: "AES-GCM" }, false, ["encrypt"]);
1406
+ const iv = globalThis.crypto.getRandomValues(new Uint8Array(12));
1407
+ const encrypted = await globalThis.crypto.subtle.encrypt({ name: "AES-GCM", iv }, cryptoKey, new TextEncoder().encode(secretValue));
1408
+ res.writeHead(200, { "Content-Type": "application/json" });
1409
+ res.end(JSON.stringify({
1410
+ encrypted: Buffer.from(encrypted).toString("base64"),
1411
+ iv: Buffer.from(iv).toString("base64"),
1412
+ }));
1413
+ }
1414
+ catch (err) {
1415
+ console.error(`[secrets] encryption failed for "${name}":`, err);
1416
+ res.writeHead(500, { "Content-Type": "application/json" });
1417
+ res.end(JSON.stringify({ error: "Encryption failed" }));
1418
+ }
1419
+ return;
1420
+ }
1421
+ // ── POST /http-request — CORS bypass for wasm worker REST connectors ─────
1422
+ if (url.pathname === "/http-request" && req.method === "POST") {
1423
+ const body = await readBody(req);
1424
+ let targetUrl;
1425
+ let method;
1426
+ let headers;
1427
+ let reqBody;
1428
+ try {
1429
+ const parsed = JSON.parse(body);
1430
+ targetUrl = parsed.url;
1431
+ method = (parsed.method ?? "GET").toUpperCase();
1432
+ headers = parsed.headers ?? {};
1433
+ reqBody = parsed.body;
1434
+ }
1435
+ catch {
1436
+ res.writeHead(400, { "Content-Type": "application/json" });
1437
+ res.end(JSON.stringify({ error: "Invalid JSON body" }));
1438
+ return;
1439
+ }
1440
+ console.log(`[http-request] ${method} ${targetUrl}`);
1441
+ try {
1442
+ const upstream = await fetch(targetUrl, {
1443
+ method,
1444
+ headers,
1445
+ body: method !== "GET" && method !== "HEAD" ? reqBody : undefined,
1446
+ });
1447
+ const responseText = await upstream.text();
1448
+ const contentType = upstream.headers.get("content-type") ?? "application/json";
1449
+ res.writeHead(upstream.status, {
1450
+ "Content-Type": contentType,
1451
+ "Access-Control-Allow-Origin": "*",
1452
+ });
1453
+ res.end(responseText);
1454
+ }
1455
+ catch (err) {
1456
+ res.writeHead(502, { "Content-Type": "application/json" });
1457
+ res.end(JSON.stringify({ error: `Upstream unreachable: ${String(err)}` }));
1458
+ }
1459
+ return;
1460
+ }
826
1461
  res.writeHead(404);
827
1462
  res.end("Not Found");
828
1463
  });
829
1464
  server.listen(PORT, () => {
830
1465
  console.log(`BPMN Kit AI Server running at http://localhost:${PORT}`);
831
1466
  console.log("Press Ctrl+C to stop");
1467
+ startWorkerDaemon();
1468
+ startTriggers();
832
1469
  });
833
1470
  //# sourceMappingURL=index.js.map