@cjhyy/code-shell-core 0.5.0-rc.1 → 0.5.0-rc.2

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 (278) hide show
  1. package/dist/agent/agent-definition-registry.d.ts +18 -3
  2. package/dist/agent/agent-definition-registry.js +47 -18
  3. package/dist/agent/agent-definition.d.ts +17 -0
  4. package/dist/agent/agent-definition.js +22 -1
  5. package/dist/arena/arena.js +11 -12
  6. package/dist/arena/context/context-tools.js +2 -4
  7. package/dist/arena/context/within-root.d.ts +7 -0
  8. package/dist/arena/context/within-root.js +15 -0
  9. package/dist/arena/iterate/phases/argue.js +8 -2
  10. package/dist/arena/iterate/phases/revise.js +1 -1
  11. package/dist/arena/iterate/phases/tournament.js +3 -3
  12. package/dist/arena/phases/adjudication.js +1 -4
  13. package/dist/arena/phases/build-consensus.js +1 -4
  14. package/dist/arena/phases/cross-review.js +2 -8
  15. package/dist/arena/phases/debate-rounds.js +1 -4
  16. package/dist/arena/phases/participant-research.js +1 -4
  17. package/dist/arena/phases/planning-detail-expansion.js +8 -10
  18. package/dist/arena/planner.js +0 -1
  19. package/dist/arena/providers/docs.d.ts +1 -0
  20. package/dist/arena/providers/docs.js +5 -2
  21. package/dist/arena/providers/repo.d.ts +1 -0
  22. package/dist/arena/providers/repo.js +5 -2
  23. package/dist/arena/strategies/utils.js +36 -3
  24. package/dist/arena/transitions.d.ts +7 -2
  25. package/dist/arena/transitions.js +9 -4
  26. package/dist/arena/types.d.ts +8 -2
  27. package/dist/automation/cron-expr.d.ts +31 -0
  28. package/dist/automation/cron-expr.js +151 -0
  29. package/dist/automation/index.d.ts +41 -0
  30. package/dist/automation/index.js +39 -0
  31. package/dist/automation/runner.d.ts +67 -0
  32. package/dist/automation/runner.js +56 -0
  33. package/dist/automation/scheduler.d.ts +156 -0
  34. package/dist/automation/scheduler.js +546 -0
  35. package/dist/automation/store.d.ts +39 -0
  36. package/dist/automation/store.js +119 -0
  37. package/dist/automation/write-policy.d.ts +34 -0
  38. package/dist/automation/write-policy.js +113 -0
  39. package/dist/automation/write-run.d.ts +51 -0
  40. package/dist/automation/write-run.js +38 -0
  41. package/dist/capability-control/index.d.ts +5 -0
  42. package/dist/capability-control/index.js +3 -0
  43. package/dist/capability-control/overlay.d.ts +49 -0
  44. package/dist/capability-control/overlay.js +89 -0
  45. package/dist/capability-control/project.d.ts +65 -0
  46. package/dist/capability-control/project.js +147 -0
  47. package/dist/capability-control/service.d.ts +70 -0
  48. package/dist/capability-control/service.js +147 -0
  49. package/dist/capability-control/types.d.ts +62 -0
  50. package/dist/capability-control/types.js +17 -0
  51. package/dist/cli/agent-server-stdio.d.ts +12 -1
  52. package/dist/cli/agent-server-stdio.js +114 -19
  53. package/dist/cli/agent-server-tcp.d.ts +20 -0
  54. package/dist/cli/agent-server-tcp.js +120 -0
  55. package/dist/cli/graceful-shutdown.d.ts +27 -0
  56. package/dist/cli/graceful-shutdown.js +30 -0
  57. package/dist/context/compaction.js +23 -15
  58. package/dist/context/manager.d.ts +0 -20
  59. package/dist/context/manager.js +0 -37
  60. package/dist/cron/cron-runtime.d.ts +2 -0
  61. package/dist/cron/cron-runtime.js +2 -0
  62. package/dist/cron/cron-store.d.ts +2 -0
  63. package/dist/cron/cron-store.js +2 -0
  64. package/dist/cron/scheduler.d.ts +5 -32
  65. package/dist/cron/scheduler.js +5 -113
  66. package/dist/data/openrouter-sync.js +1 -1
  67. package/dist/engine/engine.d.ts +208 -14
  68. package/dist/engine/engine.js +754 -169
  69. package/dist/engine/goal.d.ts +42 -0
  70. package/dist/engine/goal.js +52 -0
  71. package/dist/engine/image-compression.d.ts +81 -0
  72. package/dist/engine/image-compression.js +186 -0
  73. package/dist/engine/image-policy.d.ts +152 -0
  74. package/dist/engine/image-policy.js +165 -0
  75. package/dist/engine/reactive-threshold.d.ts +13 -0
  76. package/dist/engine/reactive-threshold.js +17 -0
  77. package/dist/engine/runtime.js +7 -0
  78. package/dist/engine/session-title.d.ts +13 -0
  79. package/dist/engine/session-title.js +34 -0
  80. package/dist/engine/turn-loop.d.ts +29 -8
  81. package/dist/engine/turn-loop.js +498 -316
  82. package/dist/git/parse-log.d.ts +13 -0
  83. package/dist/git/parse-log.js +21 -0
  84. package/dist/git/utils.d.ts +8 -6
  85. package/dist/git/utils.js +57 -26
  86. package/dist/git/worktree.js +33 -18
  87. package/dist/hooks/events.d.ts +23 -1
  88. package/dist/hooks/goal-stop-hook.d.ts +30 -0
  89. package/dist/hooks/goal-stop-hook.js +102 -0
  90. package/dist/hooks/hook-output.d.ts +33 -0
  91. package/dist/hooks/hook-output.js +79 -0
  92. package/dist/hooks/registry.d.ts +7 -0
  93. package/dist/hooks/registry.js +38 -1
  94. package/dist/hooks/shell-runner.d.ts +2 -0
  95. package/dist/hooks/shell-runner.js +43 -2
  96. package/dist/index.d.ts +31 -6
  97. package/dist/index.js +30 -3
  98. package/dist/llm/capabilities/reasoning-control.d.ts +24 -0
  99. package/dist/llm/capabilities/reasoning-control.js +38 -0
  100. package/dist/llm/capabilities/rules.js +33 -3
  101. package/dist/llm/capabilities/types.d.ts +22 -2
  102. package/dist/llm/clamp-max-tokens.d.ts +11 -0
  103. package/dist/llm/clamp-max-tokens.js +17 -0
  104. package/dist/llm/client-base.d.ts +41 -4
  105. package/dist/llm/client-base.js +67 -13
  106. package/dist/llm/client-factory.d.ts +3 -3
  107. package/dist/llm/client-factory.js +2 -2
  108. package/dist/llm/model-pool.d.ts +15 -11
  109. package/dist/llm/model-pool.js +23 -20
  110. package/dist/llm/provider-catalog.d.ts +2 -2
  111. package/dist/llm/providers/anthropic.d.ts +32 -2
  112. package/dist/llm/providers/anthropic.js +147 -8
  113. package/dist/llm/providers/openai.d.ts +11 -2
  114. package/dist/llm/providers/openai.js +253 -52
  115. package/dist/llm/reasoning-setting.d.ts +51 -0
  116. package/dist/llm/reasoning-setting.js +32 -0
  117. package/dist/llm/stop-reason.d.ts +13 -0
  118. package/dist/llm/stop-reason.js +21 -0
  119. package/dist/llm/strip-vision.d.ts +32 -0
  120. package/dist/llm/strip-vision.js +94 -0
  121. package/dist/llm/types.d.ts +5 -6
  122. package/dist/logging/logger.js +18 -4
  123. package/dist/logging/sanitize-messages.d.ts +10 -0
  124. package/dist/logging/sanitize-messages.js +90 -0
  125. package/dist/lsp/client.js +28 -8
  126. package/dist/lsp/manager.js +2 -1
  127. package/dist/lsp/root-path.d.ts +9 -0
  128. package/dist/lsp/root-path.js +12 -0
  129. package/dist/onboarding.js +18 -11
  130. package/dist/plugins/gitOps.d.ts +34 -0
  131. package/dist/plugins/gitOps.js +59 -3
  132. package/dist/plugins/installer/codex/convertAgents.d.ts +7 -0
  133. package/dist/plugins/installer/codex/convertAgents.js +55 -0
  134. package/dist/plugins/installer/codex/convertMcp.d.ts +8 -0
  135. package/dist/plugins/installer/codex/convertMcp.js +45 -0
  136. package/dist/plugins/installer/codex/convertSkills.d.ts +12 -0
  137. package/dist/plugins/installer/codex/convertSkills.js +33 -0
  138. package/dist/plugins/installer/detectFormat.d.ts +2 -0
  139. package/dist/plugins/installer/detectFormat.js +6 -0
  140. package/dist/plugins/installer/install.d.ts +7 -0
  141. package/dist/plugins/installer/install.js +99 -0
  142. package/dist/plugins/installer/installFromSource.d.ts +13 -0
  143. package/dist/plugins/installer/installFromSource.js +45 -0
  144. package/dist/plugins/installer/list.d.ts +9 -0
  145. package/dist/plugins/installer/list.js +30 -0
  146. package/dist/plugins/installer/loadPluginAgents.d.ts +3 -0
  147. package/dist/plugins/installer/loadPluginAgents.js +23 -0
  148. package/dist/plugins/installer/loadPluginMcp.d.ts +7 -0
  149. package/dist/plugins/installer/loadPluginMcp.js +74 -0
  150. package/dist/plugins/installer/parseSource.d.ts +20 -0
  151. package/dist/plugins/installer/parseSource.js +74 -0
  152. package/dist/plugins/installer/paths.d.ts +5 -0
  153. package/dist/plugins/installer/paths.js +25 -0
  154. package/dist/plugins/installer/types.d.ts +49 -0
  155. package/dist/plugins/installer/types.js +27 -0
  156. package/dist/plugins/installer/uninstall.d.ts +2 -0
  157. package/dist/plugins/installer/uninstall.js +14 -0
  158. package/dist/plugins/installer/update.d.ts +14 -0
  159. package/dist/plugins/installer/update.js +65 -0
  160. package/dist/plugins/loadPluginHooks.d.ts +8 -1
  161. package/dist/plugins/loadPluginHooks.js +11 -1
  162. package/dist/plugins/marketplaceManager.d.ts +8 -1
  163. package/dist/plugins/marketplaceManager.js +38 -4
  164. package/dist/plugins/pluginCommandHook.js +34 -0
  165. package/dist/plugins/pluginInstaller.d.ts +21 -0
  166. package/dist/plugins/pluginInstaller.js +128 -29
  167. package/dist/plugins/schemas.js +40 -6
  168. package/dist/plugins/types.d.ts +4 -0
  169. package/dist/preset/index.js +16 -0
  170. package/dist/prompt/composer.d.ts +4 -0
  171. package/dist/prompt/composer.js +28 -4
  172. package/dist/prompt/sections/orchestration.md +8 -0
  173. package/dist/protocol/chat-session-manager.d.ts +8 -1
  174. package/dist/protocol/chat-session-manager.js +10 -0
  175. package/dist/protocol/chat-session.d.ts +17 -0
  176. package/dist/protocol/chat-session.js +28 -0
  177. package/dist/protocol/redact.d.ts +50 -0
  178. package/dist/protocol/redact.js +71 -0
  179. package/dist/protocol/server.d.ts +41 -2
  180. package/dist/protocol/server.js +231 -33
  181. package/dist/protocol/tcp-transport.d.ts +45 -0
  182. package/dist/protocol/tcp-transport.js +74 -0
  183. package/dist/protocol/types.d.ts +23 -0
  184. package/dist/protocol/types.js +6 -0
  185. package/dist/remote/bridge.d.ts +11 -2
  186. package/dist/remote/bridge.js +61 -33
  187. package/dist/run/ArtifactTracker.js +5 -4
  188. package/dist/run/EngineRunner.d.ts +27 -1
  189. package/dist/run/EngineRunner.js +46 -8
  190. package/dist/run/FileRunStore.js +32 -8
  191. package/dist/run/RunLock.d.ts +28 -2
  192. package/dist/run/RunLock.js +49 -7
  193. package/dist/run/RunManager.js +51 -3
  194. package/dist/run/RunQueue.d.ts +1 -0
  195. package/dist/run/RunQueue.js +8 -2
  196. package/dist/run/factory.d.ts +7 -0
  197. package/dist/run/factory.js +1 -0
  198. package/dist/run/index.d.ts +2 -2
  199. package/dist/run/index.js +1 -1
  200. package/dist/run/redirect-target.d.ts +7 -0
  201. package/dist/run/redirect-target.js +13 -0
  202. package/dist/run/types.js +1 -1
  203. package/dist/services/browser-open.d.ts +13 -0
  204. package/dist/services/browser-open.js +16 -0
  205. package/dist/services/dream-consolidation.d.ts +57 -0
  206. package/dist/services/dream-consolidation.js +151 -0
  207. package/dist/services/memory-orchestrator.js +18 -0
  208. package/dist/services/notifier.d.ts +8 -0
  209. package/dist/services/notifier.js +39 -25
  210. package/dist/services/oauth.js +5 -7
  211. package/dist/services/session-memory-sort.d.ts +8 -0
  212. package/dist/services/session-memory-sort.js +9 -0
  213. package/dist/services/session-memory.js +8 -9
  214. package/dist/session/file-history.js +10 -3
  215. package/dist/session/session-manager.d.ts +23 -1
  216. package/dist/session/session-manager.js +73 -2
  217. package/dist/settings/disk-defaults.d.ts +35 -0
  218. package/dist/settings/disk-defaults.js +24 -0
  219. package/dist/settings/manager.d.ts +33 -0
  220. package/dist/settings/manager.js +94 -1
  221. package/dist/settings/personalization.d.ts +19 -0
  222. package/dist/settings/personalization.js +7 -0
  223. package/dist/settings/schema.d.ts +1142 -126
  224. package/dist/settings/schema.js +119 -10
  225. package/dist/tool-system/builtin/add-marketplace.d.ts +12 -0
  226. package/dist/tool-system/builtin/add-marketplace.js +76 -0
  227. package/dist/tool-system/builtin/agent-registry.d.ts +11 -0
  228. package/dist/tool-system/builtin/agent-registry.js +8 -4
  229. package/dist/tool-system/builtin/agent.d.ts +29 -10
  230. package/dist/tool-system/builtin/agent.js +108 -52
  231. package/dist/tool-system/builtin/apply-patch/index.js +22 -3
  232. package/dist/tool-system/builtin/arena.js +6 -7
  233. package/dist/tool-system/builtin/complete-goal.d.ts +25 -0
  234. package/dist/tool-system/builtin/complete-goal.js +45 -0
  235. package/dist/tool-system/builtin/cron.js +53 -9
  236. package/dist/tool-system/builtin/edit.d.ts +2 -1
  237. package/dist/tool-system/builtin/edit.js +6 -1
  238. package/dist/tool-system/builtin/generate-image.d.ts +25 -0
  239. package/dist/tool-system/builtin/generate-image.js +146 -0
  240. package/dist/tool-system/builtin/glob.js +7 -0
  241. package/dist/tool-system/builtin/grep.js +6 -0
  242. package/dist/tool-system/builtin/index.d.ts +18 -1
  243. package/dist/tool-system/builtin/index.js +62 -1
  244. package/dist/tool-system/builtin/notebook-edit.d.ts +2 -1
  245. package/dist/tool-system/builtin/notebook-edit.js +8 -1
  246. package/dist/tool-system/builtin/read.d.ts +2 -1
  247. package/dist/tool-system/builtin/read.js +9 -1
  248. package/dist/tool-system/builtin/sleep.js +9 -3
  249. package/dist/tool-system/builtin/task.js +4 -1
  250. package/dist/tool-system/builtin/update-automation-memory.d.ts +24 -0
  251. package/dist/tool-system/builtin/update-automation-memory.js +60 -0
  252. package/dist/tool-system/builtin/view-image.d.ts +18 -0
  253. package/dist/tool-system/builtin/view-image.js +96 -0
  254. package/dist/tool-system/builtin/web-search.d.ts +4 -1
  255. package/dist/tool-system/builtin/web-search.js +36 -3
  256. package/dist/tool-system/builtin/write.d.ts +2 -1
  257. package/dist/tool-system/builtin/write.js +7 -1
  258. package/dist/tool-system/context.d.ts +12 -0
  259. package/dist/tool-system/executor.d.ts +0 -1
  260. package/dist/tool-system/executor.js +34 -40
  261. package/dist/tool-system/investigation-guard.d.ts +4 -1
  262. package/dist/tool-system/investigation-guard.js +19 -3
  263. package/dist/tool-system/mcp-manager.d.ts +36 -12
  264. package/dist/tool-system/mcp-manager.js +173 -10
  265. package/dist/tool-system/path-policy.d.ts +94 -0
  266. package/dist/tool-system/path-policy.js +279 -0
  267. package/dist/tool-system/permission.js +43 -50
  268. package/dist/tool-system/plan-mode-allowlist.d.ts +21 -0
  269. package/dist/tool-system/plan-mode-allowlist.js +43 -0
  270. package/dist/tool-system/registry.js +12 -1
  271. package/dist/tool-system/validation.d.ts +10 -0
  272. package/dist/tool-system/validation.js +16 -3
  273. package/dist/types.d.ts +90 -15
  274. package/dist/utils/format.js +4 -2
  275. package/dist/utils/lockfile.js +9 -2
  276. package/dist/utils/theme.d.ts +6 -0
  277. package/dist/utils/theme.js +4 -2
  278. package/package.json +2 -1
