@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/README.md +71 -225
- package/bin/overnight.js +2 -0
- package/dist/cli.js +103923 -23493
- package/package.json +27 -6
- package/bun.lock +0 -63
- package/src/cli.ts +0 -538
- package/src/notify.ts +0 -50
- package/src/report.ts +0 -115
- package/src/runner.ts +0 -660
- package/src/security.ts +0 -162
- package/src/types.ts +0 -81
- package/tsconfig.json +0 -15
package/package.json
CHANGED
|
@@ -1,18 +1,29 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yail259/overnight",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
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": "./
|
|
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": [
|
|
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/
|
|
36
|
+
"@anthropic-ai/sdk": "^0.80.0",
|
|
37
|
+
"cli-highlight": "2",
|
|
26
38
|
"commander": "^12.0.0",
|
|
27
|
-
"
|
|
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();
|