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.
- package/README.md +53 -84
- package/bin/cli.js +1717 -6
- package/components.json +18 -18
- package/package.json +98 -109
- package/postcss.config.js +3 -3
- package/scripts/install.ps1 +1 -1
- package/scripts/install.sh +1 -1
- package/scripts/postinstall.js +47 -58
- package/shared/ai.ts +118 -202
- package/shared/config.ts +37 -39
- package/shared/logger.ts +6 -14
- package/shared/paths.ts +10 -16
- package/shared/relay.ts +89 -98
- package/supervisor/backend.ts +77 -89
- package/supervisor/chat/ARCHITECTURE.md +13 -14
- package/supervisor/chat/OnboardWizard.tsx +2194 -3419
- package/supervisor/chat/fluxy-main.tsx +495 -633
- package/supervisor/chat/fluxy.html +24 -36
- package/supervisor/chat/onboard-main.tsx +8 -8
- package/supervisor/chat/onboard.html +10 -13
- package/supervisor/chat/src/components/Chat/AudioBubble.tsx +94 -108
- package/supervisor/chat/src/components/Chat/ChatView.tsx +14 -36
- package/supervisor/chat/src/components/Chat/ImageLightbox.tsx +71 -89
- package/supervisor/chat/src/components/Chat/InputBar.tsx +498 -728
- package/supervisor/chat/src/components/Chat/MessageBubble.tsx +154 -246
- package/supervisor/chat/src/components/Chat/MessageList.tsx +114 -138
- package/supervisor/chat/src/components/Chat/TypingIndicator.tsx +38 -50
- package/supervisor/chat/src/components/LoginScreen.tsx +245 -296
- package/supervisor/chat/src/hooks/useChat.ts +210 -259
- package/supervisor/chat/src/hooks/useFluxyChat.ts +232 -322
- package/supervisor/chat/src/hooks/useSpeechRecognition.ts +126 -148
- package/supervisor/chat/src/lib/auth.ts +20 -23
- package/supervisor/chat/src/lib/ws-client.ts +113 -125
- package/supervisor/chat/src/styles/globals.css +76 -85
- package/supervisor/file-saver.ts +39 -52
- package/supervisor/fluxy-agent.ts +213 -292
- package/supervisor/index.ts +895 -1263
- package/supervisor/scheduler.ts +237 -299
- package/supervisor/tunnel.ts +113 -167
- package/supervisor/vite-dev.ts +51 -61
- package/supervisor/widget.js +135 -147
- package/supervisor/worker.ts +45 -49
- package/tsconfig.json +18 -24
- package/vite.config.ts +48 -55
- package/vite.fluxy.config.ts +34 -34
- package/worker/claude-auth.ts +202 -265
- package/worker/codex-auth.ts +144 -181
- package/worker/db.ts +63 -191
- package/worker/index.ts +601 -842
- package/workspace/MYSELF.md +1 -1
- package/workspace/PULSE.json +6 -6
- package/workspace/backend/index.ts +16 -19
- package/workspace/client/index.html +47 -60
- package/workspace/client/public/manifest.json +21 -21
- package/workspace/client/public/sw.js +29 -31
- package/workspace/client/src/App.tsx +91 -136
- package/workspace/client/src/components/Dashboard/DashboardPage.tsx +68 -72
- package/workspace/client/src/components/ErrorBoundary.tsx +11 -11
- package/workspace/client/src/components/Layout/DashboardLayout.tsx +35 -35
- package/workspace/client/src/components/Layout/Footer.tsx +12 -12
- package/workspace/client/src/components/Layout/MobileNav.tsx +23 -23
- package/workspace/client/src/components/Layout/Sidebar.tsx +81 -81
- package/workspace/client/src/components/ui/avatar.tsx +85 -85
- package/workspace/client/src/components/ui/badge.tsx +40 -41
- package/workspace/client/src/components/ui/button.tsx +56 -57
- package/workspace/client/src/components/ui/card.tsx +75 -75
- package/workspace/client/src/components/ui/dialog.tsx +114 -117
- package/workspace/client/src/components/ui/dropdown-menu.tsx +191 -199
- package/workspace/client/src/components/ui/input.tsx +17 -17
- package/workspace/client/src/components/ui/scroll-area.tsx +47 -47
- package/workspace/client/src/components/ui/select.tsx +145 -151
- package/workspace/client/src/components/ui/separator.tsx +21 -21
- package/workspace/client/src/components/ui/sheet.tsx +102 -102
- package/workspace/client/src/components/ui/skeleton.tsx +10 -10
- package/workspace/client/src/components/ui/switch.tsx +26 -26
- package/workspace/client/src/components/ui/tabs.tsx +69 -69
- package/workspace/client/src/components/ui/textarea.tsx +14 -14
- package/workspace/client/src/components/ui/tooltip.tsx +37 -37
- package/workspace/client/src/lib/utils.ts +1 -1
- package/workspace/client/src/main.tsx +3 -3
- package/workspace/client/src/styles/globals.css +76 -85
- package/workspace/skills/code-reviewer/.claude-plugin/plugin.json +3 -3
- package/workspace/skills/code-reviewer/skills/code-reviewer/SKILL.md +0 -6
- package/workspace/skills/daily-standup/.claude-plugin/plugin.json +3 -3
- package/workspace/skills/daily-standup/skills/daily-standup/SKILL.md +0 -7
- package/workspace/skills/workspace-helper/.claude-plugin/plugin.json +3 -3
- package/workspace/skills/workspace-helper/skills/workspace-helper/SKILL.md +0 -2
- package/bin/cli.backup.js +0 -2138
- package/cli/commands/daemon.ts +0 -42
- package/cli/commands/init.ts +0 -32
- package/cli/commands/start.ts +0 -113
- package/cli/commands/tunnel.temp.ts +0 -206
- package/cli/commands/tunnel.ts +0 -227
- package/cli/commands/update.ts +0 -163
- package/cli/core/base-adapter.ts +0 -156
- package/cli/core/cloudflared.ts +0 -113
- package/cli/core/config.ts +0 -73
- package/cli/core/os-detector.ts +0 -43
- package/cli/core/server.ts +0 -109
- package/cli/core/types.ts +0 -15
- package/cli/index.ts +0 -72
- package/cli/platforms/darwin.ts +0 -146
- package/cli/platforms/index.ts +0 -21
- package/cli/platforms/linux.ts +0 -156
- package/cli/platforms/win32.ts +0 -34
- package/cli/utils/ui.ts +0 -37
package/worker/claude-auth.ts
CHANGED
|
@@ -14,11 +14,11 @@ import os from 'os';
|
|
|
14
14
|
import { log } from '../shared/logger.js';
|
|
15
15
|
|
|
16
16
|
const OAUTH_CONFIG = {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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 (!
|
|
75
|
-
|
|
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
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
-
|
|
116
|
-
|
|
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
|
-
|
|
122
|
-
|
|
123
|
-
|
|
100
|
+
if (oauth.accessToken && oauth.expiresAt && Date.now() < oauth.expiresAt) {
|
|
101
|
+
return { authenticated: true };
|
|
102
|
+
}
|
|
124
103
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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
|
-
|
|
110
|
+
return { authenticated: false, error: 'Token expired' };
|
|
132
111
|
}
|
|
133
112
|
|
|
134
113
|
export function readClaudeAccessToken(): string | null {
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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
|
-
|
|
144
|
-
|
|
122
|
+
const oauth = readOAuthBlock();
|
|
123
|
+
if (!oauth?.accessToken) return null;
|
|
145
124
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
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
|
-
|
|
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
|
-
|
|
177
|
-
|
|
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
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
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
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
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
|
-
|
|
239
|
-
|
|
240
|
-
|
|
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
|
-
|
|
255
|
-
|
|
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
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
328
|
-
|
|
329
|
-
|
|
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
|
}
|