@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/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
- }