popeye-cli 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (209) hide show
  1. package/.env.example +25 -0
  2. package/.prettierrc +8 -0
  3. package/README.md +320 -0
  4. package/dist/adapters/claude.d.ts +82 -0
  5. package/dist/adapters/claude.d.ts.map +1 -0
  6. package/dist/adapters/claude.js +230 -0
  7. package/dist/adapters/claude.js.map +1 -0
  8. package/dist/adapters/openai.d.ts +48 -0
  9. package/dist/adapters/openai.d.ts.map +1 -0
  10. package/dist/adapters/openai.js +257 -0
  11. package/dist/adapters/openai.js.map +1 -0
  12. package/dist/auth/claude.d.ts +44 -0
  13. package/dist/auth/claude.d.ts.map +1 -0
  14. package/dist/auth/claude.js +139 -0
  15. package/dist/auth/claude.js.map +1 -0
  16. package/dist/auth/index.d.ts +61 -0
  17. package/dist/auth/index.d.ts.map +1 -0
  18. package/dist/auth/index.js +141 -0
  19. package/dist/auth/index.js.map +1 -0
  20. package/dist/auth/keychain.d.ts +66 -0
  21. package/dist/auth/keychain.d.ts.map +1 -0
  22. package/dist/auth/keychain.js +125 -0
  23. package/dist/auth/keychain.js.map +1 -0
  24. package/dist/auth/openai-entry.d.ts +9 -0
  25. package/dist/auth/openai-entry.d.ts.map +1 -0
  26. package/dist/auth/openai-entry.js +410 -0
  27. package/dist/auth/openai-entry.js.map +1 -0
  28. package/dist/auth/openai.d.ts +71 -0
  29. package/dist/auth/openai.d.ts.map +1 -0
  30. package/dist/auth/openai.js +212 -0
  31. package/dist/auth/openai.js.map +1 -0
  32. package/dist/auth/server.d.ts +32 -0
  33. package/dist/auth/server.d.ts.map +1 -0
  34. package/dist/auth/server.js +213 -0
  35. package/dist/auth/server.js.map +1 -0
  36. package/dist/cli/commands/auth.d.ts +10 -0
  37. package/dist/cli/commands/auth.d.ts.map +1 -0
  38. package/dist/cli/commands/auth.js +162 -0
  39. package/dist/cli/commands/auth.js.map +1 -0
  40. package/dist/cli/commands/config.d.ts +10 -0
  41. package/dist/cli/commands/config.d.ts.map +1 -0
  42. package/dist/cli/commands/config.js +215 -0
  43. package/dist/cli/commands/config.js.map +1 -0
  44. package/dist/cli/commands/create.d.ts +10 -0
  45. package/dist/cli/commands/create.d.ts.map +1 -0
  46. package/dist/cli/commands/create.js +240 -0
  47. package/dist/cli/commands/create.js.map +1 -0
  48. package/dist/cli/commands/index.d.ts +10 -0
  49. package/dist/cli/commands/index.d.ts.map +1 -0
  50. package/dist/cli/commands/index.js +10 -0
  51. package/dist/cli/commands/index.js.map +1 -0
  52. package/dist/cli/commands/resume.d.ts +18 -0
  53. package/dist/cli/commands/resume.d.ts.map +1 -0
  54. package/dist/cli/commands/resume.js +241 -0
  55. package/dist/cli/commands/resume.js.map +1 -0
  56. package/dist/cli/commands/status.d.ts +18 -0
  57. package/dist/cli/commands/status.d.ts.map +1 -0
  58. package/dist/cli/commands/status.js +154 -0
  59. package/dist/cli/commands/status.js.map +1 -0
  60. package/dist/cli/index.d.ts +17 -0
  61. package/dist/cli/index.d.ts.map +1 -0
  62. package/dist/cli/index.js +71 -0
  63. package/dist/cli/index.js.map +1 -0
  64. package/dist/cli/interactive.d.ts +9 -0
  65. package/dist/cli/interactive.d.ts.map +1 -0
  66. package/dist/cli/interactive.js +330 -0
  67. package/dist/cli/interactive.js.map +1 -0
  68. package/dist/cli/output.d.ts +182 -0
  69. package/dist/cli/output.d.ts.map +1 -0
  70. package/dist/cli/output.js +355 -0
  71. package/dist/cli/output.js.map +1 -0
  72. package/dist/config/defaults.d.ts +57 -0
  73. package/dist/config/defaults.d.ts.map +1 -0
  74. package/dist/config/defaults.js +103 -0
  75. package/dist/config/defaults.js.map +1 -0
  76. package/dist/config/index.d.ts +138 -0
  77. package/dist/config/index.d.ts.map +1 -0
  78. package/dist/config/index.js +244 -0
  79. package/dist/config/index.js.map +1 -0
  80. package/dist/config/schema.d.ts +220 -0
  81. package/dist/config/schema.d.ts.map +1 -0
  82. package/dist/config/schema.js +141 -0
  83. package/dist/config/schema.js.map +1 -0
  84. package/dist/generators/index.d.ts +101 -0
  85. package/dist/generators/index.d.ts.map +1 -0
  86. package/dist/generators/index.js +200 -0
  87. package/dist/generators/index.js.map +1 -0
  88. package/dist/generators/python.d.ts +48 -0
  89. package/dist/generators/python.d.ts.map +1 -0
  90. package/dist/generators/python.js +262 -0
  91. package/dist/generators/python.js.map +1 -0
  92. package/dist/generators/templates/index.d.ts +6 -0
  93. package/dist/generators/templates/index.d.ts.map +1 -0
  94. package/dist/generators/templates/index.js +6 -0
  95. package/dist/generators/templates/index.js.map +1 -0
  96. package/dist/generators/templates/python.d.ts +53 -0
  97. package/dist/generators/templates/python.d.ts.map +1 -0
  98. package/dist/generators/templates/python.js +454 -0
  99. package/dist/generators/templates/python.js.map +1 -0
  100. package/dist/generators/templates/typescript.d.ts +53 -0
  101. package/dist/generators/templates/typescript.d.ts.map +1 -0
  102. package/dist/generators/templates/typescript.js +394 -0
  103. package/dist/generators/templates/typescript.js.map +1 -0
  104. package/dist/generators/typescript.d.ts +64 -0
  105. package/dist/generators/typescript.d.ts.map +1 -0
  106. package/dist/generators/typescript.js +271 -0
  107. package/dist/generators/typescript.js.map +1 -0
  108. package/dist/index.d.ts +7 -0
  109. package/dist/index.d.ts.map +1 -0
  110. package/dist/index.js +12 -0
  111. package/dist/index.js.map +1 -0
  112. package/dist/state/index.d.ts +168 -0
  113. package/dist/state/index.d.ts.map +1 -0
  114. package/dist/state/index.js +338 -0
  115. package/dist/state/index.js.map +1 -0
  116. package/dist/state/persistence.d.ts +91 -0
  117. package/dist/state/persistence.d.ts.map +1 -0
  118. package/dist/state/persistence.js +201 -0
  119. package/dist/state/persistence.js.map +1 -0
  120. package/dist/types/cli.d.ts +132 -0
  121. package/dist/types/cli.d.ts.map +1 -0
  122. package/dist/types/cli.js +17 -0
  123. package/dist/types/cli.js.map +1 -0
  124. package/dist/types/consensus.d.ts +111 -0
  125. package/dist/types/consensus.d.ts.map +1 -0
  126. package/dist/types/consensus.js +29 -0
  127. package/dist/types/consensus.js.map +1 -0
  128. package/dist/types/index.d.ts +9 -0
  129. package/dist/types/index.d.ts.map +1 -0
  130. package/dist/types/index.js +13 -0
  131. package/dist/types/index.js.map +1 -0
  132. package/dist/types/project.d.ts +73 -0
  133. package/dist/types/project.d.ts.map +1 -0
  134. package/dist/types/project.js +55 -0
  135. package/dist/types/project.js.map +1 -0
  136. package/dist/types/workflow.d.ts +236 -0
  137. package/dist/types/workflow.d.ts.map +1 -0
  138. package/dist/types/workflow.js +74 -0
  139. package/dist/types/workflow.js.map +1 -0
  140. package/dist/workflow/consensus.d.ts +89 -0
  141. package/dist/workflow/consensus.d.ts.map +1 -0
  142. package/dist/workflow/consensus.js +220 -0
  143. package/dist/workflow/consensus.js.map +1 -0
  144. package/dist/workflow/execution-mode.d.ts +82 -0
  145. package/dist/workflow/execution-mode.d.ts.map +1 -0
  146. package/dist/workflow/execution-mode.js +346 -0
  147. package/dist/workflow/execution-mode.js.map +1 -0
  148. package/dist/workflow/index.d.ts +110 -0
  149. package/dist/workflow/index.d.ts.map +1 -0
  150. package/dist/workflow/index.js +283 -0
  151. package/dist/workflow/index.js.map +1 -0
  152. package/dist/workflow/plan-mode.d.ts +83 -0
  153. package/dist/workflow/plan-mode.d.ts.map +1 -0
  154. package/dist/workflow/plan-mode.js +241 -0
  155. package/dist/workflow/plan-mode.js.map +1 -0
  156. package/dist/workflow/test-runner.d.ts +87 -0
  157. package/dist/workflow/test-runner.d.ts.map +1 -0
  158. package/dist/workflow/test-runner.js +273 -0
  159. package/dist/workflow/test-runner.js.map +1 -0
  160. package/eslint.config.js +25 -0
  161. package/package.json +66 -0
  162. package/src/adapters/claude.ts +298 -0
  163. package/src/adapters/openai.ts +300 -0
  164. package/src/auth/claude.ts +166 -0
  165. package/src/auth/index.ts +171 -0
  166. package/src/auth/keychain.ts +138 -0
  167. package/src/auth/openai-entry.ts +410 -0
  168. package/src/auth/openai.ts +260 -0
  169. package/src/auth/server.ts +252 -0
  170. package/src/cli/commands/auth.ts +194 -0
  171. package/src/cli/commands/config.ts +241 -0
  172. package/src/cli/commands/create.ts +308 -0
  173. package/src/cli/commands/index.ts +10 -0
  174. package/src/cli/commands/resume.ts +304 -0
  175. package/src/cli/commands/status.ts +189 -0
  176. package/src/cli/index.ts +90 -0
  177. package/src/cli/interactive.ts +418 -0
  178. package/src/cli/output.ts +410 -0
  179. package/src/config/defaults.ts +114 -0
  180. package/src/config/index.ts +315 -0
  181. package/src/config/schema.ts +164 -0
  182. package/src/generators/index.ts +251 -0
  183. package/src/generators/python.ts +318 -0
  184. package/src/generators/templates/index.ts +6 -0
  185. package/src/generators/templates/python.ts +465 -0
  186. package/src/generators/templates/typescript.ts +417 -0
  187. package/src/generators/typescript.ts +340 -0
  188. package/src/index.ts +13 -0
  189. package/src/state/index.ts +454 -0
  190. package/src/state/persistence.ts +230 -0
  191. package/src/types/cli.ts +146 -0
  192. package/src/types/consensus.ts +116 -0
  193. package/src/types/index.ts +64 -0
  194. package/src/types/project.ts +85 -0
  195. package/src/types/workflow.ts +149 -0
  196. package/src/workflow/consensus.ts +299 -0
  197. package/src/workflow/execution-mode.ts +517 -0
  198. package/src/workflow/index.ts +396 -0
  199. package/src/workflow/plan-mode.ts +356 -0
  200. package/src/workflow/test-runner.ts +345 -0
  201. package/tests/adapters/openai.test.ts +145 -0
  202. package/tests/config/config.test.ts +208 -0
  203. package/tests/generators/generators.test.ts +185 -0
  204. package/tests/types/consensus.test.ts +152 -0
  205. package/tests/types/project.test.ts +134 -0
  206. package/tests/workflow/consensus.test.ts +221 -0
  207. package/tests/workflow/test-runner.test.ts +214 -0
  208. package/tsconfig.json +25 -0
  209. package/vitest.config.ts +22 -0
