workmatic 1.1.3 → 1.2.0

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.cjs CHANGED
@@ -31,13 +31,18 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
31
31
  var index_exports = {};
32
32
  __export(index_exports, {
33
33
  DEFAULT_WORKER_TIMEOUT_MS: () => DEFAULT_WORKER_TIMEOUT_MS,
34
+ MCP_TOOL_DEFINITIONS: () => MCP_TOOL_DEFINITIONS,
35
+ attachGracefulShutdown: () => attachGracefulShutdown,
34
36
  createClient: () => createClient,
35
37
  createDashboard: () => createDashboard,
36
38
  createDashboardMiddleware: () => createDashboardMiddleware,
37
39
  createDatabase: () => createDatabase,
40
+ createMcpServer: () => createMcpServer,
38
41
  createOrchestrator: () => createOrchestrator,
39
42
  createWorker: () => createWorker,
40
43
  defaultBackoff: () => defaultBackoff,
44
+ enableStatementCache: () => enableStatementCache,
45
+ executeTool: () => executeTool,
41
46
  getUnderlyingDb: () => getUnderlyingDb,
42
47
  validatePayload: () => validatePayload
43
48
  });
@@ -62,6 +67,11 @@ function createDatabase(options = {}) {
62
67
  sqliteDb.pragma("journal_mode = WAL");
63
68
  sqliteDb.pragma("synchronous = NORMAL");
64
69
  sqliteDb.pragma("busy_timeout = 5000");
70
+ sqliteDb.pragma("cache_size = -64000");
71
+ sqliteDb.pragma("temp_store = MEMORY");
72
+ sqliteDb.pragma("mmap_size = 268435456");
73
+ const cacheSize = options.statementCacheSize ?? 1e3;
74
+ enableStatementCache(sqliteDb, cacheSize);
65
75
  const db = new import_kysely.Kysely({
66
76
  dialect: new import_kysely.SqliteDialect({
67
77
  database: sqliteDb
@@ -89,13 +99,23 @@ function createSchema(db) {
89
99
  last_error TEXT
90
100
  )
91
101
  `);
102
+ ensurePartialIndex(
103
+ db,
104
+ "idx_workmatic_jobs_claim",
105
+ `CREATE INDEX IF NOT EXISTS idx_workmatic_jobs_claim
106
+ ON workmatic_jobs (queue, status, run_at, priority, id)
107
+ WHERE status = 'ready'`
108
+ );
109
+ ensurePartialIndex(
110
+ db,
111
+ "idx_workmatic_jobs_lease",
112
+ `CREATE INDEX IF NOT EXISTS idx_workmatic_jobs_lease
113
+ ON workmatic_jobs (status, lease_until)
114
+ WHERE status = 'running'`
115
+ );
92
116
  db.exec(`
93
- CREATE INDEX IF NOT EXISTS idx_workmatic_jobs_claim
94
- ON workmatic_jobs (queue, status, run_at, priority, id)
95
- `);
96
- db.exec(`
97
- CREATE INDEX IF NOT EXISTS idx_workmatic_jobs_lease
98
- ON workmatic_jobs (status, lease_until)
117
+ CREATE INDEX IF NOT EXISTS idx_workmatic_jobs_queue_status
118
+ ON workmatic_jobs (queue, status)
99
119
  `);
100
120
  db.exec(`
101
121
  CREATE TABLE IF NOT EXISTS workmatic_settings (
@@ -108,6 +128,15 @@ function createSchema(db) {
108
128
  UPDATE workmatic_jobs SET status = 'dead' WHERE status = 'failed'
109
129
  `);
110
130
  }
131
+ function ensurePartialIndex(db, indexName, createSql) {
132
+ const row = db.prepare(
133
+ "SELECT sql FROM sqlite_master WHERE type = 'index' AND name = ?"
134
+ ).get(indexName);
135
+ if (row?.sql && !row.sql.toUpperCase().includes("WHERE")) {
136
+ db.exec(`DROP INDEX IF EXISTS ${indexName}`);
137
+ }
138
+ db.exec(createSql);
139
+ }
111
140
  function getUnderlyingDb(db) {
112
141
  const mapped = kyselyToSqlite.get(db);
113
142
  if (mapped) {
@@ -125,6 +154,37 @@ function getUnderlyingDb(db) {
125
154
  "getUnderlyingDb: could not resolve better-sqlite3 instance (use createDatabase() or pass db from it)"
126
155
  );
127
156
  }
157
+ function enableStatementCache(db, maxStatements = 1e3) {
158
+ if (maxStatements <= 0) {
159
+ return db;
160
+ }
161
+ const originalPrepare = db.prepare.bind(db);
162
+ const cache = /* @__PURE__ */ new Map();
163
+ db.prepare = function(sql5) {
164
+ const cached = cache.get(sql5);
165
+ if (cached) {
166
+ cache.delete(sql5);
167
+ cache.set(sql5, cached);
168
+ if (!cached.busy) {
169
+ return cached;
170
+ }
171
+ return originalPrepare(sql5);
172
+ }
173
+ const stmt = originalPrepare(sql5);
174
+ if (cache.size >= maxStatements) {
175
+ const oldestKey = cache.keys().next().value;
176
+ cache.delete(oldestKey);
177
+ }
178
+ cache.set(sql5, stmt);
179
+ return stmt;
180
+ };
181
+ const originalClose = db.close.bind(db);
182
+ db.close = function() {
183
+ cache.clear();
184
+ return originalClose();
185
+ };
186
+ return db;
187
+ }
128
188
 
129
189
  // src/client.ts
130
190
  var import_nanoid = require("nanoid");
@@ -158,10 +218,16 @@ function now() {
158
218
 
159
219
  // src/client.ts
160
220
  function createClient(options) {
161
- const { db, queue = "default" } = options;
221
+ const { db, queue = "default", onJobAdded, worker } = options;
162
222
  if (!db) {
163
223
  throw new Error("Database instance is required");
164
224
  }
225
+ function notifyJobAdded(delayMs) {
226
+ if (delayMs <= 0) {
227
+ worker?.wakeUp();
228
+ onJobAdded?.();
229
+ }
230
+ }
165
231
  return {
166
232
  /**
167
233
  * Add a job to the queue
@@ -176,20 +242,13 @@ function createClient(options) {
176
242
  const publicId = (0, import_nanoid.nanoid)();
177
243
  const timestamp = now();
178
244
  const runAt = timestamp + delayMs;
179
- await db.insertInto("workmatic_jobs").values({
180
- public_id: publicId,
181
- queue,
182
- payload: payloadJson,
183
- status: "ready",
184
- priority,
185
- run_at: runAt,
186
- attempts: 0,
187
- max_attempts: maxAttempts,
188
- lease_until: 0,
189
- created_at: timestamp,
190
- updated_at: timestamp,
191
- last_error: null
192
- }).execute();
245
+ await db.executeQuery(
246
+ import_kysely2.CompiledQuery.raw(
247
+ `INSERT INTO workmatic_jobs (public_id, queue, payload, status, priority, run_at, attempts, max_attempts, lease_until, created_at, updated_at, last_error) VALUES (?, ?, ?, 'ready', ?, ?, 0, ?, 0, ?, ?, null)`,
248
+ [publicId, queue, payloadJson, priority, runAt, maxAttempts, timestamp, timestamp]
249
+ )
250
+ );
251
+ notifyJobAdded(delayMs);
193
252
  return { ok: true, id: publicId };
194
253
  },
195
254
  async addMany(payloads, opts = {}) {
@@ -203,7 +262,7 @@ function createClient(options) {
203
262
  }
204
263
  const timestamp = now();
205
264
  const runAt = timestamp + delayMs;
206
- return await db.transaction().execute(async (trx) => {
265
+ const result = await db.transaction().execute(async (trx) => {
207
266
  const ids = [];
208
267
  const rows = payloads.map((payload) => {
209
268
  const payloadJson = validatePayload(payload);
@@ -227,15 +286,19 @@ function createClient(options) {
227
286
  await trx.insertInto("workmatic_jobs").values(rows).execute();
228
287
  return { ok: true, ids };
229
288
  });
289
+ notifyJobAdded(delayMs);
290
+ return result;
230
291
  },
231
292
  /**
232
293
  * Get job statistics for the queue
233
294
  */
234
295
  async stats() {
235
- const result = await db.selectFrom("workmatic_jobs").select([
236
- "status",
237
- import_kysely2.sql`count(*)`.as("count")
238
- ]).where("queue", "=", queue).groupBy("status").execute();
296
+ const result = await db.executeQuery(
297
+ import_kysely2.CompiledQuery.raw(
298
+ "SELECT status, count(*) AS count FROM workmatic_jobs WHERE queue = ? GROUP BY status",
299
+ [queue]
300
+ )
301
+ );
239
302
  const stats = {
240
303
  ready: 0,
241
304
  running: 0,
@@ -243,7 +306,7 @@ function createClient(options) {
243
306
  dead: 0,
244
307
  total: 0
245
308
  };
246
- for (const row of result) {
309
+ for (const row of result.rows) {
247
310
  const status = row.status;
248
311
  const count = Number(row.count);
249
312
  if (status in stats) {
@@ -270,6 +333,76 @@ function createClient(options) {
270
333
  // src/worker.ts
271
334
  var import_fastq = __toESM(require("fastq"), 1);
272
335
  var import_kysely3 = require("kysely");
336
+
337
+ // src/shutdown.ts
338
+ function attachGracefulShutdown(target, options = {}) {
339
+ const {
340
+ signals = ["SIGINT", "SIGTERM"],
341
+ timeoutMs = 3e4,
342
+ exitOnComplete = true,
343
+ exitCode = 0,
344
+ timeoutExitCode = 1,
345
+ onShutdownStart,
346
+ onShutdownComplete,
347
+ onShutdownError
348
+ } = options;
349
+ let shuttingDown = false;
350
+ async function stopTarget() {
351
+ if (Array.isArray(target)) {
352
+ await Promise.all(target.map((w) => w.stop()));
353
+ } else if ("stopAll" in target) {
354
+ await target.stopAll();
355
+ } else {
356
+ await target.stop();
357
+ }
358
+ }
359
+ const handler = async (signal) => {
360
+ if (shuttingDown) {
361
+ return;
362
+ }
363
+ shuttingDown = true;
364
+ onShutdownStart?.(signal);
365
+ let timer = null;
366
+ if (timeoutMs > 0) {
367
+ timer = setTimeout(() => {
368
+ const err = new Error(`Graceful shutdown timed out after ${timeoutMs}ms`);
369
+ onShutdownError?.(err);
370
+ if (exitOnComplete) {
371
+ process.exit(timeoutExitCode);
372
+ }
373
+ }, timeoutMs);
374
+ timer.unref();
375
+ }
376
+ try {
377
+ await stopTarget();
378
+ if (timer) {
379
+ clearTimeout(timer);
380
+ }
381
+ onShutdownComplete?.();
382
+ if (exitOnComplete) {
383
+ process.exit(exitCode);
384
+ }
385
+ } catch (err) {
386
+ if (timer) {
387
+ clearTimeout(timer);
388
+ }
389
+ onShutdownError?.(err);
390
+ if (exitOnComplete) {
391
+ process.exit(timeoutExitCode);
392
+ }
393
+ }
394
+ };
395
+ for (const sig of signals) {
396
+ process.on(sig, handler);
397
+ }
398
+ return function detach() {
399
+ for (const sig of signals) {
400
+ process.removeListener(sig, handler);
401
+ }
402
+ };
403
+ }
404
+
405
+ // src/worker.ts
273
406
  var DEFAULT_WORKER_TIMEOUT_MS = 6e4;
274
407
  function parseClaimedRows(result) {
275
408
  const rows = result.rows;
@@ -307,7 +440,8 @@ function createWorker(options) {
307
440
  autoRestore = true,
308
441
  pauseCheckIntervalMs = 300,
309
442
  requeueExpiredIntervalMs = 0,
310
- onPumpError
443
+ onPumpError,
444
+ completionBatchSize = 50
311
445
  } = options;
312
446
  if (!db) {
313
447
  throw new Error("Database instance is required");
@@ -320,6 +454,8 @@ function createWorker(options) {
320
454
  let lastPauseCheckAt = 0;
321
455
  let cachedDbPaused = false;
322
456
  let lastRequeueAt = 0;
457
+ let pendingDone = [];
458
+ let flushTimeout = null;
323
459
  function notifyPumpError(error) {
324
460
  console.error("[workmatic] Pump error:", error);
325
461
  onPumpError?.(error);
@@ -367,52 +503,104 @@ function createWorker(options) {
367
503
  async function claimBatch(limit) {
368
504
  const timestamp = now();
369
505
  const leaseUntil = timestamp + leaseMs;
370
- return await db.transaction().execute(async (trx) => {
371
- const result = await import_kysely3.sql`
372
- UPDATE workmatic_jobs
373
- SET status = 'running', lease_until = ${leaseUntil}, updated_at = ${timestamp}
374
- WHERE rowid IN (
375
- SELECT rowid FROM workmatic_jobs
376
- WHERE queue = ${queue}
377
- AND status = 'ready'
378
- AND run_at <= ${timestamp}
379
- ORDER BY priority ASC, id ASC
380
- LIMIT ${limit}
506
+ const result = await import_kysely3.sql`
507
+ UPDATE workmatic_jobs
508
+ SET status = 'running', lease_until = ${leaseUntil}, updated_at = ${timestamp}
509
+ WHERE rowid IN (
510
+ SELECT rowid FROM workmatic_jobs
511
+ WHERE queue = ${queue}
512
+ AND status = 'ready'
513
+ AND run_at <= ${timestamp}
514
+ ORDER BY priority ASC, id ASC
515
+ LIMIT ${limit}
516
+ )
517
+ RETURNING id, public_id, queue, payload, attempts, max_attempts, priority, created_at, last_error
518
+ `.execute(db);
519
+ return parseClaimedRows(result);
520
+ }
521
+ async function flushDoneBatch() {
522
+ if (flushTimeout !== null) {
523
+ clearImmediate(flushTimeout);
524
+ flushTimeout = null;
525
+ }
526
+ if (pendingDone.length === 0) {
527
+ return;
528
+ }
529
+ const current = pendingDone;
530
+ pendingDone = [];
531
+ const timestamp = now();
532
+ try {
533
+ if (current.length === 1) {
534
+ await db.executeQuery(
535
+ import_kysely3.CompiledQuery.raw(
536
+ "UPDATE workmatic_jobs SET status = 'done', lease_until = 0, updated_at = ? WHERE id = ?",
537
+ [timestamp, current[0].id]
538
+ )
539
+ );
540
+ } else {
541
+ const CHUNK_SIZE = 500;
542
+ for (let i = 0; i < current.length; i += CHUNK_SIZE) {
543
+ const chunk = current.slice(i, i + CHUNK_SIZE);
544
+ const placeholders = chunk.map(() => "?").join(", ");
545
+ const params = [timestamp, ...chunk.map((item) => item.id)];
546
+ await db.executeQuery(
547
+ import_kysely3.CompiledQuery.raw(
548
+ `UPDATE workmatic_jobs SET status = 'done', lease_until = 0, updated_at = ? WHERE id IN (${placeholders})`,
549
+ params
550
+ )
551
+ );
552
+ }
553
+ }
554
+ for (const item of current) {
555
+ item.resolve();
556
+ }
557
+ } catch (err) {
558
+ for (const item of current) {
559
+ item.reject(err);
560
+ }
561
+ }
562
+ }
563
+ function markDone(jobId) {
564
+ if (completionBatchSize <= 0) {
565
+ return db.executeQuery(
566
+ import_kysely3.CompiledQuery.raw(
567
+ "UPDATE workmatic_jobs SET status = 'done', lease_until = 0, updated_at = ? WHERE id = ?",
568
+ [now(), jobId]
381
569
  )
382
- RETURNING id, public_id, queue, payload, attempts, max_attempts, priority, created_at, last_error
383
- `.execute(trx);
384
- return parseClaimedRows(result);
570
+ ).then(() => {
571
+ });
572
+ }
573
+ return new Promise((resolve, reject) => {
574
+ pendingDone.push({ id: jobId, resolve, reject });
575
+ if (pendingDone.length >= completionBatchSize) {
576
+ void flushDoneBatch();
577
+ } else if (!flushTimeout) {
578
+ flushTimeout = setImmediate(() => {
579
+ flushTimeout = null;
580
+ void flushDoneBatch();
581
+ });
582
+ }
385
583
  });
386
584
  }
387
- async function markDone(jobId) {
388
- await db.updateTable("workmatic_jobs").set({
389
- status: "done",
390
- lease_until: 0,
391
- updated_at: now()
392
- }).where("id", "=", jobId).execute();
393
- }
394
585
  async function markFailed(jobId, attempts, maxAttempts, error) {
395
586
  const timestamp = now();
396
587
  const newAttempts = attempts + 1;
397
588
  const errorMessage = error.message || String(error);
398
589
  if (newAttempts < maxAttempts) {
399
590
  const runAt = timestamp + backoff(newAttempts);
400
- await db.updateTable("workmatic_jobs").set({
401
- status: "ready",
402
- attempts: newAttempts,
403
- run_at: runAt,
404
- lease_until: 0,
405
- last_error: errorMessage,
406
- updated_at: timestamp
407
- }).where("id", "=", jobId).execute();
591
+ await db.executeQuery(
592
+ import_kysely3.CompiledQuery.raw(
593
+ "UPDATE workmatic_jobs SET status = 'ready', attempts = ?, run_at = ?, lease_until = 0, last_error = ?, updated_at = ? WHERE id = ?",
594
+ [newAttempts, runAt, errorMessage, timestamp, jobId]
595
+ )
596
+ );
408
597
  } else {
409
- await db.updateTable("workmatic_jobs").set({
410
- status: "dead",
411
- attempts: newAttempts,
412
- lease_until: 0,
413
- last_error: errorMessage,
414
- updated_at: timestamp
415
- }).where("id", "=", jobId).execute();
598
+ await db.executeQuery(
599
+ import_kysely3.CompiledQuery.raw(
600
+ "UPDATE workmatic_jobs SET status = 'dead', attempts = ?, lease_until = 0, last_error = ?, updated_at = ? WHERE id = ?",
601
+ [newAttempts, errorMessage, timestamp, jobId]
602
+ )
603
+ );
416
604
  }
417
605
  }
418
606
  async function withTimeout(promise, ms, jobId) {
@@ -459,8 +647,13 @@ function createWorker(options) {
459
647
  }
460
648
  }
461
649
  async function isQueuePausedInDb() {
462
- const setting = await db.selectFrom("workmatic_settings").select("paused").where("queue", "=", queue).executeTakeFirst();
463
- return setting?.paused === 1;
650
+ const result = await db.executeQuery(
651
+ import_kysely3.CompiledQuery.raw(
652
+ "SELECT paused FROM workmatic_settings WHERE queue = ? LIMIT 1",
653
+ [queue]
654
+ )
655
+ );
656
+ return result.rows[0]?.paused === 1;
464
657
  }
465
658
  async function pump() {
466
659
  if (!running) {
@@ -529,10 +722,12 @@ function createWorker(options) {
529
722
  }
530
723
  await fastqQueue.drained();
531
724
  fastqQueue = null;
725
+ await flushDoneBatch();
532
726
  await saveState("stopped");
533
727
  },
534
728
  pause() {
535
729
  paused = true;
730
+ void flushDoneBatch();
536
731
  void saveState("paused");
537
732
  },
538
733
  resume() {
@@ -540,10 +735,13 @@ function createWorker(options) {
540
735
  void saveState("running");
541
736
  },
542
737
  async stats() {
543
- const result = await db.selectFrom("workmatic_jobs").select([
544
- "status",
545
- import_kysely3.sql`count(*)`.as("count")
546
- ]).where("queue", "=", queue).groupBy("status").execute();
738
+ await flushDoneBatch();
739
+ const result = await db.executeQuery(
740
+ import_kysely3.CompiledQuery.raw(
741
+ "SELECT status, count(*) AS count FROM workmatic_jobs WHERE queue = ? GROUP BY status",
742
+ [queue]
743
+ )
744
+ );
547
745
  const stats = {
548
746
  ready: 0,
549
747
  running: 0,
@@ -551,7 +749,7 @@ function createWorker(options) {
551
749
  dead: 0,
552
750
  total: 0
553
751
  };
554
- for (const row of result) {
752
+ for (const row of result.rows) {
555
753
  const status = row.status;
556
754
  const count = Number(row.count);
557
755
  if (status in stats) {
@@ -587,6 +785,22 @@ function createWorker(options) {
587
785
  }
588
786
  const result = await query.execute();
589
787
  return Number(result[0]?.numDeletedRows ?? 0);
788
+ },
789
+ wakeUp() {
790
+ if (!running || paused) {
791
+ return;
792
+ }
793
+ if (pumpTimeout) {
794
+ clearTimeout(pumpTimeout);
795
+ pumpTimeout = null;
796
+ }
797
+ pumpTimeout = setTimeout(pump, 0);
798
+ },
799
+ async flushCompletions() {
800
+ await flushDoneBatch();
801
+ },
802
+ attachSignalHandlers(options2) {
803
+ return attachGracefulShutdown(this, options2);
590
804
  }
591
805
  };
592
806
  if (persistState && autoRestore) {
@@ -620,7 +834,15 @@ function createOrchestrator(options) {
620
834
  function ensureEntry(queue) {
621
835
  let entry = registry.get(queue);
622
836
  if (!entry) {
623
- entry = { client: createClient({ db, queue }) };
837
+ entry = {
838
+ client: createClient({
839
+ db,
840
+ queue,
841
+ onJobAdded: () => {
842
+ registry.get(queue)?.worker?.wakeUp();
843
+ }
844
+ })
845
+ };
624
846
  registry.set(queue, entry);
625
847
  }
626
848
  return entry;
@@ -793,6 +1015,9 @@ function createOrchestrator(options) {
793
1015
  return;
794
1016
  }
795
1017
  await db.updateTable("workmatic_jobs").set({ queue: toQueue, updated_at: timestamp }).where("public_id", "=", publicId).execute();
1018
+ },
1019
+ attachSignalHandlers(options2) {
1020
+ return attachGracefulShutdown(this, options2);
796
1021
  }
797
1022
  };
798
1023
  return orchestrator;
@@ -1087,16 +1312,757 @@ function createDashboardMiddleware(options) {
1087
1312
  void handleRequest(req, res, next);
1088
1313
  };
1089
1314
  }
1315
+
1316
+ // src/mcp/server.ts
1317
+ var import_node_readline = __toESM(require("readline"), 1);
1318
+ var import_node_events = require("events");
1319
+
1320
+ // src/mcp/tools.ts
1321
+ var import_kysely6 = require("kysely");
1322
+ var MCP_TOOL_DEFINITIONS = [
1323
+ {
1324
+ name: "workmatic_list_queues",
1325
+ description: "List all queues present in the Workmatic database along with job counts",
1326
+ inputSchema: {
1327
+ type: "object",
1328
+ properties: {}
1329
+ }
1330
+ },
1331
+ {
1332
+ name: "workmatic_get_stats",
1333
+ description: "Get real-time job counts (ready, running, done, dead, total) for a specific queue or all queues",
1334
+ inputSchema: {
1335
+ type: "object",
1336
+ properties: {
1337
+ queue: {
1338
+ type: "string",
1339
+ description: "Optional queue name. If omitted, stats for all queues will be returned."
1340
+ }
1341
+ }
1342
+ }
1343
+ },
1344
+ {
1345
+ name: "workmatic_list_jobs",
1346
+ description: "List jobs in the database filtered by queue, status, and limit",
1347
+ inputSchema: {
1348
+ type: "object",
1349
+ properties: {
1350
+ queue: {
1351
+ type: "string",
1352
+ description: "Filter by queue name"
1353
+ },
1354
+ status: {
1355
+ type: "string",
1356
+ enum: ["ready", "running", "done", "dead"],
1357
+ description: "Filter by job status"
1358
+ },
1359
+ limit: {
1360
+ type: "number",
1361
+ description: "Maximum number of jobs to return (default: 20, max: 100)"
1362
+ },
1363
+ offset: {
1364
+ type: "number",
1365
+ description: "Number of jobs to skip for pagination (default: 0)"
1366
+ }
1367
+ }
1368
+ }
1369
+ },
1370
+ {
1371
+ name: "workmatic_get_dead_jobs",
1372
+ description: "Retrieve failed/dead jobs with error details and payloads for debugging",
1373
+ inputSchema: {
1374
+ type: "object",
1375
+ properties: {
1376
+ queue: {
1377
+ type: "string",
1378
+ description: "Filter dead jobs by queue name"
1379
+ },
1380
+ limit: {
1381
+ type: "number",
1382
+ description: "Maximum number of dead jobs to return (default: 20)"
1383
+ }
1384
+ }
1385
+ }
1386
+ },
1387
+ {
1388
+ name: "workmatic_add_job",
1389
+ description: "Enqueue a new background job into Workmatic",
1390
+ inputSchema: {
1391
+ type: "object",
1392
+ properties: {
1393
+ queue: {
1394
+ type: "string",
1395
+ description: 'Target queue name (default: "default")'
1396
+ },
1397
+ payload: {
1398
+ description: "Job payload (JSON object, string, number, etc.)"
1399
+ },
1400
+ priority: {
1401
+ type: "number",
1402
+ description: "Job priority (lower number = higher priority, default: 0)"
1403
+ },
1404
+ delayMs: {
1405
+ type: "number",
1406
+ description: "Delay in milliseconds before job can run (default: 0)"
1407
+ },
1408
+ maxAttempts: {
1409
+ type: "number",
1410
+ description: "Maximum execution retry attempts (default: 3)"
1411
+ }
1412
+ },
1413
+ required: ["payload"]
1414
+ }
1415
+ },
1416
+ {
1417
+ name: "workmatic_retry_job",
1418
+ description: "Retry a specific dead or failed job by resetting it to ready status",
1419
+ inputSchema: {
1420
+ type: "object",
1421
+ properties: {
1422
+ publicId: {
1423
+ type: "string",
1424
+ description: "Public ID of the job to retry"
1425
+ }
1426
+ },
1427
+ required: ["publicId"]
1428
+ }
1429
+ },
1430
+ {
1431
+ name: "workmatic_retry_all_dead",
1432
+ description: "Retry all dead jobs (optionally in a specific queue) by resetting them to ready status",
1433
+ inputSchema: {
1434
+ type: "object",
1435
+ properties: {
1436
+ queue: {
1437
+ type: "string",
1438
+ description: "Optional queue name to restrict retrying"
1439
+ }
1440
+ }
1441
+ }
1442
+ },
1443
+ {
1444
+ name: "workmatic_pause_queue",
1445
+ description: "Pause a queue so workers stop claiming new jobs from it",
1446
+ inputSchema: {
1447
+ type: "object",
1448
+ properties: {
1449
+ queue: {
1450
+ type: "string",
1451
+ description: "Queue name to pause"
1452
+ }
1453
+ },
1454
+ required: ["queue"]
1455
+ }
1456
+ },
1457
+ {
1458
+ name: "workmatic_resume_queue",
1459
+ description: "Resume a paused queue so workers resume claiming jobs",
1460
+ inputSchema: {
1461
+ type: "object",
1462
+ properties: {
1463
+ queue: {
1464
+ type: "string",
1465
+ description: "Queue name to resume"
1466
+ }
1467
+ },
1468
+ required: ["queue"]
1469
+ }
1470
+ },
1471
+ {
1472
+ name: "workmatic_purge_jobs",
1473
+ description: "Permanently remove done or dead jobs from the database",
1474
+ inputSchema: {
1475
+ type: "object",
1476
+ properties: {
1477
+ queue: {
1478
+ type: "string",
1479
+ description: "Queue name to purge jobs from (optional)"
1480
+ },
1481
+ status: {
1482
+ type: "string",
1483
+ enum: ["done", "dead", "all"],
1484
+ description: 'Status of jobs to purge (default: "done")'
1485
+ }
1486
+ }
1487
+ }
1488
+ },
1489
+ {
1490
+ name: "workmatic_transfer_jobs",
1491
+ description: "Move jobs from one queue to another (e.g. from dead-letter queue back to primary)",
1492
+ inputSchema: {
1493
+ type: "object",
1494
+ properties: {
1495
+ fromQueue: {
1496
+ type: "string",
1497
+ description: "Source queue name"
1498
+ },
1499
+ toQueue: {
1500
+ type: "string",
1501
+ description: "Destination queue name"
1502
+ },
1503
+ status: {
1504
+ type: "string",
1505
+ enum: ["ready", "dead"],
1506
+ description: 'Status of jobs to transfer (default: "ready")'
1507
+ },
1508
+ limit: {
1509
+ type: "number",
1510
+ description: "Maximum number of jobs to transfer (default: 1000)"
1511
+ },
1512
+ resetForRetry: {
1513
+ type: "boolean",
1514
+ description: "If transferring dead jobs, reset their status to ready (default: false)"
1515
+ }
1516
+ },
1517
+ required: ["fromQueue", "toQueue"]
1518
+ }
1519
+ },
1520
+ {
1521
+ name: "workmatic_update_job_status",
1522
+ description: "Update the status of a specific job (ready, done, dead) and signal workers / listeners",
1523
+ inputSchema: {
1524
+ type: "object",
1525
+ properties: {
1526
+ publicId: {
1527
+ type: "string",
1528
+ description: "Public ID of the job to update"
1529
+ },
1530
+ status: {
1531
+ type: "string",
1532
+ enum: ["ready", "done", "dead"],
1533
+ description: "New status for the job"
1534
+ },
1535
+ error: {
1536
+ type: "string",
1537
+ description: "Optional error message when marking as dead or recording failure details"
1538
+ },
1539
+ resetAttempts: {
1540
+ type: "boolean",
1541
+ description: "Whether to reset attempts to 0 (default: true if status is ready, false otherwise)"
1542
+ },
1543
+ delayMs: {
1544
+ type: "number",
1545
+ description: "Delay in milliseconds before the job becomes ready (default: 0)"
1546
+ }
1547
+ },
1548
+ required: ["publicId", "status"]
1549
+ }
1550
+ }
1551
+ ];
1552
+ async function executeTool(db, name, args = {}, context) {
1553
+ switch (name) {
1554
+ case "workmatic_list_queues": {
1555
+ const qJobs = await db.selectFrom("workmatic_jobs").select("queue").distinct().execute();
1556
+ const qSettings = await db.selectFrom("workmatic_settings").select("queue").distinct().execute();
1557
+ const set = /* @__PURE__ */ new Set();
1558
+ for (const r of qJobs) set.add(r.queue);
1559
+ for (const r of qSettings) {
1560
+ if (!r.queue.startsWith("worker_state_")) {
1561
+ set.add(r.queue);
1562
+ }
1563
+ }
1564
+ const queueList = Array.from(set).sort();
1565
+ const result = [];
1566
+ for (const q of queueList) {
1567
+ const client = createClient({ db, queue: q });
1568
+ const stats = await client.stats();
1569
+ const setting = await db.selectFrom("workmatic_settings").select("paused").where("queue", "=", q).executeTakeFirst();
1570
+ result.push({
1571
+ queue: q,
1572
+ stats: {
1573
+ ready: stats.ready,
1574
+ running: stats.running,
1575
+ done: stats.done,
1576
+ dead: stats.dead,
1577
+ total: stats.total
1578
+ },
1579
+ isPaused: setting?.paused === 1
1580
+ });
1581
+ }
1582
+ return { queues: result, totalQueues: result.length };
1583
+ }
1584
+ case "workmatic_get_stats": {
1585
+ const queue = args.queue;
1586
+ if (queue) {
1587
+ const client = createClient({ db, queue });
1588
+ return { queue, stats: await client.stats() };
1589
+ }
1590
+ const listRes = await executeTool(db, "workmatic_list_queues");
1591
+ const summary = {};
1592
+ const grandTotal = { ready: 0, running: 0, done: 0, dead: 0, total: 0 };
1593
+ for (const item of listRes.queues) {
1594
+ summary[item.queue] = item.stats;
1595
+ grandTotal.ready += item.stats.ready;
1596
+ grandTotal.running += item.stats.running;
1597
+ grandTotal.done += item.stats.done;
1598
+ grandTotal.dead += item.stats.dead;
1599
+ grandTotal.total += item.stats.total;
1600
+ }
1601
+ return { queues: summary, grandTotal };
1602
+ }
1603
+ case "workmatic_list_jobs": {
1604
+ const queue = args.queue;
1605
+ const status = args.status;
1606
+ const limit = Math.min(Math.max(Number(args.limit ?? 20), 1), 100);
1607
+ const offset = Math.max(Number(args.offset ?? 0), 0);
1608
+ let query = db.selectFrom("workmatic_jobs").select([
1609
+ "id",
1610
+ "public_id",
1611
+ "queue",
1612
+ "status",
1613
+ "priority",
1614
+ "payload",
1615
+ "attempts",
1616
+ "max_attempts",
1617
+ "run_at",
1618
+ "created_at",
1619
+ "updated_at",
1620
+ "last_error"
1621
+ ]);
1622
+ if (queue) {
1623
+ query = query.where("queue", "=", queue);
1624
+ }
1625
+ if (status) {
1626
+ query = query.where("status", "=", status);
1627
+ }
1628
+ const rows = await query.orderBy("priority", "asc").orderBy("id", "asc").limit(limit).offset(offset).execute();
1629
+ const jobs = rows.map((r) => {
1630
+ let parsedPayload;
1631
+ try {
1632
+ parsedPayload = JSON.parse(r.payload);
1633
+ } catch {
1634
+ parsedPayload = r.payload;
1635
+ }
1636
+ return {
1637
+ id: r.id,
1638
+ publicId: r.public_id,
1639
+ queue: r.queue,
1640
+ status: r.status,
1641
+ priority: r.priority,
1642
+ attempts: r.attempts,
1643
+ maxAttempts: r.max_attempts,
1644
+ runAt: r.run_at,
1645
+ createdAt: r.created_at,
1646
+ updatedAt: r.updated_at,
1647
+ lastError: r.last_error,
1648
+ payload: parsedPayload
1649
+ };
1650
+ });
1651
+ return { jobs, count: jobs.length, limit, offset };
1652
+ }
1653
+ case "workmatic_get_dead_jobs": {
1654
+ const queue = args.queue;
1655
+ const limit = Math.min(Math.max(Number(args.limit ?? 20), 1), 100);
1656
+ return executeTool(db, "workmatic_list_jobs", {
1657
+ queue,
1658
+ status: "dead",
1659
+ limit
1660
+ });
1661
+ }
1662
+ case "workmatic_add_job": {
1663
+ const queue = args.queue || "default";
1664
+ const payload = args.payload;
1665
+ const priority = args.priority !== void 0 ? Number(args.priority) : 0;
1666
+ const delayMs = args.delayMs !== void 0 ? Number(args.delayMs) : 0;
1667
+ const maxAttempts = args.maxAttempts !== void 0 ? Number(args.maxAttempts) : 3;
1668
+ const client = createClient({ db, queue });
1669
+ const result = await client.add(payload, { priority, delayMs, maxAttempts });
1670
+ return { ok: true, id: result.id, queue };
1671
+ }
1672
+ case "workmatic_retry_job": {
1673
+ const publicId = args.publicId;
1674
+ if (!publicId) {
1675
+ throw new Error("publicId is required");
1676
+ }
1677
+ const job = await db.selectFrom("workmatic_jobs").select(["queue", "status"]).where("public_id", "=", publicId).executeTakeFirst();
1678
+ if (!job) {
1679
+ throw new Error(`Job not found: ${publicId}`);
1680
+ }
1681
+ const timestamp = now();
1682
+ await db.updateTable("workmatic_jobs").set({
1683
+ status: "ready",
1684
+ attempts: 0,
1685
+ lease_until: 0,
1686
+ last_error: null,
1687
+ run_at: timestamp,
1688
+ updated_at: timestamp
1689
+ }).where("public_id", "=", publicId).execute();
1690
+ context?.onJobStatusChanged?.({
1691
+ publicId,
1692
+ queue: job.queue,
1693
+ previousStatus: job.status,
1694
+ status: "ready",
1695
+ timestamp,
1696
+ error: null
1697
+ });
1698
+ return { ok: true, id: publicId, message: `Job ${publicId} reset to ready` };
1699
+ }
1700
+ case "workmatic_retry_all_dead": {
1701
+ const queue = args.queue;
1702
+ const timestamp = now();
1703
+ let query = db.updateTable("workmatic_jobs").set({
1704
+ status: "ready",
1705
+ attempts: 0,
1706
+ lease_until: 0,
1707
+ last_error: null,
1708
+ run_at: timestamp,
1709
+ updated_at: timestamp
1710
+ }).where("status", "=", "dead");
1711
+ if (queue) {
1712
+ query = query.where("queue", "=", queue);
1713
+ }
1714
+ const res = await query.execute();
1715
+ const retriedCount = Number(res[0].numUpdatedRows);
1716
+ if (retriedCount > 0) {
1717
+ context?.onJobStatusChanged?.({
1718
+ publicId: "*",
1719
+ queue: queue ?? "*",
1720
+ previousStatus: "dead",
1721
+ status: "ready",
1722
+ timestamp,
1723
+ error: null
1724
+ });
1725
+ }
1726
+ return { ok: true, retriedCount, queue: queue ?? "all" };
1727
+ }
1728
+ case "workmatic_update_job_status": {
1729
+ const publicId = args.publicId;
1730
+ const status = args.status;
1731
+ if (!publicId) {
1732
+ throw new Error("publicId is required");
1733
+ }
1734
+ if (!status || !["ready", "done", "dead"].includes(status)) {
1735
+ throw new Error("status is required and must be 'ready', 'done', or 'dead'");
1736
+ }
1737
+ const job = await db.selectFrom("workmatic_jobs").select(["id", "public_id", "queue", "status", "attempts"]).where("public_id", "=", publicId).executeTakeFirst();
1738
+ if (!job) {
1739
+ throw new Error(`Job not found: ${publicId}`);
1740
+ }
1741
+ if (job.status === status) {
1742
+ return {
1743
+ ok: true,
1744
+ id: publicId,
1745
+ queue: job.queue,
1746
+ status,
1747
+ unchanged: true
1748
+ };
1749
+ }
1750
+ const timestamp = now();
1751
+ const previousStatus = job.status;
1752
+ const delayMs = Math.max(Number(args.delayMs ?? 0), 0);
1753
+ const resetAttempts = args.resetAttempts !== void 0 ? Boolean(args.resetAttempts) : status === "ready";
1754
+ const updateData = {
1755
+ status,
1756
+ updated_at: timestamp,
1757
+ lease_until: 0
1758
+ };
1759
+ if (status === "ready") {
1760
+ updateData.run_at = timestamp + delayMs;
1761
+ }
1762
+ if (resetAttempts) {
1763
+ updateData.attempts = 0;
1764
+ }
1765
+ if (args.error !== void 0) {
1766
+ updateData.last_error = args.error;
1767
+ } else if (status === "ready") {
1768
+ updateData.last_error = null;
1769
+ }
1770
+ await db.updateTable("workmatic_jobs").set(updateData).where("public_id", "=", publicId).execute();
1771
+ const event = {
1772
+ publicId,
1773
+ queue: job.queue,
1774
+ previousStatus,
1775
+ status,
1776
+ timestamp,
1777
+ error: updateData.last_error ?? null
1778
+ };
1779
+ context?.onJobStatusChanged?.(event);
1780
+ return {
1781
+ ok: true,
1782
+ id: publicId,
1783
+ queue: job.queue,
1784
+ previousStatus,
1785
+ status,
1786
+ signaled: true
1787
+ };
1788
+ }
1789
+ case "workmatic_pause_queue": {
1790
+ const queue = args.queue;
1791
+ if (!queue) {
1792
+ throw new Error("queue is required");
1793
+ }
1794
+ const timestamp = now();
1795
+ await import_kysely6.sql`
1796
+ INSERT INTO workmatic_settings (queue, paused, updated_at)
1797
+ VALUES (${queue}, 1, ${timestamp})
1798
+ ON CONFLICT(queue) DO UPDATE SET
1799
+ paused = 1,
1800
+ updated_at = ${timestamp}
1801
+ `.execute(db);
1802
+ return { ok: true, queue, paused: true };
1803
+ }
1804
+ case "workmatic_resume_queue": {
1805
+ const queue = args.queue;
1806
+ if (!queue) {
1807
+ throw new Error("queue is required");
1808
+ }
1809
+ const timestamp = now();
1810
+ await import_kysely6.sql`
1811
+ INSERT INTO workmatic_settings (queue, paused, updated_at)
1812
+ VALUES (${queue}, 0, ${timestamp})
1813
+ ON CONFLICT(queue) DO UPDATE SET
1814
+ paused = 0,
1815
+ updated_at = ${timestamp}
1816
+ `.execute(db);
1817
+ return { ok: true, queue, paused: false };
1818
+ }
1819
+ case "workmatic_purge_jobs": {
1820
+ const queue = args.queue;
1821
+ const status = args.status || "done";
1822
+ let query = db.deleteFrom("workmatic_jobs");
1823
+ if (queue) {
1824
+ query = query.where("queue", "=", queue);
1825
+ }
1826
+ if (status !== "all") {
1827
+ query = query.where("status", "=", status);
1828
+ }
1829
+ const res = await query.execute();
1830
+ const deletedCount = Number(res[0].numDeletedRows);
1831
+ return { ok: true, deletedCount, queue: queue ?? "all", status };
1832
+ }
1833
+ case "workmatic_transfer_jobs": {
1834
+ const fromQueue = args.fromQueue;
1835
+ const toQueue = args.toQueue;
1836
+ if (!fromQueue || !toQueue) {
1837
+ throw new Error("fromQueue and toQueue are required");
1838
+ }
1839
+ if (fromQueue === toQueue) {
1840
+ return { ok: true, moved: 0 };
1841
+ }
1842
+ const status = args.status || "ready";
1843
+ const limit = Math.max(Number(args.limit ?? 1e3), 1);
1844
+ const resetForRetry = Boolean(args.resetForRetry);
1845
+ const orch = createOrchestrator({ db });
1846
+ const result = await orch.transfer({
1847
+ from: fromQueue,
1848
+ to: toQueue,
1849
+ status,
1850
+ limit,
1851
+ resetForRetry
1852
+ });
1853
+ return {
1854
+ ok: true,
1855
+ moved: result.moved,
1856
+ fromQueue,
1857
+ toQueue,
1858
+ status: resetForRetry && status === "dead" ? "ready" : status
1859
+ };
1860
+ }
1861
+ default:
1862
+ throw new Error(`Unknown tool: ${name}`);
1863
+ }
1864
+ }
1865
+
1866
+ // src/mcp/server.ts
1867
+ function createMcpServer(options) {
1868
+ const db = options.db;
1869
+ const input = options.input ?? process.stdin;
1870
+ const output = options.output ?? process.stdout;
1871
+ const emitter = new import_node_events.EventEmitter();
1872
+ let rl = null;
1873
+ let isRunning = false;
1874
+ function notifyJobStatusChanged(event) {
1875
+ if (options.onJobStatusChanged) {
1876
+ options.onJobStatusChanged(event);
1877
+ }
1878
+ emitter.emit("jobStatusChanged", event);
1879
+ if (event.status === "ready") {
1880
+ if (options.orchestrator) {
1881
+ if (event.queue === "*") {
1882
+ for (const worker of options.orchestrator.workers()) {
1883
+ worker.wakeUp();
1884
+ }
1885
+ } else {
1886
+ try {
1887
+ options.orchestrator.worker(event.queue).wakeUp();
1888
+ } catch {
1889
+ }
1890
+ }
1891
+ }
1892
+ if (options.workers) {
1893
+ for (const worker of options.workers) {
1894
+ if (event.queue === "*" || worker.queue === event.queue) {
1895
+ worker.wakeUp();
1896
+ }
1897
+ }
1898
+ }
1899
+ }
1900
+ if (isRunning) {
1901
+ output.write(
1902
+ JSON.stringify({
1903
+ jsonrpc: "2.0",
1904
+ method: "notifications/workmatic/job_status_changed",
1905
+ params: event
1906
+ }) + "\n"
1907
+ );
1908
+ }
1909
+ }
1910
+ async function handleMessage(raw) {
1911
+ const trimmed = raw.trim();
1912
+ if (!trimmed) {
1913
+ return null;
1914
+ }
1915
+ let msg;
1916
+ try {
1917
+ msg = JSON.parse(trimmed);
1918
+ } catch {
1919
+ return JSON.stringify({
1920
+ jsonrpc: "2.0",
1921
+ id: null,
1922
+ error: {
1923
+ code: -32700,
1924
+ message: "Parse error"
1925
+ }
1926
+ });
1927
+ }
1928
+ if (!msg || typeof msg !== "object" || msg.jsonrpc !== "2.0" || typeof msg.method !== "string") {
1929
+ return JSON.stringify({
1930
+ jsonrpc: "2.0",
1931
+ id: msg?.id ?? null,
1932
+ error: {
1933
+ code: -32600,
1934
+ message: "Invalid Request"
1935
+ }
1936
+ });
1937
+ }
1938
+ const isNotification = msg.id === void 0;
1939
+ switch (msg.method) {
1940
+ case "initialize": {
1941
+ const result = {
1942
+ protocolVersion: "2024-11-05",
1943
+ capabilities: {
1944
+ tools: {}
1945
+ },
1946
+ serverInfo: {
1947
+ name: "workmatic-mcp",
1948
+ version: "0.1.0"
1949
+ }
1950
+ };
1951
+ return isNotification ? null : JSON.stringify({ jsonrpc: "2.0", id: msg.id, result });
1952
+ }
1953
+ case "notifications/initialized": {
1954
+ return null;
1955
+ }
1956
+ case "ping": {
1957
+ return isNotification ? null : JSON.stringify({ jsonrpc: "2.0", id: msg.id, result: {} });
1958
+ }
1959
+ case "tools/list": {
1960
+ const result = {
1961
+ tools: MCP_TOOL_DEFINITIONS
1962
+ };
1963
+ return isNotification ? null : JSON.stringify({ jsonrpc: "2.0", id: msg.id, result });
1964
+ }
1965
+ case "tools/call": {
1966
+ if (!msg.params || typeof msg.params.name !== "string") {
1967
+ return isNotification ? null : JSON.stringify({
1968
+ jsonrpc: "2.0",
1969
+ id: msg.id,
1970
+ error: {
1971
+ code: -32602,
1972
+ message: 'Invalid params: tool "name" is required'
1973
+ }
1974
+ });
1975
+ }
1976
+ const toolName = msg.params.name;
1977
+ const toolArgs = msg.params.arguments ?? {};
1978
+ try {
1979
+ const toolResult = await executeTool(db, toolName, toolArgs, {
1980
+ onJobStatusChanged: notifyJobStatusChanged
1981
+ });
1982
+ const response = {
1983
+ content: [
1984
+ {
1985
+ type: "text",
1986
+ text: JSON.stringify(toolResult, null, 2)
1987
+ }
1988
+ ]
1989
+ };
1990
+ return isNotification ? null : JSON.stringify({ jsonrpc: "2.0", id: msg.id, result: response });
1991
+ } catch (err) {
1992
+ const errorMessage = err instanceof Error ? err.message : String(err);
1993
+ const response = {
1994
+ content: [
1995
+ {
1996
+ type: "text",
1997
+ text: errorMessage
1998
+ }
1999
+ ],
2000
+ isError: true
2001
+ };
2002
+ return isNotification ? null : JSON.stringify({ jsonrpc: "2.0", id: msg.id, result: response });
2003
+ }
2004
+ }
2005
+ default: {
2006
+ return isNotification ? null : JSON.stringify({
2007
+ jsonrpc: "2.0",
2008
+ id: msg.id,
2009
+ error: {
2010
+ code: -32601,
2011
+ message: `Method not found: ${msg.method}`
2012
+ }
2013
+ });
2014
+ }
2015
+ }
2016
+ }
2017
+ const server = {
2018
+ start() {
2019
+ if (isRunning) return;
2020
+ isRunning = true;
2021
+ rl = import_node_readline.default.createInterface({
2022
+ input,
2023
+ terminal: false
2024
+ });
2025
+ rl.on("line", (line) => {
2026
+ void handleMessage(line).then((response) => {
2027
+ if (response && isRunning) {
2028
+ output.write(response + "\n");
2029
+ }
2030
+ });
2031
+ });
2032
+ },
2033
+ stop() {
2034
+ if (!isRunning) return;
2035
+ isRunning = false;
2036
+ rl.close();
2037
+ rl = null;
2038
+ },
2039
+ handleMessage,
2040
+ on(event, listener) {
2041
+ emitter.on(event, listener);
2042
+ return server;
2043
+ },
2044
+ off(event, listener) {
2045
+ emitter.off(event, listener);
2046
+ return server;
2047
+ }
2048
+ };
2049
+ return server;
2050
+ }
1090
2051
  // Annotate the CommonJS export names for ESM import in node:
1091
2052
  0 && (module.exports = {
1092
2053
  DEFAULT_WORKER_TIMEOUT_MS,
2054
+ MCP_TOOL_DEFINITIONS,
2055
+ attachGracefulShutdown,
1093
2056
  createClient,
1094
2057
  createDashboard,
1095
2058
  createDashboardMiddleware,
1096
2059
  createDatabase,
2060
+ createMcpServer,
1097
2061
  createOrchestrator,
1098
2062
  createWorker,
1099
2063
  defaultBackoff,
2064
+ enableStatementCache,
2065
+ executeTool,
1100
2066
  getUnderlyingDb,
1101
2067
  validatePayload
1102
2068
  });