@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,210 @@
1
+ /**
2
+ * The words an operator or a model types for a schedule, turned into a spec.
3
+ *
4
+ * | Input | Spec |
5
+ * |---|---|
6
+ * | `at 2026-09-24T09:00`, `at 2026-09-24 09:00`, `at 09:00`, `in 30m` | `at` |
7
+ * | an ISO instant with `Z` or an offset, with or without `at` | `at` |
8
+ * | `every 30m`, `every 2h`, `every 1d`, `every 90m` | `every` |
9
+ * | five-field cron, `@hourly`, `@daily`, `@weekly`, `@monthly`, `@yearly` | `cron` |
10
+ *
11
+ * A local time is read in `tz`. Nothing is rounded silently: `every 30s` is
12
+ * refused and the refusal names the nearest interval that is allowed.
13
+ */
14
+
15
+ import { parseCronExpression } from './cron.js'
16
+ import { ScheduleValidationError } from './errors.js'
17
+ import { assertSpecFires, nextFireTime } from './next-fire.js'
18
+ import type { ScheduleSpec } from './types.js'
19
+ import { civil, hostTimeZone, instantsForWall, validateTimeZone, wallOf } from './tz.js'
20
+
21
+ const MINUTE = 60_000
22
+ const UNIT_MS: Readonly<Record<string, number>> = {
23
+ s: 1_000,
24
+ sec: 1_000,
25
+ secs: 1_000,
26
+ second: 1_000,
27
+ seconds: 1_000,
28
+ m: MINUTE,
29
+ min: MINUTE,
30
+ mins: MINUTE,
31
+ minute: MINUTE,
32
+ minutes: MINUTE,
33
+ h: 60 * MINUTE,
34
+ hr: 60 * MINUTE,
35
+ hrs: 60 * MINUTE,
36
+ hour: 60 * MINUTE,
37
+ hours: 60 * MINUTE,
38
+ d: 24 * 60 * MINUTE,
39
+ day: 24 * 60 * MINUTE,
40
+ days: 24 * 60 * MINUTE,
41
+ w: 7 * 24 * 60 * MINUTE,
42
+ week: 7 * 24 * 60 * MINUTE,
43
+ weeks: 7 * 24 * 60 * MINUTE,
44
+ }
45
+
46
+ export interface ParseScheduleOptions {
47
+ /** "Now", for `in …`, `at HH:MM` and the anchor of `every`. Default: the clock. */
48
+ readonly now?: Date
49
+ /** Zone a local time and a cron expression are read in. Default: the host's. */
50
+ readonly tz?: string
51
+ }
52
+
53
+ /** `90m` → 5 400 000. Throws on anything else. */
54
+ export function parseDuration(text: string): number {
55
+ const match = /^(\d+)\s*([a-z]+)$/i.exec(text.trim())
56
+ const unit = match ? UNIT_MS[(match[2] ?? '').toLowerCase()] : undefined
57
+ if (!match || unit === undefined) {
58
+ throw new ScheduleValidationError(
59
+ `"${text}" is not a duration (write it like 30m, 2h, 1d)`,
60
+ text,
61
+ )
62
+ }
63
+ return Number(match[1]) * unit
64
+ }
65
+
66
+ function formatInterval(ms: number): string {
67
+ if (ms % (24 * 60 * MINUTE) === 0) return `${ms / (24 * 60 * MINUTE)}d`
68
+ if (ms % (60 * MINUTE) === 0) return `${ms / (60 * MINUTE)}h`
69
+ return `${ms / MINUTE}m`
70
+ }
71
+
72
+ function everySpec(text: string, now: Date): ScheduleSpec {
73
+ const ms = parseDuration(text)
74
+ if (ms < MINUTE) {
75
+ throw new ScheduleValidationError(
76
+ `every ${text} is shorter than the minimum interval; the nearest allowed is every 1m`,
77
+ text,
78
+ )
79
+ }
80
+ if (ms % MINUTE !== 0) {
81
+ const down = Math.floor(ms / MINUTE) * MINUTE
82
+ const up = down + MINUTE
83
+ throw new ScheduleValidationError(
84
+ `every ${text} is not a whole number of minutes; the nearest allowed are every ${formatInterval(down)} and every ${formatInterval(up)}`,
85
+ text,
86
+ )
87
+ }
88
+ const anchor = new Date(Math.floor(now.getTime() / MINUTE) * MINUTE)
89
+ return { kind: 'every', everyMs: ms, anchorAt: anchor.toISOString() }
90
+ }
91
+
92
+ /** Resolve a local `YYYY-MM-DD HH:MM` in `tz` to one instant (the earlier of two). */
93
+ function localToInstant(
94
+ y: number,
95
+ mo: number,
96
+ d: number,
97
+ h: number,
98
+ mi: number,
99
+ tz: string,
100
+ token: string,
101
+ ): number {
102
+ const wall = Date.UTC(y, mo - 1, d, h, mi)
103
+ const c = civil(wall)
104
+ if (c.year !== y || c.month !== mo || c.day !== d || h > 23 || mi > 59) {
105
+ throw new ScheduleValidationError(`"${token}" is not a real date and time`, token)
106
+ }
107
+ const instants = instantsForWall(wall, tz)
108
+ if (instants.length === 0) {
109
+ throw new ScheduleValidationError(
110
+ `"${token}" does not exist in ${tz} (the clocks skip it); pick a time outside the change`,
111
+ token,
112
+ )
113
+ }
114
+ return instants[0] as number
115
+ }
116
+
117
+ function atSpec(text: string, now: Date, tz: string): ScheduleSpec {
118
+ const t = text.trim()
119
+ // An instant with its own offset: taken as written.
120
+ if (/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(:\d{2}(\.\d+)?)?(Z|[+-]\d{2}:?\d{2})$/i.test(t)) {
121
+ const at = Date.parse(t)
122
+ if (!Number.isFinite(at)) throw new ScheduleValidationError(`"${t}" is not a date`, t)
123
+ return { kind: 'at', at: new Date(at).toISOString() }
124
+ }
125
+ const full = /^(\d{4})-(\d{2})-(\d{2})[T ](\d{1,2}):(\d{2})$/.exec(t)
126
+ if (full) {
127
+ const [, y, mo, d, h, mi] = full.map(Number) as number[]
128
+ const at = localToInstant(
129
+ y as number,
130
+ mo as number,
131
+ d as number,
132
+ h as number,
133
+ mi as number,
134
+ tz,
135
+ t,
136
+ )
137
+ return { kind: 'at', at: new Date(at).toISOString() }
138
+ }
139
+ const clock = /^(\d{1,2}):(\d{2})$/.exec(t)
140
+ if (clock) {
141
+ const h = Number(clock[1])
142
+ const mi = Number(clock[2])
143
+ if (h > 23 || mi > 59) throw new ScheduleValidationError(`"${t}" is not a time of day`, t)
144
+ // The next time the wall clock reads HH:MM, today or tomorrow.
145
+ const today = civil(wallOf(now.getTime(), tz))
146
+ for (let add = 0; add < 3; add++) {
147
+ const day = new Date(Date.UTC(today.year, today.month - 1, today.day + add))
148
+ const instants = instantsForWall(
149
+ Date.UTC(day.getUTCFullYear(), day.getUTCMonth(), day.getUTCDate(), h, mi),
150
+ tz,
151
+ )
152
+ const first = instants.find((i) => i > now.getTime())
153
+ if (first !== undefined) return { kind: 'at', at: new Date(first).toISOString() }
154
+ }
155
+ throw new ScheduleValidationError(`"${t}" does not occur in ${tz} in the next two days`, t)
156
+ }
157
+ throw new ScheduleValidationError(
158
+ `"${t}" is not a time (write 2026-09-24 09:00, 09:00, or an ISO instant)`,
159
+ t,
160
+ )
161
+ }
162
+
163
+ /**
164
+ * Parse schedule words into a spec, validated: the zone exists, the
165
+ * expression parses, and the spec fires at least once after `now`.
166
+ */
167
+ export function parseScheduleSpec(input: string, options: ParseScheduleOptions = {}): ScheduleSpec {
168
+ const now = options.now ?? new Date()
169
+ const tz = validateTimeZone(options.tz ?? hostTimeZone())
170
+ const text = input.trim()
171
+ if (text === '') throw new ScheduleValidationError('a schedule is required', '')
172
+ let spec: ScheduleSpec
173
+ const lower = text.toLowerCase()
174
+ if (lower.startsWith('every ')) {
175
+ spec = everySpec(text.slice(6).trim(), now)
176
+ } else if (lower.startsWith('in ')) {
177
+ const ms = parseDuration(text.slice(3).trim())
178
+ if (ms < MINUTE) {
179
+ throw new ScheduleValidationError(
180
+ `in ${text.slice(3).trim()} is less than a minute away; the nearest allowed is in 1m`,
181
+ text.slice(3).trim(),
182
+ )
183
+ }
184
+ spec = { kind: 'at', at: new Date(now.getTime() + ms).toISOString() }
185
+ } else if (lower.startsWith('at ')) {
186
+ spec = atSpec(text.slice(3), now, tz)
187
+ } else if (/^\d{4}-\d{2}-\d{2}T/i.test(text)) {
188
+ spec = atSpec(text, now, tz)
189
+ } else if (lower === '@reboot') {
190
+ throw new ScheduleValidationError('@reboot is not supported: a job runs on a schedule', text)
191
+ } else {
192
+ const expr = parseCronExpression(text)
193
+ spec = { kind: 'cron', expr: expr.source, tz }
194
+ }
195
+ assertSpecFires(spec, now)
196
+ return spec
197
+ }
198
+
199
+ /** The next `count` fire times after `after`. */
200
+ export function upcomingFireTimes(spec: ScheduleSpec, after: Date, count = 3): Date[] {
201
+ const out: Date[] = []
202
+ let cursor = after
203
+ for (let i = 0; i < count; i++) {
204
+ const next = nextFireTime(spec, cursor)
205
+ if (!next) break
206
+ out.push(next)
207
+ cursor = next
208
+ }
209
+ return out
210
+ }
@@ -0,0 +1,163 @@
1
+ /**
2
+ * Types of the schedule time engine and evaluator.
3
+ *
4
+ * The SDK owns WHEN something is due and nothing about how a host stores a
5
+ * job or runs it: the CLI keeps its job files, claims and history records to
6
+ * itself, so its on-disk format is not SDK API. The unions exported here
7
+ * (`ScheduleDecision`, `ScheduleSkipReason`, `ScheduleMissedReason`) may grow
8
+ * in a minor release; switch over them with a `default:` branch.
9
+ */
10
+
11
+ /** A single instant (ISO-8601 UTC). */
12
+ export interface ScheduleAtSpec {
13
+ readonly kind: 'at'
14
+ readonly at: string
15
+ }
16
+
17
+ /**
18
+ * Elapsed time from an anchor: `anchorAt + k * everyMs` for k ≥ 1. Pure UTC
19
+ * arithmetic, so `every 24h` drifts against the wall clock across a DST
20
+ * change; a cron spec is the way to say "every day at 09:00".
21
+ */
22
+ export interface ScheduleEverySpec {
23
+ readonly kind: 'every'
24
+ /** At least one minute, and a whole number of minutes. */
25
+ readonly everyMs: number
26
+ readonly anchorAt: string
27
+ }
28
+
29
+ /** Five-field cron, evaluated in an IANA time zone the spec carries. */
30
+ export interface ScheduleCronSpec {
31
+ readonly kind: 'cron'
32
+ /** Normalised five-field text; macros are expanded. */
33
+ readonly expr: string
34
+ readonly tz: string
35
+ }
36
+
37
+ export type ScheduleSpec = ScheduleAtSpec | ScheduleEverySpec | ScheduleCronSpec
38
+
39
+ /** A parsed cron expression: each field as the sorted set of values it allows. */
40
+ export interface CronExpression {
41
+ readonly source: string
42
+ readonly minutes: readonly number[]
43
+ readonly hours: readonly number[]
44
+ readonly daysOfMonth: readonly number[]
45
+ readonly months: readonly number[]
46
+ /** 0-6, Sunday = 0 (a written 7 is folded into 0). */
47
+ readonly daysOfWeek: readonly number[]
48
+ /** The day-of-month field does not start with `*`. */
49
+ readonly domRestricted: boolean
50
+ /** The day-of-week field does not start with `*`. */
51
+ readonly dowRestricted: boolean
52
+ /**
53
+ * Minute and hour are single values or lists with no `*` or step. Decides
54
+ * the DST rule (cronie's): a fixed-time job fires once per day across a
55
+ * DST change, a wildcard job fires at every instant whose wall time
56
+ * matches.
57
+ */
58
+ readonly fixedTime: boolean
59
+ }
60
+
61
+ /** Why an occurrence did not run. May grow in a minor release. */
62
+ export type ScheduleSkipReason =
63
+ | 'previous-run-active'
64
+ | 'previous-run-awaiting-approval'
65
+ | 'paused'
66
+ | 'awaiting-confirmation'
67
+ | 'beyond-catch-up-window'
68
+ | 'one-shot-expired'
69
+ | 'quota-hold'
70
+
71
+ /** Why occurrences were missed. Best effort; may grow in a minor release. */
72
+ export type ScheduleMissedReason = 'daemon-not-running' | 'machine-asleep' | 'clock-jumped-forward'
73
+
74
+ /** The lifecycle a job is in, as far as the evaluator cares. */
75
+ export type ScheduleJobLifecycle =
76
+ | 'pending-confirmation'
77
+ | 'active'
78
+ | 'paused'
79
+ | 'completed'
80
+ | 'expired'
81
+
82
+ /** The evaluator's structural view of a job: what it needs and nothing else. */
83
+ export interface ScheduleEvaluationJob {
84
+ readonly spec: ScheduleSpec
85
+ readonly state: ScheduleJobLifecycle
86
+ /** Catch-up window; default {@link SCHEDULE_CATCH_UP_WINDOW_MS}. */
87
+ readonly catchUp?: { readonly windowMs: number }
88
+ readonly createdAt: string
89
+ readonly updatedAt: string
90
+ /** Bumped on every definition change. */
91
+ readonly revision: number
92
+ }
93
+
94
+ /** What the host remembers between evaluations of one job. */
95
+ export interface ScheduleEvaluationState {
96
+ /** Everything up to and including this instant has been decided. */
97
+ readonly lastEvaluatedAt?: string
98
+ /** The job revision `lastEvaluatedAt` was computed under. */
99
+ readonly jobRevision?: number
100
+ /** A run of this job that has not finished (or a fire queued but not started). */
101
+ readonly activeRun?: { readonly status: 'queued' | 'running' | 'awaiting-approval' }
102
+ /** Provider asked to be left alone until this instant. */
103
+ readonly quotaHoldUntil?: string
104
+ }
105
+
106
+ export interface ScheduleObservedGap {
107
+ readonly from: Date
108
+ readonly to: Date
109
+ readonly kind: 'asleep' | 'clock-forward' | 'clock-backward'
110
+ }
111
+
112
+ export interface ScheduleEvaluationInput {
113
+ readonly job: ScheduleEvaluationJob
114
+ readonly state: ScheduleEvaluationState
115
+ readonly now: Date
116
+ /** When this daemon instance started, and a gap it observed itself. */
117
+ readonly daemon: { readonly startedAt: Date; readonly observedGap?: ScheduleObservedGap }
118
+ /** How late a fire may be and still count as on time; default {@link SCHEDULE_LATE_GRACE_MS}. */
119
+ readonly lateGraceMs?: number
120
+ /** Whether an occurrence key was already started (claimed) by someone. */
121
+ readonly isClaimed?: (key: string) => boolean
122
+ }
123
+
124
+ export type ScheduleFireTrigger = 'scheduled' | 'late' | 'catch-up'
125
+
126
+ /** What to do about one job now. May grow in a minor release. */
127
+ export interface ScheduleDecision {
128
+ readonly fire?: {
129
+ /** Occurrence key: the scheduled instant in epoch milliseconds. */
130
+ readonly key: string
131
+ readonly scheduledFor: Date
132
+ readonly trigger: ScheduleFireTrigger
133
+ }
134
+ /** Occurrences that will not run, collapsed: one entry per reason. */
135
+ readonly skip: readonly {
136
+ readonly scheduledFor: Date
137
+ readonly reason: ScheduleSkipReason
138
+ readonly count: number
139
+ }[]
140
+ readonly missed?: {
141
+ readonly from: Date
142
+ readonly to: Date
143
+ readonly count: number
144
+ /** The count stopped at the cap; the real number is at least `count`. */
145
+ readonly capped: boolean
146
+ readonly reason: ScheduleMissedReason
147
+ }
148
+ readonly jobTransition?: 'completed' | 'expired'
149
+ readonly nextState: {
150
+ readonly lastEvaluatedAt: Date
151
+ readonly nextFireAt?: Date
152
+ readonly jobRevision: number
153
+ }
154
+ }
155
+
156
+ /** How occurrences in a window were counted. */
157
+ export interface OccurrenceCount {
158
+ readonly count: number
159
+ /** The count stopped at the cap. */
160
+ readonly capped: boolean
161
+ readonly first?: Date
162
+ readonly latest?: Date
163
+ }
@@ -0,0 +1,155 @@
1
+ /**
2
+ * Time-zone arithmetic over `Intl` alone.
3
+ *
4
+ * The SDK carries no time-zone database of its own: the runtime's ICU data is
5
+ * the source, read through `Intl.DateTimeFormat#formatToParts`. Everything in
6
+ * this file is expressed in "wall" milliseconds — a local date and time
7
+ * written as if it were UTC (`Date.UTC(y, m - 1, d, h, min)`) — so local
8
+ * arithmetic is plain integer arithmetic and only the conversions touch Intl.
9
+ */
10
+
11
+ import { ScheduleValidationError } from './errors.js'
12
+
13
+ const MINUTE = 60_000
14
+ const HOUR = 60 * MINUTE
15
+
16
+ const formatters = new Map<string, Intl.DateTimeFormat>()
17
+
18
+ function formatterFor(tz: string): Intl.DateTimeFormat {
19
+ let f = formatters.get(tz)
20
+ if (!f) {
21
+ f = new Intl.DateTimeFormat('en-US', {
22
+ timeZone: tz,
23
+ hourCycle: 'h23',
24
+ year: 'numeric',
25
+ month: 'numeric',
26
+ day: 'numeric',
27
+ hour: 'numeric',
28
+ minute: 'numeric',
29
+ second: 'numeric',
30
+ })
31
+ formatters.set(tz, f)
32
+ }
33
+ return f
34
+ }
35
+
36
+ /**
37
+ * Throw unless `tz` is an IANA zone this runtime knows. Returns the zone as
38
+ * the runtime spells it (`europe/istanbul` → `Europe/Istanbul`).
39
+ */
40
+ export function validateTimeZone(tz: string): string {
41
+ if (typeof tz !== 'string' || tz.trim() === '') {
42
+ throw new ScheduleValidationError('a time zone is required', String(tz))
43
+ }
44
+ try {
45
+ return new Intl.DateTimeFormat('en-US', { timeZone: tz.trim() }).resolvedOptions().timeZone
46
+ } catch {
47
+ throw new ScheduleValidationError(`unknown time zone "${tz}"`, tz)
48
+ }
49
+ }
50
+
51
+ /** The zone this host is in, as the runtime reports it. */
52
+ export function hostTimeZone(): string {
53
+ return Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC'
54
+ }
55
+
56
+ /** The wall clock of `instant` in `tz`, as wall milliseconds (seconds dropped). */
57
+ export function wallOf(instant: number, tz: string): number {
58
+ const parts = formatterFor(tz).formatToParts(new Date(instant))
59
+ let year = 0
60
+ let month = 1
61
+ let day = 1
62
+ let hour = 0
63
+ let minute = 0
64
+ for (const part of parts) {
65
+ switch (part.type) {
66
+ case 'year':
67
+ year = Number(part.value)
68
+ break
69
+ case 'month':
70
+ month = Number(part.value)
71
+ break
72
+ case 'day':
73
+ day = Number(part.value)
74
+ break
75
+ case 'hour':
76
+ hour = Number(part.value) % 24
77
+ break
78
+ case 'minute':
79
+ minute = Number(part.value)
80
+ break
81
+ }
82
+ }
83
+ return Date.UTC(year, month - 1, day, hour, minute)
84
+ }
85
+
86
+ /** `tz`'s offset from UTC at `instant`, in milliseconds, minute precision. */
87
+ export function offsetAt(instant: number, tz: string): number {
88
+ const floored = Math.floor(instant / MINUTE) * MINUTE
89
+ return wallOf(floored, tz) - floored
90
+ }
91
+
92
+ /**
93
+ * Every instant whose wall clock in `tz` reads `wall`.
94
+ *
95
+ * One instant normally, two in the hour a fall-back repeats (earliest first),
96
+ * none in the hour a spring-forward skips.
97
+ */
98
+ export function instantsForWall(wall: number, tz: string): number[] {
99
+ const offsets = new Set<number>([
100
+ offsetAt(wall - 26 * HOUR, tz),
101
+ offsetAt(wall, tz),
102
+ offsetAt(wall + 26 * HOUR, tz),
103
+ ])
104
+ const found = new Set<number>()
105
+ for (const offset of offsets) {
106
+ const candidate = wall - offset
107
+ if (wallOf(candidate, tz) === wall) found.add(candidate)
108
+ }
109
+ return [...found].sort((a, b) => a - b)
110
+ }
111
+
112
+ /**
113
+ * The first instant whose wall clock is later than `wall`, for a `wall` that
114
+ * falls inside a spring-forward gap: the moment the clocks jumped.
115
+ */
116
+ export function firstInstantAfterGap(wall: number, tz: string): number {
117
+ const a = offsetAt(wall - 26 * HOUR, tz)
118
+ const b = offsetAt(wall + 26 * HOUR, tz)
119
+ let lo = Math.floor((wall - Math.max(a, b)) / MINUTE)
120
+ let hi = Math.ceil((wall - Math.min(a, b)) / MINUTE)
121
+ // Smallest minute m in [lo, hi] with wallOf(m) > wall.
122
+ while (lo < hi) {
123
+ const mid = Math.floor((lo + hi) / 2)
124
+ if (wallOf(mid * MINUTE, tz) > wall) hi = mid
125
+ else lo = mid + 1
126
+ }
127
+ return lo * MINUTE
128
+ }
129
+
130
+ /** Civil date parts of a wall value. */
131
+ export function civil(wall: number): {
132
+ year: number
133
+ month: number
134
+ day: number
135
+ hour: number
136
+ minute: number
137
+ weekday: number
138
+ } {
139
+ const d = new Date(wall)
140
+ return {
141
+ year: d.getUTCFullYear(),
142
+ month: d.getUTCMonth() + 1,
143
+ day: d.getUTCDate(),
144
+ hour: d.getUTCHours(),
145
+ minute: d.getUTCMinutes(),
146
+ weekday: d.getUTCDay(),
147
+ }
148
+ }
149
+
150
+ /** `YYYY-MM-DD HH:MM` for an instant in `tz`. */
151
+ export function formatWall(instant: number, tz: string): string {
152
+ const c = civil(wallOf(instant, tz))
153
+ const p = (n: number) => String(n).padStart(2, '0')
154
+ return `${c.year}-${p(c.month)}-${p(c.day)} ${p(c.hour)}:${p(c.minute)}`
155
+ }
@@ -1,2 +1,2 @@
1
- export { loadSkill, discoverSkills } from './loader.js'
1
+ export { SKILL_FRONTMATTER_KEYS, loadSkill, discoverSkills } from './loader.js'
2
2
  export { SkillRegistry, resolveSkillChain } from './registry.js'
