@bpmnkit/proxy 0.0.16 → 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 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, buildOperateChatSystemPrompt, 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,15 +751,136 @@ 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
+ }
734
873
  // ── POST /operate/chat — operations-context AI chat ──────────────────────────
735
874
  if (url.pathname === "/operate/chat" && req.method === "POST") {
736
875
  const body = await readBody(req);
737
876
  let messages;
738
877
  let stats;
878
+ let backend;
739
879
  try {
740
880
  const parsed = JSON.parse(body);
741
881
  messages = parsed.messages;
742
882
  stats = parsed.stats ?? null;
883
+ backend = parsed.backend ?? null;
743
884
  }
744
885
  catch {
745
886
  res.writeHead(400);
@@ -747,7 +888,9 @@ const server = http.createServer(async (req, res) => {
747
888
  return;
748
889
  }
749
890
  const available = await detectAll();
750
- const detected = available[0];
891
+ const detected = backend
892
+ ? (available.find((a) => a.name === backend) ?? available[0])
893
+ : available[0];
751
894
  if (!detected) {
752
895
  res.writeHead(503);
753
896
  res.end("No AI adapter available. Install claude, copilot, or gemini.");
@@ -823,6 +966,46 @@ const server = http.createServer(async (req, res) => {
823
966
  res.end(await upstream.text());
824
967
  return;
825
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
+ }
826
1009
  res.writeHead(404);
827
1010
  res.end("Not Found");
828
1011
  });
package/dist/prompt.js CHANGED
@@ -222,6 +222,63 @@ export function buildOperateChatSystemPrompt(stats) {
222
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
223
  return lines.join("\n");
224
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
+ }
225
282
  // ── Fallback prompt builders (for non-MCP adapters like Gemini) ───────────────
226
283
  /** Full system prompt for non-MCP adapters that must return a CompactDiagram JSON block. */
227
284
  export function buildSystemPrompt(context) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bpmnkit/proxy",
3
- "version": "0.0.16",
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.15",
21
- "@bpmnkit/profiles": "0.0.10"
20
+ "@bpmnkit/core": "0.0.16",
21
+ "@bpmnkit/profiles": "0.0.11"
22
22
  },
23
23
  "publishConfig": {
24
24
  "access": "public"