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.
Files changed (112) 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 +513 -14
  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 +269 -52
  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 +19 -1
  18. package/dist/cli/v4/commands/mcp.js +358 -0
  19. package/dist/cli/v4/commands/setup.js +34 -0
  20. package/dist/cli/v4/commands/show.js +43 -0
  21. package/dist/cli/v4/commands/skills.js +169 -4
  22. package/dist/cli/v4/commands/status.js +84 -0
  23. package/dist/cli/v4/commands/subagent.js +78 -0
  24. package/dist/cli/v4/commands/verbose.js +1 -1
  25. package/dist/cli/v4/commands/voice.js +218 -0
  26. package/dist/cli/v4/cronCli.js +103 -0
  27. package/dist/cli/v4/display.js +300 -14
  28. package/dist/cli/v4/doctor.js +41 -0
  29. package/dist/cli/v4/envSources.js +105 -0
  30. package/dist/cli/v4/ghostMatch.js +74 -0
  31. package/dist/cli/v4/historyStore.js +163 -0
  32. package/dist/cli/v4/pasteCompression.js +124 -0
  33. package/dist/cli/v4/pasteIntercept.js +203 -0
  34. package/dist/cli/v4/replyRenderer.js +209 -0
  35. package/dist/cli/v4/resizeGuard.js +92 -0
  36. package/dist/cli/v4/setupWizard.js +466 -232
  37. package/dist/cli/v4/shellInterpolation.js +139 -0
  38. package/dist/cli/v4/skinEngine.js +21 -1
  39. package/dist/cli/v4/streamingPrefix.js +121 -0
  40. package/dist/cli/v4/syntaxHighlight.js +345 -0
  41. package/dist/cli/v4/table.js +216 -0
  42. package/dist/cli/v4/themeDetect.js +81 -0
  43. package/dist/cli/v4/uiBuild.js +74 -0
  44. package/dist/cli/v4/voiceCli.js +113 -0
  45. package/dist/cli/v4/voicePromptApi.js +196 -0
  46. package/dist/core/channels/discord.js +16 -10
  47. package/dist/core/channels/email.js +13 -9
  48. package/dist/core/channels/imessage.js +13 -9
  49. package/dist/core/channels/manager.js +25 -7
  50. package/dist/core/channels/pdf-extract.js +180 -0
  51. package/dist/core/channels/photo-vision.js +157 -0
  52. package/dist/core/channels/signal.js +11 -7
  53. package/dist/core/channels/slack.js +13 -10
  54. package/dist/core/channels/telegram-commands.js +154 -0
  55. package/dist/core/channels/telegram-groups.js +198 -0
  56. package/dist/core/channels/telegram-rate-limit.js +124 -0
  57. package/dist/core/channels/telegram.js +1980 -0
  58. package/dist/core/channels/twilio.js +11 -7
  59. package/dist/core/channels/webhook.js +9 -5
  60. package/dist/core/channels/whatsapp.js +15 -11
  61. package/dist/core/channels/whisper-transcribe.js +163 -0
  62. package/dist/core/cronManager.js +33 -294
  63. package/dist/core/gateway.js +29 -8
  64. package/dist/core/playwrightBridge.js +90 -0
  65. package/dist/core/v4/aidenAgent.js +35 -0
  66. package/dist/core/v4/auxiliaryClient.js +2 -2
  67. package/dist/core/v4/cron/atomicWrite.js +18 -4
  68. package/dist/core/v4/cron/cronExecute.js +300 -0
  69. package/dist/core/v4/cron/cronManager.js +502 -0
  70. package/dist/core/v4/cron/cronState.js +314 -0
  71. package/dist/core/v4/cron/cronTick.js +90 -0
  72. package/dist/core/v4/cron/diagnostics.js +104 -0
  73. package/dist/core/v4/cron/graceWindow.js +79 -0
  74. package/dist/core/v4/firstRun/providerDetection.js +287 -0
  75. package/dist/core/v4/logger/factory.js +110 -0
  76. package/dist/core/v4/logger/index.js +22 -0
  77. package/dist/core/v4/logger/logger.js +101 -0
  78. package/dist/core/v4/logger/sinks/fileSink.js +110 -0
  79. package/dist/core/v4/logger/sinks/multiSink.js +43 -0
  80. package/dist/core/v4/logger/sinks/nullSink.js +53 -0
  81. package/dist/core/v4/logger/sinks/stdSink.js +81 -0
  82. package/dist/core/v4/mcp/server/diagnostics.js +40 -0
  83. package/dist/core/v4/mcp/server/skillBridge.js +94 -0
  84. package/dist/core/v4/mcp/server/stdioServer.js +119 -0
  85. package/dist/core/v4/mcp/server/toolBridge.js +168 -0
  86. package/dist/core/v4/platformPaths.js +105 -0
  87. package/dist/core/v4/providerFallback.js +25 -0
  88. package/dist/core/v4/skillLoader.js +21 -5
  89. package/dist/core/v4/skillMining/candidateStore.js +164 -0
  90. package/dist/core/v4/skillMining/extractorPrompt.js +111 -0
  91. package/dist/core/v4/skillMining/proposalBuilder.js +139 -0
  92. package/dist/core/v4/skillMining/skillMiner.js +191 -0
  93. package/dist/core/v4/skillMining/traceFingerprint.js +51 -0
  94. package/dist/core/v4/subagent/budget.js +76 -0
  95. package/dist/core/v4/subagent/diagnostics.js +22 -0
  96. package/dist/core/v4/subagent/fanout.js +216 -0
  97. package/dist/core/v4/subagent/merger.js +148 -0
  98. package/dist/core/v4/subagent/providerRotation.js +54 -0
  99. package/dist/core/v4/voice/audioStream.js +373 -0
  100. package/dist/core/v4/voice/cliVoice.js +393 -0
  101. package/dist/core/v4/voice/diagnostics.js +66 -0
  102. package/dist/core/v4/voice/ttsStream.js +193 -0
  103. package/dist/core/version.js +1 -1
  104. package/dist/core/visionAnalyze.js +291 -90
  105. package/dist/core/voice/audio.js +61 -5
  106. package/dist/core/voice/audioBackend.js +134 -0
  107. package/dist/core/voice/stt.js +61 -6
  108. package/dist/core/voice/tts.js +19 -3
  109. package/dist/providers/v4/nullAdapter.js +58 -0
  110. package/dist/tools/v4/index.js +32 -1
  111. package/dist/tools/v4/subagent/subagentFanout.js +166 -0
  112. package/package.json +11 -2
