@namzu/sdk 45.0.0 → 46.0.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 (281) hide show
  1. package/CHANGELOG.md +166 -0
  2. package/dist/authorization/gate.d.ts +5 -2
  3. package/dist/authorization/gate.d.ts.map +1 -1
  4. package/dist/authorization/gate.js +35 -4
  5. package/dist/authorization/gate.js.map +1 -1
  6. package/dist/authorization/rules.d.ts.map +1 -1
  7. package/dist/authorization/rules.js +56 -6
  8. package/dist/authorization/rules.js.map +1 -1
  9. package/dist/authorization/shell-lexer.d.ts +38 -0
  10. package/dist/authorization/shell-lexer.d.ts.map +1 -1
  11. package/dist/authorization/shell-lexer.js +88 -57
  12. package/dist/authorization/shell-lexer.js.map +1 -1
  13. package/dist/bridge/a2a/mapper.d.ts.map +1 -1
  14. package/dist/bridge/a2a/mapper.js +2 -0
  15. package/dist/bridge/a2a/mapper.js.map +1 -1
  16. package/dist/bridge/sse/mapper.d.ts.map +1 -1
  17. package/dist/bridge/sse/mapper.js +1 -0
  18. package/dist/bridge/sse/mapper.js.map +1 -1
  19. package/dist/directory/types.d.ts +2 -0
  20. package/dist/directory/types.d.ts.map +1 -1
  21. package/dist/directory/types.js.map +1 -1
  22. package/dist/manager/resident/outbox.d.ts +4 -4
  23. package/dist/pricing/catalogue.generated.d.ts.map +1 -1
  24. package/dist/pricing/catalogue.generated.js +28 -4
  25. package/dist/pricing/catalogue.generated.js.map +1 -1
  26. package/dist/prompt/coding-agent-doctrine.d.ts +1 -1
  27. package/dist/prompt/coding-agent-doctrine.d.ts.map +1 -1
  28. package/dist/prompt/coding-agent-doctrine.js +1 -0
  29. package/dist/prompt/coding-agent-doctrine.js.map +1 -1
  30. package/dist/public-runtime.d.ts +6 -3
  31. package/dist/public-runtime.d.ts.map +1 -1
  32. package/dist/public-runtime.js +14 -2
  33. package/dist/public-runtime.js.map +1 -1
  34. package/dist/public-tools.d.ts +7 -2
  35. package/dist/public-tools.d.ts.map +1 -1
  36. package/dist/public-tools.js +15 -2
  37. package/dist/public-tools.js.map +1 -1
  38. package/dist/public-types.d.ts +8 -2
  39. package/dist/public-types.d.ts.map +1 -1
  40. package/dist/registry/tool/callable.d.ts +22 -0
  41. package/dist/registry/tool/callable.d.ts.map +1 -0
  42. package/dist/registry/tool/callable.js +29 -0
  43. package/dist/registry/tool/callable.js.map +1 -0
  44. package/dist/registry/tool/execute.d.ts.map +1 -1
  45. package/dist/registry/tool/execute.js +8 -1
  46. package/dist/registry/tool/execute.js.map +1 -1
  47. package/dist/runtime/query/declined.d.ts +12 -0
  48. package/dist/runtime/query/declined.d.ts.map +1 -0
  49. package/dist/runtime/query/declined.js +12 -0
  50. package/dist/runtime/query/declined.js.map +1 -0
  51. package/dist/runtime/query/executor/tool-call-admission.d.ts +37 -11
  52. package/dist/runtime/query/executor/tool-call-admission.d.ts.map +1 -1
  53. package/dist/runtime/query/executor/tool-call-admission.js +38 -12
  54. package/dist/runtime/query/executor/tool-call-admission.js.map +1 -1
  55. package/dist/runtime/query/executor.d.ts +7 -3
  56. package/dist/runtime/query/executor.d.ts.map +1 -1
  57. package/dist/runtime/query/executor.js +32 -7
  58. package/dist/runtime/query/executor.js.map +1 -1
  59. package/dist/runtime/query/index.d.ts.map +1 -1
  60. package/dist/runtime/query/index.js +1 -0
  61. package/dist/runtime/query/index.js.map +1 -1
  62. package/dist/runtime/query/iteration/index.d.ts.map +1 -1
  63. package/dist/runtime/query/iteration/index.js +7 -0
  64. package/dist/runtime/query/iteration/index.js.map +1 -1
  65. package/dist/runtime/query/iteration/phases/context.d.ts +6 -0
  66. package/dist/runtime/query/iteration/phases/context.d.ts.map +1 -1
  67. package/dist/runtime/query/iteration/phases/context.js.map +1 -1
  68. package/dist/runtime/query/iteration/phases/handoff.d.ts +22 -0
  69. package/dist/runtime/query/iteration/phases/handoff.d.ts.map +1 -0
  70. package/dist/runtime/query/iteration/phases/handoff.js +65 -0
  71. package/dist/runtime/query/iteration/phases/handoff.js.map +1 -0
  72. package/dist/runtime/query/iteration/phases/index.d.ts +1 -0
  73. package/dist/runtime/query/iteration/phases/index.d.ts.map +1 -1
  74. package/dist/runtime/query/iteration/phases/index.js +1 -0
  75. package/dist/runtime/query/iteration/phases/index.js.map +1 -1
  76. package/dist/runtime/query/iteration/phases/tool-review.d.ts.map +1 -1
  77. package/dist/runtime/query/iteration/phases/tool-review.js +8 -3
  78. package/dist/runtime/query/iteration/phases/tool-review.js.map +1 -1
  79. package/dist/runtime/query/resume-pending.d.ts.map +1 -1
  80. package/dist/runtime/query/resume-pending.js +3 -2
  81. package/dist/runtime/query/resume-pending.js.map +1 -1
  82. package/dist/runtime/query/review-policy.d.ts +43 -0
  83. package/dist/runtime/query/review-policy.d.ts.map +1 -1
  84. package/dist/runtime/query/review-policy.js +63 -19
  85. package/dist/runtime/query/review-policy.js.map +1 -1
  86. package/dist/schedules/cron.d.ts +21 -0
  87. package/dist/schedules/cron.d.ts.map +1 -0
  88. package/dist/schedules/cron.js +167 -0
  89. package/dist/schedules/cron.js.map +1 -0
  90. package/dist/schedules/describe.d.ts +14 -0
  91. package/dist/schedules/describe.d.ts.map +1 -0
  92. package/dist/schedules/describe.js +133 -0
  93. package/dist/schedules/describe.js.map +1 -0
  94. package/dist/schedules/errors.d.ts +11 -0
  95. package/dist/schedules/errors.d.ts.map +1 -0
  96. package/dist/schedules/errors.js +15 -0
  97. package/dist/schedules/errors.js.map +1 -0
  98. package/dist/schedules/evaluate.d.ts +35 -0
  99. package/dist/schedules/evaluate.d.ts.map +1 -0
  100. package/dist/schedules/evaluate.js +158 -0
  101. package/dist/schedules/evaluate.js.map +1 -0
  102. package/dist/schedules/index.d.ts +11 -0
  103. package/dist/schedules/index.d.ts.map +1 -0
  104. package/dist/schedules/index.js +8 -0
  105. package/dist/schedules/index.js.map +1 -0
  106. package/dist/schedules/next-fire.d.ts +50 -0
  107. package/dist/schedules/next-fire.d.ts.map +1 -0
  108. package/dist/schedules/next-fire.js +250 -0
  109. package/dist/schedules/next-fire.js.map +1 -0
  110. package/dist/schedules/spec.d.ts +30 -0
  111. package/dist/schedules/spec.d.ts.map +1 -0
  112. package/dist/schedules/spec.js +169 -0
  113. package/dist/schedules/spec.js.map +1 -0
  114. package/dist/schedules/types.d.ts +144 -0
  115. package/dist/schedules/types.d.ts.map +1 -0
  116. package/dist/schedules/types.js +11 -0
  117. package/dist/schedules/types.js.map +1 -0
  118. package/dist/schedules/tz.d.ts +44 -0
  119. package/dist/schedules/tz.d.ts.map +1 -0
  120. package/dist/schedules/tz.js +141 -0
  121. package/dist/schedules/tz.js.map +1 -0
  122. package/dist/skills/index.d.ts +1 -1
  123. package/dist/skills/index.d.ts.map +1 -1
  124. package/dist/skills/index.js +1 -1
  125. package/dist/skills/index.js.map +1 -1
  126. package/dist/skills/loader.d.ts +16 -3
  127. package/dist/skills/loader.d.ts.map +1 -1
  128. package/dist/skills/loader.js +48 -4
  129. package/dist/skills/loader.js.map +1 -1
  130. package/dist/skills/registry.d.ts +6 -0
  131. package/dist/skills/registry.d.ts.map +1 -1
  132. package/dist/skills/registry.js +1 -0
  133. package/dist/skills/registry.js.map +1 -1
  134. package/dist/tools/builtins/browser-url.d.ts +83 -0
  135. package/dist/tools/builtins/browser-url.d.ts.map +1 -0
  136. package/dist/tools/builtins/browser-url.js +240 -0
  137. package/dist/tools/builtins/browser-url.js.map +1 -0
  138. package/dist/tools/builtins/browser.d.ts +367 -0
  139. package/dist/tools/builtins/browser.d.ts.map +1 -0
  140. package/dist/tools/builtins/browser.js +704 -0
  141. package/dist/tools/builtins/browser.js.map +1 -0
  142. package/dist/tools/builtins/computer-use-coordinates.d.ts +65 -0
  143. package/dist/tools/builtins/computer-use-coordinates.d.ts.map +1 -0
  144. package/dist/tools/builtins/computer-use-coordinates.js +123 -0
  145. package/dist/tools/builtins/computer-use-coordinates.js.map +1 -0
  146. package/dist/tools/builtins/computer-use-image.d.ts +77 -0
  147. package/dist/tools/builtins/computer-use-image.d.ts.map +1 -0
  148. package/dist/tools/builtins/computer-use-image.js +223 -0
  149. package/dist/tools/builtins/computer-use-image.js.map +1 -0
  150. package/dist/tools/builtins/computer-use.d.ts +519 -14
  151. package/dist/tools/builtins/computer-use.d.ts.map +1 -1
  152. package/dist/tools/builtins/computer-use.js +1188 -183
  153. package/dist/tools/builtins/computer-use.js.map +1 -1
  154. package/dist/tools/builtins/index.d.ts +2 -1
  155. package/dist/tools/builtins/index.d.ts.map +1 -1
  156. package/dist/tools/builtins/index.js +1 -1
  157. package/dist/tools/builtins/index.js.map +1 -1
  158. package/dist/tools/builtins/skill.d.ts +74 -0
  159. package/dist/tools/builtins/skill.d.ts.map +1 -1
  160. package/dist/tools/builtins/skill.js +214 -159
  161. package/dist/tools/builtins/skill.js.map +1 -1
  162. package/dist/tools/defineTool.d.ts +4 -0
  163. package/dist/tools/defineTool.d.ts.map +1 -1
  164. package/dist/tools/defineTool.js +8 -0
  165. package/dist/tools/defineTool.js.map +1 -1
  166. package/dist/tools/schedules/index.d.ts +5 -0
  167. package/dist/tools/schedules/index.d.ts.map +1 -0
  168. package/dist/tools/schedules/index.js +4 -0
  169. package/dist/tools/schedules/index.js.map +1 -0
  170. package/dist/tools/schedules/loop-tool.d.ts +14 -0
  171. package/dist/tools/schedules/loop-tool.d.ts.map +1 -0
  172. package/dist/tools/schedules/loop-tool.js +81 -0
  173. package/dist/tools/schedules/loop-tool.js.map +1 -0
  174. package/dist/tools/schedules/present.d.ts +16 -0
  175. package/dist/tools/schedules/present.d.ts.map +1 -0
  176. package/dist/tools/schedules/present.js +71 -0
  177. package/dist/tools/schedules/present.js.map +1 -0
  178. package/dist/tools/schedules/prompt-scan.d.ts +17 -0
  179. package/dist/tools/schedules/prompt-scan.d.ts.map +1 -0
  180. package/dist/tools/schedules/prompt-scan.js +92 -0
  181. package/dist/tools/schedules/prompt-scan.js.map +1 -0
  182. package/dist/tools/schedules/schedule-tool.d.ts +17 -0
  183. package/dist/tools/schedules/schedule-tool.d.ts.map +1 -0
  184. package/dist/tools/schedules/schedule-tool.js +418 -0
  185. package/dist/tools/schedules/schedule-tool.js.map +1 -0
  186. package/dist/tools/schedules/types.d.ts +252 -0
  187. package/dist/tools/schedules/types.d.ts.map +1 -0
  188. package/dist/tools/schedules/types.js +11 -0
  189. package/dist/tools/schedules/types.js.map +1 -0
  190. package/dist/types/authorization/index.d.ts +107 -9
  191. package/dist/types/authorization/index.d.ts.map +1 -1
  192. package/dist/types/authorization/index.js +16 -1
  193. package/dist/types/authorization/index.js.map +1 -1
  194. package/dist/types/browser/index.d.ts +280 -0
  195. package/dist/types/browser/index.d.ts.map +1 -0
  196. package/dist/types/browser/index.js +12 -0
  197. package/dist/types/browser/index.js.map +1 -0
  198. package/dist/types/computer-use/index.d.ts +176 -0
  199. package/dist/types/computer-use/index.d.ts.map +1 -1
  200. package/dist/types/computer-use/index.js.map +1 -1
  201. package/dist/types/session/events.d.ts +6 -0
  202. package/dist/types/session/events.d.ts.map +1 -1
  203. package/dist/types/session/events.js.map +1 -1
  204. package/dist/types/session/records.d.ts +23 -0
  205. package/dist/types/session/records.d.ts.map +1 -1
  206. package/dist/types/session/records.js +9 -0
  207. package/dist/types/session/records.js.map +1 -1
  208. package/dist/types/tool/index.d.ts +60 -0
  209. package/dist/types/tool/index.d.ts.map +1 -1
  210. package/dist/types/tool/index.js.map +1 -1
  211. package/dist/types/tool/presentation.d.ts +7 -0
  212. package/dist/types/tool/presentation.d.ts.map +1 -1
  213. package/dist/utils/frontmatter.d.ts +18 -2
  214. package/dist/utils/frontmatter.d.ts.map +1 -1
  215. package/dist/utils/frontmatter.js +13 -3
  216. package/dist/utils/frontmatter.js.map +1 -1
  217. package/dist/utils/id.d.ts +8 -0
  218. package/dist/utils/id.d.ts.map +1 -1
  219. package/dist/utils/id.js +12 -0
  220. package/dist/utils/id.js.map +1 -1
  221. package/package.json +3 -1
  222. package/src/authorization/gate.ts +37 -4
  223. package/src/authorization/rules.ts +55 -7
  224. package/src/authorization/shell-lexer.ts +109 -51
  225. package/src/bridge/a2a/mapper.ts +2 -0
  226. package/src/bridge/sse/mapper.ts +1 -0
  227. package/src/directory/types.ts +2 -0
  228. package/src/pricing/catalogue.generated.ts +28 -4
  229. package/src/pricing/rates.source.json +27 -6
  230. package/src/prompt/coding-agent-doctrine.ts +1 -0
  231. package/src/public-runtime.ts +43 -1
  232. package/src/public-tools.ts +54 -1
  233. package/src/public-types.ts +55 -0
  234. package/src/registry/tool/callable.ts +37 -0
  235. package/src/registry/tool/execute.ts +8 -1
  236. package/src/runtime/query/declined.ts +12 -0
  237. package/src/runtime/query/executor/tool-call-admission.ts +60 -16
  238. package/src/runtime/query/executor.ts +36 -5
  239. package/src/runtime/query/index.ts +1 -0
  240. package/src/runtime/query/iteration/index.ts +8 -0
  241. package/src/runtime/query/iteration/phases/context.ts +6 -0
  242. package/src/runtime/query/iteration/phases/handoff.ts +74 -0
  243. package/src/runtime/query/iteration/phases/index.ts +1 -0
  244. package/src/runtime/query/iteration/phases/tool-review.ts +11 -4
  245. package/src/runtime/query/resume-pending.ts +3 -2
  246. package/src/runtime/query/review-policy.ts +107 -18
  247. package/src/schedules/cron.ts +202 -0
  248. package/src/schedules/describe.ts +138 -0
  249. package/src/schedules/errors.ts +15 -0
  250. package/src/schedules/evaluate.ts +178 -0
  251. package/src/schedules/index.ts +18 -0
  252. package/src/schedules/next-fire.ts +257 -0
  253. package/src/schedules/spec.ts +210 -0
  254. package/src/schedules/types.ts +163 -0
  255. package/src/schedules/tz.ts +155 -0
  256. package/src/skills/index.ts +1 -1
  257. package/src/skills/loader.ts +55 -4
  258. package/src/skills/registry.ts +8 -0
  259. package/src/tools/builtins/browser-url.ts +251 -0
  260. package/src/tools/builtins/browser.ts +817 -0
  261. package/src/tools/builtins/computer-use-coordinates.ts +144 -0
  262. package/src/tools/builtins/computer-use-image.ts +278 -0
  263. package/src/tools/builtins/computer-use.ts +1485 -191
  264. package/src/tools/builtins/index.ts +7 -1
  265. package/src/tools/builtins/skill.ts +304 -156
  266. package/src/tools/defineTool.ts +13 -0
  267. package/src/tools/schedules/index.ts +4 -0
  268. package/src/tools/schedules/loop-tool.ts +85 -0
  269. package/src/tools/schedules/present.ts +88 -0
  270. package/src/tools/schedules/prompt-scan.ts +96 -0
  271. package/src/tools/schedules/schedule-tool.ts +482 -0
  272. package/src/tools/schedules/types.ts +265 -0
  273. package/src/types/authorization/index.ts +81 -2
  274. package/src/types/browser/index.ts +341 -0
  275. package/src/types/computer-use/index.ts +202 -0
  276. package/src/types/session/events.ts +6 -0
  277. package/src/types/session/records.ts +10 -0
  278. package/src/types/tool/index.ts +61 -0
  279. package/src/types/tool/presentation.ts +7 -0
  280. package/src/utils/frontmatter.ts +29 -3
  281. package/src/utils/id.ts +14 -0
