popeye-cli 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (209) hide show
  1. package/.env.example +25 -0
  2. package/.prettierrc +8 -0
  3. package/README.md +320 -0
  4. package/dist/adapters/claude.d.ts +82 -0
  5. package/dist/adapters/claude.d.ts.map +1 -0
  6. package/dist/adapters/claude.js +230 -0
  7. package/dist/adapters/claude.js.map +1 -0
  8. package/dist/adapters/openai.d.ts +48 -0
  9. package/dist/adapters/openai.d.ts.map +1 -0
  10. package/dist/adapters/openai.js +257 -0
  11. package/dist/adapters/openai.js.map +1 -0
  12. package/dist/auth/claude.d.ts +44 -0
  13. package/dist/auth/claude.d.ts.map +1 -0
  14. package/dist/auth/claude.js +139 -0
  15. package/dist/auth/claude.js.map +1 -0
  16. package/dist/auth/index.d.ts +61 -0
  17. package/dist/auth/index.d.ts.map +1 -0
  18. package/dist/auth/index.js +141 -0
  19. package/dist/auth/index.js.map +1 -0
  20. package/dist/auth/keychain.d.ts +66 -0
  21. package/dist/auth/keychain.d.ts.map +1 -0
  22. package/dist/auth/keychain.js +125 -0
  23. package/dist/auth/keychain.js.map +1 -0
  24. package/dist/auth/openai-entry.d.ts +9 -0
  25. package/dist/auth/openai-entry.d.ts.map +1 -0
  26. package/dist/auth/openai-entry.js +410 -0
  27. package/dist/auth/openai-entry.js.map +1 -0
  28. package/dist/auth/openai.d.ts +71 -0
  29. package/dist/auth/openai.d.ts.map +1 -0
  30. package/dist/auth/openai.js +212 -0
  31. package/dist/auth/openai.js.map +1 -0
  32. package/dist/auth/server.d.ts +32 -0
  33. package/dist/auth/server.d.ts.map +1 -0
  34. package/dist/auth/server.js +213 -0
  35. package/dist/auth/server.js.map +1 -0
  36. package/dist/cli/commands/auth.d.ts +10 -0
  37. package/dist/cli/commands/auth.d.ts.map +1 -0
  38. package/dist/cli/commands/auth.js +162 -0
  39. package/dist/cli/commands/auth.js.map +1 -0
  40. package/dist/cli/commands/config.d.ts +10 -0
  41. package/dist/cli/commands/config.d.ts.map +1 -0
  42. package/dist/cli/commands/config.js +215 -0
  43. package/dist/cli/commands/config.js.map +1 -0
  44. package/dist/cli/commands/create.d.ts +10 -0
  45. package/dist/cli/commands/create.d.ts.map +1 -0
  46. package/dist/cli/commands/create.js +240 -0
  47. package/dist/cli/commands/create.js.map +1 -0
  48. package/dist/cli/commands/index.d.ts +10 -0
  49. package/dist/cli/commands/index.d.ts.map +1 -0
  50. package/dist/cli/commands/index.js +10 -0
  51. package/dist/cli/commands/index.js.map +1 -0
  52. package/dist/cli/commands/resume.d.ts +18 -0
  53. package/dist/cli/commands/resume.d.ts.map +1 -0
  54. package/dist/cli/commands/resume.js +241 -0
  55. package/dist/cli/commands/resume.js.map +1 -0
  56. package/dist/cli/commands/status.d.ts +18 -0
  57. package/dist/cli/commands/status.d.ts.map +1 -0
  58. package/dist/cli/commands/status.js +154 -0
  59. package/dist/cli/commands/status.js.map +1 -0
  60. package/dist/cli/index.d.ts +17 -0
  61. package/dist/cli/index.d.ts.map +1 -0
  62. package/dist/cli/index.js +71 -0
  63. package/dist/cli/index.js.map +1 -0
  64. package/dist/cli/interactive.d.ts +9 -0
  65. package/dist/cli/interactive.d.ts.map +1 -0
  66. package/dist/cli/interactive.js +330 -0
  67. package/dist/cli/interactive.js.map +1 -0
  68. package/dist/cli/output.d.ts +182 -0
  69. package/dist/cli/output.d.ts.map +1 -0
  70. package/dist/cli/output.js +355 -0
  71. package/dist/cli/output.js.map +1 -0
  72. package/dist/config/defaults.d.ts +57 -0
  73. package/dist/config/defaults.d.ts.map +1 -0
  74. package/dist/config/defaults.js +103 -0
  75. package/dist/config/defaults.js.map +1 -0
  76. package/dist/config/index.d.ts +138 -0
  77. package/dist/config/index.d.ts.map +1 -0
  78. package/dist/config/index.js +244 -0
  79. package/dist/config/index.js.map +1 -0
  80. package/dist/config/schema.d.ts +220 -0
  81. package/dist/config/schema.d.ts.map +1 -0
  82. package/dist/config/schema.js +141 -0
  83. package/dist/config/schema.js.map +1 -0
  84. package/dist/generators/index.d.ts +101 -0
  85. package/dist/generators/index.d.ts.map +1 -0
  86. package/dist/generators/index.js +200 -0
  87. package/dist/generators/index.js.map +1 -0
  88. package/dist/generators/python.d.ts +48 -0
  89. package/dist/generators/python.d.ts.map +1 -0
  90. package/dist/generators/python.js +262 -0
  91. package/dist/generators/python.js.map +1 -0
  92. package/dist/generators/templates/index.d.ts +6 -0
  93. package/dist/generators/templates/index.d.ts.map +1 -0
  94. package/dist/generators/templates/index.js +6 -0
  95. package/dist/generators/templates/index.js.map +1 -0
  96. package/dist/generators/templates/python.d.ts +53 -0
  97. package/dist/generators/templates/python.d.ts.map +1 -0
  98. package/dist/generators/templates/python.js +454 -0
  99. package/dist/generators/templates/python.js.map +1 -0
  100. package/dist/generators/templates/typescript.d.ts +53 -0
  101. package/dist/generators/templates/typescript.d.ts.map +1 -0
  102. package/dist/generators/templates/typescript.js +394 -0
  103. package/dist/generators/templates/typescript.js.map +1 -0
  104. package/dist/generators/typescript.d.ts +64 -0
  105. package/dist/generators/typescript.d.ts.map +1 -0
  106. package/dist/generators/typescript.js +271 -0
  107. package/dist/generators/typescript.js.map +1 -0
  108. package/dist/index.d.ts +7 -0
  109. package/dist/index.d.ts.map +1 -0
  110. package/dist/index.js +12 -0
  111. package/dist/index.js.map +1 -0
  112. package/dist/state/index.d.ts +168 -0
  113. package/dist/state/index.d.ts.map +1 -0
  114. package/dist/state/index.js +338 -0
  115. package/dist/state/index.js.map +1 -0
  116. package/dist/state/persistence.d.ts +91 -0
  117. package/dist/state/persistence.d.ts.map +1 -0
  118. package/dist/state/persistence.js +201 -0
  119. package/dist/state/persistence.js.map +1 -0
  120. package/dist/types/cli.d.ts +132 -0
  121. package/dist/types/cli.d.ts.map +1 -0
  122. package/dist/types/cli.js +17 -0
  123. package/dist/types/cli.js.map +1 -0
  124. package/dist/types/consensus.d.ts +111 -0
  125. package/dist/types/consensus.d.ts.map +1 -0
  126. package/dist/types/consensus.js +29 -0
  127. package/dist/types/consensus.js.map +1 -0
  128. package/dist/types/index.d.ts +9 -0
  129. package/dist/types/index.d.ts.map +1 -0
  130. package/dist/types/index.js +13 -0
  131. package/dist/types/index.js.map +1 -0
  132. package/dist/types/project.d.ts +73 -0
  133. package/dist/types/project.d.ts.map +1 -0
  134. package/dist/types/project.js +55 -0
  135. package/dist/types/project.js.map +1 -0
  136. package/dist/types/workflow.d.ts +236 -0
  137. package/dist/types/workflow.d.ts.map +1 -0
  138. package/dist/types/workflow.js +74 -0
  139. package/dist/types/workflow.js.map +1 -0
  140. package/dist/workflow/consensus.d.ts +89 -0
  141. package/dist/workflow/consensus.d.ts.map +1 -0
  142. package/dist/workflow/consensus.js +220 -0
  143. package/dist/workflow/consensus.js.map +1 -0
  144. package/dist/workflow/execution-mode.d.ts +82 -0
  145. package/dist/workflow/execution-mode.d.ts.map +1 -0
  146. package/dist/workflow/execution-mode.js +346 -0
  147. package/dist/workflow/execution-mode.js.map +1 -0
  148. package/dist/workflow/index.d.ts +110 -0
  149. package/dist/workflow/index.d.ts.map +1 -0
  150. package/dist/workflow/index.js +283 -0
  151. package/dist/workflow/index.js.map +1 -0
  152. package/dist/workflow/plan-mode.d.ts +83 -0
  153. package/dist/workflow/plan-mode.d.ts.map +1 -0
  154. package/dist/workflow/plan-mode.js +241 -0
  155. package/dist/workflow/plan-mode.js.map +1 -0
  156. package/dist/workflow/test-runner.d.ts +87 -0
  157. package/dist/workflow/test-runner.d.ts.map +1 -0
  158. package/dist/workflow/test-runner.js +273 -0
  159. package/dist/workflow/test-runner.js.map +1 -0
  160. package/eslint.config.js +25 -0
  161. package/package.json +66 -0
  162. package/src/adapters/claude.ts +298 -0
  163. package/src/adapters/openai.ts +300 -0
  164. package/src/auth/claude.ts +166 -0
  165. package/src/auth/index.ts +171 -0
  166. package/src/auth/keychain.ts +138 -0
  167. package/src/auth/openai-entry.ts +410 -0
  168. package/src/auth/openai.ts +260 -0
  169. package/src/auth/server.ts +252 -0
  170. package/src/cli/commands/auth.ts +194 -0
  171. package/src/cli/commands/config.ts +241 -0
  172. package/src/cli/commands/create.ts +308 -0
  173. package/src/cli/commands/index.ts +10 -0
  174. package/src/cli/commands/resume.ts +304 -0
  175. package/src/cli/commands/status.ts +189 -0
  176. package/src/cli/index.ts +90 -0
  177. package/src/cli/interactive.ts +418 -0
  178. package/src/cli/output.ts +410 -0
  179. package/src/config/defaults.ts +114 -0
  180. package/src/config/index.ts +315 -0
  181. package/src/config/schema.ts +164 -0
  182. package/src/generators/index.ts +251 -0
  183. package/src/generators/python.ts +318 -0
  184. package/src/generators/templates/index.ts +6 -0
  185. package/src/generators/templates/python.ts +465 -0
  186. package/src/generators/templates/typescript.ts +417 -0
  187. package/src/generators/typescript.ts +340 -0
  188. package/src/index.ts +13 -0
  189. package/src/state/index.ts +454 -0
  190. package/src/state/persistence.ts +230 -0
  191. package/src/types/cli.ts +146 -0
  192. package/src/types/consensus.ts +116 -0
  193. package/src/types/index.ts +64 -0
  194. package/src/types/project.ts +85 -0
  195. package/src/types/workflow.ts +149 -0
  196. package/src/workflow/consensus.ts +299 -0
  197. package/src/workflow/execution-mode.ts +517 -0
  198. package/src/workflow/index.ts +396 -0
  199. package/src/workflow/plan-mode.ts +356 -0
  200. package/src/workflow/test-runner.ts +345 -0
  201. package/tests/adapters/openai.test.ts +145 -0
  202. package/tests/config/config.test.ts +208 -0
  203. package/tests/generators/generators.test.ts +185 -0
  204. package/tests/types/consensus.test.ts +152 -0
  205. package/tests/types/project.test.ts +134 -0
  206. package/tests/workflow/consensus.test.ts +221 -0
  207. package/tests/workflow/test-runner.test.ts +214 -0
  208. package/tsconfig.json +25 -0
  209. package/vitest.config.ts +22 -0
@@ -0,0 +1,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
+ }