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.
Files changed (216) hide show
  1. package/.env.example +24 -1
  2. package/CONTRIBUTING.md +275 -0
  3. package/OPEN_SOURCE_MANIFESTO.md +172 -0
  4. package/README.md +832 -123
  5. package/dist/adapters/claude.d.ts +19 -4
  6. package/dist/adapters/claude.d.ts.map +1 -1
  7. package/dist/adapters/claude.js +908 -42
  8. package/dist/adapters/claude.js.map +1 -1
  9. package/dist/adapters/gemini.d.ts +55 -0
  10. package/dist/adapters/gemini.d.ts.map +1 -0
  11. package/dist/adapters/gemini.js +318 -0
  12. package/dist/adapters/gemini.js.map +1 -0
  13. package/dist/adapters/grok.d.ts +73 -0
  14. package/dist/adapters/grok.d.ts.map +1 -0
  15. package/dist/adapters/grok.js +430 -0
  16. package/dist/adapters/grok.js.map +1 -0
  17. package/dist/adapters/openai.d.ts +1 -1
  18. package/dist/adapters/openai.d.ts.map +1 -1
  19. package/dist/adapters/openai.js +47 -8
  20. package/dist/adapters/openai.js.map +1 -1
  21. package/dist/auth/claude.d.ts +11 -9
  22. package/dist/auth/claude.d.ts.map +1 -1
  23. package/dist/auth/claude.js +107 -71
  24. package/dist/auth/claude.js.map +1 -1
  25. package/dist/auth/gemini.d.ts +58 -0
  26. package/dist/auth/gemini.d.ts.map +1 -0
  27. package/dist/auth/gemini.js +172 -0
  28. package/dist/auth/gemini.js.map +1 -0
  29. package/dist/auth/grok.d.ts +73 -0
  30. package/dist/auth/grok.d.ts.map +1 -0
  31. package/dist/auth/grok.js +211 -0
  32. package/dist/auth/grok.js.map +1 -0
  33. package/dist/auth/index.d.ts +14 -7
  34. package/dist/auth/index.d.ts.map +1 -1
  35. package/dist/auth/index.js +41 -6
  36. package/dist/auth/index.js.map +1 -1
  37. package/dist/auth/keychain.d.ts +20 -7
  38. package/dist/auth/keychain.d.ts.map +1 -1
  39. package/dist/auth/keychain.js +85 -29
  40. package/dist/auth/keychain.js.map +1 -1
  41. package/dist/auth/openai.d.ts +2 -2
  42. package/dist/auth/openai.d.ts.map +1 -1
  43. package/dist/auth/openai.js +30 -32
  44. package/dist/auth/openai.js.map +1 -1
  45. package/dist/cli/commands/auth.d.ts +1 -1
  46. package/dist/cli/commands/auth.d.ts.map +1 -1
  47. package/dist/cli/commands/auth.js +79 -8
  48. package/dist/cli/commands/auth.js.map +1 -1
  49. package/dist/cli/commands/create.d.ts.map +1 -1
  50. package/dist/cli/commands/create.js +15 -4
  51. package/dist/cli/commands/create.js.map +1 -1
  52. package/dist/cli/interactive.d.ts.map +1 -1
  53. package/dist/cli/interactive.js +1494 -114
  54. package/dist/cli/interactive.js.map +1 -1
  55. package/dist/config/defaults.d.ts +9 -1
  56. package/dist/config/defaults.d.ts.map +1 -1
  57. package/dist/config/defaults.js +19 -2
  58. package/dist/config/defaults.js.map +1 -1
  59. package/dist/config/index.d.ts +19 -0
  60. package/dist/config/index.d.ts.map +1 -1
  61. package/dist/config/index.js +33 -1
  62. package/dist/config/index.js.map +1 -1
  63. package/dist/config/schema.d.ts +47 -0
  64. package/dist/config/schema.d.ts.map +1 -1
  65. package/dist/config/schema.js +29 -1
  66. package/dist/config/schema.js.map +1 -1
  67. package/dist/generators/fullstack.d.ts +32 -0
  68. package/dist/generators/fullstack.d.ts.map +1 -0
  69. package/dist/generators/fullstack.js +497 -0
  70. package/dist/generators/fullstack.js.map +1 -0
  71. package/dist/generators/index.d.ts +4 -3
  72. package/dist/generators/index.d.ts.map +1 -1
  73. package/dist/generators/index.js +15 -1
  74. package/dist/generators/index.js.map +1 -1
  75. package/dist/generators/python.d.ts +17 -1
  76. package/dist/generators/python.d.ts.map +1 -1
  77. package/dist/generators/python.js +34 -20
  78. package/dist/generators/python.js.map +1 -1
  79. package/dist/generators/templates/fullstack.d.ts +113 -0
  80. package/dist/generators/templates/fullstack.d.ts.map +1 -0
  81. package/dist/generators/templates/fullstack.js +1004 -0
  82. package/dist/generators/templates/fullstack.js.map +1 -0
  83. package/dist/generators/typescript.d.ts +19 -1
  84. package/dist/generators/typescript.d.ts.map +1 -1
  85. package/dist/generators/typescript.js +37 -20
  86. package/dist/generators/typescript.js.map +1 -1
  87. package/dist/state/index.d.ts +108 -0
  88. package/dist/state/index.d.ts.map +1 -1
  89. package/dist/state/index.js +551 -4
  90. package/dist/state/index.js.map +1 -1
  91. package/dist/state/registry.d.ts +52 -0
  92. package/dist/state/registry.d.ts.map +1 -0
  93. package/dist/state/registry.js +215 -0
  94. package/dist/state/registry.js.map +1 -0
  95. package/dist/types/cli.d.ts +8 -0
  96. package/dist/types/cli.d.ts.map +1 -1
  97. package/dist/types/cli.js.map +1 -1
  98. package/dist/types/consensus.d.ts +186 -4
  99. package/dist/types/consensus.d.ts.map +1 -1
  100. package/dist/types/consensus.js +35 -3
  101. package/dist/types/consensus.js.map +1 -1
  102. package/dist/types/project.d.ts +76 -0
  103. package/dist/types/project.d.ts.map +1 -1
  104. package/dist/types/project.js +1 -1
  105. package/dist/types/project.js.map +1 -1
  106. package/dist/types/workflow.d.ts +217 -16
  107. package/dist/types/workflow.d.ts.map +1 -1
  108. package/dist/types/workflow.js +40 -1
  109. package/dist/types/workflow.js.map +1 -1
  110. package/dist/workflow/auto-fix.d.ts +45 -0
  111. package/dist/workflow/auto-fix.d.ts.map +1 -0
  112. package/dist/workflow/auto-fix.js +274 -0
  113. package/dist/workflow/auto-fix.js.map +1 -0
  114. package/dist/workflow/consensus.d.ts +70 -2
  115. package/dist/workflow/consensus.d.ts.map +1 -1
  116. package/dist/workflow/consensus.js +872 -17
  117. package/dist/workflow/consensus.js.map +1 -1
  118. package/dist/workflow/execution-mode.d.ts +10 -4
  119. package/dist/workflow/execution-mode.d.ts.map +1 -1
  120. package/dist/workflow/execution-mode.js +547 -58
  121. package/dist/workflow/execution-mode.js.map +1 -1
  122. package/dist/workflow/index.d.ts +14 -2
  123. package/dist/workflow/index.d.ts.map +1 -1
  124. package/dist/workflow/index.js +69 -6
  125. package/dist/workflow/index.js.map +1 -1
  126. package/dist/workflow/milestone-workflow.d.ts +34 -0
  127. package/dist/workflow/milestone-workflow.d.ts.map +1 -0
  128. package/dist/workflow/milestone-workflow.js +414 -0
  129. package/dist/workflow/milestone-workflow.js.map +1 -0
  130. package/dist/workflow/plan-mode.d.ts +80 -3
  131. package/dist/workflow/plan-mode.d.ts.map +1 -1
  132. package/dist/workflow/plan-mode.js +767 -49
  133. package/dist/workflow/plan-mode.js.map +1 -1
  134. package/dist/workflow/plan-storage.d.ts +386 -0
  135. package/dist/workflow/plan-storage.d.ts.map +1 -0
  136. package/dist/workflow/plan-storage.js +878 -0
  137. package/dist/workflow/plan-storage.js.map +1 -0
  138. package/dist/workflow/project-verification.d.ts +37 -0
  139. package/dist/workflow/project-verification.d.ts.map +1 -0
  140. package/dist/workflow/project-verification.js +381 -0
  141. package/dist/workflow/project-verification.js.map +1 -0
  142. package/dist/workflow/task-workflow.d.ts +37 -0
  143. package/dist/workflow/task-workflow.d.ts.map +1 -0
  144. package/dist/workflow/task-workflow.js +386 -0
  145. package/dist/workflow/task-workflow.js.map +1 -0
  146. package/dist/workflow/test-runner.d.ts +9 -0
  147. package/dist/workflow/test-runner.d.ts.map +1 -1
  148. package/dist/workflow/test-runner.js +101 -5
  149. package/dist/workflow/test-runner.js.map +1 -1
  150. package/dist/workflow/ui-designer.d.ts +82 -0
  151. package/dist/workflow/ui-designer.d.ts.map +1 -0
  152. package/dist/workflow/ui-designer.js +234 -0
  153. package/dist/workflow/ui-designer.js.map +1 -0
  154. package/dist/workflow/ui-setup.d.ts +58 -0
  155. package/dist/workflow/ui-setup.d.ts.map +1 -0
  156. package/dist/workflow/ui-setup.js +685 -0
  157. package/dist/workflow/ui-setup.js.map +1 -0
  158. package/dist/workflow/ui-verification.d.ts +114 -0
  159. package/dist/workflow/ui-verification.d.ts.map +1 -0
  160. package/dist/workflow/ui-verification.js +258 -0
  161. package/dist/workflow/ui-verification.js.map +1 -0
  162. package/dist/workflow/workflow-logger.d.ts +110 -0
  163. package/dist/workflow/workflow-logger.d.ts.map +1 -0
  164. package/dist/workflow/workflow-logger.js +267 -0
  165. package/dist/workflow/workflow-logger.js.map +1 -0
  166. package/dist/workflow/workspace-manager.d.ts +342 -0
  167. package/dist/workflow/workspace-manager.d.ts.map +1 -0
  168. package/dist/workflow/workspace-manager.js +733 -0
  169. package/dist/workflow/workspace-manager.js.map +1 -0
  170. package/package.json +2 -2
  171. package/src/adapters/claude.ts +1067 -47
  172. package/src/adapters/gemini.ts +373 -0
  173. package/src/adapters/grok.ts +492 -0
  174. package/src/adapters/openai.ts +48 -9
  175. package/src/auth/claude.ts +120 -78
  176. package/src/auth/gemini.ts +207 -0
  177. package/src/auth/grok.ts +255 -0
  178. package/src/auth/index.ts +47 -9
  179. package/src/auth/keychain.ts +95 -28
  180. package/src/auth/openai.ts +29 -36
  181. package/src/cli/commands/auth.ts +89 -10
  182. package/src/cli/commands/create.ts +13 -4
  183. package/src/cli/interactive.ts +1774 -142
  184. package/src/config/defaults.ts +19 -2
  185. package/src/config/index.ts +36 -1
  186. package/src/config/schema.ts +30 -1
  187. package/src/generators/fullstack.ts +551 -0
  188. package/src/generators/index.ts +25 -1
  189. package/src/generators/python.ts +65 -20
  190. package/src/generators/templates/fullstack.ts +1047 -0
  191. package/src/generators/typescript.ts +69 -20
  192. package/src/state/index.ts +713 -4
  193. package/src/state/registry.ts +278 -0
  194. package/src/types/cli.ts +8 -0
  195. package/src/types/consensus.ts +197 -6
  196. package/src/types/project.ts +82 -1
  197. package/src/types/workflow.ts +90 -1
  198. package/src/workflow/auto-fix.ts +340 -0
  199. package/src/workflow/consensus.ts +1180 -16
  200. package/src/workflow/execution-mode.ts +673 -74
  201. package/src/workflow/index.ts +95 -6
  202. package/src/workflow/milestone-workflow.ts +576 -0
  203. package/src/workflow/plan-mode.ts +924 -50
  204. package/src/workflow/plan-storage.ts +1282 -0
  205. package/src/workflow/project-verification.ts +471 -0
  206. package/src/workflow/task-workflow.ts +528 -0
  207. package/src/workflow/test-runner.ts +120 -5
  208. package/src/workflow/ui-designer.ts +337 -0
  209. package/src/workflow/ui-setup.ts +797 -0
  210. package/src/workflow/ui-verification.ts +357 -0
  211. package/src/workflow/workflow-logger.ts +353 -0
  212. package/src/workflow/workspace-manager.ts +912 -0
  213. package/tests/config/config.test.ts +1 -1
  214. package/tests/types/consensus.test.ts +3 -3
  215. package/tests/workflow/plan-mode.test.ts +213 -0
  216. package/tests/workflow/test-runner.test.ts +5 -3
