@bpmnkit/proxy 0.0.8
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/adapters/claude.js +108 -0
- package/dist/adapters/copilot.js +46 -0
- package/dist/adapters/gemini.js +43 -0
- package/dist/apply-ops.js +79 -0
- package/dist/bridge.bundle.js +5298 -0
- package/dist/bridge.js +226 -0
- package/dist/index.js +592 -0
- package/dist/mcp-server.js +615 -0
- package/dist/prompt.js +189 -0
- package/package.json +35 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,592 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import http from "node:http";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { dirname, join } from "node:path";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
import { Bpmn, expand, optimize } from "@bpmnkit/core";
|
|
8
|
+
import { createClientFromProfile } from "@bpmnkit/profiles";
|
|
9
|
+
import { getActiveName, getActiveProfile, getAuthHeader, getProfile, listProfiles, } from "@bpmnkit/profiles";
|
|
10
|
+
import * as claude from "./adapters/claude.js";
|
|
11
|
+
import * as copilot from "./adapters/copilot.js";
|
|
12
|
+
import * as gemini from "./adapters/gemini.js";
|
|
13
|
+
import { buildIncidentSystemPrompt, buildIncidentUserMessage, buildMcpExplainPrompt, buildMcpImprovePrompt, buildMcpSystemPrompt, buildSystemPrompt, } from "./prompt.js";
|
|
14
|
+
const PORT = process.env.AI_SERVER_PORT ? Number(process.env.AI_SERVER_PORT) : 3033;
|
|
15
|
+
// Resolve the compiled mcp-server entry point relative to this file.
|
|
16
|
+
// When bundled as bundle.cjs, import.meta.url ends with .cjs → use mcp-server.cjs.
|
|
17
|
+
// When compiled by tsc to dist/index.js → use mcp-server.js.
|
|
18
|
+
const __file = fileURLToPath(import.meta.url);
|
|
19
|
+
const mcpServerFile = __file.endsWith(".cjs") ? "mcp-server.cjs" : "mcp-server.js";
|
|
20
|
+
const MCP_SERVER_PATH = join(dirname(__file), mcpServerFile);
|
|
21
|
+
async function detectAll() {
|
|
22
|
+
const results = await Promise.all([
|
|
23
|
+
claude
|
|
24
|
+
.available()
|
|
25
|
+
.then((ok) => (ok ? { adapter: claude, name: "claude" } : null)),
|
|
26
|
+
copilot
|
|
27
|
+
.available()
|
|
28
|
+
.then((ok) => (ok ? { adapter: copilot, name: "copilot" } : null)),
|
|
29
|
+
gemini
|
|
30
|
+
.available()
|
|
31
|
+
.then((ok) => (ok ? { adapter: gemini, name: "gemini" } : null)),
|
|
32
|
+
]);
|
|
33
|
+
return results.filter((r) => r !== null);
|
|
34
|
+
}
|
|
35
|
+
async function readBody(req) {
|
|
36
|
+
return new Promise((resolve, reject) => {
|
|
37
|
+
const chunks = [];
|
|
38
|
+
req.on("data", (chunk) => chunks.push(chunk));
|
|
39
|
+
req.on("end", () => resolve(Buffer.concat(chunks).toString()));
|
|
40
|
+
req.on("error", reject);
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Extract a CompactDiagram from LLM text output (fallback for non-MCP adapters).
|
|
45
|
+
* Looks for the first ```json block containing a "processes" array.
|
|
46
|
+
*/
|
|
47
|
+
function extractCompactDiagram(text) {
|
|
48
|
+
const match = /```json\s*\n([\s\S]*?)\n```/.exec(text);
|
|
49
|
+
if (!match?.[1])
|
|
50
|
+
return null;
|
|
51
|
+
try {
|
|
52
|
+
const parsed = JSON.parse(match[1]);
|
|
53
|
+
if (typeof parsed === "object" &&
|
|
54
|
+
parsed !== null &&
|
|
55
|
+
"processes" in parsed &&
|
|
56
|
+
Array.isArray(parsed.processes)) {
|
|
57
|
+
return parsed;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
/* invalid JSON */
|
|
62
|
+
}
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
const server = http.createServer(async (req, res) => {
|
|
66
|
+
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
67
|
+
res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
|
|
68
|
+
res.setHeader("Access-Control-Allow-Headers", "Content-Type, X-Profile");
|
|
69
|
+
if (req.method === "OPTIONS") {
|
|
70
|
+
res.writeHead(204);
|
|
71
|
+
res.end();
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
const url = new URL(req.url ?? "/", `http://localhost:${PORT}`);
|
|
75
|
+
console.log(`[server] ${req.method} ${url.pathname}`);
|
|
76
|
+
if (url.pathname === "/status" && req.method === "GET") {
|
|
77
|
+
const available = await detectAll();
|
|
78
|
+
const names = available.map((a) => a.name);
|
|
79
|
+
console.log(`[server] /status → available: [${names.join(", ")}]`);
|
|
80
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
81
|
+
res.end(JSON.stringify({ ready: available.length > 0, backend: names[0] ?? null, available: names }));
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
if (url.pathname === "/chat" && req.method === "POST") {
|
|
85
|
+
const body = await readBody(req);
|
|
86
|
+
let messages;
|
|
87
|
+
let context;
|
|
88
|
+
let backend;
|
|
89
|
+
let action;
|
|
90
|
+
try {
|
|
91
|
+
const parsed = JSON.parse(body);
|
|
92
|
+
messages = parsed.messages;
|
|
93
|
+
context = parsed.context ?? null;
|
|
94
|
+
backend = parsed.backend ?? null;
|
|
95
|
+
action = parsed.action ?? null;
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
res.writeHead(400);
|
|
99
|
+
res.end("Bad Request");
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
const available = await detectAll();
|
|
103
|
+
const detected = backend
|
|
104
|
+
? (available.find((a) => a.name === backend) ?? available[0])
|
|
105
|
+
: available[0];
|
|
106
|
+
if (!detected) {
|
|
107
|
+
console.log("[server] /chat → no adapter available");
|
|
108
|
+
res.writeHead(503);
|
|
109
|
+
res.end("No AI CLI available. Install claude, copilot, or gemini.");
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
console.log(`[server] /chat → adapter: ${detected.name}, action: ${action ?? "chat"}, mcp: ${detected.adapter.supportsMcp}`);
|
|
113
|
+
const currentCompact = context !== null && typeof context === "object" && "processes" in context
|
|
114
|
+
? context
|
|
115
|
+
: null;
|
|
116
|
+
// ── Apply auto-fixes, then collect remaining findings for improve ─────────
|
|
117
|
+
const findings = [];
|
|
118
|
+
// fixedDefs holds the auto-fixed diagram; used as input for the AI
|
|
119
|
+
const fixedDefs = currentCompact ? expand(currentCompact) : null;
|
|
120
|
+
if (currentCompact && fixedDefs) {
|
|
121
|
+
try {
|
|
122
|
+
const report = optimize(fixedDefs);
|
|
123
|
+
const order = { error: 0, warning: 1, info: 2 };
|
|
124
|
+
const fixable = report.findings
|
|
125
|
+
.filter((f) => f.applyFix)
|
|
126
|
+
.sort((a, b) => (order[a.severity] ?? 2) - (order[b.severity] ?? 2));
|
|
127
|
+
for (const f of fixable) {
|
|
128
|
+
f.applyFix?.(fixedDefs);
|
|
129
|
+
}
|
|
130
|
+
if (fixable.length > 0) {
|
|
131
|
+
console.log(`[server] auto-applied ${fixable.length} fix(es) from core optimize()`);
|
|
132
|
+
}
|
|
133
|
+
if (action === "improve") {
|
|
134
|
+
const remaining = optimize(fixedDefs);
|
|
135
|
+
for (const f of remaining.findings) {
|
|
136
|
+
findings.push({
|
|
137
|
+
category: f.category,
|
|
138
|
+
severity: f.severity,
|
|
139
|
+
message: f.message,
|
|
140
|
+
suggestion: f.suggestion,
|
|
141
|
+
elementIds: f.elementIds,
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
console.log(`[server] improve → ${findings.length} remaining findings after auto-fix`);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
catch (err) {
|
|
148
|
+
console.error("[server] auto-fix failed:", String(err));
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
// ── Build system prompt ───────────────────────────────────────────────────
|
|
152
|
+
let systemPrompt;
|
|
153
|
+
if (detected.adapter.supportsMcp) {
|
|
154
|
+
systemPrompt =
|
|
155
|
+
action === "improve"
|
|
156
|
+
? buildMcpImprovePrompt(findings)
|
|
157
|
+
: action === "explain"
|
|
158
|
+
? buildMcpExplainPrompt()
|
|
159
|
+
: buildMcpSystemPrompt();
|
|
160
|
+
}
|
|
161
|
+
else {
|
|
162
|
+
// Fallback for non-MCP adapters: full prompt with format instructions
|
|
163
|
+
systemPrompt = buildSystemPrompt(context);
|
|
164
|
+
}
|
|
165
|
+
// ── Set up MCP temp files (MCP-capable adapters only) ────────────────────
|
|
166
|
+
let tmpDir = null;
|
|
167
|
+
let mcpConfigFile = null;
|
|
168
|
+
let outputFile = null;
|
|
169
|
+
if (detected.adapter.supportsMcp) {
|
|
170
|
+
tmpDir = mkdtempSync(join(tmpdir(), "bpmnkit-mcp-"));
|
|
171
|
+
const inputFile = join(tmpDir, "input.json");
|
|
172
|
+
outputFile = join(tmpDir, "output.json");
|
|
173
|
+
mcpConfigFile = join(tmpDir, "mcp.json");
|
|
174
|
+
// Write input as BPMN XML (mcp-server reads XML, not CompactDiagram JSON)
|
|
175
|
+
// Use fixedDefs if available (auto-fixes already applied); fall back to raw expand
|
|
176
|
+
if (fixedDefs)
|
|
177
|
+
writeFileSync(inputFile, Bpmn.export(fixedDefs));
|
|
178
|
+
else if (currentCompact)
|
|
179
|
+
writeFileSync(inputFile, Bpmn.export(expand(currentCompact)));
|
|
180
|
+
const mcpConfig = {
|
|
181
|
+
mcpServers: {
|
|
182
|
+
bpmn: {
|
|
183
|
+
type: "stdio",
|
|
184
|
+
command: "node",
|
|
185
|
+
args: [
|
|
186
|
+
MCP_SERVER_PATH,
|
|
187
|
+
...(currentCompact ? ["--input", inputFile] : []),
|
|
188
|
+
"--output",
|
|
189
|
+
outputFile,
|
|
190
|
+
],
|
|
191
|
+
},
|
|
192
|
+
},
|
|
193
|
+
};
|
|
194
|
+
writeFileSync(mcpConfigFile, JSON.stringify(mcpConfig));
|
|
195
|
+
}
|
|
196
|
+
// ── Stream ────────────────────────────────────────────────────────────────
|
|
197
|
+
res.writeHead(200, {
|
|
198
|
+
"Content-Type": "text/event-stream",
|
|
199
|
+
"Cache-Control": "no-cache",
|
|
200
|
+
Connection: "keep-alive",
|
|
201
|
+
});
|
|
202
|
+
const accumulated = [];
|
|
203
|
+
try {
|
|
204
|
+
await detected.adapter.stream(messages, systemPrompt, mcpConfigFile, (token) => {
|
|
205
|
+
accumulated.push(token);
|
|
206
|
+
res.write(`data: ${JSON.stringify({ type: "token", text: token })}\n\n`);
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
catch (err) {
|
|
210
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
211
|
+
console.error(`[server] adapter error: ${msg}`);
|
|
212
|
+
res.write(`data: ${JSON.stringify({ type: "error", message: msg })}\n\n`);
|
|
213
|
+
}
|
|
214
|
+
// ── Post-process: get final diagram and emit XML ──────────────────────────
|
|
215
|
+
if (outputFile) {
|
|
216
|
+
// MCP path: mcp-server writes BPMN XML directly — read and emit as-is
|
|
217
|
+
try {
|
|
218
|
+
const xml = readFileSync(outputFile, "utf8");
|
|
219
|
+
res.write(`data: ${JSON.stringify({ type: "xml", xml })}\n\n`);
|
|
220
|
+
console.log("[server] MCP XML output read successfully");
|
|
221
|
+
}
|
|
222
|
+
catch {
|
|
223
|
+
console.log("[server] MCP output file not written (no diagram changes)");
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
else {
|
|
227
|
+
// Fallback path: extract CompactDiagram from LLM text response
|
|
228
|
+
const finalCompact = extractCompactDiagram(accumulated.join(""));
|
|
229
|
+
if (finalCompact) {
|
|
230
|
+
try {
|
|
231
|
+
const xml = Bpmn.export(expand(finalCompact));
|
|
232
|
+
res.write(`data: ${JSON.stringify({ type: "xml", xml })}\n\n`);
|
|
233
|
+
console.log("[server] XML emitted via core expand + export");
|
|
234
|
+
}
|
|
235
|
+
catch (err) {
|
|
236
|
+
console.error("[server] failed to expand result:", String(err));
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
// ── Clean up temp files ───────────────────────────────────────────────────
|
|
241
|
+
if (tmpDir) {
|
|
242
|
+
try {
|
|
243
|
+
rmSync(tmpDir, { recursive: true });
|
|
244
|
+
}
|
|
245
|
+
catch {
|
|
246
|
+
/* best-effort cleanup */
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
res.write(`data: ${JSON.stringify({ type: "done" })}\n\n`);
|
|
250
|
+
res.end();
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
// ── GET /profiles ─────────────────────────────────────────────────────────
|
|
254
|
+
if (url.pathname === "/profiles" && req.method === "GET") {
|
|
255
|
+
const profiles = listProfiles();
|
|
256
|
+
const activeName = getActiveName();
|
|
257
|
+
const payload = profiles.map((p) => ({
|
|
258
|
+
name: p.name,
|
|
259
|
+
active: p.name === activeName,
|
|
260
|
+
apiType: p.apiType,
|
|
261
|
+
baseUrl: p.config.baseUrl ?? null,
|
|
262
|
+
authType: p.config.auth?.type ?? "none",
|
|
263
|
+
}));
|
|
264
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
265
|
+
res.end(JSON.stringify(payload));
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
// ── GET /operate/stream — polling stream for monitoring data ─────────────
|
|
269
|
+
// Supports both SSE (Accept: text/event-stream) and one-shot JSON polling.
|
|
270
|
+
// The operate UI uses one-shot JSON polling to avoid holding HTTP connections.
|
|
271
|
+
if (url.pathname === "/operate/stream" && req.method === "GET") {
|
|
272
|
+
const topicParam = url.searchParams.get("topic") ?? "dashboard";
|
|
273
|
+
const profileParam = req.headers["x-profile"] ??
|
|
274
|
+
url.searchParams.get("profile") ??
|
|
275
|
+
undefined;
|
|
276
|
+
const intervalMs = Math.max(5_000, Number(url.searchParams.get("interval") ?? "30000"));
|
|
277
|
+
const activeProfile = profileParam ? getProfile(profileParam) : getActiveProfile();
|
|
278
|
+
if (!activeProfile?.config.baseUrl) {
|
|
279
|
+
res.writeHead(401, { "Content-Type": "application/json" });
|
|
280
|
+
res.end(JSON.stringify({ error: "No active profile" }));
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
const client = createClientFromProfile(profileParam);
|
|
284
|
+
function items(result) {
|
|
285
|
+
return (result.items ?? []);
|
|
286
|
+
}
|
|
287
|
+
function total(result) {
|
|
288
|
+
return result.page?.totalItems ?? 0;
|
|
289
|
+
}
|
|
290
|
+
// Fetch the payload for a given topic once, returning plain data.
|
|
291
|
+
async function fetchPayload() {
|
|
292
|
+
switch (topicParam) {
|
|
293
|
+
case "dashboard": {
|
|
294
|
+
const [inst, inc, jobs, tasks, defs, usage] = await Promise.all([
|
|
295
|
+
client.processInstance.searchProcessInstances({
|
|
296
|
+
filter: { state: "ACTIVE" },
|
|
297
|
+
}),
|
|
298
|
+
client.incident.searchIncidents({ filter: { state: "ACTIVE" } }),
|
|
299
|
+
client.job.searchJobs({ filter: { state: "CREATED" } }),
|
|
300
|
+
client.userTask.searchUserTasks({ filter: { state: "CREATED" } }),
|
|
301
|
+
client.processDefinition.searchProcessDefinitions({}),
|
|
302
|
+
client.system.getUsageMetrics().catch(() => null),
|
|
303
|
+
]);
|
|
304
|
+
return {
|
|
305
|
+
activeInstances: inst.page.totalItems,
|
|
306
|
+
openIncidents: inc.page.totalItems,
|
|
307
|
+
activeJobs: jobs.page.totalItems,
|
|
308
|
+
pendingTasks: tasks.page.totalItems,
|
|
309
|
+
definitions: defs.page.totalItems,
|
|
310
|
+
usageTotalProcessInstances: usage?.processInstances,
|
|
311
|
+
usageDecisionInstances: usage?.decisionInstances,
|
|
312
|
+
usageAssignees: usage?.assignees,
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
case "definitions": {
|
|
316
|
+
const result = await client.processDefinition.searchProcessDefinitions({
|
|
317
|
+
page: { limit: 1000 },
|
|
318
|
+
sort: [{ field: "version", order: "DESC" }],
|
|
319
|
+
});
|
|
320
|
+
return { items: items(result) };
|
|
321
|
+
}
|
|
322
|
+
case "instances": {
|
|
323
|
+
const stateFilter = url.searchParams.get("state");
|
|
324
|
+
const pdKey = url.searchParams.get("processDefinitionKey");
|
|
325
|
+
const filter = {};
|
|
326
|
+
if (stateFilter)
|
|
327
|
+
filter.state = stateFilter;
|
|
328
|
+
if (pdKey)
|
|
329
|
+
filter.processDefinitionKey = pdKey;
|
|
330
|
+
const result = await client.processInstance.searchProcessInstances({
|
|
331
|
+
filter,
|
|
332
|
+
page: { limit: 1000 },
|
|
333
|
+
sort: [{ field: "startDate", order: "DESC" }],
|
|
334
|
+
});
|
|
335
|
+
return { items: items(result), total: total(result) };
|
|
336
|
+
}
|
|
337
|
+
case "incidents": {
|
|
338
|
+
const piKey = url.searchParams.get("processInstanceKey");
|
|
339
|
+
const filter = {};
|
|
340
|
+
if (piKey)
|
|
341
|
+
filter.processInstanceKey = piKey;
|
|
342
|
+
const result = await client.incident.searchIncidents({
|
|
343
|
+
filter,
|
|
344
|
+
page: { limit: 1000 },
|
|
345
|
+
sort: [{ field: "creationTime", order: "DESC" }],
|
|
346
|
+
});
|
|
347
|
+
return { items: items(result), total: total(result) };
|
|
348
|
+
}
|
|
349
|
+
case "jobs": {
|
|
350
|
+
const result = await client.job.searchJobs({
|
|
351
|
+
page: { limit: 1000 },
|
|
352
|
+
sort: [{ field: "jobKey", order: "DESC" }],
|
|
353
|
+
});
|
|
354
|
+
return { items: items(result), total: total(result) };
|
|
355
|
+
}
|
|
356
|
+
case "tasks": {
|
|
357
|
+
const result = await client.userTask.searchUserTasks({
|
|
358
|
+
page: { limit: 1000 },
|
|
359
|
+
sort: [{ field: "creationDate", order: "DESC" }],
|
|
360
|
+
});
|
|
361
|
+
return { items: items(result), total: total(result) };
|
|
362
|
+
}
|
|
363
|
+
case "decisions": {
|
|
364
|
+
const result = await client.decisionDefinition.searchDecisionDefinitions({
|
|
365
|
+
page: { limit: 1000 },
|
|
366
|
+
sort: [{ field: "version", order: "DESC" }],
|
|
367
|
+
});
|
|
368
|
+
return { items: items(result) };
|
|
369
|
+
}
|
|
370
|
+
default:
|
|
371
|
+
throw new Error(`Unknown topic: ${topicParam}`);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
// One-shot JSON polling mode (used by the operate UI via fetch()).
|
|
375
|
+
// EventSource sends Accept: text/event-stream; plain fetch does not.
|
|
376
|
+
const wantsSSE = req.headers.accept?.includes("text/event-stream") ?? false;
|
|
377
|
+
if (!wantsSSE) {
|
|
378
|
+
try {
|
|
379
|
+
const payload = await fetchPayload();
|
|
380
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
381
|
+
res.end(JSON.stringify(payload));
|
|
382
|
+
}
|
|
383
|
+
catch (err) {
|
|
384
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
385
|
+
res.end(JSON.stringify({ error: String(err) }));
|
|
386
|
+
}
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
// SSE streaming mode (legacy / external clients).
|
|
390
|
+
res.writeHead(200, {
|
|
391
|
+
"Content-Type": "text/event-stream",
|
|
392
|
+
"Cache-Control": "no-cache",
|
|
393
|
+
Connection: "keep-alive",
|
|
394
|
+
});
|
|
395
|
+
async function poll() {
|
|
396
|
+
try {
|
|
397
|
+
const payload = await fetchPayload();
|
|
398
|
+
res.write(`data: ${JSON.stringify({ type: "data", topic: topicParam, payload })}\n\n`);
|
|
399
|
+
}
|
|
400
|
+
catch (err) {
|
|
401
|
+
res.write(`data: ${JSON.stringify({ type: "error", message: String(err) })}\n\n`);
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
await poll();
|
|
405
|
+
const timer = setInterval(() => {
|
|
406
|
+
void poll();
|
|
407
|
+
}, intervalMs);
|
|
408
|
+
const keepalive = setInterval(() => {
|
|
409
|
+
res.write(`data: ${JSON.stringify({ type: "keepalive" })}\n\n`);
|
|
410
|
+
}, 25_000);
|
|
411
|
+
req.on("close", () => {
|
|
412
|
+
clearInterval(timer);
|
|
413
|
+
clearInterval(keepalive);
|
|
414
|
+
console.log(`[operate/stream] client disconnected (topic: ${topicParam})`);
|
|
415
|
+
});
|
|
416
|
+
console.log(`[operate/stream] connected (topic: ${topicParam}, interval: ${intervalMs}ms)`);
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
// ── POST /operate/incident-assist ─────────────────────────────────────────
|
|
420
|
+
if (url.pathname === "/operate/incident-assist" && req.method === "POST") {
|
|
421
|
+
const body = await readBody(req);
|
|
422
|
+
let incidentKey;
|
|
423
|
+
try {
|
|
424
|
+
incidentKey = JSON.parse(body).incidentKey;
|
|
425
|
+
}
|
|
426
|
+
catch {
|
|
427
|
+
res.writeHead(400);
|
|
428
|
+
res.end("Bad Request");
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
431
|
+
const profileName = req.headers["x-profile"];
|
|
432
|
+
const profile = profileName ? getProfile(profileName) : getActiveProfile();
|
|
433
|
+
if (!profile?.config.baseUrl) {
|
|
434
|
+
res.writeHead(401, { "Content-Type": "application/json" });
|
|
435
|
+
res.end(JSON.stringify({ error: "No active profile" }));
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
const available = await detectAll();
|
|
439
|
+
const detected = available[0];
|
|
440
|
+
if (!detected) {
|
|
441
|
+
res.writeHead(503);
|
|
442
|
+
res.end("No AI adapter available");
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
res.writeHead(200, {
|
|
446
|
+
"Content-Type": "text/event-stream",
|
|
447
|
+
"Cache-Control": "no-cache",
|
|
448
|
+
Connection: "keep-alive",
|
|
449
|
+
});
|
|
450
|
+
let authHeader;
|
|
451
|
+
try {
|
|
452
|
+
authHeader = await getAuthHeader(profile.config);
|
|
453
|
+
}
|
|
454
|
+
catch (err) {
|
|
455
|
+
res.write(`data: ${JSON.stringify({ type: "error", message: String(err) })}\n\n`);
|
|
456
|
+
res.write(`data: ${JSON.stringify({ type: "done" })}\n\n`);
|
|
457
|
+
res.end();
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
const baseUrl = profile.config.baseUrl.replace(/\/$/, "");
|
|
461
|
+
const apiHeaders = {
|
|
462
|
+
authorization: authHeader,
|
|
463
|
+
"content-type": "application/json",
|
|
464
|
+
accept: "application/json",
|
|
465
|
+
};
|
|
466
|
+
let incident = null;
|
|
467
|
+
try {
|
|
468
|
+
const r = await fetch(`${baseUrl}/incidents/${incidentKey}`, { headers: apiHeaders });
|
|
469
|
+
if (r.ok)
|
|
470
|
+
incident = (await r.json());
|
|
471
|
+
}
|
|
472
|
+
catch {
|
|
473
|
+
/* ignore */
|
|
474
|
+
}
|
|
475
|
+
if (!incident) {
|
|
476
|
+
res.write(`data: ${JSON.stringify({ type: "error", message: "Could not fetch incident" })}\n\n`);
|
|
477
|
+
res.write(`data: ${JSON.stringify({ type: "done" })}\n\n`);
|
|
478
|
+
res.end();
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
// Fetch process XML
|
|
482
|
+
let processXml = null;
|
|
483
|
+
if (incident.processDefinitionKey) {
|
|
484
|
+
try {
|
|
485
|
+
const r = await fetch(`${baseUrl}/process-definitions/${incident.processDefinitionKey}/xml`, { headers: { ...apiHeaders, accept: "text/xml" } });
|
|
486
|
+
if (r.ok)
|
|
487
|
+
processXml = await r.text();
|
|
488
|
+
}
|
|
489
|
+
catch {
|
|
490
|
+
/* ignore */
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
let variables = [];
|
|
494
|
+
if (incident.processInstanceKey) {
|
|
495
|
+
try {
|
|
496
|
+
const r = await fetch(`${baseUrl}/variables/search`, {
|
|
497
|
+
method: "POST",
|
|
498
|
+
headers: apiHeaders,
|
|
499
|
+
body: JSON.stringify({ filter: { processInstanceKey: incident.processInstanceKey } }),
|
|
500
|
+
});
|
|
501
|
+
if (r.ok) {
|
|
502
|
+
const result = (await r.json());
|
|
503
|
+
variables = result.items ?? [];
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
catch {
|
|
507
|
+
/* ignore */
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
const systemPrompt = buildIncidentSystemPrompt();
|
|
511
|
+
const userMessage = buildIncidentUserMessage({
|
|
512
|
+
errorType: incident.errorType ?? "UNKNOWN",
|
|
513
|
+
errorMessage: incident.errorMessage ?? "",
|
|
514
|
+
elementId: incident.elementId ?? "",
|
|
515
|
+
processDefinitionId: incident.processDefinitionId ?? "",
|
|
516
|
+
processInstanceKey: incident.processInstanceKey ?? "",
|
|
517
|
+
state: incident.state ?? "",
|
|
518
|
+
creationTime: incident.creationTime,
|
|
519
|
+
jobKey: incident.jobKey,
|
|
520
|
+
}, variables, processXml);
|
|
521
|
+
console.log(`[server] /operate/incident-assist → adapter: ${detected.name}, incident: ${incidentKey}`);
|
|
522
|
+
try {
|
|
523
|
+
await detected.adapter.stream([{ role: "user", content: userMessage }], systemPrompt, null, (token) => {
|
|
524
|
+
res.write(`data: ${JSON.stringify({ type: "token", text: token })}\n\n`);
|
|
525
|
+
});
|
|
526
|
+
}
|
|
527
|
+
catch (err) {
|
|
528
|
+
res.write(`data: ${JSON.stringify({ type: "error", message: String(err) })}\n\n`);
|
|
529
|
+
}
|
|
530
|
+
res.write(`data: ${JSON.stringify({ type: "done" })}\n\n`);
|
|
531
|
+
res.end();
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
534
|
+
// ── ALL /api/* — transparent Camunda API proxy ─────────────────────────────
|
|
535
|
+
if (url.pathname.startsWith("/api/")) {
|
|
536
|
+
const profileName = req.headers["x-profile"];
|
|
537
|
+
const profile = profileName ? getProfile(profileName) : getActiveProfile();
|
|
538
|
+
if (!profile || !profile.config.baseUrl) {
|
|
539
|
+
res.writeHead(401, { "Content-Type": "application/json" });
|
|
540
|
+
res.end(JSON.stringify({
|
|
541
|
+
error: "No active profile. Create one with: casen profile create",
|
|
542
|
+
}));
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
let authHeader;
|
|
546
|
+
try {
|
|
547
|
+
authHeader = await getAuthHeader(profile.config);
|
|
548
|
+
}
|
|
549
|
+
catch (err) {
|
|
550
|
+
console.error(`[proxy] auth error: ${String(err)}`);
|
|
551
|
+
res.writeHead(502, { "Content-Type": "application/json" });
|
|
552
|
+
res.end(JSON.stringify({ error: `Auth failed: ${String(err)}` }));
|
|
553
|
+
return;
|
|
554
|
+
}
|
|
555
|
+
const targetPath = url.pathname.slice("/api".length) + url.search;
|
|
556
|
+
const targetUrl = profile.config.baseUrl.replace(/\/$/, "") + targetPath;
|
|
557
|
+
console.log(`[proxy] ${req.method} ${url.pathname} → ${targetUrl}`);
|
|
558
|
+
const upstreamHeaders = {
|
|
559
|
+
"content-type": req.headers["content-type"] ?? "application/json",
|
|
560
|
+
accept: req.headers.accept ?? "application/json",
|
|
561
|
+
};
|
|
562
|
+
if (authHeader)
|
|
563
|
+
upstreamHeaders.authorization = authHeader;
|
|
564
|
+
const hasBody = req.method !== "GET" && req.method !== "HEAD";
|
|
565
|
+
const body = hasBody ? await readBody(req) : undefined;
|
|
566
|
+
let upstream;
|
|
567
|
+
try {
|
|
568
|
+
upstream = await fetch(targetUrl, {
|
|
569
|
+
method: req.method,
|
|
570
|
+
headers: upstreamHeaders,
|
|
571
|
+
body,
|
|
572
|
+
});
|
|
573
|
+
}
|
|
574
|
+
catch (err) {
|
|
575
|
+
console.error(`[proxy] upstream error: ${String(err)}`);
|
|
576
|
+
res.writeHead(502, { "Content-Type": "application/json" });
|
|
577
|
+
res.end(JSON.stringify({ error: `Upstream unreachable: ${String(err)}` }));
|
|
578
|
+
return;
|
|
579
|
+
}
|
|
580
|
+
const contentType = upstream.headers.get("content-type") ?? "application/json";
|
|
581
|
+
res.writeHead(upstream.status, { "Content-Type": contentType });
|
|
582
|
+
res.end(await upstream.text());
|
|
583
|
+
return;
|
|
584
|
+
}
|
|
585
|
+
res.writeHead(404);
|
|
586
|
+
res.end("Not Found");
|
|
587
|
+
});
|
|
588
|
+
server.listen(PORT, () => {
|
|
589
|
+
console.log(`BPMN Kit AI Server running at http://localhost:${PORT}`);
|
|
590
|
+
console.log("Press Ctrl+C to stop");
|
|
591
|
+
});
|
|
592
|
+
//# sourceMappingURL=index.js.map
|