@cronwatch/sdk 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.
Files changed (46) hide show
  1. package/README.md +69 -0
  2. package/dist/anthropic.cjs +84 -0
  3. package/dist/anthropic.cjs.map +1 -0
  4. package/dist/anthropic.d.cts +35 -0
  5. package/dist/anthropic.d.ts +35 -0
  6. package/dist/anthropic.js +78 -0
  7. package/dist/anthropic.js.map +1 -0
  8. package/dist/discord.cjs +42 -0
  9. package/dist/discord.cjs.map +1 -0
  10. package/dist/discord.d.cts +11 -0
  11. package/dist/discord.d.ts +11 -0
  12. package/dist/discord.js +40 -0
  13. package/dist/discord.js.map +1 -0
  14. package/dist/index.cjs +1317 -0
  15. package/dist/index.cjs.map +1 -0
  16. package/dist/index.d.cts +219 -0
  17. package/dist/index.d.ts +219 -0
  18. package/dist/index.js +1302 -0
  19. package/dist/index.js.map +1 -0
  20. package/dist/postgres.cjs +142 -0
  21. package/dist/postgres.cjs.map +1 -0
  22. package/dist/postgres.d.cts +19 -0
  23. package/dist/postgres.d.ts +19 -0
  24. package/dist/postgres.js +136 -0
  25. package/dist/postgres.js.map +1 -0
  26. package/dist/slack.cjs +44 -0
  27. package/dist/slack.cjs.map +1 -0
  28. package/dist/slack.d.cts +12 -0
  29. package/dist/slack.d.ts +12 -0
  30. package/dist/slack.js +42 -0
  31. package/dist/slack.js.map +1 -0
  32. package/dist/sqlite.cjs +151 -0
  33. package/dist/sqlite.cjs.map +1 -0
  34. package/dist/sqlite.d.cts +17 -0
  35. package/dist/sqlite.d.ts +17 -0
  36. package/dist/sqlite.js +144 -0
  37. package/dist/sqlite.js.map +1 -0
  38. package/dist/types-BQ1P8z55.d.cts +159 -0
  39. package/dist/types-BQ1P8z55.d.ts +159 -0
  40. package/dist/webhook.cjs +24 -0
  41. package/dist/webhook.cjs.map +1 -0
  42. package/dist/webhook.d.cts +20 -0
  43. package/dist/webhook.d.ts +20 -0
  44. package/dist/webhook.js +22 -0
  45. package/dist/webhook.js.map +1 -0
  46. package/package.json +114 -0