@@ -0,0 +1,138 @@
1
+ /**
2
+ * A schedule in words, for a confirmation screen or a listing.
3
+ *
4
+ * Covers the common shapes exactly and falls back to the expression itself
5
+ * for anything else, rather than paraphrasing it wrongly.
6
+ */
7
+
8
+ import { parseCronExpression } from './cron.js'
9
+ import type { CronExpression, ScheduleSpec } from './types.js'
10
+ import { formatWall } from './tz.js'
11
+
12
+ const MINUTE = 60_000
13
+ const DAY_NAMES = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']
14
+ const MONTH_NAMES = [
15
+ 'January',
16
+ 'February',
17
+ 'March',
18
+ 'April',
19
+ 'May',
20
+ 'June',
21
+ 'July',
22
+ 'August',
23
+ 'September',
24
+ 'October',
25
+ 'November',
26
+ 'December',
27
+ ]
28
+
29
+ export interface DescribeScheduleOptions {
30
+ /** Zone an `at` or `every` anchor is shown in. Default UTC. */
31
+ readonly tz?: string
32
+ }
33
+
34
+ function pad(n: number): string {
35
+ return String(n).padStart(2, '0')
36
+ }
37
+
38
+ function plural(n: number, unit: string): string {
39
+ return `${n} ${unit}${n === 1 ? '' : 's'}`
40
+ }
41
+
42
+ function describeInterval(ms: number): string {
43
+ if (ms % (7 * 24 * 60 * MINUTE) === 0) {
44
+ const w = ms / (7 * 24 * 60 * MINUTE)
45
+ return w === 1 ? 'every week' : `every ${plural(w, 'week')}`
46
+ }
47
+ if (ms % (24 * 60 * MINUTE) === 0) {
48
+ const d = ms / (24 * 60 * MINUTE)
49
+ return d === 1 ? 'every 24 hours' : `every ${plural(d, 'day')}`
50
+ }
51
+ if (ms % (60 * MINUTE) === 0) {
52
+ const h = ms / (60 * MINUTE)
53
+ return h === 1 ? 'every hour' : `every ${plural(h, 'hour')}`
54
+ }
55
+ const m = ms / MINUTE
56
+ return m === 1 ? 'every minute' : `every ${plural(m, 'minute')}`
57
+ }
58
+
59
+ /** `1,2,3,5` → `1–3, 5`. */
60
+ function ranges(values: readonly number[], name: (n: number) => string): string {
61
+ const out: string[] = []
62
+ let i = 0
63
+ while (i < values.length) {
64
+ let j = i
65
+ while (j + 1 < values.length && (values[j + 1] as number) === (values[j] as number) + 1) j++
66
+ const a = values[i] as number
67
+ const b = values[j] as number
68
+ if (j - i >= 2) out.push(`${name(a)} through ${name(b)}`)
69
+ else for (let k = i; k <= j; k++) out.push(name(values[k] as number))
70
+ i = j + 1
71
+ }
72
+ return out.join(', ')
73
+ }
74
+
75
+ function isFull(values: readonly number[], min: number, max: number): boolean {
76
+ return values.length === max - min + 1
77
+ }
78
+
79
+ function describeDays(e: CronExpression): string {
80
+ const allDom = !e.domRestricted
81
+ const allDow = !e.dowRestricted
82
+ const allMonths = isFull(e.months, 1, 12)
83
+ let days: string
84
+ if (allDom && allDow) days = 'every day'
85
+ else if (allDom) days = `on ${ranges(e.daysOfWeek, (d) => DAY_NAMES[d] ?? String(d))}`
86
+ else if (allDow) days = `on day ${ranges(e.daysOfMonth, String)} of the month`
87
+ else
88
+ days = `on day ${ranges(e.daysOfMonth, String)} of the month and on ${ranges(e.daysOfWeek, (d) => DAY_NAMES[d] ?? String(d))}`
89
+ if (!allMonths) days += ` in ${ranges(e.months, (m) => MONTH_NAMES[m - 1] ?? String(m))}`
90
+ return days
91
+ }
92
+
93
+ function describeTimes(e: CronExpression, source: string): string {
94
+ const [mi = '', ho = ''] = source.split(' ')
95
+ const allMinutes = isFull(e.minutes, 0, 59)
96
+ const allHours = isFull(e.hours, 0, 23)
97
+ if (allMinutes && allHours) return 'every minute'
98
+ const stepMinute = /^\*\/(\d+)$/.exec(mi)
99
+ if (stepMinute && allHours) return `every ${plural(Number(stepMinute[1]), 'minute')}`
100
+ if (allHours && e.minutes.length <= 4) {
101
+ return e.minutes.length === 1 && e.minutes[0] === 0
102
+ ? 'every hour, on the hour'
103
+ : `every hour at minute ${e.minutes.join(', ')}`
104
+ }
105
+ const stepHour = /^\*\/(\d+)$/.exec(ho)
106
+ if (stepHour && e.minutes.length === 1) {
107
+ return `every ${plural(Number(stepHour[1]), 'hour')} at minute ${e.minutes[0]}`
108
+ }
109
+ if (e.hours.length * e.minutes.length <= 6) {
110
+ const times: string[] = []
111
+ for (const h of e.hours) for (const m of e.minutes) times.push(`${pad(h)}:${pad(m)}`)
112
+ return `at ${times.join(', ')}`
113
+ }
114
+ return `at minute ${ranges(e.minutes, String)} of hour ${ranges(e.hours, String)}`
115
+ }
116
+
117
+ /** The schedule in words: `at 03:00 every day (Europe/Istanbul)`. */
118
+ export function describeSchedule(
119
+ spec: ScheduleSpec,
120
+ options: DescribeScheduleOptions = {},
121
+ ): string {
122
+ const tz = options.tz ?? 'UTC'
123
+ switch (spec.kind) {
124
+ case 'at':
125
+ return `once at ${formatWall(Date.parse(spec.at), tz)} (${tz})`
126
+ case 'every':
127
+ return describeInterval(spec.everyMs)
128
+ case 'cron': {
129
+ let e: CronExpression
130
+ try {
131
+ e = parseCronExpression(spec.expr)
132
+ } catch {
133
+ return `cron "${spec.expr}" (${spec.tz})`
134
+ }
135
+ return `${describeTimes(e, e.source)} ${describeDays(e)} (${spec.tz})`
136
+ }
137
+ }
138
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * A schedule, cron expression or time zone that cannot be used.
3
+ *
4
+ * `token` names the piece of the input that was refused, so a caller can
5
+ * point at it instead of repeating the whole expression back.
6
+ */
7
+ export class ScheduleValidationError extends Error {
8
+ readonly token: string | undefined
9
+
10
+ constructor(message: string, token?: string) {
11
+ super(message)
12
+ this.name = 'ScheduleValidationError'
13
+ this.token = token
14
+ }
15
+ }
@@ -0,0 +1,178 @@
1
+ /**
2
+ * What to do about one job now: fire, skip, record a miss, or nothing.
3
+ *
4
+ * Pure: no clock, no I/O. The host passes the instant, the state it kept and
5
+ * whether an occurrence was already claimed, and applies the decision itself
6
+ * (append history, claim, dispatch). Evaluating the same `(state, now)` twice
7
+ * gives the same answer, so a host that lost its in-memory queue rebuilds it
8
+ * by evaluating again; an already-claimed key is never fired twice.
9
+ *
10
+ * Rules, in order:
11
+ *
12
+ * 1. Occurrences due are those in `(lastEvaluatedAt, now]`, counted with a
13
+ * cap and never enumerated. A new job counts from its creation. A changed
14
+ * revision counts from the change, so editing `0 3 * * *` into `0 4 * * *`
15
+ * does not invent catch-ups for 04:00s that never belonged to the job.
16
+ * 2. `lastEvaluatedAt` never moves backwards: after a backward clock jump
17
+ * nothing is due until the wall clock passes it again.
18
+ * 3. A job that is not active produces skip records only (paused and
19
+ * awaiting-confirmation are collapsed per evaluation). A one-shot whose
20
+ * time passed while inactive is expired.
21
+ * 4. The latest due occurrence fires on time (`scheduled`, or `late` once
22
+ * more than five seconds late) within the late grace; within the catch-up
23
+ * window it fires once as `catch-up` and everything earlier is one
24
+ * `missed` record; older than the window, nothing fires.
25
+ * 5. An unfinished run of the same job skips what came due meanwhile.
26
+ * 6. A provider quota hold skips what comes due before the hold ends.
27
+ */
28
+
29
+ import { countOccurrences, nextFireTime, previousFireTime } from './next-fire.js'
30
+ import type {
31
+ ScheduleDecision,
32
+ ScheduleEvaluationInput,
33
+ ScheduleMissedReason,
34
+ ScheduleSkipReason,
35
+ } from './types.js'
36
+
37
+ /** Default catch-up window: seven days. */
38
+ export const SCHEDULE_CATCH_UP_WINDOW_MS = 7 * 24 * 60 * 60 * 1000
39
+ /** Default grace for a fire to still count as on time: two minutes. */
40
+ export const SCHEDULE_LATE_GRACE_MS = 120_000
41
+ /** A fire later than this is reported `late` rather than `scheduled`. */
42
+ const LATE_THRESHOLD_MS = 5_000
43
+
44
+ type Skip = ScheduleDecision['skip'][number]
45
+
46
+ function missedReason(input: ScheduleEvaluationInput, firstMissed: Date): ScheduleMissedReason {
47
+ const gap = input.daemon.observedGap
48
+ if (gap && firstMissed.getTime() >= gap.from.getTime()) {
49
+ if (gap.kind === 'asleep') return 'machine-asleep'
50
+ if (gap.kind === 'clock-forward') return 'clock-jumped-forward'
51
+ }
52
+ return 'daemon-not-running'
53
+ }
54
+
55
+ /** Evaluate one job at `now`. */
56
+ export function evaluateJob(input: ScheduleEvaluationInput): ScheduleDecision {
57
+ const { job, state, now } = input
58
+ const nowMs = now.getTime()
59
+ const lateGrace = input.lateGraceMs ?? SCHEDULE_LATE_GRACE_MS
60
+ const windowMs = job.catchUp?.windowMs ?? SCHEDULE_CATCH_UP_WINDOW_MS
61
+
62
+ let baseMs = Date.parse(state.lastEvaluatedAt ?? job.createdAt)
63
+ if (!Number.isFinite(baseMs)) baseMs = Date.parse(job.createdAt)
64
+ if (state.jobRevision !== undefined && state.jobRevision !== job.revision) {
65
+ baseMs = Math.max(baseMs, Date.parse(job.updatedAt))
66
+ }
67
+ const nextState = (evaluatedMs: number): ScheduleDecision['nextState'] => {
68
+ const next = nextFireTime(job.spec, new Date(evaluatedMs))
69
+ return {
70
+ lastEvaluatedAt: new Date(evaluatedMs),
71
+ ...(next ? { nextFireAt: next } : {}),
72
+ jobRevision: job.revision,
73
+ }
74
+ }
75
+
76
+ if (job.state === 'completed' || job.state === 'expired') {
77
+ return {
78
+ skip: [],
79
+ nextState: { lastEvaluatedAt: new Date(Math.max(baseMs, nowMs)), jobRevision: job.revision },
80
+ }
81
+ }
82
+ // Rule 2: a clock that went backwards decides nothing new.
83
+ if (nowMs <= baseMs) return { skip: [], nextState: nextState(baseMs) }
84
+
85
+ const due = countOccurrences(job.spec, new Date(baseMs), now)
86
+ if (due.count === 0 || !due.latest) {
87
+ if (job.spec.kind === 'at' && Date.parse(job.spec.at) <= baseMs && job.state !== 'active') {
88
+ return { skip: [], jobTransition: 'expired', nextState: nextState(nowMs) }
89
+ }
90
+ return { skip: [], nextState: nextState(nowMs) }
91
+ }
92
+ const latest = due.latest
93
+
94
+ const collapsed = (reason: ScheduleSkipReason): Skip[] => [
95
+ { scheduledFor: latest, reason, count: due.count },
96
+ ]
97
+
98
+ if (job.state === 'paused' || job.state === 'pending-confirmation') {
99
+ const reason: ScheduleSkipReason = job.state === 'paused' ? 'paused' : 'awaiting-confirmation'
100
+ return {
101
+ skip: collapsed(reason),
102
+ ...(job.spec.kind === 'at' ? { jobTransition: 'expired' as const } : {}),
103
+ nextState: nextState(nowMs),
104
+ }
105
+ }
106
+
107
+ const hold = state.quotaHoldUntil ? Date.parse(state.quotaHoldUntil) : Number.NaN
108
+ if (Number.isFinite(hold) && hold > nowMs) {
109
+ return { skip: collapsed('quota-hold'), nextState: nextState(nowMs) }
110
+ }
111
+
112
+ if (state.activeRun) {
113
+ return {
114
+ skip: collapsed(
115
+ state.activeRun.status === 'awaiting-approval'
116
+ ? 'previous-run-awaiting-approval'
117
+ : 'previous-run-active',
118
+ ),
119
+ nextState: nextState(nowMs),
120
+ }
121
+ }
122
+
123
+ const key = String(latest.getTime())
124
+ const lateBy = nowMs - latest.getTime()
125
+ const earlier = due.count - 1
126
+ const missedBefore = (): ScheduleDecision['missed'] | undefined => {
127
+ if (earlier <= 0 || !due.first) return undefined
128
+ // The occurrence just before the fired one.
129
+ const to = previousFireTime(job.spec, new Date(latest.getTime() - 1)) ?? due.first
130
+ return {
131
+ from: due.first,
132
+ to,
133
+ count: earlier,
134
+ capped: due.capped,
135
+ reason: missedReason(input, due.first),
136
+ }
137
+ }
138
+ const transition = job.spec.kind === 'at' ? { jobTransition: 'completed' as const } : {}
139
+
140
+ if (input.isClaimed?.(key)) {
141
+ // Somebody already started it; nothing to fire, nothing missed.
142
+ return { skip: [], ...transition, nextState: nextState(nowMs) }
143
+ }
144
+
145
+ if (lateBy <= lateGrace || lateBy <= windowMs) {
146
+ const trigger =
147
+ lateBy <= lateGrace ? (lateBy > LATE_THRESHOLD_MS ? 'late' : 'scheduled') : 'catch-up'
148
+ const missed = missedBefore()
149
+ return {
150
+ fire: { key, scheduledFor: latest, trigger },
151
+ skip: [],
152
+ ...(missed ? { missed } : {}),
153
+ ...transition,
154
+ nextState: nextState(nowMs),
155
+ }
156
+ }
157
+
158
+ // Everything due is older than the window.
159
+ const first = due.first ?? latest
160
+ return {
161
+ skip: [
162
+ {
163
+ scheduledFor: latest,
164
+ reason: job.spec.kind === 'at' ? 'one-shot-expired' : 'beyond-catch-up-window',
165
+ count: 1,
166
+ },
167
+ ],
168
+ missed: {
169
+ from: first,
170
+ to: latest,
171
+ count: due.count,
172
+ capped: due.capped,
173
+ reason: missedReason(input, first),
174
+ },
175
+ ...(job.spec.kind === 'at' ? { jobTransition: 'expired' as const } : {}),
176
+ nextState: nextState(nowMs),
177
+ }
178
+ }
@@ -0,0 +1,18 @@
1
+ export { parseCronExpression } from './cron.js'
2
+ export { describeSchedule } from './describe.js'
3
+ export type { DescribeScheduleOptions } from './describe.js'
4
+ export { ScheduleValidationError } from './errors.js'
5
+ export {
6
+ SCHEDULE_CATCH_UP_WINDOW_MS,
7
+ SCHEDULE_LATE_GRACE_MS,
8
+ evaluateJob,
9
+ } from './evaluate.js'
10
+ export {
11
+ countOccurrences,
12
+ nextFireTime,
13
+ previousFireTime,
14
+ } from './next-fire.js'
15
+ export { parseDuration, parseScheduleSpec, upcomingFireTimes } from './spec.js'
16
+ export type { ParseScheduleOptions } from './spec.js'
17
+ export { hostTimeZone, validateTimeZone } from './tz.js'
18
+ export type * from './types.js'
@@ -0,0 +1,257 @@
1
+ /**
2
+ * When a spec fires: the next instant, the previous one, and how many fall
3
+ * in a window.
4
+ *
5
+ * Cron is walked a local DAY at a time — never a minute at a time over years
6
+ * — and a day with no DST change is computed arithmetically from one offset.
7
+ * Only a day whose offset changes is examined slot by slot, and that is where
8
+ * cronie's rule is applied:
9
+ *
10
+ * - a fixed-time expression (`30 2 * * *`) whose time falls in a
11
+ * spring-forward gap fires once, at the first instant after the gap;
12
+ * whose time is repeated by a fall-back fires once, at the first of the two;
13
+ * - a wildcard or step expression (`0 * * * *`, `*\/15 * * * *`) fires at
14
+ * every instant whose wall time matches: the repeated hour runs twice and
15
+ * slots inside the gap do not exist.
16
+ */
17
+
18
+ import { cronMatchesDay, parseCronExpression } from './cron.js'
19
+ import { ScheduleValidationError } from './errors.js'
20
+ import type { CronExpression, OccurrenceCount, ScheduleSpec } from './types.js'
21
+ import { civil, firstInstantAfterGap, instantsForWall, offsetAt, validateTimeZone } from './tz.js'
22
+
23
+ const MINUTE = 60_000
24
+ const HOUR = 60 * MINUTE
25
+ const DAY = 24 * HOUR
26
+ /** How far ahead a cron search looks before deciding the expression never fires. */
27
+ export const CRON_HORIZON_DAYS = 366 * 5 + 2
28
+ /**
29
+ * Occurrence counts stop here. Counting a day without a DST change costs one
30
+ * multiplication, so the cap bounds the pathological case (a years-long gap
31
+ * on a per-minute job) rather than the ordinary one.
32
+ */
33
+ export const OCCURRENCE_COUNT_CAP = 100_000
34
+
35
+ const parsedCache = new Map<string, CronExpression>()
36
+
37
+ function cronOf(expr: string): CronExpression {
38
+ let parsed = parsedCache.get(expr)
39
+ if (!parsed) {
40
+ parsed = parseCronExpression(expr)
41
+ if (parsedCache.size > 256) parsedCache.clear()
42
+ parsedCache.set(expr, parsed)
43
+ }
44
+ return parsed
45
+ }
46
+
47
+ /** The local-midnight wall value of the day containing `wall`. */
48
+ function dayStart(wall: number): number {
49
+ return Math.floor(wall / DAY) * DAY
50
+ }
51
+
52
+ /**
53
+ * Every instant the expression fires on one local day (`dayWall` is that
54
+ * day's midnight as a wall value), ascending.
55
+ */
56
+ export function cronInstantsOnDay(expr: CronExpression, tz: string, dayWall: number): number[] {
57
+ const c = civil(dayWall)
58
+ if (!cronMatchesDay(expr, c.month, c.day, c.weekday)) return []
59
+ const before = offsetAt(dayWall - 14 * HOUR, tz)
60
+ const after = offsetAt(dayWall + DAY + 14 * HOUR, tz)
61
+ const out: number[] = []
62
+ if (before === after) {
63
+ for (const h of expr.hours)
64
+ for (const m of expr.minutes) out.push(dayWall + h * HOUR + m * MINUTE - before)
65
+ return out
66
+ }
67
+ // A day the offset changes on (or next to): slot by slot.
68
+ const seen = new Set<number>()
69
+ for (const h of expr.hours) {
70
+ for (const m of expr.minutes) {
71
+ const wall = dayWall + h * HOUR + m * MINUTE
72
+ const instants = instantsForWall(wall, tz)
73
+ if (instants.length === 0) {
74
+ if (expr.fixedTime) {
75
+ const shifted = firstInstantAfterGap(wall, tz)
76
+ if (!seen.has(shifted)) {
77
+ seen.add(shifted)
78
+ out.push(shifted)
79
+ }
80
+ }
81
+ continue
82
+ }
83
+ const chosen = expr.fixedTime ? instants.slice(0, 1) : instants
84
+ for (const t of chosen) {
85
+ if (seen.has(t)) continue
86
+ seen.add(t)
87
+ out.push(t)
88
+ }
89
+ }
90
+ }
91
+ return out.sort((a, b) => a - b)
92
+ }
93
+
94
+ function validEvery(spec: { everyMs: number; anchorAt: string }): number {
95
+ const anchor = Date.parse(spec.anchorAt)
96
+ if (!Number.isFinite(anchor)) {
97
+ throw new ScheduleValidationError(`anchor "${spec.anchorAt}" is not a date`, spec.anchorAt)
98
+ }
99
+ if (!Number.isSafeInteger(spec.everyMs) || spec.everyMs < MINUTE || spec.everyMs % MINUTE !== 0) {
100
+ throw new ScheduleValidationError(
101
+ 'an interval must be a whole number of minutes, at least one',
102
+ String(spec.everyMs),
103
+ )
104
+ }
105
+ return anchor
106
+ }
107
+
108
+ /**
109
+ * The first instant strictly after `afterExclusive` at which `spec` fires,
110
+ * or `null` when there is none (a past `at`, or a cron expression with no
111
+ * occurrence in the next five years).
112
+ */
113
+ export function nextFireTime(spec: ScheduleSpec, afterExclusive: Date): Date | null {
114
+ const after = afterExclusive.getTime()
115
+ switch (spec.kind) {
116
+ case 'at': {
117
+ const at = Date.parse(spec.at)
118
+ return Number.isFinite(at) && at > after ? new Date(at) : null
119
+ }
120
+ case 'every': {
121
+ const anchor = validEvery(spec)
122
+ const k = after < anchor ? 1 : Math.floor((after - anchor) / spec.everyMs) + 1
123
+ return new Date(anchor + Math.max(1, k) * spec.everyMs)
124
+ }
125
+ case 'cron': {
126
+ const expr = cronOf(spec.expr)
127
+ const tz = spec.tz
128
+ // A day early: a gap-shifted or offset-crossing instant of the
129
+ // previous local day can still lie after `after`.
130
+ let day = dayStart(after + offsetAt(after, tz)) - DAY
131
+ for (let i = 0; i <= CRON_HORIZON_DAYS; i++, day += DAY) {
132
+ for (const t of cronInstantsOnDay(expr, tz, day)) if (t > after) return new Date(t)
133
+ }
134
+ return null
135
+ }
136
+ }
137
+ }
138
+
139
+ /**
140
+ * The last instant at or before `atOrBefore` at which `spec` fired, or
141
+ * `null` if none within the horizon (or before the anchor / the `at`).
142
+ */
143
+ export function previousFireTime(spec: ScheduleSpec, atOrBefore: Date): Date | null {
144
+ const before = atOrBefore.getTime()
145
+ switch (spec.kind) {
146
+ case 'at': {
147
+ const at = Date.parse(spec.at)
148
+ return Number.isFinite(at) && at <= before ? new Date(at) : null
149
+ }
150
+ case 'every': {
151
+ const anchor = validEvery(spec)
152
+ const k = Math.floor((before - anchor) / spec.everyMs)
153
+ return k >= 1 ? new Date(anchor + k * spec.everyMs) : null
154
+ }
155
+ case 'cron': {
156
+ const expr = cronOf(spec.expr)
157
+ const tz = spec.tz
158
+ let day = dayStart(before + offsetAt(before, tz)) + DAY
159
+ for (let i = 0; i <= CRON_HORIZON_DAYS; i++, day -= DAY) {
160
+ const instants = cronInstantsOnDay(expr, tz, day)
161
+ for (let j = instants.length - 1; j >= 0; j--) {
162
+ const t = instants[j] as number
163
+ if (t <= before) return new Date(t)
164
+ }
165
+ }
166
+ return null
167
+ }
168
+ }
169
+ }
170
+
171
+ /**
172
+ * How many times `spec` fires in `(fromExclusive, toInclusive]`, with the
173
+ * first and latest of them. Stops counting at `cap` (default 100 000) and
174
+ * says so, without enumerating beyond it; the latest is exact either way.
175
+ */
176
+ export function countOccurrences(
177
+ spec: ScheduleSpec,
178
+ fromExclusive: Date,
179
+ toInclusive: Date,
180
+ cap: number = OCCURRENCE_COUNT_CAP,
181
+ ): OccurrenceCount {
182
+ const from = fromExclusive.getTime()
183
+ const to = toInclusive.getTime()
184
+ if (to <= from) return { count: 0, capped: false }
185
+ switch (spec.kind) {
186
+ case 'at': {
187
+ const at = Date.parse(spec.at)
188
+ return at > from && at <= to
189
+ ? { count: 1, capped: false, first: new Date(at), latest: new Date(at) }
190
+ : { count: 0, capped: false }
191
+ }
192
+ case 'every': {
193
+ const anchor = validEvery(spec)
194
+ const firstK = from < anchor ? 1 : Math.floor((from - anchor) / spec.everyMs) + 1
195
+ const lastK = Math.floor((to - anchor) / spec.everyMs)
196
+ if (lastK < Math.max(1, firstK)) return { count: 0, capped: false }
197
+ const k0 = Math.max(1, firstK)
198
+ const total = lastK - k0 + 1
199
+ return {
200
+ count: Math.min(total, cap),
201
+ capped: total > cap,
202
+ first: new Date(anchor + k0 * spec.everyMs),
203
+ latest: new Date(anchor + lastK * spec.everyMs),
204
+ }
205
+ }
206
+ case 'cron': {
207
+ const expr = cronOf(spec.expr)
208
+ const tz = spec.tz
209
+ const latest = previousFireTime(spec, toInclusive)
210
+ if (!latest || latest.getTime() <= from) return { count: 0, capped: false }
211
+ const perUniformDay = expr.hours.length * expr.minutes.length
212
+ let count = 0
213
+ let first: number | undefined
214
+ const lastDay = dayStart(to + offsetAt(to, tz)) + DAY
215
+ for (let day = dayStart(from + offsetAt(from, tz)) - DAY; day <= lastDay; day += DAY) {
216
+ const c = civil(day)
217
+ if (!cronMatchesDay(expr, c.month, c.day, c.weekday)) continue
218
+ const o1 = offsetAt(day - 14 * HOUR, tz)
219
+ const o2 = offsetAt(day + DAY + 14 * HOUR, tz)
220
+ const dayFirst = day - o1
221
+ const dayLast = day + DAY - o1
222
+ if (first !== undefined && o1 === o2 && dayFirst > from && dayLast <= to) {
223
+ count += perUniformDay
224
+ } else {
225
+ for (const t of cronInstantsOnDay(expr, tz, day)) {
226
+ if (t <= from || t > to) continue
227
+ if (first === undefined) first = t
228
+ count++
229
+ }
230
+ }
231
+ if (count > cap) break
232
+ }
233
+ return {
234
+ count: Math.min(count, cap),
235
+ capped: count > cap,
236
+ ...(first !== undefined ? { first: new Date(first) } : {}),
237
+ latest,
238
+ }
239
+ }
240
+ }
241
+ }
242
+
243
+ /** Validate a spec as a whole: its zone, its expression, and that it can ever fire after `now`. */
244
+ export function assertSpecFires(spec: ScheduleSpec, now: Date): void {
245
+ if (spec.kind === 'cron') {
246
+ validateTimeZone(spec.tz)
247
+ cronOf(spec.expr)
248
+ }
249
+ if (nextFireTime(spec, now) === null) {
250
+ throw new ScheduleValidationError(
251
+ spec.kind === 'at'
252
+ ? `${spec.at} is in the past`
253
+ : `"${spec.kind === 'cron' ? spec.expr : ''}" never fires in the next five years`,
254
+ spec.kind === 'cron' ? spec.expr : spec.kind === 'at' ? spec.at : undefined,
255
+ )
256
+ }
257
+ }