workmatic 1.1.2 → 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/cli.cjs +1078 -90
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +1082 -94
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +1053 -89
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +155 -5
- package/dist/index.d.ts +155 -5
- package/dist/index.js +1054 -95
- package/dist/index.js.map +1 -1
- package/package.json +16 -10
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
|
|
94
|
-
ON workmatic_jobs (queue, status
|
|
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.
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
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
|
-
|
|
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.
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
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);
|
|
@@ -331,13 +467,16 @@ function createWorker(options) {
|
|
|
331
467
|
if (!persistState) return;
|
|
332
468
|
const timestamp = now();
|
|
333
469
|
const key = getStateKey();
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
470
|
+
try {
|
|
471
|
+
await import_kysely3.sql`
|
|
472
|
+
INSERT INTO workmatic_settings (queue, paused, updated_at)
|
|
473
|
+
VALUES (${key}, ${state === "paused" ? 1 : state === "running" ? 2 : 0}, ${timestamp})
|
|
474
|
+
ON CONFLICT(queue) DO UPDATE SET
|
|
475
|
+
paused = ${state === "paused" ? 1 : state === "running" ? 2 : 0},
|
|
476
|
+
updated_at = ${timestamp}
|
|
477
|
+
`.execute(db);
|
|
478
|
+
} catch {
|
|
479
|
+
}
|
|
341
480
|
}
|
|
342
481
|
async function loadState() {
|
|
343
482
|
if (!persistState) return null;
|
|
@@ -364,52 +503,104 @@ function createWorker(options) {
|
|
|
364
503
|
async function claimBatch(limit) {
|
|
365
504
|
const timestamp = now();
|
|
366
505
|
const leaseUntil = timestamp + leaseMs;
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
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]
|
|
378
569
|
)
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
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
|
+
}
|
|
382
583
|
});
|
|
383
584
|
}
|
|
384
|
-
async function markDone(jobId) {
|
|
385
|
-
await db.updateTable("workmatic_jobs").set({
|
|
386
|
-
status: "done",
|
|
387
|
-
lease_until: 0,
|
|
388
|
-
updated_at: now()
|
|
389
|
-
}).where("id", "=", jobId).execute();
|
|
390
|
-
}
|
|
391
585
|
async function markFailed(jobId, attempts, maxAttempts, error) {
|
|
392
586
|
const timestamp = now();
|
|
393
587
|
const newAttempts = attempts + 1;
|
|
394
588
|
const errorMessage = error.message || String(error);
|
|
395
589
|
if (newAttempts < maxAttempts) {
|
|
396
590
|
const runAt = timestamp + backoff(newAttempts);
|
|
397
|
-
await db.
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
updated_at: timestamp
|
|
404
|
-
}).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
|
+
);
|
|
405
597
|
} else {
|
|
406
|
-
await db.
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
}).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
|
+
);
|
|
413
604
|
}
|
|
414
605
|
}
|
|
415
606
|
async function withTimeout(promise, ms, jobId) {
|
|
@@ -456,8 +647,13 @@ function createWorker(options) {
|
|
|
456
647
|
}
|
|
457
648
|
}
|
|
458
649
|
async function isQueuePausedInDb() {
|
|
459
|
-
const
|
|
460
|
-
|
|
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;
|
|
461
657
|
}
|
|
462
658
|
async function pump() {
|
|
463
659
|
if (!running) {
|
|
@@ -512,8 +708,7 @@ function createWorker(options) {
|
|
|
512
708
|
running = true;
|
|
513
709
|
paused = false;
|
|
514
710
|
fastqQueue = import_fastq.default.promise(processJob, concurrency);
|
|
515
|
-
saveState("running")
|
|
516
|
-
});
|
|
711
|
+
void saveState("running");
|
|
517
712
|
pump();
|
|
518
713
|
},
|
|
519
714
|
async stop() {
|
|
@@ -525,27 +720,28 @@ function createWorker(options) {
|
|
|
525
720
|
clearTimeout(pumpTimeout);
|
|
526
721
|
pumpTimeout = null;
|
|
527
722
|
}
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
}
|
|
723
|
+
await fastqQueue.drained();
|
|
724
|
+
fastqQueue = null;
|
|
725
|
+
await flushDoneBatch();
|
|
532
726
|
await saveState("stopped");
|
|
533
727
|
},
|
|
534
728
|
pause() {
|
|
535
729
|
paused = true;
|
|
536
|
-
|
|
537
|
-
|
|
730
|
+
void flushDoneBatch();
|
|
731
|
+
void saveState("paused");
|
|
538
732
|
},
|
|
539
733
|
resume() {
|
|
540
734
|
paused = false;
|
|
541
|
-
saveState("running")
|
|
542
|
-
});
|
|
735
|
+
void saveState("running");
|
|
543
736
|
},
|
|
544
737
|
async stats() {
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
import_kysely3.
|
|
548
|
-
|
|
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
|
+
);
|
|
549
745
|
const stats = {
|
|
550
746
|
ready: 0,
|
|
551
747
|
running: 0,
|
|
@@ -553,7 +749,7 @@ function createWorker(options) {
|
|
|
553
749
|
dead: 0,
|
|
554
750
|
total: 0
|
|
555
751
|
};
|
|
556
|
-
for (const row of result) {
|
|
752
|
+
for (const row of result.rows) {
|
|
557
753
|
const status = row.status;
|
|
558
754
|
const count = Number(row.count);
|
|
559
755
|
if (status in stats) {
|
|
@@ -589,6 +785,22 @@ function createWorker(options) {
|
|
|
589
785
|
}
|
|
590
786
|
const result = await query.execute();
|
|
591
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);
|
|
592
804
|
}
|
|
593
805
|
};
|
|
594
806
|
if (persistState && autoRestore) {
|
|
@@ -622,7 +834,15 @@ function createOrchestrator(options) {
|
|
|
622
834
|
function ensureEntry(queue) {
|
|
623
835
|
let entry = registry.get(queue);
|
|
624
836
|
if (!entry) {
|
|
625
|
-
entry = {
|
|
837
|
+
entry = {
|
|
838
|
+
client: createClient({
|
|
839
|
+
db,
|
|
840
|
+
queue,
|
|
841
|
+
onJobAdded: () => {
|
|
842
|
+
registry.get(queue)?.worker?.wakeUp();
|
|
843
|
+
}
|
|
844
|
+
})
|
|
845
|
+
};
|
|
626
846
|
registry.set(queue, entry);
|
|
627
847
|
}
|
|
628
848
|
return entry;
|
|
@@ -795,6 +1015,9 @@ function createOrchestrator(options) {
|
|
|
795
1015
|
return;
|
|
796
1016
|
}
|
|
797
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);
|
|
798
1021
|
}
|
|
799
1022
|
};
|
|
800
1023
|
return orchestrator;
|
|
@@ -1089,16 +1312,757 @@ function createDashboardMiddleware(options) {
|
|
|
1089
1312
|
void handleRequest(req, res, next);
|
|
1090
1313
|
};
|
|
1091
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
|
+
}
|
|
1092
2051
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1093
2052
|
0 && (module.exports = {
|
|
1094
2053
|
DEFAULT_WORKER_TIMEOUT_MS,
|
|
2054
|
+
MCP_TOOL_DEFINITIONS,
|
|
2055
|
+
attachGracefulShutdown,
|
|
1095
2056
|
createClient,
|
|
1096
2057
|
createDashboard,
|
|
1097
2058
|
createDashboardMiddleware,
|
|
1098
2059
|
createDatabase,
|
|
2060
|
+
createMcpServer,
|
|
1099
2061
|
createOrchestrator,
|
|
1100
2062
|
createWorker,
|
|
1101
2063
|
defaultBackoff,
|
|
2064
|
+
enableStatementCache,
|
|
2065
|
+
executeTool,
|
|
1102
2066
|
getUnderlyingDb,
|
|
1103
2067
|
validatePayload
|
|
1104
2068
|
});
|