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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fluxy-bot",
3
- "version": "0.2.32",
3
+ "version": "0.2.34",
4
4
  "description": "Self-hosted AI bot — run your own AI assistant from anywhere",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -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 { ChevronRight, MoreVertical, Trash2, Wand2 } from 'lucide-react';
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
- <ChevronRight className="h-5 w-5" />
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>
@@ -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
- // Check credentials file
98
- try {
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 };
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
- // macOS: check Keychain as fallback
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
- 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 };
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
- // Legacy check
150
+ // Linux/Windows: credentials file
127
151
  try {
128
- const legacyPath = path.join(os.homedir(), '.claude.json');
129
- if (fs.existsSync(legacyPath)) {
130
- const config = JSON.parse(fs.readFileSync(legacyPath, 'utf-8'));
131
- if (config.oauthAccessToken) return { authenticated: true };
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 { authenticated: false };
159
+ return null;
136
160
  }
137
161
 
138
- export function readClaudeAccessToken(): string | null {
139
- // Primary: credentials file
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
- if (fs.existsSync(CREDENTIALS_FILE)) {
142
- const creds = JSON.parse(fs.readFileSync(CREDENTIALS_FILE, 'utf-8'));
143
- if (creds.accessToken) {
144
- if (creds.expiresAt && Date.now() >= creds.expiresAt) return null;
145
- return creds.accessToken;
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
- // macOS Keychain fallback
151
- if (process.platform === 'darwin') {
152
- try {
153
- const result = execFileSync('security', [
154
- 'find-generic-password', '-s', 'Claude Code-credentials', '-w',
155
- ], { stdio: ['pipe', 'pipe', 'pipe'] }).toString().trim();
156
- 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
- }
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 credentials
175
- let credentials: Record<string, any> = {};
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
- credentials = JSON.parse(fs.readFileSync(CREDENTIALS_FILE, 'utf-8'));
203
+ fileData = JSON.parse(fs.readFileSync(CREDENTIALS_FILE, 'utf-8'));
179
204
  }
180
205
  } catch {}
181
206
 
182
- credentials.accessToken = tokens.access_token;
183
- if (tokens.refresh_token) credentials.refreshToken = tokens.refresh_token;
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
- credentials.expiresAt = Date.now() + (tokens.expires_in - 300) * 1000;
213
+ oauth.expiresAt = Date.now() + (tokens.expires_in - 300) * 1000;
186
214
  }
187
215
 
188
- fs.writeFileSync(CREDENTIALS_FILE, JSON.stringify(credentials, null, 2), 'utf-8');
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: credentials });
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 ──