aiden-runtime 4.0.2 → 4.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (108) hide show
  1. package/README.md +11 -7
  2. package/config/hardware.json +2 -2
  3. package/dist/api/server.js +50 -52
  4. package/dist/cli/v4/aidenCLI.js +421 -5
  5. package/dist/cli/v4/aidenPrompt.js +317 -0
  6. package/dist/cli/v4/box.js +105 -39
  7. package/dist/cli/v4/callbacks.js +39 -6
  8. package/dist/cli/v4/chatSession.js +256 -55
  9. package/dist/cli/v4/citationFooter.js +97 -0
  10. package/dist/cli/v4/commands/channel.js +656 -0
  11. package/dist/cli/v4/commands/clear.js +1 -1
  12. package/dist/cli/v4/commands/compress.js +1 -1
  13. package/dist/cli/v4/commands/cron.js +44 -16
  14. package/dist/cli/v4/commands/fanout.js +236 -0
  15. package/dist/cli/v4/commands/help.js +15 -4
  16. package/dist/cli/v4/commands/history.js +84 -0
  17. package/dist/cli/v4/commands/index.js +16 -1
  18. package/dist/cli/v4/commands/mcp.js +358 -0
  19. package/dist/cli/v4/commands/show.js +43 -0
  20. package/dist/cli/v4/commands/skills.js +169 -4
  21. package/dist/cli/v4/commands/status.js +84 -0
  22. package/dist/cli/v4/commands/subagent.js +78 -0
  23. package/dist/cli/v4/commands/verbose.js +1 -1
  24. package/dist/cli/v4/commands/voice.js +218 -0
  25. package/dist/cli/v4/cronCli.js +103 -0
  26. package/dist/cli/v4/display.js +297 -13
  27. package/dist/cli/v4/doctor.js +41 -0
  28. package/dist/cli/v4/envSources.js +105 -0
  29. package/dist/cli/v4/ghostMatch.js +74 -0
  30. package/dist/cli/v4/historyStore.js +163 -0
  31. package/dist/cli/v4/pasteCompression.js +124 -0
  32. package/dist/cli/v4/pasteIntercept.js +203 -0
  33. package/dist/cli/v4/replyRenderer.js +209 -0
  34. package/dist/cli/v4/resizeGuard.js +92 -0
  35. package/dist/cli/v4/shellInterpolation.js +139 -0
  36. package/dist/cli/v4/skinEngine.js +21 -1
  37. package/dist/cli/v4/streamingPrefix.js +121 -0
  38. package/dist/cli/v4/syntaxHighlight.js +345 -0
  39. package/dist/cli/v4/table.js +216 -0
  40. package/dist/cli/v4/themeDetect.js +81 -0
  41. package/dist/cli/v4/uiBuild.js +74 -0
  42. package/dist/cli/v4/voiceCli.js +113 -0
  43. package/dist/cli/v4/voicePromptApi.js +196 -0
  44. package/dist/core/channels/discord.js +16 -10
  45. package/dist/core/channels/email.js +13 -9
  46. package/dist/core/channels/imessage.js +13 -9
  47. package/dist/core/channels/manager.js +25 -7
  48. package/dist/core/channels/pdf-extract.js +180 -0
  49. package/dist/core/channels/photo-vision.js +157 -0
  50. package/dist/core/channels/signal.js +11 -7
  51. package/dist/core/channels/slack.js +13 -10
  52. package/dist/core/channels/telegram-commands.js +154 -0
  53. package/dist/core/channels/telegram-groups.js +198 -0
  54. package/dist/core/channels/telegram-rate-limit.js +124 -0
  55. package/dist/core/channels/telegram.js +1980 -0
  56. package/dist/core/channels/twilio.js +11 -7
  57. package/dist/core/channels/webhook.js +9 -5
  58. package/dist/core/channels/whatsapp.js +15 -11
  59. package/dist/core/channels/whisper-transcribe.js +163 -0
  60. package/dist/core/cronManager.js +33 -294
  61. package/dist/core/gateway.js +29 -8
  62. package/dist/core/playwrightBridge.js +90 -0
  63. package/dist/core/v4/aidenAgent.js +35 -0
  64. package/dist/core/v4/auxiliaryClient.js +2 -2
  65. package/dist/core/v4/cron/atomicWrite.js +18 -4
  66. package/dist/core/v4/cron/cronExecute.js +300 -0
  67. package/dist/core/v4/cron/cronManager.js +502 -0
  68. package/dist/core/v4/cron/cronState.js +314 -0
  69. package/dist/core/v4/cron/cronTick.js +90 -0
  70. package/dist/core/v4/cron/diagnostics.js +104 -0
  71. package/dist/core/v4/cron/graceWindow.js +79 -0
  72. package/dist/core/v4/logger/factory.js +110 -0
  73. package/dist/core/v4/logger/index.js +22 -0
  74. package/dist/core/v4/logger/logger.js +101 -0
  75. package/dist/core/v4/logger/sinks/fileSink.js +110 -0
  76. package/dist/core/v4/logger/sinks/multiSink.js +43 -0
  77. package/dist/core/v4/logger/sinks/nullSink.js +53 -0
  78. package/dist/core/v4/logger/sinks/stdSink.js +81 -0
  79. package/dist/core/v4/mcp/server/diagnostics.js +40 -0
  80. package/dist/core/v4/mcp/server/skillBridge.js +94 -0
  81. package/dist/core/v4/mcp/server/stdioServer.js +119 -0
  82. package/dist/core/v4/mcp/server/toolBridge.js +168 -0
  83. package/dist/core/v4/platformPaths.js +105 -0
  84. package/dist/core/v4/providerFallback.js +25 -0
  85. package/dist/core/v4/skillLoader.js +21 -5
  86. package/dist/core/v4/skillMining/candidateStore.js +164 -0
  87. package/dist/core/v4/skillMining/extractorPrompt.js +111 -0
  88. package/dist/core/v4/skillMining/proposalBuilder.js +139 -0
  89. package/dist/core/v4/skillMining/skillMiner.js +191 -0
  90. package/dist/core/v4/skillMining/traceFingerprint.js +51 -0
  91. package/dist/core/v4/subagent/budget.js +76 -0
  92. package/dist/core/v4/subagent/diagnostics.js +22 -0
  93. package/dist/core/v4/subagent/fanout.js +216 -0
  94. package/dist/core/v4/subagent/merger.js +148 -0
  95. package/dist/core/v4/subagent/providerRotation.js +54 -0
  96. package/dist/core/v4/voice/audioStream.js +373 -0
  97. package/dist/core/v4/voice/cliVoice.js +393 -0
  98. package/dist/core/v4/voice/diagnostics.js +66 -0
  99. package/dist/core/v4/voice/ttsStream.js +193 -0
  100. package/dist/core/version.js +1 -1
  101. package/dist/core/visionAnalyze.js +291 -90
  102. package/dist/core/voice/audio.js +61 -5
  103. package/dist/core/voice/audioBackend.js +134 -0
  104. package/dist/core/voice/stt.js +61 -6
  105. package/dist/core/voice/tts.js +19 -3
  106. package/dist/tools/v4/index.js +32 -1
  107. package/dist/tools/v4/subagent/subagentFanout.js +166 -0
  108. package/package.json +11 -2