@@ -13,11 +13,38 @@ import { type Logger, resolveLogger } from '../utils/logger.js'
13
13
  export const SKILL_FILENAME = 'SKILL.md'
14
14
 
15
15
  /**
16
- * `allowed-tools` may be a YAML list. The Agent Skills format writes it
17
- * space-separated, comma-separated or as a list, and all three must mean the
18
- * same grant; the reader joins a list into the comma form.
16
+ * The frontmatter keys this loader reads.
17
+ *
18
+ * Every other key is skipped whole, whatever YAML it is written in, so a
19
+ * skill written for another agent — `argument-hint: [file]`, a `hooks:`
20
+ * block, `user-invocable: false` — loads here with those fields ignored
21
+ * instead of being refused over syntax in a field nothing reads. The keys
22
+ * listed are still parsed strictly: a value this loader USES is never read
23
+ * wrongly.
24
+ */
25
+ export const SKILL_FRONTMATTER_KEYS: readonly string[] = Object.freeze([
26
+ 'name',
27
+ 'description',
28
+ 'license',
29
+ 'compatibility',
30
+ 'allowed-tools',
31
+ 'invocation',
32
+ 'disable-model-invocation',
33
+ 'metadata',
34
+ ])
35
+
36
+ const READS_SKILL_KEY = (key: string): boolean => SKILL_FRONTMATTER_KEYS.includes(key)
37
+
38
+ /**
39
+ * How this loader reads frontmatter. `allowed-tools` may be a YAML list: the
40
+ * Agent Skills format writes it space-separated, comma-separated or as a
41
+ * list, and all three must mean the same grant; the reader joins a list into
42
+ * the comma form. Keys the loader does not read are skipped whole.
19
43
  */
