fluxy-bot 0.8.9 → 0.9.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 (106) hide show
  1. package/README.md +53 -84
  2. package/bin/cli.js +1717 -6
  3. package/components.json +18 -18
  4. package/package.json +98 -109
  5. package/postcss.config.js +3 -3
  6. package/scripts/install.ps1 +1 -1
  7. package/scripts/install.sh +1 -1
  8. package/scripts/postinstall.js +47 -58
  9. package/shared/ai.ts +118 -202
  10. package/shared/config.ts +37 -39
  11. package/shared/logger.ts +6 -14
  12. package/shared/paths.ts +10 -16
  13. package/shared/relay.ts +89 -98
  14. package/supervisor/backend.ts +77 -89
  15. package/supervisor/chat/ARCHITECTURE.md +13 -14
  16. package/supervisor/chat/OnboardWizard.tsx +2194 -3419
  17. package/supervisor/chat/fluxy-main.tsx +495 -633
  18. package/supervisor/chat/fluxy.html +24 -36
  19. package/supervisor/chat/onboard-main.tsx +8 -8
  20. package/supervisor/chat/onboard.html +10 -13
  21. package/supervisor/chat/src/components/Chat/AudioBubble.tsx +94 -108
  22. package/supervisor/chat/src/components/Chat/ChatView.tsx +14 -36
  23. package/supervisor/chat/src/components/Chat/ImageLightbox.tsx +71 -89
  24. package/supervisor/chat/src/components/Chat/InputBar.tsx +498 -728
  25. package/supervisor/chat/src/components/Chat/MessageBubble.tsx +154 -246
  26. package/supervisor/chat/src/components/Chat/MessageList.tsx +114 -138
  27. package/supervisor/chat/src/components/Chat/TypingIndicator.tsx +38 -50
  28. package/supervisor/chat/src/components/LoginScreen.tsx +245 -296
  29. package/supervisor/chat/src/hooks/useChat.ts +210 -259
  30. package/supervisor/chat/src/hooks/useFluxyChat.ts +232 -322
  31. package/supervisor/chat/src/hooks/useSpeechRecognition.ts +126 -148
  32. package/supervisor/chat/src/lib/auth.ts +20 -23
  33. package/supervisor/chat/src/lib/ws-client.ts +113 -125
  34. package/supervisor/chat/src/styles/globals.css +76 -85
  35. package/supervisor/file-saver.ts +39 -52
  36. package/supervisor/fluxy-agent.ts +213 -292
  37. package/supervisor/index.ts +895 -1263
  38. package/supervisor/scheduler.ts +237 -299
  39. package/supervisor/tunnel.ts +113 -167
  40. package/supervisor/vite-dev.ts +51 -61
  41. package/supervisor/widget.js +135 -147
  42. package/supervisor/worker.ts +45 -49
  43. package/tsconfig.json +18 -24
  44. package/vite.config.ts +48 -55
  45. package/vite.fluxy.config.ts +34 -34
  46. package/worker/claude-auth.ts +202 -265
  47. package/worker/codex-auth.ts +144 -181
  48. package/worker/db.ts +63 -191
  49. package/worker/index.ts +601 -842
  50. package/workspace/MYSELF.md +1 -1
  51. package/workspace/PULSE.json +6 -6
  52. package/workspace/backend/index.ts +16 -19
  53. package/workspace/client/index.html +47 -60
  54. package/workspace/client/public/manifest.json +21 -21
  55. package/workspace/client/public/sw.js +29 -31
  56. package/workspace/client/src/App.tsx +91 -136
  57. package/workspace/client/src/components/Dashboard/DashboardPage.tsx +68 -72
  58. package/workspace/client/src/components/ErrorBoundary.tsx +11 -11
  59. package/workspace/client/src/components/Layout/DashboardLayout.tsx +35 -35
  60. package/workspace/client/src/components/Layout/Footer.tsx +12 -12
  61. package/workspace/client/src/components/Layout/MobileNav.tsx +23 -23
  62. package/workspace/client/src/components/Layout/Sidebar.tsx +81 -81
  63. package/workspace/client/src/components/ui/avatar.tsx +85 -85
  64. package/workspace/client/src/components/ui/badge.tsx +40 -41
  65. package/workspace/client/src/components/ui/button.tsx +56 -57
  66. package/workspace/client/src/components/ui/card.tsx +75 -75
  67. package/workspace/client/src/components/ui/dialog.tsx +114 -117
  68. package/workspace/client/src/components/ui/dropdown-menu.tsx +191 -199
  69. package/workspace/client/src/components/ui/input.tsx +17 -17
  70. package/workspace/client/src/components/ui/scroll-area.tsx +47 -47
  71. package/workspace/client/src/components/ui/select.tsx +145 -151
  72. package/workspace/client/src/components/ui/separator.tsx +21 -21
  73. package/workspace/client/src/components/ui/sheet.tsx +102 -102
  74. package/workspace/client/src/components/ui/skeleton.tsx +10 -10
  75. package/workspace/client/src/components/ui/switch.tsx +26 -26
  76. package/workspace/client/src/components/ui/tabs.tsx +69 -69
  77. package/workspace/client/src/components/ui/textarea.tsx +14 -14
  78. package/workspace/client/src/components/ui/tooltip.tsx +37 -37
  79. package/workspace/client/src/lib/utils.ts +1 -1
  80. package/workspace/client/src/main.tsx +3 -3
  81. package/workspace/client/src/styles/globals.css +76 -85
  82. package/workspace/skills/code-reviewer/.claude-plugin/plugin.json +3 -3
  83. package/workspace/skills/code-reviewer/skills/code-reviewer/SKILL.md +0 -6
  84. package/workspace/skills/daily-standup/.claude-plugin/plugin.json +3 -3
  85. package/workspace/skills/daily-standup/skills/daily-standup/SKILL.md +0 -7
  86. package/workspace/skills/workspace-helper/.claude-plugin/plugin.json +3 -3
  87. package/workspace/skills/workspace-helper/skills/workspace-helper/SKILL.md +0 -2
  88. package/bin/cli.backup.js +0 -2138
  89. package/cli/commands/daemon.ts +0 -42
  90. package/cli/commands/init.ts +0 -32
  91. package/cli/commands/start.ts +0 -113
  92. package/cli/commands/tunnel.temp.ts +0 -206
  93. package/cli/commands/tunnel.ts +0 -227
  94. package/cli/commands/update.ts +0 -163
  95. package/cli/core/base-adapter.ts +0 -156
  96. package/cli/core/cloudflared.ts +0 -113
  97. package/cli/core/config.ts +0 -73
  98. package/cli/core/os-detector.ts +0 -43
  99. package/cli/core/server.ts +0 -109
  100. package/cli/core/types.ts +0 -15
  101. package/cli/index.ts +0 -72
  102. package/cli/platforms/darwin.ts +0 -146
  103. package/cli/platforms/index.ts +0 -21
  104. package/cli/platforms/linux.ts +0 -156
  105. package/cli/platforms/win32.ts +0 -34
  106. package/cli/utils/ui.ts +0 -37
