@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/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) {
|
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
import { mkdirSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { getActiveProfile, getAuthHeader } from "@bpmnkit/profiles";
|
|
5
|
+
/**
|
|
6
|
+
* Run history store — persists worker job execution history to SQLite.
|
|
7
|
+
*
|
|
8
|
+
* Schema:
|
|
9
|
+
* runs — one row per process instance (groups all steps for an instance)
|
|
10
|
+
* steps — one row per job execution (CLI, LLM, FS, JS, etc.)
|
|
11
|
+
*
|
|
12
|
+
* Routes:
|
|
13
|
+
* GET /run-history — paginated list of runs (most recent first)
|
|
14
|
+
* GET /run-history/:id — single run with all steps
|
|
15
|
+
* DELETE /run-history — clear all history
|
|
16
|
+
*/
|
|
17
|
+
import Database from "better-sqlite3";
|
|
18
|
+
// ── DB init ───────────────────────────────────────────────────────────────────
|
|
19
|
+
const DB_DIR = join(homedir(), ".bpmnkit");
|
|
20
|
+
const DB_PATH = join(DB_DIR, "run-history.db");
|
|
21
|
+
let db = null;
|
|
22
|
+
function getDb() {
|
|
23
|
+
if (db)
|
|
24
|
+
return db;
|
|
25
|
+
mkdirSync(DB_DIR, { recursive: true });
|
|
26
|
+
db = new Database(DB_PATH);
|
|
27
|
+
db.pragma("journal_mode = WAL");
|
|
28
|
+
db.exec(`
|
|
29
|
+
CREATE TABLE IF NOT EXISTS runs (
|
|
30
|
+
id TEXT PRIMARY KEY,
|
|
31
|
+
processInstanceKey TEXT NOT NULL,
|
|
32
|
+
processId TEXT,
|
|
33
|
+
startedAt TEXT NOT NULL,
|
|
34
|
+
endedAt TEXT,
|
|
35
|
+
state TEXT NOT NULL DEFAULT 'active',
|
|
36
|
+
variablesSnapshot TEXT NOT NULL DEFAULT '{}'
|
|
37
|
+
);
|
|
38
|
+
CREATE TABLE IF NOT EXISTS steps (
|
|
39
|
+
id TEXT PRIMARY KEY,
|
|
40
|
+
runId TEXT NOT NULL REFERENCES runs(id),
|
|
41
|
+
elementId TEXT NOT NULL,
|
|
42
|
+
jobType TEXT NOT NULL,
|
|
43
|
+
startedAt TEXT NOT NULL,
|
|
44
|
+
endedAt TEXT,
|
|
45
|
+
durationMs INTEGER,
|
|
46
|
+
state TEXT NOT NULL DEFAULT 'active',
|
|
47
|
+
inputs TEXT NOT NULL DEFAULT '{}',
|
|
48
|
+
outputs TEXT NOT NULL DEFAULT '{}',
|
|
49
|
+
errorMessage TEXT
|
|
50
|
+
);
|
|
51
|
+
CREATE INDEX IF NOT EXISTS steps_runId ON steps(runId);
|
|
52
|
+
CREATE INDEX IF NOT EXISTS runs_startedAt ON runs(startedAt DESC);
|
|
53
|
+
`);
|
|
54
|
+
return db;
|
|
55
|
+
}
|
|
56
|
+
// ── Interpolation (minimal copy to avoid circular imports) ────────────────────
|
|
57
|
+
const VAR_RE = /\{\{([\w.]+)\}\}/g;
|
|
58
|
+
function interpolate(template, vars) {
|
|
59
|
+
return template.replace(VAR_RE, (_match, key) => {
|
|
60
|
+
if (key.startsWith("secrets."))
|
|
61
|
+
return "***";
|
|
62
|
+
const val = vars[key];
|
|
63
|
+
return val !== undefined ? String(val) : `{{${key}}}`;
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
// ── Step input extraction ─────────────────────────────────────────────────────
|
|
67
|
+
function extractInputs(job) {
|
|
68
|
+
try {
|
|
69
|
+
switch (job.type) {
|
|
70
|
+
case "io.bpmnkit:llm:1": {
|
|
71
|
+
const rawPrompt = job.variables.prompt ?? job.customHeaders.prompt ?? "";
|
|
72
|
+
return JSON.stringify({
|
|
73
|
+
prompt: interpolate(rawPrompt, job.variables),
|
|
74
|
+
system: job.customHeaders.system
|
|
75
|
+
? interpolate(job.customHeaders.system, job.variables)
|
|
76
|
+
: undefined,
|
|
77
|
+
model: job.customHeaders.model ?? "auto",
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
case "io.bpmnkit:cli:1": {
|
|
81
|
+
const cmd = interpolate(job.customHeaders.command ?? "", job.variables);
|
|
82
|
+
return JSON.stringify({
|
|
83
|
+
command: cmd,
|
|
84
|
+
cwd: job.customHeaders.cwd ? interpolate(job.customHeaders.cwd, job.variables) : "~",
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
case "io.bpmnkit:fs:read:1":
|
|
88
|
+
case "io.bpmnkit:fs:write:1":
|
|
89
|
+
case "io.bpmnkit:fs:append:1":
|
|
90
|
+
case "io.bpmnkit:fs:list:1": {
|
|
91
|
+
const path = job.variables.path ?? job.customHeaders.path ?? "";
|
|
92
|
+
return JSON.stringify({ path: interpolate(path, job.variables) });
|
|
93
|
+
}
|
|
94
|
+
case "io.bpmnkit:js:1": {
|
|
95
|
+
return JSON.stringify({ expression: job.customHeaders.expression ?? "" });
|
|
96
|
+
}
|
|
97
|
+
case "io.bpmnkit:http:scrape:1": {
|
|
98
|
+
const url = interpolate(job.customHeaders.url ?? "", job.variables);
|
|
99
|
+
return JSON.stringify({ url });
|
|
100
|
+
}
|
|
101
|
+
case "io.bpmnkit:email:fetch:1": {
|
|
102
|
+
return JSON.stringify({
|
|
103
|
+
folder: job.customHeaders.folder ?? "INBOX",
|
|
104
|
+
limit: job.customHeaders.limit ?? "10",
|
|
105
|
+
unreadOnly: job.customHeaders.unreadOnly ?? "true",
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
case "io.bpmnkit:email:send:1": {
|
|
109
|
+
const to = String(job.variables.to ?? job.customHeaders.to ?? "");
|
|
110
|
+
const subject = String(job.variables.subject ?? job.customHeaders.subject ?? "");
|
|
111
|
+
return JSON.stringify({ to, subject });
|
|
112
|
+
}
|
|
113
|
+
default:
|
|
114
|
+
return JSON.stringify({ headers: job.customHeaders });
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
return "{}";
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
// ── Public logging API (called from worker.ts) ────────────────────────────────
|
|
122
|
+
/** Called before a job handler is invoked. */
|
|
123
|
+
export function onJobStart(job) {
|
|
124
|
+
try {
|
|
125
|
+
const d = getDb();
|
|
126
|
+
const now = new Date().toISOString();
|
|
127
|
+
// Upsert the run (create on first job for this process instance)
|
|
128
|
+
d.prepare(`INSERT INTO runs (id, processInstanceKey, processId, startedAt, state, variablesSnapshot)
|
|
129
|
+
VALUES (?, ?, ?, ?, 'active', ?)
|
|
130
|
+
ON CONFLICT(id) DO NOTHING`).run(job.processInstanceKey, job.processInstanceKey, job.type.split(":")[1] ?? job.type, now, JSON.stringify(job.variables));
|
|
131
|
+
// Insert step
|
|
132
|
+
d.prepare(`INSERT INTO steps (id, runId, elementId, jobType, startedAt, state, inputs)
|
|
133
|
+
VALUES (?, ?, ?, ?, ?, 'active', ?)`).run(job.jobKey, job.processInstanceKey, job.elementInstanceKey, job.type, now, extractInputs(job));
|
|
134
|
+
}
|
|
135
|
+
catch (err) {
|
|
136
|
+
console.error("[run-history] onJobStart failed:", err);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
/** Called after a job handler succeeds. */
|
|
140
|
+
export function onJobComplete(job, outputs, durationMs) {
|
|
141
|
+
try {
|
|
142
|
+
const d = getDb();
|
|
143
|
+
const now = new Date().toISOString();
|
|
144
|
+
d.prepare(`UPDATE steps SET endedAt=?, durationMs=?, state='completed', outputs=? WHERE id=?`).run(now, durationMs, JSON.stringify(outputs), job.jobKey);
|
|
145
|
+
d.prepare(`UPDATE runs SET endedAt=?, state='completed' WHERE id=?`).run(now, job.processInstanceKey);
|
|
146
|
+
}
|
|
147
|
+
catch (err) {
|
|
148
|
+
console.error("[run-history] onJobComplete failed:", err);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
/** Called after a job handler throws. */
|
|
152
|
+
export function onJobFail(job, errorMessage, durationMs) {
|
|
153
|
+
try {
|
|
154
|
+
const d = getDb();
|
|
155
|
+
const now = new Date().toISOString();
|
|
156
|
+
d.prepare(`UPDATE steps SET endedAt=?, durationMs=?, state='failed', errorMessage=? WHERE id=?`).run(now, durationMs, errorMessage, job.jobKey);
|
|
157
|
+
d.prepare(`UPDATE runs SET endedAt=?, state='failed' WHERE id=?`).run(now, job.processInstanceKey);
|
|
158
|
+
}
|
|
159
|
+
catch (err) {
|
|
160
|
+
console.error("[run-history] onJobFail failed:", err);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
function jsonResp(res, data, status = 200) {
|
|
164
|
+
res.writeHead(status, { "Content-Type": "application/json" });
|
|
165
|
+
res.end(JSON.stringify(data));
|
|
166
|
+
}
|
|
167
|
+
/** GET /run-history — list of runs, most recent first. */
|
|
168
|
+
export function handleGetRunHistory(req, res) {
|
|
169
|
+
try {
|
|
170
|
+
const url = new URL(req.url ?? "/", "http://x");
|
|
171
|
+
const limit = Math.min(Number(url.searchParams.get("limit") ?? "50"), 200);
|
|
172
|
+
const offset = Number(url.searchParams.get("offset") ?? "0");
|
|
173
|
+
const d = getDb();
|
|
174
|
+
const rows = d
|
|
175
|
+
.prepare(`SELECT r.*,
|
|
176
|
+
COUNT(s.id) AS stepCount,
|
|
177
|
+
SUM(CASE WHEN s.state='failed' THEN 1 ELSE 0 END) AS failedSteps
|
|
178
|
+
FROM runs r
|
|
179
|
+
LEFT JOIN steps s ON s.runId = r.id
|
|
180
|
+
GROUP BY r.id
|
|
181
|
+
ORDER BY r.startedAt DESC
|
|
182
|
+
LIMIT ? OFFSET ?`)
|
|
183
|
+
.all(limit, offset);
|
|
184
|
+
const total = d.prepare("SELECT COUNT(*) AS n FROM runs").get().n;
|
|
185
|
+
jsonResp(res, { items: rows, total, limit, offset });
|
|
186
|
+
}
|
|
187
|
+
catch (err) {
|
|
188
|
+
jsonResp(res, { error: String(err) }, 500);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
/** GET /run-history/:id — single run with steps. */
|
|
192
|
+
export function handleGetRunHistoryDetail(req, res, runId) {
|
|
193
|
+
try {
|
|
194
|
+
const d = getDb();
|
|
195
|
+
const run = d.prepare("SELECT * FROM runs WHERE id=?").get(runId);
|
|
196
|
+
if (!run) {
|
|
197
|
+
jsonResp(res, { error: "Not found" }, 404);
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
const steps = d
|
|
201
|
+
.prepare("SELECT * FROM steps WHERE runId=? ORDER BY startedAt ASC")
|
|
202
|
+
.all(runId);
|
|
203
|
+
jsonResp(res, { ...run, steps });
|
|
204
|
+
}
|
|
205
|
+
catch (err) {
|
|
206
|
+
jsonResp(res, { error: String(err) }, 500);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
/** DELETE /run-history — clear all history. */
|
|
210
|
+
export function handleDeleteRunHistory(_req, res) {
|
|
211
|
+
try {
|
|
212
|
+
const d = getDb();
|
|
213
|
+
d.exec("DELETE FROM steps; DELETE FROM runs;");
|
|
214
|
+
jsonResp(res, { deleted: true });
|
|
215
|
+
}
|
|
216
|
+
catch (err) {
|
|
217
|
+
jsonResp(res, { error: String(err) }, 500);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
/** Route matcher — returns the run ID if the URL matches /run-history/:id */
|
|
221
|
+
export function matchRunHistoryRoute(req) {
|
|
222
|
+
const url = new URL(req.url ?? "/", "http://x");
|
|
223
|
+
const m = url.pathname.match(/^\/run-history\/([^/]+)$/);
|
|
224
|
+
if (m?.[1])
|
|
225
|
+
return { id: m[1] };
|
|
226
|
+
return null;
|
|
227
|
+
}
|
|
228
|
+
// ── Re-run handler ────────────────────────────────────────────────────────────
|
|
229
|
+
function readBody(req) {
|
|
230
|
+
return new Promise((resolve, reject) => {
|
|
231
|
+
const chunks = [];
|
|
232
|
+
req.on("data", (chunk) => chunks.push(chunk));
|
|
233
|
+
req.on("end", () => resolve(Buffer.concat(chunks).toString("utf8")));
|
|
234
|
+
req.on("error", reject);
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
/** POST /run-history/:id/rerun — start a new process instance from a historical run. */
|
|
238
|
+
export async function handleRerunHistory(req, res, runId) {
|
|
239
|
+
try {
|
|
240
|
+
const d = getDb();
|
|
241
|
+
const run = d.prepare("SELECT * FROM runs WHERE id=?").get(runId);
|
|
242
|
+
if (!run) {
|
|
243
|
+
jsonResp(res, { error: "Not found" }, 404);
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
const profile = getActiveProfile();
|
|
247
|
+
if (!profile?.config.baseUrl) {
|
|
248
|
+
jsonResp(res, { error: "No active reebe profile" }, 503);
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
const baseUrl = profile.config.baseUrl.replace(/\/$/, "");
|
|
252
|
+
let authHeader = "";
|
|
253
|
+
try {
|
|
254
|
+
authHeader = await getAuthHeader(profile.config);
|
|
255
|
+
}
|
|
256
|
+
catch {
|
|
257
|
+
// proceed without auth
|
|
258
|
+
}
|
|
259
|
+
// Parse optional variableOverrides from request body.
|
|
260
|
+
let variableOverrides = {};
|
|
261
|
+
try {
|
|
262
|
+
const raw = await readBody(req);
|
|
263
|
+
if (raw.trim()) {
|
|
264
|
+
const parsed = JSON.parse(raw);
|
|
265
|
+
variableOverrides = parsed.variableOverrides ?? {};
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
catch {
|
|
269
|
+
// ignore parse errors — no overrides
|
|
270
|
+
}
|
|
271
|
+
// Merge original snapshot with overrides.
|
|
272
|
+
let variables = {};
|
|
273
|
+
try {
|
|
274
|
+
variables = JSON.parse(run.variablesSnapshot);
|
|
275
|
+
}
|
|
276
|
+
catch {
|
|
277
|
+
// use empty object
|
|
278
|
+
}
|
|
279
|
+
variables = { ...variables, ...variableOverrides };
|
|
280
|
+
const processId = run.processId ?? "";
|
|
281
|
+
const startRes = await fetch(`${baseUrl}/v2/process-instances`, {
|
|
282
|
+
method: "POST",
|
|
283
|
+
headers: { authorization: authHeader, "content-type": "application/json" },
|
|
284
|
+
body: JSON.stringify({ processDefinitionId: processId, variables }),
|
|
285
|
+
});
|
|
286
|
+
if (!startRes.ok) {
|
|
287
|
+
jsonResp(res, { error: "Failed to start process instance" }, 502);
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
const newPi = (await startRes.json());
|
|
291
|
+
jsonResp(res, { processInstanceKey: newPi.processInstanceKey });
|
|
292
|
+
}
|
|
293
|
+
catch (err) {
|
|
294
|
+
jsonResp(res, { error: String(err) }, 500);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
/** Route matcher — returns run ID if URL matches /run-history/:id/rerun */
|
|
298
|
+
export function matchRerunHistoryRoute(req) {
|
|
299
|
+
if (req.method !== "POST")
|
|
300
|
+
return null;
|
|
301
|
+
const url = new URL(req.url ?? "/", "http://x");
|
|
302
|
+
const m = url.pathname.match(/^\/run-history\/([^/]+)\/rerun$/);
|
|
303
|
+
if (m?.[1])
|
|
304
|
+
return { id: m[1] };
|
|
305
|
+
return null;
|
|
306
|
+
}
|
|
307
|
+
//# sourceMappingURL=run-history.js.map
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File-watcher trigger — watches filesystem paths and starts a process
|
|
3
|
+
* instance when files are created or modified.
|
|
4
|
+
*
|
|
5
|
+
* Convention: service tasks with job type `io.bpmnkit:trigger:file-watch:1`
|
|
6
|
+
* and task header `watchPath` (and optionally `glob`, `events`) are picked up
|
|
7
|
+
* from deployed processes and set up as filesystem watchers.
|
|
8
|
+
*
|
|
9
|
+
* Uses Node's native `fs.watch` — no extra dependencies.
|
|
10
|
+
*/
|
|
11
|
+
import { existsSync, readFileSync, readdirSync, statSync, watch } from "node:fs";
|
|
12
|
+
import { homedir } from "node:os";
|
|
13
|
+
import { basename, join, relative } from "node:path";
|
|
14
|
+
import { getActiveProfile, getAuthHeader } from "@bpmnkit/profiles";
|
|
15
|
+
// ── Active watchers ───────────────────────────────────────────────────────────
|
|
16
|
+
const watchers = new Map();
|
|
17
|
+
function stopAll() {
|
|
18
|
+
for (const w of watchers.values())
|
|
19
|
+
w.close();
|
|
20
|
+
watchers.clear();
|
|
21
|
+
}
|
|
22
|
+
// ── Glob matcher (basename only) ──────────────────────────────────────────────
|
|
23
|
+
function matchesGlob(filename, glob) {
|
|
24
|
+
const re = new RegExp(`^${glob
|
|
25
|
+
.replace(/[.+^${}()|[\]\\]/g, "\\$&")
|
|
26
|
+
.replace(/\*/g, ".*")
|
|
27
|
+
.replace(/\?/g, ".")}$`);
|
|
28
|
+
return re.test(filename);
|
|
29
|
+
}
|
|
30
|
+
// ── Start a process instance ──────────────────────────────────────────────────
|
|
31
|
+
async function fireProcess(processId, variables) {
|
|
32
|
+
const profile = getActiveProfile();
|
|
33
|
+
if (!profile?.config.baseUrl)
|
|
34
|
+
return;
|
|
35
|
+
const baseUrl = profile.config.baseUrl.replace(/\/$/, "");
|
|
36
|
+
if (!baseUrl.startsWith("http"))
|
|
37
|
+
return;
|
|
38
|
+
let authHeader = "";
|
|
39
|
+
try {
|
|
40
|
+
authHeader = await getAuthHeader(profile.config);
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
// proceed unauthenticated
|
|
44
|
+
}
|
|
45
|
+
const res = await fetch(`${baseUrl}/v2/process-instances`, {
|
|
46
|
+
method: "POST",
|
|
47
|
+
headers: { authorization: authHeader, "content-type": "application/json" },
|
|
48
|
+
body: JSON.stringify({ processDefinitionId: processId, variables }),
|
|
49
|
+
});
|
|
50
|
+
if (res.ok) {
|
|
51
|
+
const data = (await res.json());
|
|
52
|
+
console.log(`[trigger:file-watch] fired ${processId} for ${String(variables.filePath)} → ${data.processInstanceKey ?? "?"}`);
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
console.error(`[trigger:file-watch] failed to start ${processId}: ${res.status}`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
// ── Path helpers ──────────────────────────────────────────────────────────────
|
|
59
|
+
function resolvePath(raw) {
|
|
60
|
+
if (raw === "~" || raw.startsWith("~/"))
|
|
61
|
+
return homedir() + raw.slice(1);
|
|
62
|
+
return raw;
|
|
63
|
+
}
|
|
64
|
+
// Track file modification times to distinguish add vs change
|
|
65
|
+
const mtimeCache = new Map();
|
|
66
|
+
function startWatcher(def) {
|
|
67
|
+
const dir = resolvePath(def.watchPath);
|
|
68
|
+
if (!existsSync(dir)) {
|
|
69
|
+
console.warn(`[trigger:file-watch] path not found, skipping: ${dir}`);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
// Seed mtime cache so existing files don't fire as "add" on startup
|
|
73
|
+
try {
|
|
74
|
+
for (const entry of readdirSync(dir)) {
|
|
75
|
+
const full = join(dir, entry);
|
|
76
|
+
try {
|
|
77
|
+
mtimeCache.set(full, statSync(full).mtimeMs);
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
// skip
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
// skip
|
|
86
|
+
}
|
|
87
|
+
const watcher = watch(dir, { persistent: false }, (event, filename) => {
|
|
88
|
+
if (!filename)
|
|
89
|
+
return;
|
|
90
|
+
const file = basename(filename);
|
|
91
|
+
if (def.glob && !matchesGlob(file, def.glob))
|
|
92
|
+
return;
|
|
93
|
+
const filePath = join(dir, filename);
|
|
94
|
+
let mtime = 0;
|
|
95
|
+
try {
|
|
96
|
+
mtime = statSync(filePath).mtimeMs;
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
return; // file deleted — skip
|
|
100
|
+
}
|
|
101
|
+
const prev = mtimeCache.get(filePath);
|
|
102
|
+
const isAdd = prev === undefined;
|
|
103
|
+
const isChange = !isAdd && mtime !== prev;
|
|
104
|
+
mtimeCache.set(filePath, mtime);
|
|
105
|
+
if (def.events === "add" && !isAdd)
|
|
106
|
+
return;
|
|
107
|
+
if (def.events === "change" && !isChange)
|
|
108
|
+
return;
|
|
109
|
+
let content = "";
|
|
110
|
+
try {
|
|
111
|
+
if (statSync(filePath).size < 1_000_000) {
|
|
112
|
+
content = readFileSync(filePath, "utf8");
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
// ignore read errors
|
|
117
|
+
}
|
|
118
|
+
void fireProcess(def.processId, {
|
|
119
|
+
filePath,
|
|
120
|
+
fileName: file,
|
|
121
|
+
fileContent: content,
|
|
122
|
+
relativePath: relative(dir, filePath),
|
|
123
|
+
eventType: isAdd ? "add" : "change",
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
watchers.set(`${def.processId}:${dir}`, watcher);
|
|
127
|
+
console.log(`[trigger:file-watch] watching ${dir} for process ${def.processId}`);
|
|
128
|
+
}
|
|
129
|
+
// ── BPMN scanning ─────────────────────────────────────────────────────────────
|
|
130
|
+
async function fetchDeployedProcesses(baseUrl, authHeader) {
|
|
131
|
+
const res = await fetch(`${baseUrl}/v2/process-definitions/search`, {
|
|
132
|
+
method: "POST",
|
|
133
|
+
headers: { authorization: authHeader, "content-type": "application/json" },
|
|
134
|
+
body: JSON.stringify({ pageSize: 100 }),
|
|
135
|
+
});
|
|
136
|
+
if (!res.ok)
|
|
137
|
+
return [];
|
|
138
|
+
const data = (await res.json());
|
|
139
|
+
return data.items ?? [];
|
|
140
|
+
}
|
|
141
|
+
async function fetchProcessXml(baseUrl, authHeader, processId) {
|
|
142
|
+
const res = await fetch(`${baseUrl}/v2/process-definitions/${encodeURIComponent(processId)}/xml`, { headers: { authorization: authHeader } });
|
|
143
|
+
if (!res.ok)
|
|
144
|
+
return null;
|
|
145
|
+
const data = (await res.json());
|
|
146
|
+
return data.bpmnXml ?? null;
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Extract file-watch trigger definitions from BPMN XML.
|
|
150
|
+
* Looks for service tasks with job type `io.bpmnkit:trigger:file-watch:1`
|
|
151
|
+
* and task headers `watchPath`, `glob?`, `events?`.
|
|
152
|
+
*/
|
|
153
|
+
function extractWatchDefs(processId, xml) {
|
|
154
|
+
const defs = [];
|
|
155
|
+
const taskRe = /<serviceTask[^>]*>[\s\S]*?<zeebe:taskDefinition[^>]*type="io\.bpmnkit:trigger:file-watch:1"[\s\S]*?<\/serviceTask>/g;
|
|
156
|
+
let m;
|
|
157
|
+
// biome-ignore lint/suspicious/noAssignInExpressions: standard regex loop
|
|
158
|
+
while ((m = taskRe.exec(xml)) !== null) {
|
|
159
|
+
const block = m[0];
|
|
160
|
+
const watchPath = /zeebe:header key="watchPath"\s+value="([^"]+)"/.exec(block)?.[1];
|
|
161
|
+
if (!watchPath)
|
|
162
|
+
continue;
|
|
163
|
+
const glob = /zeebe:header key="glob"\s+value="([^"]+)"/.exec(block)?.[1];
|
|
164
|
+
const eventsRaw = /zeebe:header key="events"\s+value="(add|change|all)"/.exec(block)?.[1];
|
|
165
|
+
const events = (eventsRaw ?? "all");
|
|
166
|
+
defs.push({ processId, watchPath, glob, events });
|
|
167
|
+
}
|
|
168
|
+
return defs;
|
|
169
|
+
}
|
|
170
|
+
// ── Scan and (re)apply watchers ───────────────────────────────────────────────
|
|
171
|
+
async function scanAndApply() {
|
|
172
|
+
const profile = getActiveProfile();
|
|
173
|
+
if (!profile?.config.baseUrl)
|
|
174
|
+
return;
|
|
175
|
+
const baseUrl = profile.config.baseUrl.replace(/\/$/, "");
|
|
176
|
+
if (!baseUrl.startsWith("http"))
|
|
177
|
+
return;
|
|
178
|
+
let authHeader = "";
|
|
179
|
+
try {
|
|
180
|
+
authHeader = await getAuthHeader(profile.config);
|
|
181
|
+
}
|
|
182
|
+
catch {
|
|
183
|
+
// proceed unauthenticated
|
|
184
|
+
}
|
|
185
|
+
let processes;
|
|
186
|
+
try {
|
|
187
|
+
processes = await fetchDeployedProcesses(baseUrl, authHeader);
|
|
188
|
+
}
|
|
189
|
+
catch {
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
const newDefs = [];
|
|
193
|
+
for (const proc of processes) {
|
|
194
|
+
const xml = await fetchProcessXml(baseUrl, authHeader, proc.processDefinitionId);
|
|
195
|
+
if (xml) {
|
|
196
|
+
newDefs.push(...extractWatchDefs(proc.processDefinitionId, xml));
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
if (newDefs.length === 0)
|
|
200
|
+
return;
|
|
201
|
+
// Restart all watchers with the fresh set
|
|
202
|
+
stopAll();
|
|
203
|
+
for (const def of newDefs) {
|
|
204
|
+
startWatcher(def);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
// ── Public API ────────────────────────────────────────────────────────────────
|
|
208
|
+
const SCAN_INTERVAL_MS = 60_000;
|
|
209
|
+
export function startFileWatchTrigger() {
|
|
210
|
+
void scanAndApply();
|
|
211
|
+
setInterval(() => {
|
|
212
|
+
void scanAndApply();
|
|
213
|
+
}, SCAN_INTERVAL_MS);
|
|
214
|
+
}
|
|
215
|
+
//# sourceMappingURL=file-watcher.js.map
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Trigger orchestrator — starts all trigger types on proxy startup.
|
|
3
|
+
*
|
|
4
|
+
* Respects `BPMNKIT_TRIGGERS=false` to opt out of all triggers.
|
|
5
|
+
*/
|
|
6
|
+
import { startFileWatchTrigger } from "./file-watcher.js";
|
|
7
|
+
import { startTimerTrigger } from "./timer.js";
|
|
8
|
+
export { matchWebhookRoute, handleWebhook } from "./webhook.js";
|
|
9
|
+
export function startTriggers() {
|
|
10
|
+
if (process.env.BPMNKIT_TRIGGERS === "false") {
|
|
11
|
+
console.log("[triggers] disabled via BPMNKIT_TRIGGERS=false");
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
startTimerTrigger();
|
|
15
|
+
startFileWatchTrigger();
|
|
16
|
+
console.log("[triggers] timer and file-watch triggers started");
|
|
17
|
+
}
|
|
18
|
+
//# sourceMappingURL=index.js.map
|