@bpmnkit/proxy 0.0.15 → 0.0.17
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 +226 -2
- package/dist/prompt.js +74 -0
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -4,13 +4,13 @@ import http from "node:http";
|
|
|
4
4
|
import { tmpdir } from "node:os";
|
|
5
5
|
import { dirname, join } 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, buildSearchSystemPrompt, buildSystemPrompt, } from "./prompt.js";
|
|
13
|
+
import { buildImproveSystemPrompt, buildImproveUserMessage, buildIncidentSystemPrompt, buildIncidentUserMessage, buildMcpExplainPrompt, buildMcpImprovePrompt, buildMcpSystemPrompt, buildOperateChatSystemPrompt, buildSearchSystemPrompt, buildSystemPrompt, } from "./prompt.js";
|
|
14
14
|
const PORT = process.env.AI_SERVER_PORT ? Number(process.env.AI_SERVER_PORT) : 3033;
|
|
15
15
|
// Resolve the compiled mcp-server entry point relative to this file.
|
|
16
16
|
// When bundled as bundle.cjs, import.meta.url ends with .cjs → use mcp-server.cjs.
|
|
@@ -62,6 +62,24 @@ function extractCompactDiagram(text) {
|
|
|
62
62
|
}
|
|
63
63
|
return null;
|
|
64
64
|
}
|
|
65
|
+
/**
|
|
66
|
+
* Extract a BpmnOperation[] from LLM text output.
|
|
67
|
+
* Looks for the first ```json block containing a JSON array.
|
|
68
|
+
*/
|
|
69
|
+
function extractOperations(text) {
|
|
70
|
+
const match = /```json\s*\n([\s\S]*?)\n```/.exec(text);
|
|
71
|
+
if (!match?.[1])
|
|
72
|
+
return null;
|
|
73
|
+
try {
|
|
74
|
+
const parsed = JSON.parse(match[1]);
|
|
75
|
+
if (Array.isArray(parsed))
|
|
76
|
+
return parsed;
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
/* invalid JSON */
|
|
80
|
+
}
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
65
83
|
const server = http.createServer(async (req, res) => {
|
|
66
84
|
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
67
85
|
res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
|
|
@@ -260,6 +278,8 @@ const server = http.createServer(async (req, res) => {
|
|
|
260
278
|
apiType: p.apiType,
|
|
261
279
|
baseUrl: p.config.baseUrl ?? null,
|
|
262
280
|
authType: p.config.auth?.type ?? "none",
|
|
281
|
+
description: p.description,
|
|
282
|
+
tags: p.tags,
|
|
263
283
|
}));
|
|
264
284
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
265
285
|
res.end(JSON.stringify(payload));
|
|
@@ -731,6 +751,170 @@ const server = http.createServer(async (req, res) => {
|
|
|
731
751
|
res.end(JSON.stringify({ endpoint: finalSpec.endpoint, filter: finalSpec.filter, items, total }));
|
|
732
752
|
return;
|
|
733
753
|
}
|
|
754
|
+
// ── POST /improve — structured AI-assisted BPMN improvement ─────────────────
|
|
755
|
+
// Token-efficient alternative to /chat?action=improve.
|
|
756
|
+
// Phase 1: optimize() auto-fix (no AI). Phase 2: AI outputs BpmnOperation[].
|
|
757
|
+
// Emits SSE: tokens (explanation) + ops event + xml event + done.
|
|
758
|
+
if (url.pathname === "/improve" && req.method === "POST") {
|
|
759
|
+
const body = await readBody(req);
|
|
760
|
+
let context;
|
|
761
|
+
let instruction;
|
|
762
|
+
let backend;
|
|
763
|
+
try {
|
|
764
|
+
const parsed = JSON.parse(body);
|
|
765
|
+
context = parsed.context;
|
|
766
|
+
instruction = parsed.instruction ?? null;
|
|
767
|
+
backend = parsed.backend ?? null;
|
|
768
|
+
}
|
|
769
|
+
catch {
|
|
770
|
+
res.writeHead(400);
|
|
771
|
+
res.end("Bad Request");
|
|
772
|
+
return;
|
|
773
|
+
}
|
|
774
|
+
const available = await detectAll();
|
|
775
|
+
const detected = backend
|
|
776
|
+
? (available.find((a) => a.name === backend) ?? available[0])
|
|
777
|
+
: available[0];
|
|
778
|
+
if (!detected) {
|
|
779
|
+
res.writeHead(503);
|
|
780
|
+
res.end("No AI CLI available. Install claude, copilot, or gemini.");
|
|
781
|
+
return;
|
|
782
|
+
}
|
|
783
|
+
// ── Phase 1: auto-fix ─────────────────────────────────────────────────
|
|
784
|
+
let fixedCompact = context;
|
|
785
|
+
let autoFixCount = 0;
|
|
786
|
+
try {
|
|
787
|
+
const defs = expand(context);
|
|
788
|
+
const report = optimize(defs);
|
|
789
|
+
const fixable = report.findings
|
|
790
|
+
.filter((f) => f.applyFix)
|
|
791
|
+
.sort((a, b) => {
|
|
792
|
+
const ord = { error: 0, warning: 1, info: 2 };
|
|
793
|
+
return (ord[a.severity] ?? 2) - (ord[b.severity] ?? 2);
|
|
794
|
+
});
|
|
795
|
+
for (const f of fixable)
|
|
796
|
+
f.applyFix?.(defs);
|
|
797
|
+
autoFixCount = fixable.length;
|
|
798
|
+
if (autoFixCount > 0) {
|
|
799
|
+
fixedCompact = compactify(defs);
|
|
800
|
+
console.log(`[server] /improve → auto-fixed ${autoFixCount} issue(s)`);
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
catch (err) {
|
|
804
|
+
console.error("[server] /improve auto-fix failed:", String(err));
|
|
805
|
+
}
|
|
806
|
+
// ── Phase 2: collect remaining findings ───────────────────────────────
|
|
807
|
+
const findings = [];
|
|
808
|
+
try {
|
|
809
|
+
const remaining = optimize(expand(fixedCompact));
|
|
810
|
+
for (const f of remaining.findings) {
|
|
811
|
+
findings.push({
|
|
812
|
+
category: f.category,
|
|
813
|
+
severity: f.severity,
|
|
814
|
+
message: f.message,
|
|
815
|
+
suggestion: f.suggestion,
|
|
816
|
+
elementIds: f.elementIds,
|
|
817
|
+
});
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
catch {
|
|
821
|
+
/* non-fatal */
|
|
822
|
+
}
|
|
823
|
+
console.log(`[server] /improve → adapter: ${detected.name}, findings: ${findings.length}, autoFix: ${autoFixCount}`);
|
|
824
|
+
res.writeHead(200, {
|
|
825
|
+
"Content-Type": "text/event-stream",
|
|
826
|
+
"Cache-Control": "no-cache",
|
|
827
|
+
Connection: "keep-alive",
|
|
828
|
+
});
|
|
829
|
+
// ── Phase 3: AI call — outputs explanation + ```json operations block ─
|
|
830
|
+
const systemPrompt = buildImproveSystemPrompt();
|
|
831
|
+
const improveCtx = {
|
|
832
|
+
compact: fixedCompact,
|
|
833
|
+
findings,
|
|
834
|
+
autoFixCount,
|
|
835
|
+
instruction,
|
|
836
|
+
};
|
|
837
|
+
const userMessage = buildImproveUserMessage(improveCtx);
|
|
838
|
+
const accumulated = [];
|
|
839
|
+
try {
|
|
840
|
+
await detected.adapter.stream([{ role: "user", content: userMessage }], systemPrompt, null, (token) => {
|
|
841
|
+
accumulated.push(token);
|
|
842
|
+
res.write(`data: ${JSON.stringify({ type: "token", text: token })}\n\n`);
|
|
843
|
+
});
|
|
844
|
+
}
|
|
845
|
+
catch (err) {
|
|
846
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
847
|
+
console.error(`[server] /improve adapter error: ${msg}`);
|
|
848
|
+
res.write(`data: ${JSON.stringify({ type: "error", message: msg })}\n\n`);
|
|
849
|
+
res.write(`data: ${JSON.stringify({ type: "done" })}\n\n`);
|
|
850
|
+
res.end();
|
|
851
|
+
return;
|
|
852
|
+
}
|
|
853
|
+
// ── Phase 4: parse ops, apply, expand, emit ───────────────────────────
|
|
854
|
+
const fullText = accumulated.join("");
|
|
855
|
+
const ops = extractOperations(fullText) ?? [];
|
|
856
|
+
res.write(`data: ${JSON.stringify({ type: "ops", ops, autoFixCount })}\n\n`);
|
|
857
|
+
if (ops.length > 0 || autoFixCount > 0) {
|
|
858
|
+
try {
|
|
859
|
+
const finalCompact = ops.length > 0 ? applyOperations(fixedCompact, ops) : fixedCompact;
|
|
860
|
+
const xml = Bpmn.export(expand(finalCompact));
|
|
861
|
+
res.write(`data: ${JSON.stringify({ type: "xml", xml })}\n\n`);
|
|
862
|
+
console.log(`[server] /improve → ${ops.length} ops applied, XML emitted`);
|
|
863
|
+
}
|
|
864
|
+
catch (err) {
|
|
865
|
+
console.error("[server] /improve expand failed:", String(err));
|
|
866
|
+
res.write(`data: ${JSON.stringify({ type: "error", message: `Failed to apply operations: ${String(err)}` })}\n\n`);
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
res.write(`data: ${JSON.stringify({ type: "done" })}\n\n`);
|
|
870
|
+
res.end();
|
|
871
|
+
return;
|
|
872
|
+
}
|
|
873
|
+
// ── POST /operate/chat — operations-context AI chat ──────────────────────────
|
|
874
|
+
if (url.pathname === "/operate/chat" && req.method === "POST") {
|
|
875
|
+
const body = await readBody(req);
|
|
876
|
+
let messages;
|
|
877
|
+
let stats;
|
|
878
|
+
let backend;
|
|
879
|
+
try {
|
|
880
|
+
const parsed = JSON.parse(body);
|
|
881
|
+
messages = parsed.messages;
|
|
882
|
+
stats = parsed.stats ?? null;
|
|
883
|
+
backend = parsed.backend ?? null;
|
|
884
|
+
}
|
|
885
|
+
catch {
|
|
886
|
+
res.writeHead(400);
|
|
887
|
+
res.end("Bad Request");
|
|
888
|
+
return;
|
|
889
|
+
}
|
|
890
|
+
const available = await detectAll();
|
|
891
|
+
const detected = backend
|
|
892
|
+
? (available.find((a) => a.name === backend) ?? available[0])
|
|
893
|
+
: available[0];
|
|
894
|
+
if (!detected) {
|
|
895
|
+
res.writeHead(503);
|
|
896
|
+
res.end("No AI adapter available. Install claude, copilot, or gemini.");
|
|
897
|
+
return;
|
|
898
|
+
}
|
|
899
|
+
console.log(`[server] /operate/chat → adapter: ${detected.name}`);
|
|
900
|
+
res.writeHead(200, {
|
|
901
|
+
"Content-Type": "text/event-stream",
|
|
902
|
+
"Cache-Control": "no-cache",
|
|
903
|
+
Connection: "keep-alive",
|
|
904
|
+
});
|
|
905
|
+
const systemPrompt = buildOperateChatSystemPrompt(stats);
|
|
906
|
+
try {
|
|
907
|
+
await detected.adapter.stream(messages, systemPrompt, null, (token) => {
|
|
908
|
+
res.write(`data: ${JSON.stringify({ type: "token", text: token })}\n\n`);
|
|
909
|
+
});
|
|
910
|
+
}
|
|
911
|
+
catch (err) {
|
|
912
|
+
res.write(`data: ${JSON.stringify({ type: "error", message: String(err) })}\n\n`);
|
|
913
|
+
}
|
|
914
|
+
res.write(`data: ${JSON.stringify({ type: "done" })}\n\n`);
|
|
915
|
+
res.end();
|
|
916
|
+
return;
|
|
917
|
+
}
|
|
734
918
|
// ── ALL /api/* — transparent Camunda API proxy ─────────────────────────────
|
|
735
919
|
if (url.pathname.startsWith("/api/")) {
|
|
736
920
|
const profileName = req.headers["x-profile"];
|
|
@@ -782,6 +966,46 @@ const server = http.createServer(async (req, res) => {
|
|
|
782
966
|
res.end(await upstream.text());
|
|
783
967
|
return;
|
|
784
968
|
}
|
|
969
|
+
// ── POST /http-request — CORS bypass for wasm worker REST connectors ─────
|
|
970
|
+
if (url.pathname === "/http-request" && req.method === "POST") {
|
|
971
|
+
const body = await readBody(req);
|
|
972
|
+
let targetUrl;
|
|
973
|
+
let method;
|
|
974
|
+
let headers;
|
|
975
|
+
let reqBody;
|
|
976
|
+
try {
|
|
977
|
+
const parsed = JSON.parse(body);
|
|
978
|
+
targetUrl = parsed.url;
|
|
979
|
+
method = (parsed.method ?? "GET").toUpperCase();
|
|
980
|
+
headers = parsed.headers ?? {};
|
|
981
|
+
reqBody = parsed.body;
|
|
982
|
+
}
|
|
983
|
+
catch {
|
|
984
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
985
|
+
res.end(JSON.stringify({ error: "Invalid JSON body" }));
|
|
986
|
+
return;
|
|
987
|
+
}
|
|
988
|
+
console.log(`[http-request] ${method} ${targetUrl}`);
|
|
989
|
+
try {
|
|
990
|
+
const upstream = await fetch(targetUrl, {
|
|
991
|
+
method,
|
|
992
|
+
headers,
|
|
993
|
+
body: method !== "GET" && method !== "HEAD" ? reqBody : undefined,
|
|
994
|
+
});
|
|
995
|
+
const responseText = await upstream.text();
|
|
996
|
+
const contentType = upstream.headers.get("content-type") ?? "application/json";
|
|
997
|
+
res.writeHead(upstream.status, {
|
|
998
|
+
"Content-Type": contentType,
|
|
999
|
+
"Access-Control-Allow-Origin": "*",
|
|
1000
|
+
});
|
|
1001
|
+
res.end(responseText);
|
|
1002
|
+
}
|
|
1003
|
+
catch (err) {
|
|
1004
|
+
res.writeHead(502, { "Content-Type": "application/json" });
|
|
1005
|
+
res.end(JSON.stringify({ error: `Upstream unreachable: ${String(err)}` }));
|
|
1006
|
+
}
|
|
1007
|
+
return;
|
|
1008
|
+
}
|
|
785
1009
|
res.writeHead(404);
|
|
786
1010
|
res.end("Not Found");
|
|
787
1011
|
});
|
package/dist/prompt.js
CHANGED
|
@@ -205,6 +205,80 @@ export function buildSearchSystemPrompt() {
|
|
|
205
205
|
"Omit filter fields that are not relevant. Output ONLY the JSON object.",
|
|
206
206
|
].join("\n");
|
|
207
207
|
}
|
|
208
|
+
export function buildOperateChatSystemPrompt(stats) {
|
|
209
|
+
const lines = [
|
|
210
|
+
"You are an operations assistant for Camunda 8 process automation.",
|
|
211
|
+
"Help operators understand what is running in their cluster and what actions to take.",
|
|
212
|
+
"Be concise, actionable, and prioritize incidents (they block process execution).",
|
|
213
|
+
"Use markdown for formatting. Keep responses short unless detail is specifically requested.",
|
|
214
|
+
"",
|
|
215
|
+
];
|
|
216
|
+
if (stats) {
|
|
217
|
+
lines.push("## Current cluster state", `- Running instances: ${stats.runningInstances}`, `- Active incidents: ${stats.activeIncidents}`, `- Pending user tasks: ${stats.pendingTasks}`, `- Deployed process definitions: ${stats.deployedDefinitions}`, `- Active jobs: ${stats.activeJobs}`, "");
|
|
218
|
+
if (stats.activeIncidents > 0) {
|
|
219
|
+
lines.push(`There are ${stats.activeIncidents} active incident(s) — these are blocking process execution.`, "When asked what to do next, prioritize resolving incidents first.", "");
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
lines.push("## Available actions (user performs these in the UI)", "- View and cancel running instances → Instances page", "- View and retry failed incidents → Incidents page", "- Claim and complete user tasks → Tasks page", "- Start new process instances → Definitions page → Start Instance button", "- Deploy new processes → Models page → Deploy button", "", "When asked to do something, explain which UI page to visit and what to click.", "If asked about a specific instance/incident/task, say you can only see aggregate counts unless you query for details.");
|
|
223
|
+
return lines.join("\n");
|
|
224
|
+
}
|
|
225
|
+
// ── Improve prompt builders ───────────────────────────────────────────────────
|
|
226
|
+
const OPERATIONS_FORMAT = `
|
|
227
|
+
BpmnOperation types (use stable element IDs, never array positions):
|
|
228
|
+
{ "op": "rename", "id": "...", "name": "new name" }
|
|
229
|
+
{ "op": "update", "id": "...", "patch": { /* partial CompactElement fields */ } }
|
|
230
|
+
{ "op": "delete", "id": "..." }
|
|
231
|
+
{ "op": "insert", "element": { /* full CompactElement with new unique id */ }, "after"?: "id", "before"?: "id", "parent"?: "sub-process-id" }
|
|
232
|
+
{ "op": "add_flow", "from": "id", "to": "id", "condition"?: "FEEL expr", "name"?: "...", "parent"?: "sub-process-id" }
|
|
233
|
+
{ "op": "delete_flow", "id": "..." }
|
|
234
|
+
{ "op": "redirect_flow", "id": "...", "from"?: "new-source-id", "to"?: "new-target-id" }`.trim();
|
|
235
|
+
export function buildImproveSystemPrompt() {
|
|
236
|
+
return [
|
|
237
|
+
"You are a BPMN 2.0 process improvement expert.",
|
|
238
|
+
"",
|
|
239
|
+
"Output format — follow this EXACTLY:",
|
|
240
|
+
"1. Write 2–4 sentences explaining what you will change and why.",
|
|
241
|
+
"2. Then output a single ```json block containing ONLY a JSON array of BpmnOperation objects.",
|
|
242
|
+
"",
|
|
243
|
+
OPERATIONS_FORMAT,
|
|
244
|
+
"",
|
|
245
|
+
"Rules:",
|
|
246
|
+
"- Reference only IDs that exist in the provided model (except 'insert' adds new IDs).",
|
|
247
|
+
"- For 'insert': generate a short, unique camelCase ID (e.g. 'task_notify', 'gw_valid').",
|
|
248
|
+
"- Output ONLY the operations array in the ```json block — no prose inside it.",
|
|
249
|
+
"- If no changes are needed, output [].",
|
|
250
|
+
"",
|
|
251
|
+
"Apply Camunda BPMN best practices:",
|
|
252
|
+
' • Tasks: "Verb Object" — "Verify Invoice", "Send Notification"',
|
|
253
|
+
' • Start events: past participle — "Order Received", "Payment Initiated"',
|
|
254
|
+
' • End events: object + state — "Order Fulfilled", "Payment Failed"',
|
|
255
|
+
' • XOR split gateways: yes/no question ending in "?" — "Invoice valid?"',
|
|
256
|
+
' • XOR split outgoing flows: label with condition — "Yes"/"No", "Approved"/"Rejected"',
|
|
257
|
+
" • Join gateways: no label",
|
|
258
|
+
" • Never send >1 incoming flow to a task without a join gateway first",
|
|
259
|
+
].join("\n");
|
|
260
|
+
}
|
|
261
|
+
export function buildImproveUserMessage(ctx) {
|
|
262
|
+
const lines = [];
|
|
263
|
+
if (ctx.autoFixCount > 0) {
|
|
264
|
+
lines.push(`Note: ${ctx.autoFixCount} structural issue(s) were already auto-fixed before this analysis.`, "");
|
|
265
|
+
}
|
|
266
|
+
lines.push("Current process model:", "```json", JSON.stringify(ctx.compact, null, 2), "```", "");
|
|
267
|
+
if (ctx.findings.length > 0) {
|
|
268
|
+
lines.push("Detected issues to fix:");
|
|
269
|
+
for (const f of ctx.findings) {
|
|
270
|
+
const els = f.elementIds.length > 0 ? ` [elements: ${f.elementIds.join(", ")}]` : "";
|
|
271
|
+
lines.push(`- [${f.severity}/${f.category}] ${f.message}${els}`);
|
|
272
|
+
lines.push(` → ${f.suggestion}`);
|
|
273
|
+
}
|
|
274
|
+
lines.push("");
|
|
275
|
+
}
|
|
276
|
+
if (ctx.instruction) {
|
|
277
|
+
lines.push(`Additional instructions: ${ctx.instruction}`, "");
|
|
278
|
+
}
|
|
279
|
+
lines.push("Explain your changes, then output the BpmnOperation array in a ```json block.");
|
|
280
|
+
return lines.join("\n");
|
|
281
|
+
}
|
|
208
282
|
// ── Fallback prompt builders (for non-MCP adapters like Gemini) ───────────────
|
|
209
283
|
/** Full system prompt for non-MCP adapters that must return a CompactDiagram JSON block. */
|
|
210
284
|
export function buildSystemPrompt(context) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bpmnkit/proxy",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.17",
|
|
4
4
|
"description": "Local proxy server for BPMN Kit — AI bridge (SSE/MCP) and Camunda API proxy using stored CLI profiles",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -17,8 +17,8 @@
|
|
|
17
17
|
},
|
|
18
18
|
"dependencies": {
|
|
19
19
|
"@bpmnkit/api": "0.0.13",
|
|
20
|
-
"@bpmnkit/core": "0.0.
|
|
21
|
-
"@bpmnkit/profiles": "0.0.
|
|
20
|
+
"@bpmnkit/core": "0.0.16",
|
|
21
|
+
"@bpmnkit/profiles": "0.0.11"
|
|
22
22
|
},
|
|
23
23
|
"publishConfig": {
|
|
24
24
|
"access": "public"
|