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
@@ -0,0 +1,373 @@
1
+ /**
2
+ * Google Gemini API adapter
3
+ * Handles consensus reviews and arbitration
4
+ */
5
+
6
+ import { GoogleGenerativeAI } from '@google/generative-ai';
7
+ import type { ConsensusResult, ArbitrationResult } from '../types/consensus.js';
8
+ import { getGeminiToken } from '../auth/index.js';
9
+
10
+ /**
11
+ * Supported Gemini models
12
+ */
13
+ export type GeminiModel = 'gemini-2.0-flash' | 'gemini-1.5-pro' | 'gemini-1.5-flash';
14
+
15
+ /**
16
+ * Default Gemini configuration
17
+ */
18
+ export const DEFAULT_GEMINI_CONFIG = {
19
+ model: 'gemini-2.0-flash' as GeminiModel,
20
+ temperature: 0.3,
21
+ maxTokens: 4096,
22
+ };
23
+
24
+ /**
25
+ * Create a Gemini client with stored credentials
26
+ */
27
+ export async function createClient(): Promise<GoogleGenerativeAI> {
28
+ const apiKey = await getGeminiToken();
29
+
30
+ if (!apiKey) {
31
+ throw new Error('Gemini API key not found. Run: popeye auth gemini');
32
+ }
33
+
34
+ return new GoogleGenerativeAI(apiKey);
35
+ }
36
+
37
+ /**
38
+ * Request consensus review from Gemini
39
+ *
40
+ * @param plan - The development plan to review
41
+ * @param context - Project context
42
+ * @param config - Configuration options
43
+ * @returns Consensus result
44
+ */
45
+ export async function requestConsensus(
46
+ plan: string,
47
+ context: string,
48
+ config: { model?: GeminiModel; temperature?: number; maxTokens?: number } = {}
49
+ ): Promise<ConsensusResult> {
50
+ const {
51
+ model = DEFAULT_GEMINI_CONFIG.model,
52
+ temperature = DEFAULT_GEMINI_CONFIG.temperature,
53
+ maxTokens = DEFAULT_GEMINI_CONFIG.maxTokens,
54
+ } = config;
55
+
56
+ const client = await createClient();
57
+ const generativeModel = client.getGenerativeModel({ model });
58
+
59
+ const prompt = buildConsensusPrompt(plan, context);
60
+
61
+ try {
62
+ const result = await generativeModel.generateContent({
63
+ contents: [{ role: 'user', parts: [{ text: prompt }] }],
64
+ generationConfig: {
65
+ temperature,
66
+ maxOutputTokens: maxTokens,
67
+ },
68
+ });
69
+
70
+ const response = result.response.text();
71
+ return parseConsensusResponse(response);
72
+ } catch (error) {
73
+ const errorMsg = error instanceof Error ? error.message : 'Unknown error';
74
+ throw new Error(`Gemini API error: ${errorMsg}`);
75
+ }
76
+ }
77
+
78
+ /**
79
+ * Request arbitration from Gemini when consensus is stuck
80
+ *
81
+ * @param plan - The best plan achieved
82
+ * @param reviewerFeedback - Feedback from the reviewer
83
+ * @param claudeFeedback - Claude's perspective on the plan
84
+ * @param iterations - Number of iterations attempted
85
+ * @param scores - Score history
86
+ * @returns Arbitration decision
87
+ */
88
+ export async function requestArbitration(
89
+ plan: string,
90
+ reviewerFeedback: string,
91
+ claudeFeedback: string,
92
+ iterations: number,
93
+ scores: number[]
94
+ ): Promise<ArbitrationResult> {
95
+ const client = await createClient();
96
+ const generativeModel = client.getGenerativeModel({ model: 'gemini-2.0-flash' });
97
+
98
+ const prompt = buildArbitrationPrompt(plan, reviewerFeedback, claudeFeedback, iterations, scores);
99
+
100
+ try {
101
+ const result = await generativeModel.generateContent({
102
+ contents: [{ role: 'user', parts: [{ text: prompt }] }],
103
+ generationConfig: {
104
+ temperature: 0.2,
105
+ maxOutputTokens: 4096,
106
+ },
107
+ });
108
+
109
+ const response = result.response.text();
110
+ return parseArbitrationResponse(response);
111
+ } catch (error) {
112
+ const errorMsg = error instanceof Error ? error.message : 'Unknown error';
113
+ throw new Error(`Gemini arbitration error: ${errorMsg}`);
114
+ }
115
+ }
116
+
117
+ /**
118
+ * Build the consensus review prompt
119
+ */
120
+ function buildConsensusPrompt(plan: string, context: string): string {
121
+ return `You are a senior software architect reviewing a development plan.
122
+ Analyze the following plan for completeness, correctness, and feasibility.
123
+
124
+ PROJECT CONTEXT:
125
+ ${context}
126
+
127
+ PROPOSED PLAN:
128
+ ${plan}
129
+
130
+ Please provide your response in EXACTLY this format (use these exact headers):
131
+
132
+ ANALYSIS:
133
+ [Your detailed analysis here]
134
+
135
+ STRENGTHS:
136
+ - [Strength 1]
137
+ - [Strength 2]
138
+ - [etc.]
139
+
140
+ CONCERNS:
141
+ - [Concern 1]
142
+ - [Concern 2]
143
+ - [etc.]
144
+
145
+ RECOMMENDATIONS:
146
+ - [Recommendation 1]
147
+ - [Recommendation 2]
148
+ - [etc.]
149
+
150
+ CONSENSUS: [X]%
151
+
152
+ Scoring guide:
153
+ - 95-100%: Ready for execution, no changes needed
154
+ - 85-94%: Minor revisions needed, mostly good
155
+ - 70-84%: Significant revisions needed
156
+ - Below 70%: Major rework required
157
+
158
+ Be thorough but constructive. Focus on actionable feedback.`;
159
+ }
160
+
161
+ /**
162
+ * Build the arbitration prompt
163
+ */
164
+ function buildArbitrationPrompt(
165
+ plan: string,
166
+ reviewerFeedback: string,
167
+ claudeFeedback: string,
168
+ iterations: number,
169
+ scores: number[]
170
+ ): string {
171
+ const scoreHistory = scores.map((s, i) => `Iteration ${i + 1}: ${s}%`).join(', ');
172
+
173
+ return `You are an impartial arbitrator resolving a disagreement between two AI systems about a development plan.
174
+
175
+ SITUATION:
176
+ - Claude (code generator) created a plan
177
+ - A reviewer (OpenAI/Gemini) has been reviewing and providing feedback
178
+ - They have gone through ${iterations} iterations without reaching 95% consensus
179
+ - Score history: ${scoreHistory}
180
+
181
+ THE PLAN:
182
+ ${plan}
183
+
184
+ REVIEWER'S LATEST FEEDBACK:
185
+ ${reviewerFeedback}
186
+
187
+ CLAUDE'S PERSPECTIVE:
188
+ ${claudeFeedback}
189
+
190
+ As the arbitrator, you must:
191
+ 1. Analyze both perspectives objectively
192
+ 2. Determine if the remaining concerns are:
193
+ - CRITICAL: Must be addressed before proceeding
194
+ - MINOR: Can be addressed during implementation
195
+ - SUBJECTIVE: Matters of preference, not correctness
196
+ 3. Make a final decision
197
+
198
+ Respond in EXACTLY this format:
199
+
200
+ ANALYSIS:
201
+ [Your analysis of the disagreement]
202
+
203
+ CRITICAL_CONCERNS:
204
+ - [List any truly critical issues, or "None" if none exist]
205
+
206
+ MINOR_CONCERNS:
207
+ - [List minor issues that can be addressed during implementation]
208
+
209
+ SUBJECTIVE_CONCERNS:
210
+ - [List preference-based concerns that don't affect correctness]
211
+
212
+ DECISION: [APPROVE or REVISE]
213
+
214
+ FINAL_SCORE: [X]%
215
+
216
+ REASONING:
217
+ [Explain your decision]
218
+
219
+ SUGGESTED_CHANGES:
220
+ - [If REVISE, list specific changes needed]
221
+ - [If APPROVE, write "None - plan is acceptable"]`;
222
+ }
223
+
224
+ /**
225
+ * Parse the consensus response from Gemini
226
+ */
227
+ export function parseConsensusResponse(response: string): ConsensusResult {
228
+ // Extract consensus score - look for various formats
229
+ let score = 0;
230
+ const scorePatterns = [
231
+ /CONSENSUS:\s*(\d+)%/i,
232
+ /CONSENSUS\s*SCORE:\s*(\d+)%/i,
233
+ /(\d+)%\s*consensus/i,
234
+ /score[:\s]+(\d+)%/i,
235
+ ];
236
+
237
+ for (const pattern of scorePatterns) {
238
+ const match = response.match(pattern);
239
+ if (match) {
240
+ score = parseInt(match[1], 10);
241
+ break;
242
+ }
243
+ }
244
+
245
+ // Extract sections with better handling of markdown headers
246
+ const analysis = extractSection(response, ['ANALYSIS', '## Analysis', '### Analysis']);
247
+ const strengthsText = extractSection(response, ['STRENGTHS', '## Strengths', '### Strengths']);
248
+ const concernsText = extractSection(response, ['CONCERNS', '## Concerns', '### Concerns']);
249
+ const recommendationsText = extractSection(response, ['RECOMMENDATIONS', '## Recommendations', '### Recommendations']);
250
+
251
+ // Parse lists from sections
252
+ const strengths = parseList(strengthsText);
253
+ const concerns = parseList(concernsText);
254
+ const recommendations = parseList(recommendationsText);
255
+
256
+ return {
257
+ score,
258
+ analysis: analysis.trim(),
259
+ strengths,
260
+ concerns,
261
+ recommendations,
262
+ approved: score >= 95,
263
+ rawResponse: response,
264
+ };
265
+ }
266
+
267
+ /**
268
+ * Parse the arbitration response
269
+ */
270
+ function parseArbitrationResponse(response: string): ArbitrationResult {
271
+ // Extract score
272
+ const scoreMatch = response.match(/FINAL_SCORE:\s*(\d+)%/i);
273
+ const score = scoreMatch ? parseInt(scoreMatch[1], 10) : 0;
274
+
275
+ // Extract decision
276
+ const decisionMatch = response.match(/DECISION:\s*(APPROVE|REVISE)/i);
277
+ const approved = decisionMatch ? decisionMatch[1].toUpperCase() === 'APPROVE' : score >= 90;
278
+
279
+ // Extract sections
280
+ const analysis = extractSection(response, ['ANALYSIS']);
281
+ const criticalConcerns = parseList(extractSection(response, ['CRITICAL_CONCERNS']));
282
+ const minorConcerns = parseList(extractSection(response, ['MINOR_CONCERNS']));
283
+ const subjectiveConcerns = parseList(extractSection(response, ['SUBJECTIVE_CONCERNS']));
284
+ const reasoning = extractSection(response, ['REASONING']);
285
+ const suggestedChanges = parseList(extractSection(response, ['SUGGESTED_CHANGES']));
286
+
287
+ return {
288
+ approved,
289
+ score,
290
+ analysis,
291
+ criticalConcerns: criticalConcerns.filter(c => c.toLowerCase() !== 'none'),
292
+ minorConcerns: minorConcerns.filter(c => c.toLowerCase() !== 'none'),
293
+ subjectiveConcerns: subjectiveConcerns.filter(c => c.toLowerCase() !== 'none'),
294
+ reasoning,
295
+ suggestedChanges: suggestedChanges.filter(c => !c.toLowerCase().includes('none')),
296
+ rawResponse: response,
297
+ };
298
+ }
299
+
300
+ /**
301
+ * Extract a section from the response with multiple possible headers
302
+ */
303
+ function extractSection(response: string, headers: string[]): string {
304
+ // Build pattern to match any of the headers
305
+ const headerPattern = headers.map(h => h.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('|');
306
+ const startPattern = new RegExp(`(${headerPattern})[:\\s]*\\n?`, 'i');
307
+
308
+ const startMatch = response.match(startPattern);
309
+ if (!startMatch || startMatch.index === undefined) return '';
310
+
311
+ const startIndex = startMatch.index + startMatch[0].length;
312
+
313
+ // Find the next section header (any capitalized word followed by colon or markdown header)
314
+ const endPattern = /\n(?:#{1,3}\s+)?[A-Z][A-Z_]+[:\s]/;
315
+ const remaining = response.slice(startIndex);
316
+ const endMatch = remaining.match(endPattern);
317
+
318
+ if (!endMatch || endMatch.index === undefined) {
319
+ return remaining.trim();
320
+ }
321
+
322
+ return remaining.slice(0, endMatch.index).trim();
323
+ }
324
+
325
+ /**
326
+ * Parse a bulleted or numbered list from text
327
+ */
328
+ function parseList(text: string): string[] {
329
+ if (!text) return [];
330
+
331
+ const lines = text.split('\n');
332
+ const items: string[] = [];
333
+
334
+ for (const line of lines) {
335
+ const trimmed = line.trim();
336
+
337
+ // Skip empty lines and section headers
338
+ if (!trimmed) continue;
339
+ if (trimmed.match(/^#{1,3}\s/)) continue; // Skip markdown headers
340
+ if (trimmed.match(/^[A-Z][A-Z_]+:/)) continue; // Skip section headers
341
+
342
+ // Match bullets (-, *, +) or numbers (1., 2., etc.)
343
+ const bulletMatch = trimmed.match(/^[-*+]\s+(.+)$/);
344
+ const numberMatch = trimmed.match(/^\d+\.\s+(.+)$/);
345
+
346
+ if (bulletMatch) {
347
+ items.push(bulletMatch[1].trim());
348
+ } else if (numberMatch) {
349
+ items.push(numberMatch[1].trim());
350
+ } else if (trimmed && !trimmed.match(/^[A-Z]+:/i)) {
351
+ // Only add non-header lines that have substantial content
352
+ if (trimmed.length > 10 && !trimmed.startsWith('**') && !trimmed.endsWith(':')) {
353
+ items.push(trimmed);
354
+ }
355
+ }
356
+ }
357
+
358
+ return items;
359
+ }
360
+
361
+ /**
362
+ * Validate that the Gemini API key is working
363
+ */
364
+ export async function validateApiKey(): Promise<boolean> {
365
+ try {
366
+ const client = await createClient();
367
+ const model = client.getGenerativeModel({ model: 'gemini-2.0-flash' });
368
+ await model.generateContent('Say "OK" if you can hear me.');
369
+ return true;
370
+ } catch {
371
+ return false;
372
+ }
373
+ }