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,255 @@
1
+ /**
2
+ * xAI Grok API authentication module
3
+ * Handles API key validation and storage
4
+ */
5
+
6
+ import * as readline from 'node:readline';
7
+ import OpenAI from 'openai';
8
+ import {
9
+ getCredential,
10
+ setCredential,
11
+ deleteCredential,
12
+ maskCredential,
13
+ } from './keychain.js';
14
+ import { ENV_VARS } from '../config/defaults.js';
15
+
16
+ /**
17
+ * Grok API URL (OpenAI-compatible)
18
+ */
19
+ export const GROK_API_URL = 'https://api.x.ai/v1';
20
+
21
+ /**
22
+ * Keychain account for Grok
23
+ */
24
+ const GROK_ACCOUNT = 'grok-api';
25
+
26
+ /**
27
+ * Grok authentication status
28
+ */
29
+ export interface GrokAuthStatus {
30
+ authenticated: boolean;
31
+ keyLastFour?: string;
32
+ error?: string;
33
+ }
34
+
35
+ /**
36
+ * Validate a Grok API key by making a test API call
37
+ *
38
+ * @param apiKey - The API key to validate
39
+ * @returns True if the key is valid
40
+ */
41
+ export async function validateGrokToken(apiKey: string): Promise<boolean> {
42
+ try {
43
+ const client = new OpenAI({
44
+ apiKey,
45
+ baseURL: GROK_API_URL,
46
+ });
47
+
48
+ // Test the key by making a simple request
49
+ await client.chat.completions.create({
50
+ model: 'grok-3',
51
+ messages: [{ role: 'user', content: 'Say "OK"' }],
52
+ max_tokens: 5,
53
+ });
54
+
55
+ return true;
56
+ } catch (error) {
57
+ // Check for authentication errors
58
+ const errorMessage = error instanceof Error ? error.message : '';
59
+ if (
60
+ errorMessage.includes('401') ||
61
+ errorMessage.includes('Invalid API') ||
62
+ errorMessage.includes('Unauthorized')
63
+ ) {
64
+ return false;
65
+ }
66
+ // For other errors (e.g., rate limits), assume the key might be valid
67
+ console.warn('Could not fully validate Grok key:', error);
68
+ return true;
69
+ }
70
+ }
71
+
72
+ /**
73
+ * Get the Grok API credential
74
+ */
75
+ export async function getGrokCredential(): Promise<string | null> {
76
+ // First check file storage
77
+ const stored = await getCredential(GROK_ACCOUNT);
78
+ if (stored) return stored;
79
+
80
+ // Fallback to environment variable
81
+ return process.env[ENV_VARS.GROK_KEY] || null;
82
+ }
83
+
84
+ /**
85
+ * Set the Grok API credential
86
+ */
87
+ export async function setGrokCredential(apiKey: string): Promise<void> {
88
+ return setCredential(GROK_ACCOUNT, apiKey);
89
+ }
90
+
91
+ /**
92
+ * Delete the Grok API credential
93
+ */
94
+ export async function deleteGrokCredential(): Promise<boolean> {
95
+ return deleteCredential(GROK_ACCOUNT);
96
+ }
97
+
98
+ /**
99
+ * Check if Grok is already authenticated
100
+ */
101
+ export async function checkGrokAuth(): Promise<GrokAuthStatus> {
102
+ try {
103
+ const apiKey = await getGrokCredential();
104
+
105
+ if (!apiKey) {
106
+ return { authenticated: false };
107
+ }
108
+
109
+ // Validate the key
110
+ const isValid = await validateGrokToken(apiKey);
111
+
112
+ if (!isValid) {
113
+ return {
114
+ authenticated: false,
115
+ error: 'Stored API key is invalid',
116
+ };
117
+ }
118
+
119
+ return {
120
+ authenticated: true,
121
+ keyLastFour: maskCredential(apiKey),
122
+ };
123
+ } catch (error) {
124
+ return {
125
+ authenticated: false,
126
+ error: error instanceof Error ? error.message : 'Unknown error',
127
+ };
128
+ }
129
+ }
130
+
131
+ /**
132
+ * Prompt for API key in the terminal
133
+ *
134
+ * @returns The entered API key or null if cancelled
135
+ */
136
+ export async function promptForGrokAPIKey(): Promise<string | null> {
137
+ return new Promise((resolve) => {
138
+ const rl = readline.createInterface({
139
+ input: process.stdin,
140
+ output: process.stdout,
141
+ });
142
+
143
+ console.log('\nGet your API key from: https://console.x.ai/\n');
144
+
145
+ rl.question('Enter your Grok API key: ', (answer) => {
146
+ rl.close();
147
+ const key = answer.trim();
148
+ if (key) {
149
+ resolve(key);
150
+ } else {
151
+ resolve(null);
152
+ }
153
+ });
154
+ });
155
+ }
156
+
157
+ /**
158
+ * Authenticate with Grok API
159
+ *
160
+ * @returns True if authentication was successful
161
+ */
162
+ export async function authenticateGrok(): Promise<boolean> {
163
+ // Check if already authenticated
164
+ const existingAuth = await checkGrokAuth();
165
+ if (existingAuth.authenticated) {
166
+ console.log('Already authenticated with Grok API');
167
+ return true;
168
+ }
169
+
170
+ console.log('Grok API key required for AI reviews.');
171
+
172
+ try {
173
+ // Prompt for the API key
174
+ const apiKey = await promptForGrokAPIKey();
175
+
176
+ if (!apiKey) {
177
+ console.error('\nNo API key provided');
178
+ return false;
179
+ }
180
+
181
+ // Validate the token
182
+ console.log('\nValidating API key...');
183
+ const isValid = await validateGrokToken(apiKey);
184
+
185
+ if (!isValid) {
186
+ console.error('Invalid Grok API key');
187
+ return false;
188
+ }
189
+
190
+ // Store the token
191
+ await setGrokCredential(apiKey);
192
+ console.log('Grok API authenticated successfully!\n');
193
+
194
+ return true;
195
+ } catch (error) {
196
+ console.error(`Authentication error: ${error instanceof Error ? error.message : error}`);
197
+ return false;
198
+ }
199
+ }
200
+
201
+ /**
202
+ * Authenticate with a provided API key (for CLI --api-key option)
203
+ *
204
+ * @param apiKey - The API key to use
205
+ * @returns True if authentication was successful
206
+ */
207
+ export async function authenticateGrokWithKey(apiKey: string): Promise<boolean> {
208
+ // Validate the token
209
+ const isValid = await validateGrokToken(apiKey);
210
+
211
+ if (!isValid) {
212
+ console.error('Invalid Grok API key');
213
+ return false;
214
+ }
215
+
216
+ // Store the token
217
+ await setGrokCredential(apiKey);
218
+ console.log('Grok API authenticated successfully!\n');
219
+
220
+ return true;
221
+ }
222
+
223
+ /**
224
+ * Logout from Grok API
225
+ * Removes stored credentials
226
+ */
227
+ export async function logoutGrok(): Promise<void> {
228
+ const deleted = await deleteGrokCredential();
229
+ if (deleted) {
230
+ console.log('Grok API credentials removed.');
231
+ } else {
232
+ console.log('No Grok API credentials found.');
233
+ }
234
+ }
235
+
236
+ /**
237
+ * Get the Grok API key for API calls
238
+ */
239
+ export async function getGrokToken(): Promise<string | null> {
240
+ return getGrokCredential();
241
+ }
242
+
243
+ /**
244
+ * Ensure Grok is authenticated
245
+ * Prompts for authentication if not already authenticated
246
+ */
247
+ export async function ensureGrokAuth(): Promise<boolean> {
248
+ const status = await checkGrokAuth();
249
+
250
+ if (status.authenticated) {
251
+ return true;
252
+ }
253
+
254
+ return authenticateGrok();
255
+ }
package/src/auth/index.ts CHANGED
@@ -1,16 +1,20 @@
1
1
  /**
2
2
  * Authentication orchestration module
3
- * Coordinates authentication for both Claude CLI and OpenAI API
3
+ * Coordinates authentication for Claude CLI, OpenAI API, Gemini API, and Grok API
4
4
  */