@@ -14,11 +14,11 @@ import os from 'os';
14
14
  import { log } from '../shared/logger.js';
15
15
 
16
16
  const OAUTH_CONFIG = {
17
- AUTHORIZE_URL: 'https://claude.ai/oauth/authorize',
18
- TOKEN_URL: 'https://console.anthropic.com/v1/oauth/token',
19
- REDIRECT_URI: 'https://console.anthropic.com/oauth/code/callback',
20
- CLIENT_ID: '9d1c250a-e61b-44d9-88ed-5944d1962f5e',
21
- SCOPES: 'org:create_api_key user:profile user:inference'
17
+ AUTHORIZE_URL: 'https://claude.ai/oauth/authorize',
18
+ TOKEN_URL: 'https://console.anthropic.com/v1/oauth/token',
19
+ REDIRECT_URI: 'https://console.anthropic.com/oauth/code/callback',
20
+ CLIENT_ID: '9d1c250a-e61b-44d9-88ed-5944d1962f5e',
21
+ SCOPES: 'org:create_api_key user:profile user:inference',
22
22
  };
23
23
 
24
24
  const CLAUDE_DIR = path.join(os.homedir(), '.claude');
@@ -28,137 +28,116 @@ let codeVerifier: string | null = null;
28
28
 
29
29
  /* ── Public API ── */
