@bpmnkit/proxy 0.0.17 → 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 +459 -5
- 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/worker.js
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Worker daemon — polls a local reebe instance for BPMN service task jobs and
|
|
3
|
+
* dispatches them to built-in handlers (CLI, LLM, FS, JS).
|
|
4
|
+
*
|
|
5
|
+
* Activated on proxy startup; respects BPMNKIT_WORKERS=false to opt out.
|
|
6
|
+
*/
|
|
7
|
+
import { getActiveProfile, getAuthHeader } from "@bpmnkit/profiles";
|
|
8
|
+
import { onJobComplete, onJobFail, onJobStart } from "./routes/run-history.js";
|
|
9
|
+
import * as cliWorker from "./workers/cli.js";
|
|
10
|
+
import * as emailWorker from "./workers/email.js";
|
|
11
|
+
import * as fsWorker from "./workers/fs.js";
|
|
12
|
+
import * as httpWorker from "./workers/http.js";
|
|
13
|
+
import * as jsWorker from "./workers/js.js";
|
|
14
|
+
import * as llmWorker from "./workers/llm.js";
|
|
15
|
+
// ── Template variable interpolation ──────────────────────────────────────────
|
|
16
|
+
const VAR_RE = /\{\{([\w.]+)\}\}/g;
|
|
17
|
+
/**
|
|
18
|
+
* Replace `{{varName}}` and `{{secrets.NAME}}` placeholders in a string.
|
|
19
|
+
* Secrets are read from `process.env` (proxy runs in Node.js).
|
|
20
|
+
*/
|
|
21
|
+
export function interpolate(template, vars) {
|
|
22
|
+
return template.replace(VAR_RE, (match, key) => {
|
|
23
|
+
if (key.startsWith("secrets.")) {
|
|
24
|
+
const secretName = key.slice("secrets.".length);
|
|
25
|
+
return process.env[secretName] ?? match;
|
|
26
|
+
}
|
|
27
|
+
const val = vars[key];
|
|
28
|
+
return val !== undefined ? String(val) : match;
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
// ── Registry ──────────────────────────────────────────────────────────────────
|
|
32
|
+
const registry = new Map([
|
|
33
|
+
[cliWorker.JOB_TYPE, cliWorker.handle],
|
|
34
|
+
[llmWorker.JOB_TYPE, llmWorker.handle],
|
|
35
|
+
[fsWorker.JOB_TYPE_READ, fsWorker.handleRead],
|
|
36
|
+
[fsWorker.JOB_TYPE_WRITE, fsWorker.handleWrite],
|
|
37
|
+
[fsWorker.JOB_TYPE_APPEND, fsWorker.handleAppend],
|
|
38
|
+
[fsWorker.JOB_TYPE_LIST, fsWorker.handleList],
|
|
39
|
+
[jsWorker.JOB_TYPE, jsWorker.handle],
|
|
40
|
+
[httpWorker.JOB_TYPE, httpWorker.handle],
|
|
41
|
+
[emailWorker.JOB_TYPE_FETCH, emailWorker.handleFetch],
|
|
42
|
+
[emailWorker.JOB_TYPE_SEND, emailWorker.handleSend],
|
|
43
|
+
]);
|
|
44
|
+
// ── Daemon state (exported for /status) ──────────────────────────────────────
|
|
45
|
+
export const workerState = {
|
|
46
|
+
active: false,
|
|
47
|
+
pollCount: 0,
|
|
48
|
+
jobTypes: [...registry.keys()],
|
|
49
|
+
lastError: null,
|
|
50
|
+
};
|
|
51
|
+
async function activateJobs(baseUrl, authHeader, type) {
|
|
52
|
+
const res = await fetch(`${baseUrl}/v2/jobs/activation`, {
|
|
53
|
+
method: "POST",
|
|
54
|
+
headers: { authorization: authHeader, "content-type": "application/json" },
|
|
55
|
+
body: JSON.stringify({
|
|
56
|
+
type,
|
|
57
|
+
timeout: 30_000,
|
|
58
|
+
maxJobsToActivate: 5,
|
|
59
|
+
worker: "bpmnkit-worker",
|
|
60
|
+
}),
|
|
61
|
+
});
|
|
62
|
+
if (!res.ok)
|
|
63
|
+
return [];
|
|
64
|
+
const data = (await res.json());
|
|
65
|
+
return (data.jobs ?? []).map((j) => ({
|
|
66
|
+
jobKey: j.jobKey,
|
|
67
|
+
type: j.type,
|
|
68
|
+
processInstanceKey: j.processInstanceKey,
|
|
69
|
+
elementInstanceKey: j.elementInstanceKey,
|
|
70
|
+
variables: j.variables ?? {},
|
|
71
|
+
customHeaders: j.customHeaders ?? {},
|
|
72
|
+
}));
|
|
73
|
+
}
|
|
74
|
+
async function completeJob(baseUrl, authHeader, jobKey, variables) {
|
|
75
|
+
await fetch(`${baseUrl}/v2/jobs/${jobKey}/completion`, {
|
|
76
|
+
method: "POST",
|
|
77
|
+
headers: { authorization: authHeader, "content-type": "application/json" },
|
|
78
|
+
body: JSON.stringify({ variables }),
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
async function failJob(baseUrl, authHeader, jobKey, message, retries) {
|
|
82
|
+
await fetch(`${baseUrl}/v2/jobs/${jobKey}/failure`, {
|
|
83
|
+
method: "POST",
|
|
84
|
+
headers: { authorization: authHeader, "content-type": "application/json" },
|
|
85
|
+
body: JSON.stringify({ retries, errorMessage: message }),
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
// ── Dispatch ──────────────────────────────────────────────────────────────────
|
|
89
|
+
async function dispatchJob(baseUrl, authHeader, job, handler) {
|
|
90
|
+
console.log(`[worker] job ${job.jobKey} type=${job.type} pi=${job.processInstanceKey}`);
|
|
91
|
+
onJobStart(job);
|
|
92
|
+
const startMs = Date.now();
|
|
93
|
+
try {
|
|
94
|
+
const outputs = await handler(job);
|
|
95
|
+
const durationMs = Date.now() - startMs;
|
|
96
|
+
await completeJob(baseUrl, authHeader, job.jobKey, outputs);
|
|
97
|
+
onJobComplete(job, outputs, durationMs);
|
|
98
|
+
console.log(`[worker] job ${job.jobKey} completed in ${durationMs}ms`);
|
|
99
|
+
}
|
|
100
|
+
catch (err) {
|
|
101
|
+
const durationMs = Date.now() - startMs;
|
|
102
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
103
|
+
console.error(`[worker] job ${job.jobKey} failed: ${message}`);
|
|
104
|
+
await failJob(baseUrl, authHeader, job.jobKey, message, 0);
|
|
105
|
+
onJobFail(job, message, durationMs);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
async function pollOnce(baseUrl, authHeader) {
|
|
109
|
+
workerState.pollCount++;
|
|
110
|
+
const types = [...registry.keys()];
|
|
111
|
+
await Promise.all(types.map(async (type) => {
|
|
112
|
+
const handler = registry.get(type);
|
|
113
|
+
if (!handler)
|
|
114
|
+
return;
|
|
115
|
+
let jobs;
|
|
116
|
+
try {
|
|
117
|
+
jobs = await activateJobs(baseUrl, authHeader, type);
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
return; // reebe unreachable, skip
|
|
121
|
+
}
|
|
122
|
+
await Promise.all(jobs.map((job) => dispatchJob(baseUrl, authHeader, job, handler)));
|
|
123
|
+
}));
|
|
124
|
+
}
|
|
125
|
+
// ── Startup ───────────────────────────────────────────────────────────────────
|
|
126
|
+
const POLL_INTERVAL_MS = 1_000;
|
|
127
|
+
export function startWorkerDaemon() {
|
|
128
|
+
if (process.env.BPMNKIT_WORKERS === "false") {
|
|
129
|
+
console.log("[worker] disabled via BPMNKIT_WORKERS=false");
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
// Run the poll loop in the background, refreshing profile on each cycle
|
|
133
|
+
workerState.active = true;
|
|
134
|
+
console.log(`[worker] daemon starting, polling types: ${[...registry.keys()].join(", ")}`);
|
|
135
|
+
let running = false;
|
|
136
|
+
setInterval(async () => {
|
|
137
|
+
if (running)
|
|
138
|
+
return; // don't overlap
|
|
139
|
+
running = true;
|
|
140
|
+
try {
|
|
141
|
+
const profile = getActiveProfile();
|
|
142
|
+
if (!profile?.config.baseUrl)
|
|
143
|
+
return;
|
|
144
|
+
// Skip non-reebe profiles (wasm, modeler-only)
|
|
145
|
+
const baseUrl = profile.config.baseUrl.replace(/\/$/, "");
|
|
146
|
+
if (!baseUrl.startsWith("http"))
|
|
147
|
+
return;
|
|
148
|
+
let authHeader = "";
|
|
149
|
+
try {
|
|
150
|
+
authHeader = await getAuthHeader(profile.config);
|
|
151
|
+
}
|
|
152
|
+
catch {
|
|
153
|
+
// Unauthenticated reebe — proceed without auth
|
|
154
|
+
}
|
|
155
|
+
await pollOnce(baseUrl, authHeader);
|
|
156
|
+
workerState.lastError = null;
|
|
157
|
+
}
|
|
158
|
+
catch (err) {
|
|
159
|
+
workerState.lastError = err instanceof Error ? err.message : String(err);
|
|
160
|
+
}
|
|
161
|
+
finally {
|
|
162
|
+
running = false;
|
|
163
|
+
}
|
|
164
|
+
}, POLL_INTERVAL_MS);
|
|
165
|
+
}
|
|
166
|
+
//# sourceMappingURL=worker.js.map
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { exec } from "node:child_process";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { promisify } from "node:util";
|
|
4
|
+
import { interpolate } from "../worker.js";
|
|
5
|
+
const execAsync = promisify(exec);
|
|
6
|
+
export const JOB_TYPE = "io.bpmnkit:cli:1";
|
|
7
|
+
/**
|
|
8
|
+
* CLI worker — runs a shell command and returns stdout/stderr/exitCode.
|
|
9
|
+
*
|
|
10
|
+
* Task headers:
|
|
11
|
+
* command (required) — shell command, supports {{varName}} and {{secrets.NAME}} interpolation
|
|
12
|
+
* cwd (optional) — working directory; default ~
|
|
13
|
+
* timeout (optional) — timeout in seconds; default 60
|
|
14
|
+
* ignoreExitCode (optional) — "true" to complete even on non-zero exit; default false
|
|
15
|
+
* resultVariable (optional) — if set, wraps result under this key; default outputs at root
|
|
16
|
+
*/
|
|
17
|
+
export async function handle(job) {
|
|
18
|
+
const commandTemplate = job.customHeaders.command;
|
|
19
|
+
if (!commandTemplate) {
|
|
20
|
+
throw new Error('CLI worker requires a "command" task header');
|
|
21
|
+
}
|
|
22
|
+
const cwd = job.customHeaders.cwd
|
|
23
|
+
? expandHome(interpolate(job.customHeaders.cwd, job.variables))
|
|
24
|
+
: homedir();
|
|
25
|
+
const timeoutSec = Number(job.customHeaders.timeout ?? "60");
|
|
26
|
+
const ignoreExitCode = job.customHeaders.ignoreExitCode === "true";
|
|
27
|
+
const command = interpolate(commandTemplate, job.variables);
|
|
28
|
+
const allowed = process.env.BPMNKIT_CLI_ALLOWED;
|
|
29
|
+
if (allowed) {
|
|
30
|
+
const prefixes = allowed
|
|
31
|
+
.split(",")
|
|
32
|
+
.map((s) => s.trim())
|
|
33
|
+
.filter(Boolean);
|
|
34
|
+
const commandName = command.trimStart().split(/\s+/)[0] ?? "";
|
|
35
|
+
if (!prefixes.some((p) => commandName === p || commandName.startsWith(`${p}/`))) {
|
|
36
|
+
throw new Error(`CLI command "${commandName}" is not in the allowlist. Set BPMNKIT_CLI_ALLOWED to allow it.`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
console.log(`[worker:cli] running: ${command}`);
|
|
40
|
+
let stdout = "";
|
|
41
|
+
let stderr = "";
|
|
42
|
+
let exitCode = 0;
|
|
43
|
+
try {
|
|
44
|
+
const result = await execAsync(command, {
|
|
45
|
+
cwd,
|
|
46
|
+
timeout: timeoutSec * 1000,
|
|
47
|
+
shell: "/bin/sh",
|
|
48
|
+
});
|
|
49
|
+
stdout = result.stdout;
|
|
50
|
+
stderr = result.stderr;
|
|
51
|
+
}
|
|
52
|
+
catch (err) {
|
|
53
|
+
const e = err;
|
|
54
|
+
stdout = e.stdout ?? "";
|
|
55
|
+
stderr = e.stderr ?? e.message ?? String(err);
|
|
56
|
+
exitCode = e.code ?? 1;
|
|
57
|
+
if (!ignoreExitCode) {
|
|
58
|
+
throw new Error(`Command exited with code ${exitCode}: ${stderr.trim() || stdout.trim()}`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
const result = { stdout, stderr, exitCode };
|
|
62
|
+
const rv = job.customHeaders.resultVariable;
|
|
63
|
+
if (rv)
|
|
64
|
+
return { [rv]: result };
|
|
65
|
+
return result;
|
|
66
|
+
}
|
|
67
|
+
function expandHome(p) {
|
|
68
|
+
if (p === "~" || p.startsWith("~/"))
|
|
69
|
+
return homedir() + p.slice(1);
|
|
70
|
+
return p;
|
|
71
|
+
}
|
|
72
|
+
//# sourceMappingURL=cli.js.map
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { interpolate } from "../worker.js";
|
|
2
|
+
export const JOB_TYPE_FETCH = "io.bpmnkit:email:fetch:1";
|
|
3
|
+
export const JOB_TYPE_SEND = "io.bpmnkit:email:send:1";
|
|
4
|
+
/**
|
|
5
|
+
* Email fetch worker — connects via IMAP and retrieves messages.
|
|
6
|
+
*
|
|
7
|
+
* Task headers (all support {{secrets.X}} interpolation):
|
|
8
|
+
* imapHost, imapPort (default 993), imapUser, imapPassword, imapSecure (default "true")
|
|
9
|
+
* folder (default "INBOX"), limit (default 10), unreadOnly (default "true")
|
|
10
|
+
* resultVariable (default "emails")
|
|
11
|
+
*/
|
|
12
|
+
export async function handleFetch(job) {
|
|
13
|
+
const h = job.customHeaders;
|
|
14
|
+
const vars = job.variables;
|
|
15
|
+
const imapHost = interpolate(h.imapHost ?? "", vars);
|
|
16
|
+
const imapPort = Number(interpolate(h.imapPort ?? "993", vars));
|
|
17
|
+
const imapUser = interpolate(h.imapUser ?? "", vars);
|
|
18
|
+
const imapPassword = interpolate(h.imapPassword ?? "", vars);
|
|
19
|
+
const imapSecure = interpolate(h.imapSecure ?? "true", vars) !== "false";
|
|
20
|
+
const folder = interpolate(h.folder ?? "INBOX", vars);
|
|
21
|
+
const limit = Number(interpolate(h.limit ?? "10", vars));
|
|
22
|
+
const unreadOnly = interpolate(h.unreadOnly ?? "true", vars) !== "false";
|
|
23
|
+
const resultVariable = interpolate(h.resultVariable ?? "emails", vars);
|
|
24
|
+
console.log(`[worker:email:fetch] folder=${folder} limit=${limit}`);
|
|
25
|
+
const { ImapFlow } = await import("imapflow");
|
|
26
|
+
const client = new ImapFlow({
|
|
27
|
+
host: imapHost,
|
|
28
|
+
port: imapPort,
|
|
29
|
+
secure: imapSecure,
|
|
30
|
+
auth: { user: imapUser, pass: imapPassword },
|
|
31
|
+
logger: false,
|
|
32
|
+
});
|
|
33
|
+
await client.connect();
|
|
34
|
+
const emails = [];
|
|
35
|
+
try {
|
|
36
|
+
const lock = await client.getMailboxLock(folder);
|
|
37
|
+
try {
|
|
38
|
+
const searchCriteria = unreadOnly ? { seen: false } : { all: true };
|
|
39
|
+
const uidsResult = await client.search(searchCriteria, { uid: true });
|
|
40
|
+
const uids = Array.isArray(uidsResult) ? uidsResult : [];
|
|
41
|
+
const fetchUids = uids.slice(-limit);
|
|
42
|
+
if (fetchUids.length > 0) {
|
|
43
|
+
for await (const msg of client.fetch(fetchUids, { envelope: true, bodyParts: ["TEXT"] }, { uid: true })) {
|
|
44
|
+
const env = msg.envelope;
|
|
45
|
+
if (!env)
|
|
46
|
+
continue;
|
|
47
|
+
const bodyPart = msg.bodyParts?.get("TEXT");
|
|
48
|
+
const body = bodyPart ? Buffer.from(bodyPart).toString("utf-8") : "";
|
|
49
|
+
emails.push({
|
|
50
|
+
uid: msg.uid,
|
|
51
|
+
subject: env.subject ?? "",
|
|
52
|
+
from: env.from?.[0]?.address ?? "",
|
|
53
|
+
to: env.to?.[0]?.address ?? "",
|
|
54
|
+
date: env.date?.toISOString() ?? "",
|
|
55
|
+
body,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
finally {
|
|
61
|
+
lock.release();
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
finally {
|
|
65
|
+
await client.logout();
|
|
66
|
+
}
|
|
67
|
+
return { [resultVariable]: emails };
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Email send worker — sends a message via SMTP using nodemailer.
|
|
71
|
+
*
|
|
72
|
+
* Task headers (support interpolation):
|
|
73
|
+
* smtpHost, smtpPort (default 587), smtpUser, smtpPassword, smtpSecure (default "false")
|
|
74
|
+
* from (optional, defaults to smtpUser)
|
|
75
|
+
*
|
|
76
|
+
* Input variables (job.variables, headers as fallback):
|
|
77
|
+
* to, subject, body
|
|
78
|
+
*/
|
|
79
|
+
export async function handleSend(job) {
|
|
80
|
+
const h = job.customHeaders;
|
|
81
|
+
const vars = job.variables;
|
|
82
|
+
const smtpHost = interpolate(h.smtpHost ?? "", vars);
|
|
83
|
+
const smtpPort = Number(interpolate(h.smtpPort ?? "587", vars));
|
|
84
|
+
const smtpUser = interpolate(h.smtpUser ?? "", vars);
|
|
85
|
+
const smtpPassword = interpolate(h.smtpPassword ?? "", vars);
|
|
86
|
+
const smtpSecure = interpolate(h.smtpSecure ?? "false", vars) !== "false";
|
|
87
|
+
const from = interpolate(h.from ?? smtpUser, vars);
|
|
88
|
+
const to = String(vars.to ?? h.to ?? "");
|
|
89
|
+
const subject = String(vars.subject ?? h.subject ?? "");
|
|
90
|
+
const body = String(vars.body ?? h.body ?? "");
|
|
91
|
+
console.log(`[worker:email:send] to=${to} subject=${subject}`);
|
|
92
|
+
const nodemailer = await import("nodemailer");
|
|
93
|
+
const transporter = nodemailer.createTransport({
|
|
94
|
+
host: smtpHost,
|
|
95
|
+
port: smtpPort,
|
|
96
|
+
secure: smtpSecure,
|
|
97
|
+
auth: { user: smtpUser, pass: smtpPassword },
|
|
98
|
+
});
|
|
99
|
+
await transporter.sendMail({ from, to, subject, text: body });
|
|
100
|
+
return { sent: true, to, subject };
|
|
101
|
+
}
|
|
102
|
+
//# sourceMappingURL=email.js.map
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { appendFileSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { dirname, resolve } from "node:path";
|
|
4
|
+
import { interpolate } from "../worker.js";
|
|
5
|
+
export const JOB_TYPE_READ = "io.bpmnkit:fs:read:1";
|
|
6
|
+
export const JOB_TYPE_WRITE = "io.bpmnkit:fs:write:1";
|
|
7
|
+
export const JOB_TYPE_APPEND = "io.bpmnkit:fs:append:1";
|
|
8
|
+
export const JOB_TYPE_LIST = "io.bpmnkit:fs:list:1";
|
|
9
|
+
function resolvePath(raw, vars) {
|
|
10
|
+
const p = interpolate(raw, vars);
|
|
11
|
+
const resolved = p.startsWith("~/") || p === "~" ? homedir() + p.slice(1) : resolve(p);
|
|
12
|
+
const root = process.env.BPMNKIT_FS_ROOT;
|
|
13
|
+
if (root) {
|
|
14
|
+
const rootResolved = resolve(root);
|
|
15
|
+
if (!resolved.startsWith(`${rootResolved}/`) && resolved !== rootResolved) {
|
|
16
|
+
throw new Error(`Path "${resolved}" is outside the allowed root "${rootResolved}". Set BPMNKIT_FS_ROOT to change this.`);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return resolved;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* io.bpmnkit:fs:read:1
|
|
23
|
+
* Variables: path (string)
|
|
24
|
+
* Outputs: content (string)
|
|
25
|
+
*/
|
|
26
|
+
export async function handleRead(job) {
|
|
27
|
+
const raw = job.variables.path ?? job.customHeaders.path;
|
|
28
|
+
if (!raw)
|
|
29
|
+
throw new Error('fs:read requires variable or header "path"');
|
|
30
|
+
const path = resolvePath(raw, job.variables);
|
|
31
|
+
console.log(`[worker:fs:read] ${path}`);
|
|
32
|
+
const content = readFileSync(path, "utf8");
|
|
33
|
+
const rv = job.customHeaders.resultVariable;
|
|
34
|
+
if (rv)
|
|
35
|
+
return { [rv]: content };
|
|
36
|
+
return { content };
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* io.bpmnkit:fs:write:1
|
|
40
|
+
* Variables: path (string), content (string)
|
|
41
|
+
* Outputs: bytesWritten (number)
|
|
42
|
+
*/
|
|
43
|
+
export async function handleWrite(job) {
|
|
44
|
+
const rawPath = job.variables.path ?? job.customHeaders.path;
|
|
45
|
+
if (!rawPath)
|
|
46
|
+
throw new Error('fs:write requires variable or header "path"');
|
|
47
|
+
const path = resolvePath(rawPath, job.variables);
|
|
48
|
+
const content = String(job.variables.content ?? "");
|
|
49
|
+
console.log(`[worker:fs:write] ${path} (${content.length} chars)`);
|
|
50
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
51
|
+
writeFileSync(path, content, "utf8");
|
|
52
|
+
return { bytesWritten: Buffer.byteLength(content, "utf8") };
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* io.bpmnkit:fs:append:1
|
|
56
|
+
* Variables: path (string), content (string)
|
|
57
|
+
* Outputs: bytesWritten (number)
|
|
58
|
+
*/
|
|
59
|
+
export async function handleAppend(job) {
|
|
60
|
+
const rawPath = job.variables.path ?? job.customHeaders.path;
|
|
61
|
+
if (!rawPath)
|
|
62
|
+
throw new Error('fs:append requires variable or header "path"');
|
|
63
|
+
const path = resolvePath(rawPath, job.variables);
|
|
64
|
+
const content = String(job.variables.content ?? "");
|
|
65
|
+
console.log(`[worker:fs:append] ${path}`);
|
|
66
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
67
|
+
appendFileSync(path, content, "utf8");
|
|
68
|
+
return { bytesWritten: Buffer.byteLength(content, "utf8") };
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* io.bpmnkit:fs:list:1
|
|
72
|
+
* Variables: path (string)
|
|
73
|
+
* Outputs: files (string[])
|
|
74
|
+
*/
|
|
75
|
+
export async function handleList(job) {
|
|
76
|
+
const raw = job.variables.path ?? job.customHeaders.path;
|
|
77
|
+
if (!raw)
|
|
78
|
+
throw new Error('fs:list requires variable or header "path"');
|
|
79
|
+
const path = resolvePath(raw, job.variables);
|
|
80
|
+
console.log(`[worker:fs:list] ${path}`);
|
|
81
|
+
const entries = readdirSync(path, { withFileTypes: true });
|
|
82
|
+
const files = entries
|
|
83
|
+
.filter((e) => !e.name.startsWith("."))
|
|
84
|
+
.map((e) => (e.isDirectory() ? `${e.name}/` : e.name));
|
|
85
|
+
const rv = job.customHeaders.resultVariable;
|
|
86
|
+
if (rv)
|
|
87
|
+
return { [rv]: files };
|
|
88
|
+
return { files };
|
|
89
|
+
}
|
|
90
|
+
//# sourceMappingURL=fs.js.map
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { interpolate } from "../worker.js";
|
|
2
|
+
export const JOB_TYPE = "io.bpmnkit:http:scrape:1";
|
|
3
|
+
/**
|
|
4
|
+
* HTTP scraper worker — fetches a URL and extracts text content.
|
|
5
|
+
*
|
|
6
|
+
* Task headers:
|
|
7
|
+
* url (required) — URL to fetch, supports {{varName}} interpolation
|
|
8
|
+
* timeout (optional) — timeout in seconds; default 30
|
|
9
|
+
* resultVariable (optional) — if set, wraps result under this key; default outputs at root
|
|
10
|
+
*/
|
|
11
|
+
export async function handle(job) {
|
|
12
|
+
const urlTemplate = job.customHeaders.url;
|
|
13
|
+
if (!urlTemplate) {
|
|
14
|
+
throw new Error('HTTP worker requires a "url" task header');
|
|
15
|
+
}
|
|
16
|
+
const url = interpolate(urlTemplate, job.variables);
|
|
17
|
+
const timeoutSec = Number(job.customHeaders.timeout ?? "30");
|
|
18
|
+
const resultVariable = job.customHeaders.resultVariable ?? "";
|
|
19
|
+
console.log(`[worker:http] GET ${url}`);
|
|
20
|
+
const controller = new AbortController();
|
|
21
|
+
const timer = setTimeout(() => controller.abort(), timeoutSec * 1000);
|
|
22
|
+
let html;
|
|
23
|
+
let statusCode;
|
|
24
|
+
try {
|
|
25
|
+
const res = await fetch(url, { signal: controller.signal });
|
|
26
|
+
statusCode = res.status;
|
|
27
|
+
html = await res.text();
|
|
28
|
+
if (!res.ok) {
|
|
29
|
+
throw new Error(`HTTP ${statusCode} from ${url}`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
finally {
|
|
33
|
+
clearTimeout(timer);
|
|
34
|
+
}
|
|
35
|
+
const titleMatch = html.match(/<title[^>]*>([\s\S]*?)<\/title>/i);
|
|
36
|
+
const title = titleMatch?.[1]?.trim() ?? "";
|
|
37
|
+
const text = html
|
|
38
|
+
.replace(/<[^>]+>/g, " ")
|
|
39
|
+
.replace(/\s+/g, " ")
|
|
40
|
+
.trim();
|
|
41
|
+
const result = { url, html, text, title, statusCode };
|
|
42
|
+
if (resultVariable)
|
|
43
|
+
return { [resultVariable]: result };
|
|
44
|
+
return result;
|
|
45
|
+
}
|
|
46
|
+
//# sourceMappingURL=http.js.map
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { runInNewContext } from "node:vm";
|
|
2
|
+
export const JOB_TYPE = "io.bpmnkit:js:1";
|
|
3
|
+
/**
|
|
4
|
+
* JavaScript eval worker — evaluates an expression with process variables in scope.
|
|
5
|
+
*
|
|
6
|
+
* Task headers:
|
|
7
|
+
* expression (required) — JS expression; receives `variables` object in scope
|
|
8
|
+
* resultVariable (optional) — variable name to store result; default "result"
|
|
9
|
+
*
|
|
10
|
+
* Example expression: variables.items.filter(x => x.score > 0.5).length
|
|
11
|
+
*/
|
|
12
|
+
export async function handle(job) {
|
|
13
|
+
const expression = job.customHeaders.expression;
|
|
14
|
+
if (!expression) {
|
|
15
|
+
throw new Error('js worker requires an "expression" task header');
|
|
16
|
+
}
|
|
17
|
+
const resultVariable = job.customHeaders.resultVariable ?? "result";
|
|
18
|
+
const result = runInNewContext(`(${expression})`, { variables: job.variables });
|
|
19
|
+
return { [resultVariable]: result };
|
|
20
|
+
}
|
|
21
|
+
//# sourceMappingURL=js.js.map
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import * as claude from "../adapters/claude.js";
|
|
2
|
+
import * as copilot from "../adapters/copilot.js";
|
|
3
|
+
import * as gemini from "../adapters/gemini.js";
|
|
4
|
+
import { interpolate } from "../worker.js";
|
|
5
|
+
export const JOB_TYPE = "io.bpmnkit:llm:1";
|
|
6
|
+
/**
|
|
7
|
+
* LLM worker — calls the first available LLM adapter with a prompt.
|
|
8
|
+
*
|
|
9
|
+
* Variables:
|
|
10
|
+
* prompt (required) — prompt text; supports {{varName}} interpolation
|
|
11
|
+
*
|
|
12
|
+
* Task headers:
|
|
13
|
+
* system (optional) — system prompt
|
|
14
|
+
* model (optional) — "claude" | "copilot" | "gemini"; auto-detects if omitted
|
|
15
|
+
* resultVariable (optional) — variable to store response; default "response"
|
|
16
|
+
*/
|
|
17
|
+
export async function handle(job) {
|
|
18
|
+
const rawPrompt = job.variables.prompt ?? job.customHeaders.prompt;
|
|
19
|
+
if (!rawPrompt)
|
|
20
|
+
throw new Error('llm worker requires a "prompt" variable or task header');
|
|
21
|
+
const prompt = interpolate(rawPrompt, job.variables);
|
|
22
|
+
const system = job.customHeaders.system
|
|
23
|
+
? interpolate(job.customHeaders.system, job.variables)
|
|
24
|
+
: "";
|
|
25
|
+
const preferredModel = job.customHeaders.model?.toLowerCase();
|
|
26
|
+
const resultVariable = job.customHeaders.resultVariable ?? "response";
|
|
27
|
+
const adapter = await pickAdapter(preferredModel);
|
|
28
|
+
if (!adapter)
|
|
29
|
+
throw new Error("No LLM adapter available (claude, copilot, or gemini)");
|
|
30
|
+
console.log(`[worker:llm] using ${adapter.name}, prompt length=${prompt.length}`);
|
|
31
|
+
let response = "";
|
|
32
|
+
await adapter.instance.stream([{ role: "user", content: prompt }], system, null, (token) => {
|
|
33
|
+
response += token;
|
|
34
|
+
});
|
|
35
|
+
return { [resultVariable]: response.trim() };
|
|
36
|
+
}
|
|
37
|
+
async function pickAdapter(preferred) {
|
|
38
|
+
const candidates = [
|
|
39
|
+
{ name: "claude", instance: claude },
|
|
40
|
+
{ name: "copilot", instance: copilot },
|
|
41
|
+
{ name: "gemini", instance: gemini },
|
|
42
|
+
];
|
|
43
|
+
if (preferred) {
|
|
44
|
+
const found = candidates.find((c) => c.name === preferred);
|
|
45
|
+
if (found && (await found.instance.available()))
|
|
46
|
+
return found;
|
|
47
|
+
console.warn(`[worker:llm] preferred adapter "${preferred}" not available, falling back`);
|
|
48
|
+
}
|
|
49
|
+
for (const c of candidates) {
|
|
50
|
+
if (await c.instance.available())
|
|
51
|
+
return c;
|
|
52
|
+
}
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
//# sourceMappingURL=llm.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bpmnkit/proxy",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.21",
|
|
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,12 @@
|
|
|
16
16
|
"node": ">=20"
|
|
17
17
|
},
|
|
18
18
|
"dependencies": {
|
|
19
|
-
"
|
|
20
|
-
"
|
|
21
|
-
"
|
|
19
|
+
"better-sqlite3": "^12.8.0",
|
|
20
|
+
"imapflow": "^1.2.18",
|
|
21
|
+
"nodemailer": "^6.10.1",
|
|
22
|
+
"@bpmnkit/api": "0.0.16",
|
|
23
|
+
"@bpmnkit/core": "0.0.20",
|
|
24
|
+
"@bpmnkit/profiles": "0.0.14"
|
|
22
25
|
},
|
|
23
26
|
"publishConfig": {
|
|
24
27
|
"access": "public"
|
|
@@ -47,6 +50,7 @@
|
|
|
47
50
|
"build": "tsc",
|
|
48
51
|
"typecheck": "tsc --noEmit",
|
|
49
52
|
"check": "biome check .",
|
|
53
|
+
"test": "vitest run",
|
|
50
54
|
"dev": "node --watch dist/index.js",
|
|
51
55
|
"bridge": "esbuild src/bridge.ts --bundle --format=iife --global-name=Bridge --platform=neutral --outfile=dist/bridge.bundle.js",
|
|
52
56
|
"bundle": "esbuild src/index.ts --bundle --platform=node --format=cjs --outfile=dist/bundle.cjs && esbuild src/mcp-server.ts --bundle --platform=node --format=cjs --outfile=dist/mcp-server.cjs && pnpm run bridge"
|