@@ -0,0 +1,300 @@
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/cronExecute.ts — Phase v4.1-cron
10
+ *
11
+ * Fire one cron job end-to-end with the hardened safeguards:
12
+ *
13
+ * 1. ADVANCE-BEFORE-EXECUTE. Under lock, compute the next-fire
14
+ * timestamp and persist it FIRST. Only then dispatch the
15
+ * action. Hard-won lesson: "missing one run is far better
16
+ * than firing dozens of times in a crash loop." If the
17
+ * process dies during execute, restart sees the already-
18
+ * advanced nextRun and waits for that — no double-fire.
19
+ *
20
+ * 2. INACTIVITY TIMEOUT. `Promise.race` against a per-fire
21
+ * deadline (default 600s, env `AIDEN_CRON_TIMEOUT_MS`). On
22
+ * timeout, mark `last_status="timeout"` and set
23
+ * `last_error` to the timeout message.
24
+ *
25
+ * 3. TRY/FINALLY CLEANUP. Long-running shell_exec children may
26
+ * leak file descriptors / processes. The finally block
27
+ * guarantees we record the run in diagnostics + persist
28
+ * state even if the action throws synchronously.
29
+ *
30
+ * 4. EMPTY-OUTPUT WARNING. If the action returns ok=true with
31
+ * ZERO output bytes, mark `last_status="warn"`. prior systems' #6
32
+ * lesson: an empty agent response is a soft failure — don't
33
+ * claim "ok".
34
+ *
35
+ * 5. STATE="error" + enabled=true on un-computable next-fire.
36
+ * Never silently disable a recurring job because croner
37
+ * hiccupped — surface the error to the user via /cron status.
38
+ *
39
+ * The actual command dispatch is injected as `runActionFn` so
40
+ * tests can stub it without spinning up the v3 toolRegistry.
41
+ */
42
+ Object.defineProperty(exports, "__esModule", { value: true });
43
+ exports.computeNextFire = computeNextFire;
44
+ exports.decideFire = decideFire;
45
+ exports.fireJob = fireJob;
46
+ exports.defaultRunAction = defaultRunAction;
47
+ const node_fs_1 = require("node:fs");
48
+ const cronState_1 = require("./cronState");
49
+ const graceWindow_1 = require("./graceWindow");
50
+ const diagnostics_1 = require("./diagnostics");
51
+ const outputCapture_1 = require("./outputCapture");
52
+ // ── Next-fire computation ────────────────────────────────────────────────
53
+ /** Compute the next fire time for a job. Returns null when there
54
+ * is no future occurrence (one-shot already fired, malformed
55
+ * cron expr, etc.). The caller's response to null differs by
56
+ * kind:
57
+ *
58
+ * - oneshot: flip enabled=false, state='completed'
59
+ * - interval/cron: state='error', enabled=true (don't disable!)
60
+ *
61
+ * Period (ms) is also returned for the grace-window math. */
62
+ async function computeNextFire(job, nowMs = Date.now()) {
63
+ if (job.kind === 'interval') {
64
+ if (typeof job.intervalMs !== 'number' || job.intervalMs <= 0) {
65
+ return { next: null, periodMs: 0 };
66
+ }
67
+ const anchor = job.lastRun
68
+ ? new Date(job.lastRun).getTime()
69
+ : new Date(job.createdAt).getTime();
70
+ let next = anchor + job.intervalMs;
71
+ // If anchor + interval is in the past, fast-forward to the next
72
+ // future tick (consumed by graceWindow.evaluateRecurring).
73
+ while (next <= nowMs)
74
+ next += job.intervalMs;
75
+ return { next, periodMs: job.intervalMs };
76
+ }
77
+ if (job.kind === 'oneshot') {
78
+ if (!job.oneshotIso)
79
+ return { next: null, periodMs: 0 };
80
+ const t = new Date(job.oneshotIso).getTime();
81
+ return { next: Number.isFinite(t) ? t : null, periodMs: 0 };
82
+ }
83
+ // cron — delegate to croner.
84
+ if (!job.cronExpr)
85
+ return { next: null, periodMs: 0 };
86
+ try {
87
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
88
+ const { Cron } = require('croner');
89
+ const c = new Cron(job.cronExpr);
90
+ const a = c.nextRun(new Date(nowMs));
91
+ if (!a)
92
+ return { next: null, periodMs: 0 };
93
+ const b = c.nextRun(a);
94
+ const period = b ? b.getTime() - a.getTime() : 0;
95
+ return { next: a.getTime(), periodMs: period };
96
+ }
97
+ catch {
98
+ return { next: null, periodMs: 0 };
99
+ }
100
+ }
101
+ // ── Verdict ─────────────────────────────────────────────────────────────
102
+ /** Decide what to do with this job at `nowMs`. Pure — read from
103
+ * the in-memory snapshot, no state mutation. */
104
+ async function decideFire(job, nowMs = Date.now()) {
105
+ const { next, periodMs } = await computeNextFire(job, nowMs);
106
+ if (next === null) {
107
+ // Caller flips state based on kind.
108
+ return { verdict: { kind: 'wait' }, nextMs: null, periodMs };
109
+ }
110
+ if (job.kind === 'oneshot') {
111
+ return { verdict: (0, graceWindow_1.evaluateOneShot)({ runAtMs: next, nowMs }), nextMs: next, periodMs };
112
+ }
113
+ return {
114
+ verdict: (0, graceWindow_1.evaluateRecurring)({ nextRunAtMs: next, periodMs, nowMs }),
115
+ nextMs: next,
116
+ periodMs,
117
+ };
118
+ }
119
+ /** Run one job end-to-end. Acquires lock, advances next-run BEFORE
120
+ * dispatch, runs the action with timeout, persists result. NEVER
121
+ * throws — failures land in `lastError` / `lastResult='timeout'`. */
122
+ async function fireJob(opts) {
123
+ const now = opts.now ?? Date.now;
124
+ const timeoutMs = opts.timeoutMs ?? (0, diagnostics_1.resolveTimeoutMs)();
125
+ // ── Phase 1: advance under lock ─────────────────────────────
126
+ const lock = await (0, cronState_1.acquireCronLock)(opts.paths, { failFast: false });
127
+ if (!lock) {
128
+ // Lock held by another process — skip this fire.
129
+ return null;
130
+ }
131
+ let job;
132
+ let state;
133
+ try {
134
+ state = await (0, cronState_1.readCronState)(opts.paths.stateFile);
135
+ const idx = state.jobs.findIndex((j) => j.id === opts.jobId);
136
+ if (idx === -1)
137
+ return null;
138
+ job = state.jobs[idx];
139
+ // Skip paused / disabled jobs defensively (the caller filters
140
+ // these out, but a stale heartbeat could race).
141
+ if (!job.enabled || job.state === 'paused' || job.state === 'completed') {
142
+ return null;
143
+ }
144
+ // Compute next-fire BEFORE running the action.
145
+ const { next, periodMs } = await computeNextFire(job, now());
146
+ if (next === null) {
147
+ // Un-computable. Recurring → state="error"; oneshot → completed.
148
+ if (job.kind === 'oneshot') {
149
+ job.enabled = false;
150
+ job.state = 'completed';
151
+ }
152
+ else {
153
+ job.state = 'error';
154
+ job.lastError = 'Cron schedule produced no future fire time';
155
+ }
156
+ state.jobs[idx] = job;
157
+ await (0, cronState_1.writeCronState)(opts.paths.stateFile, state);
158
+ return null;
159
+ }
160
+ // For recurring jobs, set nextRun to the future before dispatch.
161
+ // For one-shots, the next fire is the SAME as this one — we'll
162
+ // mark completed when the action returns.
163
+ if (job.kind !== 'oneshot') {
164
+ // Advance to NEXT future (this fire we're about to do should
165
+ // not re-fire on restart).
166
+ job.nextRun = new Date(next).toISOString();
167
+ }
168
+ state.jobs[idx] = job;
169
+ await (0, cronState_1.writeCronState)(opts.paths.stateFile, state);
170
+ void periodMs; // recorded for diagnostics elsewhere
171
+ }
172
+ finally {
173
+ await lock.release();
174
+ }
175
+ // ── Phase 2: dispatch with timeout (no lock held) ─────────────
176
+ (0, diagnostics_1.noteFireStarted)();
177
+ const startedAt = new Date(now()).toISOString();
178
+ const t0 = now();
179
+ const aborter = new AbortController();
180
+ let timeoutFired = false;
181
+ const timer = setTimeout(() => {
182
+ timeoutFired = true;
183
+ aborter.abort();
184
+ }, timeoutMs);
185
+ let captureOutcome;
186
+ try {
187
+ captureOutcome = await (0, outputCapture_1.captureRun)(job.id, job.description || job.id, opts.paths.logsDir, async () => {
188
+ try {
189
+ // Race the action against the timeout. The timeout AbortSignal
190
+ // is plumbed in for cooperative cancellation.
191
+ const r = await opts.runAction(job, aborter.signal);
192
+ if (timeoutFired) {
193
+ return {
194
+ output: 'timeout',
195
+ failed: true,
196
+ };
197
+ }
198
+ return r;
199
+ }
200
+ catch (err) {
201
+ return {
202
+ output: err instanceof Error ? (err.stack ?? err.message) : String(err),
203
+ failed: true,
204
+ };
205
+ }
206
+ });
207
+ }
208
+ finally {
209
+ clearTimeout(timer);
210
+ }
211
+ // ── Phase 3: record result under lock ─────────────────────────
212
+ const status = timeoutFired
213
+ ? 'timeout'
214
+ : captureOutcome.result === 'ok' && captureOutcome.fullOutputBytes === 0
215
+ ? 'warn' // empty output is a soft failure (prior-systems lesson)
216
+ : captureOutcome.result === 'ok'
217
+ ? 'ok'
218
+ : 'error';
219
+ const fireRecord = {
220
+ jobId: job.id,
221
+ startedAt,
222
+ durationMs: now() - t0,
223
+ status,
224
+ error: status === 'error' || status === 'timeout'
225
+ ? captureOutcome.output.slice(0, 200)
226
+ : undefined,
227
+ };
228
+ (0, diagnostics_1.recordFire)(fireRecord);
229
+ const lock2 = await (0, cronState_1.acquireCronLock)(opts.paths, { failFast: false });
230
+ if (lock2) {
231
+ try {
232
+ const fresh = await (0, cronState_1.readCronState)(opts.paths.stateFile);
233
+ const idx = fresh.jobs.findIndex((j) => j.id === job.id);
234
+ if (idx !== -1) {
235
+ const j = fresh.jobs[idx];
236
+ j.lastRun = startedAt;
237
+ j.lastResult = status;
238
+ j.lastOutput = captureOutcome.output;
239
+ j.lastError = (status === 'ok' || status === 'warn') ? null : captureOutcome.output.slice(0, 500);
240
+ j.runCount = (j.runCount ?? 0) + 1;
241
+ if (j.kind === 'oneshot') {
242
+ j.enabled = false;
243
+ j.state = 'completed';
244
+ }
245
+ fresh.jobs[idx] = j;
246
+ await (0, cronState_1.writeCronState)(opts.paths.stateFile, fresh);
247
+ }
248
+ }
249
+ finally {
250
+ await lock2.release();
251
+ }
252
+ }
253
+ return fireRecord;
254
+ }
255
+ /** Default `runActionFn` — dispatch via plain `child_process.exec`.
256
+ * Cron jobs run a shell command; we don't need the agent loop or
257
+ * the v3 toolRegistry's gating layers for that. Direct shell out
258
+ * is faster, lighter, and honours AbortSignal cleanly via SIGTERM
259
+ * on the spawned child.
260
+ *
261
+ * Output is captured and combined (stdout + stderr) so the
262
+ * outputCapture's truncation sees the full picture. */
263
+ async function defaultRunAction(job, signal) {
264
+ if (signal.aborted)
265
+ return { output: 'aborted before dispatch', failed: true };
266
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
267
+ const { exec } = require('node:child_process');
268
+ return new Promise((resolve) => {
269
+ const child = exec(job.action, {
270
+ timeout: 0, // we own the timer via signal in cronExecute
271
+ maxBuffer: 4 * 1024 * 1024,
272
+ }, (err, stdout, stderr) => {
273
+ const out = String(stdout ?? '') + (stderr ? `\n${stderr}` : '');
274
+ if (err) {
275
+ resolve({
276
+ output: out || err.message,
277
+ failed: true,
278
+ });
279
+ }
280
+ else {
281
+ resolve({ output: out, failed: false });
282
+ }
283
+ });
284
+ signal.addEventListener('abort', () => {
285
+ try {
286
+ child.kill('SIGTERM');
287
+ }
288
+ catch { /* already dead */ }
289
+ // SIGKILL backstop after 2s for shells that ignore SIGTERM.
290
+ setTimeout(() => {
291
+ try {
292
+ child.kill('SIGKILL');
293
+ }
294
+ catch { /* dead */ }
295
+ }, 2000).unref();
296
+ }, { once: true });
297
+ });
298
+ }
299
+ // (Marked exported for the smoke; unused outside.)
300
+ void node_fs_1.promises;