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,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Claude CLI authentication module
|
|
3
|
+
* Handles OAuth flow and token management for Claude CLI
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import open from 'open';
|
|
7
|
+
import { getClaudeCredential, setClaudeCredential, deleteClaudeCredential } from './keychain.js';
|
|
8
|
+
import { startAuthCallbackServer, findAvailablePort, getCallbackUrl } from './server.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Claude authentication status
|
|
12
|
+
*/
|
|
13
|
+
export interface ClaudeAuthStatus {
|
|
14
|
+
authenticated: boolean;
|
|
15
|
+
user?: string;
|
|
16
|
+
expires?: string;
|
|
17
|
+
error?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Check if Claude CLI is already authenticated
|
|
22
|
+
* This checks for existing credentials in the keychain or environment
|
|
23
|
+
*/
|
|
24
|
+
export async function checkClaudeCLIAuth(): Promise<ClaudeAuthStatus> {
|
|
25
|
+
try {
|
|
26
|
+
const token = await getClaudeCredential();
|
|
27
|
+
|
|
28
|
+
if (!token) {
|
|
29
|
+
return { authenticated: false };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// For now, we assume if there's a token, it's valid
|
|
33
|
+
// In production, you would validate the token with the Claude API
|
|
34
|
+
return {
|
|
35
|
+
authenticated: true,
|
|
36
|
+
user: 'claude-user', // Would be extracted from token in production
|
|
37
|
+
};
|
|
38
|
+
} catch (error) {
|
|
39
|
+
return {
|
|
40
|
+
authenticated: false,
|
|
41
|
+
error: error instanceof Error ? error.message : 'Unknown error',
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Authenticate with Claude CLI using browser-based OAuth flow
|
|
48
|
+
*
|
|
49
|
+
* @returns Promise that resolves to true if authentication was successful
|
|
50
|
+
*/
|
|
51
|
+
export async function authenticateClaude(): Promise<boolean> {
|
|
52
|
+
// Check if already authenticated
|
|
53
|
+
const existingAuth = await checkClaudeCLIAuth();
|
|
54
|
+
if (existingAuth.authenticated) {
|
|
55
|
+
console.log('Already authenticated with Claude CLI');
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
console.log('Claude CLI authentication required.');
|
|
60
|
+
console.log('Opening browser for login...\n');
|
|
61
|
+
|
|
62
|
+
try {
|
|
63
|
+
// Find an available port for the callback server
|
|
64
|
+
const port = await findAvailablePort(3000, 3100);
|
|
65
|
+
const callbackUrl = getCallbackUrl(port, 'claude');
|
|
66
|
+
|
|
67
|
+
// Start the callback server
|
|
68
|
+
const authPromise = startAuthCallbackServer({
|
|
69
|
+
port,
|
|
70
|
+
type: 'claude',
|
|
71
|
+
timeout: 300000, // 5 minutes
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
// Build the login URL
|
|
75
|
+
// Note: This is a placeholder URL. The actual Claude OAuth URL would be provided by Anthropic
|
|
76
|
+
const loginUrl = buildClaudeLoginUrl(callbackUrl);
|
|
77
|
+
|
|
78
|
+
console.log(`Waiting for authentication...`);
|
|
79
|
+
console.log(`(Browser opened to: ${loginUrl})\n`);
|
|
80
|
+
|
|
81
|
+
// Open the browser to the login URL
|
|
82
|
+
await open(loginUrl);
|
|
83
|
+
|
|
84
|
+
// Wait for the callback
|
|
85
|
+
const result = await authPromise;
|
|
86
|
+
|
|
87
|
+
if (result.success && result.token) {
|
|
88
|
+
// Store the token securely
|
|
89
|
+
await setClaudeCredential(result.token);
|
|
90
|
+
console.log('Claude CLI authenticated successfully!\n');
|
|
91
|
+
return true;
|
|
92
|
+
} else {
|
|
93
|
+
console.error(`Authentication failed: ${result.error || 'Unknown error'}`);
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
} catch (error) {
|
|
97
|
+
console.error(`Authentication error: ${error instanceof Error ? error.message : error}`);
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Build the Claude login URL with the callback redirect
|
|
104
|
+
*
|
|
105
|
+
* @param callbackUrl - The local callback URL
|
|
106
|
+
* @returns The full login URL
|
|
107
|
+
*/
|
|
108
|
+
function buildClaudeLoginUrl(callbackUrl: string): string {
|
|
109
|
+
// This is a placeholder implementation
|
|
110
|
+
// The actual URL would be provided by Anthropic's OAuth configuration
|
|
111
|
+
const baseUrl = 'https://claude.ai/login';
|
|
112
|
+
const params = new URLSearchParams({
|
|
113
|
+
redirect_uri: callbackUrl,
|
|
114
|
+
client_id: 'popeye-cli',
|
|
115
|
+
response_type: 'code',
|
|
116
|
+
scope: 'cli',
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
return `${baseUrl}?${params.toString()}`;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Logout from Claude CLI
|
|
124
|
+
* Removes stored credentials
|
|
125
|
+
*/
|
|
126
|
+
export async function logoutClaude(): Promise<void> {
|
|
127
|
+
const deleted = await deleteClaudeCredential();
|
|
128
|
+
if (deleted) {
|
|
129
|
+
console.log('Claude CLI credentials removed.');
|
|
130
|
+
} else {
|
|
131
|
+
console.log('No Claude CLI credentials found.');
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Refresh Claude CLI authentication
|
|
137
|
+
* Re-authenticates if the current token is expired or invalid
|
|
138
|
+
*/
|
|
139
|
+
export async function refreshClaudeAuth(): Promise<boolean> {
|
|
140
|
+
// Remove existing credentials
|
|
141
|
+
await deleteClaudeCredential();
|
|
142
|
+
|
|
143
|
+
// Re-authenticate
|
|
144
|
+
return authenticateClaude();
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Get the Claude CLI token for API calls
|
|
149
|
+
*/
|
|
150
|
+
export async function getClaudeToken(): Promise<string | null> {
|
|
151
|
+
return getClaudeCredential();
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Ensure Claude CLI is authenticated
|
|
156
|
+
* Prompts for authentication if not already authenticated
|
|
157
|
+
*/
|
|
158
|
+
export async function ensureClaudeAuth(): Promise<boolean> {
|
|
159
|
+
const status = await checkClaudeCLIAuth();
|
|
160
|
+
|
|
161
|
+
if (status.authenticated) {
|
|
162
|
+
return true;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return authenticateClaude();
|
|
166
|
+
}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Authentication orchestration module
|
|
3
|
+
* Coordinates authentication for both Claude CLI and OpenAI API
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { checkClaudeCLIAuth, authenticateClaude, logoutClaude, type ClaudeAuthStatus } from './claude.js';
|
|
7
|
+
import { checkOpenAIAuth, authenticateOpenAI, logoutOpenAI, type OpenAIAuthStatus } from './openai.js';
|
|
8
|
+
import { clearAllCredentials } from './keychain.js';
|
|
9
|
+
import type { AuthStatus } from '../types/index.js';
|
|
10
|
+
|
|
11
|
+
// Re-export individual auth modules
|
|
12
|
+
export * from './claude.js';
|
|
13
|
+
export * from './openai.js';
|
|
14
|
+
export * from './keychain.js';
|
|
15
|
+
export * from './server.js';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Combined authentication status
|
|
19
|
+
*/
|
|
20
|
+
export interface CombinedAuthStatus {
|
|
21
|
+
claude: ClaudeAuthStatus;
|
|
22
|
+
openai: OpenAIAuthStatus;
|
|
23
|
+
fullyAuthenticated: boolean;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Get the authentication status for both services
|
|
28
|
+
*/
|
|
29
|
+
export async function getAuthStatus(): Promise<CombinedAuthStatus> {
|
|
30
|
+
const [claudeStatus, openaiStatus] = await Promise.all([
|
|
31
|
+
checkClaudeCLIAuth(),
|
|
32
|
+
checkOpenAIAuth(),
|
|
33
|
+
]);
|
|
34
|
+
|
|
35
|
+
return {
|
|
36
|
+
claude: claudeStatus,
|
|
37
|
+
openai: openaiStatus,
|
|
38
|
+
fullyAuthenticated: claudeStatus.authenticated && openaiStatus.authenticated,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Get auth status formatted for CLI display
|
|
44
|
+
*/
|
|
45
|
+
export async function getAuthStatusForDisplay(): Promise<AuthStatus> {
|
|
46
|
+
const status = await getAuthStatus();
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
claude: {
|
|
50
|
+
authenticated: status.claude.authenticated,
|
|
51
|
+
user: status.claude.user,
|
|
52
|
+
expires: status.claude.expires,
|
|
53
|
+
},
|
|
54
|
+
openai: {
|
|
55
|
+
authenticated: status.openai.authenticated,
|
|
56
|
+
keyLastFour: status.openai.keyLastFour,
|
|
57
|
+
modelAccess: status.openai.modelAccess,
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Ensure both services are authenticated
|
|
64
|
+
* Prompts for authentication if either is missing
|
|
65
|
+
*
|
|
66
|
+
* @returns True if both services are authenticated
|
|
67
|
+
*/
|
|
68
|
+
export async function ensureAuthenticated(): Promise<boolean> {
|
|
69
|
+
const status = await getAuthStatus();
|
|
70
|
+
|
|
71
|
+
if (status.fullyAuthenticated) {
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
let success = true;
|
|
76
|
+
|
|
77
|
+
// Authenticate Claude if needed
|
|
78
|
+
if (!status.claude.authenticated) {
|
|
79
|
+
console.log('\n--- Claude CLI Authentication ---\n');
|
|
80
|
+
const claudeSuccess = await authenticateClaude();
|
|
81
|
+
if (!claudeSuccess) {
|
|
82
|
+
success = false;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Authenticate OpenAI if needed
|
|
87
|
+
if (!status.openai.authenticated) {
|
|
88
|
+
console.log('\n--- OpenAI API Authentication ---\n');
|
|
89
|
+
const openaiSuccess = await authenticateOpenAI();
|
|
90
|
+
if (!openaiSuccess) {
|
|
91
|
+
success = false;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return success;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Authenticate a specific service
|
|
100
|
+
*
|
|
101
|
+
* @param service - The service to authenticate ('claude', 'openai', or 'all')
|
|
102
|
+
* @returns True if authentication was successful
|
|
103
|
+
*/
|
|
104
|
+
export async function authenticateService(
|
|
105
|
+
service: 'claude' | 'openai' | 'all'
|
|
106
|
+
): Promise<boolean> {
|
|
107
|
+
switch (service) {
|
|
108
|
+
case 'claude':
|
|
109
|
+
return authenticateClaude();
|
|
110
|
+
case 'openai':
|
|
111
|
+
return authenticateOpenAI();
|
|
112
|
+
case 'all':
|
|
113
|
+
return ensureAuthenticated();
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Logout from a specific service or all services
|
|
119
|
+
*
|
|
120
|
+
* @param service - The service to logout from ('claude', 'openai', or 'all')
|
|
121
|
+
*/
|
|
122
|
+
export async function logout(service: 'claude' | 'openai' | 'all'): Promise<void> {
|
|
123
|
+
switch (service) {
|
|
124
|
+
case 'claude':
|
|
125
|
+
await logoutClaude();
|
|
126
|
+
break;
|
|
127
|
+
case 'openai':
|
|
128
|
+
await logoutOpenAI();
|
|
129
|
+
break;
|
|
130
|
+
case 'all':
|
|
131
|
+
await clearAllCredentials();
|
|
132
|
+
console.log('All credentials removed.');
|
|
133
|
+
break;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Check if a specific service is authenticated
|
|
139
|
+
*
|
|
140
|
+
* @param service - The service to check
|
|
141
|
+
* @returns True if the service is authenticated
|
|
142
|
+
*/
|
|
143
|
+
export async function isAuthenticated(service: 'claude' | 'openai' | 'both'): Promise<boolean> {
|
|
144
|
+
const status = await getAuthStatus();
|
|
145
|
+
|
|
146
|
+
switch (service) {
|
|
147
|
+
case 'claude':
|
|
148
|
+
return status.claude.authenticated;
|
|
149
|
+
case 'openai':
|
|
150
|
+
return status.openai.authenticated;
|
|
151
|
+
case 'both':
|
|
152
|
+
return status.fullyAuthenticated;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Require authentication, throwing an error if not authenticated
|
|
158
|
+
*
|
|
159
|
+
* @throws Error if not authenticated
|
|
160
|
+
*/
|
|
161
|
+
export async function requireAuth(): Promise<void> {
|
|
162
|
+
const status = await getAuthStatus();
|
|
163
|
+
|
|
164
|
+
if (!status.claude.authenticated) {
|
|
165
|
+
throw new Error('Claude CLI authentication required. Run: popeye-cli auth claude');
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (!status.openai.authenticated) {
|
|
169
|
+
throw new Error('OpenAI API authentication required. Run: popeye-cli auth openai');
|
|
170
|
+
}
|
|
171
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Keychain wrapper for secure credential storage
|
|
3
|
+
* Uses keytar for cross-platform keychain access (macOS Keychain, Windows Credential Vault, Linux Secret Service)
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import * as keytar from 'keytar';
|
|
7
|
+
import { SERVICE_NAME, KEYCHAIN_ACCOUNTS, ENV_VARS } from '../config/defaults.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Get a credential from the system keychain
|
|
11
|
+
* Falls back to environment variable if keychain is unavailable
|
|
12
|
+
*
|
|
13
|
+
* @param account - The account name (e.g., 'claude-cli', 'openai-api')
|
|
14
|
+
* @returns The stored credential or null if not found
|
|
15
|
+
*/
|
|
16
|
+
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`);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Fallback to environment variables
|
|
28
|
+
if (account === KEYCHAIN_ACCOUNTS.OPENAI) {
|
|
29
|
+
return process.env[ENV_VARS.OPENAI_KEY] || null;
|
|
30
|
+
}
|
|
31
|
+
if (account === KEYCHAIN_ACCOUNTS.CLAUDE) {
|
|
32
|
+
return process.env[ENV_VARS.ANTHROPIC_KEY] || null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Store a credential in the system keychain
|
|
40
|
+
*
|
|
41
|
+
* @param account - The account name
|
|
42
|
+
* @param password - The credential to store
|
|
43
|
+
*/
|
|
44
|
+
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
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Delete a credential from the system keychain
|
|
56
|
+
*
|
|
57
|
+
* @param account - The account name
|
|
58
|
+
* @returns True if the credential was deleted, false if it didn't exist
|
|
59
|
+
*/
|
|
60
|
+
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;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Check if a credential exists in the keychain
|
|
71
|
+
*
|
|
72
|
+
* @param account - The account name
|
|
73
|
+
* @returns True if the credential exists
|
|
74
|
+
*/
|
|
75
|
+
export async function hasCredential(account: string): Promise<boolean> {
|
|
76
|
+
const credential = await getCredential(account);
|
|
77
|
+
return credential !== null;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Get the Claude CLI credential
|
|
82
|
+
*/
|
|
83
|
+
export async function getClaudeCredential(): Promise<string | null> {
|
|
84
|
+
return getCredential(KEYCHAIN_ACCOUNTS.CLAUDE);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Set the Claude CLI credential
|
|
89
|
+
*/
|
|
90
|
+
export async function setClaudeCredential(token: string): Promise<void> {
|
|
91
|
+
return setCredential(KEYCHAIN_ACCOUNTS.CLAUDE, token);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Delete the Claude CLI credential
|
|
96
|
+
*/
|
|
97
|
+
export async function deleteClaudeCredential(): Promise<boolean> {
|
|
98
|
+
return deleteCredential(KEYCHAIN_ACCOUNTS.CLAUDE);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Get the OpenAI API credential
|
|
103
|
+
*/
|
|
104
|
+
export async function getOpenAICredential(): Promise<string | null> {
|
|
105
|
+
return getCredential(KEYCHAIN_ACCOUNTS.OPENAI);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Set the OpenAI API credential
|
|
110
|
+
*/
|
|
111
|
+
export async function setOpenAICredential(apiKey: string): Promise<void> {
|
|
112
|
+
return setCredential(KEYCHAIN_ACCOUNTS.OPENAI, apiKey);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Delete the OpenAI API credential
|
|
117
|
+
*/
|
|
118
|
+
export async function deleteOpenAICredential(): Promise<boolean> {
|
|
119
|
+
return deleteCredential(KEYCHAIN_ACCOUNTS.OPENAI);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Clear all stored credentials
|
|
124
|
+
*/
|
|
125
|
+
export async function clearAllCredentials(): Promise<void> {
|
|
126
|
+
await deleteClaudeCredential();
|
|
127
|
+
await deleteOpenAICredential();
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Get the last 4 characters of a credential for display purposes
|
|
132
|
+
*/
|
|
133
|
+
export function maskCredential(credential: string): string {
|
|
134
|
+
if (credential.length <= 4) {
|
|
135
|
+
return '****';
|
|
136
|
+
}
|
|
137
|
+
return `****${credential.slice(-4)}`;
|
|
138
|
+
}
|