fluxy-bot 0.2.32 → 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fluxy-bot",
3
- "version": "0.2.32",
3
+ "version": "0.2.33",
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,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
- // 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 };
109
99
 
110
- // macOS: check Keychain as fallback
111
- if (process.platform === 'darwin') {
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
- // Legacy check
127
- 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 };
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
- if (creds.accessToken) {
144
- if (creds.expiresAt && Date.now() >= creds.expiresAt) return null;
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
- /* ── Helpers ── */
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 credentials
175
- let credentials: Record<string, any> = {};
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
- credentials = JSON.parse(fs.readFileSync(CREDENTIALS_FILE, 'utf-8'));
193
+ fileData = JSON.parse(fs.readFileSync(CREDENTIALS_FILE, 'utf-8'));
179
194
  }
180
195
  } catch {}
181
196
 
182
- credentials.accessToken = tokens.access_token;
183
- if (tokens.refresh_token) credentials.refreshToken = tokens.refresh_token;
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
- credentials.expiresAt = Date.now() + (tokens.expires_in - 300) * 1000;
203
+ oauth.expiresAt = Date.now() + (tokens.expires_in - 300) * 1000;
186
204
  }
187
205
 
188
- fs.writeFileSync(CREDENTIALS_FILE, JSON.stringify(credentials, null, 2), 'utf-8');
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: credentials });
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 ──