fluxy-bot 0.2.32 → 0.2.34
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/package.json
CHANGED
|
@@ -7,7 +7,6 @@ import { motion, AnimatePresence } from 'framer-motion';
|
|
|
7
7
|
const PROVIDERS = [
|
|
8
8
|
{ id: 'anthropic', name: 'Claude', subtitle: 'by Anthropic', icon: '/icons/claude.png' },
|
|
9
9
|
{ id: 'openai', name: 'OpenAI Codex', subtitle: 'ChatGPT Plus / Pro', icon: '/icons/codex.png' },
|
|
10
|
-
{ id: 'ollama', name: 'Ollama', subtitle: 'Run locally', icon: null },
|
|
11
10
|
] as const;
|
|
12
11
|
|
|
13
12
|
const MODELS: Record<string, { id: string; label: string }[]> = {
|
|
@@ -24,12 +23,6 @@ const MODELS: Record<string, { id: string; label: string }[]> = {
|
|
|
24
23
|
{ id: 'gpt-5.3-codex:high', label: 'GPT-5.3 Codex High (Pro)' },
|
|
25
24
|
{ id: 'gpt-5.3-codex:xhigh', label: 'GPT-5.3 Codex Extra High (Pro)' },
|
|
26
25
|
],
|
|
27
|
-
ollama: [
|
|
28
|
-
{ id: 'llama3.2', label: 'Llama 3.2' },
|
|
29
|
-
{ id: 'mistral', label: 'Mistral' },
|
|
30
|
-
{ id: 'codellama', label: 'Code Llama' },
|
|
31
|
-
{ id: 'phi3', label: 'Phi-3' },
|
|
32
|
-
],
|
|
33
26
|
};
|
|
34
27
|
|
|
35
28
|
const TOTAL_STEPS = 6; // 0..5
|
|
@@ -106,7 +99,6 @@ export default function OnboardWizard({ onComplete }: Props) {
|
|
|
106
99
|
const [authState, setAuthState] = useState<Record<string, 'idle' | 'authenticating' | 'connected'>>({
|
|
107
100
|
anthropic: 'idle',
|
|
108
101
|
openai: 'idle',
|
|
109
|
-
ollama: 'connected',
|
|
110
102
|
});
|
|
111
103
|
|
|
112
104
|
// Anthropic/Claude-specific
|
|
@@ -120,9 +112,6 @@ export default function OnboardWizard({ onComplete }: Props) {
|
|
|
120
112
|
const [openaiWaiting, setOpenaiWaiting] = useState(false);
|
|
121
113
|
const [openaiError, setOpenaiError] = useState<string | undefined>();
|
|
122
114
|
|
|
123
|
-
// Ollama-specific
|
|
124
|
-
const [baseUrl, setBaseUrl] = useState('');
|
|
125
|
-
|
|
126
115
|
// Bot name + Handle (step 2)
|
|
127
116
|
const [botName, setBotName] = useState('');
|
|
128
117
|
const [handleStatus, setHandleStatus] = useState<null | 'checking' | 'ready' | 'invalid'>(null);
|
|
@@ -478,7 +467,6 @@ export default function OnboardWizard({ onComplete }: Props) {
|
|
|
478
467
|
provider,
|
|
479
468
|
model,
|
|
480
469
|
apiKey: '',
|
|
481
|
-
baseUrl: provider === 'ollama' ? baseUrl || undefined : undefined,
|
|
482
470
|
whisperEnabled,
|
|
483
471
|
whisperKey: whisperEnabled ? whisperKey : '',
|
|
484
472
|
portalUser: portalUser.trim(),
|
|
@@ -1124,27 +1112,6 @@ export default function OnboardWizard({ onComplete }: Props) {
|
|
|
1124
1112
|
</div>
|
|
1125
1113
|
)}
|
|
1126
1114
|
|
|
1127
|
-
{/* ── Auth flow: Ollama ── */}
|
|
1128
|
-
{provider === 'ollama' && (
|
|
1129
|
-
<div className="space-y-2.5">
|
|
1130
|
-
<div className="bg-emerald-500/8 border border-emerald-500/15 rounded-lg px-3.5 py-2.5">
|
|
1131
|
-
<p className="text-emerald-400/90 text-[12px]">No authentication needed — Ollama runs locally.</p>
|
|
1132
|
-
</div>
|
|
1133
|
-
<div>
|
|
1134
|
-
<label className="text-[12px] text-white/40 font-medium mb-1.5 block">
|
|
1135
|
-
Base URL <span className="text-white/20">(optional)</span>
|
|
1136
|
-
</label>
|
|
1137
|
-
<input
|
|
1138
|
-
type="text"
|
|
1139
|
-
value={baseUrl}
|
|
1140
|
-
onChange={(e) => setBaseUrl(e.target.value)}
|
|
1141
|
-
placeholder="http://localhost:11434"
|
|
1142
|
-
className={inputSmCls}
|
|
1143
|
-
/>
|
|
1144
|
-
</div>
|
|
1145
|
-
</div>
|
|
1146
|
-
)}
|
|
1147
|
-
|
|
1148
1115
|
{/* ── Model dropdown (after auth) ── */}
|
|
1149
1116
|
{isConnected && (
|
|
1150
1117
|
<>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import React, { useEffect, useRef, useState } from 'react';
|
|
2
2
|
import ReactDOM from 'react-dom/client';
|
|
3
|
-
import {
|
|
3
|
+
import { ArrowLeft, MoreVertical, Trash2, Wand2 } from 'lucide-react';
|
|
4
4
|
import { WsClient } from './src/lib/ws-client';
|
|
5
5
|
import { useFluxyChat } from './src/hooks/useFluxyChat';
|
|
6
6
|
import OnboardWizard from './OnboardWizard';
|
|
@@ -97,7 +97,7 @@ function FluxyApp() {
|
|
|
97
97
|
className="flex items-center justify-center h-7 w-7 -ml-1 rounded-full text-muted-foreground hover:text-foreground hover:bg-white/[0.06] transition-colors"
|
|
98
98
|
aria-label="Close chat"
|
|
99
99
|
>
|
|
100
|
-
<
|
|
100
|
+
<ArrowLeft className="h-5 w-5" />
|
|
101
101
|
</button>
|
|
102
102
|
<img src="/fluxy.png" alt={botName} className="h-5 w-auto" />
|
|
103
103
|
<span className="text-sm font-semibold">{botName}</span>
|
package/worker/claude-auth.ts
CHANGED
|
@@ -93,106 +93,141 @@ export async function exchangeClaudeCode(codeInput: string): Promise<{ success:
|
|
|
93
93
|
}
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
-
export function getClaudeAuthStatus(): { authenticated: boolean; error?: string } {
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
}
|
|
108
|
-
}
|
|
96
|
+
export async function getClaudeAuthStatus(): Promise<{ authenticated: boolean; error?: string }> {
|
|
97
|
+
const oauth = readOAuthBlock();
|
|
98
|
+
if (!oauth) return { authenticated: false };
|
|
99
|
+
|
|
100
|
+
if (oauth.accessToken && oauth.expiresAt && Date.now() < oauth.expiresAt) {
|
|
101
|
+
return { authenticated: true };
|
|
102
|
+
}
|
|
103
|
+
|
|
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
|
+
}
|
|
109
|
+
|
|
110
|
+
return { authenticated: false, error: 'Token expired' };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function readClaudeAccessToken(): string | null {
|
|
114
|
+
const oauth = readOAuthBlock();
|
|
115
|
+
if (!oauth?.accessToken) return null;
|
|
116
|
+
if (oauth.expiresAt && Date.now() >= oauth.expiresAt) return null;
|
|
117
|
+
return oauth.accessToken;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/* ── Helpers ── */
|
|
109
121
|
|
|
110
|
-
|
|
122
|
+
/**
|
|
123
|
+
* Read the OAuth block from the most reliable source.
|
|
124
|
+
* On macOS: Keychain is the source of truth (Claude Code reads/writes there on refresh).
|
|
125
|
+
* If Keychain has no valid entry, stale files are not trusted.
|
|
126
|
+
* On Linux/Windows: credentials file is the source of truth.
|
|
127
|
+
* Handles both formats:
|
|
128
|
+
* - Claude Code format: { claudeAiOauth: { accessToken, refreshToken, expiresAt, ... } }
|
|
129
|
+
* - Legacy flat format: { accessToken, refreshToken, expiresAt }
|
|
130
|
+
*/
|
|
131
|
+
function readOAuthBlock(): { accessToken?: string; refreshToken?: string; expiresAt?: number } | null {
|
|
132
|
+
// macOS: Keychain is the source of truth
|
|
111
133
|
if (process.platform === 'darwin') {
|
|
112
134
|
try {
|
|
113
135
|
const result = execFileSync('security', [
|
|
114
136
|
'find-generic-password', '-s', 'Claude Code-credentials', '-w',
|
|
115
137
|
], { stdio: ['pipe', 'pipe', 'pipe'] }).toString().trim();
|
|
116
138
|
const parsed = JSON.parse(result);
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
return { authenticated: true };
|
|
139
|
+
const oauth = parsed.claudeAiOauth || parsed;
|
|
140
|
+
if (oauth.accessToken) {
|
|
141
|
+
log.ok('Read credentials from macOS Keychain');
|
|
142
|
+
return oauth;
|
|
122
143
|
}
|
|
123
144
|
} catch {}
|
|
145
|
+
// On macOS, if Keychain has no valid entry, don't trust stale files
|
|
146
|
+
log.warn('No valid credentials in macOS Keychain');
|
|
147
|
+
return null;
|
|
124
148
|
}
|
|
125
149
|
|
|
126
|
-
//
|
|
150
|
+
// Linux/Windows: credentials file
|
|
127
151
|
try {
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
const
|
|
131
|
-
if (
|
|
152
|
+
if (fs.existsSync(CREDENTIALS_FILE)) {
|
|
153
|
+
const creds = JSON.parse(fs.readFileSync(CREDENTIALS_FILE, 'utf-8'));
|
|
154
|
+
const oauth = creds.claudeAiOauth || creds;
|
|
155
|
+
if (oauth.accessToken) return oauth;
|
|
132
156
|
}
|
|
133
157
|
} catch {}
|
|
134
158
|
|
|
135
|
-
return
|
|
159
|
+
return null;
|
|
136
160
|
}
|
|
137
161
|
|
|
138
|
-
|
|
139
|
-
|
|
162
|
+
/**
|
|
163
|
+
* Refresh an expired token using the refresh_token grant.
|
|
164
|
+
* Returns true if refresh succeeded and new credentials were stored.
|
|
165
|
+
*/
|
|
166
|
+
async function refreshClaudeToken(refreshToken: string): Promise<boolean> {
|
|
140
167
|
try {
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
168
|
+
log.ok('Attempting Claude token refresh...');
|
|
169
|
+
const response = await fetch(OAUTH_CONFIG.TOKEN_URL, {
|
|
170
|
+
method: 'POST',
|
|
171
|
+
headers: { 'Content-Type': 'application/json' },
|
|
172
|
+
body: JSON.stringify({
|
|
173
|
+
grant_type: 'refresh_token',
|
|
174
|
+
client_id: OAUTH_CONFIG.CLIENT_ID,
|
|
175
|
+
refresh_token: refreshToken,
|
|
176
|
+
}),
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
if (!response.ok) {
|
|
180
|
+
log.warn(`Claude token refresh failed: ${response.status}`);
|
|
181
|
+
return false;
|
|
147
182
|
}
|
|
148
|
-
} catch {}
|
|
149
183
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
if (parsed.claudeAiOauth?.accessToken) {
|
|
158
|
-
if (parsed.claudeAiOauth.expiresAt && Date.now() >= parsed.claudeAiOauth.expiresAt) return null;
|
|
159
|
-
return parsed.claudeAiOauth.accessToken;
|
|
160
|
-
}
|
|
161
|
-
} catch {}
|
|
184
|
+
const tokens = await response.json();
|
|
185
|
+
storeCredentials(tokens);
|
|
186
|
+
log.ok('Claude token refreshed successfully');
|
|
187
|
+
return true;
|
|
188
|
+
} catch (err: any) {
|
|
189
|
+
log.warn(`Claude token refresh error: ${err.message}`);
|
|
190
|
+
return false;
|
|
162
191
|
}
|
|
163
|
-
|
|
164
|
-
return null;
|
|
165
192
|
}
|
|
166
193
|
|
|
167
|
-
/* ── Helpers ── */
|
|
168
|
-
|
|
169
194
|
function storeCredentials(tokens: any): void {
|
|
170
195
|
if (!fs.existsSync(CLAUDE_DIR)) {
|
|
171
196
|
fs.mkdirSync(CLAUDE_DIR, { recursive: true });
|
|
172
197
|
}
|
|
173
198
|
|
|
174
|
-
// Read existing
|
|
175
|
-
let
|
|
199
|
+
// Read existing file to preserve other fields
|
|
200
|
+
let fileData: Record<string, any> = {};
|
|
176
201
|
try {
|
|
177
202
|
if (fs.existsSync(CREDENTIALS_FILE)) {
|
|
178
|
-
|
|
203
|
+
fileData = JSON.parse(fs.readFileSync(CREDENTIALS_FILE, 'utf-8'));
|
|
179
204
|
}
|
|
180
205
|
} catch {}
|
|
181
206
|
|
|
182
|
-
|
|
183
|
-
|
|
207
|
+
// Build oauth block — merge with existing claudeAiOauth to preserve scopes etc.
|
|
208
|
+
const existing = fileData.claudeAiOauth || {};
|
|
209
|
+
const oauth: Record<string, any> = { ...existing };
|
|
210
|
+
oauth.accessToken = tokens.access_token;
|
|
211
|
+
if (tokens.refresh_token) oauth.refreshToken = tokens.refresh_token;
|
|
184
212
|
if (tokens.expires_in) {
|
|
185
|
-
|
|
213
|
+
oauth.expiresAt = Date.now() + (tokens.expires_in - 300) * 1000;
|
|
186
214
|
}
|
|
187
215
|
|
|
188
|
-
|
|
216
|
+
// Write in Claude Code's format
|
|
217
|
+
fileData.claudeAiOauth = oauth;
|
|
218
|
+
// Remove legacy flat keys if they exist
|
|
219
|
+
delete fileData.accessToken;
|
|
220
|
+
delete fileData.refreshToken;
|
|
221
|
+
delete fileData.expiresAt;
|
|
222
|
+
|
|
223
|
+
fs.writeFileSync(CREDENTIALS_FILE, JSON.stringify(fileData, null, 2), 'utf-8');
|
|
189
224
|
try { fs.chmodSync(CREDENTIALS_FILE, 0o600); } catch {}
|
|
190
225
|
log.ok('Claude credentials stored');
|
|
191
226
|
|
|
192
227
|
// macOS: also write to Keychain
|
|
193
228
|
if (process.platform === 'darwin') {
|
|
194
229
|
try {
|
|
195
|
-
const keychainValue = JSON.stringify({ claudeAiOauth:
|
|
230
|
+
const keychainValue = JSON.stringify({ claudeAiOauth: oauth });
|
|
196
231
|
try {
|
|
197
232
|
execFileSync('security', ['delete-generic-password', '-s', 'Claude Code-credentials'], {
|
|
198
233
|
stdio: ['pipe', 'pipe', 'pipe'],
|
package/worker/index.ts
CHANGED
|
@@ -109,8 +109,8 @@ app.post('/api/auth/claude/exchange', async (req, res) => {
|
|
|
109
109
|
res.json(result);
|
|
110
110
|
});
|
|
111
111
|
|
|
112
|
-
app.get('/api/auth/claude/status', (_req, res) => {
|
|
113
|
-
res.json(getClaudeAuthStatus());
|
|
112
|
+
app.get('/api/auth/claude/status', async (_req, res) => {
|
|
113
|
+
res.json(await getClaudeAuthStatus());
|
|
114
114
|
});
|
|
115
115
|
|
|
116
116
|
// ── Handle registration ──
|