@clinebot/core 0.0.36 → 0.0.37

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 (228) hide show
  1. package/dist/ClineCore.d.ts +312 -3
  2. package/dist/ClineCore.d.ts.map +1 -1
  3. package/dist/account/cline-account-service.d.ts.map +1 -1
  4. package/dist/cron/cron-event-ingress.d.ts +38 -0
  5. package/dist/cron/cron-event-ingress.d.ts.map +1 -0
  6. package/dist/cron/cron-materializer.d.ts +36 -0
  7. package/dist/cron/cron-materializer.d.ts.map +1 -0
  8. package/dist/cron/cron-reconciler.d.ts +62 -0
  9. package/dist/cron/cron-reconciler.d.ts.map +1 -0
  10. package/dist/cron/cron-report-writer.d.ts +41 -0
  11. package/dist/cron/cron-report-writer.d.ts.map +1 -0
  12. package/dist/cron/cron-runner.d.ts +43 -0
  13. package/dist/cron/cron-runner.d.ts.map +1 -0
  14. package/dist/cron/cron-schema.d.ts +3 -0
  15. package/dist/cron/cron-schema.d.ts.map +1 -0
  16. package/dist/cron/cron-service.d.ts +57 -0
  17. package/dist/cron/cron-service.d.ts.map +1 -0
  18. package/dist/cron/cron-spec-parser.d.ts +27 -0
  19. package/dist/cron/cron-spec-parser.d.ts.map +1 -0
  20. package/dist/cron/cron-watcher.d.ts +23 -0
  21. package/dist/cron/cron-watcher.d.ts.map +1 -0
  22. package/dist/cron/scheduler.d.ts +3 -1
  23. package/dist/cron/scheduler.d.ts.map +1 -1
  24. package/dist/cron/sqlite-cron-store.d.ts +230 -0
  25. package/dist/cron/sqlite-cron-store.d.ts.map +1 -0
  26. package/dist/extensions/plugin/plugin-config-loader.d.ts +7 -1
  27. package/dist/extensions/plugin/plugin-config-loader.d.ts.map +1 -1
  28. package/dist/extensions/plugin/plugin-loader.d.ts +10 -6
  29. package/dist/extensions/plugin/plugin-loader.d.ts.map +1 -1
  30. package/dist/extensions/plugin/plugin-sandbox.d.ts +7 -1
  31. package/dist/extensions/plugin/plugin-sandbox.d.ts.map +1 -1
  32. package/dist/extensions/plugin-sandbox-bootstrap.js +236 -275
  33. package/dist/extensions/tools/constants.d.ts +1 -0
  34. package/dist/extensions/tools/constants.d.ts.map +1 -1
  35. package/dist/extensions/tools/definitions.d.ts +2 -3
  36. package/dist/extensions/tools/definitions.d.ts.map +1 -1
  37. package/dist/extensions/tools/executors/editor.d.ts.map +1 -1
  38. package/dist/extensions/tools/helpers.d.ts +1 -0
  39. package/dist/extensions/tools/helpers.d.ts.map +1 -1
  40. package/dist/extensions/tools/index.d.ts +1 -2
  41. package/dist/extensions/tools/index.d.ts.map +1 -1
  42. package/dist/extensions/tools/presets.d.ts +1 -1
  43. package/dist/extensions/tools/schemas.d.ts +25 -3
  44. package/dist/extensions/tools/schemas.d.ts.map +1 -1
  45. package/dist/extensions/tools/team/delegated-agent.d.ts +2 -2
  46. package/dist/extensions/tools/team/delegated-agent.d.ts.map +1 -1
  47. package/dist/extensions/tools/team/multi-agent.d.ts +7 -3
  48. package/dist/extensions/tools/team/multi-agent.d.ts.map +1 -1
  49. package/dist/extensions/tools/team/team-tools.d.ts.map +1 -1
  50. package/dist/extensions/tools/types.d.ts +0 -5
  51. package/dist/extensions/tools/types.d.ts.map +1 -1
  52. package/dist/hooks/hook-bridge.d.ts +118 -0
  53. package/dist/hooks/hook-bridge.d.ts.map +1 -0
  54. package/dist/hooks/hook-file-hooks.d.ts +2 -1
  55. package/dist/hooks/hook-file-hooks.d.ts.map +1 -1
  56. package/dist/hooks/hook-registry.d.ts +16 -0
  57. package/dist/hooks/hook-registry.d.ts.map +1 -0
  58. package/dist/hub/browser-websocket.d.ts.map +1 -1
  59. package/dist/hub/client.d.ts +7 -1
  60. package/dist/hub/client.d.ts.map +1 -1
  61. package/dist/hub/daemon-entry.js +721 -461
  62. package/dist/hub/daemon.d.ts.map +1 -1
  63. package/dist/hub/defaults.d.ts +8 -4
  64. package/dist/hub/defaults.d.ts.map +1 -1
  65. package/dist/hub/index.js +665 -415
  66. package/dist/hub/runtime-handlers.d.ts.map +1 -1
  67. package/dist/hub/server.d.ts +18 -0
  68. package/dist/hub/server.d.ts.map +1 -1
  69. package/dist/hub/session-client.d.ts +3 -0
  70. package/dist/hub/session-client.d.ts.map +1 -1
  71. package/dist/hub/start-shared-server.d.ts.map +1 -1
  72. package/dist/hub/ui-client.d.ts +1 -0
  73. package/dist/hub/ui-client.d.ts.map +1 -1
  74. package/dist/index.d.ts +9 -7
  75. package/dist/index.d.ts.map +1 -1
  76. package/dist/index.js +756 -467
  77. package/dist/llms/cline-recommended-models.d.ts +20 -0
  78. package/dist/llms/cline-recommended-models.d.ts.map +1 -0
  79. package/dist/llms/handler-factory.d.ts +16 -0
  80. package/dist/llms/handler-factory.d.ts.map +1 -0
  81. package/dist/llms/provider-defaults.d.ts.map +1 -1
  82. package/dist/llms/provider-settings.d.ts +45 -2
  83. package/dist/llms/provider-settings.d.ts.map +1 -1
  84. package/dist/llms/runtime-registry.d.ts.map +1 -1
  85. package/dist/runtime/agent-config-adapter.d.ts +148 -0
  86. package/dist/runtime/agent-config-adapter.d.ts.map +1 -0
  87. package/dist/runtime/agent-runtime-config-builder.d.ts +96 -0
  88. package/dist/runtime/agent-runtime-config-builder.d.ts.map +1 -0
  89. package/dist/runtime/history.d.ts +6 -0
  90. package/dist/runtime/history.d.ts.map +1 -1
  91. package/dist/runtime/host.d.ts.map +1 -1
  92. package/dist/runtime/loop-detection.d.ts +59 -0
  93. package/dist/runtime/loop-detection.d.ts.map +1 -0
  94. package/dist/runtime/mistake-tracker.d.ts +69 -0
  95. package/dist/runtime/mistake-tracker.d.ts.map +1 -0
  96. package/dist/runtime/runtime-builder.d.ts.map +1 -1
  97. package/dist/runtime/runtime-event-adapter.d.ts +102 -0
  98. package/dist/runtime/runtime-event-adapter.d.ts.map +1 -0
  99. package/dist/runtime/runtime-host.d.ts +28 -3
  100. package/dist/runtime/runtime-host.d.ts.map +1 -1
  101. package/dist/runtime/session-runtime-orchestrator.d.ts +261 -0
  102. package/dist/runtime/session-runtime-orchestrator.d.ts.map +1 -0
  103. package/dist/runtime/session-runtime.d.ts +16 -3
  104. package/dist/runtime/session-runtime.d.ts.map +1 -1
  105. package/dist/runtime/user-input-builder.d.ts +24 -0
  106. package/dist/runtime/user-input-builder.d.ts.map +1 -0
  107. package/dist/services/index.js +28 -0
  108. package/dist/services/local-runtime-bootstrap.d.ts.map +1 -1
  109. package/dist/services/plugin-tools.d.ts.map +1 -1
  110. package/dist/services/providers/local-provider-registry.d.ts +197 -21
  111. package/dist/services/providers/local-provider-registry.d.ts.map +1 -1
  112. package/dist/services/providers/local-provider-service.d.ts +3 -1
  113. package/dist/services/providers/local-provider-service.d.ts.map +1 -1
  114. package/dist/services/session-data.d.ts.map +1 -1
  115. package/dist/services/session-telemetry.d.ts +7 -2
  116. package/dist/services/session-telemetry.d.ts.map +1 -1
  117. package/dist/services/storage/file-team-store.d.ts.map +1 -1
  118. package/dist/services/storage/provider-settings-legacy-migration.d.ts.map +1 -1
  119. package/dist/services/storage/provider-settings-manager.d.ts +1 -0
  120. package/dist/services/storage/provider-settings-manager.d.ts.map +1 -1
  121. package/dist/services/storage/sqlite-team-store.d.ts.map +1 -1
  122. package/dist/session/conversation-store.d.ts +30 -0
  123. package/dist/session/conversation-store.d.ts.map +1 -0
  124. package/dist/session/message-builder.d.ts +65 -0
  125. package/dist/session/message-builder.d.ts.map +1 -0
  126. package/dist/session/session-manifest.d.ts +1 -1
  127. package/dist/transports/hub.d.ts +14 -3
  128. package/dist/transports/hub.d.ts.map +1 -1
  129. package/dist/transports/local.d.ts +14 -4
  130. package/dist/transports/local.d.ts.map +1 -1
  131. package/dist/transports/remote.d.ts.map +1 -1
  132. package/dist/types/chat-schema.d.ts +5 -5
  133. package/dist/types/config.d.ts +9 -0
  134. package/dist/types/config.d.ts.map +1 -1
  135. package/dist/types/events.d.ts +7 -6
  136. package/dist/types/events.d.ts.map +1 -1
  137. package/dist/types/provider-settings.d.ts +2 -2
  138. package/dist/types/provider-settings.d.ts.map +1 -1
  139. package/dist/types/session.d.ts +5 -2
  140. package/dist/types/session.d.ts.map +1 -1
  141. package/dist/types.d.ts +4 -4
  142. package/dist/types.d.ts.map +1 -1
  143. package/package.json +4 -4
  144. package/src/ClineCore.ts +691 -6
  145. package/src/account/cline-account-service.ts +44 -6
  146. package/src/cron/cron-event-ingress.ts +357 -0
  147. package/src/cron/cron-materializer.ts +97 -0
  148. package/src/cron/cron-reconciler.ts +241 -0
  149. package/src/cron/cron-report-writer.ts +153 -0
  150. package/src/cron/cron-runner.ts +495 -0
  151. package/src/cron/cron-schema.ts +127 -0
  152. package/src/cron/cron-service.ts +163 -0
  153. package/src/cron/cron-spec-parser.ts +489 -0
  154. package/src/cron/cron-watcher.ts +102 -0
  155. package/src/cron/index.ts +10 -0
  156. package/src/cron/scheduler.ts +141 -6
  157. package/src/cron/sqlite-cron-store.ts +1286 -0
  158. package/src/extensions/plugin/plugin-config-loader.ts +21 -1
  159. package/src/extensions/plugin/plugin-loader.ts +25 -9
  160. package/src/extensions/plugin/plugin-sandbox-bootstrap.ts +151 -1
  161. package/src/extensions/plugin/plugin-sandbox.ts +131 -7
  162. package/src/extensions/tools/constants.ts +2 -0
  163. package/src/extensions/tools/definitions.ts +31 -22
  164. package/src/extensions/tools/executors/editor.ts +4 -3
  165. package/src/extensions/tools/helpers.ts +24 -0
  166. package/src/extensions/tools/index.ts +1 -2
  167. package/src/extensions/tools/presets.ts +1 -1
  168. package/src/extensions/tools/schemas.ts +13 -18
  169. package/src/extensions/tools/team/delegated-agent.ts +8 -3
  170. package/src/extensions/tools/team/multi-agent.ts +135 -19
  171. package/src/extensions/tools/team/team-tools.ts +151 -91
  172. package/src/extensions/tools/types.ts +0 -6
  173. package/src/hooks/hook-bridge.ts +489 -0
  174. package/src/hooks/hook-file-hooks.ts +58 -3
  175. package/src/hooks/hook-registry.ts +257 -0
  176. package/src/hub/browser-websocket.ts +26 -4
  177. package/src/hub/client.ts +72 -13
  178. package/src/hub/daemon-entry.ts +35 -0
  179. package/src/hub/daemon.ts +117 -14
  180. package/src/hub/defaults.ts +39 -12
  181. package/src/hub/runtime-handlers.ts +4 -3
  182. package/src/hub/server.ts +506 -77
  183. package/src/hub/session-client.ts +43 -1
  184. package/src/hub/start-shared-server.ts +3 -0
  185. package/src/hub/ui-client.ts +4 -0
  186. package/src/index.ts +46 -1
  187. package/src/llms/cline-recommended-models.ts +167 -0
  188. package/src/llms/handler-factory.ts +56 -0
  189. package/src/llms/provider-defaults.ts +17 -1
  190. package/src/llms/provider-settings.ts +48 -1
  191. package/src/llms/runtime-registry.ts +1 -0
  192. package/src/runtime/agent-config-adapter.ts +636 -0
  193. package/src/runtime/agent-runtime-config-builder.ts +205 -0
  194. package/src/runtime/error-feedback.ts +142 -0
  195. package/src/runtime/history.ts +137 -0
  196. package/src/runtime/host.ts +22 -0
  197. package/src/runtime/loop-detection.ts +162 -0
  198. package/src/runtime/mistake-tracker.ts +221 -0
  199. package/src/runtime/runtime-builder.ts +61 -5
  200. package/src/runtime/runtime-event-adapter.ts +412 -0
  201. package/src/runtime/runtime-host.ts +45 -1
  202. package/src/runtime/session-runtime-orchestrator.ts +1253 -0
  203. package/src/runtime/session-runtime.ts +16 -2
  204. package/src/runtime/user-input-builder.ts +167 -0
  205. package/src/services/local-runtime-bootstrap.ts +128 -22
  206. package/src/services/plugin-tools.ts +1 -0
  207. package/src/services/providers/local-provider-registry.ts +273 -57
  208. package/src/services/providers/local-provider-service.ts +67 -7
  209. package/src/services/session-data.ts +16 -14
  210. package/src/services/session-telemetry.ts +6 -15
  211. package/src/services/storage/file-team-store.ts +1 -5
  212. package/src/services/storage/provider-settings-legacy-migration.ts +8 -47
  213. package/src/services/storage/provider-settings-manager.ts +16 -1
  214. package/src/services/storage/sqlite-team-store.ts +1 -5
  215. package/src/session/conversation-store.ts +77 -0
  216. package/src/session/message-builder.ts +941 -0
  217. package/src/transports/hub.ts +458 -33
  218. package/src/transports/local.ts +296 -65
  219. package/src/transports/remote.ts +1 -0
  220. package/src/types/config.ts +9 -0
  221. package/src/types/events.ts +8 -6
  222. package/src/types/index.ts +3 -0
  223. package/src/types/provider-settings.ts +8 -1
  224. package/src/types/session.ts +5 -2
  225. package/src/types.ts +15 -1
  226. package/dist/cron/index.d.ts +0 -6
  227. package/dist/cron/index.d.ts.map +0 -1
  228. package/dist/services/telemetry/index.js +0 -28