5
5
 
6
6
  import { checkClaudeCLIAuth, authenticateClaude, logoutClaude, type ClaudeAuthStatus } from './claude.js';
7
7
  import { checkOpenAIAuth, authenticateOpenAI, logoutOpenAI, type OpenAIAuthStatus } from './openai.js';
8
- import { clearAllCredentials } from './keychain.js';
8
+ import { checkGeminiAuth, authenticateGemini, logoutGemini, type GeminiAuthStatus } from './gemini.js';
9
+ import { checkGrokAuth, authenticateGrok, logoutGrok, type GrokAuthStatus } from './grok.js';
10
+ import { clearAllCredentials, deleteCredential } from './keychain.js';
9
11
  import type { AuthStatus } from '../types/index.js';
10
12
 
11
13
  // Re-export individual auth modules
12
14
  export * from './claude.js';
13
15
  export * from './openai.js';
16
+ export * from './gemini.js';
17
+ export * from './grok.js';
14
18
  export * from './keychain.js';
15
19
  export * from './server.js';
16
20
 
@@ -20,22 +24,30 @@ export * from './server.js';
20
24
  export interface CombinedAuthStatus {
21
25
  claude: ClaudeAuthStatus;
22
26
  openai: OpenAIAuthStatus;
27
+ gemini: GeminiAuthStatus;
28
+ grok: GrokAuthStatus;
23
29
  fullyAuthenticated: boolean;
30
+ hasArbitrator: boolean;
24
31
  }
