fluxy-bot 0.8.2 → 0.8.4

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.
@@ -1 +1 @@
1
- import{e as o,j as e,R as n,O as r}from"./globals-CP6y1O_V.js";function a(){const t=()=>{window.parent?.postMessage({type:"fluxy:onboard-complete"},"*")};return e.jsx(r,{onComplete:t,isInitialSetup:!0})}o.createRoot(document.getElementById("root")).render(e.jsx(n.StrictMode,{children:e.jsx(a,{})}));
1
+ import{e as o,j as e,R as n,O as r}from"./globals-DYbVw4jJ.js";function a(){const t=()=>{window.parent?.postMessage({type:"fluxy:onboard-complete"},"*")};return e.jsx(r,{onComplete:t,isInitialSetup:!0})}o.createRoot(document.getElementById("root")).render(e.jsx(n.StrictMode,{children:e.jsx(a,{})}));
@@ -4,9 +4,9 @@
4
4
  <meta charset="UTF-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, interactive-widget=resizes-content" />
6
6
  <title>Fluxy Chat</title>
7
- <script type="module" crossorigin src="/fluxy/assets/fluxy-C7pmHG9i.js"></script>
8
- <link rel="modulepreload" crossorigin href="/fluxy/assets/globals-CP6y1O_V.js">
9
- <link rel="stylesheet" crossorigin href="/fluxy/assets/globals-wnYSfEuA.css">
7
+ <script type="module" crossorigin src="/fluxy/assets/fluxy-V26b7AxV.js"></script>
8
+ <link rel="modulepreload" crossorigin href="/fluxy/assets/globals-DYbVw4jJ.js">
9
+ <link rel="stylesheet" crossorigin href="/fluxy/assets/globals-BrtAPuMR.css">
10
10
  </head>
11
11
  <body class="bg-background text-foreground">
12
12
  <div id="root"></div>
@@ -4,9 +4,9 @@
4
4
  <meta charset="UTF-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, interactive-widget=resizes-content" />
6
6
  <title>Fluxy Setup</title>
7
- <script type="module" crossorigin src="/fluxy/assets/onboard-CrwD-dFw.js"></script>
8
- <link rel="modulepreload" crossorigin href="/fluxy/assets/globals-CP6y1O_V.js">
9
- <link rel="stylesheet" crossorigin href="/fluxy/assets/globals-wnYSfEuA.css">
7
+ <script type="module" crossorigin src="/fluxy/assets/onboard-Dwc03fqY.js"></script>
8
+ <link rel="modulepreload" crossorigin href="/fluxy/assets/globals-DYbVw4jJ.js">
9
+ <link rel="stylesheet" crossorigin href="/fluxy/assets/globals-BrtAPuMR.css">
10
10
  </head>
11
11
  <body class="bg-background text-foreground">
12
12
  <div id="root"></div>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fluxy-bot",
3
- "version": "0.8.2",
3
+ "version": "0.8.4",
4
4
  "releaseNotes": [
5
5
  "Fixed some bugs to iOs ",
6
6
  "2. ",
@@ -45,7 +45,8 @@
45
45
  "build": "vite build && vite build --config vite.fluxy.config.ts",
46
46
  "build:fluxy": "vite build --config vite.fluxy.config.ts",
47
47
  "start": "node --import tsx/esm supervisor/index.ts",
48
- "postinstall": "node scripts/postinstall.js"
48
+ "postinstall": "node scripts/postinstall.js",
49
+ "dev:docs": "cd ./docs && npx fumapress"
49
50
  },
50
51
  "dependencies": {
51
52
  "@anthropic-ai/claude-agent-sdk": "^0.2.50",
@@ -93,6 +94,7 @@
93
94
  "@types/react-syntax-highlighter": "^15.5.13",
94
95
  "@types/ws": "^8.18.1",
95
96
  "concurrently": "^9.2.1",
97
+ "fumapress": "^0.1.1",
96
98
  "typescript": "^5.9.3"
97
99
  }
98
100
  }
@@ -2096,24 +2096,38 @@ export default function OnboardWizard({ onComplete, isInitialSetup = false, onSa
2096
2096
  </div>
2097
2097
  )}
2098
2098
 
