popeye-cli 1.0.1 → 1.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/.env.example +24 -1
- package/CONTRIBUTING.md +275 -0
- package/OPEN_SOURCE_MANIFESTO.md +172 -0
- package/README.md +832 -123
- package/dist/adapters/claude.d.ts +19 -4
- package/dist/adapters/claude.d.ts.map +1 -1
- package/dist/adapters/claude.js +908 -42
- package/dist/adapters/claude.js.map +1 -1
- package/dist/adapters/gemini.d.ts +55 -0
- package/dist/adapters/gemini.d.ts.map +1 -0
- package/dist/adapters/gemini.js +318 -0
- package/dist/adapters/gemini.js.map +1 -0
- package/dist/adapters/grok.d.ts +73 -0
- package/dist/adapters/grok.d.ts.map +1 -0
- package/dist/adapters/grok.js +430 -0
- package/dist/adapters/grok.js.map +1 -0
- package/dist/adapters/openai.d.ts +1 -1
- package/dist/adapters/openai.d.ts.map +1 -1
- package/dist/adapters/openai.js +47 -8
- package/dist/adapters/openai.js.map +1 -1
- package/dist/auth/claude.d.ts +11 -9
- package/dist/auth/claude.d.ts.map +1 -1
- package/dist/auth/claude.js +107 -71
- package/dist/auth/claude.js.map +1 -1
- package/dist/auth/gemini.d.ts +58 -0
- package/dist/auth/gemini.d.ts.map +1 -0
- package/dist/auth/gemini.js +172 -0
- package/dist/auth/gemini.js.map +1 -0
- package/dist/auth/grok.d.ts +73 -0
- package/dist/auth/grok.d.ts.map +1 -0
- package/dist/auth/grok.js +211 -0
- package/dist/auth/grok.js.map +1 -0
- package/dist/auth/index.d.ts +14 -7
- package/dist/auth/index.d.ts.map +1 -1
- package/dist/auth/index.js +41 -6
- package/dist/auth/index.js.map +1 -1
- package/dist/auth/keychain.d.ts +20 -7
- package/dist/auth/keychain.d.ts.map +1 -1
- package/dist/auth/keychain.js +85 -29
- package/dist/auth/keychain.js.map +1 -1
- package/dist/auth/openai.d.ts +2 -2
- package/dist/auth/openai.d.ts.map +1 -1
- package/dist/auth/openai.js +30 -32
- package/dist/auth/openai.js.map +1 -1
- package/dist/cli/commands/auth.d.ts +1 -1
- package/dist/cli/commands/auth.d.ts.map +1 -1
- package/dist/cli/commands/auth.js +79 -8
- package/dist/cli/commands/auth.js.map +1 -1
- package/dist/cli/commands/create.d.ts.map +1 -1
- package/dist/cli/commands/create.js +15 -4
- package/dist/cli/commands/create.js.map +1 -1
- package/dist/cli/interactive.d.ts.map +1 -1
- package/dist/cli/interactive.js +1494 -114
- package/dist/cli/interactive.js.map +1 -1
- package/dist/config/defaults.d.ts +9 -1
- package/dist/config/defaults.d.ts.map +1 -1
- package/dist/config/defaults.js +19 -2
- package/dist/config/defaults.js.map +1 -1
- package/dist/config/index.d.ts +19 -0
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/index.js +33 -1
- package/dist/config/index.js.map +1 -1
- package/dist/config/schema.d.ts +47 -0
- package/dist/config/schema.d.ts.map +1 -1
- package/dist/config/schema.js +29 -1
- package/dist/config/schema.js.map +1 -1
- package/dist/generators/fullstack.d.ts +32 -0
- package/dist/generators/fullstack.d.ts.map +1 -0
- package/dist/generators/fullstack.js +497 -0
- package/dist/generators/fullstack.js.map +1 -0
- package/dist/generators/index.d.ts +4 -3
- package/dist/generators/index.d.ts.map +1 -1
- package/dist/generators/index.js +15 -1
- package/dist/generators/index.js.map +1 -1
- package/dist/generators/python.d.ts +17 -1
- package/dist/generators/python.d.ts.map +1 -1
- package/dist/generators/python.js +34 -20
- package/dist/generators/python.js.map +1 -1
- package/dist/generators/templates/fullstack.d.ts +113 -0
- package/dist/generators/templates/fullstack.d.ts.map +1 -0
- package/dist/generators/templates/fullstack.js +1004 -0
- package/dist/generators/templates/fullstack.js.map +1 -0
- package/dist/generators/typescript.d.ts +19 -1
- package/dist/generators/typescript.d.ts.map +1 -1
- package/dist/generators/typescript.js +37 -20
- package/dist/generators/typescript.js.map +1 -1
- package/dist/state/index.d.ts +108 -0
- package/dist/state/index.d.ts.map +1 -1
- package/dist/state/index.js +551 -4
- package/dist/state/index.js.map +1 -1
- package/dist/state/registry.d.ts +52 -0
- package/dist/state/registry.d.ts.map +1 -0
- package/dist/state/registry.js +215 -0
- package/dist/state/registry.js.map +1 -0
- package/dist/types/cli.d.ts +8 -0
- package/dist/types/cli.d.ts.map +1 -1
- package/dist/types/cli.js.map +1 -1
- package/dist/types/consensus.d.ts +186 -4
- package/dist/types/consensus.d.ts.map +1 -1
- package/dist/types/consensus.js +35 -3
- package/dist/types/consensus.js.map +1 -1
- package/dist/types/project.d.ts +76 -0
- package/dist/types/project.d.ts.map +1 -1
- package/dist/types/project.js +1 -1
- package/dist/types/project.js.map +1 -1
- package/dist/types/workflow.d.ts +217 -16
- package/dist/types/workflow.d.ts.map +1 -1
- package/dist/types/workflow.js +40 -1
- package/dist/types/workflow.js.map +1 -1
- package/dist/workflow/auto-fix.d.ts +45 -0
- package/dist/workflow/auto-fix.d.ts.map +1 -0
- package/dist/workflow/auto-fix.js +274 -0
- package/dist/workflow/auto-fix.js.map +1 -0
- package/dist/workflow/consensus.d.ts +70 -2
- package/dist/workflow/consensus.d.ts.map +1 -1
- package/dist/workflow/consensus.js +872 -17
- package/dist/workflow/consensus.js.map +1 -1
- package/dist/workflow/execution-mode.d.ts +10 -4
- package/dist/workflow/execution-mode.d.ts.map +1 -1
- package/dist/workflow/execution-mode.js +547 -58
- package/dist/workflow/execution-mode.js.map +1 -1
- package/dist/workflow/index.d.ts +14 -2
- package/dist/workflow/index.d.ts.map +1 -1
- package/dist/workflow/index.js +69 -6
- package/dist/workflow/index.js.map +1 -1
- package/dist/workflow/milestone-workflow.d.ts +34 -0
- package/dist/workflow/milestone-workflow.d.ts.map +1 -0
- package/dist/workflow/milestone-workflow.js +414 -0
- package/dist/workflow/milestone-workflow.js.map +1 -0
- package/dist/workflow/plan-mode.d.ts +80 -3
- package/dist/workflow/plan-mode.d.ts.map +1 -1
- package/dist/workflow/plan-mode.js +767 -49
- package/dist/workflow/plan-mode.js.map +1 -1
- package/dist/workflow/plan-storage.d.ts +386 -0
- package/dist/workflow/plan-storage.d.ts.map +1 -0
- package/dist/workflow/plan-storage.js +878 -0
- package/dist/workflow/plan-storage.js.map +1 -0
- package/dist/workflow/project-verification.d.ts +37 -0
- package/dist/workflow/project-verification.d.ts.map +1 -0
- package/dist/workflow/project-verification.js +381 -0
- package/dist/workflow/project-verification.js.map +1 -0
- package/dist/workflow/task-workflow.d.ts +37 -0
- package/dist/workflow/task-workflow.d.ts.map +1 -0
- package/dist/workflow/task-workflow.js +386 -0
- package/dist/workflow/task-workflow.js.map +1 -0
- package/dist/workflow/test-runner.d.ts +9 -0
- package/dist/workflow/test-runner.d.ts.map +1 -1
- package/dist/workflow/test-runner.js +101 -5
- package/dist/workflow/test-runner.js.map +1 -1
- package/dist/workflow/ui-designer.d.ts +82 -0
- package/dist/workflow/ui-designer.d.ts.map +1 -0
- package/dist/workflow/ui-designer.js +234 -0
- package/dist/workflow/ui-designer.js.map +1 -0
- package/dist/workflow/ui-setup.d.ts +58 -0
- package/dist/workflow/ui-setup.d.ts.map +1 -0
- package/dist/workflow/ui-setup.js +685 -0
- package/dist/workflow/ui-setup.js.map +1 -0
- package/dist/workflow/ui-verification.d.ts +114 -0
- package/dist/workflow/ui-verification.d.ts.map +1 -0
- package/dist/workflow/ui-verification.js +258 -0
- package/dist/workflow/ui-verification.js.map +1 -0
- package/dist/workflow/workflow-logger.d.ts +110 -0
- package/dist/workflow/workflow-logger.d.ts.map +1 -0
- package/dist/workflow/workflow-logger.js +267 -0
- package/dist/workflow/workflow-logger.js.map +1 -0
- package/dist/workflow/workspace-manager.d.ts +342 -0
- package/dist/workflow/workspace-manager.d.ts.map +1 -0
- package/dist/workflow/workspace-manager.js +733 -0
- package/dist/workflow/workspace-manager.js.map +1 -0
- package/package.json +2 -2
- package/src/adapters/claude.ts +1067 -47
- package/src/adapters/gemini.ts +373 -0
- package/src/adapters/grok.ts +492 -0
- package/src/adapters/openai.ts +48 -9
- package/src/auth/claude.ts +120 -78
- package/src/auth/gemini.ts +207 -0
- package/src/auth/grok.ts +255 -0
- package/src/auth/index.ts +47 -9
- package/src/auth/keychain.ts +95 -28
- package/src/auth/openai.ts +29 -36
- package/src/cli/commands/auth.ts +89 -10
- package/src/cli/commands/create.ts +13 -4
- package/src/cli/interactive.ts +1774 -142
- package/src/config/defaults.ts +19 -2
- package/src/config/index.ts +36 -1
- package/src/config/schema.ts +30 -1
- package/src/generators/fullstack.ts +551 -0
- package/src/generators/index.ts +25 -1
- package/src/generators/python.ts +65 -20
- package/src/generators/templates/fullstack.ts +1047 -0
- package/src/generators/typescript.ts +69 -20
- package/src/state/index.ts +713 -4
- package/src/state/registry.ts +278 -0
- package/src/types/cli.ts +8 -0
- package/src/types/consensus.ts +197 -6
- package/src/types/project.ts +82 -1
- package/src/types/workflow.ts +90 -1
- package/src/workflow/auto-fix.ts +340 -0
- package/src/workflow/consensus.ts +1180 -16
- package/src/workflow/execution-mode.ts +673 -74
- package/src/workflow/index.ts +95 -6
- package/src/workflow/milestone-workflow.ts +576 -0
- package/src/workflow/plan-mode.ts +924 -50
- package/src/workflow/plan-storage.ts +1282 -0
- package/src/workflow/project-verification.ts +471 -0
- package/src/workflow/task-workflow.ts +528 -0
- package/src/workflow/test-runner.ts +120 -5
- package/src/workflow/ui-designer.ts +337 -0
- package/src/workflow/ui-setup.ts +797 -0
- package/src/workflow/ui-verification.ts +357 -0
- package/src/workflow/workflow-logger.ts +353 -0
- package/src/workflow/workspace-manager.ts +912 -0
- package/tests/config/config.test.ts +1 -1
- package/tests/types/consensus.test.ts +3 -3
- package/tests/workflow/plan-mode.test.ts +213 -0
- package/tests/workflow/test-runner.test.ts +5 -3
package/src/adapters/claude.ts
CHANGED
|
@@ -3,6 +3,9 @@
|
|
|
3
3
|
* Wraps the Claude Agent SDK for code execution and generation
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
import { promises as fs } from 'node:fs';
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
import { homedir } from 'os';
|
|
6
9
|
import { query, type SDKMessage } from '@anthropic-ai/claude-agent-sdk';
|
|
7
10
|
|
|
8
11
|
/**
|
|
@@ -15,6 +18,287 @@ export interface ClaudeExecuteOptions {
|
|
|
15
18
|
systemPrompt?: string;
|
|
16
19
|
timeout?: number;
|
|
17
20
|
onMessage?: (message: SDKMessage) => void;
|
|
21
|
+
onProgress?: (message: string) => void;
|
|
22
|
+
/**
|
|
23
|
+
* Rate limit handling configuration
|
|
24
|
+
* Set to false to disable rate limit retries
|
|
25
|
+
*/
|
|
26
|
+
rateLimitConfig?: {
|
|
27
|
+
maxRetries?: number;
|
|
28
|
+
baseWaitMs?: number;
|
|
29
|
+
maxWaitMs?: number;
|
|
30
|
+
} | false;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Log directory for debug information
|
|
35
|
+
*/
|
|
36
|
+
const LOG_DIR = path.join(homedir(), '.popeye', 'logs');
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Rate limit handling configuration
|
|
40
|
+
*/
|
|
41
|
+
interface RateLimitConfig {
|
|
42
|
+
maxRetries: number;
|
|
43
|
+
baseWaitMs: number;
|
|
44
|
+
maxWaitMs: number;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const DEFAULT_RATE_LIMIT_CONFIG: RateLimitConfig = {
|
|
48
|
+
maxRetries: 3,
|
|
49
|
+
baseWaitMs: 60_000, // 1 minute
|
|
50
|
+
maxWaitMs: 10 * 60_000, // 10 minutes max - don't wait longer than this
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Parse rate limit reset time from error message
|
|
55
|
+
* Messages like: "You've hit your limit · resets 3pm (Asia/Jerusalem)"
|
|
56
|
+
*/
|
|
57
|
+
function parseRateLimitResetTime(message: string): Date | null {
|
|
58
|
+
// Try to parse time like "3pm", "3:30pm", "15:00"
|
|
59
|
+
const timePatterns = [
|
|
60
|
+
/resets?\s+(\d{1,2}):?(\d{2})?\s*(am|pm)?/i,
|
|
61
|
+
/until\s+(\d{1,2}):?(\d{2})?\s*(am|pm)?/i,
|
|
62
|
+
/wait\s+until\s+(\d{1,2}):?(\d{2})?\s*(am|pm)?/i,
|
|
63
|
+
];
|
|
64
|
+
|
|
65
|
+
for (const pattern of timePatterns) {
|
|
66
|
+
const match = message.match(pattern);
|
|
67
|
+
if (match) {
|
|
68
|
+
let hours = parseInt(match[1], 10);
|
|
69
|
+
const minutes = match[2] ? parseInt(match[2], 10) : 0;
|
|
70
|
+
const ampm = match[3]?.toLowerCase();
|
|
71
|
+
|
|
72
|
+
if (ampm === 'pm' && hours < 12) hours += 12;
|
|
73
|
+
if (ampm === 'am' && hours === 12) hours = 0;
|
|
74
|
+
|
|
75
|
+
const resetTime = new Date();
|
|
76
|
+
resetTime.setHours(hours, minutes, 0, 0);
|
|
77
|
+
|
|
78
|
+
// If the time has passed today, assume tomorrow
|
|
79
|
+
if (resetTime.getTime() <= Date.now()) {
|
|
80
|
+
resetTime.setDate(resetTime.getDate() + 1);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return resetTime;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Try to parse duration like "30 minutes", "1 hour"
|
|
88
|
+
const durationPatterns = [
|
|
89
|
+
/(\d+)\s*minutes?/i,
|
|
90
|
+
/(\d+)\s*hours?/i,
|
|
91
|
+
];
|
|
92
|
+
|
|
93
|
+
for (let i = 0; i < durationPatterns.length; i++) {
|
|
94
|
+
const match = message.match(durationPatterns[i]);
|
|
95
|
+
if (match) {
|
|
96
|
+
const value = parseInt(match[1], 10);
|
|
97
|
+
const multiplier = i === 0 ? 60_000 : 60 * 60_000; // minutes or hours
|
|
98
|
+
return new Date(Date.now() + value * multiplier);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Format wait time for display
|
|
107
|
+
*/
|
|
108
|
+
function formatWaitTime(ms: number): string {
|
|
109
|
+
if (ms < 60_000) {
|
|
110
|
+
return `${Math.ceil(ms / 1000)} seconds`;
|
|
111
|
+
} else if (ms < 60 * 60_000) {
|
|
112
|
+
const minutes = Math.ceil(ms / 60_000);
|
|
113
|
+
return `${minutes} minute${minutes > 1 ? 's' : ''}`;
|
|
114
|
+
} else {
|
|
115
|
+
const hours = Math.floor(ms / (60 * 60_000));
|
|
116
|
+
const minutes = Math.ceil((ms % (60 * 60_000)) / 60_000);
|
|
117
|
+
return `${hours} hour${hours > 1 ? 's' : ''}${minutes > 0 ? ` ${minutes} minute${minutes > 1 ? 's' : ''}` : ''}`;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Sleep for a specified duration with progress updates
|
|
123
|
+
*/
|
|
124
|
+
async function sleepWithProgress(
|
|
125
|
+
ms: number,
|
|
126
|
+
onProgress?: (message: string) => void
|
|
127
|
+
): Promise<void> {
|
|
128
|
+
const startTime = Date.now();
|
|
129
|
+
const endTime = startTime + ms;
|
|
130
|
+
const updateInterval = Math.min(ms / 10, 60_000); // Update every 10% or minute, whichever is smaller
|
|
131
|
+
|
|
132
|
+
while (Date.now() < endTime) {
|
|
133
|
+
const remaining = endTime - Date.now();
|
|
134
|
+
if (remaining <= 0) break;
|
|
135
|
+
|
|
136
|
+
onProgress?.(`Rate limit: waiting ${formatWaitTime(remaining)} before retry...`);
|
|
137
|
+
|
|
138
|
+
const sleepTime = Math.min(updateInterval, remaining);
|
|
139
|
+
await new Promise(resolve => setTimeout(resolve, sleepTime));
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Extract just the rate limit message from a larger string
|
|
145
|
+
* e.g., "Some content... You've hit your limit · resets 3pm (Asia/Jerusalem)" -> "You've hit your limit · resets 3pm (Asia/Jerusalem)"
|
|
146
|
+
*/
|
|
147
|
+
function extractRateLimitMessage(content: string): string {
|
|
148
|
+
// Look for specific rate limit error message patterns
|
|
149
|
+
// These patterns are designed to match actual error messages, not plan content
|
|
150
|
+
const patterns = [
|
|
151
|
+
// "You've hit your limit" patterns - common Claude error
|
|
152
|
+
/You['']ve hit your limit[^.\n]*(?:\.[\s]*(?:resets?|try again)[^.\n]*)?/i,
|
|
153
|
+
// "Rate limit exceeded" - explicit error message
|
|
154
|
+
/rate limit exceeded[^.\n]*/i,
|
|
155
|
+
// "rate limited" as verb - "you have been rate limited"
|
|
156
|
+
/(?:you\s+(?:have\s+)?(?:been\s+)?)?rate\s+limited[^.\n]*/i,
|
|
157
|
+
// "too many requests" - HTTP 429 style
|
|
158
|
+
/too many requests[^.\n]*/i,
|
|
159
|
+
// "quota exceeded" - usage limit
|
|
160
|
+
/quota exceeded[^.\n]*/i,
|
|
161
|
+
// "API rate limit" - specific to API errors
|
|
162
|
+
/api\s+rate\s+limit[^.\n]*/i,
|
|
163
|
+
// "request limit" patterns
|
|
164
|
+
/request\s+limit[^.\n]*(?:reached|exceeded|hit)[^.\n]*/i,
|
|
165
|
+
// "usage limit" patterns
|
|
166
|
+
/usage\s+limit[^.\n]*(?:reached|exceeded|hit)[^.\n]*/i,
|
|
167
|
+
];
|
|
168
|
+
|
|
169
|
+
for (const pattern of patterns) {
|
|
170
|
+
const match = content.match(pattern);
|
|
171
|
+
if (match) {
|
|
172
|
+
// Limit matched content to 200 chars to prevent capturing run-on text
|
|
173
|
+
const matched = match[0].trim();
|
|
174
|
+
return matched.length > 200 ? matched.slice(0, 197) + '...' : matched;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// If no pattern matches, try to find the first line that looks like an error
|
|
179
|
+
const lines = content.split('\n').filter(line => line.trim());
|
|
180
|
+
for (const line of lines.slice(0, 5)) {
|
|
181
|
+
const trimmedLine = line.trim();
|
|
182
|
+
// Look for lines that start with error indicators
|
|
183
|
+
if (/^(error|failed|limit|exceeded|denied)/i.test(trimmedLine)) {
|
|
184
|
+
return trimmedLine.length > 200 ? trimmedLine.slice(0, 197) + '...' : trimmedLine;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// If content is short, return it (but cap at 200 chars)
|
|
189
|
+
if (content.length < 200) {
|
|
190
|
+
return content;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Otherwise return a generic message - don't include potentially huge content
|
|
194
|
+
return 'Rate limit detected (details unavailable)';
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Check if an error indicates a rate limit
|
|
199
|
+
* Uses specific patterns to avoid false positives from plan content mentioning rate limiting
|
|
200
|
+
*/
|
|
201
|
+
function isRateLimitError(error: unknown, message?: string): boolean {
|
|
202
|
+
// Patterns that indicate actual rate limit errors (not just mentions of rate limiting)
|
|
203
|
+
// These are more specific than just "rate limit" to avoid matching plan content
|
|
204
|
+
const rateLimitPatterns = [
|
|
205
|
+
/you['']ve hit your limit/i,
|
|
206
|
+
/rate_limit_exceeded/i,
|
|
207
|
+
/rate limit exceeded/i,
|
|
208
|
+
/you have been rate limited/i,
|
|
209
|
+
/too many requests/i,
|
|
210
|
+
/quota exceeded/i,
|
|
211
|
+
/\b429\b/, // HTTP 429 status code
|
|
212
|
+
/rate limited/i, // "rate limited" as a verb phrase
|
|
213
|
+
/api rate limit/i,
|
|
214
|
+
/request limit reached/i,
|
|
215
|
+
/usage limit exceeded/i,
|
|
216
|
+
/limit reached.*try again/i,
|
|
217
|
+
/exceeded.*limit.*retry/i,
|
|
218
|
+
];
|
|
219
|
+
|
|
220
|
+
const checkString = (str: string): boolean => {
|
|
221
|
+
return rateLimitPatterns.some(pattern => pattern.test(str));
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
if (message && checkString(message)) return true;
|
|
225
|
+
|
|
226
|
+
if (error instanceof Error) {
|
|
227
|
+
if (checkString(error.message)) return true;
|
|
228
|
+
if ('code' in error && typeof error.code === 'string' && checkString(error.code)) return true;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (typeof error === 'object' && error !== null) {
|
|
232
|
+
const obj = error as Record<string, unknown>;
|
|
233
|
+
if (typeof obj.error === 'string' && checkString(obj.error)) return true;
|
|
234
|
+
if (typeof obj.code === 'string' && checkString(obj.code)) return true;
|
|
235
|
+
if (typeof obj.message === 'string' && checkString(obj.message)) return true;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
return false;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Write error details to a log file for debugging
|
|
243
|
+
*/
|
|
244
|
+
async function logErrorDetails(
|
|
245
|
+
error: unknown,
|
|
246
|
+
context: {
|
|
247
|
+
prompt?: string;
|
|
248
|
+
lastMessages?: SDKMessage[];
|
|
249
|
+
response?: string;
|
|
250
|
+
}
|
|
251
|
+
): Promise<string> {
|
|
252
|
+
try {
|
|
253
|
+
await fs.mkdir(LOG_DIR, { recursive: true });
|
|
254
|
+
|
|
255
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
256
|
+
const logFile = path.join(LOG_DIR, `claude-error-${timestamp}.log`);
|
|
257
|
+
|
|
258
|
+
const errorDetails = [
|
|
259
|
+
'='.repeat(80),
|
|
260
|
+
`CLAUDE ERROR LOG - ${new Date().toISOString()}`,
|
|
261
|
+
'='.repeat(80),
|
|
262
|
+
'',
|
|
263
|
+
'## Error Details',
|
|
264
|
+
`Type: ${error instanceof Error ? error.constructor.name : typeof error}`,
|
|
265
|
+
`Message: ${error instanceof Error ? error.message : String(error)}`,
|
|
266
|
+
'',
|
|
267
|
+
];
|
|
268
|
+
|
|
269
|
+
if (error instanceof Error && error.stack) {
|
|
270
|
+
errorDetails.push('## Stack Trace', error.stack, '');
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if (context.prompt) {
|
|
274
|
+
errorDetails.push(
|
|
275
|
+
'## Prompt (truncated)',
|
|
276
|
+
context.prompt.slice(0, 2000),
|
|
277
|
+
''
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
if (context.response) {
|
|
282
|
+
errorDetails.push(
|
|
283
|
+
'## Response Before Error (truncated)',
|
|
284
|
+
context.response.slice(-2000),
|
|
285
|
+
''
|
|
286
|
+
);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
if (context.lastMessages && context.lastMessages.length > 0) {
|
|
290
|
+
errorDetails.push(
|
|
291
|
+
'## Last Messages',
|
|
292
|
+
JSON.stringify(context.lastMessages.slice(-5), null, 2),
|
|
293
|
+
''
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
await fs.writeFile(logFile, errorDetails.join('\n'), 'utf-8');
|
|
298
|
+
return logFile;
|
|
299
|
+
} catch {
|
|
300
|
+
return '';
|
|
301
|
+
}
|
|
18
302
|
}
|
|
19
303
|
|
|
20
304
|
/**
|
|
@@ -53,29 +337,31 @@ export const DEFAULT_ALLOWED_TOOLS = [
|
|
|
53
337
|
];
|
|
54
338
|
|
|
55
339
|
/**
|
|
56
|
-
* Execute a prompt through the Claude Agent SDK
|
|
57
|
-
*
|
|
58
|
-
* @param prompt - The prompt to execute
|
|
59
|
-
* @param options - Execution options
|
|
60
|
-
* @returns The execution result
|
|
340
|
+
* Execute a prompt through the Claude Agent SDK (internal implementation)
|
|
61
341
|
*/
|
|
62
|
-
|
|
342
|
+
async function executePromptInternal(
|
|
63
343
|
prompt: string,
|
|
64
344
|
options: ClaudeExecuteOptions = {}
|
|
65
|
-
): Promise<ClaudeExecuteResult> {
|
|
345
|
+
): Promise<ClaudeExecuteResult & { rateLimitInfo?: { isRateLimit: boolean; resetTime?: Date; message?: string } }> {
|
|
66
346
|
const {
|
|
67
347
|
cwd,
|
|
68
348
|
allowedTools = DEFAULT_ALLOWED_TOOLS,
|
|
69
349
|
permissionMode = 'bypassPermissions',
|
|
70
350
|
systemPrompt,
|
|
71
351
|
onMessage,
|
|
352
|
+
onProgress,
|
|
72
353
|
} = options;
|
|
73
354
|
|
|
74
355
|
const toolCalls: ToolCallRecord[] = [];
|
|
356
|
+
const recentMessages: SDKMessage[] = [];
|
|
75
357
|
let response = '';
|
|
76
358
|
let error: string | undefined;
|
|
359
|
+
let rateLimitInfo: { isRateLimit: boolean; resetTime?: Date; message?: string } | undefined;
|
|
360
|
+
let lastProgressTime = Date.now();
|
|
77
361
|
|
|
78
362
|
try {
|
|
363
|
+
onProgress?.('Connecting to Claude...');
|
|
364
|
+
|
|
79
365
|
const result = query({
|
|
80
366
|
prompt,
|
|
81
367
|
options: {
|
|
@@ -86,49 +372,318 @@ export async function executePrompt(
|
|
|
86
372
|
},
|
|
87
373
|
});
|
|
88
374
|
|
|
375
|
+
onProgress?.('Claude is thinking...');
|
|
376
|
+
|
|
89
377
|
for await (const message of result) {
|
|
378
|
+
// Keep track of recent messages for error logging
|
|
379
|
+
recentMessages.push(message);
|
|
380
|
+
if (recentMessages.length > 10) {
|
|
381
|
+
recentMessages.shift();
|
|
382
|
+
}
|
|
383
|
+
|
|
90
384
|
// Call the message handler if provided
|
|
91
385
|
if (onMessage) {
|
|
92
386
|
onMessage(message);
|
|
93
387
|
}
|
|
94
388
|
|
|
389
|
+
// Report progress based on message type
|
|
390
|
+
const now = Date.now();
|
|
391
|
+
if (now - lastProgressTime > 2000) {
|
|
392
|
+
// Report progress every 2 seconds
|
|
393
|
+
lastProgressTime = now;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// Check for rate limit in message error field
|
|
397
|
+
const messageWithError = message as { error?: string | { message?: string; code?: string }; message?: { content?: Array<{ text?: string }> } };
|
|
398
|
+
if (messageWithError.error) {
|
|
399
|
+
const errorStr = typeof messageWithError.error === 'string'
|
|
400
|
+
? messageWithError.error
|
|
401
|
+
: messageWithError.error.message || '';
|
|
402
|
+
|
|
403
|
+
if (isRateLimitError(null, errorStr)) {
|
|
404
|
+
// Extract rate limit message from response content
|
|
405
|
+
let rateLimitMessage = errorStr;
|
|
406
|
+
if (messageWithError.message?.content) {
|
|
407
|
+
const textContent = messageWithError.message.content.find(c => c.text);
|
|
408
|
+
if (textContent?.text) {
|
|
409
|
+
rateLimitMessage = textContent.text;
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
const extractedMessage = extractRateLimitMessage(rateLimitMessage);
|
|
414
|
+
rateLimitInfo = {
|
|
415
|
+
isRateLimit: true,
|
|
416
|
+
resetTime: parseRateLimitResetTime(rateLimitMessage) ?? undefined,
|
|
417
|
+
message: extractedMessage,
|
|
418
|
+
};
|
|
419
|
+
error = `Rate limit exceeded: ${extractedMessage}`;
|
|
420
|
+
onProgress?.(`Rate limit hit: ${rateLimitMessage}`);
|
|
421
|
+
continue;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
95
425
|
// Process different message types
|
|
96
426
|
if (message.type === 'assistant') {
|
|
97
427
|
const assistantMessage = message as { type: 'assistant'; message: { content: string | Array<{ type: string; text?: string }> } };
|
|
98
428
|
if (typeof assistantMessage.message.content === 'string') {
|
|
99
429
|
response += assistantMessage.message.content;
|
|
430
|
+
|
|
431
|
+
// Check for rate limit in text response
|
|
432
|
+
if (isRateLimitError(null, assistantMessage.message.content)) {
|
|
433
|
+
const extractedMsg = extractRateLimitMessage(assistantMessage.message.content);
|
|
434
|
+
rateLimitInfo = {
|
|
435
|
+
isRateLimit: true,
|
|
436
|
+
resetTime: parseRateLimitResetTime(assistantMessage.message.content) ?? undefined,
|
|
437
|
+
message: extractedMsg,
|
|
438
|
+
};
|
|
439
|
+
error = `Rate limit exceeded: ${extractedMsg}`;
|
|
440
|
+
onProgress?.(`Rate limit hit: ${assistantMessage.message.content}`);
|
|
441
|
+
} else {
|
|
442
|
+
onProgress?.('Claude is writing...');
|
|
443
|
+
}
|
|
100
444
|
} else if (Array.isArray(assistantMessage.message.content)) {
|
|
101
445
|
for (const block of assistantMessage.message.content) {
|
|
102
446
|
if (block.type === 'text' && block.text) {
|
|
103
447
|
response += block.text;
|
|
448
|
+
|
|
449
|
+
// Check for rate limit in text block
|
|
450
|
+
if (isRateLimitError(null, block.text)) {
|
|
451
|
+
const extractedBlockMsg = extractRateLimitMessage(block.text);
|
|
452
|
+
rateLimitInfo = {
|
|
453
|
+
isRateLimit: true,
|
|
454
|
+
resetTime: parseRateLimitResetTime(block.text) ?? undefined,
|
|
455
|
+
message: extractedBlockMsg,
|
|
456
|
+
};
|
|
457
|
+
error = `Rate limit exceeded: ${extractedBlockMsg}`;
|
|
458
|
+
onProgress?.(`Rate limit hit: ${block.text}`);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
if (block.type === 'tool_use') {
|
|
462
|
+
const toolBlock = block as { type: 'tool_use'; name?: string };
|
|
463
|
+
onProgress?.(`Using tool: ${toolBlock.name || 'unknown'}...`);
|
|
104
464
|
}
|
|
105
465
|
}
|
|
106
466
|
}
|
|
107
467
|
} else if (message.type === 'result') {
|
|
108
|
-
// Handle result messages which may contain tool information
|
|
109
|
-
const resultMessage = message as {
|
|
110
|
-
|
|
111
|
-
|
|
468
|
+
// Handle result messages which may contain tool information or errors
|
|
469
|
+
const resultMessage = message as {
|
|
470
|
+
type: 'result';
|
|
471
|
+
result?: string;
|
|
472
|
+
error?: { message?: string; code?: string };
|
|
473
|
+
subtype?: string;
|
|
474
|
+
};
|
|
475
|
+
if (resultMessage.error && !rateLimitInfo) {
|
|
476
|
+
const errMsg = resultMessage.error.message || 'Unknown error';
|
|
477
|
+
const errCode = resultMessage.error.code || 'ERROR';
|
|
478
|
+
error = `${errCode}: ${errMsg}`;
|
|
479
|
+
onProgress?.(`Claude returned error: ${error}`);
|
|
112
480
|
}
|
|
113
481
|
}
|
|
482
|
+
|
|
483
|
+
// Check for any error property on the message (handles various error formats)
|
|
484
|
+
if (messageWithError.error && !error && !rateLimitInfo) {
|
|
485
|
+
const errMsg = typeof messageWithError.error === 'string'
|
|
486
|
+
? messageWithError.error
|
|
487
|
+
: messageWithError.error.message || 'Unknown error';
|
|
488
|
+
const errCode = typeof messageWithError.error === 'object'
|
|
489
|
+
? messageWithError.error.code || 'ERROR'
|
|
490
|
+
: 'ERROR';
|
|
491
|
+
error = `${errCode}: ${errMsg}`;
|
|
492
|
+
onProgress?.(`Error detected: ${error}`);
|
|
493
|
+
}
|
|
114
494
|
}
|
|
115
495
|
|
|
496
|
+
onProgress?.('Claude finished');
|
|
497
|
+
|
|
116
498
|
return {
|
|
117
499
|
success: !error,
|
|
118
500
|
response: response.trim(),
|
|
119
501
|
toolCalls,
|
|
120
502
|
error,
|
|
503
|
+
rateLimitInfo,
|
|
121
504
|
};
|
|
122
505
|
} catch (err) {
|
|
506
|
+
// First, check if we already detected a rate limit during message processing
|
|
507
|
+
// This happens when rate limit is detected in the stream but process still exits with code 1
|
|
508
|
+
if (rateLimitInfo?.isRateLimit) {
|
|
509
|
+
onProgress?.(`Rate limit detected (process exited): ${rateLimitInfo.message || 'Unknown'}`);
|
|
510
|
+
return {
|
|
511
|
+
success: false,
|
|
512
|
+
response: response.trim(),
|
|
513
|
+
toolCalls,
|
|
514
|
+
error: `Rate limit exceeded: ${rateLimitInfo.message || 'Rate limit hit'}`,
|
|
515
|
+
rateLimitInfo,
|
|
516
|
+
};
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// Check if the exception itself indicates a rate limit
|
|
520
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
521
|
+
|
|
522
|
+
if (isRateLimitError(err, errMsg) || isRateLimitError(null, response)) {
|
|
523
|
+
const combinedMessage = response || errMsg;
|
|
524
|
+
const extractedRateLimitMsg = extractRateLimitMessage(combinedMessage);
|
|
525
|
+
return {
|
|
526
|
+
success: false,
|
|
527
|
+
response: response.trim(),
|
|
528
|
+
toolCalls,
|
|
529
|
+
error: `Rate limit exceeded: ${extractedRateLimitMsg}`,
|
|
530
|
+
rateLimitInfo: {
|
|
531
|
+
isRateLimit: true,
|
|
532
|
+
resetTime: parseRateLimitResetTime(combinedMessage) ?? undefined,
|
|
533
|
+
message: extractedRateLimitMsg,
|
|
534
|
+
},
|
|
535
|
+
};
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
// Log detailed error information for debugging
|
|
539
|
+
const logFile = await logErrorDetails(err, {
|
|
540
|
+
prompt: prompt.slice(0, 5000),
|
|
541
|
+
lastMessages: recentMessages,
|
|
542
|
+
response: response.slice(-3000),
|
|
543
|
+
});
|
|
544
|
+
|
|
545
|
+
// Build a detailed error message
|
|
546
|
+
let errorMsg = err instanceof Error ? err.message : 'Unknown error executing prompt';
|
|
547
|
+
|
|
548
|
+
// Check for common error patterns and provide helpful messages
|
|
549
|
+
if (errorMsg.includes('exited with code 1')) {
|
|
550
|
+
errorMsg = `Claude Code process failed (exit code 1). `;
|
|
551
|
+
if (response) {
|
|
552
|
+
// Try to extract any error indicators from the response
|
|
553
|
+
const lastLines = response.split('\n').slice(-10).join('\n');
|
|
554
|
+
if (lastLines.includes('error') || lastLines.includes('Error') || lastLines.includes('failed')) {
|
|
555
|
+
errorMsg += `Last output: ${lastLines.slice(0, 500)}`;
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
if (logFile) {
|
|
559
|
+
errorMsg += ` Debug log: ${logFile}`;
|
|
560
|
+
}
|
|
561
|
+
} else if (errorMsg.includes('ECONNREFUSED') || errorMsg.includes('ENOTFOUND')) {
|
|
562
|
+
errorMsg = 'Cannot connect to Claude Code CLI. Is it installed and running?';
|
|
563
|
+
} else if (errorMsg.includes('timeout') || errorMsg.includes('ETIMEDOUT')) {
|
|
564
|
+
errorMsg = 'Claude Code request timed out. The task may be too complex.';
|
|
565
|
+
} else if (errorMsg.includes('permission') || errorMsg.includes('Permission')) {
|
|
566
|
+
errorMsg = `Permission error: ${errorMsg}. Check tool permissions.`;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
onProgress?.(`Error: ${errorMsg}`);
|
|
570
|
+
|
|
123
571
|
return {
|
|
124
572
|
success: false,
|
|
125
|
-
response:
|
|
573
|
+
response: response.trim(),
|
|
126
574
|
toolCalls,
|
|
127
|
-
error:
|
|
575
|
+
error: errorMsg,
|
|
128
576
|
};
|
|
129
577
|
}
|
|
130
578
|
}
|
|
131
579
|
|
|
580
|
+
/**
|
|
581
|
+
* Execute a prompt through the Claude Agent SDK with rate limit retry handling
|
|
582
|
+
*
|
|
583
|
+
* @param prompt - The prompt to execute
|
|
584
|
+
* @param options - Execution options
|
|
585
|
+
* @returns The execution result
|
|
586
|
+
*/
|
|
587
|
+
export async function executePrompt(
|
|
588
|
+
prompt: string,
|
|
589
|
+
options: ClaudeExecuteOptions = {}
|
|
590
|
+
): Promise<ClaudeExecuteResult> {
|
|
591
|
+
const { onProgress, rateLimitConfig: userRateLimitConfig } = options;
|
|
592
|
+
|
|
593
|
+
// If rate limit handling is disabled, run once without retry
|
|
594
|
+
if (userRateLimitConfig === false) {
|
|
595
|
+
const result = await executePromptInternal(prompt, options);
|
|
596
|
+
return {
|
|
597
|
+
success: result.success,
|
|
598
|
+
response: result.response,
|
|
599
|
+
toolCalls: result.toolCalls,
|
|
600
|
+
error: result.error,
|
|
601
|
+
};
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
// Merge user config with defaults
|
|
605
|
+
const rateLimitConfig: RateLimitConfig = {
|
|
606
|
+
...DEFAULT_RATE_LIMIT_CONFIG,
|
|
607
|
+
...userRateLimitConfig,
|
|
608
|
+
};
|
|
609
|
+
|
|
610
|
+
let attempt = 0;
|
|
611
|
+
|
|
612
|
+
while (attempt < rateLimitConfig.maxRetries) {
|
|
613
|
+
const result = await executePromptInternal(prompt, options);
|
|
614
|
+
|
|
615
|
+
// If no rate limit, return the result
|
|
616
|
+
if (!result.rateLimitInfo?.isRateLimit) {
|
|
617
|
+
return {
|
|
618
|
+
success: result.success,
|
|
619
|
+
response: result.response,
|
|
620
|
+
toolCalls: result.toolCalls,
|
|
621
|
+
error: result.error,
|
|
622
|
+
};
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
// Rate limit detected - calculate wait time
|
|
626
|
+
attempt++;
|
|
627
|
+
|
|
628
|
+
if (attempt >= rateLimitConfig.maxRetries) {
|
|
629
|
+
onProgress?.(`Rate limit: max retries (${rateLimitConfig.maxRetries}) exceeded`);
|
|
630
|
+
return {
|
|
631
|
+
success: false,
|
|
632
|
+
response: result.response,
|
|
633
|
+
toolCalls: result.toolCalls,
|
|
634
|
+
error: `Rate limit exceeded after ${attempt} retries. ${result.rateLimitInfo.message || ''}`,
|
|
635
|
+
};
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
// Calculate wait time
|
|
639
|
+
let waitMs: number;
|
|
640
|
+
|
|
641
|
+
if (result.rateLimitInfo.resetTime) {
|
|
642
|
+
// Use parsed reset time
|
|
643
|
+
waitMs = result.rateLimitInfo.resetTime.getTime() - Date.now();
|
|
644
|
+
// Add a small buffer
|
|
645
|
+
waitMs += 30_000;
|
|
646
|
+
} else {
|
|
647
|
+
// Use exponential backoff
|
|
648
|
+
waitMs = Math.min(
|
|
649
|
+
rateLimitConfig.baseWaitMs * Math.pow(2, attempt - 1),
|
|
650
|
+
rateLimitConfig.maxWaitMs
|
|
651
|
+
);
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
// Ensure minimum wait time
|
|
655
|
+
waitMs = Math.max(waitMs, 30_000);
|
|
656
|
+
|
|
657
|
+
// IMPORTANT: Cap wait time to maxWaitMs - don't wait hours for rate limits
|
|
658
|
+
if (waitMs > rateLimitConfig.maxWaitMs) {
|
|
659
|
+
onProgress?.(`Rate limit reset time is too far in the future (${formatWaitTime(waitMs)})`);
|
|
660
|
+
onProgress?.(`Maximum wait time is ${formatWaitTime(rateLimitConfig.maxWaitMs)}. Please try again later.`);
|
|
661
|
+
return {
|
|
662
|
+
success: false,
|
|
663
|
+
response: result.response,
|
|
664
|
+
toolCalls: result.toolCalls,
|
|
665
|
+
error: `Rate limit exceeded. Reset time is ${formatWaitTime(waitMs)} away - too long to wait. Please try again later.`,
|
|
666
|
+
};
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
onProgress?.(`Rate limit hit (attempt ${attempt}/${rateLimitConfig.maxRetries}). ${result.rateLimitInfo.message || ''}`);
|
|
670
|
+
onProgress?.(`Waiting ${formatWaitTime(waitMs)} before retry...`);
|
|
671
|
+
|
|
672
|
+
// Wait with progress updates
|
|
673
|
+
await sleepWithProgress(waitMs, onProgress);
|
|
674
|
+
|
|
675
|
+
onProgress?.(`Retrying after rate limit (attempt ${attempt + 1}/${rateLimitConfig.maxRetries})...`);
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
// Should not reach here, but just in case
|
|
679
|
+
return {
|
|
680
|
+
success: false,
|
|
681
|
+
response: '',
|
|
682
|
+
toolCalls: [],
|
|
683
|
+
error: 'Rate limit handling failed unexpectedly',
|
|
684
|
+
};
|
|
685
|
+
}
|
|
686
|
+
|
|
132
687
|
/**
|
|
133
688
|
* Execute code generation for a specific task
|
|
134
689
|
*
|
|
@@ -202,8 +757,12 @@ After running the tests:
|
|
|
202
757
|
* Analyze codebase to understand structure and patterns
|
|
203
758
|
*
|
|
204
759
|
* @param cwd - Working directory of the project
|
|
760
|
+
* @param onProgress - Progress callback
|
|
205
761
|
*/
|
|
206
|
-
export async function analyzeCodebase(
|
|
762
|
+
export async function analyzeCodebase(
|
|
763
|
+
cwd: string,
|
|
764
|
+
onProgress?: (message: string) => void
|
|
765
|
+
): Promise<ClaudeExecuteResult> {
|
|
207
766
|
const prompt = `
|
|
208
767
|
Analyze this codebase and provide:
|
|
209
768
|
|
|
@@ -221,78 +780,539 @@ Be concise but thorough in your analysis.
|
|
|
221
780
|
cwd,
|
|
222
781
|
allowedTools: ['Read', 'Glob', 'Grep', 'LS'],
|
|
223
782
|
permissionMode: 'default', // Read-only analysis
|
|
783
|
+
onProgress,
|
|
224
784
|
});
|
|
225
785
|
}
|
|
226
786
|
|
|
227
787
|
/**
|
|
228
|
-
*
|
|
788
|
+
* Extract plan file path from Claude's response
|
|
789
|
+
* Claude sometimes saves the plan to a file and responds with a summary
|
|
790
|
+
*/
|
|
791
|
+
function extractPlanFilePath(response: string): string | null {
|
|
792
|
+
// Look for plan file paths like /Users/.../.claude/plans/...
|
|
793
|
+
const patterns = [
|
|
794
|
+
/`([^`]*\.claude\/plans\/[^`]+\.md)`/i,
|
|
795
|
+
/saved to\s+`?([^\s`]+\.claude\/plans\/[^\s`]+\.md)`?/i,
|
|
796
|
+
/created at\s+`?([^\s`]+\.claude\/plans\/[^\s`]+\.md)`?/i,
|
|
797
|
+
/plan.*at\s+`?([^\s`]+\.claude\/plans\/[^\s`]+\.md)`?/i,
|
|
798
|
+
/(\/[^\s]+\.claude\/plans\/[^\s]+\.md)/i,
|
|
799
|
+
];
|
|
800
|
+
|
|
801
|
+
for (const pattern of patterns) {
|
|
802
|
+
const match = response.match(pattern);
|
|
803
|
+
if (match && match[1]) {
|
|
804
|
+
return match[1];
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
return null;
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
/**
|
|
812
|
+
* Check if response is Claude's thinking/conversation instead of actual plan
|
|
813
|
+
*/
|
|
814
|
+
function isConversationalResponse(response: string): boolean {
|
|
815
|
+
const conversationalPhrases = [
|
|
816
|
+
'let me ',
|
|
817
|
+
'i will ',
|
|
818
|
+
'i\'ll ',
|
|
819
|
+
'now i have',
|
|
820
|
+
'i now have',
|
|
821
|
+
'let me launch',
|
|
822
|
+
'let me create',
|
|
823
|
+
'i\'ve created',
|
|
824
|
+
'i\'ve analyzed',
|
|
825
|
+
'has been created',
|
|
826
|
+
'has been saved',
|
|
827
|
+
'the plan is structured',
|
|
828
|
+
];
|
|
829
|
+
|
|
830
|
+
const responseLower = response.toLowerCase();
|
|
831
|
+
return conversationalPhrases.some(phrase => responseLower.includes(phrase));
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
/**
|
|
835
|
+
* Build the appropriate prompt for plan creation based on project language
|
|
229
836
|
*
|
|
230
837
|
* @param specification - The project specification
|
|
231
|
-
* @param context - Additional context
|
|
838
|
+
* @param context - Additional context
|
|
839
|
+
* @param language - Target programming language
|
|
840
|
+
* @returns The prompt string
|
|
232
841
|
*/
|
|
233
|
-
|
|
842
|
+
function buildPlanPrompt(
|
|
234
843
|
specification: string,
|
|
235
|
-
context: string
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
844
|
+
context: string,
|
|
845
|
+
language: 'python' | 'typescript' | 'fullstack'
|
|
846
|
+
): string {
|
|
847
|
+
// Base instructions that apply to all projects
|
|
848
|
+
const baseInstructions = `
|
|
849
|
+
You are a software architect. Create a detailed, actionable development plan.
|
|
850
|
+
|
|
851
|
+
CRITICAL INSTRUCTION: You must output the COMPLETE plan content directly in your response as markdown.
|
|
852
|
+
Do NOT use tools to save the plan to a file.
|
|
853
|
+
Do NOT just describe what the plan contains - output the ACTUAL plan with all milestones and tasks.
|
|
854
|
+
Do NOT say "Let me...", "I will...", "I've created...", or any conversational text.
|
|
855
|
+
|
|
856
|
+
Start your response with "# Development Plan:" and include the FULL plan content.
|
|
857
|
+
`.trim();
|
|
858
|
+
|
|
859
|
+
// Fullstack-specific format with app tagging
|
|
860
|
+
if (language === 'fullstack') {
|
|
861
|
+
return `
|
|
862
|
+
${baseInstructions}
|
|
863
|
+
|
|
864
|
+
## Project Type: FULLSTACK MONOREPO
|
|
865
|
+
- **Frontend**: React + Vite + TypeScript + Tailwind CSS + shadcn/ui
|
|
866
|
+
- **Backend**: FastAPI (Python) + PostgreSQL
|
|
867
|
+
- **Structure**: Monorepo with apps/frontend and apps/backend
|
|
239
868
|
|
|
240
869
|
## Specification
|
|
241
870
|
${specification}
|
|
242
871
|
|
|
243
872
|
${context ? `## Additional Context\n${context}` : ''}
|
|
244
873
|
|
|
245
|
-
## Required Plan
|
|
874
|
+
## Required Plan Format for Fullstack Projects
|
|
875
|
+
|
|
876
|
+
Your response MUST be the complete plan in this EXACT format:
|
|
877
|
+
|
|
878
|
+
# Development Plan: [Project Name]
|
|
879
|
+
|
|
880
|
+
## Overview
|
|
881
|
+
[2-3 sentence summary mentioning both frontend and backend]
|
|
246
882
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
5. **Test Plan**: Define tests for each task
|
|
252
|
-
6. **Risks & Mitigations**: Identify potential issues
|
|
883
|
+
## Architecture
|
|
884
|
+
- **Frontend App**: React SPA at apps/frontend/
|
|
885
|
+
- **Backend App**: FastAPI service at apps/backend/
|
|
886
|
+
- **Communication**: REST API (OpenAPI contract)
|
|
253
887
|
|
|
254
|
-
|
|
888
|
+
---
|
|
889
|
+
|
|
890
|
+
## Milestone 1: [Name]
|
|
891
|
+
**Description**: [What this milestone achieves]
|
|
892
|
+
|
|
893
|
+
### Frontend Tasks
|
|
894
|
+
|
|
895
|
+
#### Task 1.1 [FE]: [Actionable task name]
|
|
896
|
+
**App**: frontend
|
|
897
|
+
**Files**:
|
|
898
|
+
- \`apps/frontend/src/components/...\`
|
|
899
|
+
- \`apps/frontend/src/pages/...\`
|
|
900
|
+
**Dependencies**: None
|
|
901
|
+
**Acceptance Criteria**:
|
|
902
|
+
- [ ] Criterion 1
|
|
903
|
+
- [ ] Criterion 2
|
|
904
|
+
|
|
905
|
+
### Backend Tasks
|
|
906
|
+
|
|
907
|
+
#### Task 1.2 [BE]: [Actionable task name]
|
|
908
|
+
**App**: backend
|
|
909
|
+
**Files**:
|
|
910
|
+
- \`apps/backend/src/api/routes/...\`
|
|
911
|
+
- \`apps/backend/src/models/...\`
|
|
912
|
+
**Dependencies**: None
|
|
913
|
+
**Acceptance Criteria**:
|
|
914
|
+
- [ ] Criterion 1
|
|
915
|
+
|
|
916
|
+
### Integration Tasks
|
|
917
|
+
|
|
918
|
+
#### Task 1.3 [INT]: [Actionable task name]
|
|
919
|
+
**App**: unified
|
|
920
|
+
**Dependencies**: Task 1.1, Task 1.2
|
|
921
|
+
**Acceptance Criteria**:
|
|
922
|
+
- [ ] Frontend calls backend API successfully
|
|
923
|
+
- [ ] E2E test passes
|
|
924
|
+
|
|
925
|
+
---
|
|
926
|
+
|
|
927
|
+
## Milestone 2: [Name]
|
|
928
|
+
[Continue same pattern...]
|
|
929
|
+
|
|
930
|
+
---
|
|
931
|
+
|
|
932
|
+
## Test Plan
|
|
933
|
+
|
|
934
|
+
### Frontend Tests (apps/frontend)
|
|
935
|
+
- **Unit**: Vitest + Testing Library
|
|
936
|
+
- **E2E**: Playwright
|
|
937
|
+
|
|
938
|
+
### Backend Tests (apps/backend)
|
|
939
|
+
- **Unit**: pytest
|
|
940
|
+
- **Integration**: pytest + TestClient
|
|
941
|
+
|
|
942
|
+
### Integration Tests
|
|
943
|
+
- API contract validation
|
|
944
|
+
- E2E user flows
|
|
945
|
+
|
|
946
|
+
## Risks & Mitigations
|
|
947
|
+
[Include frontend, backend, and integration risks separately]
|
|
948
|
+
|
|
949
|
+
---
|
|
950
|
+
|
|
951
|
+
## CRITICAL FULLSTACK REQUIREMENTS:
|
|
952
|
+
1. **Tag every task** with [FE], [BE], or [INT]
|
|
953
|
+
2. **Specify App field** for each task (frontend, backend, or unified)
|
|
954
|
+
3. **List exact file paths** under apps/frontend/ or apps/backend/
|
|
955
|
+
4. **Group tasks** under "Frontend Tasks", "Backend Tasks", or "Integration Tasks" headers
|
|
956
|
+
5. **Include at least 3 milestones** with tasks distributed across FE/BE/INT
|
|
957
|
+
6. Each task MUST start with an action verb: Implement, Create, Build, Add, Configure, Set up, Write, Design, etc.
|
|
958
|
+
7. Each task MUST be specific and implementable
|
|
959
|
+
|
|
960
|
+
IMPORTANT: Output the COMPLETE plan now. Start with "# Development Plan:" on the first line.
|
|
255
961
|
`.trim();
|
|
962
|
+
}
|
|
256
963
|
|
|
257
|
-
|
|
964
|
+
// Python-specific format
|
|
965
|
+
if (language === 'python') {
|
|
966
|
+
return `
|
|
967
|
+
${baseInstructions}
|
|
968
|
+
|
|
969
|
+
## Project Type: PYTHON
|
|
970
|
+
- **Language**: Python 3.11+
|
|
971
|
+
- **Framework**: FastAPI (if API) or CLI
|
|
972
|
+
- **Testing**: pytest
|
|
973
|
+
|
|
974
|
+
## Specification
|
|
975
|
+
${specification}
|
|
976
|
+
|
|
977
|
+
${context ? `## Additional Context\n${context}` : ''}
|
|
978
|
+
|
|
979
|
+
## Required Plan Format
|
|
980
|
+
|
|
981
|
+
Your response MUST be the complete plan in this EXACT format:
|
|
982
|
+
|
|
983
|
+
# Development Plan: [Project Name]
|
|
984
|
+
|
|
985
|
+
## Overview
|
|
986
|
+
[2-3 sentence summary of what will be built]
|
|
987
|
+
|
|
988
|
+
## Milestone 1: [Name]
|
|
989
|
+
**Description**: [What this milestone achieves]
|
|
990
|
+
|
|
991
|
+
### Task 1.1: [Actionable task name starting with verb]
|
|
992
|
+
**Description**: [What this task accomplishes]
|
|
993
|
+
**Files to create/modify**: [List specific Python files in src/]
|
|
994
|
+
**Acceptance Criteria**:
|
|
995
|
+
- [Specific, testable criterion]
|
|
996
|
+
- [Another criterion]
|
|
997
|
+
|
|
998
|
+
### Task 1.2: [Another actionable task]
|
|
999
|
+
...
|
|
1000
|
+
|
|
1001
|
+
## Milestone 2: [Name]
|
|
1002
|
+
...
|
|
1003
|
+
|
|
1004
|
+
## Test Plan
|
|
1005
|
+
- pytest for unit tests in tests/
|
|
1006
|
+
- httpx for API integration tests
|
|
1007
|
+
|
|
1008
|
+
## Risks & Mitigations
|
|
1009
|
+
[Potential issues and how to address them]
|
|
1010
|
+
|
|
1011
|
+
## Requirements for Tasks
|
|
1012
|
+
|
|
1013
|
+
1. Each task MUST start with an action verb: Implement, Create, Build, Add, Configure, Set up, Write, Design, etc.
|
|
1014
|
+
2. Each task MUST be specific and implementable
|
|
1015
|
+
3. Each milestone MUST have at least 3-5 specific tasks
|
|
1016
|
+
4. The plan MUST have at least 3 milestones for any non-trivial project
|
|
1017
|
+
5. Files to create/modify MUST be listed for each task
|
|
1018
|
+
6. Acceptance criteria MUST be testable
|
|
1019
|
+
|
|
1020
|
+
IMPORTANT: Output the COMPLETE plan now. Start with "# Development Plan:" on the first line.
|
|
1021
|
+
`.trim();
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
// TypeScript/default format
|
|
1025
|
+
return `
|
|
1026
|
+
${baseInstructions}
|
|
1027
|
+
|
|
1028
|
+
## Project Type: TYPESCRIPT
|
|
1029
|
+
- **Language**: TypeScript
|
|
1030
|
+
- **Framework**: React + Vite (if frontend) or Node.js
|
|
1031
|
+
- **Testing**: Vitest
|
|
1032
|
+
|
|
1033
|
+
## Specification
|
|
1034
|
+
${specification}
|
|
1035
|
+
|
|
1036
|
+
${context ? `## Additional Context\n${context}` : ''}
|
|
1037
|
+
|
|
1038
|
+
## Required Plan Format
|
|
1039
|
+
|
|
1040
|
+
Your response MUST be the complete plan in this EXACT format:
|
|
1041
|
+
|
|
1042
|
+
# Development Plan: [Project Name]
|
|
1043
|
+
|
|
1044
|
+
## Overview
|
|
1045
|
+
[2-3 sentence summary of what will be built]
|
|
1046
|
+
|
|
1047
|
+
## Milestone 1: [Name]
|
|
1048
|
+
**Description**: [What this milestone achieves]
|
|
1049
|
+
|
|
1050
|
+
### Task 1.1: [Actionable task name starting with verb]
|
|
1051
|
+
**Description**: [What this task accomplishes]
|
|
1052
|
+
**Files to create/modify**: [List specific TypeScript files in src/]
|
|
1053
|
+
**Acceptance Criteria**:
|
|
1054
|
+
- [Specific, testable criterion]
|
|
1055
|
+
- [Another criterion]
|
|
1056
|
+
|
|
1057
|
+
### Task 1.2: [Another actionable task]
|
|
1058
|
+
...
|
|
1059
|
+
|
|
1060
|
+
## Milestone 2: [Name]
|
|
1061
|
+
...
|
|
1062
|
+
|
|
1063
|
+
## Test Plan
|
|
1064
|
+
- Vitest for unit tests
|
|
1065
|
+
- Playwright for E2E tests
|
|
1066
|
+
|
|
1067
|
+
## Risks & Mitigations
|
|
1068
|
+
[Potential issues and how to address them]
|
|
1069
|
+
|
|
1070
|
+
## Requirements for Tasks
|
|
1071
|
+
|
|
1072
|
+
1. Each task MUST start with an action verb: Implement, Create, Build, Add, Configure, Set up, Write, Design, etc.
|
|
1073
|
+
2. Each task MUST be specific and implementable
|
|
1074
|
+
3. Each milestone MUST have at least 3-5 specific tasks
|
|
1075
|
+
4. The plan MUST have at least 3 milestones for any non-trivial project
|
|
1076
|
+
5. Files to create/modify MUST be listed for each task
|
|
1077
|
+
6. Acceptance criteria MUST be testable
|
|
1078
|
+
|
|
1079
|
+
IMPORTANT: Output the COMPLETE plan now. Start with "# Development Plan:" on the first line.
|
|
1080
|
+
`.trim();
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
/**
|
|
1084
|
+
* Create a development plan from a specification
|
|
1085
|
+
*
|
|
1086
|
+
* @param specification - The project specification
|
|
1087
|
+
* @param context - Additional context (existing code, etc.)
|
|
1088
|
+
* @param language - Target programming language (default: 'python')
|
|
1089
|
+
* @param onProgress - Progress callback
|
|
1090
|
+
*/
|
|
1091
|
+
export async function createPlan(
|
|
1092
|
+
specification: string,
|
|
1093
|
+
context: string = '',
|
|
1094
|
+
language: 'python' | 'typescript' | 'fullstack' = 'python',
|
|
1095
|
+
onProgress?: (message: string) => void
|
|
1096
|
+
): Promise<ClaudeExecuteResult> {
|
|
1097
|
+
const prompt = buildPlanPrompt(specification, context, language);
|
|
1098
|
+
|
|
1099
|
+
const result = await executePrompt(prompt, {
|
|
258
1100
|
allowedTools: ['Read', 'Glob'],
|
|
259
1101
|
permissionMode: 'plan',
|
|
1102
|
+
onProgress,
|
|
260
1103
|
});
|
|
1104
|
+
|
|
1105
|
+
// If Claude's response is conversational (describes the plan but doesn't contain it),
|
|
1106
|
+
// try to extract the plan from the file it may have created
|
|
1107
|
+
if (result.success && isConversationalResponse(result.response)) {
|
|
1108
|
+
onProgress?.('Detected conversational response, looking for plan file...');
|
|
1109
|
+
|
|
1110
|
+
// Try to find and read the plan file
|
|
1111
|
+
const planFilePath = extractPlanFilePath(result.response);
|
|
1112
|
+
|
|
1113
|
+
if (planFilePath) {
|
|
1114
|
+
try {
|
|
1115
|
+
onProgress?.(`Found plan file reference: ${planFilePath}`);
|
|
1116
|
+
const planContent = await fs.readFile(planFilePath, 'utf-8');
|
|
1117
|
+
|
|
1118
|
+
// Verify the plan content is actually a plan
|
|
1119
|
+
if (planContent.includes('# Development Plan') ||
|
|
1120
|
+
planContent.includes('## Milestone') ||
|
|
1121
|
+
planContent.includes('### Task')) {
|
|
1122
|
+
onProgress?.('Successfully extracted plan from file');
|
|
1123
|
+
return {
|
|
1124
|
+
...result,
|
|
1125
|
+
response: planContent,
|
|
1126
|
+
};
|
|
1127
|
+
}
|
|
1128
|
+
} catch (readError) {
|
|
1129
|
+
onProgress?.(`Could not read plan file: ${readError instanceof Error ? readError.message : 'Unknown error'}`);
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
// Also try to find any recent .claude/plans files
|
|
1134
|
+
try {
|
|
1135
|
+
const claudePlansDir = path.join(homedir(), '.claude', 'plans');
|
|
1136
|
+
const files = await fs.readdir(claudePlansDir);
|
|
1137
|
+
const mdFiles = files.filter(f => f.endsWith('.md'));
|
|
1138
|
+
|
|
1139
|
+
if (mdFiles.length > 0) {
|
|
1140
|
+
// Sort by modification time (most recent first)
|
|
1141
|
+
const fileStats = await Promise.all(
|
|
1142
|
+
mdFiles.map(async f => {
|
|
1143
|
+
const filePath = path.join(claudePlansDir, f);
|
|
1144
|
+
const stat = await fs.stat(filePath);
|
|
1145
|
+
return { name: f, path: filePath, mtime: stat.mtime };
|
|
1146
|
+
})
|
|
1147
|
+
);
|
|
1148
|
+
|
|
1149
|
+
fileStats.sort((a, b) => b.mtime.getTime() - a.mtime.getTime());
|
|
1150
|
+
|
|
1151
|
+
// Check the most recent file (created in the last 5 minutes)
|
|
1152
|
+
const recentFile = fileStats[0];
|
|
1153
|
+
const fiveMinutesAgo = Date.now() - 5 * 60 * 1000;
|
|
1154
|
+
|
|
1155
|
+
if (recentFile.mtime.getTime() > fiveMinutesAgo) {
|
|
1156
|
+
onProgress?.(`Found recent plan file: ${recentFile.name}`);
|
|
1157
|
+
const planContent = await fs.readFile(recentFile.path, 'utf-8');
|
|
1158
|
+
|
|
1159
|
+
if (planContent.includes('# Development Plan') ||
|
|
1160
|
+
planContent.includes('## Milestone') ||
|
|
1161
|
+
planContent.includes('### Task')) {
|
|
1162
|
+
onProgress?.('Successfully extracted plan from recent file');
|
|
1163
|
+
return {
|
|
1164
|
+
...result,
|
|
1165
|
+
response: planContent,
|
|
1166
|
+
};
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
}
|
|
1170
|
+
} catch {
|
|
1171
|
+
// Could not access .claude/plans directory
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
// Log warning that we couldn't extract the plan
|
|
1175
|
+
onProgress?.('WARNING: Could not extract actual plan content from file');
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
return result;
|
|
261
1179
|
}
|
|
262
1180
|
|
|
263
1181
|
/**
|
|
264
|
-
*
|
|
265
|
-
*
|
|
266
|
-
* @param originalPlan - The original plan
|
|
267
|
-
* @param feedback - Feedback to incorporate
|
|
268
|
-
* @param concerns - Specific concerns to address
|
|
1182
|
+
* Build revision prompt with language-specific instructions
|
|
269
1183
|
*/
|
|
270
|
-
|
|
1184
|
+
function buildRevisionPrompt(
|
|
271
1185
|
originalPlan: string,
|
|
272
1186
|
feedback: string,
|
|
273
|
-
concerns: string[]
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
1187
|
+
concerns: string[],
|
|
1188
|
+
language: 'python' | 'typescript' | 'fullstack'
|
|
1189
|
+
): string {
|
|
1190
|
+
const basePrompt = `
|
|
1191
|
+
CRITICAL: You must output the COMPLETE revised plan in your response.
|
|
1192
|
+
Do NOT describe what you changed - output the FULL plan with all changes incorporated.
|
|
1193
|
+
Do NOT say "Let me...", "I will...", "I've revised...", or any conversational text.
|
|
1194
|
+
Start your response directly with "# Development Plan:" and include the ENTIRE revised plan.
|
|
277
1195
|
|
|
278
|
-
## Original Plan
|
|
1196
|
+
## Original Plan to Revise
|
|
279
1197
|
${originalPlan}
|
|
280
1198
|
|
|
281
|
-
## Feedback
|
|
1199
|
+
## Feedback to Address
|
|
282
1200
|
${feedback}
|
|
283
1201
|
|
|
284
1202
|
## Specific Concerns to Address
|
|
285
1203
|
${concerns.map((c, i) => `${i + 1}. ${c}`).join('\n')}
|
|
286
1204
|
|
|
287
1205
|
## Instructions
|
|
288
|
-
1. Address each concern
|
|
289
|
-
2. Maintain the same plan structure
|
|
290
|
-
3.
|
|
291
|
-
4.
|
|
1206
|
+
1. Address each concern by incorporating changes into the plan
|
|
1207
|
+
2. Maintain the same plan structure (Overview, Milestones, Tasks, Test Plan, Risks)
|
|
1208
|
+
3. Output the COMPLETE revised plan - not just the changes
|
|
1209
|
+
4. Start with "# Development Plan:" and include ALL milestones and tasks
|
|
292
1210
|
`.trim();
|
|
293
1211
|
|
|
294
|
-
|
|
1212
|
+
if (language === 'fullstack') {
|
|
1213
|
+
return `
|
|
1214
|
+
${basePrompt}
|
|
1215
|
+
|
|
1216
|
+
## FULLSTACK-SPECIFIC REQUIREMENTS:
|
|
1217
|
+
- Maintain [FE], [BE], [INT] tags on all tasks
|
|
1218
|
+
- Keep App: field (frontend/backend/unified) for each task
|
|
1219
|
+
- Group tasks under "Frontend Tasks", "Backend Tasks", "Integration Tasks" headers
|
|
1220
|
+
- Ensure file paths use apps/frontend/ or apps/backend/ prefixes
|
|
1221
|
+
- If adding new tasks, tag them appropriately
|
|
1222
|
+
|
|
1223
|
+
OUTPUT THE COMPLETE REVISED PLAN NOW:
|
|
1224
|
+
`.trim();
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
return `${basePrompt}
|
|
1228
|
+
|
|
1229
|
+
OUTPUT THE COMPLETE REVISED PLAN NOW:
|
|
1230
|
+
`.trim();
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
/**
|
|
1234
|
+
* Revise a plan based on feedback
|
|
1235
|
+
*
|
|
1236
|
+
* @param originalPlan - The original plan
|
|
1237
|
+
* @param feedback - Feedback to incorporate
|
|
1238
|
+
* @param concerns - Specific concerns to address
|
|
1239
|
+
* @param language - Target programming language (default: 'python')
|
|
1240
|
+
* @param onProgress - Progress callback
|
|
1241
|
+
*/
|
|
1242
|
+
export async function revisePlan(
|
|
1243
|
+
originalPlan: string,
|
|
1244
|
+
feedback: string,
|
|
1245
|
+
concerns: string[],
|
|
1246
|
+
language: 'python' | 'typescript' | 'fullstack' = 'python',
|
|
1247
|
+
onProgress?: (message: string) => void
|
|
1248
|
+
): Promise<ClaudeExecuteResult> {
|
|
1249
|
+
const prompt = buildRevisionPrompt(originalPlan, feedback, concerns, language);
|
|
1250
|
+
|
|
1251
|
+
onProgress?.('Claude is revising the plan...');
|
|
1252
|
+
|
|
1253
|
+
const result = await executePrompt(prompt, {
|
|
295
1254
|
allowedTools: [],
|
|
296
1255
|
permissionMode: 'plan',
|
|
1256
|
+
onProgress,
|
|
297
1257
|
});
|
|
1258
|
+
|
|
1259
|
+
// Check if response is conversational and try to extract actual plan
|
|
1260
|
+
if (result.success && isConversationalResponse(result.response)) {
|
|
1261
|
+
// Try to find the plan file
|
|
1262
|
+
const planFilePath = extractPlanFilePath(result.response);
|
|
1263
|
+
|
|
1264
|
+
if (planFilePath) {
|
|
1265
|
+
try {
|
|
1266
|
+
const planContent = await fs.readFile(planFilePath, 'utf-8');
|
|
1267
|
+
if (planContent.includes('# Development Plan') ||
|
|
1268
|
+
planContent.includes('## Milestone') ||
|
|
1269
|
+
planContent.includes('### Task')) {
|
|
1270
|
+
return {
|
|
1271
|
+
...result,
|
|
1272
|
+
response: planContent,
|
|
1273
|
+
};
|
|
1274
|
+
}
|
|
1275
|
+
} catch {
|
|
1276
|
+
// Could not read file, fall through
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
// Try recent .claude/plans files
|
|
1281
|
+
try {
|
|
1282
|
+
const claudePlansDir = path.join(homedir(), '.claude', 'plans');
|
|
1283
|
+
const files = await fs.readdir(claudePlansDir);
|
|
1284
|
+
const mdFiles = files.filter(f => f.endsWith('.md'));
|
|
1285
|
+
|
|
1286
|
+
if (mdFiles.length > 0) {
|
|
1287
|
+
const fileStats = await Promise.all(
|
|
1288
|
+
mdFiles.map(async f => {
|
|
1289
|
+
const filePath = path.join(claudePlansDir, f);
|
|
1290
|
+
const stat = await fs.stat(filePath);
|
|
1291
|
+
return { name: f, path: filePath, mtime: stat.mtime };
|
|
1292
|
+
})
|
|
1293
|
+
);
|
|
1294
|
+
|
|
1295
|
+
fileStats.sort((a, b) => b.mtime.getTime() - a.mtime.getTime());
|
|
1296
|
+
|
|
1297
|
+
const recentFile = fileStats[0];
|
|
1298
|
+
const twoMinutesAgo = Date.now() - 2 * 60 * 1000;
|
|
1299
|
+
|
|
1300
|
+
if (recentFile.mtime.getTime() > twoMinutesAgo) {
|
|
1301
|
+
const planContent = await fs.readFile(recentFile.path, 'utf-8');
|
|
1302
|
+
if (planContent.includes('# Development Plan') ||
|
|
1303
|
+
planContent.includes('## Milestone') ||
|
|
1304
|
+
planContent.includes('### Task')) {
|
|
1305
|
+
return {
|
|
1306
|
+
...result,
|
|
1307
|
+
response: planContent,
|
|
1308
|
+
};
|
|
1309
|
+
}
|
|
1310
|
+
}
|
|
1311
|
+
}
|
|
1312
|
+
} catch {
|
|
1313
|
+
// Could not access .claude/plans directory
|
|
1314
|
+
}
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
return result;
|
|
298
1318
|
}
|