@@ -2,7 +2,227 @@
2
2
  * Claude Agent SDK adapter
3
3
  * Wraps the Claude Agent SDK for code execution and generation
4
4
  */
5
+ import { promises as fs } from 'node:fs';
6
+ import path from 'node:path';
7
+ import { homedir } from 'os';
5
8
  import { query } from '@anthropic-ai/claude-agent-sdk';
9
+ /**
10
+ * Log directory for debug information
11
+ */
12
+ const LOG_DIR = path.join(homedir(), '.popeye', 'logs');
13
+ const DEFAULT_RATE_LIMIT_CONFIG = {
14
+ maxRetries: 3,
15
+ baseWaitMs: 60_000, // 1 minute
16
+ maxWaitMs: 10 * 60_000, // 10 minutes max - don't wait longer than this
17
+ };
18
+ /**
19
+ * Parse rate limit reset time from error message
20
+ * Messages like: "You've hit your limit · resets 3pm (Asia/Jerusalem)"
21
+ */
22
+ function parseRateLimitResetTime(message) {
23
+ // Try to parse time like "3pm", "3:30pm", "15:00"
24
+ const timePatterns = [
25
+ /resets?\s+(\d{1,2}):?(\d{2})?\s*(am|pm)?/i,
26
+ /until\s+(\d{1,2}):?(\d{2})?\s*(am|pm)?/i,
27
+ /wait\s+until\s+(\d{1,2}):?(\d{2})?\s*(am|pm)?/i,
28
+ ];
29
+ for (const pattern of timePatterns) {
30
+ const match = message.match(pattern);
31
+ if (match) {
32
+ let hours = parseInt(match[1], 10);
33
+ const minutes = match[2] ? parseInt(match[2], 10) : 0;
34
+ const ampm = match[3]?.toLowerCase();
35
+ if (ampm === 'pm' && hours < 12)
36
+ hours += 12;
37
+ if (ampm === 'am' && hours === 12)
38
+ hours = 0;
39
+ const resetTime = new Date();
40
+ resetTime.setHours(hours, minutes, 0, 0);
41
+ // If the time has passed today, assume tomorrow
42
+ if (resetTime.getTime() <= Date.now()) {
43
+ resetTime.setDate(resetTime.getDate() + 1);
44
+ }
45
+ return resetTime;
46
+ }
47
+ }
48
+ // Try to parse duration like "30 minutes", "1 hour"
49
+ const durationPatterns = [
50
+ /(\d+)\s*minutes?/i,
51
+ /(\d+)\s*hours?/i,
52
+ ];
53
+ for (let i = 0; i < durationPatterns.length; i++) {
54
+ const match = message.match(durationPatterns[i]);
55
+ if (match) {
56
+ const value = parseInt(match[1], 10);
57
+ const multiplier = i === 0 ? 60_000 : 60 * 60_000; // minutes or hours
58
+ return new Date(Date.now() + value * multiplier);
59
+ }
60
+ }
61
+ return null;
62
+ }
63
+ /**
64
+ * Format wait time for display
65
+ */
66
+ function formatWaitTime(ms) {
67
+ if (ms < 60_000) {
68
+ return `${Math.ceil(ms / 1000)} seconds`;
69
+ }
70
+ else if (ms < 60 * 60_000) {
71
+ const minutes = Math.ceil(ms / 60_000);
72
+ return `${minutes} minute${minutes > 1 ? 's' : ''}`;
73
+ }
74
+ else {
75
+ const hours = Math.floor(ms / (60 * 60_000));
76
+ const minutes = Math.ceil((ms % (60 * 60_000)) / 60_000);
77
+ return `${hours} hour${hours > 1 ? 's' : ''}${minutes > 0 ? ` ${minutes} minute${minutes > 1 ? 's' : ''}` : ''}`;
78
+ }
79
+ }
80
+ /**
81
+ * Sleep for a specified duration with progress updates
82
+ */
83
+ async function sleepWithProgress(ms, onProgress) {
84
+ const startTime = Date.now();
85
+ const endTime = startTime + ms;
86
+ const updateInterval = Math.min(ms / 10, 60_000); // Update every 10% or minute, whichever is smaller
87
+ while (Date.now() < endTime) {
88
+ const remaining = endTime - Date.now();
89
+ if (remaining <= 0)
90
+ break;
91
+ onProgress?.(`Rate limit: waiting ${formatWaitTime(remaining)} before retry...`);
92
+ const sleepTime = Math.min(updateInterval, remaining);
93
+ await new Promise(resolve => setTimeout(resolve, sleepTime));
94
+ }
95
+ }
96
+ /**
97
+ * Extract just the rate limit message from a larger string
98
+ * e.g., "Some content... You've hit your limit · resets 3pm (Asia/Jerusalem)" -> "You've hit your limit · resets 3pm (Asia/Jerusalem)"
99
+ */
100
+ function extractRateLimitMessage(content) {
101
+ // Look for specific rate limit error message patterns
102
+ // These patterns are designed to match actual error messages, not plan content
103
+ const patterns = [
104
+ // "You've hit your limit" patterns - common Claude error
105
+ /You['']ve hit your limit[^.\n]*(?:\.[\s]*(?:resets?|try again)[^.\n]*)?/i,
106
+ // "Rate limit exceeded" - explicit error message
107
+ /rate limit exceeded[^.\n]*/i,
108
+ // "rate limited" as verb - "you have been rate limited"
109
+ /(?:you\s+(?:have\s+)?(?:been\s+)?)?rate\s+limited[^.\n]*/i,
110
+ // "too many requests" - HTTP 429 style
111
+ /too many requests[^.\n]*/i,
112
+ // "quota exceeded" - usage limit
113
+ /quota exceeded[^.\n]*/i,
114
+ // "API rate limit" - specific to API errors
115
+ /api\s+rate\s+limit[^.\n]*/i,
116
+ // "request limit" patterns
117
+ /request\s+limit[^.\n]*(?:reached|exceeded|hit)[^.\n]*/i,
118
+ // "usage limit" patterns
119
+ /usage\s+limit[^.\n]*(?:reached|exceeded|hit)[^.\n]*/i,
120
+ ];
121
+ for (const pattern of patterns) {
122
+ const match = content.match(pattern);
123
+ if (match) {
124
+ // Limit matched content to 200 chars to prevent capturing run-on text
125
+ const matched = match[0].trim();
126
+ return matched.length > 200 ? matched.slice(0, 197) + '...' : matched;
127
+ }
128
+ }
129
+ // If no pattern matches, try to find the first line that looks like an error
130
+ const lines = content.split('\n').filter(line => line.trim());
131
+ for (const line of lines.slice(0, 5)) {
132
+ const trimmedLine = line.trim();
133
+ // Look for lines that start with error indicators
134
+ if (/^(error|failed|limit|exceeded|denied)/i.test(trimmedLine)) {
135
+ return trimmedLine.length > 200 ? trimmedLine.slice(0, 197) + '...' : trimmedLine;
136
+ }
137
+ }
138
+ // If content is short, return it (but cap at 200 chars)
139
+ if (content.length < 200) {
140
+ return content;
141
+ }
142
+ // Otherwise return a generic message - don't include potentially huge content
143
+ return 'Rate limit detected (details unavailable)';
144
+ }
145
+ /**
146
+ * Check if an error indicates a rate limit
147
+ * Uses specific patterns to avoid false positives from plan content mentioning rate limiting
148
+ */
149
+ function isRateLimitError(error, message) {
150
+ // Patterns that indicate actual rate limit errors (not just mentions of rate limiting)
151
+ // These are more specific than just "rate limit" to avoid matching plan content
152
+ const rateLimitPatterns = [
153
+ /you['']ve hit your limit/i,
154
+ /rate_limit_exceeded/i,
155
+ /rate limit exceeded/i,
156
+ /you have been rate limited/i,
157
+ /too many requests/i,
158
+ /quota exceeded/i,
159
+ /\b429\b/, // HTTP 429 status code
160
+ /rate limited/i, // "rate limited" as a verb phrase
161
+ /api rate limit/i,
162
+ /request limit reached/i,
163
+ /usage limit exceeded/i,
164
+ /limit reached.*try again/i,
165
+ /exceeded.*limit.*retry/i,
166
+ ];
167
+ const checkString = (str) => {
168
+ return rateLimitPatterns.some(pattern => pattern.test(str));
169
+ };
170
+ if (message && checkString(message))
171
+ return true;
172
+ if (error instanceof Error) {
173
+ if (checkString(error.message))
174
+ return true;
175
+ if ('code' in error && typeof error.code === 'string' && checkString(error.code))
176
+ return true;
177
+ }
178
+ if (typeof error === 'object' && error !== null) {
179
+ const obj = error;
180
+ if (typeof obj.error === 'string' && checkString(obj.error))
181
+ return true;
182
+ if (typeof obj.code === 'string' && checkString(obj.code))
183
+ return true;
184
+ if (typeof obj.message === 'string' && checkString(obj.message))
185
+ return true;
186
+ }
187
+ return false;
188
+ }
189
+ /**
190
+ * Write error details to a log file for debugging
191
+ */
192
+ async function logErrorDetails(error, context) {
193
+ try {
194
+ await fs.mkdir(LOG_DIR, { recursive: true });
195
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
196
+ const logFile = path.join(LOG_DIR, `claude-error-${timestamp}.log`);
197
+ const errorDetails = [
198
+ '='.repeat(80),
199
+ `CLAUDE ERROR LOG - ${new Date().toISOString()}`,
200
+ '='.repeat(80),
201
+ '',
202
+ '## Error Details',
203
+ `Type: ${error instanceof Error ? error.constructor.name : typeof error}`,
204
+ `Message: ${error instanceof Error ? error.message : String(error)}`,
205
+ '',
206
+ ];
207
+ if (error instanceof Error && error.stack) {
208
+ errorDetails.push('## Stack Trace', error.stack, '');
209
+ }
210
+ if (context.prompt) {
211
+ errorDetails.push('## Prompt (truncated)', context.prompt.slice(0, 2000), '');
212
+ }
213
+ if (context.response) {
214
+ errorDetails.push('## Response Before Error (truncated)', context.response.slice(-2000), '');
215
+ }
216
+ if (context.lastMessages && context.lastMessages.length > 0) {
217
+ errorDetails.push('## Last Messages', JSON.stringify(context.lastMessages.slice(-5), null, 2), '');
218
+ }
219
+ await fs.writeFile(logFile, errorDetails.join('\n'), 'utf-8');
220
+ return logFile;
221
+ }
222
+ catch {
223
+ return '';
224
+ }
225
+ }
6
226
  /**
7
227
  * Default allowed tools for autonomous operation
8
228
  */
