@probelabs/visor 0.1.147 → 0.1.148-ee

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.
Files changed (93) hide show
  1. package/dist/frontends/github-frontend.d.ts +2 -1
  2. package/dist/frontends/github-frontend.d.ts.map +1 -1
  3. package/dist/index.js +2444 -134
  4. package/dist/providers/ai-check-provider.d.ts.map +1 -1
  5. package/dist/scheduler/schedule-tool.d.ts.map +1 -1
  6. package/dist/scheduler/scheduler.d.ts +5 -0
  7. package/dist/scheduler/scheduler.d.ts.map +1 -1
  8. package/dist/sdk/{check-provider-registry-CTZA3EVE.mjs → check-provider-registry-AMYY2ZJY.mjs} +5 -6
  9. package/dist/sdk/{check-provider-registry-SCPM6DIT.mjs → check-provider-registry-DVQDGTOE.mjs} +5 -6
  10. package/dist/sdk/{chunk-4F5UVWAN.mjs → chunk-62TNF5PJ.mjs} +2 -2
  11. package/dist/sdk/{chunk-4F5UVWAN.mjs.map → chunk-62TNF5PJ.mjs.map} +1 -1
  12. package/dist/sdk/{chunk-H23T7J6Y.mjs → chunk-75Q63UNX.mjs} +2743 -277
  13. package/dist/sdk/chunk-75Q63UNX.mjs.map +1 -0
  14. package/dist/sdk/{chunk-JKWLGLDR.mjs → chunk-CISJ6DJW.mjs} +3 -3
  15. package/dist/sdk/{chunk-EWGX7LI7.mjs → chunk-H4AYMOAT.mjs} +2742 -276
  16. package/dist/sdk/chunk-H4AYMOAT.mjs.map +1 -0
  17. package/dist/sdk/{chunk-2NFKN6CY.mjs → chunk-RJLJUTSU.mjs} +2 -2
  18. package/dist/sdk/{failure-condition-evaluator-FHNZL2US.mjs → failure-condition-evaluator-IVCTD4BZ.mjs} +3 -3
  19. package/dist/sdk/{github-frontend-V3WUHL6E.mjs → github-frontend-DFT5G32K.mjs} +16 -4
  20. package/dist/sdk/github-frontend-DFT5G32K.mjs.map +1 -0
  21. package/dist/sdk/{host-GVR4UGZ3.mjs → host-H7IX4GBK.mjs} +2 -2
  22. package/dist/sdk/{host-UQUQIYFG.mjs → host-NZXGBBJI.mjs} +2 -2
  23. package/dist/sdk/knex-store-HPXJILBL.mjs +411 -0
  24. package/dist/sdk/knex-store-HPXJILBL.mjs.map +1 -0
  25. package/dist/sdk/loader-YSRMVXC3.mjs +89 -0
  26. package/dist/sdk/loader-YSRMVXC3.mjs.map +1 -0
  27. package/dist/sdk/opa-policy-engine-S2S2ULEI.mjs +655 -0
  28. package/dist/sdk/opa-policy-engine-S2S2ULEI.mjs.map +1 -0
  29. package/dist/sdk/{routing-CZ36LVVS.mjs → routing-LU5PAREW.mjs} +4 -4
  30. package/dist/sdk/{check-provider-registry-CDL5AJSI.mjs → schedule-tool-4JMWZCCK.mjs} +15 -10
  31. package/dist/sdk/{workflow-check-provider-3K7732MW.mjs → schedule-tool-CONR4VW3.mjs} +15 -10
  32. package/dist/sdk/{schedule-tool-handler-KFYNV7HL.mjs → schedule-tool-handler-AXMR7NBI.mjs} +5 -6
  33. package/dist/sdk/{schedule-tool-handler-QUMAF2DJ.mjs → schedule-tool-handler-YUC6CAXX.mjs} +5 -6
  34. package/dist/sdk/sdk.js +2831 -371
  35. package/dist/sdk/sdk.js.map +1 -1
  36. package/dist/sdk/sdk.mjs +4 -5
  37. package/dist/sdk/sdk.mjs.map +1 -1
  38. package/dist/sdk/{trace-helpers-W7TF5ZKF.mjs → trace-helpers-6ROJR7N3.mjs} +2 -2
  39. package/dist/sdk/validator-XTZJZZJH.mjs +134 -0
  40. package/dist/sdk/validator-XTZJZZJH.mjs.map +1 -0
  41. package/dist/sdk/{workflow-check-provider-5453TW65.mjs → workflow-check-provider-DYSO3PML.mjs} +5 -6
  42. package/dist/sdk/{workflow-check-provider-HMABCGB5.mjs → workflow-check-provider-MMB7L3YG.mjs} +5 -6
  43. package/dist/state-machine/context/build-engine-context.d.ts.map +1 -1
  44. package/dist/utils/tool-resolver.d.ts.map +1 -1
  45. package/dist/utils/workspace-manager.d.ts +31 -8
  46. package/dist/utils/workspace-manager.d.ts.map +1 -1
  47. package/dist/utils/worktree-manager.d.ts +6 -0
  48. package/dist/utils/worktree-manager.d.ts.map +1 -1
  49. package/package.json +2 -2
  50. package/dist/output/traces/run-2026-02-27T11-27-22-261Z.ndjson +0 -138
  51. package/dist/output/traces/run-2026-02-27T11-28-08-546Z.ndjson +0 -1442
  52. package/dist/sdk/chunk-EWGX7LI7.mjs.map +0 -1
  53. package/dist/sdk/chunk-FBJ7MC7R.mjs +0 -1502
  54. package/dist/sdk/chunk-H23T7J6Y.mjs.map +0 -1
  55. package/dist/sdk/chunk-JKWLGLDR.mjs.map +0 -1
  56. package/dist/sdk/chunk-R77LN3OE.mjs +0 -40693
  57. package/dist/sdk/chunk-R77LN3OE.mjs.map +0 -1
  58. package/dist/sdk/chunk-V2QW6ECX.mjs +0 -739
  59. package/dist/sdk/chunk-V2QW6ECX.mjs.map +0 -1
  60. package/dist/sdk/chunk-XKCER23W.mjs +0 -1490
  61. package/dist/sdk/chunk-XKCER23W.mjs.map +0 -1
  62. package/dist/sdk/chunk-YQZW3D2V.mjs +0 -443
  63. package/dist/sdk/chunk-YQZW3D2V.mjs.map +0 -1
  64. package/dist/sdk/failure-condition-evaluator-2B5WY7QN.mjs +0 -17
  65. package/dist/sdk/github-frontend-47EU2HBY.mjs +0 -1356
  66. package/dist/sdk/github-frontend-47EU2HBY.mjs.map +0 -1
  67. package/dist/sdk/github-frontend-V3WUHL6E.mjs.map +0 -1
  68. package/dist/sdk/routing-THIWDEYY.mjs +0 -25
  69. package/dist/sdk/schedule-tool-2COUUTF7.mjs +0 -18
  70. package/dist/sdk/schedule-tool-handler-GEH62OUM.mjs +0 -40
  71. package/dist/sdk/trace-helpers-EHDZ42HH.mjs +0 -25
  72. package/dist/sdk/trace-helpers-EHDZ42HH.mjs.map +0 -1
  73. package/dist/sdk/trace-helpers-W7TF5ZKF.mjs.map +0 -1
  74. package/dist/sdk/workflow-check-provider-3K7732MW.mjs.map +0 -1
  75. package/dist/sdk/workflow-check-provider-5453TW65.mjs.map +0 -1
  76. package/dist/sdk/workflow-check-provider-HMABCGB5.mjs.map +0 -1
  77. package/dist/traces/run-2026-02-27T11-27-22-261Z.ndjson +0 -138
  78. package/dist/traces/run-2026-02-27T11-28-08-546Z.ndjson +0 -1442
  79. /package/dist/sdk/{check-provider-registry-CDL5AJSI.mjs.map → check-provider-registry-AMYY2ZJY.mjs.map} +0 -0
  80. /package/dist/sdk/{check-provider-registry-CTZA3EVE.mjs.map → check-provider-registry-DVQDGTOE.mjs.map} +0 -0
  81. /package/dist/sdk/{chunk-FBJ7MC7R.mjs.map → chunk-CISJ6DJW.mjs.map} +0 -0
  82. /package/dist/sdk/{chunk-2NFKN6CY.mjs.map → chunk-RJLJUTSU.mjs.map} +0 -0
  83. /package/dist/sdk/{check-provider-registry-SCPM6DIT.mjs.map → failure-condition-evaluator-IVCTD4BZ.mjs.map} +0 -0
  84. /package/dist/sdk/{host-GVR4UGZ3.mjs.map → host-H7IX4GBK.mjs.map} +0 -0
  85. /package/dist/sdk/{host-UQUQIYFG.mjs.map → host-NZXGBBJI.mjs.map} +0 -0
  86. /package/dist/sdk/{failure-condition-evaluator-2B5WY7QN.mjs.map → routing-LU5PAREW.mjs.map} +0 -0
  87. /package/dist/sdk/{failure-condition-evaluator-FHNZL2US.mjs.map → schedule-tool-4JMWZCCK.mjs.map} +0 -0
  88. /package/dist/sdk/{routing-CZ36LVVS.mjs.map → schedule-tool-CONR4VW3.mjs.map} +0 -0
  89. /package/dist/sdk/{routing-THIWDEYY.mjs.map → schedule-tool-handler-AXMR7NBI.mjs.map} +0 -0
  90. /package/dist/sdk/{schedule-tool-2COUUTF7.mjs.map → schedule-tool-handler-YUC6CAXX.mjs.map} +0 -0
  91. /package/dist/sdk/{schedule-tool-handler-GEH62OUM.mjs.map → trace-helpers-6ROJR7N3.mjs.map} +0 -0
  92. /package/dist/sdk/{schedule-tool-handler-KFYNV7HL.mjs.map → workflow-check-provider-DYSO3PML.mjs.map} +0 -0
  93. /package/dist/sdk/{schedule-tool-handler-QUMAF2DJ.mjs.map → workflow-check-provider-MMB7L3YG.mjs.map} +0 -0