25
32
 
26
33
  /**
27
- * Get the authentication status for both services
34
+ * Get the authentication status for all services
28
35
  */
29
36
  export async function getAuthStatus(): Promise<CombinedAuthStatus> {
30
- const [claudeStatus, openaiStatus] = await Promise.all([
37
+ const [claudeStatus, openaiStatus, geminiStatus, grokStatus] = await Promise.all([
31
38
  checkClaudeCLIAuth(),
32
39
  checkOpenAIAuth(),
40
+ checkGeminiAuth(),
41
+ checkGrokAuth(),
33
42
  ]);
34
43
 
35
44
  return {
36
45
  claude: claudeStatus,
37
46
  openai: openaiStatus,
47
+ gemini: geminiStatus,
48
+ grok: grokStatus,
38
49
  fullyAuthenticated: claudeStatus.authenticated && openaiStatus.authenticated,
50
+ hasArbitrator: geminiStatus.authenticated || openaiStatus.authenticated || grokStatus.authenticated,
39
51
  };
40
52
  }
41
53
 
@@ -56,6 +68,14 @@ export async function getAuthStatusForDisplay(): Promise<AuthStatus> {
56
68
  keyLastFour: status.openai.keyLastFour,
57
69
  modelAccess: status.openai.modelAccess,
58
70
  },
71
+ gemini: {
72
+ authenticated: status.gemini.authenticated,
73
+ keyLastFour: status.gemini.keyLastFour,
74
+ },
75
+ grok: {
76
+ authenticated: status.grok.authenticated,
77
+ keyLastFour: status.grok.keyLastFour,
78
+ },
59
79
  };
60
80
  }
61
81
 
