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.
Files changed (106) hide show
  1. package/README.md +53 -84
  2. package/bin/cli.js +1717 -6
  3. package/components.json +18 -18
  4. package/package.json +98 -109
  5. package/postcss.config.js +3 -3
  6. package/scripts/install.ps1 +1 -1
  7. package/scripts/install.sh +1 -1
  8. package/scripts/postinstall.js +47 -58
  9. package/shared/ai.ts +118 -202
  10. package/shared/config.ts +37 -39
  11. package/shared/logger.ts +6 -14
  12. package/shared/paths.ts +10 -16
  13. package/shared/relay.ts +89 -98
  14. package/supervisor/backend.ts +77 -89
  15. package/supervisor/chat/ARCHITECTURE.md +13 -14
  16. package/supervisor/chat/OnboardWizard.tsx +2194 -3419
  17. package/supervisor/chat/fluxy-main.tsx +495 -633
  18. package/supervisor/chat/fluxy.html +24 -36
  19. package/supervisor/chat/onboard-main.tsx +8 -8
  20. package/supervisor/chat/onboard.html +10 -13
  21. package/supervisor/chat/src/components/Chat/AudioBubble.tsx +94 -108
  22. package/supervisor/chat/src/components/Chat/ChatView.tsx +14 -36
  23. package/supervisor/chat/src/components/Chat/ImageLightbox.tsx +71 -89
  24. package/supervisor/chat/src/components/Chat/InputBar.tsx +498 -728
  25. package/supervisor/chat/src/components/Chat/MessageBubble.tsx +154 -246
  26. package/supervisor/chat/src/components/Chat/MessageList.tsx +114 -138
  27. package/supervisor/chat/src/components/Chat/TypingIndicator.tsx +38 -50
  28. package/supervisor/chat/src/components/LoginScreen.tsx +245 -296
  29. package/supervisor/chat/src/hooks/useChat.ts +210 -259
  30. package/supervisor/chat/src/hooks/useFluxyChat.ts +232 -322
  31. package/supervisor/chat/src/hooks/useSpeechRecognition.ts +126 -148
  32. package/supervisor/chat/src/lib/auth.ts +20 -23
  33. package/supervisor/chat/src/lib/ws-client.ts +113 -125
  34. package/supervisor/chat/src/styles/globals.css +76 -85
  35. package/supervisor/file-saver.ts +39 -52
  36. package/supervisor/fluxy-agent.ts +213 -292
  37. package/supervisor/index.ts +895 -1263
  38. package/supervisor/scheduler.ts +237 -299
  39. package/supervisor/tunnel.ts +113 -167
  40. package/supervisor/vite-dev.ts +51 -61
  41. package/supervisor/widget.js +135 -147
  42. package/supervisor/worker.ts +45 -49
  43. package/tsconfig.json +18 -24
  44. package/vite.config.ts +48 -55
  45. package/vite.fluxy.config.ts +34 -34
  46. package/worker/claude-auth.ts +202 -265
  47. package/worker/codex-auth.ts +144 -181
  48. package/worker/db.ts +63 -191
  49. package/worker/index.ts +601 -842
  50. package/workspace/MYSELF.md +1 -1
  51. package/workspace/PULSE.json +6 -6
  52. package/workspace/backend/index.ts +16 -19
  53. package/workspace/client/index.html +47 -60
  54. package/workspace/client/public/manifest.json +21 -21
  55. package/workspace/client/public/sw.js +29 -31
  56. package/workspace/client/src/App.tsx +91 -136
  57. package/workspace/client/src/components/Dashboard/DashboardPage.tsx +68 -72
  58. package/workspace/client/src/components/ErrorBoundary.tsx +11 -11
  59. package/workspace/client/src/components/Layout/DashboardLayout.tsx +35 -35
  60. package/workspace/client/src/components/Layout/Footer.tsx +12 -12
  61. package/workspace/client/src/components/Layout/MobileNav.tsx +23 -23
  62. package/workspace/client/src/components/Layout/Sidebar.tsx +81 -81
  63. package/workspace/client/src/components/ui/avatar.tsx +85 -85
  64. package/workspace/client/src/components/ui/badge.tsx +40 -41
  65. package/workspace/client/src/components/ui/button.tsx +56 -57
  66. package/workspace/client/src/components/ui/card.tsx +75 -75
  67. package/workspace/client/src/components/ui/dialog.tsx +114 -117
  68. package/workspace/client/src/components/ui/dropdown-menu.tsx +191 -199
  69. package/workspace/client/src/components/ui/input.tsx +17 -17
  70. package/workspace/client/src/components/ui/scroll-area.tsx +47 -47
  71. package/workspace/client/src/components/ui/select.tsx +145 -151
  72. package/workspace/client/src/components/ui/separator.tsx +21 -21
  73. package/workspace/client/src/components/ui/sheet.tsx +102 -102
  74. package/workspace/client/src/components/ui/skeleton.tsx +10 -10
  75. package/workspace/client/src/components/ui/switch.tsx +26 -26
  76. package/workspace/client/src/components/ui/tabs.tsx +69 -69
  77. package/workspace/client/src/components/ui/textarea.tsx +14 -14
  78. package/workspace/client/src/components/ui/tooltip.tsx +37 -37
  79. package/workspace/client/src/lib/utils.ts +1 -1
  80. package/workspace/client/src/main.tsx +3 -3
  81. package/workspace/client/src/styles/globals.css +76 -85
  82. package/workspace/skills/code-reviewer/.claude-plugin/plugin.json +3 -3
  83. package/workspace/skills/code-reviewer/skills/code-reviewer/SKILL.md +0 -6
  84. package/workspace/skills/daily-standup/.claude-plugin/plugin.json +3 -3
  85. package/workspace/skills/daily-standup/skills/daily-standup/SKILL.md +0 -7
  86. package/workspace/skills/workspace-helper/.claude-plugin/plugin.json +3 -3
  87. package/workspace/skills/workspace-helper/skills/workspace-helper/SKILL.md +0 -2
  88. package/bin/cli.backup.js +0 -2138
  89. package/cli/commands/daemon.ts +0 -42
  90. package/cli/commands/init.ts +0 -32
  91. package/cli/commands/start.ts +0 -113
  92. package/cli/commands/tunnel.temp.ts +0 -206
  93. package/cli/commands/tunnel.ts +0 -227
  94. package/cli/commands/update.ts +0 -163
  95. package/cli/core/base-adapter.ts +0 -156
  96. package/cli/core/cloudflared.ts +0 -113
  97. package/cli/core/config.ts +0 -73
  98. package/cli/core/os-detector.ts +0 -43
  99. package/cli/core/server.ts +0 -109
  100. package/cli/core/types.ts +0 -15
  101. package/cli/index.ts +0 -72
  102. package/cli/platforms/darwin.ts +0 -146
  103. package/cli/platforms/index.ts +0 -21
  104. package/cli/platforms/linux.ts +0 -156
  105. package/cli/platforms/win32.ts +0 -34
  106. package/cli/utils/ui.ts +0 -37
