@probelabs/visor 0.1.147 → 0.1.148
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/frontends/github-frontend.d.ts +2 -1
- package/dist/frontends/github-frontend.d.ts.map +1 -1
- package/dist/index.js +726 -113
- package/dist/output/traces/{run-2026-02-27T11-27-22-261Z.ndjson → run-2026-03-02T18-32-11-359Z.ndjson} +84 -84
- package/dist/{traces/run-2026-02-27T11-28-08-546Z.ndjson → output/traces/run-2026-03-02T18-32-55-702Z.ndjson} +1171 -1171
- package/dist/providers/ai-check-provider.d.ts.map +1 -1
- package/dist/scheduler/schedule-tool.d.ts.map +1 -1
- package/dist/scheduler/scheduler.d.ts +5 -0
- package/dist/scheduler/scheduler.d.ts.map +1 -1
- package/dist/sdk/{check-provider-registry-CTZA3EVE.mjs → check-provider-registry-35BPTY4W.mjs} +5 -6
- package/dist/sdk/{check-provider-registry-SCPM6DIT.mjs → check-provider-registry-DVQDGTOE.mjs} +5 -6
- package/dist/sdk/{check-provider-registry-CDL5AJSI.mjs → check-provider-registry-KHPY6LB4.mjs} +5 -6
- package/dist/sdk/{chunk-4F5UVWAN.mjs → chunk-62TNF5PJ.mjs} +2 -2
- package/dist/sdk/{chunk-4F5UVWAN.mjs.map → chunk-62TNF5PJ.mjs.map} +1 -1
- package/dist/sdk/{chunk-H23T7J6Y.mjs → chunk-6N6JRWCW.mjs} +2742 -276
- package/dist/sdk/chunk-6N6JRWCW.mjs.map +1 -0
- package/dist/sdk/{chunk-JKWLGLDR.mjs → chunk-AYQE4JCU.mjs} +3 -3
- package/dist/sdk/{chunk-FBJ7MC7R.mjs → chunk-CISJ6DJW.mjs} +3 -3
- package/dist/sdk/{chunk-YQZW3D2V.mjs → chunk-EGUHXVWS.mjs} +3 -3
- package/dist/sdk/{chunk-YQZW3D2V.mjs.map → chunk-EGUHXVWS.mjs.map} +1 -1
- package/dist/sdk/{chunk-EWGX7LI7.mjs → chunk-H4AYMOAT.mjs} +2742 -276
- package/dist/sdk/chunk-H4AYMOAT.mjs.map +1 -0
- package/dist/sdk/{chunk-R77LN3OE.mjs → chunk-IF2UD2KS.mjs} +2742 -276
- package/dist/sdk/chunk-IF2UD2KS.mjs.map +1 -0
- package/dist/sdk/{chunk-2NFKN6CY.mjs → chunk-RJLJUTSU.mjs} +2 -2
- package/dist/sdk/{chunk-V2QW6ECX.mjs → chunk-S2YO4ZE3.mjs} +2 -2
- package/dist/sdk/{failure-condition-evaluator-FHNZL2US.mjs → failure-condition-evaluator-I6QWFKV3.mjs} +3 -3
- package/dist/sdk/{failure-condition-evaluator-2B5WY7QN.mjs → failure-condition-evaluator-IVCTD4BZ.mjs} +3 -3
- package/dist/sdk/{github-frontend-V3WUHL6E.mjs → github-frontend-2MC77L7F.mjs} +16 -4
- package/dist/sdk/github-frontend-2MC77L7F.mjs.map +1 -0
- package/dist/sdk/{github-frontend-47EU2HBY.mjs → github-frontend-DFT5G32K.mjs} +16 -4
- package/dist/sdk/github-frontend-DFT5G32K.mjs.map +1 -0
- package/dist/sdk/{host-GVR4UGZ3.mjs → host-4F6I3ZXN.mjs} +2 -2
- package/dist/sdk/{host-UQUQIYFG.mjs → host-H7IX4GBK.mjs} +2 -2
- package/dist/sdk/{routing-CZ36LVVS.mjs → routing-LU5PAREW.mjs} +4 -4
- package/dist/sdk/{routing-THIWDEYY.mjs → routing-UT3BXBXH.mjs} +4 -4
- package/dist/sdk/schedule-tool-CONR4VW3.mjs +35 -0
- package/dist/sdk/schedule-tool-K3GQXCBN.mjs +35 -0
- package/dist/sdk/schedule-tool-SBXAEBDD.mjs +35 -0
- package/dist/sdk/{schedule-tool-handler-KFYNV7HL.mjs → schedule-tool-handler-GFQCJAVZ.mjs} +5 -6
- package/dist/sdk/{schedule-tool-handler-QUMAF2DJ.mjs → schedule-tool-handler-R7PG3VMR.mjs} +5 -6
- package/dist/sdk/{schedule-tool-handler-GEH62OUM.mjs → schedule-tool-handler-YUC6CAXX.mjs} +5 -6
- package/dist/sdk/sdk.js +1551 -349
- package/dist/sdk/sdk.js.map +1 -1
- package/dist/sdk/sdk.mjs +4 -5
- package/dist/sdk/sdk.mjs.map +1 -1
- package/dist/sdk/{trace-helpers-W7TF5ZKF.mjs → trace-helpers-6ROJR7N3.mjs} +2 -2
- package/dist/sdk/{trace-helpers-EHDZ42HH.mjs → trace-helpers-J463EU4B.mjs} +2 -2
- package/dist/sdk/{workflow-check-provider-5453TW65.mjs → workflow-check-provider-DYSO3PML.mjs} +5 -6
- package/dist/sdk/{workflow-check-provider-HMABCGB5.mjs → workflow-check-provider-FIFFQDQU.mjs} +5 -6
- package/dist/sdk/workflow-check-provider-FIFFQDQU.mjs.map +1 -0
- package/dist/sdk/{workflow-check-provider-3K7732MW.mjs → workflow-check-provider-GJNGTS3F.mjs} +5 -6
- package/dist/sdk/workflow-check-provider-GJNGTS3F.mjs.map +1 -0
- package/dist/state-machine/context/build-engine-context.d.ts.map +1 -1
- package/dist/traces/{run-2026-02-27T11-27-22-261Z.ndjson → run-2026-03-02T18-32-11-359Z.ndjson} +84 -84
- package/dist/{output/traces/run-2026-02-27T11-28-08-546Z.ndjson → traces/run-2026-03-02T18-32-55-702Z.ndjson} +1171 -1171
- package/dist/utils/tool-resolver.d.ts.map +1 -1
- package/dist/utils/workspace-manager.d.ts +31 -8
- package/dist/utils/workspace-manager.d.ts.map +1 -1
- package/dist/utils/worktree-manager.d.ts +6 -0
- package/dist/utils/worktree-manager.d.ts.map +1 -1
- package/package.json +2 -2
- package/dist/sdk/chunk-EWGX7LI7.mjs.map +0 -1
- package/dist/sdk/chunk-H23T7J6Y.mjs.map +0 -1
- package/dist/sdk/chunk-R77LN3OE.mjs.map +0 -1
- package/dist/sdk/chunk-XKCER23W.mjs +0 -1490
- package/dist/sdk/chunk-XKCER23W.mjs.map +0 -1
- package/dist/sdk/github-frontend-47EU2HBY.mjs.map +0 -1
- package/dist/sdk/github-frontend-V3WUHL6E.mjs.map +0 -1
- package/dist/sdk/schedule-tool-2COUUTF7.mjs +0 -18
- /package/dist/sdk/{check-provider-registry-CDL5AJSI.mjs.map → check-provider-registry-35BPTY4W.mjs.map} +0 -0
- /package/dist/sdk/{check-provider-registry-CTZA3EVE.mjs.map → check-provider-registry-DVQDGTOE.mjs.map} +0 -0
- /package/dist/sdk/{check-provider-registry-SCPM6DIT.mjs.map → check-provider-registry-KHPY6LB4.mjs.map} +0 -0
- /package/dist/sdk/{chunk-FBJ7MC7R.mjs.map → chunk-AYQE4JCU.mjs.map} +0 -0
- /package/dist/sdk/{chunk-JKWLGLDR.mjs.map → chunk-CISJ6DJW.mjs.map} +0 -0
- /package/dist/sdk/{chunk-2NFKN6CY.mjs.map → chunk-RJLJUTSU.mjs.map} +0 -0
- /package/dist/sdk/{chunk-V2QW6ECX.mjs.map → chunk-S2YO4ZE3.mjs.map} +0 -0
- /package/dist/sdk/{failure-condition-evaluator-2B5WY7QN.mjs.map → failure-condition-evaluator-I6QWFKV3.mjs.map} +0 -0
- /package/dist/sdk/{failure-condition-evaluator-FHNZL2US.mjs.map → failure-condition-evaluator-IVCTD4BZ.mjs.map} +0 -0
- /package/dist/sdk/{host-GVR4UGZ3.mjs.map → host-4F6I3ZXN.mjs.map} +0 -0
- /package/dist/sdk/{host-UQUQIYFG.mjs.map → host-H7IX4GBK.mjs.map} +0 -0
- /package/dist/sdk/{routing-CZ36LVVS.mjs.map → routing-LU5PAREW.mjs.map} +0 -0
- /package/dist/sdk/{routing-THIWDEYY.mjs.map → routing-UT3BXBXH.mjs.map} +0 -0
- /package/dist/sdk/{schedule-tool-2COUUTF7.mjs.map → schedule-tool-CONR4VW3.mjs.map} +0 -0
- /package/dist/sdk/{schedule-tool-handler-GEH62OUM.mjs.map → schedule-tool-K3GQXCBN.mjs.map} +0 -0
- /package/dist/sdk/{schedule-tool-handler-KFYNV7HL.mjs.map → schedule-tool-SBXAEBDD.mjs.map} +0 -0
- /package/dist/sdk/{schedule-tool-handler-QUMAF2DJ.mjs.map → schedule-tool-handler-GFQCJAVZ.mjs.map} +0 -0
- /package/dist/sdk/{trace-helpers-EHDZ42HH.mjs.map → schedule-tool-handler-R7PG3VMR.mjs.map} +0 -0
- /package/dist/sdk/{trace-helpers-W7TF5ZKF.mjs.map → schedule-tool-handler-YUC6CAXX.mjs.map} +0 -0
- /package/dist/sdk/{workflow-check-provider-3K7732MW.mjs.map → trace-helpers-6ROJR7N3.mjs.map} +0 -0
- /package/dist/sdk/{workflow-check-provider-5453TW65.mjs.map → trace-helpers-J463EU4B.mjs.map} +0 -0
- /package/dist/sdk/{workflow-check-provider-HMABCGB5.mjs.map → workflow-check-provider-DYSO3PML.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
|