@@ -0,0 +1,546 @@
1
+ /**
2
+ * Cron scheduler — schedule and manage recurring agent tasks.
3
+ *
4
+ * Supports simple cron-like intervals. Each job runs the Engine
5
+ * with a predefined prompt on schedule.
6
+ */
7
+ import { isCronExpression, parseCronExpression, nextCronTime } from "./cron-expr.js";
8
+ export class CronScheduler {
9
+ jobs = new Map();
10
+ timers = new Map();
11
+ /** Job ids with an execution currently in flight — prevents a tick from
12
+ * starting a second run of a job whose previous run hasn't finished. */
13
+ running = new Set();
14
+ /** In-flight runs keyed by job id: the AbortController that cancels the run,
15
+ * plus `done` — a promise that resolves when the run has FULLY settled
16
+ * (executor returned, including any post-abort bookkeeping like Engine's
17
+ * final saveState). The re-entrancy guard (`running`) ensures at most one
18
+ * run per job, so one entry per job id is sufficient. `abort(jobId)` trips
19
+ * the controller and returns `done` so callers can wait for the run to
20
+ * actually stop before acting (e.g. deleting its session dir without racing
21
+ * a final write that would recreate it). */
22
+ runningControllers = new Map();
23
+ nextId = 1;
24
+ onExecute;
25
+ /** Optional persistence backend. When set, every create/delete/pause/resume
26
+ * writes the full job set to disk so jobs survive a restart. */
27
+ store;
28
+ /** When false, the scheduler persists + tracks jobs but NEVER arms timers
29
+ * (so it can't execute). Used by the desktop agent worker, a separate
30
+ * process from the main process that owns execution. Default true. */
31
+ executionEnabled = true;
32
+ constructor(store) {
33
+ this.store = store;
34
+ }
35
+ /**
36
+ * Disable (or re-enable) timer arming / execution. A host that only needs to
37
+ * persist + read jobs — never run them — calls setExecutionEnabled(false).
38
+ * Stops any timers already armed when turning off.
39
+ */
40
+ setExecutionEnabled(enabled) {
41
+ this.executionEnabled = enabled;
42
+ if (!enabled) {
43
+ for (const timer of this.timers.values())
44
+ clearTimeout(timer);
45
+ this.timers.clear();
46
+ }
47
+ }
48
+ /** Attach (or replace) the persistence backend. Used to give the shared
49
+ * singleton a store at runtime startup without changing its identity. */
50
+ setStore(store) {
51
+ this.store = store;
52
+ }
53
+ setExecutor(fn) {
54
+ this.onExecute = fn;
55
+ }
56
+ /**
57
+ * Abort the in-flight run of `jobId`, if any. Aborts the AbortSignal handed
58
+ * to the executor (which Engine.run cooperates with — it resolves with
59
+ * reason "aborted_streaming" rather than throwing). Returns false when the
60
+ * job has no run currently in flight (already settled, never started, or
61
+ * unknown id). The run's own finally block clears the controller, so a
62
+ * settled run is a no-op here.
63
+ */
64
+ async abort(jobId) {
65
+ const entry = this.runningControllers.get(jobId);
66
+ if (!entry)
67
+ return false;
68
+ entry.controller.abort();
69
+ // Wait for the run to fully settle (the executor's promise, incl. any
70
+ // post-abort write) so callers don't race its teardown.
71
+ await entry.done;
72
+ return true;
73
+ }
74
+ /**
75
+ * Restore persisted jobs and rebuild their timers. Call once at startup
76
+ * after `setStore`. nextRun is recomputed forward from now — we do NOT
77
+ * catch up on runs missed while the process was down (avoids a restart
78
+ * thundering-herd; aligns with Codex's stateless philosophy). Disabled
79
+ * jobs are restored without a timer.
80
+ */
81
+ /**
82
+ * Reconcile in-memory jobs against the on-disk store (the store is the source
83
+ * of truth). Safe to call repeatedly — used both at startup and as a periodic
84
+ * re-sync when another process (e.g. the desktop agent worker) may have
85
+ * created/deleted/changed jobs through the same store. Reconciliation:
86
+ * - jobs on disk but not in memory → added (and armed if enabled)
87
+ * - jobs in memory but not on disk → removed + timer cleared
88
+ * - jobs whose schedule/enabled differ → re-armed to match disk
89
+ *
90
+ * Pass `{ arm: false }` in a host that must NOT execute jobs (the worker —
91
+ * separate process from the one that owns execution); it still tracks +
92
+ * persists but starts no timers, so jobs never run twice across processes.
93
+ */
94
+ loadJobs(opts) {
95
+ if (!this.store)
96
+ return;
97
+ this.reconcileJobs(this.store.load(), opts);
98
+ }
99
+ reconcileJobs(persisted, opts) {
100
+ const arm = opts?.arm ?? true;
101
+ const onDisk = new Map(persisted.map((j) => [j.id, j]));
102
+ // 1. Drop in-memory jobs that no longer exist on disk (deleted elsewhere).
103
+ for (const id of [...this.jobs.keys()]) {
104
+ if (!onDisk.has(id)) {
105
+ this.clearTimer(id);
106
+ this.jobs.delete(id);
107
+ }
108
+ }
109
+ // 2. Add or update jobs from disk.
110
+ let maxId = 0;
111
+ for (const job of persisted) {
112
+ const prev = this.jobs.get(job.id);
113
+ this.jobs.set(job.id, job);
114
+ const n = parseInt(job.id, 10);
115
+ if (Number.isFinite(n) && n > maxId)
116
+ maxId = n;
117
+ // (Re)arm when execution is on. arm() is idempotent (clears any prior
118
+ // timer first), so calling it for unchanged jobs is harmless; it also
119
+ // refreshes nextRun for display.
120
+ if (arm && job.enabled) {
121
+ this.arm(job);
122
+ }
123
+ else {
124
+ // Disabled (or arm suppressed): ensure no stale timer survives a
125
+ // prior enabled state.
126
+ if (prev && this.timers.has(job.id))
127
+ this.clearTimer(job.id);
128
+ // Still refresh nextRun for display even when not arming.
129
+ if (!arm || !job.enabled)
130
+ this.refreshNextRunForDisplay(job);
131
+ }
132
+ }
133
+ this.nextId = Math.max(this.nextId, maxId + 1);
134
+ }
135
+ nextPersistedId(jobs) {
136
+ let maxId = 0;
137
+ for (const job of jobs) {
138
+ const n = parseInt(job.id, 10);
139
+ if (Number.isFinite(n) && n > maxId)
140
+ maxId = n;
141
+ }
142
+ return String(Math.max(this.nextId, maxId + 1));
143
+ }
144
+ /** Recompute nextRun without arming a timer (for display in disabled/no-arm hosts). */
145
+ refreshNextRunForDisplay(job) {
146
+ if (isCronExpression(job.schedule)) {
147
+ const next = nextCronTime(parseCronExpression(job.schedule), job.timezone ?? "UTC", Date.now());
148
+ job.nextRun = next ?? undefined;
149
+ }
150
+ else {
151
+ try {
152
+ job.nextRun = Date.now() + parseSchedule(job.schedule);
153
+ }
154
+ catch {
155
+ job.nextRun = undefined;
156
+ }
157
+ }
158
+ }
159
+ persist() {
160
+ if (!this.store)
161
+ return;
162
+ try {
163
+ this.store.save([...this.jobs.values()]);
164
+ }
165
+ catch {
166
+ // Persistence is best-effort: an unwritable disk should not break the
167
+ // in-memory scheduler. The store logs the failure.
168
+ }
169
+ }
170
+ persistRunStats(job) {
171
+ if (!this.store) {
172
+ this.persist();
173
+ return;
174
+ }
175
+ try {
176
+ this.store.mutate((jobs) => {
177
+ const idx = jobs.findIndex((j) => j.id === job.id);
178
+ if (idx === -1)
179
+ return { jobs, result: null };
180
+ const next = [...jobs];
181
+ next[idx] = {
182
+ ...next[idx],
183
+ lastRun: job.lastRun,
184
+ nextRun: job.nextRun,
185
+ runCount: job.runCount,
186
+ ...(job.lastRunId !== undefined ? { lastRunId: job.lastRunId } : {}),
187
+ };
188
+ return { jobs: next, result: null };
189
+ });
190
+ }
191
+ catch {
192
+ // Persistence is best-effort; missed UI metadata must not stop scheduling.
193
+ }
194
+ }
195
+ create(name, schedule, prompt, opts) {
196
+ // Validate the schedule up front (interval or cron expr) so a bad string
197
+ // surfaces at create time, not silently at the first missed tick.
198
+ validateSchedule(schedule, opts?.timezone);
199
+ if (this.store) {
200
+ const tx = this.store.mutate((jobs) => {
201
+ const id = this.nextPersistedId(jobs);
202
+ const job = {
203
+ id,
204
+ name,
205
+ schedule,
206
+ prompt,
207
+ enabled: true,
208
+ runCount: 0,
209
+ createdAt: Date.now(),
210
+ ...(opts?.cwd !== undefined ? { cwd: opts.cwd } : {}),
211
+ ...(opts?.timezone !== undefined ? { timezone: opts.timezone } : {}),
212
+ ...(opts?.permissionLevel !== undefined ? { permissionLevel: opts.permissionLevel } : {}),
213
+ };
214
+ this.refreshNextRunForDisplay(job);
215
+ return { jobs: [...jobs, job], result: job };
216
+ });
217
+ this.reconcileJobs(tx.jobs);
218
+ return this.jobs.get(tx.result.id) ?? tx.result;
219
+ }
220
+ const id = String(this.nextId++);
221
+ const job = {
222
+ id,
223
+ name,
224
+ schedule,
225
+ prompt,
226
+ enabled: true,
227
+ runCount: 0,
228
+ createdAt: Date.now(),
229
+ ...(opts?.cwd !== undefined ? { cwd: opts.cwd } : {}),
230
+ ...(opts?.timezone !== undefined ? { timezone: opts.timezone } : {}),
231
+ ...(opts?.permissionLevel !== undefined ? { permissionLevel: opts.permissionLevel } : {}),
232
+ };
233
+ this.jobs.set(id, job);
234
+ this.arm(job);
235
+ this.persist();
236
+ return job;
237
+ }
238
+ delete(id) {
239
+ if (this.store) {
240
+ const tx = this.store.mutate((jobs) => {
241
+ const next = jobs.filter((j) => j.id !== id);
242
+ return { jobs: next, result: next.length !== jobs.length };
243
+ });
244
+ this.reconcileJobs(tx.jobs);
245
+ return tx.result;
246
+ }
247
+ this.clearTimer(id);
248
+ const deleted = this.jobs.delete(id);
249
+ if (deleted)
250
+ this.persist();
251
+ return deleted;
252
+ }
253
+ list() {
254
+ return [...this.jobs.values()];
255
+ }
256
+ get(id) {
257
+ return this.jobs.get(id);
258
+ }
259
+ pause(id) {
260
+ if (this.store) {
261
+ const tx = this.store.mutate((jobs) => {
262
+ let changed = false;
263
+ const next = jobs.map((j) => {
264
+ if (j.id !== id)
265
+ return j;
266
+ changed = true;
267
+ return { ...j, enabled: false };
268
+ });
269
+ return { jobs: next, result: changed };
270
+ });
271
+ this.reconcileJobs(tx.jobs);
272
+ return tx.result;
273
+ }
274
+ const job = this.jobs.get(id);
275
+ if (!job)
276
+ return false;
277
+ job.enabled = false;
278
+ this.clearTimer(id);
279
+ this.persist();
280
+ return true;
281
+ }
282
+ resume(id) {
283
+ if (this.store) {
284
+ const tx = this.store.mutate((jobs) => {
285
+ let changed = false;
286
+ const next = jobs.map((j) => {
287
+ if (j.id !== id)
288
+ return j;
289
+ changed = true;
290
+ const job = { ...j, enabled: true };
291
+ this.refreshNextRunForDisplay(job);
292
+ return job;
293
+ });
294
+ return { jobs: next, result: changed };
295
+ });
296
+ this.reconcileJobs(tx.jobs);
297
+ return tx.result;
298
+ }
299
+ const job = this.jobs.get(id);
300
+ if (!job)
301
+ return false;
302
+ job.enabled = true;
303
+ this.arm(job);
304
+ this.persist();
305
+ return true;
306
+ }
307
+ /**
308
+ * Edit an existing job's fields (name/prompt/schedule/timezone/cwd/
309
+ * permissionLevel) without recreating it. A changed schedule is validated up
310
+ * front (throws on invalid, leaving the job untouched), then the timer is
311
+ * re-armed so the new schedule takes effect immediately. Enabled state is
312
+ * preserved — a paused job stays paused (no timer). Returns the updated job,
313
+ * or null if the id is unknown.
314
+ */
315
+ update(id, patch) {
316
+ if (this.store) {
317
+ const tx = this.store.mutate((jobs) => {
318
+ let updated = null;
319
+ const next = jobs.map((j) => {
320
+ if (j.id !== id)
321
+ return j;
322
+ const job = { ...j };
323
+ const nextSchedule = patch.schedule ?? job.schedule;
324
+ const nextTimezone = patch.timezone ?? job.timezone;
325
+ if (patch.schedule !== undefined || patch.timezone !== undefined) {
326
+ validateSchedule(nextSchedule, nextTimezone);
327
+ }
328
+ if (patch.name !== undefined)
329
+ job.name = patch.name;
330
+ if (patch.prompt !== undefined)
331
+ job.prompt = patch.prompt;
332
+ if (patch.schedule !== undefined)
333
+ job.schedule = patch.schedule;
334
+ if (patch.timezone !== undefined)
335
+ job.timezone = patch.timezone;
336
+ if (patch.cwd !== undefined)
337
+ job.cwd = patch.cwd;
338
+ if (patch.permissionLevel !== undefined)
339
+ job.permissionLevel = patch.permissionLevel;
340
+ this.refreshNextRunForDisplay(job);
341
+ updated = job;
342
+ return job;
343
+ });
344
+ return { jobs: next, result: updated };
345
+ });
346
+ this.reconcileJobs(tx.jobs);
347
+ return tx.result ? this.jobs.get(id) ?? tx.result : null;
348
+ }
349
+ const job = this.jobs.get(id);
350
+ if (!job)
351
+ return null;
352
+ // Validate a new schedule/timezone BEFORE mutating anything.
353
+ const nextSchedule = patch.schedule ?? job.schedule;
354
+ const nextTimezone = patch.timezone ?? job.timezone;
355
+ if (patch.schedule !== undefined || patch.timezone !== undefined) {
356
+ validateSchedule(nextSchedule, nextTimezone);
357
+ }
358
+ if (patch.name !== undefined)
359
+ job.name = patch.name;
360
+ if (patch.prompt !== undefined)
361
+ job.prompt = patch.prompt;
362
+ if (patch.schedule !== undefined)
363
+ job.schedule = patch.schedule;
364
+ if (patch.timezone !== undefined)
365
+ job.timezone = patch.timezone;
366
+ if (patch.cwd !== undefined)
367
+ job.cwd = patch.cwd;
368
+ if (patch.permissionLevel !== undefined)
369
+ job.permissionLevel = patch.permissionLevel;
370
+ // Re-arm so a schedule/timezone change drives execution now. arm() is
371
+ // idempotent and respects enabled + executionEnabled (paused → no timer,
372
+ // but nextRun still refreshed for display).
373
+ if (job.enabled) {
374
+ this.arm(job);
375
+ }
376
+ else {
377
+ this.clearTimer(id);
378
+ this.refreshNextRunForDisplay(job);
379
+ }
380
+ this.persist();
381
+ return job;
382
+ }
383
+ stopAll() {
384
+ for (const timer of this.timers.values()) {
385
+ clearTimeout(timer);
386
+ }
387
+ this.timers.clear();
388
+ }
389
+ /**
390
+ * Fire a job immediately, out of band of its schedule (the "Run now" button).
391
+ * Respects the re-entrancy guard (no-op if a run is already in flight) and
392
+ * does not disturb the existing timer. Returns false if the id is unknown.
393
+ */
394
+ runNow(id) {
395
+ const job = this.jobs.get(id);
396
+ if (!job)
397
+ return false;
398
+ // Run-stat bookkeeping is shared with scheduled fires; nextRun is left as-is
399
+ // (a manual run shouldn't shift the next scheduled occurrence). force=true
400
+ // so a paused job can still be run on demand.
401
+ void this.fire(job, () => { }, true);
402
+ return true;
403
+ }
404
+ /**
405
+ * Arm a job's timer. Interval schedules ("5m") use setInterval; cron-
406
+ * expression schedules ("0 9 * * 1-5") compute the next trigger in the
407
+ * job's timezone and use setTimeout, re-arming after each fire. nextRun is
408
+ * updated to reflect the scheduled instant.
409
+ */
410
+ arm(job) {
411
+ this.clearTimer(job.id);
412
+ if (isCronExpression(job.schedule)) {
413
+ // Compute nextRun for display even when execution is disabled.
414
+ const cron = parseCronExpression(job.schedule);
415
+ const next = nextCronTime(cron, job.timezone ?? "UTC", Date.now());
416
+ job.nextRun = next ?? undefined;
417
+ if (!this.executionEnabled)
418
+ return;
419
+ this.armCron(job);
420
+ }
421
+ else {
422
+ const intervalMs = parseSchedule(job.schedule);
423
+ job.nextRun = Date.now() + intervalMs;
424
+ if (!this.executionEnabled)
425
+ return;
426
+ const timer = setInterval(() => void this.fire(job, () => this.refreshIntervalNextRun(job, intervalMs)), intervalMs);
427
+ this.timers.set(job.id, timer);
428
+ }
429
+ }
430
+ refreshIntervalNextRun(job, intervalMs) {
431
+ job.nextRun = Date.now() + intervalMs;
432
+ }
433
+ armCron(job) {
434
+ const cron = parseCronExpression(job.schedule);
435
+ const tz = job.timezone ?? "UTC";
436
+ const next = nextCronTime(cron, tz, Date.now());
437
+ if (next === null) {
438
+ // Unsatisfiable within the search window — leave unscheduled.
439
+ job.nextRun = undefined;
440
+ return;
441
+ }
442
+ job.nextRun = next;
443
+ const delay = Math.max(0, next - Date.now());
444
+ const timer = setTimeout(() => {
445
+ void this.fire(job, () => {
446
+ // Re-arm for the following occurrence.
447
+ if (job.enabled)
448
+ this.armCron(job);
449
+ });
450
+ }, delay);
451
+ this.timers.set(job.id, timer);
452
+ }
453
+ clearTimer(id) {
454
+ const t = this.timers.get(id);
455
+ if (t) {
456
+ clearTimeout(t);
457
+ this.timers.delete(id);
458
+ }
459
+ }
460
+ /**
461
+ * Shared fire path for both schedule kinds. Re-entrancy guard, run-stat
462
+ * bookkeeping, persistence, then the executor. `afterStats` updates nextRun
463
+ * for the next occurrence (interval: now+interval; cron: re-armed inside).
464
+ */
465
+ async fire(job, afterStats, force = false) {
466
+ if (!job.enabled && !force)
467
+ return;
468
+ // Re-entrancy guard: if this job's previous run is still in flight
469
+ // (onExecute slower than the interval), skip rather than stacking
470
+ // overlapping executions that double-count runCount.
471
+ if (this.running.has(job.id))
472
+ return;
473
+ this.running.add(job.id);
474
+ // One in-flight entry per job. abort(jobId) trips `controller` (Engine.run
475
+ // cooperates and resolves early) and awaits `done`, which we resolve in the
476
+ // finally below once the run — including any post-abort write — has settled.
477
+ const controller = new AbortController();
478
+ let resolveDone;
479
+ const done = new Promise((r) => {
480
+ resolveDone = r;
481
+ });
482
+ this.runningControllers.set(job.id, { controller, done });
483
+ job.lastRun = Date.now();
484
+ job.runCount++;
485
+ afterStats();
486
+ // Persist updated run stats so runCount/lastRun/nextRun survive a restart.
487
+ this.persistRunStats(job);
488
+ try {
489
+ await this.onExecute?.(job, controller.signal);
490
+ }
491
+ catch {
492
+ // Job execution failed — continue scheduling.
493
+ }
494
+ finally {
495
+ // The executor may have recorded lastRunId on the job. Persist only run
496
+ // metadata so an old in-flight job cannot overwrite an edited prompt or
497
+ // schedule with its stale copy.
498
+ this.persistRunStats(job);
499
+ this.running.delete(job.id);
500
+ this.runningControllers.delete(job.id);
501
+ // Unblock any abort() awaiting this run's teardown.
502
+ resolveDone();
503
+ }
504
+ }
505
+ }
506
+ /** Validate a schedule string (interval or cron expr) without scheduling. Throws on invalid. */
507
+ function validateSchedule(schedule, timezone) {
508
+ if (isCronExpression(schedule)) {
509
+ parseCronExpression(schedule); // throws on bad fields
510
+ // Validate timezone is acceptable to Intl (throws RangeError if not).
511
+ if (timezone !== undefined) {
512
+ new Intl.DateTimeFormat("en-US", { timeZone: timezone });
513
+ }
514
+ return;
515
+ }
516
+ parseSchedule(schedule); // throws on bad interval
517
+ }
518
+ /**
519
+ * Parse a schedule string into milliseconds.
520
+ * Supports: "30s", "5m", "1h", "1d", or raw (all-digit) milliseconds.
521
+ *
522
+ * Throws on anything else rather than silently falling back to a default —
523
+ * a typo like "5mn" should surface as an error, not quietly schedule every
524
+ * 10 minutes (review-2026-05-30).
525
+ */
526
+ function parseSchedule(schedule) {
527
+ const match = schedule.match(/^(\d+)(s|m|h|d)$/);
528
+ if (match) {
529
+ const value = parseInt(match[1], 10);
530
+ switch (match[2]) {
531
+ case "s": return value * 1000;
532
+ case "m": return value * 60 * 1000;
533
+ case "h": return value * 60 * 60 * 1000;
534
+ case "d": return value * 24 * 60 * 60 * 1000;
535
+ }
536
+ }
537
+ // Raw milliseconds — must be all digits and > 0 (parseInt would otherwise
538
+ // accept "1500abc").
539
+ if (/^\d+$/.test(schedule)) {
540
+ const ms = parseInt(schedule, 10);
541
+ if (ms > 0)
542
+ return ms;
543
+ }
544
+ throw new Error(`Invalid schedule: ${JSON.stringify(schedule)}. Use "30s"/"5m"/"1h"/"1d" or a positive number of milliseconds.`);
545
+ }
546
+ export const cronScheduler = new CronScheduler();
@@ -0,0 +1,39 @@
1
+ /**
2
+ * CronStore — persistence for cron jobs.
3
+ *
4
+ * Writes a single-file JSON snapshot of all jobs to `~/.code-shell/cron.json`
5
+ * (global by default), so scheduled tasks survive a process restart. Uses the
6
+ * same atomic tmp+rename write as FileRunStore so a crash mid-write can't
7
+ * truncate the file and lose every job.
8
+ *
9
+ * Cross-process writes are serialized with a directory lock. Hosts that need
10
+ * read-modify-write behavior should use mutate() so load + save happen under
11
+ * the same lock and one process cannot overwrite another process's new job
12
+ * with a stale in-memory snapshot.
13
+ */
14
+ import type { CronJob } from "./scheduler.js";
15
+ /** Default global location. Mirrors FileRunStore's `~/.code-shell/...` layout. */
16
+ export declare function defaultCronStorePath(): string;
17
+ export declare class CronStore {
18
+ private readonly file;
19
+ constructor(file?: string);
20
+ /** Load all persisted jobs. Returns [] when absent or unreadable. */
21
+ load(): CronJob[];
22
+ /**
23
+ * Atomically load, mutate, and save jobs under the store lock. This is the
24
+ * safe path for create/update/delete/pause/resume across the desktop main
25
+ * process and the agent worker process.
26
+ */
27
+ mutate<T>(fn: (jobs: CronJob[]) => {
28
+ jobs: CronJob[];
29
+ result: T;
30
+ }): {
31
+ jobs: CronJob[];
32
+ result: T;
33
+ };
34
+ /** Persist the full job set. Atomic: stage to .tmp, then rename. */
35
+ save(jobs: CronJob[]): void;
36
+ private loadUnlocked;
37
+ private saveUnlocked;
38
+ private acquireStoreLock;
39
+ }