@@ -2,285 +2,236 @@ import { useCallback, useEffect, useState, useRef } from 'react';
2
2
  import type { WsClient } from '../lib/ws-client';
3
3
 
4
4
  export interface StoredAttachment {
5
- type: string;
6
- name: string;
7
- mediaType: string;
8
- filePath: string;
5
+ type: string;
6
+ name: string;
7
+ mediaType: string;
8
+ filePath: string;
9
9
  }
10
10
 
11
11
  export interface ChatMessage {
12
- id: string;
13
- role: 'user' | 'assistant';
14
- content: string;
15
- timestamp: string; // ISO string
16
- hasAttachments?: boolean;
17
- audioData?: string; // data URL or HTTP URL for voice messages
18
- attachments?: StoredAttachment[];
12
+ id: string;
13
+ role: 'user' | 'assistant';
14
+ content: string;
15
+ timestamp: string; // ISO string
16
+ hasAttachments?: boolean;
17
+ audioData?: string; // data URL or HTTP URL for voice messages
18
+ attachments?: StoredAttachment[];
19
19
  }
20
20
 
21
21
  export interface ToolActivity {
22
- name: string;
23
- status: 'running' | 'done';
22
+ name: string;
23
+ status: 'running' | 'done';
24
24
  }
25
25
 
