@yail259/overnight 0.1.0 → 0.2.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 +73 -16
- package/dist/cli.js +599 -118
- package/package.json +1 -1
- package/src/cli.ts +123 -19
- package/src/runner.ts +422 -45
- package/src/security.ts +162 -0
- package/src/types.ts +37 -4
package/package.json
CHANGED
package/src/cli.ts
CHANGED
|
@@ -6,17 +6,22 @@ import {
|
|
|
6
6
|
type JobConfig,
|
|
7
7
|
type JobResult,
|
|
8
8
|
type TasksFile,
|
|
9
|
+
type SecurityConfig,
|
|
9
10
|
DEFAULT_TIMEOUT,
|
|
10
11
|
DEFAULT_STALL_TIMEOUT,
|
|
11
12
|
DEFAULT_VERIFY_PROMPT,
|
|
12
13
|
DEFAULT_STATE_FILE,
|
|
13
14
|
DEFAULT_NTFY_TOPIC,
|
|
15
|
+
DEFAULT_MAX_TURNS,
|
|
16
|
+
DEFAULT_DENY_PATTERNS,
|
|
14
17
|
} from "./types.js";
|
|
18
|
+
import { validateSecurityConfig } from "./security.js";
|
|
15
19
|
import {
|
|
16
20
|
runJob,
|
|
17
21
|
runJobsWithState,
|
|
18
22
|
loadState,
|
|
19
23
|
resultsToJson,
|
|
24
|
+
taskKey,
|
|
20
25
|
} from "./runner.js";
|
|
21
26
|
import { sendNtfyNotification } from "./notify.js";
|
|
22
27
|
import { generateReport } from "./report.js";
|
|
@@ -120,14 +125,38 @@ overnight resume tasks.yaml
|
|
|
120
125
|
Run \`overnight <command> --help\` for command-specific options.
|
|
121
126
|
`;
|
|
122
127
|
|
|
123
|
-
|
|
128
|
+
interface ParsedConfig {
|
|
129
|
+
configs: JobConfig[];
|
|
130
|
+
security?: SecurityConfig;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function parseTasksFile(path: string, cliSecurity?: Partial<SecurityConfig>): ParsedConfig {
|
|
124
134
|
const content = readFileSync(path, "utf-8");
|
|
125
|
-
|
|
135
|
+
let data: TasksFile | (string | JobConfig)[];
|
|
136
|
+
try {
|
|
137
|
+
data = parseYaml(content) as TasksFile | (string | JobConfig)[];
|
|
138
|
+
} catch (e) {
|
|
139
|
+
const error = e as Error;
|
|
140
|
+
console.error(`\x1b[31mError parsing ${path}:\x1b[0m`);
|
|
141
|
+
console.error(` ${error.message.split('\n')[0]}`);
|
|
142
|
+
process.exit(1);
|
|
143
|
+
}
|
|
126
144
|
|
|
127
145
|
const tasks = Array.isArray(data) ? data : data.tasks ?? [];
|
|
128
146
|
const defaults = Array.isArray(data) ? {} : data.defaults ?? {};
|
|
129
147
|
|
|
130
|
-
|
|
148
|
+
// Merge CLI security options with file security options (CLI takes precedence)
|
|
149
|
+
const fileSecurity = (!Array.isArray(data) && data.defaults?.security) || {};
|
|
150
|
+
const security: SecurityConfig | undefined = (cliSecurity || Object.keys(fileSecurity).length > 0)
|
|
151
|
+
? {
|
|
152
|
+
...fileSecurity,
|
|
153
|
+
...cliSecurity,
|
|
154
|
+
// Use default deny patterns if none specified
|
|
155
|
+
deny_patterns: cliSecurity?.deny_patterns ?? fileSecurity.deny_patterns ?? DEFAULT_DENY_PATTERNS,
|
|
156
|
+
}
|
|
157
|
+
: undefined;
|
|
158
|
+
|
|
159
|
+
const configs = tasks.map((task) => {
|
|
131
160
|
if (typeof task === "string") {
|
|
132
161
|
return {
|
|
133
162
|
prompt: task,
|
|
@@ -137,9 +166,12 @@ function parseTasksFile(path: string): JobConfig[] {
|
|
|
137
166
|
verify: defaults.verify ?? true,
|
|
138
167
|
verify_prompt: defaults.verify_prompt ?? DEFAULT_VERIFY_PROMPT,
|
|
139
168
|
allowed_tools: defaults.allowed_tools,
|
|
169
|
+
security,
|
|
140
170
|
};
|
|
141
171
|
}
|
|
142
172
|
return {
|
|
173
|
+
id: task.id ?? undefined,
|
|
174
|
+
depends_on: task.depends_on ?? undefined,
|
|
143
175
|
prompt: task.prompt,
|
|
144
176
|
working_dir: task.working_dir ?? undefined,
|
|
145
177
|
timeout_seconds:
|
|
@@ -152,8 +184,11 @@ function parseTasksFile(path: string): JobConfig[] {
|
|
|
152
184
|
verify_prompt:
|
|
153
185
|
task.verify_prompt ?? defaults.verify_prompt ?? DEFAULT_VERIFY_PROMPT,
|
|
154
186
|
allowed_tools: task.allowed_tools ?? defaults.allowed_tools,
|
|
187
|
+
security: task.security ?? security,
|
|
155
188
|
};
|
|
156
189
|
});
|
|
190
|
+
|
|
191
|
+
return { configs, security };
|
|
157
192
|
}
|
|
158
193
|
|
|
159
194
|
function printSummary(results: JobResult[]): void {
|
|
@@ -190,7 +225,7 @@ const program = new Command();
|
|
|
190
225
|
program
|
|
191
226
|
.name("overnight")
|
|
192
227
|
.description("Batch job runner for Claude Code")
|
|
193
|
-
.version("0.
|
|
228
|
+
.version("0.2.0")
|
|
194
229
|
.action(() => {
|
|
195
230
|
console.log(AGENT_HELP);
|
|
196
231
|
});
|
|
@@ -205,26 +240,58 @@ program
|
|
|
205
240
|
.option("--notify", "Send push notification via ntfy.sh")
|
|
206
241
|
.option("--notify-topic <topic>", "ntfy.sh topic", DEFAULT_NTFY_TOPIC)
|
|
207
242
|
.option("-r, --report <file>", "Generate markdown report")
|
|
243
|
+
.option("--sandbox <dir>", "Sandbox directory (restrict file access)")
|
|
244
|
+
.option("--max-turns <n>", "Max agent iterations per task", String(DEFAULT_MAX_TURNS))
|
|
245
|
+
.option("--audit-log <file>", "Audit log file path")
|
|
246
|
+
.option("--no-security", "Disable default security (deny patterns)")
|
|
208
247
|
.action(async (tasksFile, opts) => {
|
|
209
248
|
if (!existsSync(tasksFile)) {
|
|
210
249
|
console.error(`Error: File not found: ${tasksFile}`);
|
|
211
250
|
process.exit(1);
|
|
212
251
|
}
|
|
213
252
|
|
|
214
|
-
|
|
253
|
+
// Build CLI security config
|
|
254
|
+
const cliSecurity: Partial<SecurityConfig> | undefined = opts.security === false
|
|
255
|
+
? undefined
|
|
256
|
+
: {
|
|
257
|
+
...(opts.sandbox && { sandbox_dir: opts.sandbox }),
|
|
258
|
+
...(opts.maxTurns && { max_turns: parseInt(opts.maxTurns, 10) }),
|
|
259
|
+
...(opts.auditLog && { audit_log: opts.auditLog }),
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
const { configs, security } = parseTasksFile(tasksFile, cliSecurity);
|
|
215
263
|
if (configs.length === 0) {
|
|
216
264
|
console.error("No tasks found in file");
|
|
217
265
|
process.exit(1);
|
|
218
266
|
}
|
|
219
267
|
|
|
220
|
-
|
|
268
|
+
// Check if resuming from existing state
|
|
269
|
+
const existingState = loadState(opts.stateFile ?? DEFAULT_STATE_FILE);
|
|
270
|
+
if (existingState) {
|
|
271
|
+
const done = Object.keys(existingState.completed).length;
|
|
272
|
+
const pending = configs.filter(c => !(taskKey(c) in existingState.completed)).length;
|
|
273
|
+
console.log(`\x1b[1movernight: Resuming — ${done} done, ${pending} remaining\x1b[0m`);
|
|
274
|
+
console.log(`\x1b[2mLast checkpoint: ${existingState.timestamp}\x1b[0m`);
|
|
275
|
+
} else {
|
|
276
|
+
console.log(`\x1b[1movernight: Running ${configs.length} jobs...\x1b[0m`);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Show security config if enabled
|
|
280
|
+
if (security && !opts.quiet) {
|
|
281
|
+
console.log("\x1b[2mSecurity:\x1b[0m");
|
|
282
|
+
validateSecurityConfig(security);
|
|
283
|
+
}
|
|
284
|
+
console.log("");
|
|
221
285
|
|
|
222
286
|
const log = opts.quiet ? undefined : (msg: string) => console.log(msg);
|
|
223
287
|
const startTime = Date.now();
|
|
224
288
|
|
|
289
|
+
const reloadConfigs = () => parseTasksFile(tasksFile, cliSecurity).configs;
|
|
290
|
+
|
|
225
291
|
const results = await runJobsWithState(configs, {
|
|
226
292
|
stateFile: opts.stateFile,
|
|
227
293
|
log,
|
|
294
|
+
reloadConfigs,
|
|
228
295
|
});
|
|
229
296
|
|
|
230
297
|
const totalDuration = (Date.now() - startTime) / 1000;
|
|
@@ -271,6 +338,10 @@ program
|
|
|
271
338
|
.option("--notify", "Send push notification via ntfy.sh")
|
|
272
339
|
.option("--notify-topic <topic>", "ntfy.sh topic", DEFAULT_NTFY_TOPIC)
|
|
273
340
|
.option("-r, --report <file>", "Generate markdown report")
|
|
341
|
+
.option("--sandbox <dir>", "Sandbox directory (restrict file access)")
|
|
342
|
+
.option("--max-turns <n>", "Max agent iterations per task", String(DEFAULT_MAX_TURNS))
|
|
343
|
+
.option("--audit-log <file>", "Audit log file path")
|
|
344
|
+
.option("--no-security", "Disable default security (deny patterns)")
|
|
274
345
|
.action(async (tasksFile, opts) => {
|
|
275
346
|
const stateFile = opts.stateFile ?? DEFAULT_STATE_FILE;
|
|
276
347
|
const state = loadState(stateFile);
|
|
@@ -286,33 +357,43 @@ program
|
|
|
286
357
|
process.exit(1);
|
|
287
358
|
}
|
|
288
359
|
|
|
289
|
-
|
|
360
|
+
// Build CLI security config
|
|
361
|
+
const cliSecurity: Partial<SecurityConfig> | undefined = opts.security === false
|
|
362
|
+
? undefined
|
|
363
|
+
: {
|
|
364
|
+
...(opts.sandbox && { sandbox_dir: opts.sandbox }),
|
|
365
|
+
...(opts.maxTurns && { max_turns: parseInt(opts.maxTurns, 10) }),
|
|
366
|
+
...(opts.auditLog && { audit_log: opts.auditLog }),
|
|
367
|
+
};
|
|
368
|
+
|
|
369
|
+
const { configs, security } = parseTasksFile(tasksFile, cliSecurity);
|
|
290
370
|
if (configs.length === 0) {
|
|
291
371
|
console.error("No tasks found in file");
|
|
292
372
|
process.exit(1);
|
|
293
373
|
}
|
|
294
374
|
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
`Task file has ${configs.length} jobs but state has ${state.total_jobs}`
|
|
298
|
-
);
|
|
299
|
-
process.exit(1);
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
const startIndex = state.completed_indices.length;
|
|
375
|
+
const completedCount = Object.keys(state.completed).length;
|
|
376
|
+
const pendingCount = configs.filter(c => !(taskKey(c) in state.completed)).length;
|
|
303
377
|
console.log(
|
|
304
|
-
`\x1b[1movernight: Resuming
|
|
378
|
+
`\x1b[1movernight: Resuming — ${completedCount} done, ${pendingCount} remaining\x1b[0m`
|
|
305
379
|
);
|
|
306
|
-
console.log(`\x1b[2mLast checkpoint: ${state.timestamp}\x1b[0m
|
|
380
|
+
console.log(`\x1b[2mLast checkpoint: ${state.timestamp}\x1b[0m`);
|
|
381
|
+
|
|
382
|
+
// Show security config if enabled
|
|
383
|
+
if (security && !opts.quiet) {
|
|
384
|
+
console.log("\x1b[2mSecurity:\x1b[0m");
|
|
385
|
+
validateSecurityConfig(security);
|
|
386
|
+
}
|
|
387
|
+
console.log("");
|
|
307
388
|
|
|
308
389
|
const log = opts.quiet ? undefined : (msg: string) => console.log(msg);
|
|
309
390
|
const startTime = Date.now();
|
|
391
|
+
const reloadConfigs = () => parseTasksFile(tasksFile, cliSecurity).configs;
|
|
310
392
|
|
|
311
393
|
const results = await runJobsWithState(configs, {
|
|
312
394
|
stateFile,
|
|
313
395
|
log,
|
|
314
|
-
|
|
315
|
-
priorResults: state.results,
|
|
396
|
+
reloadConfigs,
|
|
316
397
|
});
|
|
317
398
|
|
|
318
399
|
const totalDuration = (Date.now() - startTime) / 1000;
|
|
@@ -357,12 +438,25 @@ program
|
|
|
357
438
|
.option("--verify", "Run verification pass", true)
|
|
358
439
|
.option("--no-verify", "Skip verification pass")
|
|
359
440
|
.option("-T, --tools <tool...>", "Allowed tools")
|
|
441
|
+
.option("--sandbox <dir>", "Sandbox directory (restrict file access)")
|
|
442
|
+
.option("--max-turns <n>", "Max agent iterations", String(DEFAULT_MAX_TURNS))
|
|
443
|
+
.option("--no-security", "Disable default security (deny patterns)")
|
|
360
444
|
.action(async (prompt, opts) => {
|
|
445
|
+
// Build security config
|
|
446
|
+
const security: SecurityConfig | undefined = opts.security === false
|
|
447
|
+
? undefined
|
|
448
|
+
: {
|
|
449
|
+
...(opts.sandbox && { sandbox_dir: opts.sandbox }),
|
|
450
|
+
...(opts.maxTurns && { max_turns: parseInt(opts.maxTurns, 10) }),
|
|
451
|
+
deny_patterns: DEFAULT_DENY_PATTERNS,
|
|
452
|
+
};
|
|
453
|
+
|
|
361
454
|
const config: JobConfig = {
|
|
362
455
|
prompt,
|
|
363
456
|
timeout_seconds: parseInt(opts.timeout, 10),
|
|
364
457
|
verify: opts.verify,
|
|
365
458
|
allowed_tools: opts.tools,
|
|
459
|
+
security,
|
|
366
460
|
};
|
|
367
461
|
|
|
368
462
|
const log = (msg: string) => console.log(msg);
|
|
@@ -392,6 +486,7 @@ program
|
|
|
392
486
|
defaults:
|
|
393
487
|
timeout_seconds: 300 # 5 minutes per task
|
|
394
488
|
verify: true # Run verification after each task
|
|
489
|
+
|
|
395
490
|
# Secure defaults - no Bash, just file operations
|
|
396
491
|
allowed_tools:
|
|
397
492
|
- Read
|
|
@@ -400,6 +495,15 @@ defaults:
|
|
|
400
495
|
- Glob
|
|
401
496
|
- Grep
|
|
402
497
|
|
|
498
|
+
# Security settings (optional - deny_patterns enabled by default)
|
|
499
|
+
security:
|
|
500
|
+
sandbox_dir: "." # Restrict to current directory
|
|
501
|
+
max_turns: 100 # Prevent runaway agents
|
|
502
|
+
# audit_log: "overnight-audit.log" # Uncomment to enable
|
|
503
|
+
# deny_patterns: # Default patterns block .env, .key, .pem, etc.
|
|
504
|
+
# - "**/.env*"
|
|
505
|
+
# - "**/*.key"
|
|
506
|
+
|
|
403
507
|
tasks:
|
|
404
508
|
# Simple string format
|
|
405
509
|
- "Find and fix any TODO comments in the codebase"
|