@@ -0,0 +1,410 @@
1
+ /**
2
+ * OpenAI API key entry HTML page
3
+ * Served by the local auth server for browser-based token entry
4
+ */
5
+
6
+ /**
7
+ * Generate the HTML for the OpenAI token entry page
8
+ */
9
+ export function getOpenAIEntryHTML(_port: number): string {
10
+ return `
11
+ <!DOCTYPE html>
12
+ <html lang="en">
13
+ <head>
14
+ <meta charset="UTF-8">
15
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
16
+ <title>OpenAI API Key - Popeye CLI</title>
17
+ <style>
18
+ * {
19
+ box-sizing: border-box;
20
+ margin: 0;
21
+ padding: 0;
22
+ }
23
+
24
+ body {
25
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
26
+ background: linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%);
27
+ min-height: 100vh;
28
+ display: flex;
29
+ justify-content: center;
30
+ align-items: center;
31
+ padding: 20px;
32
+ }
33
+
34
+ .container {
35
+ background: rgba(255, 255, 255, 0.05);
36
+ backdrop-filter: blur(20px);
37
+ border-radius: 20px;
38
+ padding: 40px;
39
+ max-width: 500px;
40
+ width: 100%;
41
+ border: 1px solid rgba(255, 255, 255, 0.1);
42
+ box-shadow: 0 25px 50px rgba(0, 0, 0, 0.3);
43
+ }
44
+
45
+ .logo {
46
+ text-align: center;
47
+ margin-bottom: 30px;
48
+ }
49
+
50
+ .logo-icon {
51
+ font-size: 48px;
52
+ margin-bottom: 10px;
53
+ }
54
+
55
+ h1 {
56
+ color: #ffffff;
57
+ font-size: 24px;
58
+ font-weight: 600;
59
+ text-align: center;
60
+ margin-bottom: 10px;
61
+ }
62
+
63
+ .subtitle {
64
+ color: rgba(255, 255, 255, 0.7);
65
+ text-align: center;
66
+ margin-bottom: 30px;
67
+ font-size: 14px;
68
+ line-height: 1.5;
69
+ }
70
+
71
+ .instructions {
72
+ background: rgba(255, 255, 255, 0.05);
73
+ border-radius: 12px;
74
+ padding: 20px;
75
+ margin-bottom: 25px;
76
+ }
77
+
78
+ .instructions h3 {
79
+ color: #10b981;
80
+ font-size: 14px;
81
+ font-weight: 600;
82
+ margin-bottom: 15px;
83
+ display: flex;
84
+ align-items: center;
85
+ gap: 8px;
86
+ }
87
+
88
+ .instructions ol {
89
+ color: rgba(255, 255, 255, 0.8);
90
+ font-size: 14px;
91
+ padding-left: 20px;
92
+ line-height: 1.8;
93
+ }
94
+
95
+ .instructions a {
96
+ color: #60a5fa;
97
+ text-decoration: none;
98
+ }
99
+
100
+ .instructions a:hover {
101
+ text-decoration: underline;
102
+ }
103
+
104
+ .input-group {
105
+ margin-bottom: 20px;
106
+ }
107
+
108
+ .input-group label {
109
+ display: block;
110
+ color: rgba(255, 255, 255, 0.9);
111
+ font-size: 14px;
112
+ font-weight: 500;
113
+ margin-bottom: 8px;
114
+ }
115
+
116
+ .input-wrapper {
117
+ position: relative;
118
+ }
119
+
120
+ input[type="password"],
121
+ input[type="text"] {
122
+ width: 100%;
123
+ padding: 14px 16px;
124
+ padding-right: 45px;
125
+ background: rgba(255, 255, 255, 0.08);
126
+ border: 1px solid rgba(255, 255, 255, 0.15);
127
+ border-radius: 10px;
128
+ color: #ffffff;
129
+ font-size: 14px;
130
+ font-family: 'SF Mono', Monaco, 'Courier New', monospace;
131
+ transition: all 0.2s ease;
132
+ }
133
+
134
+ input:focus {
135
+ outline: none;
136
+ border-color: #10b981;
137
+ background: rgba(255, 255, 255, 0.1);
138
+ }
139
+
140
+ input::placeholder {
141
+ color: rgba(255, 255, 255, 0.4);
142
+ }
143
+
144
+ .toggle-visibility {
145
+ position: absolute;
146
+ right: 12px;
147
+ top: 50%;
148
+ transform: translateY(-50%);
149
+ background: none;
150
+ border: none;
151
+ color: rgba(255, 255, 255, 0.5);
152
+ cursor: pointer;
153
+ padding: 5px;
154
+ font-size: 18px;
155
+ }
156
+
157
+ .toggle-visibility:hover {
158
+ color: rgba(255, 255, 255, 0.8);
159
+ }
160
+
161
+ .checkbox-group {
162
+ display: flex;
163
+ align-items: center;
164
+ gap: 10px;
165
+ margin-bottom: 25px;
166
+ }
167
+
168
+ .checkbox-group input[type="checkbox"] {
169
+ width: 18px;
170
+ height: 18px;
171
+ accent-color: #10b981;
172
+ }
173
+
174
+ .checkbox-group label {
175
+ color: rgba(255, 255, 255, 0.8);
176
+ font-size: 14px;
177
+ }
178
+
179
+ .button-group {
180
+ display: flex;
181
+ gap: 12px;
182
+ }
183
+
184
+ button {
185
+ flex: 1;
186
+ padding: 14px 24px;
187
+ border-radius: 10px;
188
+ font-size: 14px;
189
+ font-weight: 600;
190
+ cursor: pointer;
191
+ transition: all 0.2s ease;
192
+ border: none;
193
+ }
194
+
195
+ .btn-primary {
196
+ background: linear-gradient(135deg, #10b981 0%, #059669 100%);
197
+ color: white;
198
+ }
199
+
200
+ .btn-primary:hover {
201
+ transform: translateY(-2px);
202
+ box-shadow: 0 10px 20px rgba(16, 185, 129, 0.3);
203
+ }
204
+
205
+ .btn-primary:disabled {
206
+ opacity: 0.5;
207
+ cursor: not-allowed;
208
+ transform: none;
209
+ box-shadow: none;
210
+ }
211
+
212
+ .btn-secondary {
213
+ background: rgba(255, 255, 255, 0.1);
214
+ color: rgba(255, 255, 255, 0.8);
215
+ border: 1px solid rgba(255, 255, 255, 0.15);
216
+ }
217
+
218
+ .btn-secondary:hover {
219
+ background: rgba(255, 255, 255, 0.15);
220
+ }
221
+
222
+ .btn-link {
223
+ background: none;
224
+ color: #60a5fa;
225
+ text-decoration: none;
226
+ padding: 0;
227
+ flex: none;
228
+ width: auto;
229
+ }
230
+
231
+ .btn-link:hover {
232
+ text-decoration: underline;
233
+ }
234
+
235
+ .error-message {
236
+ background: rgba(239, 68, 68, 0.1);
237
+ border: 1px solid rgba(239, 68, 68, 0.3);
238
+ border-radius: 10px;
239
+ padding: 12px 16px;
240
+ color: #fca5a5;
241
+ font-size: 14px;
242
+ margin-bottom: 20px;
243
+ display: none;
244
+ }
245
+
246
+ .success-message {
247
+ background: rgba(16, 185, 129, 0.1);
248
+ border: 1px solid rgba(16, 185, 129, 0.3);
249
+ border-radius: 10px;
250
+ padding: 12px 16px;
251
+ color: #6ee7b7;
252
+ font-size: 14px;
253
+ margin-bottom: 20px;
254
+ display: none;
255
+ }
256
+
257
+ .footer {
258
+ text-align: center;
259
+ margin-top: 25px;
260
+ padding-top: 20px;
261
+ border-top: 1px solid rgba(255, 255, 255, 0.1);
262
+ }
263
+
264
+ .footer a {
265
+ color: #60a5fa;
266
+ text-decoration: none;
267
+ font-size: 13px;
268
+ }
269
+
270
+ .footer a:hover {
271
+ text-decoration: underline;
272
+ }
273
+ </style>
274
+ </head>
275
+ <body>
276
+ <div class="container">
277
+ <div class="logo">
278
+ <div class="logo-icon">&#127871;</div>
279
+ <h1>OpenAI API Key Required</h1>
280
+ </div>
281
+
282
+ <p class="subtitle">
283
+ Popeye CLI needs an OpenAI API key for consensus reviews.<br>
284
+ Your key will be stored securely in your system keychain.
285
+ </p>
286
+
287
+ <div class="instructions">
288
+ <h3>&#9989; How to get your API key:</h3>
289
+ <ol>
290
+ <li>Visit <a href="https://platform.openai.com/api-keys" target="_blank">platform.openai.com/api-keys</a></li>
291
+ <li>Click "Create new secret key"</li>
292
+ <li>Copy the key and paste it below</li>
293
+ </ol>
294
+ </div>
295
+
296
+ <div class="error-message" id="error"></div>
297
+ <div class="success-message" id="success"></div>
298
+
299
+ <form id="tokenForm" onsubmit="submitToken(event)">
300
+ <div class="input-group">
301
+ <label for="token">API Key</label>
302
+ <div class="input-wrapper">
303
+ <input
304
+ type="password"
305
+ id="token"
306
+ name="token"
307
+ placeholder="sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
308
+ required
309
+ autocomplete="off"
310
+ />
311
+ <button type="button" class="toggle-visibility" onclick="toggleVisibility()">
312
+ &#128065;
313
+ </button>
314
+ </div>
315
+ </div>
316
+
317
+ <div class="checkbox-group">
318
+ <input type="checkbox" id="saveToKeychain" checked />
319
+ <label for="saveToKeychain">Save to system keychain (recommended)</label>
320
+ </div>
321
+
322
+ <div class="button-group">
323
+ <button type="button" class="btn-secondary" onclick="openOpenAI()">
324
+ Open OpenAI
325
+ </button>
326
+ <button type="button" class="btn-secondary" onclick="cancel()">
327
+ Cancel
328
+ </button>
329
+ <button type="submit" class="btn-primary" id="submitBtn">
330
+ Authenticate
331
+ </button>
332
+ </div>
333
+ </form>
334
+
335
+ <div class="footer">
336
+ <a href="https://platform.openai.com/docs/api-reference" target="_blank">
337
+ OpenAI API Documentation
338
+ </a>
339
+ </div>
340
+ </div>
341
+
342
+ <script>
343
+ const tokenInput = document.getElementById('token');
344
+ const errorDiv = document.getElementById('error');
345
+ const successDiv = document.getElementById('success');
346
+ const submitBtn = document.getElementById('submitBtn');
347
+
348
+ function toggleVisibility() {
349
+ const input = document.getElementById('token');
350
+ input.type = input.type === 'password' ? 'text' : 'password';
351
+ }
352
+
353
+ function openOpenAI() {
354
+ window.open('https://platform.openai.com/api-keys', '_blank');
355
+ }
356
+
357
+ function cancel() {
358
+ window.location.href = '/cancel';
359
+ }
360
+
361
+ function showError(message) {
362
+ errorDiv.textContent = message;
363
+ errorDiv.style.display = 'block';
364
+ successDiv.style.display = 'none';
365
+ }
366
+
367
+ function showSuccess(message) {
368
+ successDiv.textContent = message;
369
+ successDiv.style.display = 'block';
370
+ errorDiv.style.display = 'none';
371
+ }
372
+
373
+ function validateToken(token) {
374
+ if (!token) {
375
+ return 'Please enter your API key';
376
+ }
377
+ if (!token.startsWith('sk-')) {
378
+ return 'API key should start with "sk-"';
379
+ }
380
+ if (token.length < 20) {
381
+ return 'API key seems too short';
382
+ }
383
+ return null;
384
+ }
385
+
386
+ async function submitToken(event) {
387
+ event.preventDefault();
388
+
389
+ const token = tokenInput.value.trim();
390
+ const validationError = validateToken(token);
391
+
392
+ if (validationError) {
393
+ showError(validationError);
394
+ return;
395
+ }
396
+
397
+ submitBtn.disabled = true;
398
+ submitBtn.textContent = 'Validating...';
399
+
400
+ // Submit the token
401
+ window.location.href = '/submit?token=' + encodeURIComponent(token);
402
+ }
403
+
404
+ // Focus the input on load
405
+ tokenInput.focus();
406
+ </script>
407
+ </body>
408
+ </html>
409
+ `;
410
+ }
@@ -0,0 +1,260 @@
1
+ /**
2
+ * OpenAI API authentication module
3
+ * Handles API key validation and storage
4
+ */
5
+
6
+ import OpenAI from 'openai';
7
+ import open from 'open';
8
+ import {
9
+ getOpenAICredential,
10
+ setOpenAICredential,
11
+ deleteOpenAICredential,
12
+ maskCredential,
13
+ } from './keychain.js';
14
+ import { startAuthCallbackServer, findAvailablePort } from './server.js';
15
+
16
+ /**
17
+ * OpenAI authentication status
18
+ */
19
+ export interface OpenAIAuthStatus {
20
+ authenticated: boolean;
21
+ keyLastFour?: string;
22
+ modelAccess?: string[];
23
+ error?: string;
24
+ }
25
+
26
+ /**
27
+ * Validate an OpenAI API key by making a test API call
28
+ *
29
+ * @param apiKey - The API key to validate
30
+ * @returns True if the key is valid
31
+ */
32
+ export async function validateOpenAIToken(apiKey: string): Promise<boolean> {
33
+ try {
34
+ const client = new OpenAI({ apiKey });
35
+ // Test the key by listing models
36
+ await client.models.list();
37
+ return true;
38
+ } catch (error) {
39
+ // 401 means invalid key, other errors might be rate limits etc
40
+ if (error instanceof OpenAI.AuthenticationError) {
41
+ return false;
42
+ }
43
+ // For other errors, assume the key might be valid
44
+ // (could be rate limiting, network issues, etc)
45
+ console.warn('Could not fully validate OpenAI key:', error);
46
+ return true;
47
+ }
48
+ }
49
+
50
+ /**
51
+ * Get available models for an API key
52
+ *
53
+ * @param apiKey - The API key
54
+ * @returns List of available model IDs
55
+ */
56
+ export async function getAvailableModels(apiKey: string): Promise<string[]> {
57
+ try {
58
+ const client = new OpenAI({ apiKey });
59
+ const models = await client.models.list();
60
+
61
+ // Filter for GPT and O1 models
62
+ return models.data
63
+ .filter(
64
+ (m) =>
65
+ m.id.includes('gpt-4') ||
66
+ m.id.includes('gpt-3.5') ||
67
+ m.id.startsWith('o1')
68
+ )
69
+ .map((m) => m.id)
70
+ .sort();
71
+ } catch {
72
+ return [];
73
+ }
74
+ }
75
+
76
+ /**
77
+ * Check if OpenAI is already authenticated
78
+ * Checks keychain first, then environment variable
79
+ */
80
+ export async function checkOpenAIAuth(): Promise<OpenAIAuthStatus> {
81
+ try {
82
+ const apiKey = await getOpenAICredential();
83
+
84
+ if (!apiKey) {
85
+ return { authenticated: false };
86
+ }
87
+
88
+ // Validate the key
89
+ const isValid = await validateOpenAIToken(apiKey);
90
+
91
+ if (!isValid) {
92
+ return {
93
+ authenticated: false,
94
+ error: 'Stored API key is invalid',
95
+ };
96
+ }
97
+
98
+ // Get available models
99
+ const models = await getAvailableModels(apiKey);
100
+
101
+ return {
102
+ authenticated: true,
103
+ keyLastFour: maskCredential(apiKey),
104
+ modelAccess: models,
105
+ };
106
+ } catch (error) {
107
+ return {
108
+ authenticated: false,
109
+ error: error instanceof Error ? error.message : 'Unknown error',
110
+ };
111
+ }
112
+ }
113
+
114
+ /**
115
+ * Launch the browser-based token entry popup
116
+ *
117
+ * @returns The entered API key or null if cancelled
118
+ */
119
+ export async function launchTokenEntryPopup(): Promise<string | null> {
120
+ try {
121
+ const port = await findAvailablePort(3000, 3100);
122
+
123
+ console.log('Opening browser for API key entry...\n');
124
+
125
+ // Start the token entry server
126
+ const authPromise = startAuthCallbackServer({
127
+ port,
128
+ type: 'openai',
129
+ timeout: 300000, // 5 minutes
130
+ });
131
+
132
+ // Open the browser
133
+ await open(`http://127.0.0.1:${port}`);
134
+
135
+ // Wait for the token
136
+ const result = await authPromise;
137
+
138
+ if (result.success && result.token) {
139
+ return result.token;
140
+ }
141
+
142
+ return null;
143
+ } catch (error) {
144
+ console.error(`Failed to launch token entry: ${error}`);
145
+ return null;
146
+ }
147
+ }
148
+
149
+ /**
150
+ * Authenticate with OpenAI API
151
+ *
152
+ * @returns True if authentication was successful
153
+ */
154
+ export async function authenticateOpenAI(): Promise<boolean> {
155
+ // Check if already authenticated
156
+ const existingAuth = await checkOpenAIAuth();
157
+ if (existingAuth.authenticated) {
158
+ console.log('Already authenticated with OpenAI API');
159
+ return true;
160
+ }
161
+
162
+ console.log('OpenAI API key required.');
163
+
164
+ try {
165
+ // Launch the token entry popup
166
+ const token = await launchTokenEntryPopup();
167
+
168
+ if (!token) {
169
+ console.error('No API key provided');
170
+ return false;
171
+ }
172
+
173
+ // Validate the token
174
+ console.log('Validating API key...');
175
+ const isValid = await validateOpenAIToken(token);
176
+
177
+ if (!isValid) {
178
+ console.error('Invalid OpenAI API key');
179
+ return false;
180
+ }
181
+
182
+ // Store the token
183
+ await setOpenAICredential(token);
184
+ console.log('OpenAI API authenticated successfully!\n');
185
+
186
+ return true;
187
+ } catch (error) {
188
+ console.error(`Authentication error: ${error instanceof Error ? error.message : error}`);
189
+ return false;
190
+ }
191
+ }
192
+
193
+ /**
194
+ * Authenticate with a provided API key (for CLI --api-key option)
195
+ *
196
+ * @param apiKey - The API key to use
197
+ * @returns True if authentication was successful
198
+ */
199
+ export async function authenticateOpenAIWithKey(apiKey: string): Promise<boolean> {
200
+ // Validate the token
201
+ const isValid = await validateOpenAIToken(apiKey);
202
+
203
+ if (!isValid) {
204
+ console.error('Invalid OpenAI API key');
205
+ return false;
206
+ }
207
+
208
+ // Store the token
209
+ await setOpenAICredential(apiKey);
210
+ console.log('OpenAI API authenticated successfully!\n');
211
+
212
+ return true;
213
+ }
214
+
215
+ /**
216
+ * Logout from OpenAI API
217
+ * Removes stored credentials
218
+ */
219
+ export async function logoutOpenAI(): Promise<void> {
220
+ const deleted = await deleteOpenAICredential();
221
+ if (deleted) {
222
+ console.log('OpenAI API credentials removed.');
223
+ } else {
224
+ console.log('No OpenAI API credentials found.');
225
+ }
226
+ }
227
+
228
+ /**
229
+ * Get the OpenAI API key for API calls
230
+ */
231
+ export async function getOpenAIToken(): Promise<string | null> {
232
+ return getOpenAICredential();
233
+ }
234
+
235
+ /**
236
+ * Ensure OpenAI is authenticated
237
+ * Prompts for authentication if not already authenticated
238
+ */
239
+ export async function ensureOpenAIAuth(): Promise<boolean> {
240
+ const status = await checkOpenAIAuth();
241
+
242
+ if (status.authenticated) {
243
+ return true;
244
+ }
245
+
246
+ return authenticateOpenAI();
247
+ }
248
+
249
+ /**
250
+ * Create an OpenAI client with the stored credentials
251
+ */
252
+ export async function createOpenAIClient(): Promise<OpenAI | null> {
253
+ const apiKey = await getOpenAIToken();
254
+
255
+ if (!apiKey) {
256
+ return null;
257
+ }
258
+
259
+ return new OpenAI({ apiKey });
260
+ }