@@ -0,0 +1,163 @@
1
+ import type { AutomationEventEnvelope, BasicLogger } from "@clinebot/shared";
2
+ import type { ResolveCronSpecsDirOptions } from "@clinebot/shared/storage";
3
+ import {
4
+ CronEventIngress,
5
+ type CronEventIngressResult,
6
+ } from "./cron-event-ingress";
7
+ import { CronMaterializer } from "./cron-materializer";
8
+ import { CronReconciler } from "./cron-reconciler";
9
+ import { CronRunner } from "./cron-runner";
10
+ import { CronWatcher } from "./cron-watcher";
11
+ import type { HubScheduleRuntimeHandlers } from "./schedule-service";
12
+ import type {
13
+ CronRunRecord,
14
+ CronSpecRecord,
15
+ ListEventLogsOptions,
16
+ ListRunsOptions,
17
+ ListSpecsOptions,
18
+ } from "./sqlite-cron-store";
19
+ import { SqliteCronStore } from "./sqlite-cron-store";
20
+
21
+ /**
22
+ * Top-level orchestrator for file-based cron automation.
23
+ *
24
+ * Wires together:
25
+ * - `SqliteCronStore` (cron.db)
26
+ * - `CronReconciler` (disk -> DB)
27
+ * - `CronWatcher` (cron specs directory filesystem events)
28
+ * - `CronMaterializer` (queue materialization)
29
+ * - `CronRunner` (claim + execute + report)
30
+ *
31
+ * This service is the forward path: the legacy `HubScheduleService`
32
+ * continues to serve programmatic schedules, while `CronService` handles
33
+ * everything sourced from the configured file-based cron directory.
34
+ */
35
+
36
+ export interface CronServiceOptions {
37
+ /** Default runtime workspace for the hub/daemon process. */
38
+ workspaceRoot: string;
39
+ /** Cron spec source/report location. Defaults to global `~/.cline/cron`. */
40
+ specs?: ResolveCronSpecsDirOptions;
41
+ runtimeHandlers: HubScheduleRuntimeHandlers;
42
+ dbPath?: string;
43
+ logger?: BasicLogger;
44
+ pollIntervalMs?: number;
45
+ claimLeaseSeconds?: number;
46
+ globalMaxConcurrency?: number;
47
+ watcherDebounceMs?: number;
48
+ }
49
+
50
+ export class CronService {
51
+ private readonly store: SqliteCronStore;
52
+ private readonly reconciler: CronReconciler;
53
+ private readonly watcher: CronWatcher;
54
+ private readonly eventIngress: CronEventIngress;
55
+ private readonly materializer: CronMaterializer;
56
+ private readonly runner: CronRunner;
57
+ private started = false;
58
+ private disposed = false;
59
+
60
+ constructor(options: CronServiceOptions) {
61
+ this.store = new SqliteCronStore({ dbPath: options.dbPath });
62
+ const specs = options.specs;
63
+ this.reconciler = new CronReconciler({
64
+ store: this.store,
65
+ specs,
66
+ });
67
+ this.materializer = new CronMaterializer({ store: this.store });
68
+ this.eventIngress = new CronEventIngress({
69
+ store: this.store,
70
+ logger: options.logger,
71
+ });
72
+ this.runner = new CronRunner({
73
+ store: this.store,
74
+ materializer: this.materializer,
75
+ runtimeHandlers: options.runtimeHandlers,
76
+ workspaceRoot: options.workspaceRoot,
77
+ specs,
78
+ logger: options.logger,
79
+ pollIntervalMs: options.pollIntervalMs,
80
+ claimLeaseSeconds: options.claimLeaseSeconds,
81
+ globalMaxConcurrency: options.globalMaxConcurrency,
82
+ });
83
+ this.watcher = new CronWatcher({
84
+ reconciler: this.reconciler,
85
+ debounceMs: options.watcherDebounceMs,
86
+ onReconciled: () => {
87
+ this.materializer.materializeAll();
88
+ },
89
+ onError: (err) => {
90
+ const log = options.logger;
91
+ if (log) {
92
+ if (log.error) log.error("cron.watcher.failed", { error: err });
93
+ else log.log("cron.watcher.failed", { error: err });
94
+ }
95
+ },
96
+ });
97
+ }
98
+
99
+ public async start(): Promise<void> {
100
+ if (this.disposed) throw new Error("CronService disposed");
101
+ if (this.started) return;
102
+ this.started = true;
103
+ await this.reconciler.reconcileAll();
104
+ this.materializer.materializeAll();
105
+ this.watcher.start();
106
+ await this.runner.start();
107
+ }
108
+
109
+ public async stop(): Promise<void> {
110
+ this.watcher.stop();
111
+ await this.runner.stop();
112
+ this.started = false;
113
+ }
114
+
115
+ public async dispose(): Promise<void> {
116
+ if (this.disposed) return;
117
+ this.disposed = true;
118
+ this.watcher.dispose();
119
+ await this.runner.dispose();
120
+ this.store.close();
121
+ }
122
+
123
+ public listSpecs(options?: ListSpecsOptions): CronSpecRecord[] {
124
+ return this.store.listSpecs(options);
125
+ }
126
+
127
+ public getSpec(specId: string): CronSpecRecord | undefined {
128
+ return this.store.getSpec(specId);
129
+ }
130
+
131
+ public listRuns(options?: ListRunsOptions): CronRunRecord[] {
132
+ return this.store.listRuns(options);
133
+ }
134
+
135
+ public getRun(runId: string): CronRunRecord | undefined {
136
+ return this.store.getRun(runId);
137
+ }
138
+
139
+ public listActiveRuns(): CronRunRecord[] {
140
+ return this.store.listRuns({ status: "running", limit: 200 });
141
+ }
142
+
143
+ public listUpcomingRuns(limit = 20): CronRunRecord[] {
144
+ return this.store.listRuns({ status: "queued", limit });
145
+ }
146
+
147
+ public async reconcileNow(): Promise<void> {
148
+ await this.reconciler.reconcileAll();
149
+ this.materializer.materializeAll();
150
+ }
151
+
152
+ public ingestEvent(event: AutomationEventEnvelope): CronEventIngressResult {
153
+ return this.eventIngress.ingestEvent(event);
154
+ }
155
+
156
+ public listEventLogs(options?: ListEventLogsOptions) {
157
+ return this.store.listEventLogs(options);
158
+ }
159
+
160
+ public getEventLog(eventId: string) {
161
+ return this.store.getEventLog(eventId);
162
+ }
163
+ }
@@ -0,0 +1,489 @@
1
+ import { createHash } from "node:crypto";
2
+ import type {
3
+ CronEventSpec,
4
+ CronOneOffSpec,
5
+ CronScheduleSpec,
6
+ CronSpec,
7
+ CronSpecExtensionKind,
8
+ CronSpecMode,
9
+ CronSpecParseResult,
10
+ CronTriggerKind,
11
+ } from "@clinebot/shared";
12
+ import YAML from "yaml";
13
+ import { ALL_DEFAULT_TOOL_NAMES } from "../extensions/tools/constants";
14
+ import { validateCronSchedule } from "./scheduler";
15
+
16
+ /**
17
+ * Markdown frontmatter parser for `.cline/cron/*.md` automation specs.
18
+ *
19
+ * Lives in @clinebot/core because it depends on `yaml`. The spec types
20
+ * themselves live in @clinebot/shared so other packages can consume them
21
+ * without pulling in a YAML parser.
22
+ *
23
+ * The parser never throws for a single bad file — it produces a
24
+ * `CronSpecParseResult` with an `error` message so the reconciler can record
25
+ * `parse_status='invalid'` durably instead of dropping state.
26
+ */
27
+
28
+ export function inferTriggerKindFromPath(
29
+ relativePath: string,
30
+ ): CronTriggerKind {
31
+ const normalized = relativePath.replace(/\\/g, "/");
32
+ if (normalized.startsWith("events/") && normalized.endsWith(".event.md")) {
33
+ return "event";
34
+ }
35
+ if (normalized.endsWith(".cron.md")) {
36
+ return "schedule";
37
+ }
38
+ return "one_off";
39
+ }
40
+
41
+ export function splitFrontmatter(raw: string): {
42
+ frontmatter: string | undefined;
43
+ body: string;
44
+ } {
45
+ const text = raw.replace(/\r\n/g, "\n");
46
+ if (!text.startsWith("---\n")) {
47
+ return { frontmatter: undefined, body: raw };
48
+ }
49
+ const afterOpen = text.slice(4);
50
+ const closeIdx = afterOpen.indexOf("\n---");
51
+ if (closeIdx === -1) {
52
+ return { frontmatter: undefined, body: raw };
53
+ }
54
+ const frontmatter = afterOpen.slice(0, closeIdx);
55
+ let rest = afterOpen.slice(closeIdx + 4);
56
+ if (rest.startsWith("\n")) rest = rest.slice(1);
57
+ return { frontmatter, body: rest };
58
+ }
59
+
60
+ function trimOrUndefined(value: unknown): string | undefined {
61
+ if (typeof value !== "string") return undefined;
62
+ const trimmed = value.trim();
63
+ return trimmed.length > 0 ? trimmed : undefined;
64
+ }
65
+
66
+ function normalizeTags(value: unknown): string[] | undefined {
67
+ if (!Array.isArray(value)) return undefined;
68
+ const tags = value
69
+ .map((item) => (typeof item === "string" ? item.trim() : ""))
70
+ .filter((item) => item.length > 0);
71
+ return tags.length > 0 ? tags : undefined;
72
+ }
73
+
74
+ function normalizeRecord(value: unknown): Record<string, unknown> | undefined {
75
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
76
+ return undefined;
77
+ }
78
+ return value as Record<string, unknown>;
79
+ }
80
+
81
+ function normalizeModelSelection(
82
+ value: unknown,
83
+ ): { providerId?: string; modelId?: string } | undefined {
84
+ const obj = normalizeRecord(value);
85
+ if (!obj) return undefined;
86
+ const providerId = trimOrUndefined(obj.providerId);
87
+ const modelId = trimOrUndefined(obj.modelId);
88
+ if (providerId === undefined && modelId === undefined) return undefined;
89
+ return { providerId, modelId };
90
+ }
91
+
92
+ function normalizeMode(value: unknown): CronSpecMode | undefined {
93
+ if (typeof value !== "string") return undefined;
94
+ const lower = value.trim().toLowerCase();
95
+ if (lower === "act" || lower === "plan" || lower === "yolo") return lower;
96
+ return undefined;
97
+ }
98
+
99
+ function normalizeStringList(
100
+ value: unknown,
101
+ options: { preserveEmptyArray?: boolean } = {},
102
+ ): string[] | undefined {
103
+ const raw = Array.isArray(value)
104
+ ? value
105
+ : typeof value === "string"
106
+ ? value.split(",")
107
+ : undefined;
108
+ if (!raw) return undefined;
109
+ const normalized = [
110
+ ...new Set(
111
+ raw
112
+ .map((item) => (typeof item === "string" ? item.trim() : ""))
113
+ .filter((item) => item.length > 0),
114
+ ),
115
+ ];
116
+ if (Array.isArray(value) && options.preserveEmptyArray) {
117
+ return normalized;
118
+ }
119
+ return normalized.length > 0 ? normalized : undefined;
120
+ }
121
+
122
+ const DEFAULT_TOOL_NAME_SET = new Set<string>(ALL_DEFAULT_TOOL_NAMES);
123
+
124
+ function normalizeToolList(value: unknown): string[] | undefined {
125
+ const tools = normalizeStringList(value, { preserveEmptyArray: true });
126
+ if (!tools) return undefined;
127
+ const invalid = tools.filter((tool) => !DEFAULT_TOOL_NAME_SET.has(tool));
128
+ if (invalid.length > 0) {
129
+ throw new Error(`unknown tool(s): ${invalid.join(", ")}`);
130
+ }
131
+ return tools;
132
+ }
133
+
134
+ const CRON_EXTENSION_KINDS = new Set<CronSpecExtensionKind>([
135
+ "rules",
136
+ "skills",
137
+ "plugins",
138
+ ]);
139
+
140
+ function normalizeExtensions(
141
+ value: unknown,
142
+ ): CronSpecExtensionKind[] | undefined {
143
+ const extensions = normalizeStringList(value, { preserveEmptyArray: true });
144
+ if (!extensions) return undefined;
145
+ const invalid = extensions.filter(
146
+ (extension) =>
147
+ !CRON_EXTENSION_KINDS.has(extension as CronSpecExtensionKind),
148
+ );
149
+ if (invalid.length > 0) {
150
+ throw new Error(`unknown extension(s): ${invalid.join(", ")}`);
151
+ }
152
+ return extensions as CronSpecExtensionKind[];
153
+ }
154
+
155
+ function asPositiveInt(value: unknown): number | undefined {
156
+ if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {
157
+ return undefined;
158
+ }
159
+ return Math.floor(value);
160
+ }
161
+
162
+ function asNonNegativeInt(value: unknown): number | undefined {
163
+ if (typeof value !== "number" || !Number.isFinite(value) || value < 0) {
164
+ return undefined;
165
+ }
166
+ return Math.floor(value);
167
+ }
168
+
169
+ function stableStringify(value: unknown): string {
170
+ if (value === null || typeof value !== "object") {
171
+ return JSON.stringify(value ?? null);
172
+ }
173
+ if (Array.isArray(value)) {
174
+ return `[${value.map((item) => stableStringify(item)).join(",")}]`;
175
+ }
176
+ const entries = Object.entries(value as Record<string, unknown>).filter(
177
+ ([, v]) => v !== undefined,
178
+ );
179
+ entries.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));
180
+ return `{${entries
181
+ .map(([k, v]) => `${JSON.stringify(k)}:${stableStringify(v)}`)
182
+ .join(",")}}`;
183
+ }
184
+
185
+ export function computeContentHash(
186
+ frontmatterJson: unknown,
187
+ body: string,
188
+ ): string {
189
+ const hash = createHash("sha256");
190
+ hash.update(stableStringify(frontmatterJson));
191
+ hash.update("\n");
192
+ hash.update(body);
193
+ return hash.digest("hex");
194
+ }
195
+
196
+ function filenameStem(relativePath: string): string {
197
+ const base = relativePath.split("/").pop() ?? relativePath;
198
+ return base
199
+ .replace(/\.event\.md$/, "")
200
+ .replace(/\.cron\.md$/, "")
201
+ .replace(/\.md$/, "");
202
+ }
203
+
204
+ const SCHEDULE_ONLY_FIELDS = ["schedule", "timezone"] as const;
205
+ const EVENT_ONLY_FIELDS = [
206
+ "event",
207
+ "filters",
208
+ "debounceSeconds",
209
+ "dedupeWindowSeconds",
210
+ "cooldownSeconds",
211
+ "maxParallel",
212
+ ] as const;
213
+ const REMOVED_FIELDS = ["cwd"] as const;
214
+
215
+ export interface ParseCronSpecInput {
216
+ relativePath: string;
217
+ raw: string;
218
+ }
219
+
220
+ function invalid(
221
+ relativePath: string,
222
+ triggerKind: CronTriggerKind,
223
+ body: string,
224
+ frontmatter: Record<string, unknown>,
225
+ error: string,
226
+ ): CronSpecParseResult {
227
+ return {
228
+ externalId: relativePath,
229
+ relativePath,
230
+ triggerKind,
231
+ body,
232
+ contentHash: computeContentHash(frontmatter, body),
233
+ error,
234
+ };
235
+ }
236
+
237
+ function invalidWithHash(
238
+ externalId: string,
239
+ relativePath: string,
240
+ triggerKind: CronTriggerKind,
241
+ body: string,
242
+ contentHash: string,
243
+ error: string,
244
+ ): CronSpecParseResult {
245
+ return {
246
+ externalId,
247
+ relativePath,
248
+ triggerKind,
249
+ body,
250
+ contentHash,
251
+ error,
252
+ };
253
+ }
254
+
255
+ /**
256
+ * Parse a single cron spec file. Never throws; always returns a result.
257
+ */
258
+ export function parseCronSpecFile(
259
+ input: ParseCronSpecInput,
260
+ ): CronSpecParseResult {
261
+ const relativePath = input.relativePath.replace(/\\/g, "/");
262
+ const triggerKind = inferTriggerKindFromPath(relativePath);
263
+ const { frontmatter, body } = splitFrontmatter(input.raw);
264
+
265
+ let frontmatterData: Record<string, unknown> = {};
266
+ if (frontmatter !== undefined && frontmatter.trim().length > 0) {
267
+ try {
268
+ const parsed = YAML.parse(frontmatter) as unknown;
269
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
270
+ frontmatterData = parsed as Record<string, unknown>;
271
+ } else if (parsed !== null && parsed !== undefined) {
272
+ return invalid(
273
+ relativePath,
274
+ triggerKind,
275
+ body,
276
+ {},
277
+ "frontmatter must be a YAML mapping",
278
+ );
279
+ }
280
+ } catch (err) {
281
+ return invalid(
282
+ relativePath,
283
+ triggerKind,
284
+ body,
285
+ {},
286
+ err instanceof Error
287
+ ? `failed to parse frontmatter: ${err.message}`
288
+ : "failed to parse frontmatter",
289
+ );
290
+ }
291
+ }
292
+
293
+ const contentHash = computeContentHash(frontmatterData, body);
294
+ const externalIdRaw = trimOrUndefined(frontmatterData.id);
295
+ const externalId = externalIdRaw ?? relativePath;
296
+
297
+ if (triggerKind !== "schedule") {
298
+ for (const key of SCHEDULE_ONLY_FIELDS) {
299
+ if (frontmatterData[key] !== undefined) {
300
+ return invalidWithHash(
301
+ externalId,
302
+ relativePath,
303
+ triggerKind,
304
+ body,
305
+ contentHash,
306
+ `field "${key}" is only allowed on *.cron.md specs`,
307
+ );
308
+ }
309
+ }
310
+ }
311
+ for (const key of REMOVED_FIELDS) {
312
+ if (frontmatterData[key] !== undefined) {
313
+ return invalidWithHash(
314
+ externalId,
315
+ relativePath,
316
+ triggerKind,
317
+ body,
318
+ contentHash,
319
+ `field "${key}" is no longer supported; cron specs use workspaceRoot as cwd`,
320
+ );
321
+ }
322
+ }
323
+ if (triggerKind !== "event") {
324
+ for (const key of EVENT_ONLY_FIELDS) {
325
+ if (frontmatterData[key] !== undefined) {
326
+ return invalidWithHash(
327
+ externalId,
328
+ relativePath,
329
+ triggerKind,
330
+ body,
331
+ contentHash,
332
+ `field "${key}" is only allowed on .event.md specs`,
333
+ );
334
+ }
335
+ }
336
+ }
337
+
338
+ const frontmatterPrompt = trimOrUndefined(frontmatterData.prompt);
339
+ const bodyTrimmed = body.trim();
340
+ const prompt =
341
+ frontmatterPrompt ?? (bodyTrimmed.length > 0 ? bodyTrimmed : undefined);
342
+ if (!prompt) {
343
+ return invalidWithHash(
344
+ externalId,
345
+ relativePath,
346
+ triggerKind,
347
+ body,
348
+ contentHash,
349
+ "prompt is required (frontmatter `prompt` or markdown body)",
350
+ );
351
+ }
352
+
353
+ const workspaceRoot = trimOrUndefined(frontmatterData.workspaceRoot);
354
+ if (!workspaceRoot) {
355
+ return invalidWithHash(
356
+ externalId,
357
+ relativePath,
358
+ triggerKind,
359
+ body,
360
+ contentHash,
361
+ "workspaceRoot is required",
362
+ );
363
+ }
364
+
365
+ let tools: string[] | undefined;
366
+ let extensions: CronSpecExtensionKind[] | undefined;
367
+ try {
368
+ tools = normalizeToolList(frontmatterData.tools);
369
+ extensions = normalizeExtensions(frontmatterData.extensions);
370
+ } catch (err) {
371
+ return invalidWithHash(
372
+ externalId,
373
+ relativePath,
374
+ triggerKind,
375
+ body,
376
+ contentHash,
377
+ err instanceof Error ? err.message : String(err),
378
+ );
379
+ }
380
+
381
+ const mode = normalizeMode(frontmatterData.mode);
382
+ if (frontmatterData.mode !== undefined && mode === undefined) {
383
+ return invalidWithHash(
384
+ externalId,
385
+ relativePath,
386
+ triggerKind,
387
+ body,
388
+ contentHash,
389
+ "mode must be one of: act, plan, yolo",
390
+ );
391
+ }
392
+
393
+ const common = {
394
+ id: externalIdRaw,
395
+ title:
396
+ trimOrUndefined(frontmatterData.title) ??
397
+ externalIdRaw ??
398
+ filenameStem(relativePath),
399
+ prompt,
400
+ workspaceRoot,
401
+ mode: mode ?? "yolo",
402
+ systemPrompt: trimOrUndefined(frontmatterData.systemPrompt),
403
+ modelSelection: normalizeModelSelection(frontmatterData.modelSelection),
404
+ maxIterations: asPositiveInt(frontmatterData.maxIterations),
405
+ timeoutSeconds: asPositiveInt(frontmatterData.timeoutSeconds),
406
+ tools,
407
+ notesDirectory: trimOrUndefined(frontmatterData.notesDirectory),
408
+ extensions,
409
+ source: trimOrUndefined(frontmatterData.source) ?? "user",
410
+ tags: normalizeTags(frontmatterData.tags),
411
+ enabled:
412
+ typeof frontmatterData.enabled === "boolean"
413
+ ? frontmatterData.enabled
414
+ : true,
415
+ metadata: normalizeRecord(frontmatterData.metadata),
416
+ };
417
+
418
+ let spec: CronSpec;
419
+ if (triggerKind === "schedule") {
420
+ const schedule = trimOrUndefined(frontmatterData.schedule);
421
+ if (!schedule) {
422
+ return invalidWithHash(
423
+ externalId,
424
+ relativePath,
425
+ triggerKind,
426
+ body,
427
+ contentHash,
428
+ "schedule is required for *.cron.md specs",
429
+ );
430
+ }
431
+ const timezone = trimOrUndefined(frontmatterData.timezone);
432
+ try {
433
+ validateCronSchedule(schedule, timezone);
434
+ } catch (err) {
435
+ return invalidWithHash(
436
+ externalId,
437
+ relativePath,
438
+ triggerKind,
439
+ body,
440
+ contentHash,
441
+ err instanceof Error ? err.message : String(err),
442
+ );
443
+ }
444
+ const s: CronScheduleSpec = {
445
+ ...common,
446
+ triggerKind: "schedule",
447
+ schedule,
448
+ timezone,
449
+ };
450
+ spec = s;
451
+ } else if (triggerKind === "event") {
452
+ const event = trimOrUndefined(frontmatterData.event);
453
+ if (!event) {
454
+ return invalidWithHash(
455
+ externalId,
456
+ relativePath,
457
+ triggerKind,
458
+ body,
459
+ contentHash,
460
+ "event is required for .event.md specs",
461
+ );
462
+ }
463
+ const e: CronEventSpec = {
464
+ ...common,
465
+ triggerKind: "event",
466
+ event,
467
+ filters: normalizeRecord(frontmatterData.filters),
468
+ debounceSeconds: asNonNegativeInt(frontmatterData.debounceSeconds),
469
+ dedupeWindowSeconds: asNonNegativeInt(
470
+ frontmatterData.dedupeWindowSeconds,
471
+ ),
472
+ cooldownSeconds: asNonNegativeInt(frontmatterData.cooldownSeconds),
473
+ maxParallel: asPositiveInt(frontmatterData.maxParallel),
474
+ };
475
+ spec = e;
476
+ } else {
477
+ const o: CronOneOffSpec = { ...common, triggerKind: "one_off" };
478
+ spec = o;
479
+ }
480
+
481
+ return {
482
+ externalId,
483
+ relativePath,
484
+ triggerKind,
485
+ body,
486
+ contentHash,
487
+ spec,
488
+ };
489
+ }