aiden-runtime 4.0.1 → 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.
- package/README.md +11 -7
- package/config/hardware.json +2 -2
- package/dist/api/server.js +50 -52
- package/dist/cli/v4/aidenCLI.js +513 -14
- package/dist/cli/v4/aidenPrompt.js +317 -0
- package/dist/cli/v4/box.js +105 -39
- package/dist/cli/v4/callbacks.js +39 -6
- package/dist/cli/v4/chatSession.js +269 -52
- package/dist/cli/v4/citationFooter.js +97 -0
- package/dist/cli/v4/commands/channel.js +656 -0
- package/dist/cli/v4/commands/clear.js +1 -1
- package/dist/cli/v4/commands/compress.js +1 -1
- package/dist/cli/v4/commands/cron.js +44 -16
- package/dist/cli/v4/commands/fanout.js +236 -0
- package/dist/cli/v4/commands/help.js +15 -4
- package/dist/cli/v4/commands/history.js +84 -0
- package/dist/cli/v4/commands/index.js +19 -1
- package/dist/cli/v4/commands/mcp.js +358 -0
- package/dist/cli/v4/commands/setup.js +34 -0
- package/dist/cli/v4/commands/show.js +43 -0
- package/dist/cli/v4/commands/skills.js +169 -4
- package/dist/cli/v4/commands/status.js +84 -0
- package/dist/cli/v4/commands/subagent.js +78 -0
- package/dist/cli/v4/commands/verbose.js +1 -1
- package/dist/cli/v4/commands/voice.js +218 -0
- package/dist/cli/v4/cronCli.js +103 -0
- package/dist/cli/v4/display.js +300 -14
- package/dist/cli/v4/doctor.js +41 -0
- package/dist/cli/v4/envSources.js +105 -0
- package/dist/cli/v4/ghostMatch.js +74 -0
- package/dist/cli/v4/historyStore.js +163 -0
- package/dist/cli/v4/pasteCompression.js +124 -0
- package/dist/cli/v4/pasteIntercept.js +203 -0
- package/dist/cli/v4/replyRenderer.js +209 -0
- package/dist/cli/v4/resizeGuard.js +92 -0
- package/dist/cli/v4/setupWizard.js +466 -232
- package/dist/cli/v4/shellInterpolation.js +139 -0
- package/dist/cli/v4/skinEngine.js +21 -1
- package/dist/cli/v4/streamingPrefix.js +121 -0
- package/dist/cli/v4/syntaxHighlight.js +345 -0
- package/dist/cli/v4/table.js +216 -0
- package/dist/cli/v4/themeDetect.js +81 -0
- package/dist/cli/v4/uiBuild.js +74 -0
- package/dist/cli/v4/voiceCli.js +113 -0
- package/dist/cli/v4/voicePromptApi.js +196 -0
- package/dist/core/channels/discord.js +16 -10
- package/dist/core/channels/email.js +13 -9
- package/dist/core/channels/imessage.js +13 -9
- package/dist/core/channels/manager.js +25 -7
- package/dist/core/channels/pdf-extract.js +180 -0
- package/dist/core/channels/photo-vision.js +157 -0
- package/dist/core/channels/signal.js +11 -7
- package/dist/core/channels/slack.js +13 -10
- package/dist/core/channels/telegram-commands.js +154 -0
- package/dist/core/channels/telegram-groups.js +198 -0
- package/dist/core/channels/telegram-rate-limit.js +124 -0
- package/dist/core/channels/telegram.js +1980 -0
- package/dist/core/channels/twilio.js +11 -7
- package/dist/core/channels/webhook.js +9 -5
- package/dist/core/channels/whatsapp.js +15 -11
- package/dist/core/channels/whisper-transcribe.js +163 -0
- package/dist/core/cronManager.js +33 -294
- package/dist/core/gateway.js +29 -8
- package/dist/core/playwrightBridge.js +90 -0
- package/dist/core/v4/aidenAgent.js +35 -0
- package/dist/core/v4/auxiliaryClient.js +2 -2
- package/dist/core/v4/cron/atomicWrite.js +18 -4
- package/dist/core/v4/cron/cronExecute.js +300 -0
- package/dist/core/v4/cron/cronManager.js +502 -0
- package/dist/core/v4/cron/cronState.js +314 -0
- package/dist/core/v4/cron/cronTick.js +90 -0
- package/dist/core/v4/cron/diagnostics.js +104 -0
- package/dist/core/v4/cron/graceWindow.js +79 -0
- package/dist/core/v4/firstRun/providerDetection.js +287 -0
- package/dist/core/v4/logger/factory.js +110 -0
- package/dist/core/v4/logger/index.js +22 -0
- package/dist/core/v4/logger/logger.js +101 -0
- package/dist/core/v4/logger/sinks/fileSink.js +110 -0
- package/dist/core/v4/logger/sinks/multiSink.js +43 -0
- package/dist/core/v4/logger/sinks/nullSink.js +53 -0
- package/dist/core/v4/logger/sinks/stdSink.js +81 -0
- package/dist/core/v4/mcp/server/diagnostics.js +40 -0
- package/dist/core/v4/mcp/server/skillBridge.js +94 -0
- package/dist/core/v4/mcp/server/stdioServer.js +119 -0
- package/dist/core/v4/mcp/server/toolBridge.js +168 -0
- package/dist/core/v4/platformPaths.js +105 -0
- package/dist/core/v4/providerFallback.js +25 -0
- package/dist/core/v4/skillLoader.js +21 -5
- package/dist/core/v4/skillMining/candidateStore.js +164 -0
- package/dist/core/v4/skillMining/extractorPrompt.js +111 -0
- package/dist/core/v4/skillMining/proposalBuilder.js +139 -0
- package/dist/core/v4/skillMining/skillMiner.js +191 -0
- package/dist/core/v4/skillMining/traceFingerprint.js +51 -0
- package/dist/core/v4/subagent/budget.js +76 -0
- package/dist/core/v4/subagent/diagnostics.js +22 -0
- package/dist/core/v4/subagent/fanout.js +216 -0
- package/dist/core/v4/subagent/merger.js +148 -0
- package/dist/core/v4/subagent/providerRotation.js +54 -0
- package/dist/core/v4/voice/audioStream.js +373 -0
- package/dist/core/v4/voice/cliVoice.js +393 -0
- package/dist/core/v4/voice/diagnostics.js +66 -0
- package/dist/core/v4/voice/ttsStream.js +193 -0
- package/dist/core/version.js +1 -1
- package/dist/core/visionAnalyze.js +291 -90
- package/dist/core/voice/audio.js +61 -5
- package/dist/core/voice/audioBackend.js +134 -0
- package/dist/core/voice/stt.js +61 -6
- package/dist/core/voice/tts.js +19 -3
- package/dist/providers/v4/nullAdapter.js +58 -0
- package/dist/tools/v4/index.js +32 -1
- package/dist/tools/v4/subagent/subagentFanout.js +166 -0
- 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
|