getbotons 0.0.0-stage → 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 (191) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +505 -2
  3. package/installer/admin.mjs +6 -0
  4. package/installer/botons.mjs +430 -0
  5. package/installer/brain-clis.mjs +28 -0
  6. package/installer/linger.mjs +39 -0
  7. package/installer/output.mjs +27 -0
  8. package/installer/revision.mjs +30 -0
  9. package/installer/shell-path.mjs +66 -0
  10. package/installer/source-lock.json +2891 -0
  11. package/package.json +26 -4
  12. package/payload/LICENSE +21 -0
  13. package/payload/SECURITY.md +33 -0
  14. package/payload/installer/admin.mjs +6 -0
  15. package/payload/installer/botons.mjs +430 -0
  16. package/payload/installer/brain-clis.mjs +28 -0
  17. package/payload/installer/linger.mjs +39 -0
  18. package/payload/installer/output.mjs +27 -0
  19. package/payload/installer/revision.mjs +30 -0
  20. package/payload/installer/shell-path.mjs +66 -0
  21. package/payload/installer/source-lock.json +2891 -0
  22. package/payload/npm-shrinkwrap.json +51 -0
  23. package/payload/package.json +13 -0
  24. package/payload/scripts/backup.ts +47 -0
  25. package/payload/scripts/botons-backup.service +15 -0
  26. package/payload/scripts/botons-backup.timer +11 -0
  27. package/payload/scripts/botons.service +36 -0
  28. package/payload/scripts/check-browser.ts +90 -0
  29. package/payload/scripts/doctor.ts +790 -0
  30. package/payload/scripts/install-backup-timer.ts +52 -0
  31. package/payload/scripts/install-service.ts +206 -0
  32. package/payload/scripts/login.ts +17 -0
  33. package/payload/scripts/migrate-names.ts +47 -0
  34. package/payload/scripts/pair.ts +163 -0
  35. package/payload/scripts/restore.ts +108 -0
  36. package/payload/server/bin/botons +4 -0
  37. package/payload/server/bin/legacy/minion +4 -0
  38. package/payload/server/bin/legacy/tots +4 -0
  39. package/payload/server/src/acting.ts +16 -0
  40. package/payload/server/src/api-keys.ts +544 -0
  41. package/payload/server/src/api.ts +1484 -0
  42. package/payload/server/src/app.ts +546 -0
  43. package/payload/server/src/approvals.ts +873 -0
  44. package/payload/server/src/ask-rules.ts +181 -0
  45. package/payload/server/src/audit.ts +158 -0
  46. package/payload/server/src/auth.ts +497 -0
  47. package/payload/server/src/backup.ts +382 -0
  48. package/payload/server/src/brain-login.ts +75 -0
  49. package/payload/server/src/browser-cert.ts +67 -0
  50. package/payload/server/src/browser-handoff.ts +119 -0
  51. package/payload/server/src/browser-handover.ts +175 -0
  52. package/payload/server/src/browser-policy.ts +210 -0
  53. package/payload/server/src/browser-proxy.ts +580 -0
  54. package/payload/server/src/browser-request.ts +335 -0
  55. package/payload/server/src/browser-screenshot.ts +56 -0
  56. package/payload/server/src/browser-send.ts +155 -0
  57. package/payload/server/src/browser.ts +2164 -0
  58. package/payload/server/src/callback.ts +218 -0
  59. package/payload/server/src/capabilities.ts +90 -0
  60. package/payload/server/src/cards.ts +243 -0
  61. package/payload/server/src/cli/approve-mcp.ts +119 -0
  62. package/payload/server/src/cli/botons.ts +629 -0
  63. package/payload/server/src/cli/browser-mcp.ts +168 -0
  64. package/payload/server/src/cli/workspace-file.ts +78 -0
  65. package/payload/server/src/cli-homes.ts +38 -0
  66. package/payload/server/src/cli-install.ts +104 -0
  67. package/payload/server/src/cli-updates.ts +112 -0
  68. package/payload/server/src/codex-config-repair.ts +80 -0
  69. package/payload/server/src/config.ts +473 -0
  70. package/payload/server/src/connector-access.ts +158 -0
  71. package/payload/server/src/connector-launch.ts +203 -0
  72. package/payload/server/src/connectors.ts +947 -0
  73. package/payload/server/src/disk-usage.ts +62 -0
  74. package/payload/server/src/engine.ts +2328 -0
  75. package/payload/server/src/env-init.ts +5 -0
  76. package/payload/server/src/events.ts +46 -0
  77. package/payload/server/src/files.ts +528 -0
  78. package/payload/server/src/github-access.ts +52 -0
  79. package/payload/server/src/health.ts +117 -0
  80. package/payload/server/src/held-dir.ts +96 -0
  81. package/payload/server/src/hooks.ts +141 -0
  82. package/payload/server/src/http.ts +207 -0
  83. package/payload/server/src/index.ts +139 -0
  84. package/payload/server/src/install-type.ts +11 -0
  85. package/payload/server/src/instructions.ts +204 -0
  86. package/payload/server/src/isolation.ts +234 -0
  87. package/payload/server/src/legacy-upgrade.ts +668 -0
  88. package/payload/server/src/limits.ts +44 -0
  89. package/payload/server/src/lock.ts +75 -0
  90. package/payload/server/src/log.ts +137 -0
  91. package/payload/server/src/managed-clis.ts +65 -0
  92. package/payload/server/src/mcp-client.ts +229 -0
  93. package/payload/server/src/notify.ts +193 -0
  94. package/payload/server/src/oauth-connections.ts +650 -0
  95. package/payload/server/src/oauth.ts +311 -0
  96. package/payload/server/src/outbound.ts +142 -0
  97. package/payload/server/src/portable.ts +1669 -0
  98. package/payload/server/src/prompt.ts +676 -0
  99. package/payload/server/src/providers/api-brain.ts +612 -0
  100. package/payload/server/src/providers/api-history.ts +131 -0
  101. package/payload/server/src/providers/api-tools.ts +481 -0
  102. package/payload/server/src/providers/api-wire.ts +690 -0
  103. package/payload/server/src/providers/claude-cli.ts +620 -0
  104. package/payload/server/src/providers/cli-check.ts +265 -0
  105. package/payload/server/src/providers/cli-turn.ts +20 -0
  106. package/payload/server/src/providers/codex-cli.ts +552 -0
  107. package/payload/server/src/providers/codex-workspace.ts +61 -0
  108. package/payload/server/src/providers/failures.ts +143 -0
  109. package/payload/server/src/providers/index.ts +131 -0
  110. package/payload/server/src/providers/mcp-stdio.ts +147 -0
  111. package/payload/server/src/providers/mock.ts +94 -0
  112. package/payload/server/src/providers/process.ts +268 -0
  113. package/payload/server/src/providers/readiness.ts +43 -0
  114. package/payload/server/src/providers/sse.ts +73 -0
  115. package/payload/server/src/providers/types.ts +173 -0
  116. package/payload/server/src/rates.ts +128 -0
  117. package/payload/server/src/reactions.ts +266 -0
  118. package/payload/server/src/recent-logs.ts +489 -0
  119. package/payload/server/src/rehearsal.ts +593 -0
  120. package/payload/server/src/rehearsals.ts +303 -0
  121. package/payload/server/src/replay.ts +165 -0
  122. package/payload/server/src/sandbox.ts +767 -0
  123. package/payload/server/src/schedule.ts +214 -0
  124. package/payload/server/src/scheduler.ts +24 -0
  125. package/payload/server/src/schema.ts +882 -0
  126. package/payload/server/src/secrets.ts +46 -0
  127. package/payload/server/src/setup.ts +356 -0
  128. package/payload/server/src/skill-packages.ts +363 -0
  129. package/payload/server/src/skills.ts +527 -0
  130. package/payload/server/src/spend.ts +255 -0
  131. package/payload/server/src/ssrf.ts +126 -0
  132. package/payload/server/src/starter-gate.ts +168 -0
  133. package/payload/server/src/starters.ts +610 -0
  134. package/payload/server/src/storage.ts +32 -0
  135. package/payload/server/src/store.ts +3087 -0
  136. package/payload/server/src/tailnet.ts +132 -0
  137. package/payload/server/src/templates.ts +454 -0
  138. package/payload/server/src/trash.ts +772 -0
  139. package/payload/server/src/util.ts +97 -0
  140. package/payload/server/src/webhooks.ts +216 -0
  141. package/payload/server/src/work-feed.ts +473 -0
  142. package/payload/shared/app-version.d.mts +1 -0
  143. package/payload/shared/app-version.mjs +3 -0
  144. package/payload/shared/boton.ts +323 -0
  145. package/payload/shared/brand.ts +107 -0
  146. package/payload/shared/cards.ts +427 -0
  147. package/payload/shared/effort.ts +91 -0
  148. package/payload/shared/files.ts +104 -0
  149. package/payload/shared/handoff.ts +97 -0
  150. package/payload/shared/install-commands.ts +7 -0
  151. package/payload/shared/legacy-names.d.mts +35 -0
  152. package/payload/shared/legacy-names.mjs +244 -0
  153. package/payload/shared/live-view.ts +94 -0
  154. package/payload/shared/look-critter.ts +189 -0
  155. package/payload/shared/look-tater.ts +127 -0
  156. package/payload/shared/look.ts +200 -0
  157. package/payload/shared/reactions.ts +43 -0
  158. package/payload/shared/seeded.ts +34 -0
  159. package/payload/shared/services.ts +90 -0
  160. package/payload/shared/types.ts +3016 -0
  161. package/payload/shared/util.ts +68 -0
  162. package/payload/shared/versions.ts +44 -0
  163. package/payload/shared/wake.ts +164 -0
  164. package/payload/web/dist/apple-touch-icon.png +0 -0
  165. package/payload/web/dist/assets/Icons-nytVAa9g.js +1 -0
  166. package/payload/web/dist/assets/NewBotDialog-DXr2XhDP.js +1 -0
  167. package/payload/web/dist/assets/PairScreen-CMcJO7cQ.js +1 -0
  168. package/payload/web/dist/assets/Panel-Bvqz3I3m.js +4 -0
  169. package/payload/web/dist/assets/ReactionPicker-B0SKsjiy.js +1 -0
  170. package/payload/web/dist/assets/SettingRows-CvLomtuo.js +4 -0
  171. package/payload/web/dist/assets/SettingsDialog-CQ41LV-P.js +8 -0
  172. package/payload/web/dist/assets/Setup-C-3P7Mtn.js +1 -0
  173. package/payload/web/dist/assets/Skills-DXMH69kH.js +1 -0
  174. package/payload/web/dist/assets/StarterSetup-Ce2V1_Cs.js +5 -0
  175. package/payload/web/dist/assets/Steps-CKtB3GAM.js +1 -0
  176. package/payload/web/dist/assets/emoji-catalog-DfpPz8qw.js +540 -0
  177. package/payload/web/dist/assets/index-BEU7FDxV.css +1 -0
  178. package/payload/web/dist/assets/index-Dhwn-rf0.js +3 -0
  179. package/payload/web/dist/assets/react-C1iE8mnY.js +9 -0
  180. package/payload/web/dist/assets/reactions-rw0YeCmA.js +1 -0
  181. package/payload/web/dist/assets/rolldown-runtime-XKyaHtl9.js +1 -0
  182. package/payload/web/dist/assets/util-Cx5feSJu.js +1 -0
  183. package/payload/web/dist/assets/vendor-B3qdcjGo.js +14 -0
  184. package/payload/web/dist/favicon.svg +1 -0
  185. package/payload/web/dist/icon-192.png +0 -0
  186. package/payload/web/dist/icon-512.png +0 -0
  187. package/payload/web/dist/icon.svg +1 -0
  188. package/payload/web/dist/index.html +67 -0
  189. package/payload/web/dist/manifest.webmanifest +12 -0
  190. package/shared/legacy-names.d.mts +35 -0
  191. package/shared/legacy-names.mjs +244 -0
