@synergenius/flowweaver-pack-weaver 0.6.1 → 0.7.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 (168) hide show
  1. package/dist/bot/ai-client.d.ts +1 -0
  2. package/dist/bot/ai-client.d.ts.map +1 -1
  3. package/dist/bot/ai-client.js +52 -1
  4. package/dist/bot/ai-client.js.map +1 -1
  5. package/dist/bot/audit-logger.d.ts +5 -0
  6. package/dist/bot/audit-logger.d.ts.map +1 -0
  7. package/dist/bot/audit-logger.js +42 -0
  8. package/dist/bot/audit-logger.js.map +1 -0
  9. package/dist/bot/audit-store.d.ts +13 -0
  10. package/dist/bot/audit-store.d.ts.map +1 -0
  11. package/dist/bot/audit-store.js +59 -0
  12. package/dist/bot/audit-store.js.map +1 -0
  13. package/dist/bot/cli-provider.d.ts +1 -0
  14. package/dist/bot/cli-provider.d.ts.map +1 -1
  15. package/dist/bot/cli-provider.js +86 -22
  16. package/dist/bot/cli-provider.js.map +1 -1
  17. package/dist/bot/cli-stream-parser.d.ts +11 -0
  18. package/dist/bot/cli-stream-parser.d.ts.map +1 -0
  19. package/dist/bot/cli-stream-parser.js +53 -0
  20. package/dist/bot/cli-stream-parser.js.map +1 -0
  21. package/dist/bot/file-validator.d.ts +1 -1
  22. package/dist/bot/file-validator.d.ts.map +1 -1
  23. package/dist/bot/file-validator.js +13 -27
  24. package/dist/bot/file-validator.js.map +1 -1
  25. package/dist/bot/fw-api.d.ts +8 -0
  26. package/dist/bot/fw-api.d.ts.map +1 -0
  27. package/dist/bot/fw-api.js +12 -0
  28. package/dist/bot/fw-api.js.map +1 -0
  29. package/dist/bot/runner.d.ts +2 -1
  30. package/dist/bot/runner.d.ts.map +1 -1
  31. package/dist/bot/runner.js +8 -0
  32. package/dist/bot/runner.js.map +1 -1
  33. package/dist/bot/step-executor.d.ts +3 -2
  34. package/dist/bot/step-executor.d.ts.map +1 -1
  35. package/dist/bot/step-executor.js +9 -30
  36. package/dist/bot/step-executor.js.map +1 -1
  37. package/dist/bot/system-prompt.d.ts +13 -1
  38. package/dist/bot/system-prompt.d.ts.map +1 -1
  39. package/dist/bot/system-prompt.js +28 -22
  40. package/dist/bot/system-prompt.js.map +1 -1
  41. package/dist/bot/types.d.ts +9 -1
  42. package/dist/bot/types.d.ts.map +1 -1
  43. package/dist/cli-bridge.d.ts.map +1 -1
  44. package/dist/cli-bridge.js +2 -1
  45. package/dist/cli-bridge.js.map +1 -1
  46. package/dist/cli-handlers.d.ts +2 -1
  47. package/dist/cli-handlers.d.ts.map +1 -1
  48. package/dist/cli-handlers.js +69 -0
  49. package/dist/cli-handlers.js.map +1 -1
  50. package/dist/node-types/approval-gate.d.ts.map +1 -1
  51. package/dist/node-types/approval-gate.js +4 -0
  52. package/dist/node-types/approval-gate.js.map +1 -1
  53. package/dist/node-types/exec-validate-retry.d.ts.map +1 -1
  54. package/dist/node-types/exec-validate-retry.js +10 -4
  55. package/dist/node-types/exec-validate-retry.js.map +1 -1
  56. package/dist/node-types/execute-plan.js +1 -1
  57. package/dist/node-types/execute-plan.js.map +1 -1
  58. package/dist/node-types/git-ops.d.ts.map +1 -1
  59. package/dist/node-types/git-ops.js +2 -0
  60. package/dist/node-types/git-ops.js.map +1 -1
  61. package/dist/node-types/plan-task.d.ts.map +1 -1
  62. package/dist/node-types/plan-task.js +9 -1
  63. package/dist/node-types/plan-task.js.map +1 -1
  64. package/dist/node-types/send-notify.d.ts.map +1 -1
  65. package/dist/node-types/send-notify.js +4 -1
  66. package/dist/node-types/send-notify.js.map +1 -1
  67. package/dist/node-types/validate-result.d.ts +2 -2
  68. package/dist/node-types/validate-result.d.ts.map +1 -1
  69. package/dist/node-types/validate-result.js +2 -2
  70. package/dist/node-types/validate-result.js.map +1 -1
  71. package/dist/workflows/weaver-bot-batch.d.ts +4 -1
  72. package/dist/workflows/weaver-bot-batch.d.ts.map +1 -1
  73. package/dist/workflows/weaver-bot-batch.js +1 -1
  74. package/dist/workflows/weaver-bot-batch.js.map +1 -1
  75. package/dist/workflows/weaver-bot.d.ts +4 -1
  76. package/dist/workflows/weaver-bot.d.ts.map +1 -1
  77. package/dist/workflows/weaver-bot.js +1 -1
  78. package/dist/workflows/weaver-bot.js.map +1 -1
  79. package/flowweaver.manifest.json +1 -1
  80. package/package.json +3 -2
  81. package/src/bot/agent-provider.ts +273 -0
  82. package/src/bot/ai-client.ts +109 -0
  83. package/src/bot/approvals.ts +273 -0
  84. package/src/bot/audit-logger.ts +45 -0
  85. package/src/bot/audit-store.ts +69 -0
  86. package/src/bot/bot-agent-channel.ts +99 -0
  87. package/src/bot/cli-provider.ts +169 -0
  88. package/src/bot/cli-stream-parser.ts +59 -0
  89. package/src/bot/cost-store.ts +92 -0
  90. package/src/bot/cost-tracker.ts +72 -0
  91. package/src/bot/cron-parser.ts +153 -0
  92. package/src/bot/cron-scheduler.ts +48 -0
  93. package/src/bot/dashboard.ts +658 -0
  94. package/src/bot/design-checker.ts +327 -0
  95. package/src/bot/file-lock.ts +73 -0
  96. package/src/bot/file-validator.ts +41 -0
  97. package/src/bot/file-watcher.ts +103 -0
  98. package/src/bot/fw-api.ts +18 -0
  99. package/src/bot/genesis-prompt-context.ts +135 -0
  100. package/src/bot/genesis-store.ts +180 -0
  101. package/src/bot/index.ts +127 -0
  102. package/src/bot/notifications.ts +263 -0
  103. package/src/bot/pipeline-runner.ts +324 -0
  104. package/src/bot/provider-registry.ts +236 -0
  105. package/src/bot/run-store.ts +169 -0
  106. package/src/bot/runner.ts +311 -0
  107. package/src/bot/session-state.ts +73 -0
  108. package/src/bot/steering.ts +44 -0
  109. package/src/bot/step-executor.ts +34 -0
  110. package/src/bot/system-prompt.ts +280 -0
  111. package/src/bot/task-queue.ts +111 -0
  112. package/src/bot/types.ts +571 -0
  113. package/src/bot/utils.ts +17 -0
  114. package/src/bot/watch-daemon.ts +203 -0
  115. package/src/bot/web-approval.ts +240 -0
  116. package/src/cli-bridge.ts +41 -0
  117. package/src/cli-handlers.ts +1271 -0
  118. package/src/docs/weaver-config.md +135 -0
  119. package/src/index.ts +173 -0
  120. package/src/mcp-tools.ts +274 -0
  121. package/src/node-types/abort-task.ts +31 -0
  122. package/src/node-types/approval-gate.ts +75 -0
  123. package/src/node-types/bot-report.ts +82 -0
  124. package/src/node-types/build-context.ts +65 -0
  125. package/src/node-types/detect-provider.ts +75 -0
  126. package/src/node-types/exec-validate-retry.ts +175 -0
  127. package/src/node-types/execute-plan.ts +130 -0
  128. package/src/node-types/execute-target.ts +267 -0
  129. package/src/node-types/fix-errors.ts +68 -0
  130. package/src/node-types/genesis-apply-retry.ts +138 -0
  131. package/src/node-types/genesis-apply.ts +96 -0
  132. package/src/node-types/genesis-approve.ts +73 -0
  133. package/src/node-types/genesis-check-stabilize.ts +37 -0
  134. package/src/node-types/genesis-check-threshold.ts +34 -0
  135. package/src/node-types/genesis-commit.ts +71 -0
  136. package/src/node-types/genesis-compile-validate.ts +77 -0
  137. package/src/node-types/genesis-diff-fingerprint.ts +67 -0
  138. package/src/node-types/genesis-diff-workflow.ts +71 -0
  139. package/src/node-types/genesis-escrow-grace.ts +62 -0
  140. package/src/node-types/genesis-escrow-migrate.ts +138 -0
  141. package/src/node-types/genesis-escrow-recover.ts +99 -0
  142. package/src/node-types/genesis-escrow-stage.ts +104 -0
  143. package/src/node-types/genesis-escrow-validate.ts +120 -0
  144. package/src/node-types/genesis-load-config.ts +44 -0
  145. package/src/node-types/genesis-observe.ts +119 -0
  146. package/src/node-types/genesis-propose.ts +97 -0
  147. package/src/node-types/genesis-report.ts +95 -0
  148. package/src/node-types/genesis-snapshot.ts +30 -0
  149. package/src/node-types/genesis-try-apply.ts +165 -0
  150. package/src/node-types/genesis-update-history.ts +72 -0
  151. package/src/node-types/genesis-validate-proposal.ts +124 -0
  152. package/src/node-types/git-ops.ts +72 -0
  153. package/src/node-types/index.ts +36 -0
  154. package/src/node-types/load-config.ts +27 -0
  155. package/src/node-types/plan-task.ts +77 -0
  156. package/src/node-types/read-workflow.ts +68 -0
  157. package/src/node-types/receive-task.ts +92 -0
  158. package/src/node-types/report.ts +25 -0
  159. package/src/node-types/resolve-target.ts +64 -0
  160. package/src/node-types/route-task.ts +25 -0
  161. package/src/node-types/send-notify.ts +75 -0
  162. package/src/node-types/validate-result.ts +49 -0
  163. package/src/templates/index.ts +5 -0
  164. package/src/templates/weaver-bot-template.ts +106 -0
  165. package/src/workflows/genesis-task.ts +91 -0
  166. package/src/workflows/index.ts +3 -0
  167. package/src/workflows/weaver-bot-batch.ts +65 -0
  168. package/src/workflows/weaver-bot.ts +79 -0
