@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/README.md +2 -0
- package/dist/adapters/copilot.js +1 -1
- package/dist/adapters/gemini.js +1 -1
- package/dist/index.js +645 -8
- package/dist/prompt.js +57 -0
- package/dist/routes/run-history.js +307 -0
- package/dist/triggers/file-watcher.js +215 -0
- package/dist/triggers/index.js +18 -0
- package/dist/triggers/timer.js +239 -0
- package/dist/triggers/webhook.js +76 -0
- package/dist/worker-templates.js +612 -0
- package/dist/worker.js +166 -0
- package/dist/workers/cli.js +72 -0
- package/dist/workers/email.js +102 -0
- package/dist/workers/fs.js +90 -0
- package/dist/workers/http.js +46 -0
- package/dist/workers/js.js +21 -0
- package/dist/workers/llm.js +55 -0
- package/package.json +8 -4
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({
|
|
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 =
|
|
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
|