@@ -0,0 +1,214 @@
1
+ import { Cron } from 'croner';
2
+
3
+ type Schedule =
4
+ | { kind: 'cron'; expr: string }
5
+ | { kind: 'every'; minutes: number }
6
+ | { kind: 'once'; at: Date };
7
+
8
+ const DAYS = ['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday'];
9
+ const DAY_ABBR = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
10
+
11
+ const SCHEDULE_HELP =
12
+ 'Use a 5-field cron expression like "0 8 * * 1-5", "every 30m" / "every 2h" / "every 1d", ' +
13
+ '"daily at 8:00", "weekdays at 7:30am", "mondays at 9am", "in 10m", or "at 2026-11-05 09:00".';
14
+
15
+ export function parseSchedule(input: string, tz: string, now = new Date()): Schedule {
16
+ const s = parseAny(input, tz, now);
17
+ if (s.kind === 'once' && s.at.getTime() <= now.getTime()) {
18
+ throw new Error(`"${input.trim()}" (${describeSchedule(toSpec(s), tz).replace(/^Once, /, '')}) has already passed. A one-off routine needs a time in the future.`);
19
+ }
20
+ return s;
21
+ }
22
+
23
+ function parseAny(input: string, tz: string, now: Date): Schedule {
24
+ const raw = input.trim();
25
+ const s = raw.toLowerCase().replace(/\s+/g, ' ');
26
+ if (!s) throw new Error(`Empty schedule. ${SCHEDULE_HELP}`);
27
+
28
+ if (s.startsWith('cron ') || s.startsWith('cron:')) return cron(raw.slice(5).trim(), tz);
29
+ if (/^every \d/.test(s)) return { kind: 'every', minutes: parseDuration(s.slice(6), raw) };
30
+ if (s === 'every minute') return { kind: 'every', minutes: 1 };
31
+ if (s === 'hourly' || s === 'every hour') return cron('0 * * * *', tz);
32
+ if (s.startsWith('once ')) return parseAny(raw.replace(/^once\s+/i, 'at '), tz, now);
33
+ if (s.startsWith('in ')) return { kind: 'once', at: new Date(now.getTime() + parseDuration(s.slice(3), raw) * 60_000) };
34
+ if (s.startsWith('at ') && /\d{4}-\d{2}-\d{2}/.test(s)) return { kind: 'once', at: parseDateTime(raw.slice(3).trim(), tz) };
35
+
36
+ const timed = /^(?:every )?(day|daily|weekday|weekdays|weekend|weekends|sunday|monday|tuesday|wednesday|thursday|friday|saturday)s? at (.+)$/.exec(
37
+ s.replace(/^daily at/, 'day at'),
38
+ );
39
+ if (timed) {
40
+ const { h, m } = parseClock(timed[2], raw);
41
+ const word = timed[1];
42
+ let dow = '*';
43
+ if (word.startsWith('weekday')) dow = '1-5';
44
+ else if (word.startsWith('weekend')) dow = '0,6';
45
+ else if (DAYS.includes(word)) dow = String(DAYS.indexOf(word));
46
+ return cron(`${m} ${h} * * ${dow}`, tz);
47
+ }
48
+
49
+ // Bare cron: numeric-ish minute and hour fields rule out ordinary words.
50
+ const fields = raw.split(/\s+/);
51
+ if ((fields.length === 5 || fields.length === 6) && fields.slice(0, 2).every((f) => /^[\d*/,\-]+$/.test(f))) return cron(raw, tz);
52
+ if (/^\d{4}-\d{2}-\d{2}/.test(s)) return { kind: 'once', at: parseDateTime(raw, tz) };
53
+
54
+ throw new Error(`Couldn't understand schedule "${raw}". ${SCHEDULE_HELP}`);
55
+ }
56
+
57
+ function cron(expr: string, tz: string): Schedule {
58
+ let job: Cron;
59
+ try {
60
+ job = new Cron(expr, { timezone: tz, paused: true });
61
+ } catch (e) {
62
+ throw new Error(`Invalid cron expression "${expr}": ${(e as Error).message}. ${SCHEDULE_HELP}`);
63
+ }
64
+ const runs = job.nextRuns(2);
65
+ if (runs.length === 2 && runs[1].getTime() - runs[0].getTime() < 60_000) {
66
+ throw new Error('Schedules must be at least one minute apart.');
67
+ }
68
+ return { kind: 'cron', expr };
69
+ }
70
+
71
+ function parseDuration(text: string, raw: string): number {
72
+ const m = /^(\d+(?:\.\d+)?) ?(m|min|mins|minute|minutes|h|hr|hrs|hour|hours|d|day|days)$/.exec(text.trim());
73
+ if (!m) throw new Error(`Couldn't read the interval in "${raw}". ${SCHEDULE_HELP}`);
74
+ const n = Number(m[1]);
75
+ const unit = m[2][0];
76
+ const minutes = Math.round(unit === 'm' ? n : unit === 'h' ? n * 60 : n * 1440);
77
+ if (minutes < 1) throw new Error('Schedules must be at least one minute apart.');
78
+ return minutes;
79
+ }
80
+
81
+ function parseClock(text: string, raw: string): { h: number; m: number } {
82
+ const m = /^(\d{1,2})(?::(\d{2}))? ?(am|pm|a\.m\.|p\.m\.)?$/.exec(text.trim());
83
+ if (!m) throw new Error(`Couldn't read the time in "${raw}". ${SCHEDULE_HELP}`);
84
+ let h = Number(m[1]);
85
+ const min = Number(m[2] ?? 0);
86
+ const ap = m[3]?.[0];
87
+ if (ap === 'p' && h < 12) h += 12;
88
+ if (ap === 'a' && h === 12) h = 0;
89
+ if (h > 23 || min > 59) throw new Error(`Invalid time in "${raw}".`);
90
+ return { h, m: min };
91
+ }
92
+
93
+ function parseDateTime(text: string, tz: string): Date {
94
+ if (/[zZ]$|[+-]\d{2}:?\d{2}$/.test(text)) {
95
+ const d = new Date(text);
96
+ if (Number.isNaN(d.getTime())) throw new Error(`Invalid date "${text}".`);
97
+ return d;
98
+ }
99
+ const m = /^(\d{4})-(\d{2})-(\d{2})(?:[ T](\d{1,2}):(\d{2})(?::\d{2})?)?$/.exec(text.trim());
100
+ if (!m) throw new Error(`Invalid date "${text}". Use YYYY-MM-DD HH:MM.`);
101
+ return zonedToUtc(+m[1], +m[2], +m[3], +(m[4] ?? 9), +(m[5] ?? 0), tz);
102
+ }
103
+
104
+ function tzOffset(ts: number, tz: string): number {
105
+ const parts = new Intl.DateTimeFormat('en-US', {
106
+ timeZone: tz,
107
+ hourCycle: 'h23',
108
+ year: 'numeric',
109
+ month: 'numeric',
110
+ day: 'numeric',
111
+ hour: 'numeric',
112
+ minute: 'numeric',
113
+ second: 'numeric',
114
+ }).formatToParts(new Date(ts));
115
+ const get = (t: string) => Number(parts.find((p) => p.type === t)?.value);
116
+ const wall = Date.UTC(get('year'), get('month') - 1, get('day'), get('hour'), get('minute'), get('second'));
117
+ return wall - Math.floor(ts / 1000) * 1000;
118
+ }
119
+
120
+ export function zonedToUtc(y: number, mo: number, d: number, h: number, mi: number, tz: string): Date {
121
+ const guess = Date.UTC(y, mo - 1, d, h, mi);
122
+ const off1 = tzOffset(guess, tz);
123
+ let utc = guess - off1;
124
+ const off2 = tzOffset(utc, tz);
125
+ if (off2 !== off1) utc = guess - off2;
126
+ return new Date(utc);
127
+ }
128
+
129
+ export function toSpec(s: Schedule): string {
130
+ if (s.kind === 'cron') return `cron ${s.expr}`;
131
+ if (s.kind === 'every') return `every ${s.minutes}m`;
132
+ return `once ${s.at.toISOString()}`;
133
+ }
134
+
135
+ function fromSpec(spec: string): Schedule {
136
+ if (spec.startsWith('cron ')) return { kind: 'cron', expr: spec.slice(5) };
137
+ if (spec.startsWith('every ')) return { kind: 'every', minutes: parseInt(spec.slice(6), 10) };
138
+ if (spec.startsWith('once ')) return { kind: 'once', at: new Date(spec.slice(5)) };
139
+ throw new Error(`Bad schedule spec: ${spec}`);
140
+ }
141
+
142
+ /** Strictly after `after`; null once a one-off has passed. */
143
+ export function nextRun(spec: string, tz: string, after: Date): Date | null {
144
+ const s = fromSpec(spec);
145
+ if (s.kind === 'cron') return new Cron(s.expr, { timezone: tz, paused: true }).nextRun(after);
146
+ if (s.kind === 'every') return new Date(after.getTime() + s.minutes * 60_000);
147
+ return s.at.getTime() > after.getTime() ? s.at : null;
148
+ }
149
+
150
+ export function nextRuns(spec: string, tz: string, n: number, after = new Date()): Date[] {
151
+ const out: Date[] = [];
152
+ let cursor = after;
153
+ for (let i = 0; i < n; i++) {
154
+ const next = nextRun(spec, tz, cursor);
155
+ if (!next) break;
156
+ out.push(next);
157
+ cursor = next;
158
+ }
159
+ return out;
160
+ }
161
+
162
+ function clock(h: number, m: number): string {
163
+ const ap = h >= 12 ? 'PM' : 'AM';
164
+ const hh = h % 12 === 0 ? 12 : h % 12;
165
+ return `${hh}:${String(m).padStart(2, '0')} ${ap}`;
166
+ }
167
+
168
+ function plural(n: number, unit: string): string {
169
+ return n === 1 ? `Every ${unit}` : `Every ${n} ${unit}s`;
170
+ }
171
+
172
+ export function describeSchedule(spec: string, tz: string): string {
173
+ const s = fromSpec(spec);
174
+ if (s.kind === 'every') {
175
+ if (s.minutes % 1440 === 0) return plural(s.minutes / 1440, 'day');
176
+ if (s.minutes % 60 === 0) return plural(s.minutes / 60, 'hour');
177
+ return plural(s.minutes, 'minute');
178
+ }
179
+ if (s.kind === 'once') {
180
+ const when = s.at.toLocaleString('en-US', {
181
+ timeZone: tz,
182
+ weekday: 'short',
183
+ month: 'short',
184
+ day: 'numeric',
185
+ hour: 'numeric',
186
+ minute: '2-digit',
187
+ });
188
+ return `Once, ${when}`;
189
+ }
190
+ const f = s.expr.split(/\s+/);
191
+ if (f.length !== 5) return `Cron: ${s.expr}`;
192
+ const [min, hour, dom, mon, dow] = f;
193
+ const num = (x: string) => /^\d+$/.test(x);
194
+ if (num(min) && num(hour) && dom === '*' && mon === '*') {
195
+ const at = clock(+hour, +min);
196
+ if (dow === '*') return `Every day at ${at}`;
197
+ if (dow === '1-5') return `Weekdays at ${at}`;
198
+ if (dow === '0,6' || dow === '6,0') return `Weekends at ${at}`;
199
+ if (/^[0-6](,[0-6])*$/.test(dow)) {
200
+ const days = dow.split(',').map((d) => DAY_ABBR[+d]);
201
+ return days.length === 1 ? `${DAYS[+dow][0].toUpperCase()}${DAYS[+dow].slice(1)}s at ${at}` : `${days.join(', ')} at ${at}`;
202
+ }
203
+ }
204
+ if (num(min) && num(hour) && num(dom) && mon === '*' && dow === '*') return `Monthly on day ${dom} at ${clock(+hour, +min)}`;
205
+ if (hour === '*' && dom === '*' && mon === '*' && dow === '*') {
206
+ if (min === '*') return 'Every minute';
207
+ if (/^\*\/\d+$/.test(min)) return plural(+min.slice(2), 'minute');
208
+ if (num(min)) return `Every hour at :${min.padStart(2, '0')}`;
209
+ }
210
+ if (num(min) && /^\*\/\d+$/.test(hour) && dom === '*' && mon === '*' && dow === '*') {
211
+ return plural(+hour.slice(2), 'hour');
212
+ }
213
+ return `Cron: ${s.expr}`;
214
+ }
@@ -0,0 +1,24 @@
1
+ import type { Engine } from './engine.ts';
2
+ import { log } from './log.ts';
3
+ import { nextRun } from './schedule.ts';
4
+ import type { Store } from './store.ts';
5
+
6
+ /** Fires due routines, once each however many slots were missed while the server was down. */
7
+ export function startScheduler(store: Store, engine: Engine, intervalMs: number, onTick: () => void = () => {}): () => void {
8
+ const tick = () => {
9
+ const now = new Date();
10
+ for (const routine of store.dueRoutines(now)) {
11
+ try {
12
+ if (routine.schedule?.startsWith('once ')) store.retireOneOff(routine.id);
13
+ else store.setNextRun(routine.id, routine.schedule ? nextRun(routine.schedule, routine.timezone, now) : null);
14
+ engine.fireScheduled(routine);
15
+ } catch (err) {
16
+ log.error('scheduled routine failed to start', { botId: routine.botId, routineId: routine.id, trigger: 'schedule', err });
17
+ }
18
+ }
19
+ onTick();
20
+ };
21
+ tick();
22
+ const timer = setInterval(tick, intervalMs);
23
+ return () => clearInterval(timer);
24
+ }