@@ -0,0 +1,59 @@
1
+ import type { StreamChunk } from './types.js';
2
+
3
+ /**
4
+ * Parse one NDJSON line from `claude --output-format stream-json`.
5
+ * Returns null for unrecognized event types.
6
+ */
7
+ export function parseStreamLine(line: string): StreamChunk | null {
8
+ const trimmed = line.trim();
9
+ if (!trimmed) return null;
10
+
11
+ let parsed: Record<string, unknown>;
12
+ try {
13
+ parsed = JSON.parse(trimmed);
14
+ } catch {
15
+ return null;
16
+ }
17
+
18
+ const type = parsed.type as string | undefined;
19
+
20
+ if (type === 'content_block_delta') {
21
+ const delta = parsed.delta as { type?: string; text?: string } | undefined;
22
+ if (delta?.type === 'text_delta' && typeof delta.text === 'string') {
23
+ return { type: 'text', text: delta.text };
24
+ }
25
+ return null;
26
+ }
27
+
28
+ if (type === 'message_delta') {
29
+ const usage = parsed.usage as Record<string, number> | undefined;
30
+ if (usage) {
31
+ return {
32
+ type: 'usage',
33
+ usage: {
34
+ inputTokens: usage.input_tokens ?? 0,
35
+ outputTokens: usage.output_tokens ?? 0,
36
+ cacheCreationInputTokens: usage.cache_creation_input_tokens,
37
+ cacheReadInputTokens: usage.cache_read_input_tokens,
38
+ },
39
+ };
40
+ }
41
+ return null;
42
+ }
43
+
44
+ if (type === 'message_stop') {
45
+ return { type: 'done' };
46
+ }
47
+
48
+ return null;
49
+ }
50
+
51
+ /**
52
+ * Concatenate text from collected StreamChunks into a single string.
53
+ */
54
+ export function extractTextFromChunks(chunks: StreamChunk[]): string {
55
+ return chunks
56
+ .filter((c) => c.type === 'text' && c.text)
57
+ .map((c) => c.text!)
58
+ .join('');
59
+ }
@@ -0,0 +1,92 @@
1
+ import * as fs from 'node:fs';
2
+ import * as path from 'node:path';
3
+ import * as os from 'node:os';
4
+ import type { CostRecord, CostSummary } from './types.js';
5
+
6
+ const MAX_ENTRIES = 10_000;
7
+ const CAP_CHECK_INTERVAL = 100;
8
+
9
+ export class CostStore {
10
+ private readonly dir: string;
11
+ private readonly filePath: string;
12
+ private appendCount = 0;
13
+
14
+ constructor(dir?: string) {
15
+ this.dir = dir ?? process.env.WEAVER_DATA_DIR ?? path.join(os.homedir(), '.weaver');
16
+ fs.mkdirSync(this.dir, { recursive: true });
17
+ this.filePath = path.join(this.dir, 'costs.ndjson');
18
+ }
19
+
20
+ append(record: CostRecord): void {
21
+ fs.appendFileSync(this.filePath, JSON.stringify(record) + '\n', 'utf-8');
22
+ this.appendCount++;
23
+ if (this.appendCount % CAP_CHECK_INTERVAL === 0) {
24
+ this.enforceCap();
25
+ }
26
+ }
27
+
28
+ query(filters?: { since?: number; model?: string }): CostRecord[] {
29
+ if (!fs.existsSync(this.filePath)) return [];
30
+
31
+ const content = fs.readFileSync(this.filePath, 'utf-8');
32
+ const lines = content.split('\n').filter((l) => l.trim().length > 0);
33
+ const records: CostRecord[] = [];
34
+
35
+ for (const line of lines) {
36
+ try {
37
+ const record = JSON.parse(line) as CostRecord;
38
+ if (filters?.since && record.timestamp < filters.since) continue;
39
+ if (filters?.model && record.model !== filters.model) continue;
40
+ records.push(record);
41
+ } catch {
42
+ // skip corrupt lines
43
+ }
44
+ }
45
+
46
+ return records;
47
+ }
48
+
49
+ summarize(filters?: { since?: number; model?: string }): CostSummary {
50
+ const records = this.query(filters);
51
+
52
+ const summary: CostSummary = {
53
+ totalCost: 0,
54
+ totalInputTokens: 0,
55
+ totalOutputTokens: 0,
56
+ totalRuns: records.length,
57
+ byModel: {},
58
+ };
59
+
60
+ for (const r of records) {
61
+ summary.totalCost += r.estimatedCost;
62
+ summary.totalInputTokens += r.inputTokens;
63
+ summary.totalOutputTokens += r.outputTokens;
64
+
65
+ const model = r.model || 'unknown';
66
+ if (!summary.byModel[model]) {
67
+ summary.byModel[model] = { runs: 0, inputTokens: 0, outputTokens: 0, cost: 0 };
68
+ }
69
+ const m = summary.byModel[model]!;
70
+ m.runs++;
71
+ m.inputTokens += r.inputTokens;
72
+ m.outputTokens += r.outputTokens;
73
+ m.cost += r.estimatedCost;
74
+ }
75
+
76
+ return summary;
77
+ }
78
+
79
+ private enforceCap(): void {
80
+ if (!fs.existsSync(this.filePath)) return;
81
+
82
+ const content = fs.readFileSync(this.filePath, 'utf-8');
83
+ const lines = content.split('\n').filter((l) => l.trim().length > 0);
84
+
85
+ if (lines.length <= MAX_ENTRIES) return;
86
+
87
+ const kept = lines.slice(lines.length - MAX_ENTRIES);
88
+ const tmpPath = this.filePath + '.tmp';
89
+ fs.writeFileSync(tmpPath, kept.join('\n') + '\n', 'utf-8');
90
+ fs.renameSync(tmpPath, this.filePath);
91
+ }
92
+ }
@@ -0,0 +1,72 @@
1
+ import type { TokenUsage, RunCostEntry, RunCostSummary } from './types.js';
2
+
3
+ // Snapshot date: 2026-03-06
4
+ export const MODEL_PRICING: Record<
5
+ string,
6
+ { inputPer1M: number; outputPer1M: number; cacheReadPer1M?: number; cacheCreationPer1M?: number }
7
+ > = {
8
+ 'claude-sonnet-4-6': { inputPer1M: 3, outputPer1M: 15, cacheReadPer1M: 0.3, cacheCreationPer1M: 3.75 },
9
+ 'claude-sonnet-4-20250514': { inputPer1M: 3, outputPer1M: 15, cacheReadPer1M: 0.3, cacheCreationPer1M: 3.75 },
10
+ 'claude-opus-4-6': { inputPer1M: 15, outputPer1M: 75, cacheReadPer1M: 1.5, cacheCreationPer1M: 18.75 },
11
+ 'claude-opus-4-20250514': { inputPer1M: 15, outputPer1M: 75, cacheReadPer1M: 1.5, cacheCreationPer1M: 18.75 },
12
+ 'claude-haiku-4-5': { inputPer1M: 0.8, outputPer1M: 4, cacheReadPer1M: 0.08, cacheCreationPer1M: 1.0 },
13
+ 'claude-3-5-haiku-20241022': { inputPer1M: 0.8, outputPer1M: 4, cacheReadPer1M: 0.08, cacheCreationPer1M: 1.0 },
14
+ 'claude-3-5-sonnet-20241022': { inputPer1M: 3, outputPer1M: 15, cacheReadPer1M: 0.3, cacheCreationPer1M: 3.75 },
15
+ };
16
+
17
+ export class CostTracker {
18
+ private entries: RunCostEntry[] = [];
19
+
20
+ constructor(
21
+ private defaultModel: string,
22
+ private provider: string,
23
+ ) {}
24
+
25
+ track(step: string, model: string, usage: TokenUsage): void {
26
+ this.entries.push({
27
+ step,
28
+ model,
29
+ usage,
30
+ estimatedCost: CostTracker.estimateCost(model, usage),
31
+ timestamp: Date.now(),
32
+ });
33
+ }
34
+
35
+ static estimateCost(model: string, usage: TokenUsage): number {
36
+ const pricing = MODEL_PRICING[model];
37
+ if (!pricing) return 0;
38
+
39
+ return (
40
+ (usage.inputTokens * pricing.inputPer1M +
41
+ usage.outputTokens * pricing.outputPer1M +
42
+ (usage.cacheReadInputTokens ?? 0) * (pricing.cacheReadPer1M ?? pricing.inputPer1M) +
43
+ (usage.cacheCreationInputTokens ?? 0) * (pricing.cacheCreationPer1M ?? pricing.inputPer1M)) /
44
+ 1_000_000
45
+ );
46
+ }
47
+
48
+ getRunSummary(): RunCostSummary {
49
+ let totalIn = 0;
50
+ let totalOut = 0;
51
+ let totalCost = 0;
52
+
53
+ for (const entry of this.entries) {
54
+ totalIn += entry.usage.inputTokens;
55
+ totalOut += entry.usage.outputTokens;
56
+ totalCost += entry.estimatedCost;
57
+ }
58
+
59
+ return {
60
+ entries: [...this.entries],
61
+ totalInputTokens: totalIn,
62
+ totalOutputTokens: totalOut,
63
+ totalCost,
64
+ model: this.entries[0]?.model ?? this.defaultModel,
65
+ provider: this.provider,
66
+ };
67
+ }
68
+
69
+ hasEntries(): boolean {
70
+ return this.entries.length > 0;
71
+ }
72
+ }
@@ -0,0 +1,153 @@
1
+ import type { ParsedCron, CronField } from './types.js';
2
+
3
+ export function parseCron(expression: string): ParsedCron {
4
+ const parts = expression.trim().split(/\s+/);
5
+ if (parts.length !== 5) {
6
+ throw new Error(
7
+ `Invalid cron expression "${expression}": expected 5 fields (minute hour day-of-month month day-of-week)`,
8
+ );
9
+ }
10
+
11
+ return {
12
+ minute: parseField(parts[0]!, 0, 59),
13
+ hour: parseField(parts[1]!, 0, 23),
14
+ dayOfMonth: parseField(parts[2]!, 1, 31),
15
+ month: parseField(parts[3]!, 1, 12),
16
+ dayOfWeek: parseField(parts[4]!, 0, 6),
17
+ source: expression,
18
+ };
19
+ }
20
+
21
+ function parseField(token: string, min: number, max: number): CronField {
22
+ if (token === '*') {
23
+ return { type: 'wildcard', values: range(min, max) };
24
+ }
25
+
26
+ const values = new Set<number>();
27
+
28
+ for (const part of token.split(',')) {
29
+ if (part.includes('/')) {
30
+ const [rangeStr, stepStr] = part.split('/');
31
+ const step = parseInt(stepStr!, 10);
32
+ if (isNaN(step) || step <= 0) throw new Error(`Invalid step in "${part}"`);
33
+
34
+ let start: number;
35
+ let end: number;
36
+ if (rangeStr === '*') {
37
+ start = min;
38
+ end = max;
39
+ } else if (rangeStr!.includes('-')) {
40
+ [start, end] = rangeStr!.split('-').map(Number) as [number, number];
41
+ } else {
42
+ start = parseInt(rangeStr!, 10);
43
+ end = max;
44
+ }
45
+
46
+ for (let i = start; i <= end; i += step) {
47
+ if (i >= min && i <= max) values.add(i);
48
+ }
49
+ } else if (part.includes('-')) {
50
+ const [startStr, endStr] = part.split('-');
51
+ const start = parseInt(startStr!, 10);
52
+ const end = parseInt(endStr!, 10);
53
+ if (isNaN(start) || isNaN(end)) throw new Error(`Invalid range in "${part}"`);
54
+ for (let i = start; i <= end; i++) {
55
+ if (i >= min && i <= max) values.add(i);
56
+ }
57
+ } else {
58
+ const val = parseInt(part, 10);
59
+ if (isNaN(val) || val < min || val > max) {
60
+ throw new Error(`Invalid value "${part}" (expected ${min}-${max})`);
61
+ }
62
+ values.add(val);
63
+ }
64
+ }
65
+
66
+ return {
67
+ type: values.size === max - min + 1 ? 'wildcard' : 'list',
68
+ values: [...values].sort((a, b) => a - b),
69
+ };
70
+ }
71
+
72
+ function range(min: number, max: number): number[] {
73
+ const result: number[] = [];
74
+ for (let i = min; i <= max; i++) result.push(i);
75
+ return result;
76
+ }
77
+
78
+ export function matches(parsed: ParsedCron, date: Date): boolean {
79
+ const minute = date.getMinutes();
80
+ const hour = date.getHours();
81
+ const dayOfMonth = date.getDate();
82
+ const month = date.getMonth() + 1;
83
+ const dayOfWeek = date.getDay();
84
+
85
+ if (!includes(parsed.minute, minute)) return false;
86
+ if (!includes(parsed.hour, hour)) return false;
87
+ if (!includes(parsed.month, month)) return false;
88
+
89
+ // Vixie cron: if both dayOfMonth and dayOfWeek are restricted, match on EITHER
90
+ const domRestricted = parsed.dayOfMonth.type === 'list';
91
+ const dowRestricted = parsed.dayOfWeek.type === 'list';
92
+
93
+ if (domRestricted && dowRestricted) {
94
+ return includes(parsed.dayOfMonth, dayOfMonth) || includes(parsed.dayOfWeek, dayOfWeek);
95
+ }
96
+
97
+ if (!includes(parsed.dayOfMonth, dayOfMonth)) return false;
98
+ if (!includes(parsed.dayOfWeek, dayOfWeek)) return false;
99
+
100
+ return true;
101
+ }
102
+
103
+ function includes(field: CronField, value: number): boolean {
104
+ if (field.type === 'wildcard') return true;
105
+ return field.values.includes(value);
106
+ }
107
+
108
+ export function nextMatch(parsed: ParsedCron, after: Date): Date {
109
+ const candidate = new Date(after.getTime());
110
+ candidate.setSeconds(0, 0);
111
+ candidate.setMinutes(candidate.getMinutes() + 1);
112
+
113
+ // Safety cap: 2 years
114
+ const limit = after.getTime() + 2 * 365 * 24 * 60 * 60 * 1000;
115
+
116
+ while (candidate.getTime() < limit) {
117
+ if (matches(parsed, candidate)) return candidate;
118
+
119
+ // Skip non-matching months
120
+ if (!includes(parsed.month, candidate.getMonth() + 1)) {
121
+ candidate.setMonth(candidate.getMonth() + 1, 1);
122
+ candidate.setHours(0, 0, 0, 0);
123
+ continue;
124
+ }
125
+
126
+ // Skip non-matching days
127
+ const dayOfMonth = candidate.getDate();
128
+ const dayOfWeek = candidate.getDay();
129
+ const domRestricted = parsed.dayOfMonth.type === 'list';
130
+ const dowRestricted = parsed.dayOfWeek.type === 'list';
131
+
132
+ const dayMatches = domRestricted && dowRestricted
133
+ ? includes(parsed.dayOfMonth, dayOfMonth) || includes(parsed.dayOfWeek, dayOfWeek)
134
+ : includes(parsed.dayOfMonth, dayOfMonth) && includes(parsed.dayOfWeek, dayOfWeek);
135
+
136
+ if (!dayMatches) {
137
+ candidate.setDate(candidate.getDate() + 1);
138
+ candidate.setHours(0, 0, 0, 0);
139
+ continue;
140
+ }
141
+
142
+ // Skip non-matching hours
143
+ if (!includes(parsed.hour, candidate.getHours())) {
144
+ candidate.setHours(candidate.getHours() + 1, 0, 0, 0);
145
+ continue;
146
+ }
147
+
148
+ // Advance by one minute
149
+ candidate.setMinutes(candidate.getMinutes() + 1);
150
+ }
151
+
152
+ throw new Error(`No cron match found within 2 years for "${parsed.source}"`);
153
+ }
@@ -0,0 +1,48 @@
1
+ import { EventEmitter } from 'node:events';
2
+ import type { ParsedCron } from './types.js';
3
+ import { nextMatch } from './cron-parser.js';
4
+
5
+ const MAX_TIMEOUT = 2_147_483_647; // ~24.8 days (2^31 - 1)
6
+
7
+ export class CronScheduler extends EventEmitter {
8
+ private parsed: ParsedCron;
9
+ private timer: ReturnType<typeof setTimeout> | null = null;
10
+ private stopped = false;
11
+
12
+ constructor(parsed: ParsedCron) {
13
+ super();
14
+ this.parsed = parsed;
15
+ }
16
+
17
+ start(): void {
18
+ this.stopped = false;
19
+ this.scheduleNext();
20
+ }
21
+
22
+ stop(): void {
23
+ this.stopped = true;
24
+ if (this.timer) {
25
+ clearTimeout(this.timer);
26
+ this.timer = null;
27
+ }
28
+ }
29
+
30
+ private scheduleNext(): void {
31
+ if (this.stopped) return;
32
+
33
+ const now = new Date();
34
+ const next = nextMatch(this.parsed, now);
35
+ const delayMs = next.getTime() - Date.now();
36
+
37
+ if (delayMs > MAX_TIMEOUT) {
38
+ // Chain intermediate timeouts for very long delays
39
+ this.timer = setTimeout(() => this.scheduleNext(), MAX_TIMEOUT);
40
+ } else {
41
+ this.timer = setTimeout(() => {
42
+ if (this.stopped) return;
43
+ this.emit('tick');
44
+ this.scheduleNext();
45
+ }, Math.max(delayMs, 0));
46
+ }
47
+ }
48
+ }