fluxy-bot 0.2.31 → 0.2.33
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/supervisor/widget.js
CHANGED
|
@@ -6,10 +6,10 @@
|
|
|
6
6
|
// ── Styles ──
|
|
7
7
|
var style = document.createElement('style');
|
|
8
8
|
style.textContent = [
|
|
9
|
-
'#fluxy-widget-bubble{position:fixed;bottom:24px;right:24px;z-index:99998;cursor:pointer;width:
|
|
9
|
+
'#fluxy-widget-bubble{position:fixed;bottom:24px;right:24px;z-index:99998;cursor:pointer;width:60px;height:60px;border-radius:50%;display:flex;align-items:center;justify-content:center;transition:transform .15s ease;-webkit-tap-highlight-color:transparent}',
|
|
10
10
|
'#fluxy-widget-bubble:hover{transform:scale(1.1)}',
|
|
11
11
|
'#fluxy-widget-bubble:active{transform:scale(0.95)}',
|
|
12
|
-
'#fluxy-widget-bubble video,#fluxy-widget-bubble img{height:
|
|
12
|
+
'#fluxy-widget-bubble video,#fluxy-widget-bubble img{height:60px;width:auto;pointer-events:none;-webkit-user-drag:none}',
|
|
13
13
|
'#fluxy-widget-backdrop{position:fixed;inset:0;z-index:99998;background:rgba(0,0,0,0.4);opacity:0;transition:opacity .2s ease;pointer-events:none}',
|
|
14
14
|
'#fluxy-widget-backdrop.open{opacity:1;pointer-events:auto}',
|
|
15
15
|
'#fluxy-widget-panel{position:fixed;top:0;right:0;bottom:0;z-index:99999;width:' + PANEL_WIDTH + ';max-width:100vw;transform:translateX(100%);transition:transform .25s cubic-bezier(.4,0,.2,1);box-shadow:-4px 0 24px rgba(0,0,0,0.3);border-left:1px solid #3a3a3a;overflow:hidden}',
|
package/worker/claude-auth.ts
CHANGED
|
@@ -93,57 +93,45 @@ 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
|
-
if (fs.existsSync(CREDENTIALS_FILE)) {
|
|
100
|
-
const creds = JSON.parse(fs.readFileSync(CREDENTIALS_FILE, 'utf-8'));
|
|
101
|
-
if (creds.accessToken) {
|
|
102
|
-
if (creds.expiresAt && Date.now() >= creds.expiresAt) {
|
|
103
|
-
return { authenticated: false, error: 'Token expired' };
|
|
104
|
-
}
|
|
105
|
-
return { authenticated: true };
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
} catch {}
|
|
96
|
+
export async function getClaudeAuthStatus(): Promise<{ authenticated: boolean; error?: string }> {
|
|
97
|
+
const oauth = readOAuthBlock();
|
|
98
|
+
if (!oauth) return { authenticated: false };
|
|
109
99
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
try {
|
|
113
|
-
const result = execFileSync('security', [
|
|
114
|
-
'find-generic-password', '-s', 'Claude Code-credentials', '-w',
|
|
115
|
-
], { stdio: ['pipe', 'pipe', 'pipe'] }).toString().trim();
|
|
116
|
-
const parsed = JSON.parse(result);
|
|
117
|
-
if (parsed.claudeAiOauth?.accessToken) {
|
|
118
|
-
if (parsed.claudeAiOauth.expiresAt && Date.now() >= parsed.claudeAiOauth.expiresAt) {
|
|
119
|
-
return { authenticated: false, error: 'Token expired' };
|
|
120
|
-
}
|
|
121
|
-
return { authenticated: true };
|
|
122
|
-
}
|
|
123
|
-
} catch {}
|
|
100
|
+
if (oauth.accessToken && oauth.expiresAt && Date.now() < oauth.expiresAt) {
|
|
101
|
+
return { authenticated: true };
|
|
124
102
|
}
|
|
125
103
|
|
|
126
|
-
//
|
|
127
|
-
|
|
128
|
-
const
|
|
129
|
-
if (
|
|
130
|
-
|
|
131
|
-
if (config.oauthAccessToken) return { authenticated: true };
|
|
132
|
-
}
|
|
133
|
-
} catch {}
|
|
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
|
+
}
|
|
134
109
|
|
|
135
|
-
return { authenticated: false };
|
|
110
|
+
return { authenticated: false, error: 'Token expired' };
|
|
136
111
|
}
|
|
137
112
|
|
|
138
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 ── */
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Read the OAuth block from credentials file or macOS Keychain.
|
|
124
|
+
* Handles both formats:
|
|
125
|
+
* - Claude Code format: { claudeAiOauth: { accessToken, refreshToken, expiresAt, ... } }
|
|
126
|
+
* - Legacy flat format: { accessToken, refreshToken, expiresAt }
|
|
127
|
+
*/
|
|
128
|
+
function readOAuthBlock(): { accessToken?: string; refreshToken?: string; expiresAt?: number } | null {
|
|
139
129
|
// Primary: credentials file
|
|
140
130
|
try {
|
|
141
131
|
if (fs.existsSync(CREDENTIALS_FILE)) {
|
|
142
132
|
const creds = JSON.parse(fs.readFileSync(CREDENTIALS_FILE, 'utf-8'));
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
return creds.accessToken;
|
|
146
|
-
}
|
|
133
|
+
const oauth = creds.claudeAiOauth || creds;
|
|
134
|
+
if (oauth.accessToken) return oauth;
|
|
147
135
|
}
|
|
148
136
|
} catch {}
|
|
149
137
|
|
|
@@ -154,45 +142,82 @@ export function readClaudeAccessToken(): string | null {
|
|
|
154
142
|
'find-generic-password', '-s', 'Claude Code-credentials', '-w',
|
|
155
143
|
], { stdio: ['pipe', 'pipe', 'pipe'] }).toString().trim();
|
|
156
144
|
const parsed = JSON.parse(result);
|
|
157
|
-
if (parsed.claudeAiOauth?.accessToken)
|
|
158
|
-
if (parsed.claudeAiOauth.expiresAt && Date.now() >= parsed.claudeAiOauth.expiresAt) return null;
|
|
159
|
-
return parsed.claudeAiOauth.accessToken;
|
|
160
|
-
}
|
|
145
|
+
if (parsed.claudeAiOauth?.accessToken) return parsed.claudeAiOauth;
|
|
161
146
|
} catch {}
|
|
162
147
|
}
|
|
163
148
|
|
|
164
149
|
return null;
|
|
165
150
|
}
|
|
166
151
|
|
|
167
|
-
|
|
152
|
+
/**
|
|
153
|
+
* Refresh an expired token using the refresh_token grant.
|
|
154
|
+
* Returns true if refresh succeeded and new credentials were stored.
|
|
155
|
+
*/
|
|
156
|
+
async function refreshClaudeToken(refreshToken: string): Promise<boolean> {
|
|
157
|
+
try {
|
|
158
|
+
log.ok('Attempting Claude token refresh...');
|
|
159
|
+
const response = await fetch(OAUTH_CONFIG.TOKEN_URL, {
|
|
160
|
+
method: 'POST',
|
|
161
|
+
headers: { 'Content-Type': 'application/json' },
|
|
162
|
+
body: JSON.stringify({
|
|
163
|
+
grant_type: 'refresh_token',
|
|
164
|
+
client_id: OAUTH_CONFIG.CLIENT_ID,
|
|
165
|
+
refresh_token: refreshToken,
|
|
166
|
+
}),
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
if (!response.ok) {
|
|
170
|
+
log.warn(`Claude token refresh failed: ${response.status}`);
|
|
171
|
+
return false;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const tokens = await response.json();
|
|
175
|
+
storeCredentials(tokens);
|
|
176
|
+
log.ok('Claude token refreshed successfully');
|
|
177
|
+
return true;
|
|
178
|
+
} catch (err: any) {
|
|
179
|
+
log.warn(`Claude token refresh error: ${err.message}`);
|
|
180
|
+
return false;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
168
183
|
|
|
169
184
|
function storeCredentials(tokens: any): void {
|
|
170
185
|
if (!fs.existsSync(CLAUDE_DIR)) {
|
|
171
186
|
fs.mkdirSync(CLAUDE_DIR, { recursive: true });
|
|
172
187
|
}
|
|
173
188
|
|
|
174
|
-
// Read existing
|
|
175
|
-
let
|
|
189
|
+
// Read existing file to preserve other fields
|
|
190
|
+
let fileData: Record<string, any> = {};
|
|
176
191
|
try {
|
|
177
192
|
if (fs.existsSync(CREDENTIALS_FILE)) {
|
|
178
|
-
|
|
193
|
+
fileData = JSON.parse(fs.readFileSync(CREDENTIALS_FILE, 'utf-8'));
|
|
179
194
|
}
|
|
180
195
|
} catch {}
|
|
181
196
|
|
|
182
|
-
|
|
183
|
-
|
|
197
|
+
// Build oauth block — merge with existing claudeAiOauth to preserve scopes etc.
|
|
198
|
+
const existing = fileData.claudeAiOauth || {};
|
|
199
|
+
const oauth: Record<string, any> = { ...existing };
|
|
200
|
+
oauth.accessToken = tokens.access_token;
|
|
201
|
+
if (tokens.refresh_token) oauth.refreshToken = tokens.refresh_token;
|
|
184
202
|
if (tokens.expires_in) {
|
|
185
|
-
|
|
203
|
+
oauth.expiresAt = Date.now() + (tokens.expires_in - 300) * 1000;
|
|
186
204
|
}
|
|
187
205
|
|
|
188
|
-
|
|
206
|
+
// Write in Claude Code's format
|
|
207
|
+
fileData.claudeAiOauth = oauth;
|
|
208
|
+
// Remove legacy flat keys if they exist
|
|
209
|
+
delete fileData.accessToken;
|
|
210
|
+
delete fileData.refreshToken;
|
|
211
|
+
delete fileData.expiresAt;
|
|
212
|
+
|
|
213
|
+
fs.writeFileSync(CREDENTIALS_FILE, JSON.stringify(fileData, null, 2), 'utf-8');
|
|
189
214
|
try { fs.chmodSync(CREDENTIALS_FILE, 0o600); } catch {}
|
|
190
215
|
log.ok('Claude credentials stored');
|
|
191
216
|
|
|
192
217
|
// macOS: also write to Keychain
|
|
193
218
|
if (process.platform === 'darwin') {
|
|
194
219
|
try {
|
|
195
|
-
const keychainValue = JSON.stringify({ claudeAiOauth:
|
|
220
|
+
const keychainValue = JSON.stringify({ claudeAiOauth: oauth });
|
|
196
221
|
try {
|
|
197
222
|
execFileSync('security', ['delete-generic-password', '-s', 'Claude Code-credentials'], {
|
|
198
223
|
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 ──
|