neuro-cli 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 (175) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +510 -0
  3. package/dist/advisor/advisor.d.ts +50 -0
  4. package/dist/advisor/advisor.js +178 -0
  5. package/dist/agents/base.d.ts +62 -0
  6. package/dist/agents/base.js +215 -0
  7. package/dist/agents/orchestrator.d.ts +46 -0
  8. package/dist/agents/orchestrator.js +192 -0
  9. package/dist/agents/team.d.ts +51 -0
  10. package/dist/agents/team.js +210 -0
  11. package/dist/api/models.d.ts +23 -0
  12. package/dist/api/models.js +514 -0
  13. package/dist/api/ollama.d.ts +153 -0
  14. package/dist/api/ollama.js +751 -0
  15. package/dist/api/openrouter.d.ts +55 -0
  16. package/dist/api/openrouter.js +223 -0
  17. package/dist/commands/commands.d.ts +43 -0
  18. package/dist/commands/commands.js +308 -0
  19. package/dist/config/config.d.ts +8 -0
  20. package/dist/config/config.js +311 -0
  21. package/dist/context/compaction.d.ts +54 -0
  22. package/dist/context/compaction.js +251 -0
  23. package/dist/context/custom-agents.d.ts +107 -0
  24. package/dist/context/custom-agents.js +397 -0
  25. package/dist/context/custom-tools.d.ts +120 -0
  26. package/dist/context/custom-tools.js +564 -0
  27. package/dist/context/git-checkpoint.d.ts +52 -0
  28. package/dist/context/git-checkpoint.js +240 -0
  29. package/dist/context/neuro-md.d.ts +48 -0
  30. package/dist/context/neuro-md.js +202 -0
  31. package/dist/context/neuroignore.d.ts +102 -0
  32. package/dist/context/neuroignore.js +441 -0
  33. package/dist/context/repo-map.d.ts +38 -0
  34. package/dist/context/repo-map.js +220 -0
  35. package/dist/context/skill-standard.d.ts +262 -0
  36. package/dist/context/skill-standard.js +1156 -0
  37. package/dist/context/skill-system.d.ts +75 -0
  38. package/dist/context/skill-system.js +578 -0
  39. package/dist/context/tree-sitter.d.ts +99 -0
  40. package/dist/context/tree-sitter.js +1956 -0
  41. package/dist/core/acp.d.ts +325 -0
  42. package/dist/core/acp.js +1498 -0
  43. package/dist/core/api-server.d.ts +143 -0
  44. package/dist/core/api-server.js +550 -0
  45. package/dist/core/approval.d.ts +81 -0
  46. package/dist/core/approval.js +432 -0
  47. package/dist/core/auto-compact.d.ts +127 -0
  48. package/dist/core/auto-compact.js +436 -0
  49. package/dist/core/auto-mode.d.ts +232 -0
  50. package/dist/core/auto-mode.js +831 -0
  51. package/dist/core/background-session.d.ts +166 -0
  52. package/dist/core/background-session.js +696 -0
  53. package/dist/core/cicd.d.ts +257 -0
  54. package/dist/core/cicd.js +1443 -0
  55. package/dist/core/cloud-sync.d.ts +156 -0
  56. package/dist/core/cloud-sync.js +582 -0
  57. package/dist/core/code-review.d.ts +132 -0
  58. package/dist/core/code-review.js +1191 -0
  59. package/dist/core/completion.d.ts +49 -0
  60. package/dist/core/completion.js +384 -0
  61. package/dist/core/context.d.ts +38 -0
  62. package/dist/core/context.js +144 -0
  63. package/dist/core/diff-preview.d.ts +35 -0
  64. package/dist/core/diff-preview.js +173 -0
  65. package/dist/core/doom-loop.d.ts +51 -0
  66. package/dist/core/doom-loop.js +179 -0
  67. package/dist/core/engine.d.ts +183 -0
  68. package/dist/core/engine.js +942 -0
  69. package/dist/core/extended-thinking.d.ts +103 -0
  70. package/dist/core/extended-thinking.js +269 -0
  71. package/dist/core/fallback.d.ts +54 -0
  72. package/dist/core/fallback.js +104 -0
  73. package/dist/core/git-worktree.d.ts +139 -0
  74. package/dist/core/git-worktree.js +614 -0
  75. package/dist/core/headless.d.ts +30 -0
  76. package/dist/core/headless.js +101 -0
  77. package/dist/core/i18n.d.ts +83 -0
  78. package/dist/core/i18n.js +739 -0
  79. package/dist/core/linting.d.ts +129 -0
  80. package/dist/core/linting.js +699 -0
  81. package/dist/core/model-router.d.ts +109 -0
  82. package/dist/core/model-router.js +581 -0
  83. package/dist/core/multi-model.d.ts +243 -0
  84. package/dist/core/multi-model.js +1099 -0
  85. package/dist/core/multi-session.d.ts +144 -0
  86. package/dist/core/multi-session.js +442 -0
  87. package/dist/core/multimodal.d.ts +125 -0
  88. package/dist/core/multimodal.js +286 -0
  89. package/dist/core/observability.d.ts +93 -0
  90. package/dist/core/observability.js +737 -0
  91. package/dist/core/os-sandbox.d.ts +122 -0
  92. package/dist/core/os-sandbox.js +1193 -0
  93. package/dist/core/outcome-grading.d.ts +228 -0
  94. package/dist/core/outcome-grading.js +1123 -0
  95. package/dist/core/output-styles.d.ts +57 -0
  96. package/dist/core/output-styles.js +382 -0
  97. package/dist/core/parallel-agents.d.ts +183 -0
  98. package/dist/core/parallel-agents.js +563 -0
  99. package/dist/core/plugin-bundle.d.ts +236 -0
  100. package/dist/core/plugin-bundle.js +887 -0
  101. package/dist/core/plugin-sdk.d.ts +139 -0
  102. package/dist/core/plugin-sdk.js +273 -0
  103. package/dist/core/prompt-cache.d.ts +163 -0
  104. package/dist/core/prompt-cache.js +599 -0
  105. package/dist/core/sandbox.d.ts +127 -0
  106. package/dist/core/sandbox.js +369 -0
  107. package/dist/core/scheduled-tasks.d.ts +151 -0
  108. package/dist/core/scheduled-tasks.js +502 -0
  109. package/dist/core/security-scanner.d.ts +160 -0
  110. package/dist/core/security-scanner.js +1494 -0
  111. package/dist/core/session.d.ts +83 -0
  112. package/dist/core/session.js +269 -0
  113. package/dist/core/shell-completion.d.ts +51 -0
  114. package/dist/core/shell-completion.js +674 -0
  115. package/dist/core/smart-monitor.d.ts +146 -0
  116. package/dist/core/smart-monitor.js +1199 -0
  117. package/dist/core/spec-driven.d.ts +233 -0
  118. package/dist/core/spec-driven.js +1485 -0
  119. package/dist/core/spending-warnings.d.ts +123 -0
  120. package/dist/core/spending-warnings.js +456 -0
  121. package/dist/core/sub-agent.d.ts +298 -0
  122. package/dist/core/sub-agent.js +1023 -0
  123. package/dist/core/telemetry.d.ts +157 -0
  124. package/dist/core/telemetry.js +412 -0
  125. package/dist/core/terminal-ux.d.ts +134 -0
  126. package/dist/core/terminal-ux.js +649 -0
  127. package/dist/core/testing.d.ts +146 -0
  128. package/dist/core/testing.js +1200 -0
  129. package/dist/core/types.d.ts +439 -0
  130. package/dist/core/types.js +6 -0
  131. package/dist/core/undo-redo.d.ts +112 -0
  132. package/dist/core/undo-redo.js +290 -0
  133. package/dist/core/updater.d.ts +159 -0
  134. package/dist/core/updater.js +608 -0
  135. package/dist/core/vim-mode.d.ts +151 -0
  136. package/dist/core/vim-mode.js +771 -0
  137. package/dist/core/voice.d.ts +137 -0
  138. package/dist/core/voice.js +538 -0
  139. package/dist/core/web-dashboard.d.ts +109 -0
  140. package/dist/core/web-dashboard.js +484 -0
  141. package/dist/hooks/hooks.d.ts +74 -0
  142. package/dist/hooks/hooks.js +160 -0
  143. package/dist/hooks/llm-evaluator.d.ts +165 -0
  144. package/dist/hooks/llm-evaluator.js +560 -0
  145. package/dist/index.d.ts +3 -0
  146. package/dist/index.js +1186 -0
  147. package/dist/lsp/lsp-manager.d.ts +63 -0
  148. package/dist/lsp/lsp-manager.js +351 -0
  149. package/dist/mcp/client.d.ts +133 -0
  150. package/dist/mcp/client.js +684 -0
  151. package/dist/mcp/mcp-apps.d.ts +70 -0
  152. package/dist/mcp/mcp-apps.js +1007 -0
  153. package/dist/tools/bash.d.ts +5 -0
  154. package/dist/tools/bash.js +195 -0
  155. package/dist/tools/browser.d.ts +92 -0
  156. package/dist/tools/browser.js +1570 -0
  157. package/dist/tools/extended.d.ts +6 -0
  158. package/dist/tools/extended.js +191 -0
  159. package/dist/tools/file.d.ts +10 -0
  160. package/dist/tools/file.js +382 -0
  161. package/dist/tools/github.d.ts +389 -0
  162. package/dist/tools/github.js +759 -0
  163. package/dist/tools/index.d.ts +9 -0
  164. package/dist/tools/index.js +40 -0
  165. package/dist/tools/memory.d.ts +6 -0
  166. package/dist/tools/memory.js +197 -0
  167. package/dist/tools/registry.d.ts +29 -0
  168. package/dist/tools/registry.js +64 -0
  169. package/dist/tools/web.d.ts +6 -0
  170. package/dist/tools/web.js +150 -0
  171. package/dist/ui/renderer.d.ts +97 -0
  172. package/dist/ui/renderer.js +279 -0
  173. package/dist/ui/theme.d.ts +27 -0
  174. package/dist/ui/theme.js +106 -0
  175. package/package.json +83 -0
