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.
- package/.env.example +25 -0
- package/.prettierrc +8 -0
- package/README.md +320 -0
- package/dist/adapters/claude.d.ts +82 -0
- package/dist/adapters/claude.d.ts.map +1 -0
- package/dist/adapters/claude.js +230 -0
- package/dist/adapters/claude.js.map +1 -0
- package/dist/adapters/openai.d.ts +48 -0
- package/dist/adapters/openai.d.ts.map +1 -0
- package/dist/adapters/openai.js +257 -0
- package/dist/adapters/openai.js.map +1 -0
- package/dist/auth/claude.d.ts +44 -0
- package/dist/auth/claude.d.ts.map +1 -0
- package/dist/auth/claude.js +139 -0
- package/dist/auth/claude.js.map +1 -0
- package/dist/auth/index.d.ts +61 -0
- package/dist/auth/index.d.ts.map +1 -0
- package/dist/auth/index.js +141 -0
- package/dist/auth/index.js.map +1 -0
- package/dist/auth/keychain.d.ts +66 -0
- package/dist/auth/keychain.d.ts.map +1 -0
- package/dist/auth/keychain.js +125 -0
- package/dist/auth/keychain.js.map +1 -0
- package/dist/auth/openai-entry.d.ts +9 -0
- package/dist/auth/openai-entry.d.ts.map +1 -0
- package/dist/auth/openai-entry.js +410 -0
- package/dist/auth/openai-entry.js.map +1 -0
- package/dist/auth/openai.d.ts +71 -0
- package/dist/auth/openai.d.ts.map +1 -0
- package/dist/auth/openai.js +212 -0
- package/dist/auth/openai.js.map +1 -0
- package/dist/auth/server.d.ts +32 -0
- package/dist/auth/server.d.ts.map +1 -0
- package/dist/auth/server.js +213 -0
- package/dist/auth/server.js.map +1 -0
- package/dist/cli/commands/auth.d.ts +10 -0
- package/dist/cli/commands/auth.d.ts.map +1 -0
- package/dist/cli/commands/auth.js +162 -0
- package/dist/cli/commands/auth.js.map +1 -0
- package/dist/cli/commands/config.d.ts +10 -0
- package/dist/cli/commands/config.d.ts.map +1 -0
- package/dist/cli/commands/config.js +215 -0
- package/dist/cli/commands/config.js.map +1 -0
- package/dist/cli/commands/create.d.ts +10 -0
- package/dist/cli/commands/create.d.ts.map +1 -0
- package/dist/cli/commands/create.js +240 -0
- package/dist/cli/commands/create.js.map +1 -0
- package/dist/cli/commands/index.d.ts +10 -0
- package/dist/cli/commands/index.d.ts.map +1 -0
- package/dist/cli/commands/index.js +10 -0
- package/dist/cli/commands/index.js.map +1 -0
- package/dist/cli/commands/resume.d.ts +18 -0
- package/dist/cli/commands/resume.d.ts.map +1 -0
- package/dist/cli/commands/resume.js +241 -0
- package/dist/cli/commands/resume.js.map +1 -0
- package/dist/cli/commands/status.d.ts +18 -0
- package/dist/cli/commands/status.d.ts.map +1 -0
- package/dist/cli/commands/status.js +154 -0
- package/dist/cli/commands/status.js.map +1 -0
- package/dist/cli/index.d.ts +17 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +71 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/interactive.d.ts +9 -0
- package/dist/cli/interactive.d.ts.map +1 -0
- package/dist/cli/interactive.js +330 -0
- package/dist/cli/interactive.js.map +1 -0
- package/dist/cli/output.d.ts +182 -0
- package/dist/cli/output.d.ts.map +1 -0
- package/dist/cli/output.js +355 -0
- package/dist/cli/output.js.map +1 -0
- package/dist/config/defaults.d.ts +57 -0
- package/dist/config/defaults.d.ts.map +1 -0
- package/dist/config/defaults.js +103 -0
- package/dist/config/defaults.js.map +1 -0
- package/dist/config/index.d.ts +138 -0
- package/dist/config/index.d.ts.map +1 -0
- package/dist/config/index.js +244 -0
- package/dist/config/index.js.map +1 -0
- package/dist/config/schema.d.ts +220 -0
- package/dist/config/schema.d.ts.map +1 -0
- package/dist/config/schema.js +141 -0
- package/dist/config/schema.js.map +1 -0
- package/dist/generators/index.d.ts +101 -0
- package/dist/generators/index.d.ts.map +1 -0
- package/dist/generators/index.js +200 -0
- package/dist/generators/index.js.map +1 -0
- package/dist/generators/python.d.ts +48 -0
- package/dist/generators/python.d.ts.map +1 -0
- package/dist/generators/python.js +262 -0
- package/dist/generators/python.js.map +1 -0
- package/dist/generators/templates/index.d.ts +6 -0
- package/dist/generators/templates/index.d.ts.map +1 -0
- package/dist/generators/templates/index.js +6 -0
- package/dist/generators/templates/index.js.map +1 -0
- package/dist/generators/templates/python.d.ts +53 -0
- package/dist/generators/templates/python.d.ts.map +1 -0
- package/dist/generators/templates/python.js +454 -0
- package/dist/generators/templates/python.js.map +1 -0
- package/dist/generators/templates/typescript.d.ts +53 -0
- package/dist/generators/templates/typescript.d.ts.map +1 -0
- package/dist/generators/templates/typescript.js +394 -0
- package/dist/generators/templates/typescript.js.map +1 -0
- package/dist/generators/typescript.d.ts +64 -0
- package/dist/generators/typescript.d.ts.map +1 -0
- package/dist/generators/typescript.js +271 -0
- package/dist/generators/typescript.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +12 -0
- package/dist/index.js.map +1 -0
- package/dist/state/index.d.ts +168 -0
- package/dist/state/index.d.ts.map +1 -0
- package/dist/state/index.js +338 -0
- package/dist/state/index.js.map +1 -0
- package/dist/state/persistence.d.ts +91 -0
- package/dist/state/persistence.d.ts.map +1 -0
- package/dist/state/persistence.js +201 -0
- package/dist/state/persistence.js.map +1 -0
- package/dist/types/cli.d.ts +132 -0
- package/dist/types/cli.d.ts.map +1 -0
- package/dist/types/cli.js +17 -0
- package/dist/types/cli.js.map +1 -0
- package/dist/types/consensus.d.ts +111 -0
- package/dist/types/consensus.d.ts.map +1 -0
- package/dist/types/consensus.js +29 -0
- package/dist/types/consensus.js.map +1 -0
- package/dist/types/index.d.ts +9 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +13 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/project.d.ts +73 -0
- package/dist/types/project.d.ts.map +1 -0
- package/dist/types/project.js +55 -0
- package/dist/types/project.js.map +1 -0
- package/dist/types/workflow.d.ts +236 -0
- package/dist/types/workflow.d.ts.map +1 -0
- package/dist/types/workflow.js +74 -0
- package/dist/types/workflow.js.map +1 -0
- package/dist/workflow/consensus.d.ts +89 -0
- package/dist/workflow/consensus.d.ts.map +1 -0
- package/dist/workflow/consensus.js +220 -0
- package/dist/workflow/consensus.js.map +1 -0
- package/dist/workflow/execution-mode.d.ts +82 -0
- package/dist/workflow/execution-mode.d.ts.map +1 -0
- package/dist/workflow/execution-mode.js +346 -0
- package/dist/workflow/execution-mode.js.map +1 -0
- package/dist/workflow/index.d.ts +110 -0
- package/dist/workflow/index.d.ts.map +1 -0
- package/dist/workflow/index.js +283 -0
- package/dist/workflow/index.js.map +1 -0
- package/dist/workflow/plan-mode.d.ts +83 -0
- package/dist/workflow/plan-mode.d.ts.map +1 -0
- package/dist/workflow/plan-mode.js +241 -0
- package/dist/workflow/plan-mode.js.map +1 -0
- package/dist/workflow/test-runner.d.ts +87 -0
- package/dist/workflow/test-runner.d.ts.map +1 -0
- package/dist/workflow/test-runner.js +273 -0
- package/dist/workflow/test-runner.js.map +1 -0
- package/eslint.config.js +25 -0
- package/package.json +66 -0
- package/src/adapters/claude.ts +298 -0
- package/src/adapters/openai.ts +300 -0
- package/src/auth/claude.ts +166 -0
- package/src/auth/index.ts +171 -0
- package/src/auth/keychain.ts +138 -0
- package/src/auth/openai-entry.ts +410 -0
- package/src/auth/openai.ts +260 -0
- package/src/auth/server.ts +252 -0
- package/src/cli/commands/auth.ts +194 -0
- package/src/cli/commands/config.ts +241 -0
- package/src/cli/commands/create.ts +308 -0
- package/src/cli/commands/index.ts +10 -0
- package/src/cli/commands/resume.ts +304 -0
- package/src/cli/commands/status.ts +189 -0
- package/src/cli/index.ts +90 -0
- package/src/cli/interactive.ts +418 -0
- package/src/cli/output.ts +410 -0
- package/src/config/defaults.ts +114 -0
- package/src/config/index.ts +315 -0
- package/src/config/schema.ts +164 -0
- package/src/generators/index.ts +251 -0
- package/src/generators/python.ts +318 -0
- package/src/generators/templates/index.ts +6 -0
- package/src/generators/templates/python.ts +465 -0
- package/src/generators/templates/typescript.ts +417 -0
- package/src/generators/typescript.ts +340 -0
- package/src/index.ts +13 -0
- package/src/state/index.ts +454 -0
- package/src/state/persistence.ts +230 -0
- package/src/types/cli.ts +146 -0
- package/src/types/consensus.ts +116 -0
- package/src/types/index.ts +64 -0
- package/src/types/project.ts +85 -0
- package/src/types/workflow.ts +149 -0
- package/src/workflow/consensus.ts +299 -0
- package/src/workflow/execution-mode.ts +517 -0
- package/src/workflow/index.ts +396 -0
- package/src/workflow/plan-mode.ts +356 -0
- package/src/workflow/test-runner.ts +345 -0
- package/tests/adapters/openai.test.ts +145 -0
- package/tests/config/config.test.ts +208 -0
- package/tests/generators/generators.test.ts +185 -0
- package/tests/types/consensus.test.ts +152 -0
- package/tests/types/project.test.ts +134 -0
- package/tests/workflow/consensus.test.ts +221 -0
- package/tests/workflow/test-runner.test.ts +214 -0
- package/tsconfig.json +25 -0
- 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">🍿</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>✅ 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
|
+
👁
|
|
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
|
+
}
|