30
30
 
31
- export function startClaudeOAuth(): {
32
- success: boolean;
33
- authUrl?: string;
34
- error?: string;
35
- } {
36
- // Generate PKCE
37
- codeVerifier = crypto.randomBytes(32).toString('base64url');
38
- const codeChallenge = crypto
39
- .createHash('sha256')
40
- .update(codeVerifier)
41
- .digest('base64url');
42
-
43
- const params = new URLSearchParams({
44
- code: 'true',
45
- client_id: OAUTH_CONFIG.CLIENT_ID,
46
- response_type: 'code',
47
- redirect_uri: OAUTH_CONFIG.REDIRECT_URI,
48
- scope: OAUTH_CONFIG.SCOPES,
49
- code_challenge: codeChallenge,
50
- code_challenge_method: 'S256',
51
- state: codeVerifier // state = verifier (OpenClaw legacy flow)
52
- });
53
-
54
- const authUrl = `${OAUTH_CONFIG.AUTHORIZE_URL}?${params.toString()}`;
55
- log.ok('Claude OAuth flow started');
56
- return { success: true, authUrl };
31
+ export function startClaudeOAuth(): { success: boolean; authUrl?: string; error?: string } {
32
+ // Generate PKCE
33
+ codeVerifier = crypto.randomBytes(32).toString('base64url');
34
+ const codeChallenge = crypto.createHash('sha256').update(codeVerifier).digest('base64url');
35
+
36
+ const params = new URLSearchParams({
37
+ code: 'true',
38
+ client_id: OAUTH_CONFIG.CLIENT_ID,
39
+ response_type: 'code',
40
+ redirect_uri: OAUTH_CONFIG.REDIRECT_URI,
41
+ scope: OAUTH_CONFIG.SCOPES,
42
+ code_challenge: codeChallenge,
43
+ code_challenge_method: 'S256',
44
+ state: codeVerifier, // state = verifier (OpenClaw legacy flow)
45
+ });
46
+
47
+ const authUrl = `${OAUTH_CONFIG.AUTHORIZE_URL}?${params.toString()}`;
48
+ log.ok('Claude OAuth flow started');
49
+ return { success: true, authUrl };
57
50
  }
58
51
 
59
- export async function exchangeClaudeCode(
60
- codeInput: string
61
- ): Promise<{ success: boolean; error?: string }> {
62
- if (!codeVerifier) {
63
- return {
64
- success: false,
65
- error: 'OAuth flow not started. Click "Authenticate" first.'
66
- };
67
- }
68
-
69
- // Parse code — might be "code#state" or just "code"
70
- const parts = codeInput.trim().split('#');
71
- const code = parts[0].trim();
72
- const state = parts[1]?.trim() || codeVerifier;
52
+ export async function exchangeClaudeCode(codeInput: string): Promise<{ success: boolean; error?: string }> {
53
+ if (!codeVerifier) {
54
+ return { success: false, error: 'OAuth flow not started. Click "Authenticate" first.' };
55
+ }
56
+
57
+ // Parse code — might be "code#state" or just "code"
58
+ const parts = codeInput.trim().split('#');
59
+ const code = parts[0].trim();
60
+ const state = parts[1]?.trim() || codeVerifier;
61
+
62
+ if (!code) {
63
+ return { success: false, error: 'Invalid code. Please copy the full code from the page.' };
64
+ }
65
+
66
+ try {
67
+ // Token exchange uses JSON body (not form-urlencoded)
68
+ const payload = {
69
+ grant_type: 'authorization_code',
70
+ client_id: OAUTH_CONFIG.CLIENT_ID,
71
+ code,
72
+ state,
73
+ redirect_uri: OAUTH_CONFIG.REDIRECT_URI,
74
+ code_verifier: codeVerifier,
75
+ };
76
+
77
+ const response = await fetch(OAUTH_CONFIG.TOKEN_URL, {
78
+ method: 'POST',
79
+ headers: { 'Content-Type': 'application/json' },
80
+ body: JSON.stringify(payload),
81
+ });
73
82
 
74
- if (!code) {
75
- return {
76
- success: false,
77
- error: 'Invalid code. Please copy the full code from the page.'
78
- };
83
+ if (!response.ok) {
84
+ return { success: false, error: `Authentication failed (${response.status}). Please try again.` };
79
85
  }
80
86
 
81
- try {
82
- // Token exchange uses JSON body (not form-urlencoded)
83
- const payload = {
84
- grant_type: 'authorization_code',
85
- client_id: OAUTH_CONFIG.CLIENT_ID,
86
- code,
87
- state,
88
- redirect_uri: OAUTH_CONFIG.REDIRECT_URI,
89
- code_verifier: codeVerifier
90
- };
91
-
92
- const response = await fetch(OAUTH_CONFIG.TOKEN_URL, {
93
- method: 'POST',
94
- headers: { 'Content-Type': 'application/json' },
95
- body: JSON.stringify(payload)
96
- });
97
-
98
- if (!response.ok) {
99
- return {
100
- success: false,
101
- error: `Authentication failed (${response.status}). Please try again.`
102
- };
103
- }
104
-
105
- const tokens = await response.json();
106
- storeCredentials(tokens);
107
- codeVerifier = null;
108
- return { success: true };
109
- } catch (err: any) {
110
- return { success: false, error: err.message };
111
- }
87
+ const tokens = await response.json();
88
+ storeCredentials(tokens);
89
+ codeVerifier = null;
90
+ return { success: true };
91
+ } catch (err: any) {
92
+ return { success: false, error: err.message };
93
+ }
112
94
  }
113
95
 
114
- export async function getClaudeAuthStatus(): Promise<{
115
- authenticated: boolean;
116
- error?: string;
117
- }> {
118
- const oauth = readOAuthBlock();
119
- if (!oauth) return { authenticated: false };
96
+ export async function getClaudeAuthStatus(): Promise<{ authenticated: boolean; error?: string }> {
97
+ const oauth = readOAuthBlock();
98
+ if (!oauth) return { authenticated: false };
120
99
 
121
- if (oauth.accessToken && oauth.expiresAt && Date.now() < oauth.expiresAt) {
122
- return { authenticated: true };
123
- }
100
+ if (oauth.accessToken && oauth.expiresAt && Date.now() < oauth.expiresAt) {
101
+ return { authenticated: true };
102
+ }
124
103
 
125
- // Token expired or missing — try refresh
126
- if (oauth.refreshToken) {
127
- const refreshed = await refreshClaudeToken(oauth.refreshToken);
128
- if (refreshed) return { authenticated: true };
129
- }
104
+ // Token expired or missing — try refresh
105
+ if (oauth.refreshToken) {
106
+ const refreshed = await refreshClaudeToken(oauth.refreshToken);
107
+ if (refreshed) return { authenticated: true };
108
+ }
130
109
 
131
- return { authenticated: false, error: 'Token expired' };
110
+ return { authenticated: false, error: 'Token expired' };
132
111
  }
133
112
 
134
113
  export function readClaudeAccessToken(): string | null {
135
- const oauth = readOAuthBlock();
136
- if (!oauth?.accessToken) return null;
137
- if (oauth.expiresAt && Date.now() >= oauth.expiresAt) return null;
138
- return oauth.accessToken;
114
+ const oauth = readOAuthBlock();
115
+ if (!oauth?.accessToken) return null;
116
+ if (oauth.expiresAt && Date.now() >= oauth.expiresAt) return null;
117
+ return oauth.accessToken;
139
118
  }
140
119
 
141
120
  /** Read a valid access token, refreshing if expired. Returns null if unavailable. */
142
121
  export async function getClaudeAccessToken(): Promise<string | null> {
143
- const oauth = readOAuthBlock();
144
- if (!oauth?.accessToken) return null;
122
+ const oauth = readOAuthBlock();
123
+ if (!oauth?.accessToken) return null;
145
124
 
146
- // Token still valid
147
- if (oauth.expiresAt && Date.now() < oauth.expiresAt) {
148
- return oauth.accessToken;
149
- }
150
-
151
- // Try refresh
152
- if (oauth.refreshToken) {
153
- const refreshed = await refreshClaudeToken(oauth.refreshToken);
154
- if (refreshed) {
155
- // Re-read after refresh
156
- const fresh = readOAuthBlock();
157
- return fresh?.accessToken || null;
158
- }
125
+ // Token still valid
126
+ if (oauth.expiresAt && Date.now() < oauth.expiresAt) {
127
+ return oauth.accessToken;
128
+ }
129
+
130
+ // Try refresh
131
+ if (oauth.refreshToken) {
132
+ const refreshed = await refreshClaudeToken(oauth.refreshToken);
133
+ if (refreshed) {
134
+ // Re-read after refresh
135
+ const fresh = readOAuthBlock();
136
+ return fresh?.accessToken || null;
159
137
  }
138
+ }
160
139
 
161
- return null;
140
+ return null;
162
141
  }
163
142
 
164
143
  /* ── Helpers ── */
@@ -172,50 +151,35 @@ export async function getClaudeAccessToken(): Promise<string | null> {
172
151
  * - Claude Code format: { claudeAiOauth: { accessToken, refreshToken, expiresAt, ... } }
173
152
  * - Legacy flat format: { accessToken, refreshToken, expiresAt }
174
153
  */
175
- function readOAuthBlock(): {
176
- accessToken?: string;
177
- refreshToken?: string;
178
- expiresAt?: number;
179
- } | null {
180
- // macOS: Keychain is the source of truth
181
- if (process.platform === 'darwin') {
182
- try {
183
- const result = execFileSync(
184
- 'security',
185
- [
186
- 'find-generic-password',
187
- '-s',
188
- 'Claude Code-credentials',
189
- '-w'
190
- ],
191
- { stdio: ['pipe', 'pipe', 'pipe'] }
192
- )
193
- .toString()
194
- .trim();
195
- const parsed = JSON.parse(result);
196
- const oauth = parsed.claudeAiOauth || parsed;
197
- if (oauth.accessToken) {
198
- log.ok('Read credentials from macOS Keychain');
199
- return oauth;
200
- }
201
- } catch {}
202
- // On macOS, if Keychain has no valid entry, don't trust stale files
203
- log.warn('No valid credentials in macOS Keychain');
204
- return null;
205
- }
206
-
207
- // Linux/Windows: credentials file
154
+ function readOAuthBlock(): { accessToken?: string; refreshToken?: string; expiresAt?: number } | null {
155
+ // macOS: Keychain is the source of truth
156
+ if (process.platform === 'darwin') {
208
157
  try {
209
- if (fs.existsSync(CREDENTIALS_FILE)) {
210
- const creds = JSON.parse(
211
- fs.readFileSync(CREDENTIALS_FILE, 'utf-8')
212
- );
213
- const oauth = creds.claudeAiOauth || creds;
214
- if (oauth.accessToken) return oauth;
215
- }
158
+ const result = execFileSync('security', [
159
+ 'find-generic-password', '-s', 'Claude Code-credentials', '-w',
160
+ ], { stdio: ['pipe', 'pipe', 'pipe'] }).toString().trim();
161
+ const parsed = JSON.parse(result);
162
+ const oauth = parsed.claudeAiOauth || parsed;
163
+ if (oauth.accessToken) {
164
+ log.ok('Read credentials from macOS Keychain');
165
+ return oauth;
166
+ }
216
167
  } catch {}
217
-
168
+ // On macOS, if Keychain has no valid entry, don't trust stale files
169
+ log.warn('No valid credentials in macOS Keychain');
218
170
  return null;
171
+ }
172
+
173
+ // Linux/Windows: credentials file
174
+ try {
175
+ if (fs.existsSync(CREDENTIALS_FILE)) {
176
+ const creds = JSON.parse(fs.readFileSync(CREDENTIALS_FILE, 'utf-8'));
177
+ const oauth = creds.claudeAiOauth || creds;
178
+ if (oauth.accessToken) return oauth;
179
+ }
180
+ } catch {}
181
+
182
+ return null;
219
183
  }
220
184
 
221
185
  /**
@@ -223,123 +187,96 @@ function readOAuthBlock(): {
223
187
  * Returns true if refresh succeeded and new credentials were stored.
224
188
  */
225
189
  async function refreshClaudeToken(refreshToken: string): Promise<boolean> {
226
- try {
227
- log.ok('Attempting Claude token refresh...');
228
- const response = await fetch(OAUTH_CONFIG.TOKEN_URL, {
229
- method: 'POST',
230
- headers: { 'Content-Type': 'application/json' },
231
- body: JSON.stringify({
232
- grant_type: 'refresh_token',
233
- client_id: OAUTH_CONFIG.CLIENT_ID,
234
- refresh_token: refreshToken
235
- })
236
- });
190
+ try {
191
+ log.ok('Attempting Claude token refresh...');
192
+ const response = await fetch(OAUTH_CONFIG.TOKEN_URL, {
193
+ method: 'POST',
194
+ headers: { 'Content-Type': 'application/json' },
195
+ body: JSON.stringify({
196
+ grant_type: 'refresh_token',
197
+ client_id: OAUTH_CONFIG.CLIENT_ID,
198
+ refresh_token: refreshToken,
199
+ }),
200
+ });
237
201
 
238
- if (!response.ok) {
239
- log.warn(`Claude token refresh failed: ${response.status}`);
240
- return false;
241
- }
242
-
243
- const tokens = await response.json();
244
- storeCredentials(tokens);
245
- log.ok('Claude token refreshed successfully');
246
- return true;
247
- } catch (err: any) {
248
- log.warn(`Claude token refresh error: ${err.message}`);
249
- return false;
202
+ if (!response.ok) {
203
+ log.warn(`Claude token refresh failed: ${response.status}`);
204
+ return false;
250
205
  }
206
+
207
+ const tokens = await response.json();
208
+ storeCredentials(tokens);
209
+ log.ok('Claude token refreshed successfully');
210
+ return true;
211
+ } catch (err: any) {
212
+ log.warn(`Claude token refresh error: ${err.message}`);
213
+ return false;
214
+ }
251
215
  }
252
216
 
253
217
  function storeCredentials(tokens: any): void {
254
- if (!fs.existsSync(CLAUDE_DIR)) {
255
- fs.mkdirSync(CLAUDE_DIR, { recursive: true });
218
+ if (!fs.existsSync(CLAUDE_DIR)) {
219
+ fs.mkdirSync(CLAUDE_DIR, { recursive: true });
220
+ }
221
+
222
+ // Read existing file to preserve other fields
223
+ let fileData: Record<string, any> = {};
224
+ try {
225
+ if (fs.existsSync(CREDENTIALS_FILE)) {
226
+ fileData = JSON.parse(fs.readFileSync(CREDENTIALS_FILE, 'utf-8'));
256
227
  }
257
-
258
- // Read existing file to preserve other fields
259
- let fileData: Record<string, any> = {};
260
- try {
261
- if (fs.existsSync(CREDENTIALS_FILE)) {
262
- fileData = JSON.parse(fs.readFileSync(CREDENTIALS_FILE, 'utf-8'));
263
- }
264
- } catch {}
265
-
266
- // Build oauth block — merge with existing claudeAiOauth to preserve scopes etc.
267
- const existing = fileData.claudeAiOauth || {};
268
- const oauth: Record<string, any> = { ...existing };
269
- oauth.accessToken = tokens.access_token;
270
- if (tokens.refresh_token) oauth.refreshToken = tokens.refresh_token;
271
- if (tokens.expires_in) {
272
- oauth.expiresAt = Date.now() + (tokens.expires_in - 300) * 1000;
273
- }
274
-
275
- // Write in Claude Code's format
276
- fileData.claudeAiOauth = oauth;
277
- // Remove legacy flat keys if they exist
278
- delete fileData.accessToken;
279
- delete fileData.refreshToken;
280
- delete fileData.expiresAt;
281
-
282
- fs.writeFileSync(
283
- CREDENTIALS_FILE,
284
- JSON.stringify(fileData, null, 2),
285
- 'utf-8'
286
- );
228
+ } catch {}
229
+
230
+ // Build oauth block merge with existing claudeAiOauth to preserve scopes etc.
231
+ const existing = fileData.claudeAiOauth || {};
232
+ const oauth: Record<string, any> = { ...existing };
233
+ oauth.accessToken = tokens.access_token;
234
+ if (tokens.refresh_token) oauth.refreshToken = tokens.refresh_token;
235
+ if (tokens.expires_in) {
236
+ oauth.expiresAt = Date.now() + (tokens.expires_in - 300) * 1000;
237
+ }
238
+
239
+ // Write in Claude Code's format
240
+ fileData.claudeAiOauth = oauth;
241
+ // Remove legacy flat keys if they exist
242
+ delete fileData.accessToken;
243
+ delete fileData.refreshToken;
244
+ delete fileData.expiresAt;
245
+
246
+ fs.writeFileSync(CREDENTIALS_FILE, JSON.stringify(fileData, null, 2), 'utf-8');
247
+ try { fs.chmodSync(CREDENTIALS_FILE, 0o600); } catch {}
248
+ log.ok('Claude credentials stored');
249
+
250
+ // macOS: also write to Keychain
251
+ if (process.platform === 'darwin') {
287
252
  try {
288
- fs.chmodSync(CREDENTIALS_FILE, 0o600);
253
+ const keychainValue = JSON.stringify({ claudeAiOauth: oauth });
254
+ try {
255
+ execFileSync('security', ['delete-generic-password', '-s', 'Claude Code-credentials'], {
256
+ stdio: ['pipe', 'pipe', 'pipe'],
257
+ });
258
+ } catch {} // OK if entry doesn't exist
259
+ execFileSync('security', [
260
+ 'add-generic-password',
261
+ '-s', 'Claude Code-credentials',
262
+ '-a', os.userInfo().username,
263
+ '-w', keychainValue,
264
+ ], { stdio: ['pipe', 'pipe', 'pipe'] });
289
265
  } catch {}
290
- log.ok('Claude credentials stored');
291
-
292
- // macOS: also write to Keychain
293
- if (process.platform === 'darwin') {
294
- try {
295
- const keychainValue = JSON.stringify({ claudeAiOauth: oauth });
296
- try {
297
- execFileSync(
298
- 'security',
299
- [
300
- 'delete-generic-password',
301
- '-s',
302
- 'Claude Code-credentials'
303
- ],
304
- {
305
- stdio: ['pipe', 'pipe', 'pipe']
306
- }
307
- );
308
- } catch {} // OK if entry doesn't exist
309
- execFileSync(
310
- 'security',
311
- [
312
- 'add-generic-password',
313
- '-s',
314
- 'Claude Code-credentials',
315
- '-a',
316
- os.userInfo().username,
317
- '-w',
318
- keychainValue
319
- ],
320
- { stdio: ['pipe', 'pipe', 'pipe'] }
321
- );
322
- } catch {}
323
- }
266
+ }
324
267
 
325
- // Legacy fallback (~/.claude.json)
268
+ // Legacy fallback (~/.claude.json)
269
+ try {
270
+ const legacyPath = path.join(os.homedir(), '.claude.json');
271
+ let legacyConfig: Record<string, any> = {};
326
272
  try {
327
- const legacyPath = path.join(os.homedir(), '.claude.json');
328
- let legacyConfig: Record<string, any> = {};
329
- try {
330
- if (fs.existsSync(legacyPath)) {
331
- legacyConfig = JSON.parse(fs.readFileSync(legacyPath, 'utf-8'));
332
- }
333
- } catch {}
334
- legacyConfig.oauthAccessToken = tokens.access_token;
335
- legacyConfig.hasCompletedOnboarding = true;
336
- fs.writeFileSync(
337
- legacyPath,
338
- JSON.stringify(legacyConfig, null, 2),
339
- 'utf-8'
340
- );
341
- try {
342
- fs.chmodSync(legacyPath, 0o600);
343
- } catch {}
273
+ if (fs.existsSync(legacyPath)) {
274
+ legacyConfig = JSON.parse(fs.readFileSync(legacyPath, 'utf-8'));
275
+ }
344
276
  } catch {}
277
+ legacyConfig.oauthAccessToken = tokens.access_token;
278
+ legacyConfig.hasCompletedOnboarding = true;
279
+ fs.writeFileSync(legacyPath, JSON.stringify(legacyConfig, null, 2), 'utf-8');
280
+ try { fs.chmodSync(legacyPath, 0o600); } catch {}
281
+ } catch {}
345
282
  }