26
26
  export interface Attachment {
27
- id: string;
28
- type: 'image' | 'file';
29
- name: string;
30
- preview: string; // base64 data URL
27
+ id: string;
28
+ type: 'image' | 'file';
29
+ name: string;
30
+ preview: string; // base64 data URL
31
31
  }
32
32
 
33
33
  export function useChat(ws: WsClient | null) {
34
- const [messages, setMessages] = useState<ChatMessage[]>([]);
35
- const [conversationId, setConversationId] = useState<string | null>(null);
36
- const [streaming, setStreaming] = useState(false);
37
- const [streamBuffer, setStreamBuffer] = useState('');
38
- const [tools, setTools] = useState<ToolActivity[]>([]);
39
- const loaded = useRef(false);
40
-
41
- // Load current conversation from DB on mount
42
- useEffect(() => {
43
- if (loaded.current) return;
44
- loaded.current = true;
45
- fetch('/api/context/current')
46
- .then(r => r.json())
47
- .then(data => {
48
- if (data.conversationId) {
49
- setConversationId(data.conversationId);
34
+ const [messages, setMessages] = useState<ChatMessage[]>([]);
35
+ const [conversationId, setConversationId] = useState<string | null>(null);
36
+ const [streaming, setStreaming] = useState(false);
37
+ const [streamBuffer, setStreamBuffer] = useState('');
38
+ const [tools, setTools] = useState<ToolActivity[]>([]);
39
+ const loaded = useRef(false);
40
+
41
+ // Load current conversation from DB on mount
42
+ useEffect(() => {
43
+ if (loaded.current) return;
44
+ loaded.current = true;
45
+ fetch('/api/context/current')
46
+ .then((r) => r.json())
47
+ .then((data) => {
48
+ if (data.conversationId) {
49
+ setConversationId(data.conversationId);
50
+ }
51
+ })
52
+ .catch(() => {});
53
+ }, []);
54
+
55
+ // Load messages when conversationId is set
56
+ useEffect(() => {
57
+ if (!conversationId) return;
58
+ fetch(`/api/conversations/${conversationId}`)
59
+ .then((r) => {
60
+ if (!r.ok) throw new Error('not found');
61
+ return r.json();
62
+ })
63
+ .then((data) => {
64
+ if (data.messages?.length) {
65
+ setMessages(
66
+ data.messages
67
+ .filter((m: any) => m.role === 'user' || m.role === 'assistant')
68
+ .map((m: any) => {
69
+ // Backward compat for audio_data: file path → URL, data: prefix → legacy, else → prepend data URL
70
+ let audioData: string | undefined;
71
+ if (m.audio_data) {
72
+ if (m.audio_data.startsWith('data:')) {
73
+ audioData = m.audio_data; // legacy data URL
74
+ } else if (m.audio_data.includes('/')) {
75
+ audioData = `/api/files/${m.audio_data}`; // file path → HTTP URL
76
+ } else {
77
+ audioData = `data:audio/webm;base64,${m.audio_data}`; // raw base64
78
+ }
50
79
  }
51
- })
52
- .catch(() => {});
53
- }, []);
54
-
55
- // Load messages when conversationId is set
56
- useEffect(() => {
57
- if (!conversationId) return;
58
- fetch(`/api/conversations/${conversationId}`)
59
- .then(r => {
60
- if (!r.ok) throw new Error('not found');
61
- return r.json();
62
- })
63
- .then(data => {
64
- if (data.messages?.length) {
65
- setMessages(
66
- data.messages
67
- .filter(
68
- (m: any) =>
69
- m.role === 'user' || m.role === 'assistant'
70
- )
71
- .map((m: any) => {
72
- // Backward compat for audio_data: file path → URL, data: prefix → legacy, else → prepend data URL
73
- let audioData: string | undefined;
74
- if (m.audio_data) {
75
- if (m.audio_data.startsWith('data:')) {
76
- audioData = m.audio_data; // legacy data URL
77
- } else if (m.audio_data.includes('/')) {
78
- audioData = `/api/files/${m.audio_data}`; // file path → HTTP URL
79
- } else {
80
- audioData = `data:audio/webm;base64,${m.audio_data}`; // raw base64
81
- }
82
- }
83
80
 
84
- // Parse stored attachments
85
- let attachments: StoredAttachment[] | undefined;
86
- if (m.attachments) {
87
- try {
88
- attachments = JSON.parse(m.attachments);
89
- } catch {
90
- /* ignore malformed */
91
- }
92
- }
93
-
94
- return {
95
- id: m.id,
96
- role: m.role,
97
- content: m.content,
98
- timestamp: m.created_at,
99
- audioData,
100
- hasAttachments: !!(
101
- attachments && attachments.length > 0
102
- ),
103
- attachments
104
- };
105
- })
106
- );
81
+ // Parse stored attachments
82
+ let attachments: StoredAttachment[] | undefined;
83
+ if (m.attachments) {
84
+ try {
85
+ attachments = JSON.parse(m.attachments);
86
+ } catch { /* ignore malformed */ }
107
87
  }
108
- })
109
- .catch(() => {
110
- // Conversation gone — clear
111
- setConversationId(null);
112
- fetch('/api/context/clear', { method: 'POST' }).catch(() => {});
113
- });
114
- }, [conversationId]);
115
88
 
116
- // Persist conversationId to DB when it changes
117
- const prevConvId = useRef<string | null>(null);
118
- useEffect(() => {
119
- if (conversationId && conversationId !== prevConvId.current) {
120
- prevConvId.current = conversationId;
121
- fetch('/api/context/set', {
122
- method: 'POST',
123
- headers: { 'Content-Type': 'application/json' },
124
- body: JSON.stringify({ conversationId })
125
- }).catch(() => {});
89
+ return {
90
+ id: m.id,
91
+ role: m.role,
92
+ content: m.content,
93
+ timestamp: m.created_at,
94
+ audioData,
95
+ hasAttachments: !!(attachments && attachments.length > 0),
96
+ attachments,
97
+ };
98
+ }),
99
+ );
126
100
  }
127
- }, [conversationId]);
128
-
129
- useEffect(() => {
130
- if (!ws) return;
131
-
132
- const unsubs = [
133
- ws.on('bot:typing', () => {
134
- setStreaming(true);
135
- setTools([]);
136
- }),
137
- ws.on('bot:token', (data: { token: string }) => {
138
- setStreamBuffer(buf => buf + data.token);
139
- }),
140
- ws.on(
141
- 'bot:tool',
142
- (data: { name: string; input?: any; status?: string }) => {
143
- setTools(prev => {
144
- const existing = prev.find(
145
- t => t.name === data.name && t.status === 'running'
146
- );
147
- if (existing) return prev;
148
- return [
149
- ...prev,
150
- { name: data.name, status: 'running' }
151
- ];
152
- });
153
- }
154
- ),
155
- ws.on(
156
- 'bot:response',
157
- (data: {
158
- conversationId: string;
159
- messageId?: string;
160
- content: string;
161
- }) => {
162
- setConversationId(data.conversationId);
163
- setMessages(msgs => [
164
- ...msgs,
165
- {
166
- id: data.messageId || Date.now().toString(),
167
- role: 'assistant',
168
- content: data.content,
169
- timestamp: new Date().toISOString()
170
- }
171
- ]);
172
- setStreamBuffer('');
173
- setStreaming(false);
174
- setTools([]);
175
- }
176
- ),
177
- ws.on('bot:error', (data: { error: string }) => {
178
- setStreamBuffer('');
179
- setStreaming(false);
180
- setTools([]);
181
- setMessages(msgs => [
182
- ...msgs,
183
- {
184
- id: Date.now().toString(),
185
- role: 'assistant',
186
- content: `Error: ${data.error}`,
187
- timestamp: new Date().toISOString()
188
- }
189
- ]);
190
- })
191
- ];
192
-
193
- return () => unsubs.forEach(u => u());
194
- }, [ws]);
195
-
196
- const sendMessage = useCallback(
197
- (content: string, attachments?: Attachment[], audioData?: string) => {
198
- if (
199
- !ws ||
200
- (!content.trim() && (!attachments || attachments.length === 0))
201
- )
202
- return;
203
-
204
- // Build optimistic stored attachments from client previews
205
- const optimisticAttachments: StoredAttachment[] | undefined =
206
- attachments?.map(att => {
207
- const match = att.preview.match(/^data:([^;]+);/);
208
- return {
209
- type: att.type,
210
- name: att.name,
211
- mediaType: match?.[1] || 'application/octet-stream',
212
- filePath: att.preview
213
- };
214
- });
215
-
216
- const userMsg: ChatMessage = {
217
- id: Date.now().toString(),
218
- role: 'user',
219
- content,
220
- timestamp: new Date().toISOString(),
221
- hasAttachments: !!(attachments && attachments.length > 0),
222
- audioData: audioData
223
- ? audioData.startsWith('data:')
224
- ? audioData
225
- : `data:audio/webm;base64,${audioData}`
226
- : undefined,
227
- attachments: optimisticAttachments
228
- };
229
- setMessages(msgs => [...msgs, userMsg]);
230
-
231
- // Build WS payload
232
- const payload: any = { conversationId, content };
233
- if (audioData) {
234
- // Send raw base64 (strip data URL prefix)
235
- payload.audioData = audioData.includes(',')
236
- ? audioData.split(',')[1]
237
- : audioData;
238
- }
239
- if (attachments?.length) {
240
- payload.attachments = attachments.map(att => {
241
- const match = att.preview.match(
242
- /^data:([^;]+);base64,(.+)$/
243
- );
244
- return {
245
- type: att.type,
246
- name: att.name,
247
- mediaType: match?.[1] || 'application/octet-stream',
248
- data: match?.[2] || ''
249
- };
250
- });
251
- }
252
- ws.send('user:message', payload);
253
- },
254
- [ws, conversationId]
255
- );
256
-
257
- const stopStreaming = useCallback(() => {
258
- if (!ws || !conversationId) return;
259
- ws.send('user:stop', { conversationId });
260
- setStreaming(false);
101
+ })
102
+ .catch(() => {
103
+ // Conversation gone — clear
104
+ setConversationId(null);
105
+ fetch('/api/context/clear', { method: 'POST' }).catch(() => {});
106
+ });
107
+ }, [conversationId]);
108
+
109
+ // Persist conversationId to DB when it changes
110
+ const prevConvId = useRef<string | null>(null);
111
+ useEffect(() => {
112
+ if (conversationId && conversationId !== prevConvId.current) {
113
+ prevConvId.current = conversationId;
114
+ fetch('/api/context/set', {
115
+ method: 'POST',
116
+ headers: { 'Content-Type': 'application/json' },
117
+ body: JSON.stringify({ conversationId }),
118
+ }).catch(() => {});
119
+ }
120
+ }, [conversationId]);
121
+
122
+ useEffect(() => {
123
+ if (!ws) return;
124
+
125
+ const unsubs = [
126
+ ws.on('bot:typing', () => {
127
+ setStreaming(true);
128
+ setTools([]);
129
+ }),
130
+ ws.on('bot:token', (data: { token: string }) => {
131
+ setStreamBuffer((buf) => buf + data.token);
132
+ }),
133
+ ws.on('bot:tool', (data: { name: string; input?: any; status?: string }) => {
134
+ setTools((prev) => {
135
+ const existing = prev.find((t) => t.name === data.name && t.status === 'running');
136
+ if (existing) return prev;
137
+ return [...prev, { name: data.name, status: 'running' }];
138
+ });
139
+ }),
140
+ ws.on('bot:response', (data: { conversationId: string; messageId?: string; content: string }) => {
141
+ setConversationId(data.conversationId);
142
+ setMessages((msgs) => [
143
+ ...msgs,
144
+ {
145
+ id: data.messageId || Date.now().toString(),
146
+ role: 'assistant',
147
+ content: data.content,
148
+ timestamp: new Date().toISOString(),
149
+ },
150
+ ]);
261
151
  setStreamBuffer('');
152
+ setStreaming(false);
262
153
  setTools([]);
263
- }, [ws, conversationId]);
264
-
265
- const clearContext = useCallback(() => {
266
- setMessages([]);
267
- setConversationId(null);
154
+ }),
155
+ ws.on('bot:error', (data: { error: string }) => {
268
156
  setStreamBuffer('');
269
157
  setStreaming(false);
270
158
  setTools([]);
271
- prevConvId.current = null;
272
- loaded.current = false;
273
- fetch('/api/context/clear', { method: 'POST' }).catch(() => {});
274
- }, []);
275
-
276
- return {
277
- messages,
278
- streaming,
279
- streamBuffer,
280
- conversationId,
281
- tools,
282
- sendMessage,
283
- stopStreaming,
284
- clearContext
285
- };
159
+ setMessages((msgs) => [
160
+ ...msgs,
161
+ {
162
+ id: Date.now().toString(),
163
+ role: 'assistant',
164
+ content: `Error: ${data.error}`,
165
+ timestamp: new Date().toISOString(),
166
+ },
167
+ ]);
168
+ }),
169
+ ];
170
+
171
+ return () => unsubs.forEach((u) => u());
172
+ }, [ws]);
173
+
174
+ const sendMessage = useCallback(
175
+ (content: string, attachments?: Attachment[], audioData?: string) => {
176
+ if (!ws || (!content.trim() && (!attachments || attachments.length === 0))) return;
177
+
178
+ // Build optimistic stored attachments from client previews
179
+ const optimisticAttachments: StoredAttachment[] | undefined = attachments?.map((att) => {
180
+ const match = att.preview.match(/^data:([^;]+);/);
181
+ return { type: att.type, name: att.name, mediaType: match?.[1] || 'application/octet-stream', filePath: att.preview };
182
+ });
183
+
184
+ const userMsg: ChatMessage = {
185
+ id: Date.now().toString(),
186
+ role: 'user',
187
+ content,
188
+ timestamp: new Date().toISOString(),
189
+ hasAttachments: !!(attachments && attachments.length > 0),
190
+ audioData: audioData ? (audioData.startsWith('data:') ? audioData : `data:audio/webm;base64,${audioData}`) : undefined,
191
+ attachments: optimisticAttachments,
192
+ };
193
+ setMessages((msgs) => [...msgs, userMsg]);
194
+
195
+ // Build WS payload
196
+ const payload: any = { conversationId, content };
197
+ if (audioData) {
198
+ // Send raw base64 (strip data URL prefix)
199
+ payload.audioData = audioData.includes(',') ? audioData.split(',')[1] : audioData;
200
+ }
201
+ if (attachments?.length) {
202
+ payload.attachments = attachments.map((att) => {
203
+ const match = att.preview.match(/^data:([^;]+);base64,(.+)$/);
204
+ return {
205
+ type: att.type,
206
+ name: att.name,
207
+ mediaType: match?.[1] || 'application/octet-stream',
208
+ data: match?.[2] || '',
209
+ };
210
+ });
211
+ }
212
+ ws.send('user:message', payload);
213
+ },
214
+ [ws, conversationId],
215
+ );
216
+
217
+ const stopStreaming = useCallback(() => {
218
+ if (!ws || !conversationId) return;
219
+ ws.send('user:stop', { conversationId });
220
+ setStreaming(false);
221
+ setStreamBuffer('');
222
+ setTools([]);
223
+ }, [ws, conversationId]);
224
+
225
+ const clearContext = useCallback(() => {
226
+ setMessages([]);
227
+ setConversationId(null);
228
+ setStreamBuffer('');
229
+ setStreaming(false);
230
+ setTools([]);
231
+ prevConvId.current = null;
232
+ loaded.current = false;
233
+ fetch('/api/context/clear', { method: 'POST' }).catch(() => {});
234
+ }, []);
235
+
236
+ return { messages, streaming, streamBuffer, conversationId, tools, sendMessage, stopStreaming, clearContext };
286
237
  }