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.
- package/.env.example +24 -1
- package/CONTRIBUTING.md +275 -0
- package/OPEN_SOURCE_MANIFESTO.md +172 -0
- package/README.md +832 -123
- package/dist/adapters/claude.d.ts +19 -4
- package/dist/adapters/claude.d.ts.map +1 -1
- package/dist/adapters/claude.js +908 -42
- package/dist/adapters/claude.js.map +1 -1
- package/dist/adapters/gemini.d.ts +55 -0
- package/dist/adapters/gemini.d.ts.map +1 -0
- package/dist/adapters/gemini.js +318 -0
- package/dist/adapters/gemini.js.map +1 -0
- package/dist/adapters/grok.d.ts +73 -0
- package/dist/adapters/grok.d.ts.map +1 -0
- package/dist/adapters/grok.js +430 -0
- package/dist/adapters/grok.js.map +1 -0
- package/dist/adapters/openai.d.ts +1 -1
- package/dist/adapters/openai.d.ts.map +1 -1
- package/dist/adapters/openai.js +47 -8
- package/dist/adapters/openai.js.map +1 -1
- package/dist/auth/claude.d.ts +11 -9
- package/dist/auth/claude.d.ts.map +1 -1
- package/dist/auth/claude.js +107 -71
- package/dist/auth/claude.js.map +1 -1
- package/dist/auth/gemini.d.ts +58 -0
- package/dist/auth/gemini.d.ts.map +1 -0
- package/dist/auth/gemini.js +172 -0
- package/dist/auth/gemini.js.map +1 -0
- package/dist/auth/grok.d.ts +73 -0
- package/dist/auth/grok.d.ts.map +1 -0
- package/dist/auth/grok.js +211 -0
- package/dist/auth/grok.js.map +1 -0
- package/dist/auth/index.d.ts +14 -7
- package/dist/auth/index.d.ts.map +1 -1
- package/dist/auth/index.js +41 -6
- package/dist/auth/index.js.map +1 -1
- package/dist/auth/keychain.d.ts +20 -7
- package/dist/auth/keychain.d.ts.map +1 -1
- package/dist/auth/keychain.js +85 -29
- package/dist/auth/keychain.js.map +1 -1
- package/dist/auth/openai.d.ts +2 -2
- package/dist/auth/openai.d.ts.map +1 -1
- package/dist/auth/openai.js +30 -32
- package/dist/auth/openai.js.map +1 -1
- package/dist/cli/commands/auth.d.ts +1 -1
- package/dist/cli/commands/auth.d.ts.map +1 -1
- package/dist/cli/commands/auth.js +79 -8
- package/dist/cli/commands/auth.js.map +1 -1
- package/dist/cli/commands/create.d.ts.map +1 -1
- package/dist/cli/commands/create.js +15 -4
- package/dist/cli/commands/create.js.map +1 -1
- package/dist/cli/interactive.d.ts.map +1 -1
- package/dist/cli/interactive.js +1494 -114
- package/dist/cli/interactive.js.map +1 -1
- package/dist/config/defaults.d.ts +9 -1
- package/dist/config/defaults.d.ts.map +1 -1
- package/dist/config/defaults.js +19 -2
- package/dist/config/defaults.js.map +1 -1
- package/dist/config/index.d.ts +19 -0
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/index.js +33 -1
- package/dist/config/index.js.map +1 -1
- package/dist/config/schema.d.ts +47 -0
- package/dist/config/schema.d.ts.map +1 -1
- package/dist/config/schema.js +29 -1
- package/dist/config/schema.js.map +1 -1
- package/dist/generators/fullstack.d.ts +32 -0
- package/dist/generators/fullstack.d.ts.map +1 -0
- package/dist/generators/fullstack.js +497 -0
- package/dist/generators/fullstack.js.map +1 -0
- package/dist/generators/index.d.ts +4 -3
- package/dist/generators/index.d.ts.map +1 -1
- package/dist/generators/index.js +15 -1
- package/dist/generators/index.js.map +1 -1
- package/dist/generators/python.d.ts +17 -1
- package/dist/generators/python.d.ts.map +1 -1
- package/dist/generators/python.js +34 -20
- package/dist/generators/python.js.map +1 -1
- package/dist/generators/templates/fullstack.d.ts +113 -0
- package/dist/generators/templates/fullstack.d.ts.map +1 -0
- package/dist/generators/templates/fullstack.js +1004 -0
- package/dist/generators/templates/fullstack.js.map +1 -0
- package/dist/generators/typescript.d.ts +19 -1
- package/dist/generators/typescript.d.ts.map +1 -1
- package/dist/generators/typescript.js +37 -20
- package/dist/generators/typescript.js.map +1 -1
- package/dist/state/index.d.ts +108 -0
- package/dist/state/index.d.ts.map +1 -1
- package/dist/state/index.js +551 -4
- package/dist/state/index.js.map +1 -1
- package/dist/state/registry.d.ts +52 -0
- package/dist/state/registry.d.ts.map +1 -0
- package/dist/state/registry.js +215 -0
- package/dist/state/registry.js.map +1 -0
- package/dist/types/cli.d.ts +8 -0
- package/dist/types/cli.d.ts.map +1 -1
- package/dist/types/cli.js.map +1 -1
- package/dist/types/consensus.d.ts +186 -4
- package/dist/types/consensus.d.ts.map +1 -1
- package/dist/types/consensus.js +35 -3
- package/dist/types/consensus.js.map +1 -1
- package/dist/types/project.d.ts +76 -0
- package/dist/types/project.d.ts.map +1 -1
- package/dist/types/project.js +1 -1
- package/dist/types/project.js.map +1 -1
- package/dist/types/workflow.d.ts +217 -16
- package/dist/types/workflow.d.ts.map +1 -1
- package/dist/types/workflow.js +40 -1
- package/dist/types/workflow.js.map +1 -1
- package/dist/workflow/auto-fix.d.ts +45 -0
- package/dist/workflow/auto-fix.d.ts.map +1 -0
- package/dist/workflow/auto-fix.js +274 -0
- package/dist/workflow/auto-fix.js.map +1 -0
- package/dist/workflow/consensus.d.ts +70 -2
- package/dist/workflow/consensus.d.ts.map +1 -1
- package/dist/workflow/consensus.js +872 -17
- package/dist/workflow/consensus.js.map +1 -1
- package/dist/workflow/execution-mode.d.ts +10 -4
- package/dist/workflow/execution-mode.d.ts.map +1 -1
- package/dist/workflow/execution-mode.js +547 -58
- package/dist/workflow/execution-mode.js.map +1 -1
- package/dist/workflow/index.d.ts +14 -2
- package/dist/workflow/index.d.ts.map +1 -1
- package/dist/workflow/index.js +69 -6
- package/dist/workflow/index.js.map +1 -1
- package/dist/workflow/milestone-workflow.d.ts +34 -0
- package/dist/workflow/milestone-workflow.d.ts.map +1 -0
- package/dist/workflow/milestone-workflow.js +414 -0
- package/dist/workflow/milestone-workflow.js.map +1 -0
- package/dist/workflow/plan-mode.d.ts +80 -3
- package/dist/workflow/plan-mode.d.ts.map +1 -1
- package/dist/workflow/plan-mode.js +767 -49
- package/dist/workflow/plan-mode.js.map +1 -1
- package/dist/workflow/plan-storage.d.ts +386 -0
- package/dist/workflow/plan-storage.d.ts.map +1 -0
- package/dist/workflow/plan-storage.js +878 -0
- package/dist/workflow/plan-storage.js.map +1 -0
- package/dist/workflow/project-verification.d.ts +37 -0
- package/dist/workflow/project-verification.d.ts.map +1 -0
- package/dist/workflow/project-verification.js +381 -0
- package/dist/workflow/project-verification.js.map +1 -0
- package/dist/workflow/task-workflow.d.ts +37 -0
- package/dist/workflow/task-workflow.d.ts.map +1 -0
- package/dist/workflow/task-workflow.js +386 -0
- package/dist/workflow/task-workflow.js.map +1 -0
- package/dist/workflow/test-runner.d.ts +9 -0
- package/dist/workflow/test-runner.d.ts.map +1 -1
- package/dist/workflow/test-runner.js +101 -5
- package/dist/workflow/test-runner.js.map +1 -1
- package/dist/workflow/ui-designer.d.ts +82 -0
- package/dist/workflow/ui-designer.d.ts.map +1 -0
- package/dist/workflow/ui-designer.js +234 -0
- package/dist/workflow/ui-designer.js.map +1 -0
- package/dist/workflow/ui-setup.d.ts +58 -0
- package/dist/workflow/ui-setup.d.ts.map +1 -0
- package/dist/workflow/ui-setup.js +685 -0
- package/dist/workflow/ui-setup.js.map +1 -0
- package/dist/workflow/ui-verification.d.ts +114 -0
- package/dist/workflow/ui-verification.d.ts.map +1 -0
- package/dist/workflow/ui-verification.js +258 -0
- package/dist/workflow/ui-verification.js.map +1 -0
- package/dist/workflow/workflow-logger.d.ts +110 -0
- package/dist/workflow/workflow-logger.d.ts.map +1 -0
- package/dist/workflow/workflow-logger.js +267 -0
- package/dist/workflow/workflow-logger.js.map +1 -0
- package/dist/workflow/workspace-manager.d.ts +342 -0
- package/dist/workflow/workspace-manager.d.ts.map +1 -0
- package/dist/workflow/workspace-manager.js +733 -0
- package/dist/workflow/workspace-manager.js.map +1 -0
- package/package.json +2 -2
- package/src/adapters/claude.ts +1067 -47
- package/src/adapters/gemini.ts +373 -0
- package/src/adapters/grok.ts +492 -0
- package/src/adapters/openai.ts +48 -9
- package/src/auth/claude.ts +120 -78
- package/src/auth/gemini.ts +207 -0
- package/src/auth/grok.ts +255 -0
- package/src/auth/index.ts +47 -9
- package/src/auth/keychain.ts +95 -28
- package/src/auth/openai.ts +29 -36
- package/src/cli/commands/auth.ts +89 -10
- package/src/cli/commands/create.ts +13 -4
- package/src/cli/interactive.ts +1774 -142
- package/src/config/defaults.ts +19 -2
- package/src/config/index.ts +36 -1
- package/src/config/schema.ts +30 -1
- package/src/generators/fullstack.ts +551 -0
- package/src/generators/index.ts +25 -1
- package/src/generators/python.ts +65 -20
- package/src/generators/templates/fullstack.ts +1047 -0
- package/src/generators/typescript.ts +69 -20
- package/src/state/index.ts +713 -4
- package/src/state/registry.ts +278 -0
- package/src/types/cli.ts +8 -0
- package/src/types/consensus.ts +197 -6
- package/src/types/project.ts +82 -1
- package/src/types/workflow.ts +90 -1
- package/src/workflow/auto-fix.ts +340 -0
- package/src/workflow/consensus.ts +1180 -16
- package/src/workflow/execution-mode.ts +673 -74
- package/src/workflow/index.ts +95 -6
- package/src/workflow/milestone-workflow.ts +576 -0
- package/src/workflow/plan-mode.ts +924 -50
- package/src/workflow/plan-storage.ts +1282 -0
- package/src/workflow/project-verification.ts +471 -0
- package/src/workflow/task-workflow.ts +528 -0
- package/src/workflow/test-runner.ts +120 -5
- package/src/workflow/ui-designer.ts +337 -0
- package/src/workflow/ui-setup.ts +797 -0
- package/src/workflow/ui-verification.ts +357 -0
- package/src/workflow/workflow-logger.ts +353 -0
- package/src/workflow/workspace-manager.ts +912 -0
- package/tests/config/config.test.ts +1 -1
- package/tests/types/consensus.test.ts +3 -3
- package/tests/workflow/plan-mode.test.ts +213 -0
- package/tests/workflow/test-runner.test.ts +5 -3
package/src/auth/grok.ts
ADDED
|
@@ -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
|
|
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 {
|
|
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
|
|
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
|
|
package/src/auth/keychain.ts
CHANGED
|
@@ -1,27 +1,68 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
* Uses
|
|
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
|
|
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
|
|
11
|
-
|
|
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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
|
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
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
|
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
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
|
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
|
/**
|
package/src/auth/openai.ts
CHANGED
|
@@ -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
|
-
*
|
|
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
|
|
120
|
-
|
|
121
|
-
const
|
|
122
|
-
|
|
123
|
-
|
|
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
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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
|
-
//
|
|
166
|
-
const
|
|
158
|
+
// Prompt for the API key
|
|
159
|
+
const apiKey = await promptForAPIKey();
|
|
167
160
|
|
|
168
|
-
if (!
|
|
169
|
-
console.error('
|
|
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('
|
|
175
|
-
const isValid = await validateOpenAIToken(
|
|
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(
|
|
176
|
+
await setOpenAICredential(apiKey);
|
|
184
177
|
console.log('OpenAI API authenticated successfully!\n');
|
|
185
178
|
|
|
186
179
|
return true;
|