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.
- package/LICENSE +21 -0
- package/README.md +510 -0
- package/dist/advisor/advisor.d.ts +50 -0
- package/dist/advisor/advisor.js +178 -0
- package/dist/agents/base.d.ts +62 -0
- package/dist/agents/base.js +215 -0
- package/dist/agents/orchestrator.d.ts +46 -0
- package/dist/agents/orchestrator.js +192 -0
- package/dist/agents/team.d.ts +51 -0
- package/dist/agents/team.js +210 -0
- package/dist/api/models.d.ts +23 -0
- package/dist/api/models.js +514 -0
- package/dist/api/ollama.d.ts +153 -0
- package/dist/api/ollama.js +751 -0
- package/dist/api/openrouter.d.ts +55 -0
- package/dist/api/openrouter.js +223 -0
- package/dist/commands/commands.d.ts +43 -0
- package/dist/commands/commands.js +308 -0
- package/dist/config/config.d.ts +8 -0
- package/dist/config/config.js +311 -0
- package/dist/context/compaction.d.ts +54 -0
- package/dist/context/compaction.js +251 -0
- package/dist/context/custom-agents.d.ts +107 -0
- package/dist/context/custom-agents.js +397 -0
- package/dist/context/custom-tools.d.ts +120 -0
- package/dist/context/custom-tools.js +564 -0
- package/dist/context/git-checkpoint.d.ts +52 -0
- package/dist/context/git-checkpoint.js +240 -0
- package/dist/context/neuro-md.d.ts +48 -0
- package/dist/context/neuro-md.js +202 -0
- package/dist/context/neuroignore.d.ts +102 -0
- package/dist/context/neuroignore.js +441 -0
- package/dist/context/repo-map.d.ts +38 -0
- package/dist/context/repo-map.js +220 -0
- package/dist/context/skill-standard.d.ts +262 -0
- package/dist/context/skill-standard.js +1156 -0
- package/dist/context/skill-system.d.ts +75 -0
- package/dist/context/skill-system.js +578 -0
- package/dist/context/tree-sitter.d.ts +99 -0
- package/dist/context/tree-sitter.js +1956 -0
- package/dist/core/acp.d.ts +325 -0
- package/dist/core/acp.js +1498 -0
- package/dist/core/api-server.d.ts +143 -0
- package/dist/core/api-server.js +550 -0
- package/dist/core/approval.d.ts +81 -0
- package/dist/core/approval.js +432 -0
- package/dist/core/auto-compact.d.ts +127 -0
- package/dist/core/auto-compact.js +436 -0
- package/dist/core/auto-mode.d.ts +232 -0
- package/dist/core/auto-mode.js +831 -0
- package/dist/core/background-session.d.ts +166 -0
- package/dist/core/background-session.js +696 -0
- package/dist/core/cicd.d.ts +257 -0
- package/dist/core/cicd.js +1443 -0
- package/dist/core/cloud-sync.d.ts +156 -0
- package/dist/core/cloud-sync.js +582 -0
- package/dist/core/code-review.d.ts +132 -0
- package/dist/core/code-review.js +1191 -0
- package/dist/core/completion.d.ts +49 -0
- package/dist/core/completion.js +384 -0
- package/dist/core/context.d.ts +38 -0
- package/dist/core/context.js +144 -0
- package/dist/core/diff-preview.d.ts +35 -0
- package/dist/core/diff-preview.js +173 -0
- package/dist/core/doom-loop.d.ts +51 -0
- package/dist/core/doom-loop.js +179 -0
- package/dist/core/engine.d.ts +183 -0
- package/dist/core/engine.js +942 -0
- package/dist/core/extended-thinking.d.ts +103 -0
- package/dist/core/extended-thinking.js +269 -0
- package/dist/core/fallback.d.ts +54 -0
- package/dist/core/fallback.js +104 -0
- package/dist/core/git-worktree.d.ts +139 -0
- package/dist/core/git-worktree.js +614 -0
- package/dist/core/headless.d.ts +30 -0
- package/dist/core/headless.js +101 -0
- package/dist/core/i18n.d.ts +83 -0
- package/dist/core/i18n.js +739 -0
- package/dist/core/linting.d.ts +129 -0
- package/dist/core/linting.js +699 -0
- package/dist/core/model-router.d.ts +109 -0
- package/dist/core/model-router.js +581 -0
- package/dist/core/multi-model.d.ts +243 -0
- package/dist/core/multi-model.js +1099 -0
- package/dist/core/multi-session.d.ts +144 -0
- package/dist/core/multi-session.js +442 -0
- package/dist/core/multimodal.d.ts +125 -0
- package/dist/core/multimodal.js +286 -0
- package/dist/core/observability.d.ts +93 -0
- package/dist/core/observability.js +737 -0
- package/dist/core/os-sandbox.d.ts +122 -0
- package/dist/core/os-sandbox.js +1193 -0
- package/dist/core/outcome-grading.d.ts +228 -0
- package/dist/core/outcome-grading.js +1123 -0
- package/dist/core/output-styles.d.ts +57 -0
- package/dist/core/output-styles.js +382 -0
- package/dist/core/parallel-agents.d.ts +183 -0
- package/dist/core/parallel-agents.js +563 -0
- package/dist/core/plugin-bundle.d.ts +236 -0
- package/dist/core/plugin-bundle.js +887 -0
- package/dist/core/plugin-sdk.d.ts +139 -0
- package/dist/core/plugin-sdk.js +273 -0
- package/dist/core/prompt-cache.d.ts +163 -0
- package/dist/core/prompt-cache.js +599 -0
- package/dist/core/sandbox.d.ts +127 -0
- package/dist/core/sandbox.js +369 -0
- package/dist/core/scheduled-tasks.d.ts +151 -0
- package/dist/core/scheduled-tasks.js +502 -0
- package/dist/core/security-scanner.d.ts +160 -0
- package/dist/core/security-scanner.js +1494 -0
- package/dist/core/session.d.ts +83 -0
- package/dist/core/session.js +269 -0
- package/dist/core/shell-completion.d.ts +51 -0
- package/dist/core/shell-completion.js +674 -0
- package/dist/core/smart-monitor.d.ts +146 -0
- package/dist/core/smart-monitor.js +1199 -0
- package/dist/core/spec-driven.d.ts +233 -0
- package/dist/core/spec-driven.js +1485 -0
- package/dist/core/spending-warnings.d.ts +123 -0
- package/dist/core/spending-warnings.js +456 -0
- package/dist/core/sub-agent.d.ts +298 -0
- package/dist/core/sub-agent.js +1023 -0
- package/dist/core/telemetry.d.ts +157 -0
- package/dist/core/telemetry.js +412 -0
- package/dist/core/terminal-ux.d.ts +134 -0
- package/dist/core/terminal-ux.js +649 -0
- package/dist/core/testing.d.ts +146 -0
- package/dist/core/testing.js +1200 -0
- package/dist/core/types.d.ts +439 -0
- package/dist/core/types.js +6 -0
- package/dist/core/undo-redo.d.ts +112 -0
- package/dist/core/undo-redo.js +290 -0
- package/dist/core/updater.d.ts +159 -0
- package/dist/core/updater.js +608 -0
- package/dist/core/vim-mode.d.ts +151 -0
- package/dist/core/vim-mode.js +771 -0
- package/dist/core/voice.d.ts +137 -0
- package/dist/core/voice.js +538 -0
- package/dist/core/web-dashboard.d.ts +109 -0
- package/dist/core/web-dashboard.js +484 -0
- package/dist/hooks/hooks.d.ts +74 -0
- package/dist/hooks/hooks.js +160 -0
- package/dist/hooks/llm-evaluator.d.ts +165 -0
- package/dist/hooks/llm-evaluator.js +560 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +1186 -0
- package/dist/lsp/lsp-manager.d.ts +63 -0
- package/dist/lsp/lsp-manager.js +351 -0
- package/dist/mcp/client.d.ts +133 -0
- package/dist/mcp/client.js +684 -0
- package/dist/mcp/mcp-apps.d.ts +70 -0
- package/dist/mcp/mcp-apps.js +1007 -0
- package/dist/tools/bash.d.ts +5 -0
- package/dist/tools/bash.js +195 -0
- package/dist/tools/browser.d.ts +92 -0
- package/dist/tools/browser.js +1570 -0
- package/dist/tools/extended.d.ts +6 -0
- package/dist/tools/extended.js +191 -0
- package/dist/tools/file.d.ts +10 -0
- package/dist/tools/file.js +382 -0
- package/dist/tools/github.d.ts +389 -0
- package/dist/tools/github.js +759 -0
- package/dist/tools/index.d.ts +9 -0
- package/dist/tools/index.js +40 -0
- package/dist/tools/memory.d.ts +6 -0
- package/dist/tools/memory.js +197 -0
- package/dist/tools/registry.d.ts +29 -0
- package/dist/tools/registry.js +64 -0
- package/dist/tools/web.d.ts +6 -0
- package/dist/tools/web.js +150 -0
- package/dist/ui/renderer.d.ts +97 -0
- package/dist/ui/renderer.js +279 -0
- package/dist/ui/theme.d.ts +27 -0
- package/dist/ui/theme.js +106 -0
- 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
|