@bpmnkit/proxy 0.0.11 → 0.0.13

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 CHANGED
@@ -1,5 +1,5 @@
1
1
  <div align="center">
2
- <img src="https://raw.githubusercontent.com/bpmnkit/monorepo/main/doc/logos/logo-2-gateway.svg" width="72" height="72" alt="BPMN Kit logo">
2
+ <a href="https://bpmnkit.com"><img src="https://bpmnkit.com/favicon.svg" width="72" height="72" alt="BPMN Kit logo"></a>
3
3
  <h1>@bpmnkit/proxy</h1>
4
4
  <p>Local proxy server for BPMN Kit — AI bridge (SSE/MCP) and Camunda API proxy using stored CLI profiles</p>
5
5
 
@@ -87,3 +87,7 @@ curl -H "X-Profile: production" http://localhost:3033/api/v2/process-definitions
87
87
  ## License
88
88
 
89
89
  [MIT](https://github.com/bpmnkit/monorepo/blob/main/LICENSE) © BPMN Kit — made by [u11g](https://u11g.com)
90
+
91
+ <div align="center">
92
+ <a href="https://bpmnkit.com"><img src="https://bpmnkit.com/favicon.svg" width="32" height="32" alt="BPMN Kit"></a>
93
+ </div>
package/dist/index.js CHANGED
@@ -10,7 +10,7 @@ import { getActiveName, getActiveProfile, getAuthHeader, getProfile, listProfile
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, buildSystemPrompt, } from "./prompt.js";
13
+ import { buildIncidentSystemPrompt, buildIncidentUserMessage, buildMcpExplainPrompt, buildMcpImprovePrompt, buildMcpSystemPrompt, 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.
@@ -531,6 +531,206 @@ const server = http.createServer(async (req, res) => {
531
531
  res.end();
532
532
  return;
533
533
  }
534
+ // ── POST /operate/ai-search ────────────────────────────────────────────────
535
+ // Translates a plain-text query to a Camunda API filter, executes the search,
536
+ // and returns results as JSON. AI is only called when the quick-parser cannot
537
+ // resolve the query deterministically (saves tokens for simple queries).
538
+ if (url.pathname === "/operate/ai-search" && req.method === "POST") {
539
+ const body = await readBody(req);
540
+ let query;
541
+ try {
542
+ const parsed = JSON.parse(body);
543
+ query = parsed.query?.trim() ?? "";
544
+ if (!query)
545
+ throw new Error("empty");
546
+ }
547
+ catch {
548
+ res.writeHead(400, { "Content-Type": "application/json" });
549
+ res.end(JSON.stringify({ error: "{ query: string } required" }));
550
+ return;
551
+ }
552
+ const profileName = req.headers["x-profile"];
553
+ const profile = profileName ? getProfile(profileName) : getActiveProfile();
554
+ if (!profile?.config.baseUrl) {
555
+ res.writeHead(401, { "Content-Type": "application/json" });
556
+ res.end(JSON.stringify({ error: "No active profile" }));
557
+ return;
558
+ }
559
+ let authHeader;
560
+ try {
561
+ authHeader = await getAuthHeader(profile.config);
562
+ }
563
+ catch (err) {
564
+ res.writeHead(502, { "Content-Type": "application/json" });
565
+ res.end(JSON.stringify({ error: `Auth failed: ${String(err)}` }));
566
+ return;
567
+ }
568
+ const baseUrl = profile.config.baseUrl.replace(/\/$/, "");
569
+ const apiHeaders = {
570
+ authorization: authHeader,
571
+ "content-type": "application/json",
572
+ accept: "application/json",
573
+ };
574
+ function tryQuickParse(q) {
575
+ const trimmed = q.trim();
576
+ // Pure numeric string → instance key lookup
577
+ if (/^\d+$/.test(trimmed)) {
578
+ return { endpoint: "instances", filter: { processInstanceKey: trimmed } };
579
+ }
580
+ // Single state keyword
581
+ const stateMap = {
582
+ active: "ACTIVE",
583
+ completed: "COMPLETED",
584
+ terminated: "TERMINATED",
585
+ };
586
+ const lower = trimmed.toLowerCase();
587
+ if (stateMap[lower]) {
588
+ return { endpoint: "instances", filter: { state: stateMap[lower] } };
589
+ }
590
+ return null;
591
+ }
592
+ function extractSearchSpec(text) {
593
+ // Try raw JSON first, then a ```json block, then any {...} substring
594
+ const candidates = [
595
+ text.trim(),
596
+ (/```(?:json)?\s*\n?([\s\S]*?)\n?```/.exec(text) ?? [])[1] ?? "",
597
+ (/(\{[\s\S]*\})/.exec(text) ?? [])[1] ?? "",
598
+ ];
599
+ for (const candidate of candidates) {
600
+ if (!candidate)
601
+ continue;
602
+ try {
603
+ const parsed = JSON.parse(candidate);
604
+ if (typeof parsed === "object" &&
605
+ parsed !== null &&
606
+ "endpoint" in parsed &&
607
+ "filter" in parsed) {
608
+ return parsed;
609
+ }
610
+ }
611
+ catch {
612
+ /* try next */
613
+ }
614
+ }
615
+ return null;
616
+ }
617
+ let spec = tryQuickParse(query);
618
+ console.log(`[server] /operate/ai-search → query: "${query}", quick-parse: ${spec ? "hit" : "miss"}`);
619
+ // Step 2: AI translation — only when quick-parse has no answer
620
+ if (!spec) {
621
+ const available = await detectAll();
622
+ const detected = available[0];
623
+ if (!detected) {
624
+ res.writeHead(503, { "Content-Type": "application/json" });
625
+ res.end(JSON.stringify({ error: "No AI adapter available" }));
626
+ return;
627
+ }
628
+ console.log(`[server] /operate/ai-search → adapter: ${detected.name}`);
629
+ const tokens = [];
630
+ try {
631
+ await detected.adapter.stream([{ role: "user", content: query }], buildSearchSystemPrompt(), null, (t) => tokens.push(t));
632
+ }
633
+ catch (err) {
634
+ res.writeHead(500, { "Content-Type": "application/json" });
635
+ res.end(JSON.stringify({ error: String(err) }));
636
+ return;
637
+ }
638
+ spec = extractSearchSpec(tokens.join(""));
639
+ if (!spec) {
640
+ res.writeHead(422, { "Content-Type": "application/json" });
641
+ res.end(JSON.stringify({ error: "Could not interpret search query" }));
642
+ return;
643
+ }
644
+ }
645
+ // Step 3: Execute search against Camunda
646
+ // Coerce variables filter.value to JSON-serialized string form.
647
+ // The Camunda API stores variable values as JSON strings (e.g. "3355" for the number 3355).
648
+ // If the AI emits a number or boolean, stringify it to match the stored representation.
649
+ if (spec.endpoint === "variables" &&
650
+ spec.filter.value !== undefined &&
651
+ typeof spec.filter.value !== "string") {
652
+ spec.filter.value = JSON.stringify(spec.filter.value);
653
+ }
654
+ const finalSpec = spec;
655
+ let items = [];
656
+ let total = 0;
657
+ try {
658
+ if (finalSpec.endpoint === "variables") {
659
+ const r = await fetch(`${baseUrl}/variables/search`, {
660
+ method: "POST",
661
+ headers: apiHeaders,
662
+ body: JSON.stringify({ filter: finalSpec.filter, page: { limit: 100 } }),
663
+ });
664
+ if (r.ok) {
665
+ const result = (await r.json());
666
+ items = result.items ?? [];
667
+ total = result.page?.totalItems ?? items.length;
668
+ // Enrich variable results with instance startDate (best-effort, single batch request)
669
+ const keys = [
670
+ ...new Set(items
671
+ .map((v) => v.processInstanceKey)
672
+ .filter((k) => typeof k === "string")),
673
+ ];
674
+ if (keys.length > 0) {
675
+ try {
676
+ const ir = await fetch(`${baseUrl}/process-instances/search`, {
677
+ method: "POST",
678
+ headers: apiHeaders,
679
+ body: JSON.stringify({
680
+ filter: { processInstanceKey: { $in: keys } },
681
+ page: { limit: keys.length },
682
+ }),
683
+ });
684
+ if (ir.ok) {
685
+ const instResult = (await ir.json());
686
+ const instMap = new Map((instResult.items ?? []).map((inst) => [inst.processInstanceKey, inst]));
687
+ items = items.map((v) => {
688
+ const inst = instMap.get(v.processInstanceKey);
689
+ return {
690
+ ...v,
691
+ instanceStartDate: inst?.startDate ?? null,
692
+ instanceProcessName: inst?.processDefinitionName ?? null,
693
+ instanceProcessId: inst?.processDefinitionId ?? null,
694
+ instanceState: inst?.state ?? null,
695
+ instanceIsSubprocess: inst != null &&
696
+ inst.parentProcessInstanceKey != null &&
697
+ inst.parentProcessInstanceKey !== "",
698
+ };
699
+ });
700
+ }
701
+ }
702
+ catch {
703
+ // Enrichment is best-effort; variable results are still returned
704
+ }
705
+ }
706
+ }
707
+ }
708
+ else {
709
+ const r = await fetch(`${baseUrl}/process-instances/search`, {
710
+ method: "POST",
711
+ headers: apiHeaders,
712
+ body: JSON.stringify({
713
+ filter: finalSpec.filter,
714
+ page: { limit: 100 },
715
+ sort: [{ field: "startDate", order: "DESC" }],
716
+ }),
717
+ });
718
+ if (r.ok) {
719
+ const result = (await r.json());
720
+ items = result.items ?? [];
721
+ total = result.page?.totalItems ?? items.length;
722
+ }
723
+ }
724
+ }
725
+ catch (err) {
726
+ res.writeHead(502, { "Content-Type": "application/json" });
727
+ res.end(JSON.stringify({ error: `Search failed: ${String(err)}` }));
728
+ return;
729
+ }
730
+ res.writeHead(200, { "Content-Type": "application/json" });
731
+ res.end(JSON.stringify({ endpoint: finalSpec.endpoint, filter: finalSpec.filter, items, total }));
732
+ return;
733
+ }
534
734
  // ── ALL /api/* — transparent Camunda API proxy ─────────────────────────────
535
735
  if (url.pathname.startsWith("/api/")) {
536
736
  const profileName = req.headers["x-profile"];
package/dist/prompt.js CHANGED
@@ -171,6 +171,40 @@ export function buildIncidentUserMessage(incident, variables, processXml) {
171
171
  }
172
172
  return lines.join("\n");
173
173
  }
174
+ // ── Operate AI search prompt ───────────────────────────────────────────────────
175
+ /**
176
+ * Minimal system prompt for the AI search endpoint.
177
+ * Instructs the model to output ONLY a JSON object (no prose) to keep token usage low.
178
+ */
179
+ export function buildSearchSystemPrompt() {
180
+ return [
181
+ "You are a Camunda 8 search assistant.",
182
+ "Convert the user query into a JSON search request. Output ONLY a valid JSON object — no explanation, no markdown, no extra text.",
183
+ "",
184
+ 'Schema: { "endpoint": "instances" | "variables", "filter": { ... } }',
185
+ "",
186
+ 'Instance filter fields (endpoint "instances"):',
187
+ ' state: "ACTIVE" | "COMPLETED" | "TERMINATED"',
188
+ " processDefinitionKey: string (numeric ID)",
189
+ " processDefinitionId: string (BPMN process ID, substring)",
190
+ " hasIncident: boolean",
191
+ " processInstanceKey: string (numeric key)",
192
+ " parentProcessInstanceKey: string",
193
+ "",
194
+ 'Variable filter fields (endpoint "variables"):',
195
+ " name: string (exact variable name)",
196
+ ' value: string (JSON-serialized form — number 3355 → "3355", boolean true → "true", string hello → "\\"hello\\"")',
197
+ " processInstanceKey: string",
198
+ " isTruncated: boolean",
199
+ " tenantId: string",
200
+ "",
201
+ 'Use "instances" for queries about process state, definition, incidents, or dates.',
202
+ 'Use "variables" whenever a variable name or value is mentioned — even if phrased as "instances with variable X" or "find instances where variable Y equals Z" (the instances endpoint has no variable filter; use variables instead).',
203
+ "",
204
+ 'Example: "find instances with the variable value 3355" → {"endpoint":"variables","filter":{"value":"3355"}}',
205
+ "Omit filter fields that are not relevant. Output ONLY the JSON object.",
206
+ ].join("\n");
207
+ }
174
208
  // ── Fallback prompt builders (for non-MCP adapters like Gemini) ───────────────
175
209
  /** Full system prompt for non-MCP adapters that must return a CompactDiagram JSON block. */
176
210
  export function buildSystemPrompt(context) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bpmnkit/proxy",
3
- "version": "0.0.11",
3
+ "version": "0.0.13",
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": {
@@ -16,9 +16,9 @@
16
16
  "node": ">=20"
17
17
  },
18
18
  "dependencies": {
19
- "@bpmnkit/api": "0.0.11",
20
- "@bpmnkit/core": "0.0.11",
21
- "@bpmnkit/profiles": "0.0.8"
19
+ "@bpmnkit/api": "0.0.12",
20
+ "@bpmnkit/core": "0.0.13",
21
+ "@bpmnkit/profiles": "0.0.9"
22
22
  },
23
23
  "publishConfig": {
24
24
  "access": "public"