crontick 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +56 -0
- package/dist/chunk-35FFLWP3.js +79 -0
- package/dist/chunk-35FFLWP3.js.map +1 -0
- package/dist/chunk-FMGZ3SSS.js +57 -0
- package/dist/chunk-FMGZ3SSS.js.map +1 -0
- package/dist/chunk-LPAFZNXK.js +214 -0
- package/dist/chunk-LPAFZNXK.js.map +1 -0
- package/dist/chunk-YMAT5MON.js +22 -0
- package/dist/chunk-YMAT5MON.js.map +1 -0
- package/dist/cli/index.cjs +879 -0
- package/dist/cli/index.cjs.map +1 -0
- package/dist/cli/index.d.cts +2 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +554 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/daemon/index.cjs +1655 -0
- package/dist/daemon/index.cjs.map +1 -0
- package/dist/daemon/index.d.cts +2 -0
- package/dist/daemon/index.d.ts +2 -0
- package/dist/daemon/index.js +1306 -0
- package/dist/daemon/index.js.map +1 -0
- package/dist/dashboard/.gitkeep +0 -0
- package/dist/dashboard/dashboard.css +192 -0
- package/dist/dashboard/dashboard.js +316 -0
- package/dist/dashboard/index.html +108 -0
- package/dist/index.cjs +180 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +121 -0
- package/dist/index.d.ts +121 -0
- package/dist/index.js +32 -0
- package/dist/index.js.map +1 -0
- package/dist/job-JQGLDEJV.js +26 -0
- package/dist/job-JQGLDEJV.js.map +1 -0
- package/dist/mcp/index.cjs +1000 -0
- package/dist/mcp/index.cjs.map +1 -0
- package/dist/mcp/index.d.cts +13 -0
- package/dist/mcp/index.d.ts +13 -0
- package/dist/mcp/index.js +876 -0
- package/dist/mcp/index.js.map +1 -0
- package/package.json +81 -0
- package/plugin/.gitkeep +0 -0
- package/plugin/README.md +50 -0
- package/plugin/install.mjs +155 -0
- package/plugin/plugin.json +11 -0
- package/plugin/uninstall.mjs +68 -0
- package/src/skill/SKILL.md +227 -0
|
@@ -0,0 +1,876 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
VERSION,
|
|
4
|
+
ensureDirs,
|
|
5
|
+
logsDir,
|
|
6
|
+
pidFilePath,
|
|
7
|
+
portFilePath
|
|
8
|
+
} from "../chunk-FMGZ3SSS.js";
|
|
9
|
+
import {
|
|
10
|
+
ActionSchema,
|
|
11
|
+
JobSchema,
|
|
12
|
+
ScheduleSchema
|
|
13
|
+
} from "../chunk-35FFLWP3.js";
|
|
14
|
+
|
|
15
|
+
// src/mcp/index.ts
|
|
16
|
+
import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
17
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
18
|
+
import { z } from "zod";
|
|
19
|
+
import { readFileSync, existsSync, appendFileSync, statSync, mkdirSync } from "fs";
|
|
20
|
+
import { spawn } from "child_process";
|
|
21
|
+
import { fileURLToPath } from "url";
|
|
22
|
+
import { dirname, resolve as pathResolve, join as pathJoin } from "path";
|
|
23
|
+
import { zodToJsonSchema } from "zod-to-json-schema";
|
|
24
|
+
var __dirname = dirname(fileURLToPath(import.meta.url));
|
|
25
|
+
function getDaemonBaseUrl() {
|
|
26
|
+
const envUrl = process.env["CRONTICK_DAEMON_URL"];
|
|
27
|
+
if (envUrl) return envUrl.replace(/\/$/, "");
|
|
28
|
+
const pf = portFilePath();
|
|
29
|
+
if (!existsSync(pf)) throw new DaemonUnavailableError("Port file not found.");
|
|
30
|
+
const port = parseInt(readFileSync(pf, "utf-8").trim(), 10);
|
|
31
|
+
if (isNaN(port) || port <= 0) throw new DaemonUnavailableError("Invalid port in port file.");
|
|
32
|
+
return `http://127.0.0.1:${port}`;
|
|
33
|
+
}
|
|
34
|
+
var DaemonUnavailableError = class extends Error {
|
|
35
|
+
constructor(detail) {
|
|
36
|
+
super(
|
|
37
|
+
`Daemon is not running (${detail}) \u2014 run: crontick daemon start`
|
|
38
|
+
);
|
|
39
|
+
this.name = "DaemonUnavailableError";
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
function daemonScript() {
|
|
43
|
+
return pathResolve(__dirname, "../daemon/index.js");
|
|
44
|
+
}
|
|
45
|
+
function appendAutostartLog(text) {
|
|
46
|
+
try {
|
|
47
|
+
mkdirSync(logsDir(), { recursive: true });
|
|
48
|
+
const logPath = pathJoin(logsDir(), "daemon.autostart.log");
|
|
49
|
+
let size = 0;
|
|
50
|
+
try {
|
|
51
|
+
size = statSync(logPath).size;
|
|
52
|
+
} catch {
|
|
53
|
+
}
|
|
54
|
+
if (size < 256 * 1024) {
|
|
55
|
+
appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] ${text}
|
|
56
|
+
`);
|
|
57
|
+
}
|
|
58
|
+
} catch {
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
function waitForPortFile(maxMs = 1e4, getStderr) {
|
|
62
|
+
const start = Date.now();
|
|
63
|
+
return new Promise((resolve, reject) => {
|
|
64
|
+
const check = () => {
|
|
65
|
+
if (existsSync(portFilePath())) {
|
|
66
|
+
try {
|
|
67
|
+
const port = parseInt(readFileSync(portFilePath(), "utf-8").trim(), 10);
|
|
68
|
+
if (!isNaN(port) && port > 0) return resolve();
|
|
69
|
+
} catch {
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
if (Date.now() - start > maxMs) {
|
|
73
|
+
const stderr = getStderr?.() ?? "";
|
|
74
|
+
const hint = stderr ? `
|
|
75
|
+
Autostart stderr: ${stderr.slice(0, 500)}` : "";
|
|
76
|
+
return reject(new DaemonUnavailableError(`Timed out waiting for daemon to start.${hint}`));
|
|
77
|
+
}
|
|
78
|
+
setTimeout(check, 200);
|
|
79
|
+
};
|
|
80
|
+
check();
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
async function ensureDaemon() {
|
|
84
|
+
try {
|
|
85
|
+
const base = getDaemonBaseUrl();
|
|
86
|
+
const res = await fetch(`${base}/health`, {
|
|
87
|
+
signal: AbortSignal.timeout(2e3)
|
|
88
|
+
});
|
|
89
|
+
if (res.ok) return;
|
|
90
|
+
} catch {
|
|
91
|
+
}
|
|
92
|
+
if (process.env["CRONTICK_MCP_NO_AUTOSTART"] === "1") {
|
|
93
|
+
throw new DaemonUnavailableError(
|
|
94
|
+
"CRONTICK_MCP_NO_AUTOSTART=1 is set \u2014 start the daemon manually: crontick daemon start"
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
ensureDirs();
|
|
98
|
+
const script = daemonScript();
|
|
99
|
+
if (!existsSync(script)) {
|
|
100
|
+
throw new DaemonUnavailableError(
|
|
101
|
+
`Daemon script not found. Run: npm run build`
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
const stderrChunks = [];
|
|
105
|
+
const child = spawn(process.execPath, [script], {
|
|
106
|
+
detached: true,
|
|
107
|
+
stdio: ["ignore", "ignore", "pipe"],
|
|
108
|
+
env: process.env
|
|
109
|
+
});
|
|
110
|
+
if (child.stderr) {
|
|
111
|
+
child.stderr.on("data", (chunk) => {
|
|
112
|
+
if (Buffer.concat(stderrChunks).length < 4096) stderrChunks.push(chunk);
|
|
113
|
+
});
|
|
114
|
+
child.stderr.unref?.();
|
|
115
|
+
}
|
|
116
|
+
child.unref();
|
|
117
|
+
const getStderr = () => {
|
|
118
|
+
const raw = Buffer.concat(stderrChunks).toString("utf-8");
|
|
119
|
+
return raw.length > 4096 ? raw.slice(0, 4096) + "\u2026" : raw;
|
|
120
|
+
};
|
|
121
|
+
try {
|
|
122
|
+
await waitForPortFile(1e4, getStderr);
|
|
123
|
+
} catch (err) {
|
|
124
|
+
const stderr = getStderr();
|
|
125
|
+
if (stderr) appendAutostartLog(`ensureDaemon failed:
|
|
126
|
+
${stderr}`);
|
|
127
|
+
throw err;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
async function callDaemon(method, path, body) {
|
|
131
|
+
const base = getDaemonBaseUrl();
|
|
132
|
+
const res = await fetch(`${base}${path}`, {
|
|
133
|
+
method,
|
|
134
|
+
headers: { "Content-Type": "application/json" },
|
|
135
|
+
body: body !== void 0 ? JSON.stringify(body) : void 0,
|
|
136
|
+
signal: AbortSignal.timeout(3e4)
|
|
137
|
+
});
|
|
138
|
+
const text = await res.text();
|
|
139
|
+
let data;
|
|
140
|
+
try {
|
|
141
|
+
data = JSON.parse(text);
|
|
142
|
+
} catch {
|
|
143
|
+
throw new Error(`Daemon returned non-JSON: ${text.slice(0, 200)}`);
|
|
144
|
+
}
|
|
145
|
+
if (!res.ok) {
|
|
146
|
+
const err = data?.error;
|
|
147
|
+
throw new Error(`[${err?.code ?? "API_ERROR"}] ${err?.message ?? `HTTP ${res.status}`}`);
|
|
148
|
+
}
|
|
149
|
+
return data;
|
|
150
|
+
}
|
|
151
|
+
function okResult(data) {
|
|
152
|
+
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
|
153
|
+
}
|
|
154
|
+
function redactForLlm(msg) {
|
|
155
|
+
return msg.replace(/127\.0\.0\.1:\d+/g, "<daemon-addr>").replace(/[A-Za-z]:\\[^\s"']+/g, "<path>").replace(/(^|[\s(["'])\/(?:[^\s"'/]+\/)+[^\s"'/]+/g, "$1<path>");
|
|
156
|
+
}
|
|
157
|
+
function errResult(err) {
|
|
158
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
159
|
+
const redacted = redactForLlm(msg);
|
|
160
|
+
return {
|
|
161
|
+
content: [{ type: "text", text: JSON.stringify({ error: redacted }, null, 2) }],
|
|
162
|
+
isError: true
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
async function toolWrap(fn) {
|
|
166
|
+
try {
|
|
167
|
+
await ensureDaemon();
|
|
168
|
+
const result = await fn();
|
|
169
|
+
return okResult(result);
|
|
170
|
+
} catch (err) {
|
|
171
|
+
return errResult(err);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
var kebabCase = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
175
|
+
function createMcpServer() {
|
|
176
|
+
const server = new McpServer({
|
|
177
|
+
name: "crontick",
|
|
178
|
+
version: VERSION
|
|
179
|
+
});
|
|
180
|
+
server.registerTool(
|
|
181
|
+
"crontick_job_create",
|
|
182
|
+
{
|
|
183
|
+
description: "Create and schedule a new cron job. Provide the full job definition including id, schedule (kind: cron|interval|one-shot), and action (kind: script|exec). Validate the schedule first with crontick_schedule_validate.",
|
|
184
|
+
inputSchema: {
|
|
185
|
+
id: z.string().regex(kebabCase, 'Job ID must be kebab-case (e.g. "my-job")'),
|
|
186
|
+
description: z.string().optional(),
|
|
187
|
+
enabled: z.boolean().optional(),
|
|
188
|
+
schedule: ScheduleSchema,
|
|
189
|
+
action: ActionSchema,
|
|
190
|
+
catchup: z.enum(["run-once", "run-all", "skip"]).optional(),
|
|
191
|
+
overlap: z.enum(["skip", "queue", "cancel-previous"]).optional(),
|
|
192
|
+
retry: z.object({ max: z.number().int().min(0), backoffSec: z.number().positive() }).optional(),
|
|
193
|
+
budgets: z.object({
|
|
194
|
+
maxRunsPerDay: z.number().int().positive().nullable(),
|
|
195
|
+
maxTokensPerRun: z.number().int().positive().nullable()
|
|
196
|
+
}).optional()
|
|
197
|
+
}
|
|
198
|
+
},
|
|
199
|
+
async (args) => toolWrap(() => callDaemon("POST", "/api/jobs", args))
|
|
200
|
+
);
|
|
201
|
+
server.registerTool(
|
|
202
|
+
"crontick_job_list",
|
|
203
|
+
{
|
|
204
|
+
description: "List all scheduled jobs with their current status and next run time.",
|
|
205
|
+
inputSchema: {}
|
|
206
|
+
},
|
|
207
|
+
async () => toolWrap(() => callDaemon("GET", "/api/jobs"))
|
|
208
|
+
);
|
|
209
|
+
server.registerTool(
|
|
210
|
+
"crontick_job_get",
|
|
211
|
+
{
|
|
212
|
+
description: "Get the full definition and status of a specific job by ID.",
|
|
213
|
+
inputSchema: { id: z.string() }
|
|
214
|
+
},
|
|
215
|
+
async (args) => toolWrap(() => callDaemon("GET", `/api/jobs/${encodeURIComponent(args.id)}`))
|
|
216
|
+
);
|
|
217
|
+
server.registerTool(
|
|
218
|
+
"crontick_job_update",
|
|
219
|
+
{
|
|
220
|
+
description: "Update an existing job. Provide the job ID and any fields to change (partial update is merged with existing definition).",
|
|
221
|
+
inputSchema: {
|
|
222
|
+
id: z.string(),
|
|
223
|
+
description: z.string().optional(),
|
|
224
|
+
enabled: z.boolean().optional(),
|
|
225
|
+
schedule: ScheduleSchema.optional(),
|
|
226
|
+
action: ActionSchema.optional(),
|
|
227
|
+
catchup: z.enum(["run-once", "run-all", "skip"]).optional(),
|
|
228
|
+
overlap: z.enum(["skip", "queue", "cancel-previous"]).optional(),
|
|
229
|
+
retry: z.object({ max: z.number().int().min(0), backoffSec: z.number().positive() }).optional(),
|
|
230
|
+
budgets: z.object({
|
|
231
|
+
maxRunsPerDay: z.number().int().positive().nullable(),
|
|
232
|
+
maxTokensPerRun: z.number().int().positive().nullable()
|
|
233
|
+
}).optional()
|
|
234
|
+
}
|
|
235
|
+
},
|
|
236
|
+
async (args) => {
|
|
237
|
+
const { id, ...patch } = args;
|
|
238
|
+
return toolWrap(
|
|
239
|
+
() => callDaemon("PUT", `/api/jobs/${encodeURIComponent(id)}`, patch)
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
);
|
|
243
|
+
server.registerTool(
|
|
244
|
+
"crontick_job_delete",
|
|
245
|
+
{
|
|
246
|
+
description: "Permanently delete a job and all its run history. This cannot be undone \u2014 confirm with the user first.",
|
|
247
|
+
inputSchema: { id: z.string() }
|
|
248
|
+
},
|
|
249
|
+
async (args) => toolWrap(() => callDaemon("DELETE", `/api/jobs/${encodeURIComponent(args.id)}`))
|
|
250
|
+
);
|
|
251
|
+
server.registerTool(
|
|
252
|
+
"crontick_job_enable",
|
|
253
|
+
{
|
|
254
|
+
description: "Enable a disabled job so it will run on its next scheduled time.",
|
|
255
|
+
inputSchema: { id: z.string() }
|
|
256
|
+
},
|
|
257
|
+
async (args) => toolWrap(
|
|
258
|
+
() => callDaemon("POST", `/api/jobs/${encodeURIComponent(args.id)}/enable`)
|
|
259
|
+
)
|
|
260
|
+
);
|
|
261
|
+
server.registerTool(
|
|
262
|
+
"crontick_job_disable",
|
|
263
|
+
{
|
|
264
|
+
description: "Disable a job so it will not run until re-enabled.",
|
|
265
|
+
inputSchema: { id: z.string() }
|
|
266
|
+
},
|
|
267
|
+
async (args) => toolWrap(
|
|
268
|
+
() => callDaemon("POST", `/api/jobs/${encodeURIComponent(args.id)}/disable`)
|
|
269
|
+
)
|
|
270
|
+
);
|
|
271
|
+
server.registerTool(
|
|
272
|
+
"crontick_job_run_now",
|
|
273
|
+
{
|
|
274
|
+
description: "Trigger an immediate run of a job, bypassing its schedule. Returns a runId to track progress with crontick_run_get.",
|
|
275
|
+
inputSchema: { id: z.string() }
|
|
276
|
+
},
|
|
277
|
+
async (args) => toolWrap(
|
|
278
|
+
() => callDaemon("POST", `/api/jobs/${encodeURIComponent(args.id)}/run`)
|
|
279
|
+
)
|
|
280
|
+
);
|
|
281
|
+
server.registerTool(
|
|
282
|
+
"crontick_job_cancel_run",
|
|
283
|
+
{
|
|
284
|
+
description: "Cancel an in-progress run by its run ID.",
|
|
285
|
+
inputSchema: { runId: z.string() }
|
|
286
|
+
},
|
|
287
|
+
async (args) => toolWrap(
|
|
288
|
+
() => callDaemon("POST", `/api/runs/${encodeURIComponent(args.runId)}/cancel`)
|
|
289
|
+
)
|
|
290
|
+
);
|
|
291
|
+
server.registerTool(
|
|
292
|
+
"crontick_run_list",
|
|
293
|
+
{
|
|
294
|
+
description: "List recent runs, optionally filtered by job ID.",
|
|
295
|
+
inputSchema: {
|
|
296
|
+
jobId: z.string().optional(),
|
|
297
|
+
limit: z.number().int().positive().optional(),
|
|
298
|
+
since: z.number().int().optional()
|
|
299
|
+
}
|
|
300
|
+
},
|
|
301
|
+
async (args) => {
|
|
302
|
+
const params = new URLSearchParams();
|
|
303
|
+
if (args.jobId) params.set("jobId", args.jobId);
|
|
304
|
+
if (args.limit !== void 0) params.set("limit", String(args.limit));
|
|
305
|
+
if (args.since !== void 0) params.set("since", String(args.since));
|
|
306
|
+
const qs = params.toString();
|
|
307
|
+
return toolWrap(() => callDaemon("GET", `/api/runs${qs ? `?${qs}` : ""}`));
|
|
308
|
+
}
|
|
309
|
+
);
|
|
310
|
+
server.registerTool(
|
|
311
|
+
"crontick_run_get",
|
|
312
|
+
{
|
|
313
|
+
description: "Get the details and current status of a specific run by run ID.",
|
|
314
|
+
inputSchema: { runId: z.string() }
|
|
315
|
+
},
|
|
316
|
+
async (args) => toolWrap(
|
|
317
|
+
() => callDaemon("GET", `/api/runs/${encodeURIComponent(args.runId)}`)
|
|
318
|
+
)
|
|
319
|
+
);
|
|
320
|
+
server.registerTool(
|
|
321
|
+
"crontick_run_logs_tail",
|
|
322
|
+
{
|
|
323
|
+
description: "Get the last N lines of output for a run. Useful for diagnosing failures.",
|
|
324
|
+
inputSchema: {
|
|
325
|
+
runId: z.string(),
|
|
326
|
+
lines: z.number().int().positive().default(50)
|
|
327
|
+
}
|
|
328
|
+
},
|
|
329
|
+
async (args) => toolWrap(async () => {
|
|
330
|
+
const logs = await callDaemon(
|
|
331
|
+
"GET",
|
|
332
|
+
`/api/runs/${encodeURIComponent(args.runId)}/logs`
|
|
333
|
+
);
|
|
334
|
+
const all = Array.isArray(logs) ? logs : [];
|
|
335
|
+
const tail = all.slice(-args.lines);
|
|
336
|
+
return { runId: args.runId, lines: tail };
|
|
337
|
+
})
|
|
338
|
+
);
|
|
339
|
+
server.registerTool(
|
|
340
|
+
"crontick_schedule_validate",
|
|
341
|
+
{
|
|
342
|
+
description: "Validate a schedule definition. Returns ok:true and human-readable description on success, or an error message on failure. Always call this before creating a job.",
|
|
343
|
+
inputSchema: {
|
|
344
|
+
schedule: ScheduleSchema
|
|
345
|
+
}
|
|
346
|
+
},
|
|
347
|
+
async (args) => toolWrap(() => callDaemon("POST", "/api/schedules/validate", args.schedule))
|
|
348
|
+
);
|
|
349
|
+
server.registerTool(
|
|
350
|
+
"crontick_schedule_preview",
|
|
351
|
+
{
|
|
352
|
+
description: "Preview the next N fire times for a schedule. Useful to confirm the schedule is what the user expects before creating the job.",
|
|
353
|
+
inputSchema: {
|
|
354
|
+
schedule: ScheduleSchema,
|
|
355
|
+
n: z.number().int().positive().max(20).default(5),
|
|
356
|
+
tz: z.string().optional()
|
|
357
|
+
}
|
|
358
|
+
},
|
|
359
|
+
async (args) => toolWrap(
|
|
360
|
+
() => callDaemon("POST", "/api/schedules/preview", {
|
|
361
|
+
schedule: args.schedule,
|
|
362
|
+
n: args.n,
|
|
363
|
+
tz: args.tz
|
|
364
|
+
})
|
|
365
|
+
)
|
|
366
|
+
);
|
|
367
|
+
server.registerTool(
|
|
368
|
+
"crontick_stats_summary",
|
|
369
|
+
{
|
|
370
|
+
description: "Get an aggregate summary of all jobs: total count, enabled count, run history, success/failure counts, average duration.",
|
|
371
|
+
inputSchema: {}
|
|
372
|
+
},
|
|
373
|
+
async () => toolWrap(() => callDaemon("GET", "/api/stats/summary"))
|
|
374
|
+
);
|
|
375
|
+
server.registerTool(
|
|
376
|
+
"crontick_stats_job",
|
|
377
|
+
{
|
|
378
|
+
description: "Get run statistics for a specific job: total runs, success/failure rates, last status.",
|
|
379
|
+
inputSchema: { id: z.string() }
|
|
380
|
+
},
|
|
381
|
+
async (args) => toolWrap(
|
|
382
|
+
() => callDaemon("GET", `/api/stats/jobs/${encodeURIComponent(args.id)}`)
|
|
383
|
+
)
|
|
384
|
+
);
|
|
385
|
+
server.registerTool(
|
|
386
|
+
"crontick_daemon_status",
|
|
387
|
+
{
|
|
388
|
+
description: "Get the daemon process status: PID, version, uptime, job counts, run stats, Node version, and platform.",
|
|
389
|
+
inputSchema: {}
|
|
390
|
+
},
|
|
391
|
+
async () => toolWrap(() => callDaemon("GET", "/health"))
|
|
392
|
+
);
|
|
393
|
+
server.registerTool(
|
|
394
|
+
"crontick_daemon_reload",
|
|
395
|
+
{
|
|
396
|
+
description: "Reload job definitions from disk without restarting the daemon. Use after manually editing job files.",
|
|
397
|
+
inputSchema: {}
|
|
398
|
+
},
|
|
399
|
+
async () => toolWrap(() => callDaemon("POST", "/api/daemon/reload"))
|
|
400
|
+
);
|
|
401
|
+
server.registerTool(
|
|
402
|
+
"crontick_daemon_restart",
|
|
403
|
+
{
|
|
404
|
+
description: "Restart the crontick daemon (stop + start). Running jobs will be interrupted. Confirm with the user before calling.",
|
|
405
|
+
inputSchema: {}
|
|
406
|
+
},
|
|
407
|
+
async () => toolWrap(async () => {
|
|
408
|
+
const pidFile = pidFilePath();
|
|
409
|
+
if (existsSync(pidFile)) {
|
|
410
|
+
try {
|
|
411
|
+
const pid = parseInt(readFileSync(pidFile, "utf-8").trim(), 10);
|
|
412
|
+
if (!isNaN(pid)) process.kill(pid, "SIGTERM");
|
|
413
|
+
} catch {
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
await new Promise((res) => {
|
|
417
|
+
const deadline = Date.now() + 3e3;
|
|
418
|
+
const poll = setInterval(() => {
|
|
419
|
+
if (!existsSync(portFilePath()) || Date.now() > deadline) {
|
|
420
|
+
clearInterval(poll);
|
|
421
|
+
res();
|
|
422
|
+
}
|
|
423
|
+
}, 100);
|
|
424
|
+
});
|
|
425
|
+
const script = daemonScript();
|
|
426
|
+
const restartChunks = [];
|
|
427
|
+
const child = spawn(process.execPath, [script], {
|
|
428
|
+
detached: true,
|
|
429
|
+
stdio: ["ignore", "ignore", "pipe"],
|
|
430
|
+
env: process.env
|
|
431
|
+
});
|
|
432
|
+
if (child.stderr) {
|
|
433
|
+
child.stderr.on("data", (chunk) => {
|
|
434
|
+
if (Buffer.concat(restartChunks).length < 4096) restartChunks.push(chunk);
|
|
435
|
+
});
|
|
436
|
+
child.stderr.unref?.();
|
|
437
|
+
}
|
|
438
|
+
child.unref();
|
|
439
|
+
const getRestartStderr = () => {
|
|
440
|
+
const raw = Buffer.concat(restartChunks).toString("utf-8");
|
|
441
|
+
return raw.length > 4096 ? raw.slice(0, 4096) + "\u2026" : raw;
|
|
442
|
+
};
|
|
443
|
+
try {
|
|
444
|
+
await waitForPortFile(1e4, getRestartStderr);
|
|
445
|
+
} catch (err) {
|
|
446
|
+
const stderr = getRestartStderr();
|
|
447
|
+
if (stderr) appendAutostartLog(`daemon restart failed:
|
|
448
|
+
${stderr}`);
|
|
449
|
+
throw err;
|
|
450
|
+
}
|
|
451
|
+
return { ok: true, message: "Daemon restarted successfully." };
|
|
452
|
+
})
|
|
453
|
+
);
|
|
454
|
+
server.registerTool(
|
|
455
|
+
"crontick_autostart_status",
|
|
456
|
+
{
|
|
457
|
+
description: "Check whether the crontick daemon is registered to start automatically at login. (v1: Windows uses HKCU Run; other platforms return manual instructions.)",
|
|
458
|
+
inputSchema: {}
|
|
459
|
+
},
|
|
460
|
+
async () => toolWrap(() => callDaemon("GET", "/api/autostart/status"))
|
|
461
|
+
);
|
|
462
|
+
server.registerTool(
|
|
463
|
+
"crontick_autostart_install",
|
|
464
|
+
{
|
|
465
|
+
description: "Register the crontick daemon to start automatically at login. (v1: Windows only via HKCU Run + VBS shim. On other platforms returns manual instructions.) On non-Windows, this returns a 501 with instructions to use manual autostart.",
|
|
466
|
+
inputSchema: {}
|
|
467
|
+
},
|
|
468
|
+
async () => toolWrap(() => callDaemon("POST", "/api/autostart/install", {}))
|
|
469
|
+
);
|
|
470
|
+
server.registerTool(
|
|
471
|
+
"crontick_autostart_remove",
|
|
472
|
+
{
|
|
473
|
+
description: "Remove the crontick daemon from the automatic startup registry.",
|
|
474
|
+
inputSchema: {}
|
|
475
|
+
},
|
|
476
|
+
async () => toolWrap(() => callDaemon("POST", "/api/autostart/remove", {}))
|
|
477
|
+
);
|
|
478
|
+
server.registerTool(
|
|
479
|
+
"crontick_export",
|
|
480
|
+
{
|
|
481
|
+
description: "Export all job definitions as a JSON object. Use this to back up or migrate jobs.",
|
|
482
|
+
inputSchema: {}
|
|
483
|
+
},
|
|
484
|
+
async () => toolWrap(() => callDaemon("GET", "/api/export"))
|
|
485
|
+
);
|
|
486
|
+
server.registerTool(
|
|
487
|
+
"crontick_import",
|
|
488
|
+
{
|
|
489
|
+
description: "Import job definitions from a JSON array. Jobs are upserted (existing jobs with the same ID are updated).",
|
|
490
|
+
inputSchema: {
|
|
491
|
+
jobs: z.array(z.unknown())
|
|
492
|
+
}
|
|
493
|
+
},
|
|
494
|
+
async (args) => toolWrap(() => callDaemon("POST", "/api/import", { jobs: args.jobs }))
|
|
495
|
+
);
|
|
496
|
+
server.registerTool(
|
|
497
|
+
"crontick_dashboard_open",
|
|
498
|
+
{
|
|
499
|
+
description: "Get the URL for the crontick dashboard web UI. Open it in a browser to view jobs and run history visually.",
|
|
500
|
+
inputSchema: {}
|
|
501
|
+
},
|
|
502
|
+
async () => toolWrap(async () => {
|
|
503
|
+
const base = getDaemonBaseUrl();
|
|
504
|
+
return {
|
|
505
|
+
url: `${base}/dashboard`,
|
|
506
|
+
message: `Dashboard available at: ${base}/dashboard \u2014 open in your browser.`
|
|
507
|
+
};
|
|
508
|
+
})
|
|
509
|
+
);
|
|
510
|
+
server.registerTool(
|
|
511
|
+
"crontick_doctor",
|
|
512
|
+
{
|
|
513
|
+
description: "Run a suite of health checks: Node.js version, SQLite, data directory, daemon connectivity, dashboard reachability, autostart, and MCP server availability.",
|
|
514
|
+
inputSchema: {}
|
|
515
|
+
},
|
|
516
|
+
async () => {
|
|
517
|
+
const checks = [];
|
|
518
|
+
let daemonReachable = false;
|
|
519
|
+
const major = parseInt(process.versions.node.split(".")[0], 10);
|
|
520
|
+
checks.push({ name: "Node.js >= 22.5", ok: major >= 22, note: `v${process.versions.node}` });
|
|
521
|
+
try {
|
|
522
|
+
const { DatabaseSync } = await import("node:sqlite");
|
|
523
|
+
new DatabaseSync(":memory:").close();
|
|
524
|
+
checks.push({ name: "node:sqlite", ok: true });
|
|
525
|
+
} catch (err) {
|
|
526
|
+
checks.push({ name: "node:sqlite", ok: false, note: String(err) });
|
|
527
|
+
}
|
|
528
|
+
try {
|
|
529
|
+
ensureDirs();
|
|
530
|
+
checks.push({ name: "data dir writable", ok: true });
|
|
531
|
+
} catch (err) {
|
|
532
|
+
checks.push({ name: "data dir writable", ok: false, note: String(err) });
|
|
533
|
+
}
|
|
534
|
+
const portFile = portFilePath();
|
|
535
|
+
const portFileExists = existsSync(portFile);
|
|
536
|
+
checks.push({
|
|
537
|
+
name: "port file readable",
|
|
538
|
+
ok: portFileExists,
|
|
539
|
+
note: portFileExists ? portFile : "not found"
|
|
540
|
+
});
|
|
541
|
+
try {
|
|
542
|
+
const base = getDaemonBaseUrl();
|
|
543
|
+
const res = await fetch(`${base}/health`, { signal: AbortSignal.timeout(2e3) });
|
|
544
|
+
if (res.ok) {
|
|
545
|
+
daemonReachable = true;
|
|
546
|
+
checks.push({ name: "daemon reachable", ok: true });
|
|
547
|
+
} else {
|
|
548
|
+
checks.push({ name: "daemon reachable", ok: false, note: `HTTP ${res.status}` });
|
|
549
|
+
}
|
|
550
|
+
} catch {
|
|
551
|
+
checks.push({ name: "daemon reachable", ok: false, note: "not running" });
|
|
552
|
+
}
|
|
553
|
+
try {
|
|
554
|
+
const base = getDaemonBaseUrl();
|
|
555
|
+
const dashRes = await fetch(`${base}/dashboard`, { signal: AbortSignal.timeout(2e3) });
|
|
556
|
+
const text = await dashRes.text();
|
|
557
|
+
checks.push({
|
|
558
|
+
name: "dashboard reachable",
|
|
559
|
+
ok: dashRes.status === 200 && text.includes("crontick"),
|
|
560
|
+
note: dashRes.status === 200 ? "ok" : `HTTP ${dashRes.status}`
|
|
561
|
+
});
|
|
562
|
+
} catch {
|
|
563
|
+
checks.push({ name: "dashboard reachable", ok: false, note: "daemon not running or no dashboard" });
|
|
564
|
+
}
|
|
565
|
+
if (daemonReachable) {
|
|
566
|
+
try {
|
|
567
|
+
const asResult = await callDaemon("GET", "/api/autostart/status");
|
|
568
|
+
const as = asResult;
|
|
569
|
+
checks.push({
|
|
570
|
+
name: "autostart",
|
|
571
|
+
ok: true,
|
|
572
|
+
note: `backend=${as.backend ?? "?"}, installed=${String(as.installed ?? false)}`
|
|
573
|
+
});
|
|
574
|
+
} catch {
|
|
575
|
+
checks.push({ name: "autostart", ok: false, note: "could not check (daemon not running)" });
|
|
576
|
+
}
|
|
577
|
+
} else {
|
|
578
|
+
checks.push({ name: "autostart", ok: false, note: "could not check (daemon not running)" });
|
|
579
|
+
}
|
|
580
|
+
const mcpScript = pathResolve(__dirname, "../mcp/index.js");
|
|
581
|
+
checks.push({
|
|
582
|
+
name: "MCP server binary",
|
|
583
|
+
ok: existsSync(mcpScript),
|
|
584
|
+
note: mcpScript
|
|
585
|
+
});
|
|
586
|
+
const allOk = checks.every((c) => c.ok);
|
|
587
|
+
return okResult({ ok: allOk, checks });
|
|
588
|
+
}
|
|
589
|
+
);
|
|
590
|
+
server.resource(
|
|
591
|
+
"crontick-jobs-list",
|
|
592
|
+
"crontick://jobs",
|
|
593
|
+
{
|
|
594
|
+
description: "List of all crontick job IDs",
|
|
595
|
+
mimeType: "application/json"
|
|
596
|
+
},
|
|
597
|
+
async () => {
|
|
598
|
+
try {
|
|
599
|
+
await ensureDaemon();
|
|
600
|
+
const jobs = await callDaemon("GET", "/api/jobs");
|
|
601
|
+
const ids = Array.isArray(jobs) ? jobs.map((j) => j.id) : [];
|
|
602
|
+
return {
|
|
603
|
+
contents: [
|
|
604
|
+
{
|
|
605
|
+
uri: "crontick://jobs",
|
|
606
|
+
mimeType: "application/json",
|
|
607
|
+
text: JSON.stringify({ jobIds: ids }, null, 2)
|
|
608
|
+
}
|
|
609
|
+
]
|
|
610
|
+
};
|
|
611
|
+
} catch (err) {
|
|
612
|
+
return {
|
|
613
|
+
contents: [
|
|
614
|
+
{
|
|
615
|
+
uri: "crontick://jobs",
|
|
616
|
+
mimeType: "application/json",
|
|
617
|
+
text: JSON.stringify({ error: String(err) }, null, 2)
|
|
618
|
+
}
|
|
619
|
+
]
|
|
620
|
+
};
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
);
|
|
624
|
+
const jobTemplate = new ResourceTemplate("crontick://jobs/{id}", {
|
|
625
|
+
list: async () => ({
|
|
626
|
+
resources: []
|
|
627
|
+
})
|
|
628
|
+
});
|
|
629
|
+
server.resource(
|
|
630
|
+
"crontick-job",
|
|
631
|
+
jobTemplate,
|
|
632
|
+
{ description: "Full job definition as JSON", mimeType: "application/json" },
|
|
633
|
+
async (uri, variables) => {
|
|
634
|
+
const id = Array.isArray(variables.id) ? variables.id[0] : variables.id;
|
|
635
|
+
try {
|
|
636
|
+
await ensureDaemon();
|
|
637
|
+
const job = await callDaemon("GET", `/api/jobs/${encodeURIComponent(String(id ?? ""))}`);
|
|
638
|
+
return {
|
|
639
|
+
contents: [
|
|
640
|
+
{
|
|
641
|
+
uri: uri.href,
|
|
642
|
+
mimeType: "application/json",
|
|
643
|
+
text: JSON.stringify(job, null, 2)
|
|
644
|
+
}
|
|
645
|
+
]
|
|
646
|
+
};
|
|
647
|
+
} catch (err) {
|
|
648
|
+
return {
|
|
649
|
+
contents: [
|
|
650
|
+
{
|
|
651
|
+
uri: uri.href,
|
|
652
|
+
mimeType: "application/json",
|
|
653
|
+
text: JSON.stringify({ error: String(err) }, null, 2)
|
|
654
|
+
}
|
|
655
|
+
]
|
|
656
|
+
};
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
);
|
|
660
|
+
const runTemplate = new ResourceTemplate("crontick://runs/{id}", {
|
|
661
|
+
list: async () => ({ resources: [] })
|
|
662
|
+
});
|
|
663
|
+
server.resource(
|
|
664
|
+
"crontick-run",
|
|
665
|
+
runTemplate,
|
|
666
|
+
{ description: "Run record as JSON", mimeType: "application/json" },
|
|
667
|
+
async (uri, variables) => {
|
|
668
|
+
const id = Array.isArray(variables.id) ? variables.id[0] : variables.id;
|
|
669
|
+
try {
|
|
670
|
+
await ensureDaemon();
|
|
671
|
+
const run = await callDaemon("GET", `/api/runs/${encodeURIComponent(String(id ?? ""))}`);
|
|
672
|
+
return {
|
|
673
|
+
contents: [
|
|
674
|
+
{
|
|
675
|
+
uri: uri.href,
|
|
676
|
+
mimeType: "application/json",
|
|
677
|
+
text: JSON.stringify(run, null, 2)
|
|
678
|
+
}
|
|
679
|
+
]
|
|
680
|
+
};
|
|
681
|
+
} catch (err) {
|
|
682
|
+
return {
|
|
683
|
+
contents: [
|
|
684
|
+
{
|
|
685
|
+
uri: uri.href,
|
|
686
|
+
mimeType: "application/json",
|
|
687
|
+
text: JSON.stringify({ error: String(err) }, null, 2)
|
|
688
|
+
}
|
|
689
|
+
]
|
|
690
|
+
};
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
);
|
|
694
|
+
const runLogTemplate = new ResourceTemplate("crontick://runs/{id}/log", {
|
|
695
|
+
list: async () => ({ resources: [] })
|
|
696
|
+
});
|
|
697
|
+
server.resource(
|
|
698
|
+
"crontick-run-log",
|
|
699
|
+
runLogTemplate,
|
|
700
|
+
{ description: "Full log output for a run as plain text", mimeType: "text/plain" },
|
|
701
|
+
async (uri, variables) => {
|
|
702
|
+
const id = Array.isArray(variables.id) ? variables.id[0] : variables.id;
|
|
703
|
+
try {
|
|
704
|
+
await ensureDaemon();
|
|
705
|
+
const logs = await callDaemon(
|
|
706
|
+
"GET",
|
|
707
|
+
`/api/runs/${encodeURIComponent(String(id ?? ""))}/logs`
|
|
708
|
+
);
|
|
709
|
+
const text = Array.isArray(logs) ? logs.map((l) => `[${l.stream}] ${l.data}`).join("") : "";
|
|
710
|
+
return {
|
|
711
|
+
contents: [{ uri: uri.href, mimeType: "text/plain", text }]
|
|
712
|
+
};
|
|
713
|
+
} catch (err) {
|
|
714
|
+
return {
|
|
715
|
+
contents: [{ uri: uri.href, mimeType: "text/plain", text: `Error: ${String(err)}` }]
|
|
716
|
+
};
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
);
|
|
720
|
+
server.resource(
|
|
721
|
+
"crontick-schema-job",
|
|
722
|
+
"crontick://schemas/job",
|
|
723
|
+
{ description: "JSON Schema for a crontick job definition", mimeType: "application/json" },
|
|
724
|
+
async () => {
|
|
725
|
+
const schema = zodToJsonSchema(JobSchema, { name: "CrontickJob" });
|
|
726
|
+
return {
|
|
727
|
+
contents: [
|
|
728
|
+
{
|
|
729
|
+
uri: "crontick://schemas/job",
|
|
730
|
+
mimeType: "application/json",
|
|
731
|
+
text: JSON.stringify(schema, null, 2)
|
|
732
|
+
}
|
|
733
|
+
]
|
|
734
|
+
};
|
|
735
|
+
}
|
|
736
|
+
);
|
|
737
|
+
server.prompt(
|
|
738
|
+
"create-scheduled-script",
|
|
739
|
+
"Guide the LLM through creating a new scheduled script job: understand intent, draft a self-contained shell script, validate/preview the schedule, then create the job.",
|
|
740
|
+
{
|
|
741
|
+
intent: z.string(),
|
|
742
|
+
os: z.enum(["windows", "unix"]).optional()
|
|
743
|
+
},
|
|
744
|
+
(args) => {
|
|
745
|
+
const shell = args.os === "windows" ? "PowerShell" : "bash";
|
|
746
|
+
const shellHint = args.os === "windows" ? "pwsh" : "bash";
|
|
747
|
+
return {
|
|
748
|
+
description: "Create a new scheduled script job",
|
|
749
|
+
messages: [
|
|
750
|
+
{
|
|
751
|
+
role: "user",
|
|
752
|
+
content: {
|
|
753
|
+
type: "text",
|
|
754
|
+
text: `You are helping me schedule an automated task with crontick.
|
|
755
|
+
|
|
756
|
+
My intent: ${args.intent}
|
|
757
|
+
|
|
758
|
+
Please follow these steps in order:
|
|
759
|
+
|
|
760
|
+
**Step 1 \u2014 Understand the intent**
|
|
761
|
+
Clarify what the task does, when it should run, and any side effects or requirements.
|
|
762
|
+
|
|
763
|
+
**Step 2 \u2014 Draft the script**
|
|
764
|
+
Write a self-contained ${shell} script that accomplishes the task idempotently.
|
|
765
|
+
- The script must not rely on external state set up by other scripts.
|
|
766
|
+
- If it calls an LLM CLI (e.g. \`copilot\`, \`claude\`), that command goes inside the script.
|
|
767
|
+
- Use error handling: \`set -euo pipefail\` for bash, \`$ErrorActionPreference = 'Stop'\` for ${shell}.
|
|
768
|
+
|
|
769
|
+
**Step 3 \u2014 Choose a schedule**
|
|
770
|
+
Decide on the cron expression or interval. Then call:
|
|
771
|
+
- \`crontick_schedule_validate\` with \`schedule: { kind: "cron", cron: "<expr>", tz: "<tz>" }\`
|
|
772
|
+
- \`crontick_schedule_preview\` to show the next 5 fire times to the user for confirmation.
|
|
773
|
+
|
|
774
|
+
**Step 4 \u2014 Create the job**
|
|
775
|
+
Once the user approves the schedule, call \`crontick_job_create\` with \`action.kind: "script"\`:
|
|
776
|
+
\`\`\`json
|
|
777
|
+
{
|
|
778
|
+
"id": "<kebab-case-id>",
|
|
779
|
+
"description": "<one-line description>",
|
|
780
|
+
"schedule": { "kind": "cron", "cron": "<expr>", "tz": "<tz>" },
|
|
781
|
+
"action": {
|
|
782
|
+
"kind": "script",
|
|
783
|
+
"script": "<full script body>",
|
|
784
|
+
"shell": "${shellHint}"
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
\`\`\`
|
|
788
|
+
|
|
789
|
+
**Step 5 \u2014 Confirm**
|
|
790
|
+
Report the returned job ID and next run time to the user.
|
|
791
|
+
Always confirm before calling crontick_job_delete or crontick_job_disable.`
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
]
|
|
795
|
+
};
|
|
796
|
+
}
|
|
797
|
+
);
|
|
798
|
+
server.prompt(
|
|
799
|
+
"investigate-failed-run",
|
|
800
|
+
"Load a failed run record and its logs, then help diagnose the failure and propose a fix.",
|
|
801
|
+
{
|
|
802
|
+
runId: z.string()
|
|
803
|
+
},
|
|
804
|
+
async (args) => {
|
|
805
|
+
let runInfo = "Run record unavailable.";
|
|
806
|
+
let logInfo = "Logs unavailable.";
|
|
807
|
+
try {
|
|
808
|
+
await ensureDaemon();
|
|
809
|
+
const run = await callDaemon("GET", `/api/runs/${encodeURIComponent(args.runId)}`);
|
|
810
|
+
runInfo = JSON.stringify(run, null, 2);
|
|
811
|
+
} catch (err) {
|
|
812
|
+
runInfo = `Error fetching run: ${String(err)}`;
|
|
813
|
+
}
|
|
814
|
+
try {
|
|
815
|
+
const logs = await callDaemon(
|
|
816
|
+
"GET",
|
|
817
|
+
`/api/runs/${encodeURIComponent(args.runId)}/logs`
|
|
818
|
+
);
|
|
819
|
+
const lines = Array.isArray(logs) ? logs.slice(-100) : [];
|
|
820
|
+
logInfo = lines.length > 0 ? lines.map((l) => `[${l.stream}] ${l.data}`).join("") : "(no log output)";
|
|
821
|
+
} catch (err) {
|
|
822
|
+
logInfo = `Error fetching logs: ${String(err)}`;
|
|
823
|
+
}
|
|
824
|
+
return {
|
|
825
|
+
description: `Investigate failed run ${args.runId}`,
|
|
826
|
+
messages: [
|
|
827
|
+
{
|
|
828
|
+
role: "user",
|
|
829
|
+
content: {
|
|
830
|
+
type: "text",
|
|
831
|
+
text: `Please investigate why run \`${args.runId}\` failed and propose a fix.
|
|
832
|
+
|
|
833
|
+
## Run Record
|
|
834
|
+
\`\`\`json
|
|
835
|
+
${runInfo}
|
|
836
|
+
\`\`\`
|
|
837
|
+
|
|
838
|
+
## Last 100 Log Lines
|
|
839
|
+
\`\`\`
|
|
840
|
+
${logInfo}
|
|
841
|
+
\`\`\`
|
|
842
|
+
|
|
843
|
+
## What to diagnose
|
|
844
|
+
1. What caused the failure? (exit code, timeout, budget exceeded, script error, etc.)
|
|
845
|
+
2. Is this a one-time fluke or likely to recur?
|
|
846
|
+
3. Proposed fix \u2014 choose the most appropriate:
|
|
847
|
+
- **Script fix**: edit the script body via \`crontick_job_update\` with a corrected \`action.script\`
|
|
848
|
+
- **Schedule change**: adjust timing/tz via \`crontick_job_update\` with a new \`schedule\`
|
|
849
|
+
- **Retry policy**: increase retry max via \`crontick_job_update\` with \`retry.max\`
|
|
850
|
+
- **Budget cap**: set \`budgets.maxRunsPerDay\` if it's running too often
|
|
851
|
+
- **Timeout**: increase \`action.timeoutSec\` if the job was killed by timeout
|
|
852
|
+
4. After proposing the fix, ask for user confirmation before applying it.`
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
]
|
|
856
|
+
};
|
|
857
|
+
}
|
|
858
|
+
);
|
|
859
|
+
return server;
|
|
860
|
+
}
|
|
861
|
+
async function main() {
|
|
862
|
+
const server = createMcpServer();
|
|
863
|
+
const transport = new StdioServerTransport();
|
|
864
|
+
await server.connect(transport);
|
|
865
|
+
}
|
|
866
|
+
main().catch((err) => {
|
|
867
|
+
process.stderr.write(`[crontick-mcp] Fatal: ${err instanceof Error ? err.message : String(err)}
|
|
868
|
+
`);
|
|
869
|
+
process.exit(1);
|
|
870
|
+
});
|
|
871
|
+
export {
|
|
872
|
+
createMcpServer,
|
|
873
|
+
main,
|
|
874
|
+
redactForLlm
|
|
875
|
+
};
|
|
876
|
+
//# sourceMappingURL=index.js.map
|