@@ -0,0 +1,502 @@
1
+ // ============================================================
2
+ // NeuroCLI - Scheduled Tasks
3
+ // Recurring / scheduled task execution similar to Claude Code's
4
+ // /loop command. Supports cron-like intervals, persistence,
5
+ // execution history, and pausing / resuming.
6
+ // Uses ONLY Node.js built-in modules.
7
+ // ============================================================
8
+ import { existsSync, mkdirSync, readFileSync, writeFileSync, readdirSync, unlinkSync, } from 'fs';
9
+ import { join } from 'path';
10
+ import { homedir } from 'os';
11
+ import { randomUUID } from 'crypto';
12
+ // ---------------------------------------------------------------------------
13
+ // Constants
14
+ // ---------------------------------------------------------------------------
15
+ const NEURO_DIR = join(homedir(), '.neuro');
16
+ const TASKS_FILE = join(NEURO_DIR, 'scheduled-tasks.json');
17
+ const RESULTS_DIR = join(NEURO_DIR, 'scheduled-results');
18
+ const MAX_HISTORY_PER_TASK = 100;
19
+ const MAX_RESULT_FILE_SIZE = 5 * 1024 * 1024; // 5 MB
20
+ // ---------------------------------------------------------------------------
21
+ // ScheduledTaskManager
22
+ // ---------------------------------------------------------------------------
23
+ export class ScheduledTaskManager {
24
+ tasks = new Map();
25
+ timers = new Map();
26
+ engine = null;
27
+ shuttingDown = false;
28
+ constructor(engine) {
29
+ if (engine)
30
+ this.engine = engine;
31
+ this.loadTasks();
32
+ this.registerShutdownHooks();
33
+ }
34
+ // -----------------------------------------------------------------------
35
+ // Engine registration
36
+ // -----------------------------------------------------------------------
37
+ /** Set or replace the execution engine */
38
+ setEngine(engine) {
39
+ this.engine = engine;
40
+ }
41
+ // -----------------------------------------------------------------------
42
+ // Task CRUD
43
+ // -----------------------------------------------------------------------
44
+ /**
45
+ * Schedule a new recurring task.
46
+ * Returns the full task state including its generated ID.
47
+ */
48
+ scheduleTask(config) {
49
+ const id = config.id ?? randomUUID();
50
+ const fullConfig = { ...config, id };
51
+ const now = new Date().toISOString();
52
+ const intervalMs = this.intervalToMs(fullConfig.interval, fullConfig.intervalUnit);
53
+ const nextRunAt = new Date(Date.now() + intervalMs).toISOString();
54
+ const state = {
55
+ config: fullConfig,
56
+ status: 'scheduled',
57
+ runCount: 0,
58
+ failCount: 0,
59
+ nextRunAt,
60
+ createdAt: now,
61
+ updatedAt: now,
62
+ history: [],
63
+ totalCost: 0,
64
+ totalIterations: 0,
65
+ };
66
+ this.tasks.set(id, state);
67
+ this.scheduleTimer(id);
68
+ this.persistTasks();
69
+ return state;
70
+ }
71
+ /** Cancel a scheduled task permanently */
72
+ cancelTask(taskId) {
73
+ const task = this.tasks.get(taskId);
74
+ if (!task)
75
+ return false;
76
+ this.clearTimer(taskId);
77
+ task.status = 'cancelled';
78
+ task.updatedAt = new Date().toISOString();
79
+ task.nextRunAt = undefined;
80
+ this.persistTasks();
81
+ return true;
82
+ }
83
+ /** List all tasks, optionally filtered by status */
84
+ listTasks(status) {
85
+ const all = Array.from(this.tasks.values());
86
+ if (status)
87
+ return all.filter(t => t.status === status);
88
+ return all;
89
+ }
90
+ /** Temporarily pause a task */
91
+ pauseTask(taskId) {
92
+ const task = this.tasks.get(taskId);
93
+ if (!task)
94
+ return false;
95
+ if (task.status === 'paused')
96
+ return true; // already paused
97
+ if (task.status !== 'scheduled' && task.status !== 'running')
98
+ return false;
99
+ this.clearTimer(taskId);
100
+ task.status = 'paused';
101
+ task.nextRunAt = undefined;
102
+ task.updatedAt = new Date().toISOString();
103
+ this.persistTasks();
104
+ return true;
105
+ }
106
+ /** Resume a paused task */
107
+ resumeTask(taskId) {
108
+ const task = this.tasks.get(taskId);
109
+ if (!task)
110
+ return false;
111
+ if (task.status !== 'paused')
112
+ return false;
113
+ const intervalMs = this.intervalToMs(task.config.interval, task.config.intervalUnit);
114
+ task.status = 'scheduled';
115
+ task.nextRunAt = new Date(Date.now() + intervalMs).toISOString();
116
+ task.updatedAt = new Date().toISOString();
117
+ this.scheduleTimer(taskId);
118
+ this.persistTasks();
119
+ return true;
120
+ }
121
+ /** Get execution status and history for a task */
122
+ getTaskStatus(taskId) {
123
+ return this.tasks.get(taskId);
124
+ }
125
+ /** Update a task's configuration (resets timer) */
126
+ updateTask(taskId, updates) {
127
+ const task = this.tasks.get(taskId);
128
+ if (!task)
129
+ return false;
130
+ // Apply updates
131
+ Object.assign(task.config, updates);
132
+ // If interval changed, reschedule
133
+ if (updates.interval !== undefined || updates.intervalUnit !== undefined) {
134
+ this.clearTimer(taskId);
135
+ if (task.status === 'scheduled') {
136
+ const intervalMs = this.intervalToMs(task.config.interval, task.config.intervalUnit);
137
+ task.nextRunAt = new Date(Date.now() + intervalMs).toISOString();
138
+ this.scheduleTimer(taskId);
139
+ }
140
+ }
141
+ task.updatedAt = new Date().toISOString();
142
+ this.persistTasks();
143
+ return true;
144
+ }
145
+ /** Delete a task entirely (removes from memory and persistence) */
146
+ deleteTask(taskId) {
147
+ if (!this.tasks.has(taskId))
148
+ return false;
149
+ this.clearTimer(taskId);
150
+ this.tasks.delete(taskId);
151
+ this.persistTasks();
152
+ return true;
153
+ }
154
+ // -----------------------------------------------------------------------
155
+ // Manual execution
156
+ // -----------------------------------------------------------------------
157
+ /** Manually trigger a task run (outside its schedule) */
158
+ async runTaskNow(taskId) {
159
+ const task = this.tasks.get(taskId);
160
+ if (!task)
161
+ return null;
162
+ if (task.status === 'running')
163
+ return null; // already running
164
+ return this.executeTask(taskId);
165
+ }
166
+ // -----------------------------------------------------------------------
167
+ // Bulk operations
168
+ // -----------------------------------------------------------------------
169
+ /** Pause all scheduled tasks */
170
+ pauseAll() {
171
+ let count = 0;
172
+ for (const [id, task] of Array.from(this.tasks.entries())) {
173
+ if (task.status === 'scheduled') {
174
+ this.pauseTask(id);
175
+ count++;
176
+ }
177
+ }
178
+ return count;
179
+ }
180
+ /** Resume all paused tasks */
181
+ resumeAll() {
182
+ let count = 0;
183
+ for (const [id, task] of Array.from(this.tasks.entries())) {
184
+ if (task.status === 'paused') {
185
+ this.resumeTask(id);
186
+ count++;
187
+ }
188
+ }
189
+ return count;
190
+ }
191
+ /** Cancel all tasks */
192
+ cancelAll() {
193
+ let count = 0;
194
+ for (const id of Array.from(this.tasks.keys())) {
195
+ if (this.cancelTask(id))
196
+ count++;
197
+ }
198
+ return count;
199
+ }
200
+ // -----------------------------------------------------------------------
201
+ // Statistics & Reporting
202
+ // -----------------------------------------------------------------------
203
+ /** Get aggregate stats across all tasks */
204
+ getAggregateStats() {
205
+ let totalRuns = 0;
206
+ let totalFailures = 0;
207
+ let totalCost = 0;
208
+ let activeTasks = 0;
209
+ let pausedTasks = 0;
210
+ for (const task of Array.from(this.tasks.values())) {
211
+ totalRuns += task.runCount;
212
+ totalFailures += task.failCount;
213
+ totalCost += task.totalCost;
214
+ if (task.status === 'scheduled' || task.status === 'running')
215
+ activeTasks++;
216
+ if (task.status === 'paused')
217
+ pausedTasks++;
218
+ }
219
+ return {
220
+ totalTasks: this.tasks.size,
221
+ activeTasks,
222
+ pausedTasks,
223
+ totalRuns,
224
+ totalFailures,
225
+ totalCost,
226
+ };
227
+ }
228
+ /** Get recent executions across all tasks, sorted by time (most recent first) */
229
+ getRecentExecutions(limit = 20) {
230
+ const all = [];
231
+ for (const task of Array.from(this.tasks.values())) {
232
+ all.push(...task.history);
233
+ }
234
+ all.sort((a, b) => new Date(b.startedAt).getTime() - new Date(a.startedAt).getTime());
235
+ return all.slice(0, limit);
236
+ }
237
+ // -----------------------------------------------------------------------
238
+ // Shutdown
239
+ // -----------------------------------------------------------------------
240
+ /** Gracefully shut down – cancels all timers */
241
+ shutdown() {
242
+ this.shuttingDown = true;
243
+ for (const id of Array.from(this.timers.keys())) {
244
+ this.clearTimer(id);
245
+ }
246
+ this.persistTasks();
247
+ }
248
+ // -----------------------------------------------------------------------
249
+ // Private – Timer scheduling
250
+ // -----------------------------------------------------------------------
251
+ scheduleTimer(taskId) {
252
+ this.clearTimer(taskId);
253
+ const task = this.tasks.get(taskId);
254
+ if (!task || task.status !== 'scheduled')
255
+ return;
256
+ const intervalMs = this.intervalToMs(task.config.interval, task.config.intervalUnit);
257
+ // Calculate delay until next run
258
+ let delay;
259
+ if (task.nextRunAt) {
260
+ const nextRun = new Date(task.nextRunAt).getTime();
261
+ delay = Math.max(0, nextRun - Date.now());
262
+ }
263
+ else {
264
+ delay = intervalMs;
265
+ }
266
+ const timer = setTimeout(async () => {
267
+ await this.onTimerFire(taskId);
268
+ }, delay);
269
+ // Allow the Node.js process to exit cleanly if this is the only timer
270
+ if (timer && typeof timer === 'object' && 'unref' in timer) {
271
+ timer.unref();
272
+ }
273
+ this.timers.set(taskId, timer);
274
+ }
275
+ clearTimer(taskId) {
276
+ const timer = this.timers.get(taskId);
277
+ if (timer) {
278
+ clearTimeout(timer);
279
+ this.timers.delete(taskId);
280
+ }
281
+ }
282
+ async onTimerFire(taskId) {
283
+ const task = this.tasks.get(taskId);
284
+ if (!task || task.status !== 'scheduled')
285
+ return;
286
+ await this.executeTask(taskId);
287
+ // Re-schedule if still active
288
+ const updated = this.tasks.get(taskId);
289
+ if (updated && updated.status === 'scheduled') {
290
+ // Check maxRuns
291
+ if (updated.config.maxRuns > 0 && updated.runCount >= updated.config.maxRuns) {
292
+ updated.status = 'completed';
293
+ updated.nextRunAt = undefined;
294
+ updated.updatedAt = new Date().toISOString();
295
+ this.persistTasks();
296
+ return;
297
+ }
298
+ const intervalMs = this.intervalToMs(updated.config.interval, updated.config.intervalUnit);
299
+ updated.nextRunAt = new Date(Date.now() + intervalMs).toISOString();
300
+ updated.updatedAt = new Date().toISOString();
301
+ this.persistTasks();
302
+ this.scheduleTimer(taskId);
303
+ }
304
+ }
305
+ // -----------------------------------------------------------------------
306
+ // Private – Task execution
307
+ // -----------------------------------------------------------------------
308
+ async executeTask(taskId) {
309
+ const task = this.tasks.get(taskId);
310
+ if (!task)
311
+ return null;
312
+ if (!this.engine)
313
+ return null;
314
+ const executionId = randomUUID();
315
+ const execution = {
316
+ id: executionId,
317
+ taskId,
318
+ startedAt: new Date().toISOString(),
319
+ status: 'success',
320
+ prompt: task.config.prompt,
321
+ iterations: 0,
322
+ cost: 0,
323
+ filesChanged: 0,
324
+ commandsRun: 0,
325
+ };
326
+ // Mark as running
327
+ task.status = 'running';
328
+ this.persistTasks();
329
+ try {
330
+ const result = await this.engine.runPrompt(task.config.prompt, task.config.model, task.config.workingDirectory);
331
+ execution.endedAt = new Date().toISOString();
332
+ execution.durationMs = new Date(execution.endedAt).getTime() - new Date(execution.startedAt).getTime();
333
+ execution.result = result.text;
334
+ execution.iterations = result.iterations;
335
+ execution.cost = result.cost;
336
+ execution.filesChanged = result.filesChanged;
337
+ execution.commandsRun = result.commandsRun;
338
+ if (result.error) {
339
+ execution.status = 'error';
340
+ execution.error = result.error;
341
+ task.failCount++;
342
+ }
343
+ else {
344
+ execution.status = 'success';
345
+ }
346
+ task.runCount++;
347
+ task.totalCost += result.cost;
348
+ task.totalIterations += result.iterations;
349
+ task.lastRunAt = execution.endedAt;
350
+ }
351
+ catch (err) {
352
+ execution.endedAt = new Date().toISOString();
353
+ execution.durationMs = new Date(execution.endedAt).getTime() - new Date(execution.startedAt).getTime();
354
+ execution.status = 'error';
355
+ execution.error = err instanceof Error ? err.message : String(err);
356
+ task.failCount++;
357
+ task.runCount++;
358
+ }
359
+ // Add to history
360
+ task.history.unshift(execution);
361
+ if (task.history.length > MAX_HISTORY_PER_TASK) {
362
+ task.history = task.history.slice(0, MAX_HISTORY_PER_TASK);
363
+ }
364
+ // Restore status
365
+ task.status = 'scheduled';
366
+ task.updatedAt = new Date().toISOString();
367
+ // Save results to file if configured
368
+ if (task.config.saveResults && execution.result) {
369
+ this.saveResultToFile(taskId, execution);
370
+ }
371
+ // Notify if configured
372
+ if (task.config.notifyOnComplete) {
373
+ this.notifyCompletion(task.config.name, execution);
374
+ }
375
+ this.persistTasks();
376
+ return execution;
377
+ }
378
+ // -----------------------------------------------------------------------
379
+ // Private – Helpers
380
+ // -----------------------------------------------------------------------
381
+ intervalToMs(interval, unit) {
382
+ switch (unit) {
383
+ case 'minutes': return interval * 60 * 1000;
384
+ case 'hours': return interval * 60 * 60 * 1000;
385
+ case 'days': return interval * 24 * 60 * 60 * 1000;
386
+ default: return interval * 60 * 1000; // default to minutes
387
+ }
388
+ }
389
+ saveResultToFile(taskId, execution) {
390
+ try {
391
+ const taskDir = join(RESULTS_DIR, taskId);
392
+ if (!existsSync(taskDir))
393
+ mkdirSync(taskDir, { recursive: true });
394
+ // Sanitise the execution for file storage
395
+ const resultData = {
396
+ executionId: execution.id,
397
+ taskId,
398
+ taskName: this.tasks.get(taskId)?.config.name ?? 'unknown',
399
+ startedAt: execution.startedAt,
400
+ endedAt: execution.endedAt,
401
+ durationMs: execution.durationMs,
402
+ status: execution.status,
403
+ iterations: execution.iterations,
404
+ cost: execution.cost,
405
+ filesChanged: execution.filesChanged,
406
+ commandsRun: execution.commandsRun,
407
+ result: execution.result
408
+ ? execution.result.slice(0, MAX_RESULT_FILE_SIZE)
409
+ : undefined,
410
+ error: execution.error,
411
+ };
412
+ const filename = `run-${new Date(execution.startedAt).toISOString().replace(/[:.]/g, '-')}.json`;
413
+ writeFileSync(join(taskDir, filename), JSON.stringify(resultData, null, 2), 'utf-8');
414
+ // Clean up old result files – keep at most 50 per task
415
+ try {
416
+ const files = readdirSync(taskDir)
417
+ .filter(f => f.startsWith('run-') && f.endsWith('.json'))
418
+ .sort()
419
+ .reverse();
420
+ for (const file of files.slice(50)) {
421
+ unlinkSync(join(taskDir, file));
422
+ }
423
+ }
424
+ catch { /* ignore cleanup errors */ }
425
+ }
426
+ catch { /* best effort */ }
427
+ }
428
+ notifyCompletion(taskName, execution) {
429
+ // Best-effort desktop notification using built-in tools
430
+ const status = execution.status === 'success' ? '✅' : '❌';
431
+ const message = `${status} "${taskName}" ${execution.status} (${execution.durationMs ?? 0}ms)`;
432
+ // Log to console (always)
433
+ // eslint-disable-next-line no-console
434
+ console.log(`[scheduled-tasks] ${message}`);
435
+ // Try platform notification (macOS / Linux)
436
+ try {
437
+ if (process.platform === 'darwin') {
438
+ // osascript is a built-in macOS tool
439
+ const { execSync } = require('child_process');
440
+ execSync(`osascript -e 'display notification "${message}" with title "NeuroCLI"'`, { stdio: 'pipe', timeout: 5000 });
441
+ }
442
+ else if (process.platform === 'linux') {
443
+ const { execSync } = require('child_process');
444
+ // notify-send is common on Linux desktops
445
+ execSync(`notify-send "NeuroCLI" "${message}"`, { stdio: 'pipe', timeout: 5000 });
446
+ }
447
+ }
448
+ catch {
449
+ // Notification failed – not critical
450
+ }
451
+ }
452
+ // -----------------------------------------------------------------------
453
+ // Private – Persistence
454
+ // -----------------------------------------------------------------------
455
+ persistTasks() {
456
+ try {
457
+ if (!existsSync(NEURO_DIR))
458
+ mkdirSync(NEURO_DIR, { recursive: true });
459
+ const data = Array.from(this.tasks.values()).map(task => ({
460
+ ...task,
461
+ // Don't persist 'running' status – on restart tasks should be 'scheduled'
462
+ status: task.status === 'running' ? 'scheduled' : task.status,
463
+ }));
464
+ writeFileSync(TASKS_FILE, JSON.stringify(data, null, 2), 'utf-8');
465
+ }
466
+ catch { /* best effort */ }
467
+ }
468
+ loadTasks() {
469
+ try {
470
+ if (!existsSync(TASKS_FILE))
471
+ return;
472
+ const data = JSON.parse(readFileSync(TASKS_FILE, 'utf-8'));
473
+ for (const taskState of data) {
474
+ this.tasks.set(taskState.config.id, taskState);
475
+ // Re-schedule timers for tasks that were active
476
+ if (taskState.status === 'scheduled') {
477
+ // If the nextRunAt is in the past, execute immediately then reschedule
478
+ if (taskState.nextRunAt && new Date(taskState.nextRunAt).getTime() <= Date.now()) {
479
+ const intervalMs = this.intervalToMs(taskState.config.interval, taskState.config.intervalUnit);
480
+ taskState.nextRunAt = new Date(Date.now() + intervalMs).toISOString();
481
+ }
482
+ this.scheduleTimer(taskState.config.id);
483
+ }
484
+ }
485
+ }
486
+ catch { /* ignore corrupt state */ }
487
+ }
488
+ // -----------------------------------------------------------------------
489
+ // Private – Shutdown hooks
490
+ // -----------------------------------------------------------------------
491
+ registerShutdownHooks() {
492
+ const handler = () => {
493
+ if (!this.shuttingDown) {
494
+ this.shutdown();
495
+ }
496
+ };
497
+ process.on('SIGINT', handler);
498
+ process.on('SIGTERM', handler);
499
+ process.on('beforeExit', handler);
500
+ }
501
+ }
502
+ //# sourceMappingURL=scheduled-tasks.js.map
@@ -0,0 +1,160 @@
1
+ export interface SecurityVulnerability {
2
+ id: string;
3
+ ruleId: string;
4
+ file: string;
5
+ line: number;
6
+ column: number;
7
+ severity: 'critical' | 'high' | 'medium' | 'low' | 'info';
8
+ category: string;
9
+ title: string;
10
+ description: string;
11
+ remediation: string;
12
+ cwe?: string;
13
+ owasp?: string;
14
+ confidence: 'high' | 'medium' | 'low';
15
+ }
16
+ export interface SecurityScanResult {
17
+ totalVulnerabilities: number;
18
+ critical: number;
19
+ high: number;
20
+ medium: number;
21
+ low: number;
22
+ info: number;
23
+ files: number;
24
+ duration: number;
25
+ vulnerabilities: SecurityVulnerability[];
26
+ }
27
+ export interface SecurityScanConfig {
28
+ enabled: boolean;
29
+ autoScanOnChange: boolean;
30
+ failOnSeverity: 'critical' | 'high' | 'medium' | 'low';
31
+ excludePatterns: string[];
32
+ customRules: SecurityRule[];
33
+ }
34
+ export interface SecurityRule {
35
+ id: string;
36
+ name: string;
37
+ category: string;
38
+ severity: 'critical' | 'high' | 'medium' | 'low' | 'info';
39
+ pattern: string;
40
+ description: string;
41
+ remediation: string;
42
+ cwe?: string;
43
+ owasp?: string;
44
+ }
45
+ interface ScanOptions {
46
+ recursive?: boolean;
47
+ excludePatterns?: string[];
48
+ maxFileSize?: number;
49
+ fileExtensions?: string[];
50
+ }
51
+ export declare class SecurityScanner {
52
+ private rules;
53
+ private vulnerabilities;
54
+ private ignoredRules;
55
+ private ignoredFiles;
56
+ private ignoreEntries;
57
+ private config;
58
+ private vulnCounter;
59
+ private rootDir;
60
+ constructor(rootDir?: string, config?: Partial<SecurityScanConfig>);
61
+ /**
62
+ * Scan a single file for security issues.
63
+ */
64
+ scanFile(filePath: string): SecurityVulnerability[];
65
+ /**
66
+ * Scan an entire directory for security issues.
67
+ */
68
+ scanDirectory(dirPath: string, options?: ScanOptions): SecurityScanResult;
69
+ /**
70
+ * Scan a unified diff for security issues.
71
+ */
72
+ scanDiff(diff: string): SecurityVulnerability[];
73
+ /**
74
+ * Get all found vulnerabilities.
75
+ */
76
+ getVulnerabilities(): SecurityVulnerability[];
77
+ /**
78
+ * Get vulnerability counts by severity.
79
+ */
80
+ getSeverityCounts(): Record<string, number>;
81
+ /**
82
+ * Generate a report in the specified format.
83
+ */
84
+ generateReport(format?: 'json' | 'markdown' | 'sarif'): string;
85
+ /**
86
+ * Configure custom rules. Replaces existing custom rules.
87
+ */
88
+ setRules(rules: SecurityRule[]): void;
89
+ /**
90
+ * Ignore a specific rule by its ID.
91
+ */
92
+ ignoreRule(ruleId: string): void;
93
+ /**
94
+ * Ignore findings in a specific file.
95
+ */
96
+ ignoreFile(filePath: string): void;
97
+ /**
98
+ * Get remediation suggestions for a vulnerability.
99
+ */
100
+ getRemediation(vulnId: string): string | null;
101
+ /**
102
+ * Compute a CVSS-like score for the current set of vulnerabilities.
103
+ */
104
+ getCVSSLikeScore(): number;
105
+ /**
106
+ * Clear all found vulnerabilities.
107
+ */
108
+ clear(): void;
109
+ /**
110
+ * Get all active rules.
111
+ */
112
+ getRules(): SecurityRule[];
113
+ /**
114
+ * Get the current configuration.
115
+ */
116
+ getConfig(): SecurityScanConfig;
117
+ /**
118
+ * Update configuration.
119
+ */
120
+ updateConfig(partial: Partial<SecurityScanConfig>): void;
121
+ /**
122
+ * Export findings to a file.
123
+ */
124
+ exportReport(outputPath: string, format?: 'json' | 'markdown' | 'sarif'): void;
125
+ private scanContent;
126
+ private walkDirectory;
127
+ private parseDiff;
128
+ private loadIgnoreFile;
129
+ private isFilePathIgnored;
130
+ private isFindingIgnored;
131
+ private isPathExcluded;
132
+ private assessConfidence;
133
+ private generateVulnId;
134
+ private generateJsonReport;
135
+ private generateMarkdownReport;
136
+ private generateSarifReport;
137
+ private severityToSarifLevel;
138
+ }
139
+ /**
140
+ * Create a pre-configured SecurityScanner instance.
141
+ */
142
+ export declare function createSecurityScanner(rootDir?: string, config?: Partial<SecurityScanConfig>): SecurityScanner;
143
+ /**
144
+ * Quick-scan a single file and return findings.
145
+ */
146
+ export declare function quickScanFile(filePath: string): SecurityVulnerability[];
147
+ /**
148
+ * Quick-scan a directory and return a full result.
149
+ */
150
+ export declare function quickScanDirectory(dirPath: string, options?: ScanOptions): SecurityScanResult;
151
+ /**
152
+ * Quick-scan a diff and return findings.
153
+ */
154
+ export declare function quickScanDiff(diff: string): SecurityVulnerability[];
155
+ /**
156
+ * Get the default set of security rules.
157
+ */
158
+ export declare function getDefaultSecurityRules(): SecurityRule[];
159
+ export {};
160
+ //# sourceMappingURL=security-scanner.d.ts.map