@@ -19,18 +239,18 @@ export const DEFAULT_ALLOWED_TOOLS = [
19
239
  'TodoWrite',
20
240
  ];
21
241
  /**
22
- * Execute a prompt through the Claude Agent SDK
23
- *
24
- * @param prompt - The prompt to execute
25
- * @param options - Execution options
26
- * @returns The execution result
242
+ * Execute a prompt through the Claude Agent SDK (internal implementation)
27
243
  */
28
- export async function executePrompt(prompt, options = {}) {
29
- const { cwd, allowedTools = DEFAULT_ALLOWED_TOOLS, permissionMode = 'bypassPermissions', systemPrompt, onMessage, } = options;
244
+ async function executePromptInternal(prompt, options = {}) {
245
+ const { cwd, allowedTools = DEFAULT_ALLOWED_TOOLS, permissionMode = 'bypassPermissions', systemPrompt, onMessage, onProgress, } = options;
30
246
  const toolCalls = [];
247
+ const recentMessages = [];
31
248
  let response = '';
32
249
  let error;
250
+ let rateLimitInfo;
251
+ let lastProgressTime = Date.now();
33
252
  try {
253
+ onProgress?.('Connecting to Claude...');
34
254
  const result = query({
35
255
  prompt,
36
256
  options: {
@@ -40,49 +260,279 @@ export async function executePrompt(prompt, options = {}) {
40
260
  systemPrompt: systemPrompt || { type: 'preset', preset: 'claude_code' },
41
261
  },
42
262
  });
263
+ onProgress?.('Claude is thinking...');
43
264
  for await (const message of result) {
265
+ // Keep track of recent messages for error logging
266
+ recentMessages.push(message);
267
+ if (recentMessages.length > 10) {
268
+ recentMessages.shift();
269
+ }
44
270
  // Call the message handler if provided
45
271
  if (onMessage) {
46
272
  onMessage(message);
47
273
  }
274
+ // Report progress based on message type
275
+ const now = Date.now();
276
+ if (now - lastProgressTime > 2000) {
277
+ // Report progress every 2 seconds
278
+ lastProgressTime = now;
279
+ }
280
+ // Check for rate limit in message error field
281
+ const messageWithError = message;
282
+ if (messageWithError.error) {
283
+ const errorStr = typeof messageWithError.error === 'string'
284
+ ? messageWithError.error
285
+ : messageWithError.error.message || '';
286
+ if (isRateLimitError(null, errorStr)) {
287
+ // Extract rate limit message from response content
288
+ let rateLimitMessage = errorStr;
289
+ if (messageWithError.message?.content) {
290
+ const textContent = messageWithError.message.content.find(c => c.text);
291
+ if (textContent?.text) {
292
+ rateLimitMessage = textContent.text;
293
+ }
294
+ }
295
+ const extractedMessage = extractRateLimitMessage(rateLimitMessage);
296
+ rateLimitInfo = {
297
+ isRateLimit: true,
298
+ resetTime: parseRateLimitResetTime(rateLimitMessage) ?? undefined,
299
+ message: extractedMessage,
300
+ };
301
+ error = `Rate limit exceeded: ${extractedMessage}`;
302
+ onProgress?.(`Rate limit hit: ${rateLimitMessage}`);
303
+ continue;
304
+ }
305
+ }
48
306
  // Process different message types
49
307
  if (message.type === 'assistant') {
50
308
  const assistantMessage = message;
51
309
  if (typeof assistantMessage.message.content === 'string') {
52
310
  response += assistantMessage.message.content;
311
+ // Check for rate limit in text response
312
+ if (isRateLimitError(null, assistantMessage.message.content)) {
313
+ const extractedMsg = extractRateLimitMessage(assistantMessage.message.content);
314
+ rateLimitInfo = {
315
+ isRateLimit: true,
316
+ resetTime: parseRateLimitResetTime(assistantMessage.message.content) ?? undefined,
317
+ message: extractedMsg,
318
+ };
319
+ error = `Rate limit exceeded: ${extractedMsg}`;
320
+ onProgress?.(`Rate limit hit: ${assistantMessage.message.content}`);
321
+ }
322
+ else {
323
+ onProgress?.('Claude is writing...');
324
+ }
53
325
  }
54
326
  else if (Array.isArray(assistantMessage.message.content)) {
55
327
  for (const block of assistantMessage.message.content) {
56
328
  if (block.type === 'text' && block.text) {
57
329
  response += block.text;
330
+ // Check for rate limit in text block
331
+ if (isRateLimitError(null, block.text)) {
332
+ const extractedBlockMsg = extractRateLimitMessage(block.text);
333
+ rateLimitInfo = {
334
+ isRateLimit: true,
335
+ resetTime: parseRateLimitResetTime(block.text) ?? undefined,
336
+ message: extractedBlockMsg,
337
+ };
338
+ error = `Rate limit exceeded: ${extractedBlockMsg}`;
339
+ onProgress?.(`Rate limit hit: ${block.text}`);
340
+ }
341
+ }
342
+ if (block.type === 'tool_use') {
343
+ const toolBlock = block;
344
+ onProgress?.(`Using tool: ${toolBlock.name || 'unknown'}...`);
58
345
  }
59
346
  }
60
347
  }
61
348
  }
62
349
  else if (message.type === 'result') {
63
- // Handle result messages which may contain tool information
350
+ // Handle result messages which may contain tool information or errors
64
351
  const resultMessage = message;
65
- if (resultMessage.error) {
66
- error = resultMessage.error.message;
352
+ if (resultMessage.error && !rateLimitInfo) {
353
+ const errMsg = resultMessage.error.message || 'Unknown error';
354
+ const errCode = resultMessage.error.code || 'ERROR';
355
+ error = `${errCode}: ${errMsg}`;
356
+ onProgress?.(`Claude returned error: ${error}`);
67
357
  }
68
358
  }
359
+ // Check for any error property on the message (handles various error formats)
360
+ if (messageWithError.error && !error && !rateLimitInfo) {
361
+ const errMsg = typeof messageWithError.error === 'string'
362
+ ? messageWithError.error
363
+ : messageWithError.error.message || 'Unknown error';
364
+ const errCode = typeof messageWithError.error === 'object'
365
+ ? messageWithError.error.code || 'ERROR'
366
+ : 'ERROR';
367
+ error = `${errCode}: ${errMsg}`;
368
+ onProgress?.(`Error detected: ${error}`);
369
+ }
69
370
  }
371
+ onProgress?.('Claude finished');
70
372
  return {
71
373
  success: !error,
72
374
  response: response.trim(),
73
375
  toolCalls,
74
376
  error,
377
+ rateLimitInfo,
75
378
  };
76
379
  }
77
380
  catch (err) {
381
+ // First, check if we already detected a rate limit during message processing
382
+ // This happens when rate limit is detected in the stream but process still exits with code 1
383
+ if (rateLimitInfo?.isRateLimit) {
384
+ onProgress?.(`Rate limit detected (process exited): ${rateLimitInfo.message || 'Unknown'}`);
385
+ return {
386
+ success: false,
387
+ response: response.trim(),
388
+ toolCalls,
389
+ error: `Rate limit exceeded: ${rateLimitInfo.message || 'Rate limit hit'}`,
390
+ rateLimitInfo,
391
+ };
392
+ }
393
+ // Check if the exception itself indicates a rate limit
394
+ const errMsg = err instanceof Error ? err.message : String(err);
395
+ if (isRateLimitError(err, errMsg) || isRateLimitError(null, response)) {
396
+ const combinedMessage = response || errMsg;
397
+ const extractedRateLimitMsg = extractRateLimitMessage(combinedMessage);
398
+ return {
399
+ success: false,
400
+ response: response.trim(),
401
+ toolCalls,
402
+ error: `Rate limit exceeded: ${extractedRateLimitMsg}`,
403
+ rateLimitInfo: {
404
+ isRateLimit: true,
405
+ resetTime: parseRateLimitResetTime(combinedMessage) ?? undefined,
406
+ message: extractedRateLimitMsg,
407
+ },
408
+ };
409
+ }
410
+ // Log detailed error information for debugging
411
+ const logFile = await logErrorDetails(err, {
412
+ prompt: prompt.slice(0, 5000),
413
+ lastMessages: recentMessages,
414
+ response: response.slice(-3000),
415
+ });
416
+ // Build a detailed error message
417
+ let errorMsg = err instanceof Error ? err.message : 'Unknown error executing prompt';
418
+ // Check for common error patterns and provide helpful messages
419
+ if (errorMsg.includes('exited with code 1')) {
420
+ errorMsg = `Claude Code process failed (exit code 1). `;
421
+ if (response) {
422
+ // Try to extract any error indicators from the response
423
+ const lastLines = response.split('\n').slice(-10).join('\n');
424
+ if (lastLines.includes('error') || lastLines.includes('Error') || lastLines.includes('failed')) {
425
+ errorMsg += `Last output: ${lastLines.slice(0, 500)}`;
426
+ }
427
+ }
428
+ if (logFile) {
429
+ errorMsg += ` Debug log: ${logFile}`;
430
+ }
431
+ }
432
+ else if (errorMsg.includes('ECONNREFUSED') || errorMsg.includes('ENOTFOUND')) {
433
+ errorMsg = 'Cannot connect to Claude Code CLI. Is it installed and running?';
434
+ }
435
+ else if (errorMsg.includes('timeout') || errorMsg.includes('ETIMEDOUT')) {
436
+ errorMsg = 'Claude Code request timed out. The task may be too complex.';
437
+ }
438
+ else if (errorMsg.includes('permission') || errorMsg.includes('Permission')) {
439
+ errorMsg = `Permission error: ${errorMsg}. Check tool permissions.`;
440
+ }
441
+ onProgress?.(`Error: ${errorMsg}`);
78
442
  return {
79
443
  success: false,
80
- response: '',
444
+ response: response.trim(),
81
445
  toolCalls,
82
- error: err instanceof Error ? err.message : 'Unknown error executing prompt',
446
+ error: errorMsg,
83
447
  };
84
448
  }
85
449
  }
450
+ /**
451
+ * Execute a prompt through the Claude Agent SDK with rate limit retry handling
452
+ *
453
+ * @param prompt - The prompt to execute
454
+ * @param options - Execution options
455
+ * @returns The execution result
456
+ */
457
+ export async function executePrompt(prompt, options = {}) {
458
+ const { onProgress, rateLimitConfig: userRateLimitConfig } = options;
459
+ // If rate limit handling is disabled, run once without retry
460
+ if (userRateLimitConfig === false) {
461
+ const result = await executePromptInternal(prompt, options);
462
+ return {
463
+ success: result.success,
464
+ response: result.response,
465
+ toolCalls: result.toolCalls,
466
+ error: result.error,
467
+ };
468
+ }
469
+ // Merge user config with defaults
470
+ const rateLimitConfig = {
471
+ ...DEFAULT_RATE_LIMIT_CONFIG,
472
+ ...userRateLimitConfig,
473
+ };
474
+ let attempt = 0;
475
+ while (attempt < rateLimitConfig.maxRetries) {
476
+ const result = await executePromptInternal(prompt, options);
477
+ // If no rate limit, return the result
478
+ if (!result.rateLimitInfo?.isRateLimit) {
479
+ return {
480
+ success: result.success,
481
+ response: result.response,
482
+ toolCalls: result.toolCalls,
483
+ error: result.error,
484
+ };
485
+ }
486
+ // Rate limit detected - calculate wait time
487
+ attempt++;
488
+ if (attempt >= rateLimitConfig.maxRetries) {
489
+ onProgress?.(`Rate limit: max retries (${rateLimitConfig.maxRetries}) exceeded`);
490
+ return {
491
+ success: false,
492
+ response: result.response,
493
+ toolCalls: result.toolCalls,
494
+ error: `Rate limit exceeded after ${attempt} retries. ${result.rateLimitInfo.message || ''}`,
495
+ };
496
+ }
497
+ // Calculate wait time
498
+ let waitMs;
499
+ if (result.rateLimitInfo.resetTime) {
500
+ // Use parsed reset time
501
+ waitMs = result.rateLimitInfo.resetTime.getTime() - Date.now();
502
+ // Add a small buffer
503
+ waitMs += 30_000;
504
+ }
505
+ else {
506
+ // Use exponential backoff
507
+ waitMs = Math.min(rateLimitConfig.baseWaitMs * Math.pow(2, attempt - 1), rateLimitConfig.maxWaitMs);
508
+ }
509
+ // Ensure minimum wait time
510
+ waitMs = Math.max(waitMs, 30_000);
511
+ // IMPORTANT: Cap wait time to maxWaitMs - don't wait hours for rate limits
512
+ if (waitMs > rateLimitConfig.maxWaitMs) {
513
+ onProgress?.(`Rate limit reset time is too far in the future (${formatWaitTime(waitMs)})`);
514
+ onProgress?.(`Maximum wait time is ${formatWaitTime(rateLimitConfig.maxWaitMs)}. Please try again later.`);
515
+ return {
516
+ success: false,
517
+ response: result.response,
518
+ toolCalls: result.toolCalls,
519
+ error: `Rate limit exceeded. Reset time is ${formatWaitTime(waitMs)} away - too long to wait. Please try again later.`,
520
+ };
521
+ }
522
+ onProgress?.(`Rate limit hit (attempt ${attempt}/${rateLimitConfig.maxRetries}). ${result.rateLimitInfo.message || ''}`);
523
+ onProgress?.(`Waiting ${formatWaitTime(waitMs)} before retry...`);
524
+ // Wait with progress updates
525
+ await sleepWithProgress(waitMs, onProgress);
526
+ onProgress?.(`Retrying after rate limit (attempt ${attempt + 1}/${rateLimitConfig.maxRetries})...`);
527
+ }
528
+ // Should not reach here, but just in case
529
+ return {
530
+ success: false,
531
+ response: '',
532
+ toolCalls: [],
533
+ error: 'Rate limit handling failed unexpectedly',
534
+ };
535
+ }
86
536
  /**
87
537
  * Execute code generation for a specific task
88
538
  *
@@ -145,8 +595,9 @@ After running the tests:
145
595
  * Analyze codebase to understand structure and patterns
146
596
  *
147
597
  * @param cwd - Working directory of the project
598
+ * @param onProgress - Progress callback
148
599
  */
149
- export async function analyzeCodebase(cwd) {
600
+ export async function analyzeCodebase(cwd, onProgress) {
150
601
  const prompt = `
151
602
  Analyze this codebase and provide:
152
603
 
@@ -163,68 +614,483 @@ Be concise but thorough in your analysis.
163
614
  cwd,
164
615
  allowedTools: ['Read', 'Glob', 'Grep', 'LS'],
165
616
  permissionMode: 'default', // Read-only analysis
617
+ onProgress,
166
618
  });
167
619
  }
168
620
  /**
169
- * Create a development plan from a specification
621
+ * Extract plan file path from Claude's response
622
+ * Claude sometimes saves the plan to a file and responds with a summary
623
+ */
624
+ function extractPlanFilePath(response) {
625
+ // Look for plan file paths like /Users/.../.claude/plans/...
626
+ const patterns = [
627
+ /`([^`]*\.claude\/plans\/[^`]+\.md)`/i,
628
+ /saved to\s+`?([^\s`]+\.claude\/plans\/[^\s`]+\.md)`?/i,
629
+ /created at\s+`?([^\s`]+\.claude\/plans\/[^\s`]+\.md)`?/i,
630
+ /plan.*at\s+`?([^\s`]+\.claude\/plans\/[^\s`]+\.md)`?/i,
631
+ /(\/[^\s]+\.claude\/plans\/[^\s]+\.md)/i,
632
+ ];
633
+ for (const pattern of patterns) {
634
+ const match = response.match(pattern);
635
+ if (match && match[1]) {
636
+ return match[1];
637
+ }
638
+ }
639
+ return null;
640
+ }
641
+ /**
642
+ * Check if response is Claude's thinking/conversation instead of actual plan
643
+ */
644
+ function isConversationalResponse(response) {
645
+ const conversationalPhrases = [
646
+ 'let me ',
647
+ 'i will ',
648
+ 'i\'ll ',
649
+ 'now i have',
650
+ 'i now have',
651
+ 'let me launch',
652
+ 'let me create',
653
+ 'i\'ve created',
654
+ 'i\'ve analyzed',
655
+ 'has been created',
656
+ 'has been saved',
657
+ 'the plan is structured',
658
+ ];
659
+ const responseLower = response.toLowerCase();
660
+ return conversationalPhrases.some(phrase => responseLower.includes(phrase));
661
+ }
662
+ /**
663
+ * Build the appropriate prompt for plan creation based on project language
170
664
  *
171
665
  * @param specification - The project specification
172
- * @param context - Additional context (existing code, etc.)
666
+ * @param context - Additional context
667
+ * @param language - Target programming language
668
+ * @returns The prompt string
173
669
  */
174
- export async function createPlan(specification, context = '') {
175
- const prompt = `
176
- Create a detailed development plan for the following specification:
670
+ function buildPlanPrompt(specification, context, language) {
671
+ // Base instructions that apply to all projects
672
+ const baseInstructions = `
673
+ You are a software architect. Create a detailed, actionable development plan.
674
+
675
+ CRITICAL INSTRUCTION: You must output the COMPLETE plan content directly in your response as markdown.
676
+ Do NOT use tools to save the plan to a file.
677
+ Do NOT just describe what the plan contains - output the ACTUAL plan with all milestones and tasks.
678
+ Do NOT say "Let me...", "I will...", "I've created...", or any conversational text.
679
+
680
+ Start your response with "# Development Plan:" and include the FULL plan content.
681
+ `.trim();
682
+ // Fullstack-specific format with app tagging
683
+ if (language === 'fullstack') {
684
+ return `
685
+ ${baseInstructions}
686
+
687
+ ## Project Type: FULLSTACK MONOREPO
688
+ - **Frontend**: React + Vite + TypeScript + Tailwind CSS + shadcn/ui
689
+ - **Backend**: FastAPI (Python) + PostgreSQL
690
+ - **Structure**: Monorepo with apps/frontend and apps/backend
177
691
 
178
692
  ## Specification
179
693
  ${specification}
180
694
 
181
695
  ${context ? `## Additional Context\n${context}` : ''}
182
696
 
183
- ## Required Plan Sections
697
+ ## Required Plan Format for Fullstack Projects
698
+
699
+ Your response MUST be the complete plan in this EXACT format:
700
+
701
+ # Development Plan: [Project Name]
702
+
703
+ ## Overview
704
+ [2-3 sentence summary mentioning both frontend and backend]
705
+
706
+ ## Architecture
707
+ - **Frontend App**: React SPA at apps/frontend/
708
+ - **Backend App**: FastAPI service at apps/backend/
709
+ - **Communication**: REST API (OpenAPI contract)
710
+
711
+ ---
712
+
713
+ ## Milestone 1: [Name]
714
+ **Description**: [What this milestone achieves]
184
715
 
185
- 1. **Background & Context**: Summarize the project requirements
186
- 2. **Goals & Objectives**: List measurable objectives
187
- 3. **Milestones**: Break down into major phases
188
- 4. **Tasks**: Detail specific tasks for each milestone
189
- 5. **Test Plan**: Define tests for each task
190
- 6. **Risks & Mitigations**: Identify potential issues
716
+ ### Frontend Tasks
191
717
 
192
- Format the plan as markdown with clear sections and bullet points.
718
+ #### Task 1.1 [FE]: [Actionable task name]
719
+ **App**: frontend
720
+ **Files**:
721
+ - \`apps/frontend/src/components/...\`
722
+ - \`apps/frontend/src/pages/...\`
723
+ **Dependencies**: None
724
+ **Acceptance Criteria**:
725
+ - [ ] Criterion 1
726
+ - [ ] Criterion 2
727
+
728
+ ### Backend Tasks
729
+
730
+ #### Task 1.2 [BE]: [Actionable task name]
731
+ **App**: backend
732
+ **Files**:
733
+ - \`apps/backend/src/api/routes/...\`
734
+ - \`apps/backend/src/models/...\`
735
+ **Dependencies**: None
736
+ **Acceptance Criteria**:
737
+ - [ ] Criterion 1
738
+
739
+ ### Integration Tasks
740
+
741
+ #### Task 1.3 [INT]: [Actionable task name]
742
+ **App**: unified
743
+ **Dependencies**: Task 1.1, Task 1.2
744
+ **Acceptance Criteria**:
745
+ - [ ] Frontend calls backend API successfully
746
+ - [ ] E2E test passes
747
+
748
+ ---
749
+
750
+ ## Milestone 2: [Name]
751
+ [Continue same pattern...]
752
+
753
+ ---
754
+
755
+ ## Test Plan
756
+
757
+ ### Frontend Tests (apps/frontend)
758
+ - **Unit**: Vitest + Testing Library
759
+ - **E2E**: Playwright
760
+
761
+ ### Backend Tests (apps/backend)
762
+ - **Unit**: pytest
763
+ - **Integration**: pytest + TestClient
764
+
765
+ ### Integration Tests
766
+ - API contract validation
767
+ - E2E user flows
768
+
769
+ ## Risks & Mitigations
770
+ [Include frontend, backend, and integration risks separately]
771
+
772
+ ---
773
+
774
+ ## CRITICAL FULLSTACK REQUIREMENTS:
775
+ 1. **Tag every task** with [FE], [BE], or [INT]
776
+ 2. **Specify App field** for each task (frontend, backend, or unified)
777
+ 3. **List exact file paths** under apps/frontend/ or apps/backend/
778
+ 4. **Group tasks** under "Frontend Tasks", "Backend Tasks", or "Integration Tasks" headers
779
+ 5. **Include at least 3 milestones** with tasks distributed across FE/BE/INT
780
+ 6. Each task MUST start with an action verb: Implement, Create, Build, Add, Configure, Set up, Write, Design, etc.
781
+ 7. Each task MUST be specific and implementable
782
+
783
+ IMPORTANT: Output the COMPLETE plan now. Start with "# Development Plan:" on the first line.
193
784
  `.trim();
194
- return executePrompt(prompt, {
785
+ }
786
+ // Python-specific format
787
+ if (language === 'python') {
788
+ return `
789
+ ${baseInstructions}
790
+
791
+ ## Project Type: PYTHON
792
+ - **Language**: Python 3.11+
793
+ - **Framework**: FastAPI (if API) or CLI
794
+ - **Testing**: pytest
795
+
796
+ ## Specification
797
+ ${specification}
798
+
799
+ ${context ? `## Additional Context\n${context}` : ''}
800
+
801
+ ## Required Plan Format
802
+
803
+ Your response MUST be the complete plan in this EXACT format:
804
+
805
+ # Development Plan: [Project Name]
806
+
807
+ ## Overview
808
+ [2-3 sentence summary of what will be built]
809
+
810
+ ## Milestone 1: [Name]
811
+ **Description**: [What this milestone achieves]
812
+
813
+ ### Task 1.1: [Actionable task name starting with verb]
814
+ **Description**: [What this task accomplishes]
815
+ **Files to create/modify**: [List specific Python files in src/]
816
+ **Acceptance Criteria**:
817
+ - [Specific, testable criterion]
818
+ - [Another criterion]
819
+
820
+ ### Task 1.2: [Another actionable task]
821
+ ...
822
+
823
+ ## Milestone 2: [Name]
824
+ ...
825
+
826
+ ## Test Plan
827
+ - pytest for unit tests in tests/
828
+ - httpx for API integration tests
829
+
830
+ ## Risks & Mitigations
831
+ [Potential issues and how to address them]
832
+
833
+ ## Requirements for Tasks
834
+
835
+ 1. Each task MUST start with an action verb: Implement, Create, Build, Add, Configure, Set up, Write, Design, etc.
836
+ 2. Each task MUST be specific and implementable
837
+ 3. Each milestone MUST have at least 3-5 specific tasks
838
+ 4. The plan MUST have at least 3 milestones for any non-trivial project
839
+ 5. Files to create/modify MUST be listed for each task
840
+ 6. Acceptance criteria MUST be testable
841
+
842
+ IMPORTANT: Output the COMPLETE plan now. Start with "# Development Plan:" on the first line.
843
+ `.trim();
844
+ }
845
+ // TypeScript/default format
846
+ return `
847
+ ${baseInstructions}
848
+
849
+ ## Project Type: TYPESCRIPT
850
+ - **Language**: TypeScript
851
+ - **Framework**: React + Vite (if frontend) or Node.js
852
+ - **Testing**: Vitest
853
+
854
+ ## Specification
855
+ ${specification}
856
+
857
+ ${context ? `## Additional Context\n${context}` : ''}
858
+
859
+ ## Required Plan Format
860
+
861
+ Your response MUST be the complete plan in this EXACT format:
862
+
863
+ # Development Plan: [Project Name]
864
+
865
+ ## Overview
866
+ [2-3 sentence summary of what will be built]
867
+
868
+ ## Milestone 1: [Name]
869
+ **Description**: [What this milestone achieves]
870
+
871
+ ### Task 1.1: [Actionable task name starting with verb]
872
+ **Description**: [What this task accomplishes]
873
+ **Files to create/modify**: [List specific TypeScript files in src/]
874
+ **Acceptance Criteria**:
875
+ - [Specific, testable criterion]
876
+ - [Another criterion]
877
+
878
+ ### Task 1.2: [Another actionable task]
879
+ ...
880
+
881
+ ## Milestone 2: [Name]
882
+ ...
883
+
884
+ ## Test Plan
885
+ - Vitest for unit tests
886
+ - Playwright for E2E tests
887
+
888
+ ## Risks & Mitigations
889
+ [Potential issues and how to address them]
890
+
891
+ ## Requirements for Tasks
892
+
893
+ 1. Each task MUST start with an action verb: Implement, Create, Build, Add, Configure, Set up, Write, Design, etc.
894
+ 2. Each task MUST be specific and implementable
895
+ 3. Each milestone MUST have at least 3-5 specific tasks
896
+ 4. The plan MUST have at least 3 milestones for any non-trivial project
897
+ 5. Files to create/modify MUST be listed for each task
898
+ 6. Acceptance criteria MUST be testable
899
+
900
+ IMPORTANT: Output the COMPLETE plan now. Start with "# Development Plan:" on the first line.
901
+ `.trim();
902
+ }
903
+ /**
904
+ * Create a development plan from a specification
905
+ *
906
+ * @param specification - The project specification
907
+ * @param context - Additional context (existing code, etc.)
908
+ * @param language - Target programming language (default: 'python')
909
+ * @param onProgress - Progress callback
910
+ */
911
+ export async function createPlan(specification, context = '', language = 'python', onProgress) {
912
+ const prompt = buildPlanPrompt(specification, context, language);
913
+ const result = await executePrompt(prompt, {
195
914
  allowedTools: ['Read', 'Glob'],
196
915
  permissionMode: 'plan',
916
+ onProgress,
197
917
  });
918
+ // If Claude's response is conversational (describes the plan but doesn't contain it),
919
+ // try to extract the plan from the file it may have created
920
+ if (result.success && isConversationalResponse(result.response)) {
921
+ onProgress?.('Detected conversational response, looking for plan file...');
922
+ // Try to find and read the plan file
923
+ const planFilePath = extractPlanFilePath(result.response);
924
+ if (planFilePath) {
925
+ try {
926
+ onProgress?.(`Found plan file reference: ${planFilePath}`);
927
+ const planContent = await fs.readFile(planFilePath, 'utf-8');
928
+ // Verify the plan content is actually a plan
929
+ if (planContent.includes('# Development Plan') ||
930
+ planContent.includes('## Milestone') ||
931
+ planContent.includes('### Task')) {
932
+ onProgress?.('Successfully extracted plan from file');
933
+ return {
934
+ ...result,
935
+ response: planContent,
936
+ };
937
+ }
938
+ }
939
+ catch (readError) {
940
+ onProgress?.(`Could not read plan file: ${readError instanceof Error ? readError.message : 'Unknown error'}`);
941
+ }
942
+ }
943
+ // Also try to find any recent .claude/plans files
944
+ try {
945
+ const claudePlansDir = path.join(homedir(), '.claude', 'plans');
946
+ const files = await fs.readdir(claudePlansDir);
947
+ const mdFiles = files.filter(f => f.endsWith('.md'));
948
+ if (mdFiles.length > 0) {
949
+ // Sort by modification time (most recent first)
950
+ const fileStats = await Promise.all(mdFiles.map(async (f) => {
951
+ const filePath = path.join(claudePlansDir, f);
952
+ const stat = await fs.stat(filePath);
953
+ return { name: f, path: filePath, mtime: stat.mtime };
954
+ }));
955
+ fileStats.sort((a, b) => b.mtime.getTime() - a.mtime.getTime());
956
+ // Check the most recent file (created in the last 5 minutes)
957
+ const recentFile = fileStats[0];
958
+ const fiveMinutesAgo = Date.now() - 5 * 60 * 1000;
959
+ if (recentFile.mtime.getTime() > fiveMinutesAgo) {
960
+ onProgress?.(`Found recent plan file: ${recentFile.name}`);
961
+ const planContent = await fs.readFile(recentFile.path, 'utf-8');
962
+ if (planContent.includes('# Development Plan') ||
963
+ planContent.includes('## Milestone') ||
964
+ planContent.includes('### Task')) {
965
+ onProgress?.('Successfully extracted plan from recent file');
966
+ return {
967
+ ...result,
968
+ response: planContent,
969
+ };
970
+ }
971
+ }
972
+ }
973
+ }
974
+ catch {
975
+ // Could not access .claude/plans directory
976
+ }
977
+ // Log warning that we couldn't extract the plan
978
+ onProgress?.('WARNING: Could not extract actual plan content from file');
979
+ }
980
+ return result;
198
981
  }
199
982
  /**
200
- * Revise a plan based on feedback
201
- *
202
- * @param originalPlan - The original plan
203
- * @param feedback - Feedback to incorporate
204
- * @param concerns - Specific concerns to address
983
+ * Build revision prompt with language-specific instructions
205
984
  */
206
- export async function revisePlan(originalPlan, feedback, concerns) {
207
- const prompt = `
208
- Revise the following plan based on the feedback provided:
985
+ function buildRevisionPrompt(originalPlan, feedback, concerns, language) {
986
+ const basePrompt = `
987
+ CRITICAL: You must output the COMPLETE revised plan in your response.
988
+ Do NOT describe what you changed - output the FULL plan with all changes incorporated.
989
+ Do NOT say "Let me...", "I will...", "I've revised...", or any conversational text.
990
+ Start your response directly with "# Development Plan:" and include the ENTIRE revised plan.
209
991
 
210
- ## Original Plan
992
+ ## Original Plan to Revise
211
993
  ${originalPlan}
212
994
 
213
- ## Feedback
995
+ ## Feedback to Address
214
996
  ${feedback}
215
997
 
216
998
  ## Specific Concerns to Address
217
999
  ${concerns.map((c, i) => `${i + 1}. ${c}`).join('\n')}
218
1000
 
219
1001
  ## Instructions
220
- 1. Address each concern specifically
221
- 2. Maintain the same plan structure
222
- 3. Note what changed from the original
223
- 4. Ensure the revised plan is complete and actionable
1002
+ 1. Address each concern by incorporating changes into the plan
1003
+ 2. Maintain the same plan structure (Overview, Milestones, Tasks, Test Plan, Risks)
1004
+ 3. Output the COMPLETE revised plan - not just the changes
1005
+ 4. Start with "# Development Plan:" and include ALL milestones and tasks
224
1006
  `.trim();
225
- return executePrompt(prompt, {
1007
+ if (language === 'fullstack') {
1008
+ return `
1009
+ ${basePrompt}
1010
+
1011
+ ## FULLSTACK-SPECIFIC REQUIREMENTS:
1012
+ - Maintain [FE], [BE], [INT] tags on all tasks
1013
+ - Keep App: field (frontend/backend/unified) for each task
1014
+ - Group tasks under "Frontend Tasks", "Backend Tasks", "Integration Tasks" headers
1015
+ - Ensure file paths use apps/frontend/ or apps/backend/ prefixes
1016
+ - If adding new tasks, tag them appropriately
1017
+
1018
+ OUTPUT THE COMPLETE REVISED PLAN NOW:
1019
+ `.trim();
1020
+ }
1021
+ return `${basePrompt}
1022
+
1023
+ OUTPUT THE COMPLETE REVISED PLAN NOW:
1024
+ `.trim();
1025
+ }
1026
+ /**
1027
+ * Revise a plan based on feedback
1028
+ *
1029
+ * @param originalPlan - The original plan
1030
+ * @param feedback - Feedback to incorporate
1031
+ * @param concerns - Specific concerns to address
1032
+ * @param language - Target programming language (default: 'python')
1033
+ * @param onProgress - Progress callback
1034
+ */
1035
+ export async function revisePlan(originalPlan, feedback, concerns, language = 'python', onProgress) {
1036
+ const prompt = buildRevisionPrompt(originalPlan, feedback, concerns, language);
1037
+ onProgress?.('Claude is revising the plan...');
1038
+ const result = await executePrompt(prompt, {
226
1039
  allowedTools: [],
227
1040
  permissionMode: 'plan',
1041
+ onProgress,
228
1042
  });
1043
+ // Check if response is conversational and try to extract actual plan
1044
+ if (result.success && isConversationalResponse(result.response)) {
1045
+ // Try to find the plan file
1046
+ const planFilePath = extractPlanFilePath(result.response);
1047
+ if (planFilePath) {
1048
+ try {
1049
+ const planContent = await fs.readFile(planFilePath, 'utf-8');
1050
+ if (planContent.includes('# Development Plan') ||
1051
+ planContent.includes('## Milestone') ||
1052
+ planContent.includes('### Task')) {
1053
+ return {
1054
+ ...result,
1055
+ response: planContent,
1056
+ };
1057
+ }
1058
+ }
1059
+ catch {
1060
+ // Could not read file, fall through
1061
+ }
1062
+ }
1063
+ // Try recent .claude/plans files
1064
+ try {
1065
+ const claudePlansDir = path.join(homedir(), '.claude', 'plans');
1066
+ const files = await fs.readdir(claudePlansDir);
1067
+ const mdFiles = files.filter(f => f.endsWith('.md'));
1068
+ if (mdFiles.length > 0) {
1069
+ const fileStats = await Promise.all(mdFiles.map(async (f) => {
1070
+ const filePath = path.join(claudePlansDir, f);
1071
+ const stat = await fs.stat(filePath);
1072
+ return { name: f, path: filePath, mtime: stat.mtime };
1073
+ }));
1074
+ fileStats.sort((a, b) => b.mtime.getTime() - a.mtime.getTime());
1075
+ const recentFile = fileStats[0];
1076
+ const twoMinutesAgo = Date.now() - 2 * 60 * 1000;
1077
+ if (recentFile.mtime.getTime() > twoMinutesAgo) {
1078
+ const planContent = await fs.readFile(recentFile.path, 'utf-8');
1079
+ if (planContent.includes('# Development Plan') ||
1080
+ planContent.includes('## Milestone') ||
1081
+ planContent.includes('### Task')) {
1082
+ return {
1083
+ ...result,
1084
+ response: planContent,
1085
+ };
1086
+ }
1087
+ }
1088
+ }
1089
+ }
1090
+ catch {
1091
+ // Could not access .claude/plans directory
1092
+ }
1093
+ }
1094
+ return result;
229
1095
  }
230
1096
  //# sourceMappingURL=claude.js.map