2099
- {/* ── Step 5: Whisper (optional) + Complete ── */}
2099
+ {/* ── Step 5: Voice Messages + Complete ── */}
2100
2100
  {step === 5 && (
2101
2101
  <div>
2102
- <div className="flex items-center gap-2 mb-1">
2103
- <h1 className="text-xl font-bold text-white tracking-tight">
2104
- Voice Messages
2105
- </h1>
2106
- <span className="text-[11px] text-white/25 font-medium bg-white/[0.04] border border-white/[0.06] rounded-full px-2.5 py-0.5">
2107
- Optional
2108
- </span>
2109
- </div>
2102
+ <h1 className="text-xl font-bold text-white tracking-tight mb-1">
2103
+ Voice Messages
2104
+ </h1>
2110
2105
  <p className="text-white/40 text-[13px] mt-1 leading-relaxed">
2111
- Allow users to send audio messages that are automatically transcribed.
2106
+ Voice input works out of the box using your browser's built-in speech recognition. For better accuracy, you can optionally enable OpenAI Whisper.
2112
2107
  </p>
2113
2108
 
2109
+ {/* Browser Speech Recognition card */}
2110
+ <div className="w-full mt-5 rounded-xl border border-emerald-500/20 bg-emerald-500/[0.04] p-4">
2111
+ <div className="flex items-center gap-3.5">
2112
+ <div className="w-10 h-10 rounded-xl bg-emerald-500/10 flex items-center justify-center shrink-0">
2113
+ <Mic className="h-5 w-5 text-emerald-400" />
2114
+ </div>
2115
+ <div className="flex-1">
2116
+ <div className="text-[14px] font-medium text-white">Browser Speech Recognition</div>
2117
+ <div className="text-[12px] text-white/35 mt-0.5 leading-relaxed">
2118
+ Built-in voice input — no setup needed. Works in Chrome, Edge, and Safari.
2119
+ </div>
2120
+ </div>
2121
+ <div className="flex items-center justify-center w-6 h-6 rounded-full bg-emerald-500/20 shrink-0">
2122
+ <Check className="h-3.5 w-3.5 text-emerald-400" />
2123
+ </div>
2124
+ </div>
2125
+ </div>
2126
+
2127
+ {/* Whisper upgrade toggle */}
2114
2128
  <button
2115
2129
  onClick={() => setWhisperEnabled((v) => !v)}
2116
- className={`w-full mt-5 rounded-xl border transition-all duration-200 p-4 text-left ${
2130
+ className={`w-full mt-3 rounded-xl border transition-all duration-200 p-4 text-left ${
2117
2131
  whisperEnabled
2118
2132
  ? 'bg-white/[0.04] border-[#AF27E3]/40'
2119
2133
  : 'bg-transparent border-white/[0.06] hover:border-white/10 hover:bg-white/[0.02]'
@@ -2124,7 +2138,7 @@ export default function OnboardWizard({ onComplete, isInitialSetup = false, onSa
2124
2138
  <div className="flex-1">
2125
2139
  <div className="text-[14px] font-medium text-white">OpenAI Whisper</div>
2126
2140
  <div className="text-[12px] text-white/35 mt-0.5 leading-relaxed">
2127
- Speech-to-text powered by OpenAI. Requires an OpenAI API key.
2141
+ Upgrade to more accurate transcription that works in all browsers including Firefox.
2128
2142
  </div>
2129
2143
  </div>
2130
2144
  <div className={`w-10 h-[22px] rounded-full transition-colors duration-200 flex items-center px-0.5 shrink-0 ${
@@ -2157,7 +2171,7 @@ export default function OnboardWizard({ onComplete, isInitialSetup = false, onSa
2157
2171
  <div className="flex items-start gap-2.5 mt-3 bg-white/[0.02] border border-white/[0.06] rounded-xl px-4 py-3">
2158
2172
  <Mic className="h-4 w-4 text-[#AF27E3]/60 mt-0.5 shrink-0" />
2159
2173
  <p className="text-white/35 text-[12px] leading-relaxed">
2160
- Users will see a microphone button in the chat. Audio is sent to OpenAI's Whisper API for transcription, then processed as a regular text message.
2174
+ Whisper provides more accurate transcription and works in all browsers including Firefox.
2161
2175
  </p>
2162
2176
  </div>
2163
2177
  </div>
@@ -2177,7 +2191,7 @@ export default function OnboardWizard({ onComplete, isInitialSetup = false, onSa
2177
2191
 
2178
2192
  {!whisperEnabled && (
2179
2193
  <p className="text-center text-white/20 text-[11px] mt-2.5">
2180
- You can enable this later in Settings.
2194
+ Voice input is active using your browser's built-in speech recognition.
2181
2195
  </p>
2182
2196
  )}
2183
2197
  </div>
@@ -2,6 +2,7 @@ import { useState, useRef, useCallback, useEffect, type PointerEvent as RPointer
2
2
  import { SendHorizontal, Mic, Square, Trash2, Paperclip, Camera, X } from 'lucide-react';
3
3
  import { motion, AnimatePresence } from 'framer-motion';
4
4
  import type { Attachment } from '../../hooks/useChat';
5
+ import { useSpeechRecognition } from '../../hooks/useSpeechRecognition';
5
6
 
6
7
  interface Props {
7
8
  onSend: (msg: string, attachments?: Attachment[], audioData?: string) => void;
@@ -55,6 +56,8 @@ function compressImage(dataUrl: string, maxBytes = 4 * 1024 * 1024): Promise<str
55
56
  const DRAFT_KEY = 'fluxy_draft';
56
57
 
57
58
  export default function InputBar({ onSend, onStop, streaming, whisperEnabled, onTranscribe }: Props) {
59
+ const { start: startSpeech, stop: stopSpeech, abort: abortSpeech, isSupported: webSpeechSupported } = useSpeechRecognition();
60
+ const voiceEnabled = whisperEnabled || webSpeechSupported;
58
61
  const [text, setText] = useState(() => {
59
62
  try { return localStorage.getItem(DRAFT_KEY) || ''; } catch { return ''; }
60
63
  });
@@ -112,12 +115,20 @@ export default function InputBar({ onSend, onStop, streaming, whisperEnabled, on
112
115
  const recorder = mediaRecorderRef.current;
113
116
  const stream = streamRef.current;
114
117
 
115
- if (cancelled || !recorder || recorder.state === 'inactive') {
116
- // Clean up mic
118
+ if (cancelled) {
119
+ // Clean up mic + abort speech recognition
117
120
  stream?.getTracks().forEach((t) => t.stop());
118
121
  mediaRecorderRef.current = null;
119
122
  streamRef.current = null;
120
123
  audioChunksRef.current = [];
124
+ abortSpeech();
125
+ } else if (!recorder || recorder.state === 'inactive') {
126
+ // No recorder active, clean up
127
+ stream?.getTracks().forEach((t) => t.stop());
128
+ mediaRecorderRef.current = null;
129
+ streamRef.current = null;
130
+ audioChunksRef.current = [];
131
+ abortSpeech();
121
132
  } else {
122
133
  // Stop recorder — ondataavailable + onstop will fire
123
134
  recorder.onstop = async () => {
@@ -127,7 +138,10 @@ export default function InputBar({ onSend, onStop, streaming, whisperEnabled, on
127
138
  mediaRecorderRef.current = null;
128
139
  streamRef.current = null;
129
140
 
130
- if (blob.size < 1000) return; // too small, skip
141
+ if (blob.size < 1000) {
142
+ abortSpeech();
143
+ return; // too small, skip
144
+ }
131
145
 
132
146
  // Convert to base64
133
147
  const reader = new FileReader();
@@ -137,23 +151,36 @@ export default function InputBar({ onSend, onStop, streaming, whisperEnabled, on
137
151
  if (!base64) return;
138
152
 
139
153
  try {
140
- let data: { transcript?: string };
141
- if (onTranscribe) {
142
- data = await onTranscribe(base64);
154
+ // Whisper path: send audio to Whisper API for transcription
155
+ if (whisperEnabled && (onTranscribe || true)) {
156
+ let data: { transcript?: string };
157
+ if (onTranscribe) {
158
+ data = await onTranscribe(base64);
159
+ } else {
160
+ const res = await fetch('/api/whisper/transcribe', {
161
+ method: 'POST',
162
+ headers: { 'Content-Type': 'application/json' },
163
+ body: JSON.stringify({ audio: base64 }),
164
+ });
165
+ data = await res.json();
166
+ }
167
+ abortSpeech(); // discard Web Speech results when using Whisper
168
+ if (data.transcript?.trim()) {
169
+ const pendingAtts = attachments.length > 0 ? attachments : undefined;
170
+ onSend(data.transcript.trim(), pendingAtts, dataUrl);
171
+ if (pendingAtts) setAttachments([]);
172
+ }
143
173
  } else {
144
- const res = await fetch('/api/whisper/transcribe', {
145
- method: 'POST',
146
- headers: { 'Content-Type': 'application/json' },
147
- body: JSON.stringify({ audio: base64 }),
148
- });
149
- data = await res.json();
150
- }
151
- if (data.transcript?.trim()) {
152
- const pendingAtts = attachments.length > 0 ? attachments : undefined;
153
- onSend(data.transcript.trim(), pendingAtts, dataUrl);
154
- if (pendingAtts) setAttachments([]);
174
+ // Web Speech API path: use accumulated transcript
175
+ const transcript = await stopSpeech();
176
+ if (transcript.trim()) {
177
+ const pendingAtts = attachments.length > 0 ? attachments : undefined;
178
+ onSend(transcript.trim(), pendingAtts, dataUrl);
179
+ if (pendingAtts) setAttachments([]);
180
+ }
155
181
  }
156
182
  } catch {
183
+ abortSpeech();
157
184
  // Transcription failed silently
158
185
  }
159
186
  };
@@ -166,7 +193,7 @@ export default function InputBar({ onSend, onStop, streaming, whisperEnabled, on
166
193
  setIsRecording(false);
167
194
  setRecordingTime(0);
168
195
  dragRef.current = 0;
169
- }, [onSend, onTranscribe, attachments]);
196
+ }, [onSend, onTranscribe, attachments, whisperEnabled, abortSpeech, stopSpeech]);
170
197
 
171
198
  // ── File handling ──
172
199
 
@@ -241,7 +268,7 @@ export default function InputBar({ onSend, onStop, streaming, whisperEnabled, on
241
268
  (e.currentTarget as HTMLElement).setPointerCapture(e.pointerId);
242
269
 
243
270
  holdTimerRef.current = setTimeout(async () => {
244
- if (!whisperEnabled) return;
271
+ if (!voiceEnabled) return;
245
272
  try {
246
273
  const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
247
274
  streamRef.current = stream;
@@ -253,6 +280,12 @@ export default function InputBar({ onSend, onStop, streaming, whisperEnabled, on
253
280
  };
254
281
  mediaRecorderRef.current = recorder;
255
282
  recorder.start();
283
+
284
+ // Start Web Speech API alongside MediaRecorder (used as fallback or primary)
285
+ if (webSpeechSupported) {
286
+ startSpeech();
287
+ }
288
+
256
289
  isHolding.current = true;
257
290
  setIsRecording(true);
258
291
  setRecordingTime(0);
@@ -260,7 +293,7 @@ export default function InputBar({ onSend, onStop, streaming, whisperEnabled, on
260
293
  // Mic permission denied or not available
261
294
  }
262
295
  }, 200);
263
- }, [whisperEnabled]);
296
+ }, [voiceEnabled, webSpeechSupported, startSpeech]);
264
297
 
265
298
  const handleMicMove = useCallback((e: RPointerEvent) => {
266
299
  if (!isHolding.current) return;
@@ -391,14 +424,15 @@ export default function InputBar({ onSend, onStop, streaming, whisperEnabled, on
391
424
  ) : (
392
425
  <div
393
426
  className={`flex items-center justify-center h-12 w-12 shrink-0 rounded-full transition-colors touch-none select-none ${
394
- whisperEnabled
427
+ voiceEnabled
395
428
  ? 'bg-gradient-brand text-white cursor-pointer'
396
429
  : 'bg-gray-200 text-gray-400 cursor-default'
397
430
  }`}
398
- onPointerDown={whisperEnabled ? handleMicDown : undefined}
399
- onPointerMove={whisperEnabled ? handleMicMove : undefined}
400
- onPointerUp={whisperEnabled ? handleMicUp : undefined}
401
- onPointerCancel={whisperEnabled ? handleMicCancel : undefined}
431
+ title={!voiceEnabled ? 'Voice input requires Chrome, Edge, or Safari. Enable Whisper in Settings for any browser.' : undefined}
432
+ onPointerDown={voiceEnabled ? handleMicDown : undefined}
433
+ onPointerMove={voiceEnabled ? handleMicMove : undefined}
434
+ onPointerUp={voiceEnabled ? handleMicUp : undefined}
435
+ onPointerCancel={voiceEnabled ? handleMicCancel : undefined}
402
436
  >
403
437
  <Mic className="h-4 w-4" />
404
438
  </div>
@@ -0,0 +1,132 @@
1
+ import { useRef, useCallback, useMemo } from 'react';
2
+
3
+ // Extend Window for vendor-prefixed SpeechRecognition
4
+ interface SpeechRecognitionEvent extends Event {
5
+ results: SpeechRecognitionResultList;
6
+ resultIndex: number;
7
+ }
8
+
9
+ type SpeechRecognitionConstructor = new () => SpeechRecognitionInstance;
10
+
11
+ interface SpeechRecognitionInstance extends EventTarget {
12
+ continuous: boolean;
13
+ interimResults: boolean;
14
+ lang: string;
15
+ start(): void;
16
+ stop(): void;
17
+ abort(): void;
18
+ onresult: ((event: SpeechRecognitionEvent) => void) | null;
19
+ onend: (() => void) | null;
20
+ onerror: ((event: Event & { error: string }) => void) | null;
21
+ }
22
+
23
+ const getSpeechRecognitionCtor = (): SpeechRecognitionConstructor | null => {
24
+ if (typeof window === 'undefined') return null;
25
+ return (
26
+ (window as unknown as Record<string, SpeechRecognitionConstructor>).SpeechRecognition ??
27
+ (window as unknown as Record<string, SpeechRecognitionConstructor>).webkitSpeechRecognition ??
28
+ null
29
+ );
30
+ };
31
+
32
+ export const isWebSpeechSupported = getSpeechRecognitionCtor() !== null;
33
+
34
+ export function useSpeechRecognition() {
35
+ const instanceRef = useRef<SpeechRecognitionInstance | null>(null);
36
+ const transcriptRef = useRef('');
37
+ const activeRef = useRef(false);
38
+ const resolveRef = useRef<((text: string) => void) | null>(null);
39
+
40
+ const isSupported = useMemo(() => isWebSpeechSupported, []);
41
+
42
+ const start = useCallback(() => {
43
+ const Ctor = getSpeechRecognitionCtor();
44
+ if (!Ctor) return;
45
+
46
+ // Clean up any lingering instance
47
+ if (instanceRef.current) {
48
+ try { instanceRef.current.abort(); } catch {}
49
+ }
50
+
51
+ const recognition = new Ctor();
52
+ recognition.continuous = true;
53
+ recognition.interimResults = true;
54
+ recognition.lang = navigator.language || 'en-US';
55
+
56
+ transcriptRef.current = '';
57
+ activeRef.current = true;
58
+ instanceRef.current = recognition;
59
+
60
+ recognition.onresult = (event: SpeechRecognitionEvent) => {
61
+ let finalTranscript = '';
62
+ for (let i = 0; i < event.results.length; i++) {
63
+ if (event.results[i].isFinal) {
64
+ finalTranscript += event.results[i][0].transcript;
65
+ }
66
+ }
67
+ if (finalTranscript) {
68
+ transcriptRef.current = finalTranscript;
69
+ }
70
+ };
71
+
72
+ recognition.onend = () => {
73
+ // Auto-restart if we're still supposed to be listening (handles silence gaps)
74
+ if (activeRef.current) {
75
+ try { recognition.start(); } catch {}
76
+ }
77
+ };
78
+
79
+ recognition.onerror = () => {
80
+ // On error, stop gracefully
81
+ if (activeRef.current) {
82
+ activeRef.current = false;
83
+ resolveRef.current?.(transcriptRef.current);
84
+ resolveRef.current = null;
85
+ }
86
+ };
87
+
88
+ try { recognition.start(); } catch {}
89
+ }, []);
90
+
91
+ const stop = useCallback((): Promise<string> => {
92
+ return new Promise((resolve) => {
93
+ activeRef.current = false;
94
+ const instance = instanceRef.current;
95
+ if (!instance) {
96
+ resolve(transcriptRef.current);
97
+ return;
98
+ }
99
+
100
+ resolveRef.current = resolve;
101
+
102
+ // Give a brief moment for any final results, then resolve
103
+ setTimeout(() => {
104
+ try { instance.stop(); } catch {}
105
+ instanceRef.current = null;
106
+
107
+ // Resolve with whatever we accumulated
108
+ const text = transcriptRef.current;
109
+ if (resolveRef.current) {
110
+ resolveRef.current(text);
111
+ resolveRef.current = null;
112
+ }
113
+ }, 300);
114
+ });
115
+ }, []);
116
+
117
+ const abort = useCallback(() => {
118
+ activeRef.current = false;
119
+ transcriptRef.current = '';
120
+ const instance = instanceRef.current;
121
+ if (instance) {
122
+ try { instance.abort(); } catch {}
123
+ instanceRef.current = null;
124
+ }
125
+ if (resolveRef.current) {
126
+ resolveRef.current('');
127
+ resolveRef.current = null;
128
+ }
129
+ }, []);
130
+
131
+ return { start, stop, abort, isSupported };
132
+ }
@@ -17,7 +17,6 @@ import { ensureFileDirs, saveAttachment, type SavedFile } from './file-saver.js'
17
17
  import { startViteDevServers, stopViteDevServers } from './vite-dev.js';
18
18
  import { startScheduler, stopScheduler } from './scheduler.js';
19
19
  import { execSync, spawn as cpSpawn } from 'child_process';
20
- import os from 'os';
21
20
 
22
21
  const DIST_FLUXY = path.join(PKG_DIR, 'dist-fluxy');
23
22
 
@@ -785,27 +784,12 @@ export async function startSupervisor() {
785
784
  const cliPath = path.join(PKG_DIR, 'bin', 'cli.js');
786
785
  log.info('Deferred update triggered — running fluxy update...');
787
786
  try {
788
- if (os.platform() === 'linux') {
789
- // Use systemd-run on Linux so the update survives daemon restart
790
- const user = os.userInfo().username;
791
- const child = cpSpawn('sudo', [
792
- 'systemd-run', '--quiet',
793
- '--unit=fluxy-update',
794
- '--uid=' + user,
795
- '--setenv=HOME=' + os.homedir(),
796
- '--setenv=PATH=' + process.env.PATH,
797
- process.execPath, cliPath, 'update',
798
- ], { detached: true, stdio: 'ignore' });
799
- child.unref();
800
- } else {
801
- // macOS / other: detached child process survives parent exit
802
- const child = cpSpawn(process.execPath, [cliPath, 'update'], {
803
- detached: true,
804
- stdio: 'ignore',
805
- env: { ...process.env },
806
- });
807
- child.unref();
808
- }
787
+ const child = cpSpawn(process.execPath, [cliPath, 'update'], {
788
+ detached: true,
789
+ stdio: 'ignore',
790
+ env: { ...process.env },
791
+ });
792
+ child.unref();
809
793
  } catch (err) {
810
794
  log.error(`Deferred update failed: ${err instanceof Error ? err.message : err}`);
811
795
  }
@@ -156,17 +156,17 @@ Notify your human only if importance is 7+ — otherwise log results silently.
156
156
 
157
157
  **Check version:** `cat ~/.fluxy/VERSION` (current) vs `npm view fluxy-bot version` (latest).
158
158
 
159
- **To update:** Create the trigger file `touch ~/.fluxy/workspace/.update` — the supervisor runs the update after your turn ends. You will
159
+ **To update:** Create the trigger file `touch .update` — the supervisor runs the update after your turn ends. You will
160
160
  NOT die. Finish your turn normally.
161
161
 
162
162
  **On PULSE:** Occasionally check for updates (not every pulse — once every few hours). If a new version exists:
163
163
  1. Read release notes: `npm view fluxy-bot releaseNotes --json`
164
- 2. `touch ~/.fluxy/workspace/.update`
164
+ 2. `touch .update`
165
165
  3. Save to daily notes: "Updated from vX to vY" + notable changes to talk with your human later "Btw I updated myself this night"
166
166
 
167
167
  **When user asks to update:**
168
168
  1. Check version, if already latest say so
169
- 2. Otherwise read release notes, `touch ~/.fluxy/workspace/.update`
169
+ 2. Otherwise read release notes, `touch .update`
170
170
  3. Tell your human what is new and that the page will be unresponsive for up to 2 minutes while this happens.
171
171
 
172
172
  ## Task Files — `tasks/`
@@ -0,0 +1,56 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="256" height="256" viewBox="0 0 256 256" fill="none">
2
+ <path d="M 240.91,138.91 C 233.17,132.97 219.01,128.82 219.78,105.04 C 222.51,70.32 202.18,34.26 155.19,27.17 C 122.91,22.28 98.81,35.96 80.08,59.11 C 69.99,71.74 59.18,76.12 43.12,75.61 C 39.98,75.51 38.02,75.06 35.42,75.17 C 17.29,75.97 2.11,88.69 2.24,103.78 C 2.35,116.39 13.41,127.29 26.58,127.07 C 35.21,126.92 41.39,123.86 47.89,118.76 C 51.29,116.21 55.71,114.78 58.31,116.93 C 62.68,120.47 59.98,127.97 46.92,136.32 C 32.82,144.78 17.01,156.87 18.19,173.94 C 19.53,191.76 37.19,199.93 54.61,202.21 C 63.18,202.97 70.92,201.38 78.32,201.99 C 88.92,202.91 96.07,207.81 103.91,214.81 C 113.25,223.09 123.72,227.77 136.99,227.34 C 152.17,226.82 163.18,218.71 174.28,207.06 C 184.32,196.76 196.62,191.96 208.76,191.59 C 218.29,191.29 227.23,190.96 235.12,187.22 C 252.26,178.94 259.46,153.81 240.91,138.91 Z" fill="url(#paint0_radial_143_2721)"/>
3
+ <path d="M 142.28,26.41 C 119.01,26.08 98.31,36.68 80.38,58.48 C 70.89,70.46 61.18,76.07 43.66,75.74 C 60.22,76.76 70.92,70.93 79.92,60.09 C 96.71,39.08 117.29,28.48 142.28,26.41 Z" fill="url(#paint1_linear_143_2721)"/>
4
+ <path d="M 200.43,201.63 C 191.8,202.29 184.71,209.18 184.81,214.81 C 184.92,221.44 192.42,226.56 200.01,226.51 C 207.91,226.46 214.46,219.12 214.46,212.93 C 214.46,206.04 207.26,201.15 200.43,201.63 Z" fill="url(#paint2_radial_143_2721)"/>
5
+ <path d="M 241.12,150.21 C 239.69,141.98 232.86,137.88 223.71,139.26 C 210.71,141.38 201.61,157.41 194.78,166.13 C 204.11,159.65 209.17,151.7 222.34,150.1 C 231.13,149.02 237.01,155.06 238.76,156.24 C 240.61,157.46 241.69,153.81 241.12,150.21 Z" fill="url(#paint3_radial_143_2721)"/>
6
+ <path d="M 42.41,80.18 C 33.13,77.01 21.04,78.71 13.84,87.91 C 9.01,94.05 9.49,97.49 11.39,97.65 C 13.73,97.86 16.02,94.16 20.22,90.11 C 26.88,83.64 33.33,82.89 42.41,80.18 Z" fill="url(#paint4_radial_143_2721)"/>
7
+ <path d="M 61.82,142.19 C 55.22,138.19 41.23,143.21 33.74,154.11 C 28.73,161.16 28.16,170.04 33.28,172.1 C 38.69,174.31 46.19,165.86 51.07,160.5 C 56.79,154.21 66.02,144.9 61.82,142.19 Z" fill="url(#paint5_radial_143_2721)"/>
8
+ <path d="M 111.11,86.68 C 105.39,86.68 103.96,91.68 103.96,93.74 C 103.96,99.01 108.01,101.51 111.11,101.51 C 116.98,101.51 119.09,96.87 119.09,93.95 C 119.09,90.06 116.15,86.68 111.11,86.68 Z" fill="url(#paint6_radial_143_2721)"/>
9
+ <path d="M 166.45,86.68 C 160.26,86.68 157.92,91.84 157.92,93.95 C 157.92,99.22 162.02,101.62 165.68,101.62 C 171.09,101.62 173.64,97.13 173.64,93.95 C 173.64,89.85 170.4,86.68 166.45,86.68 Z" fill="url(#paint7_radial_143_2721)"/>
10
+ <path d="M 146.82,99.06 C 143.88,101.83 141.49,102.04 138.79,102.04 C 135.44,102.04 133.16,100.91 130.82,99.06 C 128.81,97.54 126.96,100.76 128.19,102.14 C 130.58,104.96 134.58,106.56 138.79,106.56 C 143.31,106.56 146.82,104.76 148.99,102.14 C 150.32,100.55 148.62,97.33 146.82,99.06 Z" fill="url(#paint8_radial_143_2721)"/>
11
+ <path d="M 199.81,204.07 C 194.82,205.25 191.83,208.94 192.25,211.06 C 192.82,213.88 197.81,213.61 200.41,212.69 C 205.08,211.05 207.15,208.42 206.73,206.41 C 206.31,204.67 203.71,203.29 199.81,204.07 Z" fill="url(#paint9_radial_143_2721)"/>
12
+ <defs>
13
+ <radialGradient id="paint0_radial_143_2721" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(109.896 49.9071) scale(193.071 191.493)">
14
+ <stop stop-color="#00FFFF"/>
15
+ <stop offset="0.354167" stop-color="#9933FF"/>
16
+ <stop offset="0.692708" stop-color="#FE336F"/>
17
+ <stop offset="1" stop-color="#FF7777"/>
18
+ </radialGradient>
19
+ <linearGradient id="paint1_linear_143_2721" x1="113.903" y1="26.3556" x2="113.903" y2="75.8114" gradientUnits="userSpaceOnUse">
20
+ <stop stop-color="#00FFFF"/>
21
+ <stop offset="1" stop-color="#00FFFF" stop-opacity="0.01"/>
22
+ </linearGradient>
23
+ <radialGradient id="paint2_radial_143_2721" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(200.253 209.179) scale(19.3566 17.7296)">
24
+ <stop stop-color="#FFCDBB"/>
25
+ <stop offset="0.994792" stop-color="#FF5555"/>
26
+ </radialGradient>
27
+ <radialGradient id="paint3_radial_143_2721" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(227.953 148.408) rotate(16.5911) scale(18.3468 12.2944)">
28
+ <stop stop-color="#FFD9C5"/>
29
+ <stop offset="1" stop-color="#FFD9C5" stop-opacity="0.01"/>
30
+ </radialGradient>
31
+ <radialGradient id="paint4_radial_143_2721" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(23.7314 86.0908) rotate(29.0072) scale(15.4089 9.09911)">
32
+ <stop stop-color="#99FFFF"/>
33
+ <stop offset="1" stop-color="#00CCFF" stop-opacity="0.01"/>
34
+ </radialGradient>
35
+ <radialGradient id="paint5_radial_143_2721" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(46.4881 153.877) rotate(37.8519) scale(17.2361 10.9818)">
36
+ <stop stop-color="#9999FF"/>
37
+ <stop offset="1" stop-color="#9999FF" stop-opacity="0.01"/>
38
+ </radialGradient>
39
+ <radialGradient id="paint6_radial_143_2721" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(111.098 93.3764) scale(9.10161 8.12939)">
40
+ <stop stop-color="#1E1240"/>
41
+ <stop offset="1" stop-color="#0A003B"/>
42
+ </radialGradient>
43
+ <radialGradient id="paint7_radial_143_2721" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(165.666 93.3764) scale(9.10161 8.12939)">
44
+ <stop stop-color="#1E1240"/>
45
+ <stop offset="1" stop-color="#0A003B"/>
46
+ </radialGradient>
47
+ <radialGradient id="paint8_radial_143_2721" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(138.658 101.292) scale(10.0069 5.26581)">
48
+ <stop stop-color="#1E1240"/>
49
+ <stop offset="1" stop-color="#0A003B"/>
50
+ </radialGradient>
51
+ <radialGradient id="paint9_radial_143_2721" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(199.318 208.168) rotate(16.4322) scale(9.07216 5.68359)">
52
+ <stop stop-color="#FFCDBB"/>
53
+ <stop offset="0.994792" stop-color="#FFCDBB" stop-opacity="0.01"/>
54
+ </radialGradient>
55
+ </defs>
56
+ </svg>