package/README.md ADDED
@@ -0,0 +1,69 @@
1
+ # @cronwatch/sdk
2
+
3
+ Cron and scheduled-job monitoring that lives inside your app. Wrap a job once; every run is recorded in a database you already have, and you are told when a run is missed, fails, gets stuck, runs slow or goes over budget. No server to run, no account to make.
4
+
5
+ Docs: [cronwatch.dev/docs](https://cronwatch.dev/docs/)
6
+
7
+ ```bash
8
+ npm install @cronwatch/sdk
9
+ npm install better-sqlite3 # or pg
10
+ ```
11
+
12
+ ```ts
13
+ import { cronwatch } from "@cronwatch/sdk";
14
+ import { sqlite } from "@cronwatch/sdk/sqlite";
15
+ import { slack } from "@cronwatch/sdk/slack";
16
+
17
+ export const cw = cronwatch({
18
+ store: sqlite({ path: "./data/cronwatch.db" }),
19
+ alerts: [slack({ webhookUrl: process.env.SLACK_WEBHOOK_URL! })],
20
+ });
21
+
22
+ export const nightlyReport = cw.job("nightly-report", {
23
+ schedule: "0 2 * * *", // cron, "@hourly", or "every 15m"
24
+ timezone: "UTC",
25
+ grace: "15m", // how late a start may be before it is missed
26
+ timeout: "30m", // a run still going after this is stuck
27
+ expect: "Report written", // output must contain this, or the run failed
28
+ budget: { cost: 2 }, // cost above 2 is over budget
29
+ });
30
+
31
+ // A fetch-style handler (Next.js route, Hono, Bun): checks Authorization: Bearer CRON_SECRET
32
+ export const GET = nightlyReport.handler(async (job) => {
33
+ const report = await buildReport();
34
+ job.log("Report written:", report.path);
35
+ job.metric("cost", report.usdCost);
36
+ });
37
+
38
+ // Or a plain function
39
+ await nightlyReport.run(async (job) => { /* ... */ });
40
+
41
+ // Dashboard and JSON API, behind CRONWATCH_TOKEN
42
+ export const { GET, POST, DELETE } = cw.routes();
43
+
44
+ // Missed and stuck runs are found by the check: on an interval, or a cron hitting /cronwatch/api/check
45
+ cw.start();
46
+ ```
47
+
48
+ ## What it catches
49
+
50
+ - **missed**: the schedule said a run was due and none started within the grace period
51
+ - **failed**: the function threw, the handler returned 4xx or 5xx, or the output did not satisfy `expect`
52
+ - **stuck**: a run started and never reported finishing within `timeout`
53
+ - **slow**: a successful run took longer than `maxDuration`, or twice the job's recent p95
54
+ - **over_budget**: a metric went above its `budget` ceiling, or three times its usual median
55
+ - **recovered**: a run succeeded after any of the above
56
+
57
+ Each condition alerts once when it opens and once when it clears.
58
+
59
+ ## Entry points
60
+
61
+ | Import | |
62
+ |---|---|
63
+ | `@cronwatch/sdk` | `cronwatch`, `memory`, `custom`, `consoleChannel`, `createRoutes`, types |
64
+ | `@cronwatch/sdk/sqlite` | `sqlite({ path })`, needs `better-sqlite3` |
65
+ | `@cronwatch/sdk/postgres` | `postgres({ connectionString })`, needs `pg` |
66
+ | `@cronwatch/sdk/slack`, `/discord`, `/webhook` | alert channels |
67
+ | `@cronwatch/sdk/anthropic` | `anthropic()` triage: a short diagnosis on every failure alert, needs `@anthropic-ai/sdk` |
68
+
69
+ Node 20 or newer; `better-sqlite3` 13 needs Node 22. MIT.
@@ -0,0 +1,84 @@
1
+ 'use strict';
2
+
3
+ var Anthropic = require('@anthropic-ai/sdk');
4
+
5
+ function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
6
+
7
+ var Anthropic__default = /*#__PURE__*/_interopDefault(Anthropic);
8
+
9
+ // src/triage/anthropic.ts
10
+
11
+ // src/duration.ts
12
+ function formatDuration(ms) {
13
+ if (!Number.isFinite(ms)) return "?";
14
+ if (ms < 1e3) return `${Math.round(ms)}ms`;
15
+ const parts = [];
16
+ let rest = Math.round(ms / 1e3);
17
+ const units = [["d", 86400], ["h", 3600], ["m", 60], ["s", 1]];
18
+ for (const [unit, size] of units) {
19
+ if (rest >= size) {
20
+ const n = Math.floor(rest / size);
21
+ rest -= n * size;
22
+ parts.push(`${n}${unit}`);
23
+ }
24
+ if (parts.length === 2) break;
25
+ }
26
+ return parts.join(" ") || "0s";
27
+ }
28
+
29
+ // src/triage/anthropic.ts
30
+ var SYSTEM = `You help an engineer understand why a scheduled job misbehaved. You are given the alert, the job's definition, the run that triggered it and a few earlier runs.
31
+
32
+ Reply with two to four sentences of plain prose: the most likely cause, and the first concrete thing to check or change. Be specific to the evidence given; if the evidence is thin, say what is missing rather than guessing. No headings, no lists, no preamble, no restating the error verbatim.`;
33
+ function describe(ctx) {
34
+ const { alert, recentRuns } = ctx;
35
+ const run = alert.run;
36
+ const lines = [];
37
+ lines.push(`Alert: ${alert.type}. ${alert.title}`);
38
+ lines.push(alert.message);
39
+ lines.push("");
40
+ lines.push(`Job definition: ${JSON.stringify(alert.definition)}`);
41
+ if (run) {
42
+ lines.push("");
43
+ lines.push(`Triggering run: status ${run.status}, started ${new Date(run.startedAt).toISOString()}, duration ${run.durationMs === null ? "unknown" : formatDuration(run.durationMs)}, trigger ${run.trigger}`);
44
+ if (Object.keys(run.metrics).length) lines.push(`Metrics: ${JSON.stringify(run.metrics)}`);
45
+ if (run.error) lines.push(`Error:
46
+ ${run.error.slice(0, 3e3)}`);
47
+ if (run.output) lines.push(`Output (tail):
48
+ ${run.output.slice(-3e3)}`);
49
+ }
50
+ const earlier = recentRuns.filter((r) => r.id !== run?.id).slice(0, 5);
51
+ if (earlier.length) {
52
+ lines.push("");
53
+ lines.push("Earlier runs, newest first:");
54
+ for (const r of earlier) {
55
+ lines.push(`- ${r.status}, ${new Date(r.startedAt).toISOString()}, ${r.durationMs === null ? "unknown" : formatDuration(r.durationMs)}${r.error ? `, error: ${r.error.split("\n")[0].slice(0, 160)}` : ""}${Object.keys(r.metrics).length ? `, metrics ${JSON.stringify(r.metrics)}` : ""}`);
56
+ }
57
+ }
58
+ return lines.join("\n");
59
+ }
60
+ function anthropic(options = {}) {
61
+ const client = options.client ?? new Anthropic__default.default(options.apiKey ? { apiKey: options.apiKey } : {});
62
+ const model = options.model ?? "claude-opus-5";
63
+ const useFallbacks = options.fallbacks ?? true;
64
+ return async (ctx) => {
65
+ const user = (options.context ? `About this app: ${options.context}
66
+
67
+ ` : "") + describe(ctx);
68
+ const response = await client.beta.messages.create({
69
+ model,
70
+ max_tokens: options.maxTokens ?? 800,
71
+ system: SYSTEM,
72
+ output_config: { effort: options.effort ?? "medium" },
73
+ messages: [{ role: "user", content: user }],
74
+ ...useFallbacks ? { betas: ["server-side-fallback-2026-07-01"], fallbacks: "default" } : {}
75
+ });
76
+ if (response.stop_reason === "refusal") return null;
77
+ const text = response.content.filter((block) => block.type === "text").map((block) => block.text).join("\n").trim();
78
+ return text || null;
79
+ };
80
+ }
81
+
82
+ exports.anthropic = anthropic;
83
+ //# sourceMappingURL=anthropic.cjs.map
84
+ //# sourceMappingURL=anthropic.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/duration.ts","../src/triage/anthropic.ts"],"names":["Anthropic"],"mappings":"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","file":"anthropic.cjs","sourcesContent":["import type { Duration } from \"./types.js\";\n\nconst UNIT_MS: Record<string, number> = {\n ms: 1,\n s: 1000,\n m: 60_000,\n h: 3_600_000,\n d: 86_400_000,\n w: 604_800_000,\n};\n\n/**\n * \"15m\" -> 900000. Accepts a plain number of milliseconds, and compound\n * strings such as \"1h30m\". Whitespace between parts is fine.\n */\nexport function parseDuration(value: Duration, label = \"duration\"): number {\n if (typeof value === \"number\") {\n if (!Number.isFinite(value) || value < 0) throw new Error(`${label} must be a non-negative number of milliseconds`);\n return value;\n }\n const text = value.trim().toLowerCase();\n if (text === \"\") throw new Error(`${label} is empty`);\n const re = /(\\d+(?:\\.\\d+)?)\\s*(ms|s|m|h|d|w)/g;\n let total = 0;\n let consumed = \"\";\n let match: RegExpExecArray | null;\n while ((match = re.exec(text)) !== null) {\n total += parseFloat(match[1]!) * UNIT_MS[match[2]!]!;\n consumed += match[0];\n }\n if (consumed.replace(/\\s+/g, \"\") !== text.replace(/\\s+/g, \"\")) {\n throw new Error(`${label} \"${value}\" is not a duration like \"15m\", \"1h30m\" or \"90s\"`);\n }\n return Math.round(total);\n}\n\n/** 90000 -> \"1m 30s\". For messages, not for parsing back. */\nexport function formatDuration(ms: number): string {\n if (!Number.isFinite(ms)) return \"?\";\n if (ms < 1000) return `${Math.round(ms)}ms`;\n const parts: string[] = [];\n let rest = Math.round(ms / 1000);\n const units: [string, number][] = [[\"d\", 86_400], [\"h\", 3_600], [\"m\", 60], [\"s\", 1]];\n for (const [unit, size] of units) {\n if (rest >= size) {\n const n = Math.floor(rest / size);\n rest -= n * size;\n parts.push(`${n}${unit}`);\n }\n if (parts.length === 2) break;\n }\n return parts.join(\" \") || \"0s\";\n}\n\n/** \"5 minutes ago\", \"in 2h\". Relative to `now`. */\nexport function formatRelative(at: number, now: number): string {\n const diff = at - now;\n const abs = Math.abs(diff);\n const text = abs < 5_000 ? \"now\" : formatDuration(abs);\n if (abs < 5_000) return text;\n return diff < 0 ? `${text} ago` : `in ${text}`;\n}\n","import Anthropic from \"@anthropic-ai/sdk\";\nimport { formatDuration } from \"../duration.js\";\nimport type { TriageContext, TriageFn } from \"../types.js\";\n\nexport interface AnthropicTriageOptions {\n /** Defaults to what the Anthropic SDK resolves: ANTHROPIC_API_KEY, or an `ant auth login` profile. */\n apiKey?: string;\n /** Bring a configured client instead. */\n client?: Anthropic;\n /** Default \"claude-opus-5\". */\n model?: string;\n /** How hard the model thinks. Default \"medium\"; a stack trace rarely needs more. */\n effort?: \"low\" | \"medium\" | \"high\";\n /** Default 800. A diagnosis is a paragraph. */\n maxTokens?: number;\n /**\n * Route a policy refusal to Anthropic's default fallback model inside the\n * same request, so a diagnosis still comes back. On by default; turn off if\n * your account or gateway rejects the beta.\n */\n fallbacks?: boolean;\n /** Anything the model should know about this app: \"A Next.js app on Vercel with a Neon database.\" */\n context?: string;\n}\n\nconst SYSTEM = `You help an engineer understand why a scheduled job misbehaved. You are given the alert, the job's definition, the run that triggered it and a few earlier runs.\n\nReply with two to four sentences of plain prose: the most likely cause, and the first concrete thing to check or change. Be specific to the evidence given; if the evidence is thin, say what is missing rather than guessing. No headings, no lists, no preamble, no restating the error verbatim.`;\n\nfunction describe(ctx: TriageContext): string {\n const { alert, recentRuns } = ctx;\n const run = alert.run;\n const lines: string[] = [];\n lines.push(`Alert: ${alert.type}. ${alert.title}`);\n lines.push(alert.message);\n lines.push(\"\");\n lines.push(`Job definition: ${JSON.stringify(alert.definition)}`);\n if (run) {\n lines.push(\"\");\n lines.push(`Triggering run: status ${run.status}, started ${new Date(run.startedAt).toISOString()}, duration ${run.durationMs === null ? \"unknown\" : formatDuration(run.durationMs)}, trigger ${run.trigger}`);\n if (Object.keys(run.metrics).length) lines.push(`Metrics: ${JSON.stringify(run.metrics)}`);\n if (run.error) lines.push(`Error:\\n${run.error.slice(0, 3000)}`);\n if (run.output) lines.push(`Output (tail):\\n${run.output.slice(-3000)}`);\n }\n const earlier = recentRuns.filter((r) => r.id !== run?.id).slice(0, 5);\n if (earlier.length) {\n lines.push(\"\");\n lines.push(\"Earlier runs, newest first:\");\n for (const r of earlier) {\n lines.push(`- ${r.status}, ${new Date(r.startedAt).toISOString()}, ${r.durationMs === null ? \"unknown\" : formatDuration(r.durationMs)}${r.error ? `, error: ${r.error.split(\"\\n\")[0]!.slice(0, 160)}` : \"\"}${Object.keys(r.metrics).length ? `, metrics ${JSON.stringify(r.metrics)}` : \"\"}`);\n }\n }\n return lines.join(\"\\n\");\n}\n\n/**\n * A triage function backed by Claude. Pass it as `triage` to cronwatch():\n *\n * cronwatch({ triage: anthropic({ context: \"A Next.js app on Vercel.\" }) })\n *\n * It runs only when an alert is sent (never per run), so cost is bounded by\n * how often things go wrong, and it never blocks an alert: the client gives\n * it 25 seconds and moves on without a diagnosis if it takes longer.\n */\nexport function anthropic(options: AnthropicTriageOptions = {}): TriageFn {\n const client = options.client ?? new Anthropic(options.apiKey ? { apiKey: options.apiKey } : {});\n const model = options.model ?? \"claude-opus-5\";\n const useFallbacks = options.fallbacks ?? true;\n\n return async (ctx) => {\n const user = (options.context ? `About this app: ${options.context}\\n\\n` : \"\") + describe(ctx);\n const response = await client.beta.messages.create({\n model,\n max_tokens: options.maxTokens ?? 800,\n system: SYSTEM,\n output_config: { effort: options.effort ?? \"medium\" },\n messages: [{ role: \"user\", content: user }],\n ...(useFallbacks ? { betas: [\"server-side-fallback-2026-07-01\"], fallbacks: \"default\" } : {}),\n });\n if (response.stop_reason === \"refusal\") return null;\n const text = response.content\n .filter((block): block is Anthropic.Beta.BetaTextBlock => block.type === \"text\")\n .map((block) => block.text)\n .join(\"\\n\")\n .trim();\n return text || null;\n };\n}\n"]}
@@ -0,0 +1,35 @@
1
+ import Anthropic from '@anthropic-ai/sdk';
2
+ import { T as TriageFn } from './types-BQ1P8z55.cjs';
3
+
4
+ interface AnthropicTriageOptions {
5
+ /** Defaults to what the Anthropic SDK resolves: ANTHROPIC_API_KEY, or an `ant auth login` profile. */
6
+ apiKey?: string;
7
+ /** Bring a configured client instead. */
8
+ client?: Anthropic;
9
+ /** Default "claude-opus-5". */
10
+ model?: string;
11
+ /** How hard the model thinks. Default "medium"; a stack trace rarely needs more. */
12
+ effort?: "low" | "medium" | "high";
13
+ /** Default 800. A diagnosis is a paragraph. */
14
+ maxTokens?: number;
15
+ /**
16
+ * Route a policy refusal to Anthropic's default fallback model inside the
17
+ * same request, so a diagnosis still comes back. On by default; turn off if
18
+ * your account or gateway rejects the beta.
19
+ */
20
+ fallbacks?: boolean;
21
+ /** Anything the model should know about this app: "A Next.js app on Vercel with a Neon database." */
22
+ context?: string;
23
+ }
24
+ /**
25
+ * A triage function backed by Claude. Pass it as `triage` to cronwatch():
26
+ *
27
+ * cronwatch({ triage: anthropic({ context: "A Next.js app on Vercel." }) })
28
+ *
29
+ * It runs only when an alert is sent (never per run), so cost is bounded by
30
+ * how often things go wrong, and it never blocks an alert: the client gives
31
+ * it 25 seconds and moves on without a diagnosis if it takes longer.
32
+ */
33
+ declare function anthropic(options?: AnthropicTriageOptions): TriageFn;
34
+
35
+ export { type AnthropicTriageOptions, anthropic };
@@ -0,0 +1,35 @@
1
+ import Anthropic from '@anthropic-ai/sdk';
2
+ import { T as TriageFn } from './types-BQ1P8z55.js';
3
+
4
+ interface AnthropicTriageOptions {
5
+ /** Defaults to what the Anthropic SDK resolves: ANTHROPIC_API_KEY, or an `ant auth login` profile. */
6
+ apiKey?: string;
7
+ /** Bring a configured client instead. */
8
+ client?: Anthropic;
9
+ /** Default "claude-opus-5". */
10
+ model?: string;
11
+ /** How hard the model thinks. Default "medium"; a stack trace rarely needs more. */
12
+ effort?: "low" | "medium" | "high";
13
+ /** Default 800. A diagnosis is a paragraph. */
14
+ maxTokens?: number;
15
+ /**
16
+ * Route a policy refusal to Anthropic's default fallback model inside the
17
+ * same request, so a diagnosis still comes back. On by default; turn off if
18
+ * your account or gateway rejects the beta.
19
+ */
20
+ fallbacks?: boolean;
21
+ /** Anything the model should know about this app: "A Next.js app on Vercel with a Neon database." */
22
+ context?: string;
23
+ }
24
+ /**
25
+ * A triage function backed by Claude. Pass it as `triage` to cronwatch():
26
+ *
27
+ * cronwatch({ triage: anthropic({ context: "A Next.js app on Vercel." }) })
28
+ *
29
+ * It runs only when an alert is sent (never per run), so cost is bounded by
30
+ * how often things go wrong, and it never blocks an alert: the client gives
31
+ * it 25 seconds and moves on without a diagnosis if it takes longer.
32
+ */
33
+ declare function anthropic(options?: AnthropicTriageOptions): TriageFn;
34
+
35
+ export { type AnthropicTriageOptions, anthropic };
@@ -0,0 +1,78 @@
1
+ import Anthropic from '@anthropic-ai/sdk';
2
+
3
+ // src/triage/anthropic.ts
4
+
5
+ // src/duration.ts
6
+ function formatDuration(ms) {
7
+ if (!Number.isFinite(ms)) return "?";
8
+ if (ms < 1e3) return `${Math.round(ms)}ms`;
9
+ const parts = [];
10
+ let rest = Math.round(ms / 1e3);
11
+ const units = [["d", 86400], ["h", 3600], ["m", 60], ["s", 1]];
12
+ for (const [unit, size] of units) {
13
+ if (rest >= size) {
14
+ const n = Math.floor(rest / size);
15
+ rest -= n * size;
16
+ parts.push(`${n}${unit}`);
17
+ }
18
+ if (parts.length === 2) break;
19
+ }
20
+ return parts.join(" ") || "0s";
21
+ }
22
+
23
+ // src/triage/anthropic.ts
24
+ var SYSTEM = `You help an engineer understand why a scheduled job misbehaved. You are given the alert, the job's definition, the run that triggered it and a few earlier runs.
25
+
26
+ Reply with two to four sentences of plain prose: the most likely cause, and the first concrete thing to check or change. Be specific to the evidence given; if the evidence is thin, say what is missing rather than guessing. No headings, no lists, no preamble, no restating the error verbatim.`;
27
+ function describe(ctx) {
28
+ const { alert, recentRuns } = ctx;
29
+ const run = alert.run;
30
+ const lines = [];
31
+ lines.push(`Alert: ${alert.type}. ${alert.title}`);
32
+ lines.push(alert.message);
33
+ lines.push("");
34
+ lines.push(`Job definition: ${JSON.stringify(alert.definition)}`);
35
+ if (run) {
36
+ lines.push("");
37
+ lines.push(`Triggering run: status ${run.status}, started ${new Date(run.startedAt).toISOString()}, duration ${run.durationMs === null ? "unknown" : formatDuration(run.durationMs)}, trigger ${run.trigger}`);
38
+ if (Object.keys(run.metrics).length) lines.push(`Metrics: ${JSON.stringify(run.metrics)}`);
39
+ if (run.error) lines.push(`Error:
40
+ ${run.error.slice(0, 3e3)}`);
41
+ if (run.output) lines.push(`Output (tail):
42
+ ${run.output.slice(-3e3)}`);
43
+ }
44
+ const earlier = recentRuns.filter((r) => r.id !== run?.id).slice(0, 5);
45
+ if (earlier.length) {
46
+ lines.push("");
47
+ lines.push("Earlier runs, newest first:");
48
+ for (const r of earlier) {
49
+ lines.push(`- ${r.status}, ${new Date(r.startedAt).toISOString()}, ${r.durationMs === null ? "unknown" : formatDuration(r.durationMs)}${r.error ? `, error: ${r.error.split("\n")[0].slice(0, 160)}` : ""}${Object.keys(r.metrics).length ? `, metrics ${JSON.stringify(r.metrics)}` : ""}`);
50
+ }
51
+ }
52
+ return lines.join("\n");
53
+ }
54
+ function anthropic(options = {}) {
55
+ const client = options.client ?? new Anthropic(options.apiKey ? { apiKey: options.apiKey } : {});
56
+ const model = options.model ?? "claude-opus-5";
57
+ const useFallbacks = options.fallbacks ?? true;
58
+ return async (ctx) => {
59
+ const user = (options.context ? `About this app: ${options.context}
60
+
61
+ ` : "") + describe(ctx);
62
+ const response = await client.beta.messages.create({
63
+ model,
64
+ max_tokens: options.maxTokens ?? 800,
65
+ system: SYSTEM,
66
+ output_config: { effort: options.effort ?? "medium" },
67
+ messages: [{ role: "user", content: user }],
68
+ ...useFallbacks ? { betas: ["server-side-fallback-2026-07-01"], fallbacks: "default" } : {}
69
+ });
70
+ if (response.stop_reason === "refusal") return null;
71
+ const text = response.content.filter((block) => block.type === "text").map((block) => block.text).join("\n").trim();
72
+ return text || null;
73
+ };
74
+ }
75
+
76
+ export { anthropic };
77
+ //# sourceMappingURL=anthropic.js.map
78
+ //# sourceMappingURL=anthropic.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/duration.ts","../src/triage/anthropic.ts"],"names":[],"mappings":"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","file":"anthropic.js","sourcesContent":["import type { Duration } from \"./types.js\";\n\nconst UNIT_MS: Record<string, number> = {\n ms: 1,\n s: 1000,\n m: 60_000,\n h: 3_600_000,\n d: 86_400_000,\n w: 604_800_000,\n};\n\n/**\n * \"15m\" -> 900000. Accepts a plain number of milliseconds, and compound\n * strings such as \"1h30m\". Whitespace between parts is fine.\n */\nexport function parseDuration(value: Duration, label = \"duration\"): number {\n if (typeof value === \"number\") {\n if (!Number.isFinite(value) || value < 0) throw new Error(`${label} must be a non-negative number of milliseconds`);\n return value;\n }\n const text = value.trim().toLowerCase();\n if (text === \"\") throw new Error(`${label} is empty`);\n const re = /(\\d+(?:\\.\\d+)?)\\s*(ms|s|m|h|d|w)/g;\n let total = 0;\n let consumed = \"\";\n let match: RegExpExecArray | null;\n while ((match = re.exec(text)) !== null) {\n total += parseFloat(match[1]!) * UNIT_MS[match[2]!]!;\n consumed += match[0];\n }\n if (consumed.replace(/\\s+/g, \"\") !== text.replace(/\\s+/g, \"\")) {\n throw new Error(`${label} \"${value}\" is not a duration like \"15m\", \"1h30m\" or \"90s\"`);\n }\n return Math.round(total);\n}\n\n/** 90000 -> \"1m 30s\". For messages, not for parsing back. */\nexport function formatDuration(ms: number): string {\n if (!Number.isFinite(ms)) return \"?\";\n if (ms < 1000) return `${Math.round(ms)}ms`;\n const parts: string[] = [];\n let rest = Math.round(ms / 1000);\n const units: [string, number][] = [[\"d\", 86_400], [\"h\", 3_600], [\"m\", 60], [\"s\", 1]];\n for (const [unit, size] of units) {\n if (rest >= size) {\n const n = Math.floor(rest / size);\n rest -= n * size;\n parts.push(`${n}${unit}`);\n }\n if (parts.length === 2) break;\n }\n return parts.join(\" \") || \"0s\";\n}\n\n/** \"5 minutes ago\", \"in 2h\". Relative to `now`. */\nexport function formatRelative(at: number, now: number): string {\n const diff = at - now;\n const abs = Math.abs(diff);\n const text = abs < 5_000 ? \"now\" : formatDuration(abs);\n if (abs < 5_000) return text;\n return diff < 0 ? `${text} ago` : `in ${text}`;\n}\n","import Anthropic from \"@anthropic-ai/sdk\";\nimport { formatDuration } from \"../duration.js\";\nimport type { TriageContext, TriageFn } from \"../types.js\";\n\nexport interface AnthropicTriageOptions {\n /** Defaults to what the Anthropic SDK resolves: ANTHROPIC_API_KEY, or an `ant auth login` profile. */\n apiKey?: string;\n /** Bring a configured client instead. */\n client?: Anthropic;\n /** Default \"claude-opus-5\". */\n model?: string;\n /** How hard the model thinks. Default \"medium\"; a stack trace rarely needs more. */\n effort?: \"low\" | \"medium\" | \"high\";\n /** Default 800. A diagnosis is a paragraph. */\n maxTokens?: number;\n /**\n * Route a policy refusal to Anthropic's default fallback model inside the\n * same request, so a diagnosis still comes back. On by default; turn off if\n * your account or gateway rejects the beta.\n */\n fallbacks?: boolean;\n /** Anything the model should know about this app: \"A Next.js app on Vercel with a Neon database.\" */\n context?: string;\n}\n\nconst SYSTEM = `You help an engineer understand why a scheduled job misbehaved. You are given the alert, the job's definition, the run that triggered it and a few earlier runs.\n\nReply with two to four sentences of plain prose: the most likely cause, and the first concrete thing to check or change. Be specific to the evidence given; if the evidence is thin, say what is missing rather than guessing. No headings, no lists, no preamble, no restating the error verbatim.`;\n\nfunction describe(ctx: TriageContext): string {\n const { alert, recentRuns } = ctx;\n const run = alert.run;\n const lines: string[] = [];\n lines.push(`Alert: ${alert.type}. ${alert.title}`);\n lines.push(alert.message);\n lines.push(\"\");\n lines.push(`Job definition: ${JSON.stringify(alert.definition)}`);\n if (run) {\n lines.push(\"\");\n lines.push(`Triggering run: status ${run.status}, started ${new Date(run.startedAt).toISOString()}, duration ${run.durationMs === null ? \"unknown\" : formatDuration(run.durationMs)}, trigger ${run.trigger}`);\n if (Object.keys(run.metrics).length) lines.push(`Metrics: ${JSON.stringify(run.metrics)}`);\n if (run.error) lines.push(`Error:\\n${run.error.slice(0, 3000)}`);\n if (run.output) lines.push(`Output (tail):\\n${run.output.slice(-3000)}`);\n }\n const earlier = recentRuns.filter((r) => r.id !== run?.id).slice(0, 5);\n if (earlier.length) {\n lines.push(\"\");\n lines.push(\"Earlier runs, newest first:\");\n for (const r of earlier) {\n lines.push(`- ${r.status}, ${new Date(r.startedAt).toISOString()}, ${r.durationMs === null ? \"unknown\" : formatDuration(r.durationMs)}${r.error ? `, error: ${r.error.split(\"\\n\")[0]!.slice(0, 160)}` : \"\"}${Object.keys(r.metrics).length ? `, metrics ${JSON.stringify(r.metrics)}` : \"\"}`);\n }\n }\n return lines.join(\"\\n\");\n}\n\n/**\n * A triage function backed by Claude. Pass it as `triage` to cronwatch():\n *\n * cronwatch({ triage: anthropic({ context: \"A Next.js app on Vercel.\" }) })\n *\n * It runs only when an alert is sent (never per run), so cost is bounded by\n * how often things go wrong, and it never blocks an alert: the client gives\n * it 25 seconds and moves on without a diagnosis if it takes longer.\n */\nexport function anthropic(options: AnthropicTriageOptions = {}): TriageFn {\n const client = options.client ?? new Anthropic(options.apiKey ? { apiKey: options.apiKey } : {});\n const model = options.model ?? \"claude-opus-5\";\n const useFallbacks = options.fallbacks ?? true;\n\n return async (ctx) => {\n const user = (options.context ? `About this app: ${options.context}\\n\\n` : \"\") + describe(ctx);\n const response = await client.beta.messages.create({\n model,\n max_tokens: options.maxTokens ?? 800,\n system: SYSTEM,\n output_config: { effort: options.effort ?? \"medium\" },\n messages: [{ role: \"user\", content: user }],\n ...(useFallbacks ? { betas: [\"server-side-fallback-2026-07-01\"], fallbacks: \"default\" } : {}),\n });\n if (response.stop_reason === \"refusal\") return null;\n const text = response.content\n .filter((block): block is Anthropic.Beta.BetaTextBlock => block.type === \"text\")\n .map((block) => block.text)\n .join(\"\\n\")\n .trim();\n return text || null;\n };\n}\n"]}
@@ -0,0 +1,42 @@
1
+ 'use strict';
2
+
3
+ // src/alerts/discord.ts
4
+ var COLOR = {
5
+ missed: 12024095,
6
+ failed: 12986408,
7
+ stuck: 12986408,
8
+ slow: 12024095,
9
+ over_budget: 12024095,
10
+ recovered: 2067020
11
+ };
12
+ function discord(options) {
13
+ if (!options.webhookUrl) throw new Error("discord() needs a webhookUrl");
14
+ return {
15
+ name: "discord",
16
+ async send(alert) {
17
+ const url = options.link?.(alert);
18
+ const response = await fetch(options.webhookUrl, {
19
+ method: "POST",
20
+ headers: { "content-type": "application/json" },
21
+ body: JSON.stringify({
22
+ content: alert.title,
23
+ embeds: [
24
+ {
25
+ title: alert.title,
26
+ ...url ? { url } : {},
27
+ description: "```\n" + alert.message.slice(0, 3800) + "\n```" + (alert.triage ? `
28
+ **Triage:** ${alert.triage.slice(0, 1e3)}` : ""),
29
+ color: COLOR[alert.type],
30
+ timestamp: new Date(alert.at).toISOString()
31
+ }
32
+ ]
33
+ })
34
+ });
35
+ if (!response.ok) throw new Error(`Discord webhook answered ${response.status}: ${(await response.text()).slice(0, 200)}`);
36
+ }
37
+ };
38
+ }
39
+
40
+ exports.discord = discord;
41
+ //# sourceMappingURL=discord.cjs.map
42
+ //# sourceMappingURL=discord.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/alerts/discord.ts"],"names":[],"mappings":";;;AAQA,IAAM,KAAA,GAAuC;AAAA,EAC3C,MAAA,EAAQ,QAAA;AAAA,EACR,MAAA,EAAQ,QAAA;AAAA,EACR,KAAA,EAAO,QAAA;AAAA,EACP,IAAA,EAAM,QAAA;AAAA,EACN,WAAA,EAAa,QAAA;AAAA,EACb,SAAA,EAAW;AACb,CAAA;AAGO,SAAS,QAAQ,OAAA,EAAuC;AAC7D,EAAA,IAAI,CAAC,OAAA,CAAQ,UAAA,EAAY,MAAM,IAAI,MAAM,8BAA8B,CAAA;AACvE,EAAA,OAAO;AAAA,IACL,IAAA,EAAM,SAAA;AAAA,IACN,MAAM,KAAK,KAAA,EAAO;AAChB,MAAA,MAAM,GAAA,GAAM,OAAA,CAAQ,IAAA,GAAO,KAAK,CAAA;AAChC,MAAA,MAAM,QAAA,GAAW,MAAM,KAAA,CAAM,OAAA,CAAQ,UAAA,EAAY;AAAA,QAC/C,MAAA,EAAQ,MAAA;AAAA,QACR,OAAA,EAAS,EAAE,cAAA,EAAgB,kBAAA,EAAmB;AAAA,QAC9C,IAAA,EAAM,KAAK,SAAA,CAAU;AAAA,UACnB,SAAS,KAAA,CAAM,KAAA;AAAA,UACf,MAAA,EAAQ;AAAA,YACN;AAAA,cACE,OAAO,KAAA,CAAM,KAAA;AAAA,cACb,GAAI,GAAA,GAAM,EAAE,GAAA,KAAQ,EAAC;AAAA,cACrB,WAAA,EAAa,OAAA,GAAU,KAAA,CAAM,OAAA,CAAQ,KAAA,CAAM,GAAG,IAAI,CAAA,GAAI,OAAA,IAAW,KAAA,CAAM,MAAA,GAAS;AAAA,YAAA,EAAiB,MAAM,MAAA,CAAO,KAAA,CAAM,CAAA,EAAG,GAAI,CAAC,CAAA,CAAA,GAAK,EAAA,CAAA;AAAA,cACjI,KAAA,EAAO,KAAA,CAAM,KAAA,CAAM,IAAI,CAAA;AAAA,cACvB,WAAW,IAAI,IAAA,CAAK,KAAA,CAAM,EAAE,EAAE,WAAA;AAAY;AAC5C;AACF,SACD;AAAA,OACF,CAAA;AACD,MAAA,IAAI,CAAC,QAAA,CAAS,EAAA,QAAU,IAAI,KAAA,CAAM,4BAA4B,QAAA,CAAS,MAAM,CAAA,EAAA,EAAA,CAAM,MAAM,SAAS,IAAA,EAAK,EAAG,MAAM,CAAA,EAAG,GAAG,CAAC,CAAA,CAAE,CAAA;AAAA,IAC3H;AAAA,GACF;AACF","file":"discord.cjs","sourcesContent":["import type { Alert, AlertChannel } from \"../types.js\";\n\nexport interface DiscordOptions {\n /** A channel webhook URL from Server Settings, Integrations, Webhooks. */\n webhookUrl: string;\n link?: (alert: Alert) => string;\n}\n\nconst COLOR: Record<Alert[\"type\"], number> = {\n missed: 0xb7791f,\n failed: 0xc62828,\n stuck: 0xc62828,\n slow: 0xb7791f,\n over_budget: 0xb7791f,\n recovered: 0x1f8a4c,\n};\n\n/** Sends alerts to a Discord channel through a webhook. */\nexport function discord(options: DiscordOptions): AlertChannel {\n if (!options.webhookUrl) throw new Error(\"discord() needs a webhookUrl\");\n return {\n name: \"discord\",\n async send(alert) {\n const url = options.link?.(alert);\n const response = await fetch(options.webhookUrl, {\n method: \"POST\",\n headers: { \"content-type\": \"application/json\" },\n body: JSON.stringify({\n content: alert.title,\n embeds: [\n {\n title: alert.title,\n ...(url ? { url } : {}),\n description: \"```\\n\" + alert.message.slice(0, 3800) + \"\\n```\" + (alert.triage ? `\\n**Triage:** ${alert.triage.slice(0, 1000)}` : \"\"),\n color: COLOR[alert.type],\n timestamp: new Date(alert.at).toISOString(),\n },\n ],\n }),\n });\n if (!response.ok) throw new Error(`Discord webhook answered ${response.status}: ${(await response.text()).slice(0, 200)}`);\n },\n };\n}\n"]}
@@ -0,0 +1,11 @@
1
+ import { A as Alert, a as AlertChannel } from './types-BQ1P8z55.cjs';
2
+
3
+ interface DiscordOptions {
4
+ /** A channel webhook URL from Server Settings, Integrations, Webhooks. */
5
+ webhookUrl: string;
6
+ link?: (alert: Alert) => string;
7
+ }
8
+ /** Sends alerts to a Discord channel through a webhook. */
9
+ declare function discord(options: DiscordOptions): AlertChannel;
10
+
11
+ export { type DiscordOptions, discord };
@@ -0,0 +1,11 @@
1
+ import { A as Alert, a as AlertChannel } from './types-BQ1P8z55.js';
2
+
3
+ interface DiscordOptions {
4
+ /** A channel webhook URL from Server Settings, Integrations, Webhooks. */
5
+ webhookUrl: string;
6
+ link?: (alert: Alert) => string;
7
+ }
8
+ /** Sends alerts to a Discord channel through a webhook. */
9
+ declare function discord(options: DiscordOptions): AlertChannel;
10
+
11
+ export { type DiscordOptions, discord };
@@ -0,0 +1,40 @@
1
+ // src/alerts/discord.ts
2
+ var COLOR = {
3
+ missed: 12024095,
4
+ failed: 12986408,
5
+ stuck: 12986408,
6
+ slow: 12024095,
7
+ over_budget: 12024095,
8
+ recovered: 2067020
9
+ };
10
+ function discord(options) {
11
+ if (!options.webhookUrl) throw new Error("discord() needs a webhookUrl");
12
+ return {
13
+ name: "discord",
14
+ async send(alert) {
15
+ const url = options.link?.(alert);
16
+ const response = await fetch(options.webhookUrl, {
17
+ method: "POST",
18
+ headers: { "content-type": "application/json" },
19
+ body: JSON.stringify({
20
+ content: alert.title,
21
+ embeds: [
22
+ {
23
+ title: alert.title,
24
+ ...url ? { url } : {},
25
+ description: "```\n" + alert.message.slice(0, 3800) + "\n```" + (alert.triage ? `
26
+ **Triage:** ${alert.triage.slice(0, 1e3)}` : ""),
27
+ color: COLOR[alert.type],
28
+ timestamp: new Date(alert.at).toISOString()
29
+ }
30
+ ]
31
+ })
32
+ });
33
+ if (!response.ok) throw new Error(`Discord webhook answered ${response.status}: ${(await response.text()).slice(0, 200)}`);
34
+ }
35
+ };
36
+ }
37
+
38
+ export { discord };
39
+ //# sourceMappingURL=discord.js.map
40
+ //# sourceMappingURL=discord.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/alerts/discord.ts"],"names":[],"mappings":";AAQA,IAAM,KAAA,GAAuC;AAAA,EAC3C,MAAA,EAAQ,QAAA;AAAA,EACR,MAAA,EAAQ,QAAA;AAAA,EACR,KAAA,EAAO,QAAA;AAAA,EACP,IAAA,EAAM,QAAA;AAAA,EACN,WAAA,EAAa,QAAA;AAAA,EACb,SAAA,EAAW;AACb,CAAA;AAGO,SAAS,QAAQ,OAAA,EAAuC;AAC7D,EAAA,IAAI,CAAC,OAAA,CAAQ,UAAA,EAAY,MAAM,IAAI,MAAM,8BAA8B,CAAA;AACvE,EAAA,OAAO;AAAA,IACL,IAAA,EAAM,SAAA;AAAA,IACN,MAAM,KAAK,KAAA,EAAO;AAChB,MAAA,MAAM,GAAA,GAAM,OAAA,CAAQ,IAAA,GAAO,KAAK,CAAA;AAChC,MAAA,MAAM,QAAA,GAAW,MAAM,KAAA,CAAM,OAAA,CAAQ,UAAA,EAAY;AAAA,QAC/C,MAAA,EAAQ,MAAA;AAAA,QACR,OAAA,EAAS,EAAE,cAAA,EAAgB,kBAAA,EAAmB;AAAA,QAC9C,IAAA,EAAM,KAAK,SAAA,CAAU;AAAA,UACnB,SAAS,KAAA,CAAM,KAAA;AAAA,UACf,MAAA,EAAQ;AAAA,YACN;AAAA,cACE,OAAO,KAAA,CAAM,KAAA;AAAA,cACb,GAAI,GAAA,GAAM,EAAE,GAAA,KAAQ,EAAC;AAAA,cACrB,WAAA,EAAa,OAAA,GAAU,KAAA,CAAM,OAAA,CAAQ,KAAA,CAAM,GAAG,IAAI,CAAA,GAAI,OAAA,IAAW,KAAA,CAAM,MAAA,GAAS;AAAA,YAAA,EAAiB,MAAM,MAAA,CAAO,KAAA,CAAM,CAAA,EAAG,GAAI,CAAC,CAAA,CAAA,GAAK,EAAA,CAAA;AAAA,cACjI,KAAA,EAAO,KAAA,CAAM,KAAA,CAAM,IAAI,CAAA;AAAA,cACvB,WAAW,IAAI,IAAA,CAAK,KAAA,CAAM,EAAE,EAAE,WAAA;AAAY;AAC5C;AACF,SACD;AAAA,OACF,CAAA;AACD,MAAA,IAAI,CAAC,QAAA,CAAS,EAAA,QAAU,IAAI,KAAA,CAAM,4BAA4B,QAAA,CAAS,MAAM,CAAA,EAAA,EAAA,CAAM,MAAM,SAAS,IAAA,EAAK,EAAG,MAAM,CAAA,EAAG,GAAG,CAAC,CAAA,CAAE,CAAA;AAAA,IAC3H;AAAA,GACF;AACF","file":"discord.js","sourcesContent":["import type { Alert, AlertChannel } from \"../types.js\";\n\nexport interface DiscordOptions {\n /** A channel webhook URL from Server Settings, Integrations, Webhooks. */\n webhookUrl: string;\n link?: (alert: Alert) => string;\n}\n\nconst COLOR: Record<Alert[\"type\"], number> = {\n missed: 0xb7791f,\n failed: 0xc62828,\n stuck: 0xc62828,\n slow: 0xb7791f,\n over_budget: 0xb7791f,\n recovered: 0x1f8a4c,\n};\n\n/** Sends alerts to a Discord channel through a webhook. */\nexport function discord(options: DiscordOptions): AlertChannel {\n if (!options.webhookUrl) throw new Error(\"discord() needs a webhookUrl\");\n return {\n name: \"discord\",\n async send(alert) {\n const url = options.link?.(alert);\n const response = await fetch(options.webhookUrl, {\n method: \"POST\",\n headers: { \"content-type\": \"application/json\" },\n body: JSON.stringify({\n content: alert.title,\n embeds: [\n {\n title: alert.title,\n ...(url ? { url } : {}),\n description: \"```\\n\" + alert.message.slice(0, 3800) + \"\\n```\" + (alert.triage ? `\\n**Triage:** ${alert.triage.slice(0, 1000)}` : \"\"),\n color: COLOR[alert.type],\n timestamp: new Date(alert.at).toISOString(),\n },\n ],\n }),\n });\n if (!response.ok) throw new Error(`Discord webhook answered ${response.status}: ${(await response.text()).slice(0, 200)}`);\n },\n };\n}\n"]}