@yail259/overnight 0.2.0 → 1.0.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/package.json CHANGED
@@ -1,18 +1,29 @@
1
1
  {
2
2
  "name": "@yail259/overnight",
3
- "version": "0.2.0",
4
- "description": "Batch job runner for Claude Code - queue tasks, run overnight, wake up to results",
3
+ "version": "1.0.0",
4
+ "description": "another you for when you're asleep adaptive Claude Code message prediction",
5
5
  "type": "module",
6
6
  "bin": {
7
- "overnight": "./dist/cli.js"
7
+ "overnight": "./bin/overnight.js"
8
8
  },
9
9
  "repository": {
10
10
  "type": "git",
11
11
  "url": "https://github.com/yail259/overnight.git"
12
12
  },
13
- "keywords": ["claude", "claude-code", "batch", "jobs", "automation", "ai"],
13
+ "keywords": [
14
+ "claude",
15
+ "claude-code",
16
+ "prediction",
17
+ "autonomous",
18
+ "overnight"
19
+ ],
14
20
  "author": "yail259",
15
21
  "license": "MIT",
22
+ "files": [
23
+ "dist/",
24
+ "bin/",
25
+ "README.md"
26
+ ],
16
27
  "publishConfig": {
17
28
  "access": "public"
18
29
  },
@@ -22,12 +33,22 @@
22
33
  "dev": "bun run src/cli.ts"
23
34
  },
24
35
  "dependencies": {
25
- "@anthropic-ai/claude-agent-sdk": "^0.1.0",
36
+ "@anthropic-ai/sdk": "^0.80.0",
37
+ "cli-highlight": "2",
26
38
  "commander": "^12.0.0",
27
- "yaml": "^2.3.4"
39
+ "ink": "^6.8.0",
40
+ "ink-text-input": "^6.0.0",
41
+ "js-tiktoken": "^1.0.21",
42
+ "marked": "16",
43
+ "marked-terminal": "7",
44
+ "react": "^19.2.4",
45
+ "strip-ansi": "7",
46
+ "wrap-ansi": "9"
28
47
  },
29
48
  "devDependencies": {
30
49
  "@types/node": "^20.11.0",
50
+ "@types/react": "^19.2.14",
51
+ "react-devtools-core": "^7.0.1",
31
52
  "typescript": "^5.3.0"
32
53
  }
33
54
  }
