@yail259/overnight 0.3.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 +103919 -24513
- package/package.json +27 -6
- package/.context/notes.md +0 -0
- package/.context/todos.md +0 -0
- package/bun.lock +0 -63
- package/src/cli.ts +0 -821
- package/src/goal-runner.ts +0 -709
- package/src/notify.ts +0 -50
- package/src/planner.ts +0 -238
- package/src/report.ts +0 -115
- package/src/runner.ts +0 -663
- package/src/security.ts +0 -162
- package/src/types.ts +0 -129
- package/tsconfig.json +0 -15
package/src/notify.ts
DELETED
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
import { type JobResult, DEFAULT_NTFY_TOPIC } from "./types.js";
|
|
2
|
-
|
|
3
|
-
export async function sendNtfyNotification(
|
|
4
|
-
results: JobResult[],
|
|
5
|
-
totalDuration: number,
|
|
6
|
-
topic: string = DEFAULT_NTFY_TOPIC
|
|
7
|
-
): Promise<boolean> {
|
|
8
|
-
const succeeded = results.filter((r) => r.status === "success").length;
|
|
9
|
-
const failed = results.length - succeeded;
|
|
10
|
-
|
|
11
|
-
// Format duration
|
|
12
|
-
let durationStr: string;
|
|
13
|
-
if (totalDuration >= 3600) {
|
|
14
|
-
const hours = Math.floor(totalDuration / 3600);
|
|
15
|
-
const mins = Math.floor((totalDuration % 3600) / 60);
|
|
16
|
-
durationStr = `${hours}h ${mins}m`;
|
|
17
|
-
} else if (totalDuration >= 60) {
|
|
18
|
-
const mins = Math.floor(totalDuration / 60);
|
|
19
|
-
const secs = Math.floor(totalDuration % 60);
|
|
20
|
-
durationStr = `${mins}m ${secs}s`;
|
|
21
|
-
} else {
|
|
22
|
-
durationStr = `${totalDuration.toFixed(0)}s`;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
const title =
|
|
26
|
-
failed === 0
|
|
27
|
-
? `overnight: ${succeeded}/${results.length} succeeded`
|
|
28
|
-
: `overnight: ${failed} failed`;
|
|
29
|
-
|
|
30
|
-
const message = `Completed in ${durationStr}\n${succeeded} succeeded, ${failed} failed`;
|
|
31
|
-
|
|
32
|
-
const priority = failed === 0 ? "default" : "high";
|
|
33
|
-
const tags = failed === 0 ? "white_check_mark" : "warning";
|
|
34
|
-
|
|
35
|
-
try {
|
|
36
|
-
const response = await fetch(`https://ntfy.sh/${topic}`, {
|
|
37
|
-
method: "POST",
|
|
38
|
-
headers: {
|
|
39
|
-
Title: title,
|
|
40
|
-
Priority: priority,
|
|
41
|
-
Tags: tags,
|
|
42
|
-
},
|
|
43
|
-
body: message,
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
return response.ok;
|
|
47
|
-
} catch {
|
|
48
|
-
return false;
|
|
49
|
-
}
|
|
50
|
-
}
|
package/src/planner.ts
DELETED
|
@@ -1,238 +0,0 @@
|
|
|
1
|
-
import { query, type Options as ClaudeCodeOptions } from "@anthropic-ai/claude-agent-sdk";
|
|
2
|
-
import { readFileSync, writeFileSync, existsSync } from "fs";
|
|
3
|
-
import { execSync } from "child_process";
|
|
4
|
-
import { stringify as stringifyYaml } from "yaml";
|
|
5
|
-
import * as readline from "readline";
|
|
6
|
-
import {
|
|
7
|
-
type GoalConfig,
|
|
8
|
-
DEFAULT_TOOLS,
|
|
9
|
-
DEFAULT_TIMEOUT,
|
|
10
|
-
DEFAULT_MAX_TURNS,
|
|
11
|
-
DEFAULT_MAX_ITERATIONS,
|
|
12
|
-
DEFAULT_CONVERGENCE_THRESHOLD,
|
|
13
|
-
DEFAULT_DENY_PATTERNS,
|
|
14
|
-
} from "./types.js";
|
|
15
|
-
|
|
16
|
-
type LogCallback = (msg: string) => void;
|
|
17
|
-
|
|
18
|
-
// --- Claude executable ---
|
|
19
|
-
|
|
20
|
-
let claudeExecutablePath: string | undefined;
|
|
21
|
-
|
|
22
|
-
function findClaudeExecutable(): string | undefined {
|
|
23
|
-
if (claudeExecutablePath !== undefined) return claudeExecutablePath;
|
|
24
|
-
if (process.env.CLAUDE_CODE_PATH) {
|
|
25
|
-
claudeExecutablePath = process.env.CLAUDE_CODE_PATH;
|
|
26
|
-
return claudeExecutablePath;
|
|
27
|
-
}
|
|
28
|
-
try {
|
|
29
|
-
const cmd = process.platform === "win32" ? "where claude" : "which claude";
|
|
30
|
-
claudeExecutablePath = execSync(cmd, { encoding: "utf-8" }).trim().split("\n")[0];
|
|
31
|
-
return claudeExecutablePath;
|
|
32
|
-
} catch {
|
|
33
|
-
const commonPaths = [
|
|
34
|
-
"/usr/local/bin/claude",
|
|
35
|
-
"/opt/homebrew/bin/claude",
|
|
36
|
-
`${process.env.HOME}/.local/bin/claude`,
|
|
37
|
-
];
|
|
38
|
-
for (const p of commonPaths) {
|
|
39
|
-
if (existsSync(p)) {
|
|
40
|
-
claudeExecutablePath = p;
|
|
41
|
-
return claudeExecutablePath;
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
return undefined;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
// --- Interactive prompting ---
|
|
49
|
-
|
|
50
|
-
function createReadline(): readline.Interface {
|
|
51
|
-
return readline.createInterface({
|
|
52
|
-
input: process.stdin,
|
|
53
|
-
output: process.stdout,
|
|
54
|
-
});
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
function ask(rl: readline.Interface, question: string): Promise<string> {
|
|
58
|
-
return new Promise((resolve) => {
|
|
59
|
-
rl.question(question, (answer) => resolve(answer.trim()));
|
|
60
|
-
});
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
// --- Planner ---
|
|
64
|
-
|
|
65
|
-
const PLANNER_SYSTEM_PROMPT = `You are an expert software architect helping plan an autonomous overnight build.
|
|
66
|
-
|
|
67
|
-
Your job is to have a focused design conversation with the user, then produce a goal.yaml file that an autonomous build agent will use to implement the project overnight.
|
|
68
|
-
|
|
69
|
-
Guidelines:
|
|
70
|
-
- Ask clarifying questions about scope, technology choices, priorities, and constraints
|
|
71
|
-
- Keep the conversation focused and efficient — 3-5 rounds max
|
|
72
|
-
- When you have enough information, produce the goal.yaml
|
|
73
|
-
- The goal.yaml should be specific enough for an agent to work autonomously
|
|
74
|
-
- Include concrete acceptance criteria that can be verified
|
|
75
|
-
- Include verification commands when possible (build, test, lint)
|
|
76
|
-
- Set realistic constraints
|
|
77
|
-
|
|
78
|
-
When you're ready to produce the final plan, output it in this format:
|
|
79
|
-
|
|
80
|
-
\`\`\`yaml
|
|
81
|
-
goal: "Clear description of what to build"
|
|
82
|
-
|
|
83
|
-
acceptance_criteria:
|
|
84
|
-
- "Specific, verifiable criterion 1"
|
|
85
|
-
- "Specific, verifiable criterion 2"
|
|
86
|
-
|
|
87
|
-
verification_commands:
|
|
88
|
-
- "npm run build"
|
|
89
|
-
- "npm test"
|
|
90
|
-
|
|
91
|
-
constraints:
|
|
92
|
-
- "Don't modify existing API contracts"
|
|
93
|
-
|
|
94
|
-
max_iterations: 15
|
|
95
|
-
convergence_threshold: 3
|
|
96
|
-
|
|
97
|
-
defaults:
|
|
98
|
-
timeout_seconds: 600
|
|
99
|
-
allowed_tools:
|
|
100
|
-
- Read
|
|
101
|
-
- Edit
|
|
102
|
-
- Write
|
|
103
|
-
- Glob
|
|
104
|
-
- Grep
|
|
105
|
-
- Bash
|
|
106
|
-
security:
|
|
107
|
-
sandbox_dir: "."
|
|
108
|
-
max_turns: 150
|
|
109
|
-
\`\`\`
|
|
110
|
-
|
|
111
|
-
IMPORTANT: Only output the yaml block when you and the user agree the plan is ready. Before that, ask questions and discuss.`;
|
|
112
|
-
|
|
113
|
-
export async function runPlanner(
|
|
114
|
-
initialGoal: string,
|
|
115
|
-
options: {
|
|
116
|
-
outputFile?: string;
|
|
117
|
-
log?: LogCallback;
|
|
118
|
-
} = {}
|
|
119
|
-
): Promise<GoalConfig | null> {
|
|
120
|
-
const log = options.log ?? ((msg: string) => console.log(msg));
|
|
121
|
-
const outputFile = options.outputFile ?? "goal.yaml";
|
|
122
|
-
const claudePath = findClaudeExecutable();
|
|
123
|
-
|
|
124
|
-
if (!claudePath) {
|
|
125
|
-
log("\x1b[31m✗ Error: Could not find 'claude' CLI.\x1b[0m");
|
|
126
|
-
return null;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
log("\x1b[1movernight plan: Interactive design session\x1b[0m");
|
|
130
|
-
log("\x1b[2mDescribe your goal and I'll help shape it into a plan.\x1b[0m");
|
|
131
|
-
log("\x1b[2mType 'done' to finalize, 'quit' to abort.\x1b[0m\n");
|
|
132
|
-
|
|
133
|
-
const rl = createReadline();
|
|
134
|
-
const conversationHistory: Array<{ role: "user" | "assistant"; content: string }> = [];
|
|
135
|
-
|
|
136
|
-
// First turn: send the initial goal to the planner
|
|
137
|
-
let currentPrompt = `The user wants to plan the following project for an overnight autonomous build:\n\n${initialGoal}\n\nAsk clarifying questions to understand scope, tech choices, priorities, and constraints. Be concise.`;
|
|
138
|
-
|
|
139
|
-
try {
|
|
140
|
-
let sessionId: string | undefined;
|
|
141
|
-
|
|
142
|
-
for (let round = 0; round < 10; round++) {
|
|
143
|
-
// Run Claude
|
|
144
|
-
const sdkOptions: ClaudeCodeOptions = {
|
|
145
|
-
allowedTools: ["Read", "Glob", "Grep"], // Read-only for planning
|
|
146
|
-
systemPrompt: PLANNER_SYSTEM_PROMPT,
|
|
147
|
-
permissionMode: "acceptEdits",
|
|
148
|
-
pathToClaudeCodeExecutable: claudePath,
|
|
149
|
-
...(sessionId && { resume: sessionId }),
|
|
150
|
-
};
|
|
151
|
-
|
|
152
|
-
let result: string | undefined;
|
|
153
|
-
|
|
154
|
-
const conversation = query({ prompt: currentPrompt, options: sdkOptions });
|
|
155
|
-
for await (const message of conversation) {
|
|
156
|
-
if (message.type === "result") {
|
|
157
|
-
sessionId = message.session_id;
|
|
158
|
-
if (message.subtype === "success") {
|
|
159
|
-
result = message.result;
|
|
160
|
-
}
|
|
161
|
-
} else if (message.type === "system" && "subtype" in message) {
|
|
162
|
-
if (message.subtype === "init") {
|
|
163
|
-
sessionId = message.session_id;
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
if (!result) {
|
|
169
|
-
log("\x1b[31m✗ No response from planner\x1b[0m");
|
|
170
|
-
break;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
conversationHistory.push({ role: "assistant", content: result });
|
|
174
|
-
|
|
175
|
-
// Check if the planner produced a goal.yaml
|
|
176
|
-
const yamlMatch = result.match(/```yaml\n([\s\S]*?)\n```/);
|
|
177
|
-
if (yamlMatch) {
|
|
178
|
-
// Show the plan
|
|
179
|
-
log("\n\x1b[1m━━━ Proposed Plan ━━━\x1b[0m\n");
|
|
180
|
-
log(yamlMatch[1]);
|
|
181
|
-
log("\n\x1b[1m━━━━━━━━━━━━━━━━━━━━\x1b[0m\n");
|
|
182
|
-
|
|
183
|
-
const answer = await ask(rl, "\x1b[36m?\x1b[0m Accept this plan? (yes/no/revise): ");
|
|
184
|
-
|
|
185
|
-
if (answer.toLowerCase() === "yes" || answer.toLowerCase() === "y") {
|
|
186
|
-
// Write the goal.yaml
|
|
187
|
-
writeFileSync(outputFile, yamlMatch[1]);
|
|
188
|
-
log(`\n\x1b[32m✓ Plan saved to ${outputFile}\x1b[0m`);
|
|
189
|
-
log(`Run with: \x1b[1movernight run ${outputFile}\x1b[0m`);
|
|
190
|
-
rl.close();
|
|
191
|
-
|
|
192
|
-
// Parse and return
|
|
193
|
-
const { parse: parseYaml } = await import("yaml");
|
|
194
|
-
return parseYaml(yamlMatch[1]) as GoalConfig;
|
|
195
|
-
} else if (answer.toLowerCase() === "quit" || answer.toLowerCase() === "q") {
|
|
196
|
-
log("\x1b[33mAborted\x1b[0m");
|
|
197
|
-
rl.close();
|
|
198
|
-
return null;
|
|
199
|
-
} else {
|
|
200
|
-
// User wants revisions
|
|
201
|
-
const revision = await ask(rl, "\x1b[36m?\x1b[0m What would you like to change? ");
|
|
202
|
-
currentPrompt = revision;
|
|
203
|
-
conversationHistory.push({ role: "user", content: revision });
|
|
204
|
-
continue;
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
// Show the assistant's response
|
|
209
|
-
log(`\n\x1b[2m─── Planner ───\x1b[0m\n`);
|
|
210
|
-
log(result);
|
|
211
|
-
log("");
|
|
212
|
-
|
|
213
|
-
// Get user input
|
|
214
|
-
const userInput = await ask(rl, "\x1b[36m>\x1b[0m ");
|
|
215
|
-
|
|
216
|
-
if (userInput.toLowerCase() === "done") {
|
|
217
|
-
// Ask the planner to finalize
|
|
218
|
-
currentPrompt = "The user is satisfied. Please produce the final goal.yaml now based on our discussion.";
|
|
219
|
-
conversationHistory.push({ role: "user", content: currentPrompt });
|
|
220
|
-
continue;
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
if (userInput.toLowerCase() === "quit" || userInput.toLowerCase() === "q") {
|
|
224
|
-
log("\x1b[33mAborted\x1b[0m");
|
|
225
|
-
rl.close();
|
|
226
|
-
return null;
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
currentPrompt = userInput;
|
|
230
|
-
conversationHistory.push({ role: "user", content: userInput });
|
|
231
|
-
}
|
|
232
|
-
} finally {
|
|
233
|
-
rl.close();
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
log("\x1b[33m⚠ Design session ended without producing a plan\x1b[0m");
|
|
237
|
-
return null;
|
|
238
|
-
}
|
package/src/report.ts
DELETED
|
@@ -1,115 +0,0 @@
|
|
|
1
|
-
import { writeFileSync } from "fs";
|
|
2
|
-
import { type JobResult } from "./types.js";
|
|
3
|
-
|
|
4
|
-
export function generateReport(
|
|
5
|
-
results: JobResult[],
|
|
6
|
-
totalDuration: number,
|
|
7
|
-
outputPath?: string
|
|
8
|
-
): string {
|
|
9
|
-
const lines: string[] = [];
|
|
10
|
-
|
|
11
|
-
// Header
|
|
12
|
-
lines.push("# Overnight Run Report");
|
|
13
|
-
lines.push("");
|
|
14
|
-
lines.push(`**Generated:** ${new Date().toISOString().replace("T", " ").split(".")[0]}`);
|
|
15
|
-
lines.push("");
|
|
16
|
-
|
|
17
|
-
// Summary
|
|
18
|
-
const succeeded = results.filter((r) => r.status === "success").length;
|
|
19
|
-
const failed = results.length - succeeded;
|
|
20
|
-
|
|
21
|
-
lines.push("## Summary");
|
|
22
|
-
lines.push("");
|
|
23
|
-
lines.push(`- **Jobs:** ${succeeded}/${results.length} succeeded`);
|
|
24
|
-
if (failed > 0) {
|
|
25
|
-
lines.push(`- **Failed:** ${failed}`);
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
// Duration formatting
|
|
29
|
-
let durationStr: string;
|
|
30
|
-
if (totalDuration >= 3600) {
|
|
31
|
-
const hours = Math.floor(totalDuration / 3600);
|
|
32
|
-
const mins = Math.floor((totalDuration % 3600) / 60);
|
|
33
|
-
durationStr = `${hours}h ${mins}m`;
|
|
34
|
-
} else if (totalDuration >= 60) {
|
|
35
|
-
const mins = Math.floor(totalDuration / 60);
|
|
36
|
-
const secs = Math.floor(totalDuration % 60);
|
|
37
|
-
durationStr = `${mins}m ${secs}s`;
|
|
38
|
-
} else {
|
|
39
|
-
durationStr = `${totalDuration.toFixed(1)}s`;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
lines.push(`- **Total duration:** ${durationStr}`);
|
|
43
|
-
lines.push("");
|
|
44
|
-
|
|
45
|
-
// Job details table
|
|
46
|
-
lines.push("## Job Results");
|
|
47
|
-
lines.push("");
|
|
48
|
-
lines.push("| # | Status | Duration | Task |");
|
|
49
|
-
lines.push("|---|--------|----------|------|");
|
|
50
|
-
|
|
51
|
-
const statusEmoji: Record<string, string> = {
|
|
52
|
-
success: "✅",
|
|
53
|
-
failed: "❌",
|
|
54
|
-
timeout: "⏱️",
|
|
55
|
-
stalled: "🔄",
|
|
56
|
-
verification_failed: "⚠️",
|
|
57
|
-
};
|
|
58
|
-
|
|
59
|
-
results.forEach((r, i) => {
|
|
60
|
-
let taskPreview = r.task.slice(0, 50).replace(/\n/g, " ").trim();
|
|
61
|
-
if (r.task.length > 50) taskPreview += "...";
|
|
62
|
-
const emoji = statusEmoji[r.status] ?? "❓";
|
|
63
|
-
lines.push(
|
|
64
|
-
`| ${i + 1} | ${emoji} ${r.status} | ${r.duration_seconds.toFixed(1)}s | ${taskPreview} |`
|
|
65
|
-
);
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
lines.push("");
|
|
69
|
-
|
|
70
|
-
// Failed jobs details
|
|
71
|
-
const failures = results.filter((r) => r.status !== "success");
|
|
72
|
-
if (failures.length > 0) {
|
|
73
|
-
lines.push("## Failed Jobs");
|
|
74
|
-
lines.push("");
|
|
75
|
-
|
|
76
|
-
failures.forEach((r, i) => {
|
|
77
|
-
const taskPreview = r.task.slice(0, 80).replace(/\n/g, " ").trim();
|
|
78
|
-
lines.push(`### ${i + 1}. ${taskPreview}`);
|
|
79
|
-
lines.push("");
|
|
80
|
-
lines.push(`- **Status:** ${r.status}`);
|
|
81
|
-
if (r.error) {
|
|
82
|
-
lines.push(`- **Error:** ${r.error.slice(0, 200)}`);
|
|
83
|
-
}
|
|
84
|
-
if (r.retries > 0) {
|
|
85
|
-
lines.push(`- **Retries:** ${r.retries}`);
|
|
86
|
-
}
|
|
87
|
-
lines.push("");
|
|
88
|
-
});
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
// Next steps
|
|
92
|
-
lines.push("## Next Steps");
|
|
93
|
-
lines.push("");
|
|
94
|
-
if (failed === 0) {
|
|
95
|
-
lines.push("All jobs completed successfully! No action needed.");
|
|
96
|
-
} else {
|
|
97
|
-
lines.push("The following jobs need attention:");
|
|
98
|
-
lines.push("");
|
|
99
|
-
results.forEach((r, i) => {
|
|
100
|
-
if (r.status !== "success") {
|
|
101
|
-
const taskPreview = r.task.slice(0, 60).replace(/\n/g, " ").trim();
|
|
102
|
-
lines.push(`- [ ] Job ${i + 1}: ${taskPreview} (${r.status})`);
|
|
103
|
-
}
|
|
104
|
-
});
|
|
105
|
-
}
|
|
106
|
-
lines.push("");
|
|
107
|
-
|
|
108
|
-
const content = lines.join("\n");
|
|
109
|
-
|
|
110
|
-
if (outputPath) {
|
|
111
|
-
writeFileSync(outputPath, content);
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
return content;
|
|
115
|
-
}
|