@@ -98,17 +118,21 @@ export async function ensureAuthenticated(): Promise<boolean> {
98
118
  /**
99
119
  * Authenticate a specific service
100
120
  *
101
- * @param service - The service to authenticate ('claude', 'openai', or 'all')
121
+ * @param service - The service to authenticate ('claude', 'openai', 'gemini', 'grok', or 'all')
102
122
  * @returns True if authentication was successful
103
123
  */
104
124
  export async function authenticateService(
105
- service: 'claude' | 'openai' | 'all'
125
+ service: 'claude' | 'openai' | 'gemini' | 'grok' | 'all'
106
126
  ): Promise<boolean> {
107
127
  switch (service) {
108
128
  case 'claude':
109
129
  return authenticateClaude();
110
130
  case 'openai':
111
131
  return authenticateOpenAI();
132
+ case 'gemini':
133
+ return authenticateGemini();
134
+ case 'grok':
135
+ return authenticateGrok();
112
136
  case 'all':
113
137
  return ensureAuthenticated();
114
138
  }
@@ -117,9 +141,9 @@ export async function authenticateService(
117
141
  /**
118
142
  * Logout from a specific service or all services
119
143
  *
120
- * @param service - The service to logout from ('claude', 'openai', or 'all')
144
+ * @param service - The service to logout from ('claude', 'openai', 'gemini', 'grok', or 'all')
121
145
  */
122
- export async function logout(service: 'claude' | 'openai' | 'all'): Promise<void> {
146
+ export async function logout(service: 'claude' | 'openai' | 'gemini' | 'grok' | 'all'): Promise<void> {
123
147
  switch (service) {
124
148
  case 'claude':
125
149
  await logoutClaude();
@@ -127,8 +151,16 @@ export async function logout(service: 'claude' | 'openai' | 'all'): Promise<void
127
151
  case 'openai':
128
152
  await logoutOpenAI();
129
153
  break;
154
+ case 'gemini':
155
+ await logoutGemini();
156
+ break;
157
+ case 'grok':
158
+ await logoutGrok();
159
+ break;
130
160
  case 'all':
131
161
  await clearAllCredentials();
162
+ // Also clear grok credential
163
+ await deleteCredential('grok-api');
132
164
  console.log('All credentials removed.');
133
165
  break;
134
166
  }
@@ -140,7 +172,7 @@ export async function logout(service: 'claude' | 'openai' | 'all'): Promise<void
140
172
  * @param service - The service to check
141
173
  * @returns True if the service is authenticated
142
174
  */
143
- export async function isAuthenticated(service: 'claude' | 'openai' | 'both'): Promise<boolean> {
175
+ export async function isAuthenticated(service: 'claude' | 'openai' | 'gemini' | 'grok' | 'both' | 'all'): Promise<boolean> {
144
176
  const status = await getAuthStatus();
145
177
 
146
178
  switch (service) {
@@ -148,8 +180,14 @@ export async function isAuthenticated(service: 'claude' | 'openai' | 'both'): Pr
148
180
  return status.claude.authenticated;
149
181
  case 'openai':
150
182
  return status.openai.authenticated;
183
+ case 'gemini':
184
+ return status.gemini.authenticated;
185
+ case 'grok':
186
+ return status.grok.authenticated;
151
187
  case 'both':
152
188
  return status.fullyAuthenticated;
189
+ case 'all':
190
+ return status.fullyAuthenticated && status.gemini.authenticated;
153
191
  }
154
192
  }
155
193
 
@@ -1,27 +1,68 @@
1
1
  /**
2
- * Keychain wrapper for secure credential storage
3
- * Uses keytar for cross-platform keychain access (macOS Keychain, Windows Credential Vault, Linux Secret Service)
2
+ * Credential storage module
3
+ * Uses file-based storage in ~/.popeye/ directory
4
+ * Falls back to environment variables
4
5
  */
5
6
 
6
- import * as keytar from 'keytar';
7
+ import * as fs from 'node:fs';
8
+ import * as path from 'node:path';
9
+ import * as os from 'node:os';
7
10
  import { SERVICE_NAME, KEYCHAIN_ACCOUNTS, ENV_VARS } from '../config/defaults.js';
8
11
 
9
12
  /**
10
- * Get a credential from the system keychain
11
- * Falls back to environment variable if keychain is unavailable
13
+ * Get the credentials file path
14
+ */
15
+ function getCredentialsPath(): string {
16
+ const popeyeDir = path.join(os.homedir(), '.popeye');
17
+
18
+ // Ensure directory exists
19
+ if (!fs.existsSync(popeyeDir)) {
20
+ fs.mkdirSync(popeyeDir, { mode: 0o700, recursive: true });
21
+ }
22
+
23
+ return path.join(popeyeDir, 'credentials.json');
24
+ }
25
+
26
+ /**
27
+ * Load credentials from file
28
+ */
29
+ function loadCredentials(): Record<string, string> {
30
+ try {
31
+ const filePath = getCredentialsPath();
32
+ if (fs.existsSync(filePath)) {
33
+ const data = fs.readFileSync(filePath, 'utf-8');
34
+ return JSON.parse(data);
35
+ }
36
+ } catch {
37
+ // Ignore errors, return empty
38
+ }
39
+ return {};
40
+ }
41
+
42
+ /**
43
+ * Save credentials to file
44
+ */
45
+ function saveCredentials(credentials: Record<string, string>): void {
46
+ const filePath = getCredentialsPath();
47
+ fs.writeFileSync(filePath, JSON.stringify(credentials, null, 2), {
48
+ mode: 0o600, // Owner read/write only
49
+ });
50
+ }
51
+
52
+ /**
53
+ * Get a credential from storage
54
+ * Falls back to environment variable if not found
12
55
  *
13
56
  * @param account - The account name (e.g., 'claude-cli', 'openai-api')
14
57
  * @returns The stored credential or null if not found
15
58
  */
16
59
  export async function getCredential(account: string): Promise<string | null> {
17
- try {
18
- const password = await keytar.getPassword(SERVICE_NAME, account);
19
- if (password) {
20
- return password;
21
- }
22
- } catch {
23
- // Keychain unavailable, fall back to environment variables
24
- console.warn(`Keychain unavailable for ${account}, checking environment variables`);
60
+ // First check file storage
61
+ const credentials = loadCredentials();
62
+ const key = `${SERVICE_NAME}:${account}`;
63
+
64
+ if (credentials[key]) {
65
+ return credentials[key];
25
66
  }
26
67
 
27
68
  // Fallback to environment variables
@@ -31,43 +72,47 @@ export async function getCredential(account: string): Promise<string | null> {
31
72
  if (account === KEYCHAIN_ACCOUNTS.CLAUDE) {
32
73
  return process.env[ENV_VARS.ANTHROPIC_KEY] || null;
33
74
  }
75
+ if (account === KEYCHAIN_ACCOUNTS.GEMINI) {
76
+ return process.env[ENV_VARS.GEMINI_KEY] || null;
77
+ }
34
78
 
35
79
  return null;
36
80
  }
37
81
 
38
82
  /**
39
- * Store a credential in the system keychain
83
+ * Store a credential
40
84
  *
41
85
  * @param account - The account name
42
86
  * @param password - The credential to store
43
87
  */
44
88
  export async function setCredential(account: string, password: string): Promise<void> {
45
- try {
46
- await keytar.setPassword(SERVICE_NAME, account, password);
47
- } catch (error) {
48
- throw new Error(
49
- `Failed to store credential in keychain: ${error instanceof Error ? error.message : 'Unknown error'}`
50
- );
51
- }
89
+ const credentials = loadCredentials();
90
+ const key = `${SERVICE_NAME}:${account}`;
91
+ credentials[key] = password;
92
+ saveCredentials(credentials);
52
93
  }
53
94
 
54
95
  /**
55
- * Delete a credential from the system keychain
96
+ * Delete a credential from storage
56
97
  *
57
98
  * @param account - The account name
58
99
  * @returns True if the credential was deleted, false if it didn't exist
59
100
  */
60
101
  export async function deleteCredential(account: string): Promise<boolean> {
61
- try {
62
- return await keytar.deletePassword(SERVICE_NAME, account);
63
- } catch (error) {
64
- console.warn(`Failed to delete credential from keychain: ${error}`);
65
- return false;
102
+ const credentials = loadCredentials();
103
+ const key = `${SERVICE_NAME}:${account}`;
104
+
105
+ if (credentials[key]) {
106
+ delete credentials[key];
107
+ saveCredentials(credentials);
108
+ return true;
66
109
  }
110
+
111
+ return false;
67
112
  }
68
113
 
69
114
  /**
70
- * Check if a credential exists in the keychain
115
+ * Check if a credential exists
71
116
  *
72
117
  * @param account - The account name
73
118
  * @returns True if the credential exists
@@ -119,12 +164,34 @@ export async function deleteOpenAICredential(): Promise<boolean> {
119
164
  return deleteCredential(KEYCHAIN_ACCOUNTS.OPENAI);
120
165
  }
121
166
 
167
+ /**
168
+ * Get the Gemini API credential
169
+ */
170
+ export async function getGeminiCredential(): Promise<string | null> {
171
+ return getCredential(KEYCHAIN_ACCOUNTS.GEMINI);
172
+ }
173
+
174
+ /**
175
+ * Set the Gemini API credential
176
+ */
177
+ export async function setGeminiCredential(apiKey: string): Promise<void> {
178
+ return setCredential(KEYCHAIN_ACCOUNTS.GEMINI, apiKey);
179
+ }
180
+
181
+ /**
182
+ * Delete the Gemini API credential
183
+ */
184
+ export async function deleteGeminiCredential(): Promise<boolean> {
185
+ return deleteCredential(KEYCHAIN_ACCOUNTS.GEMINI);
186
+ }
187
+
122
188
  /**
123
189
  * Clear all stored credentials
124
190
  */
125
191
  export async function clearAllCredentials(): Promise<void> {
126
192
  await deleteClaudeCredential();
127
193
  await deleteOpenAICredential();
194
+ await deleteGeminiCredential();
128
195
  }
129
196
 
130
197
  /**
@@ -3,15 +3,14 @@
3
3
  * Handles API key validation and storage
4
4
  */
5
5
 
6
+ import * as readline from 'node:readline';
6
7
  import OpenAI from 'openai';
7
- import open from 'open';
8
8
  import {
9
9
  getOpenAICredential,
10
10
  setOpenAICredential,
11
11
  deleteOpenAICredential,
12
12
  maskCredential,
13
13
  } from './keychain.js';
14
- import { startAuthCallbackServer, findAvailablePort } from './server.js';
15
14
 
16
15
  /**
17
16
  * OpenAI authentication status
@@ -112,38 +111,32 @@ export async function checkOpenAIAuth(): Promise<OpenAIAuthStatus> {
112
111
  }
113
112
 
114
113
  /**
115
- * Launch the browser-based token entry popup
114
+ * Prompt for API key in the terminal
116
115
  *
117
116
  * @returns The entered API key or null if cancelled
118
117
  */
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
118
+ export async function promptForAPIKey(): Promise<string | null> {
119
+ return new Promise((resolve) => {
120
+ const rl = readline.createInterface({
121
+ input: process.stdin,
122
+ output: process.stdout,
130
123
  });
131
124
 
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
- }
125
+ console.log('\nGet your API key from: https://platform.openai.com/api-keys\n');
126
+
127
+ rl.question('Enter your OpenAI API key (starts with sk-): ', (answer) => {
128
+ rl.close();
129
+ const key = answer.trim();
130
+ if (key && key.startsWith('sk-')) {
131
+ resolve(key);
132
+ } else if (key) {
133
+ console.log('\nWarning: Key does not start with "sk-", but trying anyway...');
134
+ resolve(key);
135
+ } else {
136
+ resolve(null);
137
+ }
138
+ });
139
+ });
147
140
  }
148
141
 
149
142
  /**
@@ -162,17 +155,17 @@ export async function authenticateOpenAI(): Promise<boolean> {
162
155
  console.log('OpenAI API key required.');
163
156
 
164
157
  try {
165
- // Launch the token entry popup
166
- const token = await launchTokenEntryPopup();
158
+ // Prompt for the API key
159
+ const apiKey = await promptForAPIKey();
167
160
 
168
- if (!token) {
169
- console.error('No API key provided');
161
+ if (!apiKey) {
162
+ console.error('\nNo API key provided');
170
163
  return false;
171
164
  }
172
165
 
173
166
  // Validate the token
174
- console.log('Validating API key...');
175
- const isValid = await validateOpenAIToken(token);
167
+ console.log('\nValidating API key...');
168
+ const isValid = await validateOpenAIToken(apiKey);
176
169
 
177
170
  if (!isValid) {
178
171
  console.error('Invalid OpenAI API key');
@@ -180,7 +173,7 @@ export async function authenticateOpenAI(): Promise<boolean> {
180
173
  }
181
174
 
182
175
  // Store the token
183
- await setOpenAICredential(token);
176
+ await setOpenAICredential(apiKey);
184
177
  console.log('OpenAI API authenticated successfully!\n');
185
178
 
186
179
  return true;