@@ -1,1490 +0,0 @@
1
- import {
2
- init_logger,
3
- logger
4
- } from "./chunk-SZXICFQ3.mjs";
5
- import {
6
- __esm,
7
- __require
8
- } from "./chunk-J7LXIPZS.mjs";
9
-
10
- // src/scheduler/store/sqlite-store.ts
11
- import path from "path";
12
- import fs from "fs";
13
- import { v4 as uuidv4 } from "uuid";
14
- function toDbRow(schedule) {
15
- return {
16
- id: schedule.id,
17
- creator_id: schedule.creatorId,
18
- creator_context: schedule.creatorContext ?? null,
19
- creator_name: schedule.creatorName ?? null,
20
- timezone: schedule.timezone,
21
- schedule_expr: schedule.schedule,
22
- run_at: schedule.runAt ?? null,
23
- is_recurring: schedule.isRecurring ? 1 : 0,
24
- original_expression: schedule.originalExpression,
25
- workflow: schedule.workflow ?? null,
26
- workflow_inputs: schedule.workflowInputs ? JSON.stringify(schedule.workflowInputs) : null,
27
- output_context: schedule.outputContext ? JSON.stringify(schedule.outputContext) : null,
28
- status: schedule.status,
29
- created_at: schedule.createdAt,
30
- last_run_at: schedule.lastRunAt ?? null,
31
- next_run_at: schedule.nextRunAt ?? null,
32
- run_count: schedule.runCount,
33
- failure_count: schedule.failureCount,
34
- last_error: schedule.lastError ?? null,
35
- previous_response: schedule.previousResponse ?? null
36
- };
37
- }
38
- function safeJsonParse(value) {
39
- if (!value) return void 0;
40
- try {
41
- return JSON.parse(value);
42
- } catch {
43
- return void 0;
44
- }
45
- }
46
- function fromDbRow(row) {
47
- return {
48
- id: row.id,
49
- creatorId: row.creator_id,
50
- creatorContext: row.creator_context ?? void 0,
51
- creatorName: row.creator_name ?? void 0,
52
- timezone: row.timezone,
53
- schedule: row.schedule_expr,
54
- runAt: row.run_at ?? void 0,
55
- isRecurring: row.is_recurring === 1,
56
- originalExpression: row.original_expression,
57
- workflow: row.workflow ?? void 0,
58
- workflowInputs: safeJsonParse(row.workflow_inputs),
59
- outputContext: safeJsonParse(row.output_context),
60
- status: row.status,
61
- createdAt: row.created_at,
62
- lastRunAt: row.last_run_at ?? void 0,
63
- nextRunAt: row.next_run_at ?? void 0,
64
- runCount: row.run_count,
65
- failureCount: row.failure_count,
66
- lastError: row.last_error ?? void 0,
67
- previousResponse: row.previous_response ?? void 0
68
- };
69
- }
70
- var SqliteStoreBackend;
71
- var init_sqlite_store = __esm({
72
- "src/scheduler/store/sqlite-store.ts"() {
73
- "use strict";
74
- init_logger();
75
- SqliteStoreBackend = class {
76
- db = null;
77
- dbPath;
78
- // In-memory locks (single-node only; SQLite doesn't support distributed locking)
79
- locks = /* @__PURE__ */ new Map();
80
- constructor(filename) {
81
- this.dbPath = filename || ".visor/schedules.db";
82
- }
83
- async initialize() {
84
- const resolvedPath = path.resolve(process.cwd(), this.dbPath);
85
- const dir = path.dirname(resolvedPath);
86
- fs.mkdirSync(dir, { recursive: true });
87
- const { createRequire } = __require("module");
88
- const runtimeRequire = createRequire(__filename);
89
- let Database;
90
- try {
91
- Database = runtimeRequire("better-sqlite3");
92
- } catch (err) {
93
- const code = err?.code;
94
- if (code === "MODULE_NOT_FOUND" || code === "ERR_MODULE_NOT_FOUND") {
95
- throw new Error(
96
- "better-sqlite3 is required for SQLite schedule storage. Install it with: npm install better-sqlite3"
97
- );
98
- }
99
- throw err;
100
- }
101
- this.db = new Database(resolvedPath);
102
- this.db.pragma("journal_mode = WAL");
103
- this.migrateSchema();
104
- logger.info(`[SqliteStore] Initialized at ${this.dbPath}`);
105
- }
106
- async shutdown() {
107
- if (this.db) {
108
- this.db.close();
109
- this.db = null;
110
- }
111
- this.locks.clear();
112
- }
113
- // --- Schema Migration ---
114
- migrateSchema() {
115
- const db = this.getDb();
116
- db.exec(`
117
- CREATE TABLE IF NOT EXISTS schedules (
118
- id VARCHAR(36) PRIMARY KEY,
119
- creator_id VARCHAR(255) NOT NULL,
120
- creator_context VARCHAR(255),
121
- creator_name VARCHAR(255),
122
- timezone VARCHAR(64) NOT NULL DEFAULT 'UTC',
123
- schedule_expr VARCHAR(255),
124
- run_at BIGINT,
125
- is_recurring BOOLEAN NOT NULL,
126
- original_expression TEXT,
127
- workflow VARCHAR(255),
128
- workflow_inputs TEXT,
129
- output_context TEXT,
130
- status VARCHAR(20) NOT NULL,
131
- created_at BIGINT NOT NULL,
132
- last_run_at BIGINT,
133
- next_run_at BIGINT,
134
- run_count INTEGER NOT NULL DEFAULT 0,
135
- failure_count INTEGER NOT NULL DEFAULT 0,
136
- last_error TEXT,
137
- previous_response TEXT,
138
- claimed_by VARCHAR(255),
139
- claimed_at BIGINT,
140
- lock_token VARCHAR(36)
141
- );
142
-
143
- CREATE INDEX IF NOT EXISTS idx_schedules_creator_id
144
- ON schedules(creator_id);
145
-
146
- CREATE INDEX IF NOT EXISTS idx_schedules_status
147
- ON schedules(status);
148
-
149
- CREATE INDEX IF NOT EXISTS idx_schedules_status_next_run
150
- ON schedules(status, next_run_at);
151
-
152
- CREATE TABLE IF NOT EXISTS scheduler_locks (
153
- lock_id VARCHAR(255) PRIMARY KEY,
154
- node_id VARCHAR(255) NOT NULL,
155
- lock_token VARCHAR(36) NOT NULL,
156
- acquired_at BIGINT NOT NULL,
157
- expires_at BIGINT NOT NULL
158
- );
159
- `);
160
- }
161
- // --- Helpers ---
162
- getDb() {
163
- if (!this.db) {
164
- throw new Error("[SqliteStore] Database not initialized. Call initialize() first.");
165
- }
166
- return this.db;
167
- }
168
- // --- CRUD ---
169
- async create(schedule) {
170
- const db = this.getDb();
171
- const newSchedule = {
172
- ...schedule,
173
- id: uuidv4(),
174
- createdAt: Date.now(),
175
- runCount: 0,
176
- failureCount: 0,
177
- status: "active"
178
- };
179
- const row = toDbRow(newSchedule);
180
- db.prepare(
181
- `
182
- INSERT INTO schedules (
183
- id, creator_id, creator_context, creator_name, timezone,
184
- schedule_expr, run_at, is_recurring, original_expression,
185
- workflow, workflow_inputs, output_context,
186
- status, created_at, last_run_at, next_run_at,
187
- run_count, failure_count, last_error, previous_response
188
- ) VALUES (
189
- ?, ?, ?, ?, ?,
190
- ?, ?, ?, ?,
191
- ?, ?, ?,
192
- ?, ?, ?, ?,
193
- ?, ?, ?, ?
194
- )
195
- `
196
- ).run(
197
- row.id,
198
- row.creator_id,
199
- row.creator_context,
200
- row.creator_name,
201
- row.timezone,
202
- row.schedule_expr,
203
- row.run_at,
204
- row.is_recurring,
205
- row.original_expression,
206
- row.workflow,
207
- row.workflow_inputs,
208
- row.output_context,
209
- row.status,
210
- row.created_at,
211
- row.last_run_at,
212
- row.next_run_at,
213
- row.run_count,
214
- row.failure_count,
215
- row.last_error,
216
- row.previous_response
217
- );
218
- logger.info(
219
- `[SqliteStore] Created schedule ${newSchedule.id} for user ${newSchedule.creatorId}`
220
- );
221
- return newSchedule;
222
- }
223
- async importSchedule(schedule) {
224
- const db = this.getDb();
225
- const row = toDbRow(schedule);
226
- db.prepare(
227
- `
228
- INSERT OR IGNORE INTO schedules (
229
- id, creator_id, creator_context, creator_name, timezone,
230
- schedule_expr, run_at, is_recurring, original_expression,
231
- workflow, workflow_inputs, output_context,
232
- status, created_at, last_run_at, next_run_at,
233
- run_count, failure_count, last_error, previous_response
234
- ) VALUES (
235
- ?, ?, ?, ?, ?,
236
- ?, ?, ?, ?,
237
- ?, ?, ?,
238
- ?, ?, ?, ?,
239
- ?, ?, ?, ?
240
- )
241
- `
242
- ).run(
243
- row.id,
244
- row.creator_id,
245
- row.creator_context,
246
- row.creator_name,
247
- row.timezone,
248
- row.schedule_expr,
249
- row.run_at,
250
- row.is_recurring,
251
- row.original_expression,
252
- row.workflow,
253
- row.workflow_inputs,
254
- row.output_context,
255
- row.status,
256
- row.created_at,
257
- row.last_run_at,
258
- row.next_run_at,
259
- row.run_count,
260
- row.failure_count,
261
- row.last_error,
262
- row.previous_response
263
- );
264
- }
265
- async get(id) {
266
- const db = this.getDb();
267
- const row = db.prepare("SELECT * FROM schedules WHERE id = ?").get(id);
268
- return row ? fromDbRow(row) : void 0;
269
- }
270
- async update(id, patch) {
271
- const db = this.getDb();
272
- const existing = db.prepare("SELECT * FROM schedules WHERE id = ?").get(id);
273
- if (!existing) return void 0;
274
- const current = fromDbRow(existing);
275
- const updated = { ...current, ...patch, id: current.id };
276
- const row = toDbRow(updated);
277
- db.prepare(
278
- `
279
- UPDATE schedules SET
280
- creator_id = ?, creator_context = ?, creator_name = ?, timezone = ?,
281
- schedule_expr = ?, run_at = ?, is_recurring = ?, original_expression = ?,
282
- workflow = ?, workflow_inputs = ?, output_context = ?,
283
- status = ?, last_run_at = ?, next_run_at = ?,
284
- run_count = ?, failure_count = ?, last_error = ?, previous_response = ?
285
- WHERE id = ?
286
- `
287
- ).run(
288
- row.creator_id,
289
- row.creator_context,
290
- row.creator_name,
291
- row.timezone,
292
- row.schedule_expr,
293
- row.run_at,
294
- row.is_recurring,
295
- row.original_expression,
296
- row.workflow,
297
- row.workflow_inputs,
298
- row.output_context,
299
- row.status,
300
- row.last_run_at,
301
- row.next_run_at,
302
- row.run_count,
303
- row.failure_count,
304
- row.last_error,
305
- row.previous_response,
306
- row.id
307
- );
308
- return updated;
309
- }
310
- async delete(id) {
311
- const db = this.getDb();
312
- const result = db.prepare("DELETE FROM schedules WHERE id = ?").run(id);
313
- if (result.changes > 0) {
314
- logger.info(`[SqliteStore] Deleted schedule ${id}`);
315
- return true;
316
- }
317
- return false;
318
- }
319
- // --- Queries ---
320
- async getByCreator(creatorId) {
321
- const db = this.getDb();
322
- const rows = db.prepare("SELECT * FROM schedules WHERE creator_id = ?").all(creatorId);
323
- return rows.map(fromDbRow);
324
- }
325
- async getActiveSchedules() {
326
- const db = this.getDb();
327
- const rows = db.prepare("SELECT * FROM schedules WHERE status = 'active'").all();
328
- return rows.map(fromDbRow);
329
- }
330
- async getDueSchedules(now) {
331
- const ts = now ?? Date.now();
332
- const db = this.getDb();
333
- const rows = db.prepare(
334
- `SELECT * FROM schedules
335
- WHERE status = 'active'
336
- AND (
337
- (is_recurring = 0 AND run_at IS NOT NULL AND run_at <= ?)
338
- OR
339
- (is_recurring = 1 AND next_run_at IS NOT NULL AND next_run_at <= ?)
340
- )`
341
- ).all(ts, ts);
342
- return rows.map(fromDbRow);
343
- }
344
- async findByWorkflow(creatorId, workflowName) {
345
- const db = this.getDb();
346
- const escaped = workflowName.toLowerCase().replace(/[%_\\]/g, "\\$&");
347
- const pattern = `%${escaped}%`;
348
- const rows = db.prepare(
349
- `SELECT * FROM schedules
350
- WHERE creator_id = ? AND status = 'active'
351
- AND LOWER(workflow) LIKE ? ESCAPE '\\'`
352
- ).all(creatorId, pattern);
353
- return rows.map(fromDbRow);
354
- }
355
- async getAll() {
356
- const db = this.getDb();
357
- const rows = db.prepare("SELECT * FROM schedules").all();
358
- return rows.map(fromDbRow);
359
- }
360
- async getStats() {
361
- const db = this.getDb();
362
- const row = db.prepare(
363
- `SELECT
364
- COUNT(*) as total,
365
- SUM(CASE WHEN status = 'active' THEN 1 ELSE 0 END) as active,
366
- SUM(CASE WHEN status = 'paused' THEN 1 ELSE 0 END) as paused,
367
- SUM(CASE WHEN status = 'completed' THEN 1 ELSE 0 END) as completed,
368
- SUM(CASE WHEN status = 'failed' THEN 1 ELSE 0 END) as failed,
369
- SUM(CASE WHEN is_recurring = 1 THEN 1 ELSE 0 END) as recurring,
370
- SUM(CASE WHEN is_recurring = 0 THEN 1 ELSE 0 END) as one_time
371
- FROM schedules`
372
- ).get();
373
- return {
374
- total: row.total,
375
- active: row.active,
376
- paused: row.paused,
377
- completed: row.completed,
378
- failed: row.failed,
379
- recurring: row.recurring,
380
- oneTime: row.one_time
381
- };
382
- }
383
- async validateLimits(creatorId, isRecurring, limits) {
384
- const db = this.getDb();
385
- if (limits.maxGlobal) {
386
- const row = db.prepare("SELECT COUNT(*) as cnt FROM schedules").get();
387
- if (row.cnt >= limits.maxGlobal) {
388
- throw new Error(`Global schedule limit reached (${limits.maxGlobal})`);
389
- }
390
- }
391
- if (limits.maxPerUser) {
392
- const row = db.prepare("SELECT COUNT(*) as cnt FROM schedules WHERE creator_id = ?").get(creatorId);
393
- if (row.cnt >= limits.maxPerUser) {
394
- throw new Error(`You have reached the maximum number of schedules (${limits.maxPerUser})`);
395
- }
396
- }
397
- if (isRecurring && limits.maxRecurringPerUser) {
398
- const row = db.prepare("SELECT COUNT(*) as cnt FROM schedules WHERE creator_id = ? AND is_recurring = 1").get(creatorId);
399
- if (row.cnt >= limits.maxRecurringPerUser) {
400
- throw new Error(
401
- `You have reached the maximum number of recurring schedules (${limits.maxRecurringPerUser})`
402
- );
403
- }
404
- }
405
- }
406
- // --- HA Locking (in-memory for SQLite — single-node only) ---
407
- async tryAcquireLock(scheduleId, nodeId, ttlSeconds) {
408
- const now = Date.now();
409
- const existing = this.locks.get(scheduleId);
410
- if (existing && existing.expiresAt > now) {
411
- if (existing.nodeId === nodeId) {
412
- return existing.token;
413
- }
414
- return null;
415
- }
416
- const token = uuidv4();
417
- this.locks.set(scheduleId, {
418
- nodeId,
419
- token,
420
- expiresAt: now + ttlSeconds * 1e3
421
- });
422
- return token;
423
- }
424
- async releaseLock(scheduleId, lockToken) {
425
- const existing = this.locks.get(scheduleId);
426
- if (existing && existing.token === lockToken) {
427
- this.locks.delete(scheduleId);
428
- }
429
- }
430
- async renewLock(scheduleId, lockToken, ttlSeconds) {
431
- const existing = this.locks.get(scheduleId);
432
- if (!existing || existing.token !== lockToken) {
433
- return false;
434
- }
435
- existing.expiresAt = Date.now() + ttlSeconds * 1e3;
436
- return true;
437
- }
438
- async flush() {
439
- }
440
- };
441
- }
442
- });
443
-
444
- // src/scheduler/store/index.ts
445
- async function createStoreBackend(storageConfig, haConfig) {
446
- const driver = storageConfig?.driver || "sqlite";
447
- switch (driver) {
448
- case "sqlite": {
449
- const conn = storageConfig?.connection;
450
- return new SqliteStoreBackend(conn?.filename);
451
- }
452
- case "postgresql":
453
- case "mysql":
454
- case "mssql": {
455
- try {
456
- const loaderPath = "../../enterprise/loader";
457
- const { loadEnterpriseStoreBackend } = await import(loaderPath);
458
- return await loadEnterpriseStoreBackend(driver, storageConfig, haConfig);
459
- } catch (err) {
460
- const msg = err instanceof Error ? err.message : String(err);
461
- logger.error(`[StoreFactory] Failed to load enterprise ${driver} backend: ${msg}`);
462
- throw new Error(
463
- `The ${driver} schedule storage driver requires a Visor Enterprise license. Install the enterprise package or use driver: 'sqlite' (default). Original error: ${msg}`
464
- );
465
- }
466
- }
467
- default:
468
- throw new Error(`Unknown schedule storage driver: ${driver}`);
469
- }
470
- }
471
- var init_store = __esm({
472
- "src/scheduler/store/index.ts"() {
473
- "use strict";
474
- init_logger();
475
- init_sqlite_store();
476
- }
477
- });
478
-
479
- // src/scheduler/store/json-migrator.ts
480
- import fs2 from "fs/promises";
481
- import path2 from "path";
482
- async function migrateJsonToBackend(jsonPath, backend) {
483
- const resolvedPath = path2.resolve(process.cwd(), jsonPath);
484
- let content;
485
- try {
486
- content = await fs2.readFile(resolvedPath, "utf-8");
487
- } catch (err) {
488
- if (err.code === "ENOENT") {
489
- return 0;
490
- }
491
- throw err;
492
- }
493
- let data;
494
- try {
495
- data = JSON.parse(content);
496
- } catch {
497
- logger.warn(`[JsonMigrator] Failed to parse ${jsonPath}, skipping migration`);
498
- return 0;
499
- }
500
- const schedules = data.schedules;
501
- if (!Array.isArray(schedules) || schedules.length === 0) {
502
- logger.debug("[JsonMigrator] No schedules to migrate");
503
- await renameToMigrated(resolvedPath);
504
- return 0;
505
- }
506
- let migrated = 0;
507
- for (const schedule of schedules) {
508
- if (!schedule.id) {
509
- logger.warn("[JsonMigrator] Skipping schedule without ID");
510
- continue;
511
- }
512
- const existing = await backend.get(schedule.id);
513
- if (existing) {
514
- logger.debug(`[JsonMigrator] Schedule ${schedule.id} already exists, skipping`);
515
- continue;
516
- }
517
- try {
518
- await backend.importSchedule(schedule);
519
- migrated++;
520
- } catch (err) {
521
- logger.warn(
522
- `[JsonMigrator] Failed to migrate schedule ${schedule.id}: ${err instanceof Error ? err.message : err}`
523
- );
524
- }
525
- }
526
- await renameToMigrated(resolvedPath);
527
- logger.info(`[JsonMigrator] Migrated ${migrated}/${schedules.length} schedules from ${jsonPath}`);
528
- return migrated;
529
- }
530
- async function renameToMigrated(resolvedPath) {
531
- const migratedPath = `${resolvedPath}.migrated`;
532
- try {
533
- await fs2.rename(resolvedPath, migratedPath);
534
- logger.info(`[JsonMigrator] Backed up ${resolvedPath} \u2192 ${migratedPath}`);
535
- } catch (err) {
536
- logger.warn(
537
- `[JsonMigrator] Failed to rename ${resolvedPath}: ${err instanceof Error ? err.message : err}`
538
- );
539
- }
540
- }
541
- var init_json_migrator = __esm({
542
- "src/scheduler/store/json-migrator.ts"() {
543
- "use strict";
544
- init_logger();
545
- }
546
- });
547
-
548
- // src/scheduler/schedule-store.ts
549
- var ScheduleStore;
550
- var init_schedule_store = __esm({
551
- "src/scheduler/schedule-store.ts"() {
552
- "use strict";
553
- init_logger();
554
- init_store();
555
- init_json_migrator();
556
- ScheduleStore = class _ScheduleStore {
557
- static instance;
558
- backend = null;
559
- initialized = false;
560
- limits;
561
- config;
562
- externalBackend = null;
563
- constructor(config, limits, backend) {
564
- this.config = config || {};
565
- this.limits = {
566
- maxPerUser: limits?.maxPerUser ?? 25,
567
- maxRecurringPerUser: limits?.maxRecurringPerUser ?? 10,
568
- maxGlobal: limits?.maxGlobal ?? 1e3
569
- };
570
- if (backend) {
571
- this.externalBackend = backend;
572
- }
573
- }
574
- /**
575
- * Get singleton instance
576
- *
577
- * Note: Config and limits are only applied on first call. Subsequent calls
578
- * with different parameters will log a warning and return the existing instance.
579
- * Use createIsolated() for testing with different configurations.
580
- */
581
- static getInstance(config, limits) {
582
- if (!_ScheduleStore.instance) {
583
- _ScheduleStore.instance = new _ScheduleStore(config, limits);
584
- } else if (config || limits) {
585
- logger.warn(
586
- "[ScheduleStore] getInstance() called with config/limits but instance already exists. Parameters ignored. Use createIsolated() for testing or resetInstance() first."
587
- );
588
- }
589
- return _ScheduleStore.instance;
590
- }
591
- /**
592
- * Create a new isolated instance (for testing)
593
- */
594
- static createIsolated(config, limits, backend) {
595
- return new _ScheduleStore(config, limits, backend);
596
- }
597
- /**
598
- * Reset singleton instance (for testing)
599
- */
600
- static resetInstance() {
601
- if (_ScheduleStore.instance) {
602
- if (_ScheduleStore.instance.backend) {
603
- _ScheduleStore.instance.backend.shutdown().catch(() => {
604
- });
605
- }
606
- }
607
- _ScheduleStore.instance = void 0;
608
- }
609
- /**
610
- * Initialize the store - creates backend and runs migrations
611
- */
612
- async initialize() {
613
- if (this.initialized) {
614
- return;
615
- }
616
- if (this.externalBackend) {
617
- this.backend = this.externalBackend;
618
- } else {
619
- this.backend = await createStoreBackend(this.config.storage, this.config.ha);
620
- }
621
- await this.backend.initialize();
622
- const jsonPath = this.config.path || ".visor/schedules.json";
623
- try {
624
- await migrateJsonToBackend(jsonPath, this.backend);
625
- } catch (err) {
626
- logger.warn(
627
- `[ScheduleStore] JSON migration failed (non-fatal): ${err instanceof Error ? err.message : err}`
628
- );
629
- }
630
- this.initialized = true;
631
- }
632
- /**
633
- * Create a new schedule (async, persists immediately)
634
- */
635
- async createAsync(schedule) {
636
- const backend = this.getBackend();
637
- await backend.validateLimits(schedule.creatorId, schedule.isRecurring, this.limits);
638
- return backend.create(schedule);
639
- }
640
- /**
641
- * Get a schedule by ID
642
- */
643
- async getAsync(id) {
644
- return this.getBackend().get(id);
645
- }
646
- /**
647
- * Update a schedule
648
- */
649
- async updateAsync(id, patch) {
650
- return this.getBackend().update(id, patch);
651
- }
652
- /**
653
- * Delete a schedule
654
- */
655
- async deleteAsync(id) {
656
- return this.getBackend().delete(id);
657
- }
658
- /**
659
- * Get all schedules for a specific creator
660
- */
661
- async getByCreatorAsync(creatorId) {
662
- return this.getBackend().getByCreator(creatorId);
663
- }
664
- /**
665
- * Get all active schedules
666
- */
667
- async getActiveSchedulesAsync() {
668
- return this.getBackend().getActiveSchedules();
669
- }
670
- /**
671
- * Get all schedules due for execution
672
- * @param now Current timestamp in milliseconds
673
- */
674
- async getDueSchedulesAsync(now = Date.now()) {
675
- return this.getBackend().getDueSchedules(now);
676
- }
677
- /**
678
- * Find schedules by workflow name
679
- */
680
- async findByWorkflowAsync(creatorId, workflowName) {
681
- return this.getBackend().findByWorkflow(creatorId, workflowName);
682
- }
683
- /**
684
- * Get schedule count statistics
685
- */
686
- async getStatsAsync() {
687
- return this.getBackend().getStats();
688
- }
689
- /**
690
- * Force immediate save (useful for shutdown)
691
- */
692
- async flush() {
693
- if (this.backend) {
694
- await this.backend.flush();
695
- }
696
- }
697
- /**
698
- * Check if initialized
699
- */
700
- isInitialized() {
701
- return this.initialized;
702
- }
703
- /**
704
- * Check if there are unsaved changes
705
- */
706
- hasPendingChanges() {
707
- return false;
708
- }
709
- /**
710
- * Get all schedules
711
- */
712
- async getAllAsync() {
713
- return this.getBackend().getAll();
714
- }
715
- /**
716
- * Get the underlying backend (for HA lock operations)
717
- */
718
- getBackend() {
719
- if (!this.backend) {
720
- throw new Error("[ScheduleStore] Not initialized. Call initialize() first.");
721
- }
722
- return this.backend;
723
- }
724
- /**
725
- * Shut down the backend cleanly
726
- */
727
- async shutdown() {
728
- if (this.backend) {
729
- await this.backend.shutdown();
730
- this.backend = null;
731
- }
732
- this.initialized = false;
733
- }
734
- };
735
- }
736
- });
737
-
738
- // src/scheduler/schedule-parser.ts
739
- function getNextRunTime(cronExpression, _timezone = "UTC") {
740
- const parts = cronExpression.split(" ");
741
- if (parts.length !== 5) {
742
- throw new Error(`Invalid cron expression: ${cronExpression}`);
743
- }
744
- const [minute, hour, dayOfMonth, month, dayOfWeek] = parts;
745
- const now = /* @__PURE__ */ new Date();
746
- const next = new Date(now);
747
- next.setSeconds(0, 0);
748
- next.setMinutes(next.getMinutes() + 1);
749
- const maxAttempts = 365 * 24 * 60;
750
- for (let i = 0; i < maxAttempts; i++) {
751
- if (matchesCronPart(next.getMinutes(), minute) && matchesCronPart(next.getHours(), hour) && matchesCronPart(next.getDate(), dayOfMonth) && matchesCronPart(next.getMonth() + 1, month) && matchesCronPart(next.getDay(), dayOfWeek)) {
752
- return next;
753
- }
754
- next.setMinutes(next.getMinutes() + 1);
755
- }
756
- const fallback = new Date(now);
757
- fallback.setDate(fallback.getDate() + 1);
758
- fallback.setHours(parseInt(hour, 10) || 9);
759
- fallback.setMinutes(parseInt(minute, 10) || 0);
760
- fallback.setSeconds(0, 0);
761
- return fallback;
762
- }
763
- function matchesCronPart(value, cronPart) {
764
- if (cronPart === "*") return true;
765
- if (cronPart.startsWith("*/")) {
766
- const step = parseInt(cronPart.slice(2), 10);
767
- return value % step === 0;
768
- }
769
- if (cronPart.includes("-")) {
770
- const [start, end] = cronPart.split("-").map((n) => parseInt(n, 10));
771
- return value >= start && value <= end;
772
- }
773
- if (cronPart.includes(",")) {
774
- return cronPart.split(",").map((n) => parseInt(n, 10)).includes(value);
775
- }
776
- return parseInt(cronPart, 10) === value;
777
- }
778
- function isValidCronExpression(expr) {
779
- if (!expr || typeof expr !== "string") return false;
780
- const parts = expr.trim().split(/\s+/);
781
- if (parts.length !== 5) return false;
782
- const ranges = [
783
- [0, 59],
784
- // minute
785
- [0, 23],
786
- // hour
787
- [1, 31],
788
- // day of month
789
- [1, 12],
790
- // month
791
- [0, 7]
792
- // day of week (0 and 7 are Sunday)
793
- ];
794
- return parts.every((part, i) => {
795
- if (part === "*") return true;
796
- if (part.startsWith("*/")) {
797
- const step = parseInt(part.slice(2), 10);
798
- return !isNaN(step) && step > 0;
799
- }
800
- if (part.includes("-")) {
801
- const [start, end] = part.split("-").map((n) => parseInt(n, 10));
802
- return !isNaN(start) && !isNaN(end) && start >= ranges[i][0] && end <= ranges[i][1];
803
- }
804
- if (part.includes(",")) {
805
- return part.split(",").every((n) => {
806
- const val2 = parseInt(n, 10);
807
- return !isNaN(val2) && val2 >= ranges[i][0] && val2 <= ranges[i][1];
808
- });
809
- }
810
- const val = parseInt(part, 10);
811
- return !isNaN(val) && val >= ranges[i][0] && val <= ranges[i][1];
812
- });
813
- }
814
- var init_schedule_parser = __esm({
815
- "src/scheduler/schedule-parser.ts"() {
816
- "use strict";
817
- }
818
- });
819
-
820
- // src/scheduler/schedule-tool.ts
821
- function matchGlobPattern(pattern, value) {
822
- const regexPattern = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*").replace(/\?/g, ".");
823
- return new RegExp(`^${regexPattern}$`).test(value);
824
- }
825
- function isWorkflowAllowedByPatterns(workflow, allowedPatterns, deniedPatterns) {
826
- if (deniedPatterns && deniedPatterns.length > 0) {
827
- for (const pattern of deniedPatterns) {
828
- if (matchGlobPattern(pattern, workflow)) {
829
- return {
830
- allowed: false,
831
- reason: `Workflow "${workflow}" matches denied pattern "${pattern}"`
832
- };
833
- }
834
- }
835
- }
836
- if (allowedPatterns && allowedPatterns.length > 0) {
837
- for (const pattern of allowedPatterns) {
838
- if (matchGlobPattern(pattern, workflow)) {
839
- return { allowed: true };
840
- }
841
- }
842
- return {
843
- allowed: false,
844
- reason: `Workflow "${workflow}" does not match any allowed patterns: ${allowedPatterns.join(", ")}`
845
- };
846
- }
847
- return { allowed: true };
848
- }
849
- function checkSchedulePermissions(context, workflow, requestedScheduleType) {
850
- const permissions = context.permissions;
851
- const scheduleType = requestedScheduleType || context.scheduleType || "personal";
852
- if (context.allowedScheduleType && scheduleType !== context.allowedScheduleType) {
853
- const contextNames = {
854
- personal: "a direct message (DM)",
855
- channel: "a channel",
856
- dm: "a group DM"
857
- };
858
- const targetNames = {
859
- personal: "personal",
860
- channel: "channel",
861
- dm: "group"
862
- };
863
- return {
864
- allowed: false,
865
- reason: `From ${contextNames[context.allowedScheduleType]}, you can only create ${targetNames[context.allowedScheduleType]} schedules. To create a ${targetNames[scheduleType]} schedule, please use the appropriate context.`
866
- };
867
- }
868
- if (!permissions) {
869
- return { allowed: true };
870
- }
871
- switch (scheduleType) {
872
- case "personal":
873
- if (permissions.allowPersonal === false) {
874
- return {
875
- allowed: false,
876
- reason: "Personal schedules are not allowed in this configuration"
877
- };
878
- }
879
- break;
880
- case "channel":
881
- if (permissions.allowChannel === false) {
882
- return {
883
- allowed: false,
884
- reason: "Channel schedules are not allowed in this configuration"
885
- };
886
- }
887
- break;
888
- case "dm":
889
- if (permissions.allowDm === false) {
890
- return {
891
- allowed: false,
892
- reason: "DM schedules are not allowed in this configuration"
893
- };
894
- }
895
- break;
896
- }
897
- return isWorkflowAllowedByPatterns(
898
- workflow,
899
- permissions.allowedWorkflows,
900
- permissions.deniedWorkflows
901
- );
902
- }
903
- function formatSchedule(schedule) {
904
- const time = schedule.isRecurring ? schedule.originalExpression : new Date(schedule.runAt).toLocaleString();
905
- const status = schedule.status !== "active" ? ` (${schedule.status})` : "";
906
- const displayName = schedule.workflow || schedule.workflowInputs?.text || "scheduled message";
907
- const truncatedName = displayName.length > 30 ? displayName.substring(0, 27) + "..." : displayName;
908
- const output = schedule.outputContext?.type || "none";
909
- return `\`${schedule.id.substring(0, 8)}\` - "${truncatedName}" - ${time} (\u2192 ${output})${status}`;
910
- }
911
- function formatCreateConfirmation(schedule) {
912
- const outputDesc = schedule.outputContext?.type ? `${schedule.outputContext.type}${schedule.outputContext.target ? `:${schedule.outputContext.target}` : ""}` : "none";
913
- const displayName = schedule.workflow || schedule.workflowInputs?.text || "scheduled message";
914
- if (schedule.isRecurring) {
915
- const nextRun = schedule.nextRunAt ? new Date(schedule.nextRunAt).toLocaleString("en-US", {
916
- weekday: "long",
917
- month: "short",
918
- day: "numeric",
919
- hour: "numeric",
920
- minute: "2-digit"
921
- }) : "calculating...";
922
- return `**Schedule created!**
923
-
924
- **${schedule.workflow ? "Workflow" : "Reminder"}**: ${displayName}
925
- **When**: ${schedule.originalExpression}
926
- **Output**: ${outputDesc}
927
- **Next run**: ${nextRun}
928
-
929
- ID: \`${schedule.id.substring(0, 8)}\``;
930
- } else {
931
- const when = new Date(schedule.runAt).toLocaleString("en-US", {
932
- weekday: "long",
933
- month: "short",
934
- day: "numeric",
935
- hour: "numeric",
936
- minute: "2-digit"
937
- });
938
- return `**Schedule created!**
939
-
940
- **${schedule.workflow ? "Workflow" : "Reminder"}**: ${displayName}
941
- **When**: ${when}
942
- **Output**: ${outputDesc}
943
-
944
- ID: \`${schedule.id.substring(0, 8)}\``;
945
- }
946
- }
947
- function formatScheduleList(schedules) {
948
- if (schedules.length === 0) {
949
- return `You don't have any active schedules.
950
-
951
- To create one: "remind me every Monday at 9am to check PRs" or "schedule %daily-report every Monday at 9am"`;
952
- }
953
- const lines = schedules.map((s, i) => `${i + 1}. ${formatSchedule(s)}`);
954
- return `**Your active schedules:**
955
-
956
- ${lines.join("\n")}
957
-
958
- To cancel: "cancel schedule <id>"
959
- To pause: "pause schedule <id>"`;
960
- }
961
- async function handleScheduleAction(args, context) {
962
- const store = ScheduleStore.getInstance();
963
- if (!store.isInitialized()) {
964
- await store.initialize();
965
- }
966
- switch (args.action) {
967
- case "create":
968
- return handleCreate(args, context, store);
969
- case "list":
970
- return handleList(context, store);
971
- case "cancel":
972
- return handleCancel(args, context, store);
973
- case "pause":
974
- return handlePauseResume(args, context, store, "paused");
975
- case "resume":
976
- return handlePauseResume(args, context, store, "active");
977
- default:
978
- return {
979
- success: false,
980
- message: `Unknown action: ${args.action}`,
981
- error: `Supported actions: create, list, cancel, pause, resume`
982
- };
983
- }
984
- }
985
- async function handleCreate(args, context, store) {
986
- if (!args.reminder_text && !args.workflow) {
987
- return {
988
- success: false,
989
- message: "Missing reminder content",
990
- error: "Please specify either reminder_text (what to say) or workflow (what to run)"
991
- };
992
- }
993
- if (!args.cron && !args.run_at) {
994
- return {
995
- success: false,
996
- message: "Missing schedule timing",
997
- error: 'Please specify either cron (for recurring, e.g., "* * * * *") or run_at (ISO timestamp for one-time)'
998
- };
999
- }
1000
- if (args.cron && !isValidCronExpression(args.cron)) {
1001
- return {
1002
- success: false,
1003
- message: "Invalid cron expression",
1004
- error: `"${args.cron}" is not a valid cron expression. Format: "minute hour day-of-month month day-of-week"`
1005
- };
1006
- }
1007
- let runAtTimestamp;
1008
- if (args.run_at) {
1009
- const parsed = new Date(args.run_at);
1010
- if (isNaN(parsed.getTime())) {
1011
- return {
1012
- success: false,
1013
- message: "Invalid run_at timestamp",
1014
- error: `"${args.run_at}" is not a valid ISO 8601 timestamp`
1015
- };
1016
- }
1017
- if (parsed.getTime() <= Date.now()) {
1018
- return {
1019
- success: false,
1020
- message: "run_at must be in the future",
1021
- error: "Cannot schedule a reminder in the past"
1022
- };
1023
- }
1024
- runAtTimestamp = parsed.getTime();
1025
- }
1026
- if (args.target_type && !args.target_id) {
1027
- return {
1028
- success: false,
1029
- message: "Missing target_id",
1030
- error: `target_type "${args.target_type}" requires a target_id (channel ID, user ID, or thread_ts)`
1031
- };
1032
- }
1033
- let scheduleType = "personal";
1034
- if (args.target_type === "channel") {
1035
- scheduleType = "channel";
1036
- } else if (args.target_type === "user") {
1037
- scheduleType = "dm";
1038
- }
1039
- const workflowName = args.workflow || "reminder";
1040
- const permissionCheck = checkSchedulePermissions(context, workflowName, scheduleType);
1041
- if (!permissionCheck.allowed) {
1042
- logger.warn(
1043
- `[ScheduleTool] Permission denied for user ${context.userId}: ${permissionCheck.reason}`
1044
- );
1045
- return {
1046
- success: false,
1047
- message: "Permission denied",
1048
- error: permissionCheck.reason || "You do not have permission to create this schedule"
1049
- };
1050
- }
1051
- if (args.workflow && context.availableWorkflows && !context.availableWorkflows.includes(args.workflow)) {
1052
- return {
1053
- success: false,
1054
- message: `Workflow "${args.workflow}" not found`,
1055
- error: `Available workflows: ${context.availableWorkflows.slice(0, 5).join(", ")}${context.availableWorkflows.length > 5 ? "..." : ""}`
1056
- };
1057
- }
1058
- try {
1059
- const timezone = context.timezone || "UTC";
1060
- const isRecurring = args.is_recurring === true || !!args.cron;
1061
- let outputContext;
1062
- if (args.target_type && args.target_id) {
1063
- outputContext = {
1064
- type: "slack",
1065
- // Currently only Slack supported
1066
- target: args.target_id,
1067
- // Channel ID (C... or D...)
1068
- threadId: args.thread_ts,
1069
- // Thread timestamp for replies
1070
- metadata: {
1071
- targetType: args.target_type,
1072
- reminderText: args.reminder_text
1073
- }
1074
- };
1075
- }
1076
- let nextRunAt;
1077
- if (isRecurring && args.cron) {
1078
- nextRunAt = getNextRunTime(args.cron, timezone).getTime();
1079
- } else if (runAtTimestamp) {
1080
- nextRunAt = runAtTimestamp;
1081
- }
1082
- const schedule = await store.createAsync({
1083
- creatorId: context.userId,
1084
- creatorContext: context.contextType,
1085
- creatorName: context.userName,
1086
- timezone,
1087
- schedule: args.cron || "",
1088
- runAt: runAtTimestamp,
1089
- isRecurring,
1090
- originalExpression: args.original_expression || args.cron || args.run_at || "",
1091
- workflow: args.workflow,
1092
- // Only set if explicitly provided
1093
- workflowInputs: args.workflow_inputs || (args.reminder_text ? { text: args.reminder_text } : void 0),
1094
- outputContext,
1095
- nextRunAt
1096
- });
1097
- const displayText = args.reminder_text || args.workflow || "scheduled task";
1098
- logger.info(
1099
- `[ScheduleTool] Created schedule ${schedule.id} for user ${context.userId}: "${displayText}"`
1100
- );
1101
- return {
1102
- success: true,
1103
- message: formatCreateConfirmation(schedule),
1104
- schedule
1105
- };
1106
- } catch (error) {
1107
- const errorMsg = error instanceof Error ? error.message : "Unknown error";
1108
- logger.warn(`[ScheduleTool] Failed to create schedule: ${errorMsg}`);
1109
- return {
1110
- success: false,
1111
- message: `Failed to create schedule: ${errorMsg}`,
1112
- error: errorMsg
1113
- };
1114
- }
1115
- }
1116
- async function handleList(context, store) {
1117
- const allUserSchedules = await store.getByCreatorAsync(context.userId);
1118
- const schedules = allUserSchedules.filter((s) => s.status !== "completed");
1119
- let filteredSchedules = schedules;
1120
- if (context.allowedScheduleType) {
1121
- filteredSchedules = schedules.filter((s) => {
1122
- const scheduleOutputType = s.outputContext?.type;
1123
- if (!scheduleOutputType || scheduleOutputType === "none") {
1124
- return context.allowedScheduleType === "personal";
1125
- }
1126
- if (scheduleOutputType === "slack") {
1127
- const target = s.outputContext?.target || "";
1128
- if (target.startsWith("#") || target.match(/^C[A-Z0-9]+$/)) {
1129
- return context.allowedScheduleType === "channel";
1130
- }
1131
- if (target.startsWith("@") || target.match(/^U[A-Z0-9]+$/)) {
1132
- return context.allowedScheduleType === "dm";
1133
- }
1134
- }
1135
- return context.allowedScheduleType === "personal";
1136
- });
1137
- }
1138
- return {
1139
- success: true,
1140
- message: formatScheduleList(filteredSchedules),
1141
- schedules: filteredSchedules
1142
- };
1143
- }
1144
- async function handleCancel(args, context, store) {
1145
- let schedule;
1146
- if (args.schedule_id) {
1147
- const userSchedules = await store.getByCreatorAsync(context.userId);
1148
- schedule = userSchedules.find((s) => s.id === args.schedule_id);
1149
- if (!schedule) {
1150
- schedule = userSchedules.find((s) => s.id.startsWith(args.schedule_id));
1151
- }
1152
- }
1153
- if (!schedule) {
1154
- return {
1155
- success: false,
1156
- message: "Schedule not found",
1157
- error: `Could not find schedule with ID "${args.schedule_id}" in your schedules. Use "list my schedules" to see your schedules.`
1158
- };
1159
- }
1160
- if (schedule.creatorId !== context.userId) {
1161
- logger.warn(
1162
- `[ScheduleTool] Attempted cross-user schedule cancellation: ${context.userId} tried to cancel ${schedule.id} owned by ${schedule.creatorId}`
1163
- );
1164
- return {
1165
- success: false,
1166
- message: "Not your schedule",
1167
- error: "You can only cancel your own schedules."
1168
- };
1169
- }
1170
- await store.deleteAsync(schedule.id);
1171
- logger.info(`[ScheduleTool] Cancelled schedule ${schedule.id} for user ${context.userId}`);
1172
- return {
1173
- success: true,
1174
- message: `**Schedule cancelled!**
1175
-
1176
- Was: "${schedule.workflow}" scheduled for ${schedule.originalExpression}`
1177
- };
1178
- }
1179
- async function handlePauseResume(args, context, store, newStatus) {
1180
- if (!args.schedule_id) {
1181
- return {
1182
- success: false,
1183
- message: "Missing schedule ID",
1184
- error: "Please specify which schedule to pause/resume."
1185
- };
1186
- }
1187
- const userSchedules = await store.getByCreatorAsync(context.userId);
1188
- let schedule = userSchedules.find((s) => s.id === args.schedule_id);
1189
- if (!schedule) {
1190
- schedule = userSchedules.find((s) => s.id.startsWith(args.schedule_id));
1191
- }
1192
- if (!schedule) {
1193
- return {
1194
- success: false,
1195
- message: "Schedule not found",
1196
- error: `Could not find schedule with ID "${args.schedule_id}" in your schedules.`
1197
- };
1198
- }
1199
- if (schedule.creatorId !== context.userId) {
1200
- logger.warn(
1201
- `[ScheduleTool] Attempted cross-user schedule modification: ${context.userId} tried to modify ${schedule.id} owned by ${schedule.creatorId}`
1202
- );
1203
- return {
1204
- success: false,
1205
- message: "Not your schedule",
1206
- error: "You can only modify your own schedules."
1207
- };
1208
- }
1209
- const updated = await store.updateAsync(schedule.id, { status: newStatus });
1210
- const action = newStatus === "paused" ? "paused" : "resumed";
1211
- logger.info(`[ScheduleTool] ${action} schedule ${schedule.id} for user ${context.userId}`);
1212
- return {
1213
- success: true,
1214
- message: `**Schedule ${action}!**
1215
-
1216
- "${schedule.workflow}" - ${schedule.originalExpression}`,
1217
- schedule: updated
1218
- };
1219
- }
1220
- function getScheduleToolDefinition() {
1221
- return {
1222
- name: "schedule",
1223
- description: `Schedule, list, and manage reminders or workflow executions.
1224
-
1225
- YOU (the AI) must extract and structure all scheduling parameters. Do NOT pass natural language time expressions - convert them to cron or ISO timestamps.
1226
-
1227
- CRITICAL WORKFLOW RULE:
1228
- - To schedule a WORKFLOW, the user MUST use a '%' prefix (e.g., "schedule %my-workflow daily").
1229
- - If the '%' prefix is present, extract the word following it as the 'workflow' parameter (without the '%').
1230
- - If the '%' prefix is NOT present, the request is a simple text reminder. The ENTIRE user request (excluding the schedule expression) MUST be placed in the 'reminder_text' parameter.
1231
- - DO NOT guess or infer a workflow name from a user's request without the '%' prefix.
1232
-
1233
- ACTIONS:
1234
- - create: Schedule a new reminder or workflow
1235
- - list: Show user's active schedules
1236
- - cancel: Remove a schedule by ID
1237
- - pause/resume: Temporarily disable/enable a schedule
1238
-
1239
- FOR CREATE ACTION - Extract these from user's request:
1240
- 1. WHAT:
1241
- - If user says "schedule %some-workflow ...", populate 'workflow' with "some-workflow".
1242
- - Otherwise, populate 'reminder_text' with the user's full request text.
1243
- 2. WHERE: Use the CURRENT channel from context
1244
- - target_id: The channel ID from context (C... for channels, D... for DMs)
1245
- - target_type: "channel" for public/private channels, "dm" for direct messages
1246
- - ONLY use target_type="thread" with thread_ts if user is INSIDE a thread
1247
- - When NOT in a thread, reminders post as NEW messages (not thread replies)
1248
- 3. WHEN: Either cron (for recurring) OR run_at (ISO 8601 for one-time)
1249
- - Recurring: Generate cron expression (minute hour day-of-month month day-of-week)
1250
- - One-time: Generate ISO 8601 timestamp
1251
-
1252
- CRON EXAMPLES:
1253
- - "every minute" \u2192 cron: "* * * * *"
1254
- - "every hour" \u2192 cron: "0 * * * *"
1255
- - "every day at 9am" \u2192 cron: "0 9 * * *"
1256
- - "every Monday at 9am" \u2192 cron: "0 9 * * 1"
1257
- - "weekdays at 8:30am" \u2192 cron: "30 8 * * 1-5"
1258
- - "every 5 minutes" \u2192 cron: "*/5 * * * *"
1259
-
1260
- ONE-TIME EXAMPLES:
1261
- - "in 2 hours" \u2192 run_at: "<ISO timestamp 2 hours from now>"
1262
- - "tomorrow at 3pm" \u2192 run_at: "2026-02-08T15:00:00Z"
1263
-
1264
- USAGE EXAMPLES:
1265
-
1266
- User in DM: "remind me to check builds every day at 9am"
1267
- \u2192 {
1268
- "action": "create",
1269
- "reminder_text": "check builds",
1270
- "is_recurring": true,
1271
- "cron": "0 9 * * *",
1272
- "target_type": "dm",
1273
- "target_id": "<DM channel ID from context, e.g., D09SZABNLG3>",
1274
- "original_expression": "every day at 9am"
1275
- }
1276
-
1277
- User in #security channel: "schedule %security-scan every Monday at 10am"
1278
- \u2192 {
1279
- "action": "create",
1280
- "workflow": "security-scan",
1281
- "is_recurring": true,
1282
- "cron": "0 10 * * 1",
1283
- "target_type": "channel",
1284
- "target_id": "<channel ID from context, e.g., C05ABC123>",
1285
- "original_expression": "every Monday at 10am"
1286
- }
1287
-
1288
- User in #security channel: "run security-scan every Monday at 10am" (NO % prefix!)
1289
- \u2192 {
1290
- "action": "create",
1291
- "reminder_text": "run security-scan every Monday at 10am",
1292
- "is_recurring": true,
1293
- "cron": "0 10 * * 1",
1294
- "target_type": "channel",
1295
- "target_id": "<channel ID from context, e.g., C05ABC123>",
1296
- "original_expression": "every Monday at 10am"
1297
- }
1298
-
1299
- User in DM: "remind me in 2 hours to review the PR"
1300
- \u2192 {
1301
- "action": "create",
1302
- "reminder_text": "review the PR",
1303
- "is_recurring": false,
1304
- "run_at": "2026-02-07T18:00:00Z",
1305
- "target_type": "dm",
1306
- "target_id": "<DM channel ID from context>",
1307
- "original_expression": "in 2 hours"
1308
- }
1309
-
1310
- User inside a thread: "remind me about this tomorrow"
1311
- \u2192 {
1312
- "action": "create",
1313
- "reminder_text": "Check this thread",
1314
- "is_recurring": false,
1315
- "run_at": "2026-02-08T09:00:00Z",
1316
- "target_type": "thread",
1317
- "target_id": "<channel ID>",
1318
- "thread_ts": "<thread_ts from context>",
1319
- "original_expression": "tomorrow"
1320
- }
1321
-
1322
- User: "list my schedules"
1323
- \u2192 { "action": "list" }
1324
-
1325
- User: "cancel schedule abc123"
1326
- \u2192 { "action": "cancel", "schedule_id": "abc123" }`,
1327
- inputSchema: {
1328
- type: "object",
1329
- properties: {
1330
- action: {
1331
- type: "string",
1332
- enum: ["create", "list", "cancel", "pause", "resume"],
1333
- description: "What to do: create new, list existing, cancel/pause/resume by ID"
1334
- },
1335
- // WHAT to do
1336
- reminder_text: {
1337
- type: "string",
1338
- description: "For create: the message/reminder text to send when triggered"
1339
- },
1340
- workflow: {
1341
- type: "string",
1342
- description: 'For create: workflow ID to run. ONLY populate this if the user used the % prefix (e.g., "%my-workflow"). Extract the name without the % symbol. If no % prefix, use reminder_text instead.'
1343
- },
1344
- workflow_inputs: {
1345
- type: "object",
1346
- description: "For create: optional inputs to pass to the workflow"
1347
- },
1348
- // WHERE to send
1349
- target_type: {
1350
- type: "string",
1351
- enum: ["channel", "dm", "thread", "user"],
1352
- description: "For create: where to send output. channel=public/private channel, dm=DM to self (current DM channel), user=DM to specific user, thread=reply in current thread"
1353
- },
1354
- target_id: {
1355
- type: "string",
1356
- description: "For create: Slack channel ID. Channels start with C, DMs start with D. Always use the channel ID from the current context."
1357
- },
1358
- thread_ts: {
1359
- type: "string",
1360
- description: "For create with target_type=thread: the thread timestamp to reply to. Get this from the current thread context."
1361
- },
1362
- // WHEN to run
1363
- is_recurring: {
1364
- type: "boolean",
1365
- description: "For create: true for recurring schedules (cron), false for one-time (run_at)"
1366
- },
1367
- cron: {
1368
- type: "string",
1369
- description: 'For create recurring: cron expression (minute hour day-of-month month day-of-week). Examples: "0 9 * * *" (daily 9am), "* * * * *" (every minute), "0 9 * * 1" (Mondays 9am)'
1370
- },
1371
- run_at: {
1372
- type: "string",
1373
- description: 'For create one-time: ISO 8601 timestamp when to run (e.g., "2026-02-07T15:00:00Z")'
1374
- },
1375
- original_expression: {
1376
- type: "string",
1377
- description: "For create: the original natural language expression from user (for display only)"
1378
- },
1379
- // For cancel/pause/resume
1380
- schedule_id: {
1381
- type: "string",
1382
- description: "For cancel/pause/resume: the schedule ID to act on (first 8 chars is enough)"
1383
- }
1384
- },
1385
- required: ["action"]
1386
- },
1387
- exec: ""
1388
- // Not used - this tool has a custom handler
1389
- };
1390
- }
1391
- function isScheduleTool(toolName) {
1392
- return toolName === "schedule";
1393
- }
1394
- function determineScheduleType(contextType, outputType, outputTarget) {
1395
- if (outputType === "slack" && outputTarget) {
1396
- if (outputTarget.startsWith("#") || outputTarget.match(/^C[A-Z0-9]+$/)) {
1397
- return "channel";
1398
- }
1399
- if (outputTarget.startsWith("@") || outputTarget.match(/^U[A-Z0-9]+$/)) {
1400
- return "dm";
1401
- }
1402
- }
1403
- if (contextType === "cli" || contextType.startsWith("github:")) {
1404
- return "personal";
1405
- }
1406
- return "personal";
1407
- }
1408
- function slackChannelTypeToScheduleType(channelType) {
1409
- switch (channelType) {
1410
- case "channel":
1411
- return "channel";
1412
- case "group":
1413
- return "dm";
1414
- // Group DMs map to 'dm' schedule type
1415
- case "dm":
1416
- default:
1417
- return "personal";
1418
- }
1419
- }
1420
- function buildScheduleToolContext(sources, availableWorkflows, permissions, outputInfo) {
1421
- if (sources.slackContext) {
1422
- const contextType = `slack:${sources.slackContext.userId}`;
1423
- const scheduleType = determineScheduleType(
1424
- contextType,
1425
- outputInfo?.outputType,
1426
- outputInfo?.outputTarget
1427
- );
1428
- let allowedScheduleType;
1429
- if (sources.slackContext.channelType) {
1430
- allowedScheduleType = slackChannelTypeToScheduleType(sources.slackContext.channelType);
1431
- }
1432
- let finalScheduleType = scheduleType;
1433
- if (!outputInfo?.outputType && sources.slackContext.channelType) {
1434
- finalScheduleType = slackChannelTypeToScheduleType(sources.slackContext.channelType);
1435
- }
1436
- return {
1437
- userId: sources.slackContext.userId,
1438
- userName: sources.slackContext.userName,
1439
- contextType,
1440
- timezone: sources.slackContext.timezone,
1441
- availableWorkflows,
1442
- scheduleType: finalScheduleType,
1443
- permissions,
1444
- allowedScheduleType
1445
- };
1446
- }
1447
- if (sources.githubContext) {
1448
- return {
1449
- userId: sources.githubContext.login,
1450
- contextType: `github:${sources.githubContext.login}`,
1451
- timezone: "UTC",
1452
- // GitHub doesn't provide timezone
1453
- availableWorkflows,
1454
- scheduleType: "personal",
1455
- permissions,
1456
- allowedScheduleType: "personal"
1457
- // GitHub context only allows personal schedules
1458
- };
1459
- }
1460
- return {
1461
- userId: sources.cliContext?.userId || process.env.USER || "cli-user",
1462
- contextType: "cli",
1463
- timezone: Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC",
1464
- availableWorkflows,
1465
- scheduleType: "personal",
1466
- permissions,
1467
- allowedScheduleType: "personal"
1468
- // CLI context only allows personal schedules
1469
- };
1470
- }
1471
- var init_schedule_tool = __esm({
1472
- "src/scheduler/schedule-tool.ts"() {
1473
- init_schedule_store();
1474
- init_schedule_parser();
1475
- init_logger();
1476
- }
1477
- });
1478
-
1479
- export {
1480
- init_store,
1481
- ScheduleStore,
1482
- init_schedule_store,
1483
- init_schedule_parser,
1484
- handleScheduleAction,
1485
- getScheduleToolDefinition,
1486
- isScheduleTool,
1487
- buildScheduleToolContext,
1488
- init_schedule_tool
1489
- };
1490
- //# sourceMappingURL=chunk-XKCER23W.mjs.map