20
- export const SKILL_FRONTMATTER_OPTIONS = { lists: ['allowed-tools'] } as const
44
+ export const SKILL_FRONTMATTER_OPTIONS = {
45
+ lists: ['allowed-tools'],
46
+ readsKey: READS_SKILL_KEY,
47
+ } as const
21
48
 
22
49
  /**
23
50
  * How this file's errors name themselves. Passed to the shared reader so a
@@ -110,6 +137,30 @@ function toSkillMetadata(parsed: ParsedFrontmatter, dirPath: string): SkillMetad
110
137
  skillMetadata.invocation = invocation
111
138
  }
112
139
 
140
+ // The other common spelling of "the model may not pick this":
141
+ // `disable-model-invocation: true` is `invocation: operator`. Read here,
142
+ // at the one parser, so a registry that re-reads the file on every load
143
+ // cannot lose it — a host-side override would be dropped by the first
144
+ // freshness reload and the model could then load an operator-only skill.
145
+ const disableModel = scalarAt(values, 'disable-model-invocation')
146
+ if (disableModel !== undefined) {
147
+ if (disableModel !== 'true' && disableModel !== 'false') {
148
+ // Refused for the reason `invocation` is: `yes` quietly reading as
149
+ // "not disabled" would put the skill back in front of the model.
150
+ throw new Error(
151
+ `${source}: disable-model-invocation must be true or false — got "${disableModel}"`,
152
+ )
153
+ }
154
+ if (disableModel === 'true') {
155
+ if (skillMetadata.invocation !== undefined && skillMetadata.invocation !== 'operator') {
156
+ throw new Error(
157
+ `${source}: disable-model-invocation: true contradicts invocation: ${skillMetadata.invocation}`,
158
+ )
159
+ }
160
+ skillMetadata.invocation = 'operator'
161
+ }
162
+ }
163
+
113
164
  const extra = mappingAt(values, 'metadata')
114
165
  if (extra && Object.keys(extra).length > 0) {
115
166
  skillMetadata.metadata = { ...extra }
@@ -220,6 +220,12 @@ export class SkillRegistry {
220
220
  registeredName: string
221
221
  description: string
222
222
  location: string
223
+ /**
224
+ * The skill's directory (`Skill.dirPath`); `location` is its SKILL.md.
225
+ * Always set here. Declared optional so a subclass that overrides
226
+ * `catalog()` without it still compiles.
227
+ */
228
+ directory?: string
223
229
  allowedTools?: string
224
230
  invocation?: 'model' | 'operator' | 'both'
225
231
  }[]
@@ -228,6 +234,7 @@ export class SkillRegistry {
228
234
  registeredName: string
229
235
  description: string
230
236
  location: string
237
+ directory: string
231
238
  allowedTools?: string
232
239
  invocation?: 'model' | 'operator' | 'both'
233
240
  }> = []
@@ -243,6 +250,7 @@ export class SkillRegistry {
243
250
  registeredName,
244
251
  description: skill.metadata.description,
245
252
  location: join(skill.dirPath, SKILL_FILENAME),
253
+ directory: skill.dirPath,
246
254
  ...(skill.metadata.allowedTools === undefined
247
255
  ? {}
248
256
  : { allowedTools: skill.metadata.allowedTools }),