package/bun.lock DELETED
@@ -1,63 +0,0 @@
1
- {
2
- "lockfileVersion": 1,
3
- "configVersion": 1,
4
- "workspaces": {
5
- "": {
6
- "name": "overnight",
7
- "dependencies": {
8
- "@anthropic-ai/claude-agent-sdk": "^0.1.0",
9
- "commander": "^12.0.0",
10
- "yaml": "^2.3.4",
11
- },
12
- "devDependencies": {
13
- "@types/node": "^20.11.0",
14
- "typescript": "^5.3.0",
15
- },
16
- },
17
- },
18
- "packages": {
19
- "@anthropic-ai/claude-agent-sdk": ["@anthropic-ai/claude-agent-sdk@0.1.77", "", { "optionalDependencies": { "@img/sharp-darwin-arm64": "^0.33.5", "@img/sharp-darwin-x64": "^0.33.5", "@img/sharp-linux-arm": "^0.33.5", "@img/sharp-linux-arm64": "^0.33.5", "@img/sharp-linux-x64": "^0.33.5", "@img/sharp-linuxmusl-arm64": "^0.33.5", "@img/sharp-linuxmusl-x64": "^0.33.5", "@img/sharp-win32-x64": "^0.33.5" }, "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" } }, "sha512-ZEjWQtkoB2MEY6K16DWMmF+8OhywAynH0m08V265cerbZ8xPD/2Ng2jPzbbO40mPeFSsMDJboShL+a3aObP0Jg=="],
20
-
21
- "@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.0.4" }, "os": "darwin", "cpu": "arm64" }, "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ=="],
22
-
23
- "@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.0.4" }, "os": "darwin", "cpu": "x64" }, "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q=="],
24
-
25
- "@img/sharp-libvips-darwin-arm64": ["@img/sharp-libvips-darwin-arm64@1.0.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg=="],
26
-
27
- "@img/sharp-libvips-darwin-x64": ["@img/sharp-libvips-darwin-x64@1.0.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ=="],
28
-
29
- "@img/sharp-libvips-linux-arm": ["@img/sharp-libvips-linux-arm@1.0.5", "", { "os": "linux", "cpu": "arm" }, "sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g=="],
30
-
31
- "@img/sharp-libvips-linux-arm64": ["@img/sharp-libvips-linux-arm64@1.0.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA=="],
32
-
33
- "@img/sharp-libvips-linux-x64": ["@img/sharp-libvips-linux-x64@1.0.4", "", { "os": "linux", "cpu": "x64" }, "sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw=="],
34
-
35
- "@img/sharp-libvips-linuxmusl-arm64": ["@img/sharp-libvips-linuxmusl-arm64@1.0.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA=="],
36
-
37
- "@img/sharp-libvips-linuxmusl-x64": ["@img/sharp-libvips-linuxmusl-x64@1.0.4", "", { "os": "linux", "cpu": "x64" }, "sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw=="],
38
-
39
- "@img/sharp-linux-arm": ["@img/sharp-linux-arm@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm": "1.0.5" }, "os": "linux", "cpu": "arm" }, "sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ=="],
40
-
41
- "@img/sharp-linux-arm64": ["@img/sharp-linux-arm64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm64": "1.0.4" }, "os": "linux", "cpu": "arm64" }, "sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA=="],
42
-
43
- "@img/sharp-linux-x64": ["@img/sharp-linux-x64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-x64": "1.0.4" }, "os": "linux", "cpu": "x64" }, "sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA=="],
44
-
45
- "@img/sharp-linuxmusl-arm64": ["@img/sharp-linuxmusl-arm64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-arm64": "1.0.4" }, "os": "linux", "cpu": "arm64" }, "sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g=="],
46
-
47
- "@img/sharp-linuxmusl-x64": ["@img/sharp-linuxmusl-x64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-x64": "1.0.4" }, "os": "linux", "cpu": "x64" }, "sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw=="],
48
-
49
- "@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.33.5", "", { "os": "win32", "cpu": "x64" }, "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg=="],
50
-
51
- "@types/node": ["@types/node@20.19.30", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-WJtwWJu7UdlvzEAUm484QNg5eAoq5QR08KDNx7g45Usrs2NtOPiX8ugDqmKdXkyL03rBqU5dYNYVQetEpBHq2g=="],
52
-
53
- "commander": ["commander@12.1.0", "", {}, "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA=="],
54
-
55
- "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
56
-
57
- "undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
58
-
59
- "yaml": ["yaml@2.8.2", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A=="],
60
-
61
- "zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="],
62
- }
63
- }
package/src/cli.ts DELETED
@@ -1,538 +0,0 @@
1
- #!/usr/bin/env node
2
- import { Command } from "commander";
3
- import { readFileSync, writeFileSync, existsSync } from "fs";
4
- import { parse as parseYaml } from "yaml";
5
- import {
6
- type JobConfig,
7
- type JobResult,
8
- type TasksFile,
9
- type SecurityConfig,
10
- DEFAULT_TIMEOUT,
11
- DEFAULT_STALL_TIMEOUT,
12
- DEFAULT_VERIFY_PROMPT,
13
- DEFAULT_STATE_FILE,
14
- DEFAULT_NTFY_TOPIC,
15
- DEFAULT_MAX_TURNS,
16
- DEFAULT_DENY_PATTERNS,
17
- } from "./types.js";
18
- import { validateSecurityConfig } from "./security.js";
19
- import {
20
- runJob,
21
- runJobsWithState,
22
- loadState,
23
- resultsToJson,
24
- taskKey,
25
- } from "./runner.js";
26
- import { sendNtfyNotification } from "./notify.js";
27
- import { generateReport } from "./report.js";
28
-
29
- const AGENT_HELP = `
30
- # overnight - Batch Job Runner for Claude Code
31
-
32
- Queue tasks, run them unattended, get results. Designed for overnight/AFK use.
33
-
34
- ## Quick Start
35
-
36
- \`\`\`bash
37
- # Create a tasks.yaml file
38
- overnight init
39
-
40
- # Run all tasks
41
- overnight run tasks.yaml
42
-
43
- # Run with notifications and report
44
- overnight run tasks.yaml --notify -r report.md
45
- \`\`\`
46
-
47
- ## Commands
48
-
49
- | Command | Description |
50
- |---------|-------------|
51
- | \`overnight run <file>\` | Run jobs from YAML file |
52
- | \`overnight resume <file>\` | Resume interrupted run from checkpoint |
53
- | \`overnight single "<prompt>"\` | Run a single task directly |
54
- | \`overnight init\` | Create example tasks.yaml |
55
-
56
- ## tasks.yaml Format
57
-
58
- \`\`\`yaml
59
- defaults:
60
- timeout_seconds: 300 # Per-task timeout (default: 300)
61
- verify: true # Run verification pass (default: true)
62
- allowed_tools: # Whitelist tools (default: Read,Edit,Write,Glob,Grep)
63
- - Read
64
- - Edit
65
- - Glob
66
- - Grep
67
-
68
- tasks:
69
- # Simple format
70
- - "Fix the bug in auth.py"
71
-
72
- # Detailed format
73
- - prompt: "Add input validation"
74
- timeout_seconds: 600
75
- verify: false
76
- allowed_tools: [Read, Edit, Bash, Glob, Grep]
77
- \`\`\`
78
-
79
- ## Key Options
80
-
81
- | Option | Description |
82
- |--------|-------------|
83
- | \`-o, --output <file>\` | Save results JSON |
84
- | \`-r, --report <file>\` | Generate markdown report |
85
- | \`-s, --state-file <file>\` | Custom checkpoint file |
86
- | \`--notify\` | Send push notification via ntfy.sh |
87
- | \`--notify-topic <topic>\` | ntfy.sh topic (default: overnight) |
88
- | \`-q, --quiet\` | Minimal output |
89
-
90
- ## Features
91
-
92
- 1. **Crash Recovery**: Auto-checkpoints after each job. Use \`overnight resume\` to continue.
93
- 2. **Retry Logic**: Auto-retries 3x on API/network errors with exponential backoff.
94
- 3. **Notifications**: \`--notify\` sends summary to ntfy.sh (free, no signup).
95
- 4. **Reports**: \`-r report.md\` generates markdown summary with next steps.
96
- 5. **Security**: No Bash by default. Whitelist tools per-task.
97
-
98
- ## Example Workflows
99
-
100
- \`\`\`bash
101
- # Development: run overnight, check in morning
102
- nohup overnight run tasks.yaml --notify -r report.md -o results.json > overnight.log 2>&1 &
103
-
104
- # CI/CD: run and fail if any task fails
105
- overnight run tasks.yaml -q
106
-
107
- # Single task with Bash access
108
- overnight single "Run tests and fix failures" -T Read -T Edit -T Bash -T Glob
109
-
110
- # Resume after crash/interrupt
111
- overnight resume tasks.yaml
112
- \`\`\`
113
-
114
- ## Exit Codes
115
-
116
- - 0: All tasks succeeded
117
- - 1: One or more tasks failed
118
-
119
- ## Files Created
120
-
121
- - \`.overnight-state.json\` - Checkpoint file (deleted on success)
122
- - \`report.md\` - Summary report (if -r used)
123
- - \`results.json\` - Full results (if -o used)
124
-
125
- Run \`overnight <command> --help\` for command-specific options.
126
- `;
127
-
128
- interface ParsedConfig {
129
- configs: JobConfig[];
130
- security?: SecurityConfig;
131
- }
132
-
133
- function parseTasksFile(path: string, cliSecurity?: Partial<SecurityConfig>): ParsedConfig {
134
- const content = readFileSync(path, "utf-8");
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
- }
144
-
145
- const tasks = Array.isArray(data) ? data : data.tasks ?? [];
146
- const defaults = Array.isArray(data) ? {} : data.defaults ?? {};
147
-
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) => {
160
- if (typeof task === "string") {
161
- return {
162
- prompt: task,
163
- timeout_seconds: defaults.timeout_seconds ?? DEFAULT_TIMEOUT,
164
- stall_timeout_seconds:
165
- defaults.stall_timeout_seconds ?? DEFAULT_STALL_TIMEOUT,
166
- verify: defaults.verify ?? true,
167
- verify_prompt: defaults.verify_prompt ?? DEFAULT_VERIFY_PROMPT,
168
- allowed_tools: defaults.allowed_tools,
169
- security,
170
- };
171
- }
172
- return {
173
- id: task.id ?? undefined,
174
- depends_on: task.depends_on ?? undefined,
175
- prompt: task.prompt,
176
- working_dir: task.working_dir ?? undefined,
177
- timeout_seconds:
178
- task.timeout_seconds ?? defaults.timeout_seconds ?? DEFAULT_TIMEOUT,
179
- stall_timeout_seconds:
180
- task.stall_timeout_seconds ??
181
- defaults.stall_timeout_seconds ??
182
- DEFAULT_STALL_TIMEOUT,
183
- verify: task.verify ?? defaults.verify ?? true,
184
- verify_prompt:
185
- task.verify_prompt ?? defaults.verify_prompt ?? DEFAULT_VERIFY_PROMPT,
186
- allowed_tools: task.allowed_tools ?? defaults.allowed_tools,
187
- security: task.security ?? security,
188
- };
189
- });
190
-
191
- return { configs, security };
192
- }
193
-
194
- function printSummary(results: JobResult[]): void {
195
- const statusColors: Record<string, string> = {
196
- success: "\x1b[32m",
197
- failed: "\x1b[31m",
198
- timeout: "\x1b[33m",
199
- stalled: "\x1b[35m",
200
- verification_failed: "\x1b[33m",
201
- };
202
- const reset = "\x1b[0m";
203
- const bold = "\x1b[1m";
204
-
205
- console.log(`\n${bold}Job Results${reset}`);
206
- console.log("─".repeat(70));
207
-
208
- results.forEach((r, i) => {
209
- const color = statusColors[r.status] ?? "";
210
- const task = r.task.length > 40 ? r.task.slice(0, 40) + "..." : r.task;
211
- const verified = r.verified ? "✓" : "✗";
212
- console.log(
213
- `${i + 1}. ${color}${r.status.padEnd(12)}${reset} ${r.duration_seconds.toFixed(1).padStart(6)}s ${verified} ${task}`
214
- );
215
- });
216
-
217
- const succeeded = results.filter((r) => r.status === "success").length;
218
- console.log(
219
- `\n${bold}Summary:${reset} ${succeeded}/${results.length} succeeded`
220
- );
221
- }
222
-
223
- const program = new Command();
224
-
225
- program
226
- .name("overnight")
227
- .description("Batch job runner for Claude Code")
228
- .version("0.2.0")
229
- .action(() => {
230
- console.log(AGENT_HELP);
231
- });
232
-
233
- program
234
- .command("run")
235
- .description("Run jobs from a YAML tasks file")
236
- .argument("<tasks-file>", "Path to tasks.yaml file")
237
- .option("-o, --output <file>", "Output file for results JSON")
238
- .option("-q, --quiet", "Minimal output")
239
- .option("-s, --state-file <file>", "Custom state file path")
240
- .option("--notify", "Send push notification via ntfy.sh")
241
- .option("--notify-topic <topic>", "ntfy.sh topic", DEFAULT_NTFY_TOPIC)
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)")
247
- .action(async (tasksFile, opts) => {
248
- if (!existsSync(tasksFile)) {
249
- console.error(`Error: File not found: ${tasksFile}`);
250
- process.exit(1);
251
- }
252
-
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);
263
- if (configs.length === 0) {
264
- console.error("No tasks found in file");
265
- process.exit(1);
266
- }
267
-
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("");
285
-
286
- const log = opts.quiet ? undefined : (msg: string) => console.log(msg);
287
- const startTime = Date.now();
288
-
289
- const reloadConfigs = () => parseTasksFile(tasksFile, cliSecurity).configs;
290
-
291
- const results = await runJobsWithState(configs, {
292
- stateFile: opts.stateFile,
293
- log,
294
- reloadConfigs,
295
- });
296
-
297
- const totalDuration = (Date.now() - startTime) / 1000;
298
-
299
- if (opts.notify) {
300
- const success = await sendNtfyNotification(
301
- results,
302
- totalDuration,
303
- opts.notifyTopic
304
- );
305
- if (success) {
306
- console.log(`\x1b[2mNotification sent to ntfy.sh/${opts.notifyTopic}\x1b[0m`);
307
- } else {
308
- console.log("\x1b[33mWarning: Failed to send notification\x1b[0m");
309
- }
310
- }
311
-
312
- if (opts.report) {
313
- generateReport(results, totalDuration, opts.report);
314
- console.log(`\x1b[2mReport saved to ${opts.report}\x1b[0m`);
315
- }
316
-
317
- if (!opts.quiet) {
318
- printSummary(results);
319
- }
320
-
321
- if (opts.output) {
322
- writeFileSync(opts.output, resultsToJson(results));
323
- console.log(`\n\x1b[2mResults saved to ${opts.output}\x1b[0m`);
324
- }
325
-
326
- if (results.some((r) => r.status !== "success")) {
327
- process.exit(1);
328
- }
329
- });
330
-
331
- program
332
- .command("resume")
333
- .description("Resume a previous run from saved state")
334
- .argument("<tasks-file>", "Path to tasks.yaml file")
335
- .option("-o, --output <file>", "Output file for results JSON")
336
- .option("-q, --quiet", "Minimal output")
337
- .option("-s, --state-file <file>", "Custom state file path")
338
- .option("--notify", "Send push notification via ntfy.sh")
339
- .option("--notify-topic <topic>", "ntfy.sh topic", DEFAULT_NTFY_TOPIC)
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)")
345
- .action(async (tasksFile, opts) => {
346
- const stateFile = opts.stateFile ?? DEFAULT_STATE_FILE;
347
- const state = loadState(stateFile);
348
-
349
- if (!state) {
350
- console.error(`No state file found at ${stateFile}`);
351
- console.error("Run 'overnight run' first to start jobs.");
352
- process.exit(1);
353
- }
354
-
355
- if (!existsSync(tasksFile)) {
356
- console.error(`Error: File not found: ${tasksFile}`);
357
- process.exit(1);
358
- }
359
-
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);
370
- if (configs.length === 0) {
371
- console.error("No tasks found in file");
372
- process.exit(1);
373
- }
374
-
375
- const completedCount = Object.keys(state.completed).length;
376
- const pendingCount = configs.filter(c => !(taskKey(c) in state.completed)).length;
377
- console.log(
378
- `\x1b[1movernight: Resuming — ${completedCount} done, ${pendingCount} remaining\x1b[0m`
379
- );
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("");
388
-
389
- const log = opts.quiet ? undefined : (msg: string) => console.log(msg);
390
- const startTime = Date.now();
391
- const reloadConfigs = () => parseTasksFile(tasksFile, cliSecurity).configs;
392
-
393
- const results = await runJobsWithState(configs, {
394
- stateFile,
395
- log,
396
- reloadConfigs,
397
- });
398
-
399
- const totalDuration = (Date.now() - startTime) / 1000;
400
-
401
- if (opts.notify) {
402
- const success = await sendNtfyNotification(
403
- results,
404
- totalDuration,
405
- opts.notifyTopic
406
- );
407
- if (success) {
408
- console.log(`\x1b[2mNotification sent to ntfy.sh/${opts.notifyTopic}\x1b[0m`);
409
- } else {
410
- console.log("\x1b[33mWarning: Failed to send notification\x1b[0m");
411
- }
412
- }
413
-
414
- if (opts.report) {
415
- generateReport(results, totalDuration, opts.report);
416
- console.log(`\x1b[2mReport saved to ${opts.report}\x1b[0m`);
417
- }
418
-
419
- if (!opts.quiet) {
420
- printSummary(results);
421
- }
422
-
423
- if (opts.output) {
424
- writeFileSync(opts.output, resultsToJson(results));
425
- console.log(`\n\x1b[2mResults saved to ${opts.output}\x1b[0m`);
426
- }
427
-
428
- if (results.some((r) => r.status !== "success")) {
429
- process.exit(1);
430
- }
431
- });
432
-
433
- program
434
- .command("single")
435
- .description("Run a single job directly")
436
- .argument("<prompt>", "The task prompt")
437
- .option("-t, --timeout <seconds>", "Timeout in seconds", "300")
438
- .option("--verify", "Run verification pass", true)
439
- .option("--no-verify", "Skip verification pass")
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)")
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
-
454
- const config: JobConfig = {
455
- prompt,
456
- timeout_seconds: parseInt(opts.timeout, 10),
457
- verify: opts.verify,
458
- allowed_tools: opts.tools,
459
- security,
460
- };
461
-
462
- const log = (msg: string) => console.log(msg);
463
- const result = await runJob(config, log);
464
-
465
- if (result.status === "success") {
466
- console.log("\n\x1b[32mSuccess\x1b[0m");
467
- if (result.result) {
468
- console.log(result.result);
469
- }
470
- } else {
471
- console.log(`\n\x1b[31m${result.status}\x1b[0m`);
472
- if (result.error) {
473
- console.log(`\x1b[31m${result.error}\x1b[0m`);
474
- }
475
- process.exit(1);
476
- }
477
- });
478
-
479
- program
480
- .command("init")
481
- .description("Create an example tasks.yaml file")
482
- .action(() => {
483
- const example = `# overnight task file
484
- # Run with: overnight run tasks.yaml
485
-
486
- defaults:
487
- timeout_seconds: 300 # 5 minutes per task
488
- verify: true # Run verification after each task
489
-
490
- # Secure defaults - no Bash, just file operations
491
- allowed_tools:
492
- - Read
493
- - Edit
494
- - Write
495
- - Glob
496
- - Grep
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
-
507
- tasks:
508
- # Simple string format
509
- - "Find and fix any TODO comments in the codebase"
510
-
511
- # Dict format with overrides
512
- - prompt: "Add input validation to all form handlers"
513
- timeout_seconds: 600 # Allow more time
514
-
515
- - prompt: "Review code for security issues"
516
- verify: false # Don't need to verify a review
517
-
518
- # Can add Bash for specific tasks that need it
519
- - prompt: "Run the test suite and fix any failures"
520
- allowed_tools:
521
- - Read
522
- - Edit
523
- - Bash
524
- - Glob
525
- - Grep
526
- `;
527
-
528
- if (existsSync("tasks.yaml")) {
529
- console.log("\x1b[33mtasks.yaml already exists\x1b[0m");
530
- process.exit(1);
531
- }
532
-
533
- writeFileSync("tasks.yaml", example);
534
- console.log("\x1b[32mCreated tasks.yaml\x1b[0m");
535
- console.log("Edit the file, then run: \x1b[1movernight run tasks.yaml\x1b[0m");
536
- });
537
-
538
- program.parse();