@@ -0,0 +1,502 @@
1
+ "use strict";
2
+ /**
3
+ * Copyright (c) 2026 Shiva Deore (Taracod).
4
+ * Licensed under AGPL-3.0. See LICENSE for details.
5
+ *
6
+ * Aiden — local-first agent.
7
+ */
8
+ /**
9
+ * core/v4/cron/cronManager.ts — Phase v4.1-cron
10
+ *
11
+ * Public scheduler API. Replaces the legacy `core/cronManager.ts`
12
+ * with the same exported function names so existing callers
13
+ * (cli/v4/commands/cron.ts) keep working.
14
+ *
15
+ * Architecture: state lives on disk (cron_jobs.json), in-memory
16
+ * cache + timers are this module's singleton state, refreshed
17
+ * by the heartbeat. All API calls acquire the file lock before
18
+ * mutating state — multi-process safety.
19
+ *
20
+ * Public surface (preserved from legacy):
21
+ * - createJob(description, schedule, action) → CronJob
22
+ * - listJobs() → CronJob[]
23
+ * - getJob(id) → CronJob | undefined
24
+ * - pauseJob(id, reason?) → boolean
25
+ * - resumeJob(id) → boolean
26
+ * - deleteJob(id) → boolean
27
+ * - triggerJob(id) → Promise<boolean>
28
+ * - parseSchedule(input) → ScheduleSpec (re-exported)
29
+ * - loadJobs() → void (idempotent boot)
30
+ * - awaitPendingSaves() → Promise<void> (test/shutdown)
31
+ * - __resetForTests() → void
32
+ *
33
+ * New surface (additive):
34
+ * - getDiagnostics() → CronDiagnostics
35
+ * - startHeartbeat() / stopHeartbeat()
36
+ */
37
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
38
+ if (k2 === undefined) k2 = k;
39
+ var desc = Object.getOwnPropertyDescriptor(m, k);
40
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
41
+ desc = { enumerable: true, get: function() { return m[k]; } };
42
+ }
43
+ Object.defineProperty(o, k2, desc);
44
+ }) : (function(o, m, k, k2) {
45
+ if (k2 === undefined) k2 = k;
46
+ o[k2] = m[k];
47
+ }));
48
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
49
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
50
+ }) : function(o, v) {
51
+ o["default"] = v;
52
+ });
53
+ var __importStar = (this && this.__importStar) || (function () {
54
+ var ownKeys = function(o) {
55
+ ownKeys = Object.getOwnPropertyNames || function (o) {
56
+ var ar = [];
57
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
58
+ return ar;
59
+ };
60
+ return ownKeys(o);
61
+ };
62
+ return function (mod) {
63
+ if (mod && mod.__esModule) return mod;
64
+ var result = {};
65
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
66
+ __setModuleDefault(result, mod);
67
+ return result;
68
+ };
69
+ })();
70
+ var __importDefault = (this && this.__importDefault) || function (mod) {
71
+ return (mod && mod.__esModule) ? mod : { "default": mod };
72
+ };
73
+ Object.defineProperty(exports, "__esModule", { value: true });
74
+ exports.isHeartbeatActive = exports.stopHeartbeat = exports.AIDEN_CRON_BUILD = exports.parseSchedule = void 0;
75
+ exports.setCronPathsForTests = setCronPathsForTests;
76
+ exports.setRunActionForTests = setRunActionForTests;
77
+ exports.loadJobs = loadJobs;
78
+ exports.listJobs = listJobs;
79
+ exports.listJobsAsync = listJobsAsync;
80
+ exports.getJob = getJob;
81
+ exports.getJobAsync = getJobAsync;
82
+ exports.createJob = createJob;
83
+ exports.createJobAsync = createJobAsync;
84
+ exports.pauseJob = pauseJob;
85
+ exports.resumeJob = resumeJob;
86
+ exports.deleteJob = deleteJob;
87
+ exports.triggerJob = triggerJob;
88
+ exports.getDiagnostics = getDiagnostics;
89
+ exports.getStateSnapshot = getStateSnapshot;
90
+ exports.startHeartbeat = startHeartbeat;
91
+ exports.awaitPendingSaves = awaitPendingSaves;
92
+ exports.__resetForTests = __resetForTests;
93
+ exports.__testPaths = __testPaths;
94
+ const node_path_1 = __importDefault(require("node:path"));
95
+ const node_os_1 = __importDefault(require("node:os"));
96
+ const scheduleParser_1 = require("./scheduleParser");
97
+ const cronState_1 = require("./cronState");
98
+ const cronExecute_1 = require("./cronExecute");
99
+ const cronTick_1 = require("./cronTick");
100
+ Object.defineProperty(exports, "stopHeartbeat", { enumerable: true, get: function () { return cronTick_1.stopHeartbeat; } });
101
+ Object.defineProperty(exports, "isHeartbeatActive", { enumerable: true, get: function () { return cronTick_1.isHeartbeatActive; } });
102
+ const diagnostics_1 = require("./diagnostics");
103
+ // ── Re-exports for backwards compat ──────────────────────────────────────
104
+ var scheduleParser_2 = require("./scheduleParser");
105
+ Object.defineProperty(exports, "parseSchedule", { enumerable: true, get: function () { return scheduleParser_2.parseSchedule; } });
106
+ var diagnostics_2 = require("./diagnostics");
107
+ Object.defineProperty(exports, "AIDEN_CRON_BUILD", { enumerable: true, get: function () { return diagnostics_2.AIDEN_CRON_BUILD; } });
108
+ // ── State (in-memory cache + per-job timers) ─────────────────────────────
109
+ const _timers = new Map();
110
+ let _paths = (0, cronState_1.defaultCronPaths)();
111
+ let _runAction = cronExecute_1.defaultRunAction;
112
+ let _bootedPid = null;
113
+ /** Override paths — for tests. */
114
+ function setCronPathsForTests(paths) {
115
+ _paths = paths;
116
+ }
117
+ /** Override the action runner — for tests. */
118
+ function setRunActionForTests(fn) {
119
+ _runAction = fn;
120
+ }
121
+ // ── Helpers ──────────────────────────────────────────────────────────────
122
+ function clearTimer(id) {
123
+ const h = _timers.get(id);
124
+ if (h) {
125
+ clearTimeout(h);
126
+ _timers.delete(id);
127
+ }
128
+ }
129
+ async function withLock(fn) {
130
+ const lock = await (0, cronState_1.acquireCronLock)(_paths, { failFast: false });
131
+ if (!lock) {
132
+ throw new Error('cron lock held by another process — try again');
133
+ }
134
+ try {
135
+ const state = await (0, cronState_1.readCronState)(_paths.stateFile);
136
+ return await fn(state);
137
+ }
138
+ finally {
139
+ await lock.release();
140
+ }
141
+ }
142
+ /** Re-arm a per-job setTimeout based on its current `nextRun`.
143
+ * Cancels any existing timer first. */
144
+ async function armJobTimer(job) {
145
+ clearTimer(job.id);
146
+ if (!job.enabled || job.state === 'paused' || job.state === 'completed')
147
+ return;
148
+ const { next } = await (0, cronExecute_1.computeNextFire)(job);
149
+ if (next === null)
150
+ return;
151
+ const delay = Math.max(0, next - Date.now());
152
+ const handle = setTimeout(() => {
153
+ _timers.delete(job.id);
154
+ void (0, cronExecute_1.fireJob)({
155
+ paths: _paths,
156
+ jobId: job.id,
157
+ runAction: _runAction,
158
+ }).then(async () => {
159
+ // After fire, re-arm based on the freshly persisted state.
160
+ const refreshed = await (0, cronState_1.readCronState)(_paths.stateFile);
161
+ const fresh = refreshed.jobs.find((j) => j.id === job.id);
162
+ if (fresh)
163
+ await armJobTimer(fresh);
164
+ }).catch(() => undefined);
165
+ }, delay);
166
+ if (typeof handle.unref === 'function')
167
+ handle.unref();
168
+ _timers.set(job.id, handle);
169
+ }
170
+ function genId(state) {
171
+ let max = 0;
172
+ for (const j of state.jobs) {
173
+ const n = Number.parseInt(j.id, 10);
174
+ if (Number.isFinite(n) && n > max)
175
+ max = n;
176
+ }
177
+ return String(max + 1);
178
+ }
179
+ // ── In-memory cache (sync read fallback for legacy callers) ─────────────
180
+ /** Cache of last-read jobs. Refreshed by every async-public-API call
181
+ * + the heartbeat. Sync wrappers below read from this — accepting
182
+ * brief staleness in exchange for source-compat with v3 callers. */
183
+ let _cache = [];
184
+ function refreshCacheFromState(state) {
185
+ _cache = state.jobs;
186
+ }
187
+ // ── Public API ───────────────────────────────────────────────────────────
188
+ /** Idempotent boot — call once at runtime startup. Loads state +
189
+ * arms timers for every enabled job. Safe to call multiple times
190
+ * (re-arms cleanly). */
191
+ async function loadJobs() {
192
+ if (_bootedPid === process.pid)
193
+ return;
194
+ _bootedPid = process.pid;
195
+ const lock = await (0, cronState_1.acquireCronLock)(_paths, { failFast: false });
196
+ let state;
197
+ if (lock) {
198
+ try {
199
+ state = await (0, cronState_1.readCronState)(_paths.stateFile);
200
+ }
201
+ finally {
202
+ await lock.release();
203
+ }
204
+ }
205
+ else {
206
+ // Lock held — best-effort read without lock. Persisters use
207
+ // atomicWrite so the read always sees a consistent file.
208
+ state = await (0, cronState_1.readCronState)(_paths.stateFile);
209
+ }
210
+ refreshCacheFromState(state);
211
+ for (const job of state.jobs) {
212
+ void armJobTimer(job);
213
+ }
214
+ }
215
+ /** SYNC list — reads from the in-memory cache populated by
216
+ * loadJobs / heartbeat / API mutations. Backward-compat for
217
+ * legacy callers (cli/v4/commands/cron.ts, core/toolRegistry.ts).
218
+ * For up-to-the-millisecond accuracy use `listJobsAsync()`. */
219
+ function listJobs() {
220
+ return [..._cache];
221
+ }
222
+ /** Async list — re-reads under lock. Preferred for new code. */
223
+ async function listJobsAsync() {
224
+ const lock = await (0, cronState_1.acquireCronLock)(_paths, { failFast: false });
225
+ let state;
226
+ if (lock) {
227
+ try {
228
+ state = await (0, cronState_1.readCronState)(_paths.stateFile);
229
+ }
230
+ finally {
231
+ await lock.release();
232
+ }
233
+ }
234
+ else {
235
+ state = await (0, cronState_1.readCronState)(_paths.stateFile);
236
+ }
237
+ refreshCacheFromState(state);
238
+ return state.jobs;
239
+ }
240
+ /** SYNC accessor — uses cache. */
241
+ function getJob(id) {
242
+ return _cache.find((j) => j.id === id);
243
+ }
244
+ async function getJobAsync(id) {
245
+ const jobs = await listJobsAsync();
246
+ return jobs.find((j) => j.id === id);
247
+ }
248
+ /** SYNC create — returns the job object immediately. Persistence
249
+ * happens in the background via `withLock`; legacy callers that
250
+ * expected sync semantics keep working. The cache reflects the
251
+ * new job before this returns. */
252
+ function createJob(description, schedule, action) {
253
+ const spec = (0, scheduleParser_1.parseSchedule)(schedule);
254
+ // Compute id from in-memory cache.
255
+ let max = 0;
256
+ for (const j of _cache) {
257
+ const n = Number.parseInt(j.id, 10);
258
+ if (Number.isFinite(n) && n > max)
259
+ max = n;
260
+ }
261
+ const id = String(max + 1);
262
+ const createdAt = new Date().toISOString();
263
+ const job = {
264
+ id,
265
+ description,
266
+ schedule: spec.display,
267
+ kind: spec.kind,
268
+ action,
269
+ enabled: true,
270
+ state: 'scheduled',
271
+ pausedAt: null,
272
+ pausedReason: null,
273
+ createdAt,
274
+ lastError: null,
275
+ lastDeliveryError: null,
276
+ runCount: 0,
277
+ ...attachKindFields(spec),
278
+ };
279
+ // Update cache immediately — getJob right after createJob sees it.
280
+ _cache = [..._cache, job];
281
+ // Persist + arm timer in background.
282
+ void (async () => {
283
+ try {
284
+ await withLock(async (state) => {
285
+ // Re-genId in case another process added a job between now
286
+ // and the lock acquisition.
287
+ const idx = state.jobs.findIndex((j) => j.id === job.id);
288
+ if (idx === -1)
289
+ state.jobs.push(job);
290
+ const { next } = await (0, cronExecute_1.computeNextFire)(job);
291
+ if (next !== null)
292
+ job.nextRun = new Date(next).toISOString();
293
+ await (0, cronState_1.writeCronState)(_paths.stateFile, state);
294
+ });
295
+ void armJobTimer(job);
296
+ }
297
+ catch { /* persistence error — surface via logger only */ }
298
+ })();
299
+ return job;
300
+ }
301
+ /** Async variant — awaitable. */
302
+ async function createJobAsync(description, schedule, action) {
303
+ const spec = (0, scheduleParser_1.parseSchedule)(schedule);
304
+ return withLock(async (state) => {
305
+ const id = genId(state);
306
+ const createdAt = new Date().toISOString();
307
+ const job = {
308
+ id,
309
+ description,
310
+ schedule: spec.display,
311
+ kind: spec.kind,
312
+ action,
313
+ enabled: true,
314
+ state: 'scheduled',
315
+ pausedAt: null,
316
+ pausedReason: null,
317
+ createdAt,
318
+ lastError: null,
319
+ lastDeliveryError: null,
320
+ runCount: 0,
321
+ ...attachKindFields(spec),
322
+ };
323
+ const { next } = await (0, cronExecute_1.computeNextFire)(job);
324
+ if (next !== null)
325
+ job.nextRun = new Date(next).toISOString();
326
+ state.jobs.push(job);
327
+ refreshCacheFromState(state);
328
+ await (0, cronState_1.writeCronState)(_paths.stateFile, state);
329
+ void armJobTimer(job);
330
+ return job;
331
+ });
332
+ }
333
+ function attachKindFields(spec) {
334
+ if (spec.kind === 'interval')
335
+ return { intervalMs: spec.intervalMs };
336
+ if (spec.kind === 'cron')
337
+ return { cronExpr: spec.cronExpr };
338
+ return { oneshotIso: spec.runAtIso };
339
+ }
340
+ /** SYNC pause — updates cache immediately, persists in background.
341
+ * `reason` is the new optional second arg added by v4.1-cron
342
+ * (legacy callers passing one arg still work). */
343
+ function pauseJob(id, reason) {
344
+ const idx = _cache.findIndex((j) => j.id === id);
345
+ if (idx === -1)
346
+ return false;
347
+ const job = { ..._cache[idx] };
348
+ job.enabled = false;
349
+ job.state = 'paused';
350
+ job.pausedAt = new Date().toISOString();
351
+ job.pausedReason = reason ?? null;
352
+ _cache = [..._cache];
353
+ _cache[idx] = job;
354
+ clearTimer(id);
355
+ void (async () => {
356
+ try {
357
+ await withLock(async (state) => {
358
+ const sIdx = state.jobs.findIndex((j) => j.id === id);
359
+ if (sIdx === -1)
360
+ return;
361
+ state.jobs[sIdx] = job;
362
+ await (0, cronState_1.writeCronState)(_paths.stateFile, state);
363
+ });
364
+ }
365
+ catch { /* surfaced via logger only */ }
366
+ })();
367
+ return true;
368
+ }
369
+ /** SYNC resume — recomputes nextRun from now. Hard-learned: don't
370
+ * carry forward stale next_run after a long pause. */
371
+ function resumeJob(id) {
372
+ const idx = _cache.findIndex((j) => j.id === id);
373
+ if (idx === -1)
374
+ return false;
375
+ const job = { ..._cache[idx] };
376
+ job.enabled = true;
377
+ job.state = 'scheduled';
378
+ job.pausedAt = null;
379
+ job.pausedReason = null;
380
+ _cache = [..._cache];
381
+ _cache[idx] = job;
382
+ void (async () => {
383
+ try {
384
+ const { next } = await (0, cronExecute_1.computeNextFire)(job);
385
+ if (next !== null)
386
+ job.nextRun = new Date(next).toISOString();
387
+ _cache[idx] = job;
388
+ await withLock(async (state) => {
389
+ const sIdx = state.jobs.findIndex((j) => j.id === id);
390
+ if (sIdx !== -1) {
391
+ state.jobs[sIdx] = job;
392
+ await (0, cronState_1.writeCronState)(_paths.stateFile, state);
393
+ }
394
+ });
395
+ await armJobTimer(job);
396
+ }
397
+ catch { /* surfaced via logger only */ }
398
+ })();
399
+ return true;
400
+ }
401
+ /** SYNC delete — removes from cache + clears timer immediately,
402
+ * persists in background. */
403
+ function deleteJob(id) {
404
+ const idx = _cache.findIndex((j) => j.id === id);
405
+ if (idx === -1)
406
+ return false;
407
+ _cache = _cache.filter((j) => j.id !== id);
408
+ clearTimer(id);
409
+ void (async () => {
410
+ try {
411
+ await withLock(async (state) => {
412
+ const sIdx = state.jobs.findIndex((j) => j.id === id);
413
+ if (sIdx !== -1) {
414
+ state.jobs.splice(sIdx, 1);
415
+ await (0, cronState_1.writeCronState)(_paths.stateFile, state);
416
+ }
417
+ });
418
+ }
419
+ catch { /* surfaced via logger only */ }
420
+ })();
421
+ return true;
422
+ }
423
+ async function triggerJob(id) {
424
+ // A trigger fires NOW, then the post-fire armJobTimer re-schedules.
425
+ const exists = await getJob(id);
426
+ if (!exists)
427
+ return false;
428
+ clearTimer(id);
429
+ await (0, cronExecute_1.fireJob)({
430
+ paths: _paths,
431
+ jobId: id,
432
+ runAction: _runAction,
433
+ });
434
+ const refreshed = await getJob(id);
435
+ if (refreshed && refreshed.enabled && refreshed.state === 'scheduled') {
436
+ await armJobTimer(refreshed);
437
+ }
438
+ return true;
439
+ }
440
+ // ── Diagnostics + heartbeat ──────────────────────────────────────────────
441
+ async function getDiagnostics() {
442
+ const lockHeld = await (0, cronState_1.isCronLockHeld)(_paths);
443
+ return (0, diagnostics_1.getDiagnosticsSnapshot)({
444
+ lockPath: _paths.lockFile,
445
+ lockHeld,
446
+ schemaVersion: diagnostics_1.CRON_SCHEMA_VERSION,
447
+ });
448
+ }
449
+ async function getStateSnapshot() {
450
+ return (0, cronState_1.readCronState)(_paths.stateFile);
451
+ }
452
+ /** Start the heartbeat singleton with a default onTick that
453
+ * re-arms changed timers. Idempotent. */
454
+ function startHeartbeat() {
455
+ (0, cronTick_1.startHeartbeat)({
456
+ paths: _paths,
457
+ onTick: async (jobs) => {
458
+ for (const j of jobs) {
459
+ const armed = _timers.has(j.id);
460
+ const shouldArm = j.enabled
461
+ && j.state !== 'paused'
462
+ && j.state !== 'completed';
463
+ if (shouldArm && !armed) {
464
+ await armJobTimer(j);
465
+ }
466
+ else if (!shouldArm && armed) {
467
+ clearTimer(j.id);
468
+ }
469
+ }
470
+ // Drop timers for deleted jobs.
471
+ const live = new Set(jobs.map((j) => j.id));
472
+ for (const id of [..._timers.keys()]) {
473
+ if (!live.has(id))
474
+ clearTimer(id);
475
+ }
476
+ },
477
+ });
478
+ }
479
+ // ── Drain hook ───────────────────────────────────────────────────────────
480
+ /** Test/shutdown drain. */
481
+ async function awaitPendingSaves() {
482
+ const { awaitAllPending } = await Promise.resolve().then(() => __importStar(require('./atomicWrite')));
483
+ await awaitAllPending();
484
+ }
485
+ // ── Test reset ───────────────────────────────────────────────────────────
486
+ function __resetForTests() {
487
+ for (const id of [..._timers.keys()])
488
+ clearTimer(id);
489
+ _bootedPid = null;
490
+ _paths = (0, cronState_1.defaultCronPaths)();
491
+ _runAction = cronExecute_1.defaultRunAction;
492
+ (0, cronTick_1.stopHeartbeat)();
493
+ }
494
+ // Used by test bench to exercise the full path under a temp dir.
495
+ function __testPaths(rootDir) {
496
+ return {
497
+ stateFile: node_path_1.default.join(rootDir, 'cron_jobs.json'),
498
+ lockFile: node_path_1.default.join(rootDir, 'cron_jobs.json.lock'),
499
+ logsDir: node_path_1.default.join(rootDir, 'cron-logs'),
500
+ };
501
+ }
502
+ void node_os_1.default; // import retained for future homedir-relative APIs