fluxy-bot 0.5.1 → 0.5.2

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.5.1",
3
+ "version": "0.5.2",
4
4
  "description": "Self-hosted, self-evolving AI agent with its own dashboard.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -14,6 +14,11 @@ interface Props {
14
14
  audioData?: string;
15
15
  attachments?: StoredAttachment[];
16
16
  onImageClick?: (images: string[], index: number) => void;
17
+ notification?: {
18
+ source: string;
19
+ title?: string;
20
+ priority?: string;
21
+ };
17
22
  }
18
23
 
19
24
  function formatTime(iso: string): string {
@@ -25,7 +30,7 @@ function formatTime(iso: string): string {
25
30
  }
26
31
  }
27
32
 
28
- export default function MessageBubble({ role, content, timestamp, hasAttachments, audioData, attachments, onImageClick }: Props) {
33
+ export default function MessageBubble({ role, content, timestamp, hasAttachments, audioData, attachments, onImageClick, notification }: Props) {
29
34
  const isUser = role === 'user';
30
35
  const time = timestamp ? formatTime(timestamp) : '';
31
36
 
@@ -90,6 +95,52 @@ export default function MessageBubble({ role, content, timestamp, hasAttachments
90
95
  );
91
96
  }
92
97
 
98
+ // Notification bubble (autonomous pulse/cron messages)
99
+ if (notification) {
100
+ const isHigh = notification.priority === 'high';
101
+ const sourceLabel = notification.source === 'pulse' ? 'PULSE' : `CRON`;
102
+ const sourceIcon = notification.source === 'pulse' ? '\u{1F514}' : '\u23F0';
103
+
104
+ return (
105
+ <div className="flex flex-col items-start gap-0.5">
106
+ <div
107
+ className={`min-w-0 rounded-2xl px-4 py-2.5 text-sm leading-relaxed prose prose-sm prose-invert max-w-none [&>*:first-child]:mt-0 [&>*:last-child]:mb-0 break-words overflow-hidden ${
108
+ isHigh
109
+ ? 'bg-amber-500/15 border border-amber-500/30 text-foreground'
110
+ : 'bg-blue-500/10 border border-blue-500/20 text-foreground'
111
+ }`}
112
+ style={{ maxWidth: '92%', overflowWrap: 'anywhere' }}
113
+ >
114
+ <div className={`flex items-center gap-1.5 mb-1.5 text-xs font-medium ${isHigh ? 'text-amber-400' : 'text-blue-400'}`}>
115
+ <span>{sourceIcon}</span>
116
+ <span>{sourceLabel}</span>
117
+ {notification.title && <span className="text-muted-foreground">— {notification.title}</span>}
118
+ </div>
119
+ <ReactMarkdown
120
+ remarkPlugins={[remarkGfm]}
121
+ components={{
122
+ pre({ children }) { return <pre className="overflow-x-auto my-2">{children}</pre>; },
123
+ code({ className, children, ...props }) {
124
+ const match = /language-(\w+)/.exec(className || '');
125
+ const code = String(children).replace(/\n$/, '');
126
+ if (match) {
127
+ return <SyntaxHighlighter style={oneDark} language={match[1]} PreTag="div" customStyle={{ margin: '0.5rem 0', borderRadius: '0.5rem', fontSize: '0.8rem', overflowX: 'auto' }}>{code}</SyntaxHighlighter>;
128
+ }
129
+ return <code className="bg-white/10 rounded px-1 py-0.5 text-[0.8rem] break-all" style={{ whiteSpace: 'pre-wrap', overflowWrap: 'anywhere' }} {...props}>{children}</code>;
130
+ },
131
+ p({ children }) { return <p className="my-1.5">{children}</p>; },
132
+ ul({ children }) { return <ul className="my-1.5 pl-4 list-disc">{children}</ul>; },
133
+ ol({ children }) { return <ol className="my-1.5 pl-4 list-decimal">{children}</ol>; },
134
+ }}
135
+ >
136
+ {content}
137
+ </ReactMarkdown>
138
+ </div>
139
+ {time && <span className="text-[10px] text-muted-foreground/50 px-1">{time}</span>}
140
+ </div>
141
+ );
142
+ }
143
+
93
144
  return (
94
145
  <div className="flex flex-col items-start gap-0.5">
95
146
  <div className="min-w-0 rounded-2xl px-4 py-2.5 text-sm leading-relaxed bg-muted text-foreground prose prose-sm prose-invert max-w-none [&>*:first-child]:mt-0 [&>*:last-child]:mb-0 break-words overflow-hidden" style={{ maxWidth: '92%', overflowWrap: 'anywhere' }}>
@@ -49,6 +49,7 @@ export default function MessageList({ messages, streaming, streamBuffer, tools }
49
49
  audioData={msg.audioData}
50
50
  attachments={msg.attachments}
51
51
  onImageClick={handleImageClick}
52
+ notification={msg.notification}
52
53
  />
53
54
  ))}
54
55
 
@@ -16,6 +16,11 @@ export interface ChatMessage {
16
16
  hasAttachments?: boolean;
17
17
  audioData?: string; // data URL or HTTP URL for voice messages
18
18
  attachments?: StoredAttachment[];
19
+ notification?: {
20
+ source: string; // 'pulse' or cron id
21
+ title?: string;
22
+ priority?: string; // 'high', 'normal', etc.
23
+ };
19
24
  }
20
25
 
21
26
  export interface ToolActivity {
@@ -166,6 +171,23 @@ export function useChat(ws: WsClient | null) {
166
171
  },
167
172
  ]);
168
173
  }),
174
+ // Autonomous agent notifications (pulse/cron)
175
+ ws.on('bot:autonomous-message', (data: { content: string; source: string; title?: string; priority?: string; timestamp: string }) => {
176
+ setMessages((msgs) => [
177
+ ...msgs,
178
+ {
179
+ id: `notif-${Date.now()}`,
180
+ role: 'assistant',
181
+ content: data.content,
182
+ timestamp: data.timestamp,
183
+ notification: {
184
+ source: data.source,
185
+ title: data.title,
186
+ priority: data.priority,
187
+ },
188
+ },
189
+ ]);
190
+ }),
169
191
  ];
170
192
 
171
193
  return () => unsubs.forEach((u) => u());
@@ -123,6 +123,23 @@ export function useFluxyChat(ws: WsClient | null, triggerReload?: number, enable
123
123
  },
124
124
  ]);
125
125
  }),
126
+ // Autonomous agent notifications (pulse/cron)
127
+ ws.on('bot:autonomous-message', (data: { content: string; source: string; title?: string; priority?: string; timestamp: string }) => {
128
+ setMessages((msgs) => [
129
+ ...msgs,
130
+ {
131
+ id: `notif-${Date.now()}`,
132
+ role: 'assistant',
133
+ content: data.content,
134
+ timestamp: data.timestamp,
135
+ notification: {
136
+ source: data.source,
137
+ title: data.title,
138
+ priority: data.priority,
139
+ },
140
+ },
141
+ ]);
142
+ }),
126
143
  // Cross-device sync: append message from another client
127
144
  ws.on('chat:sync', (data: { conversationId: string; message: { role: string; content: string; timestamp: string } }) => {
128
145
  if (conversationId && data.conversationId !== conversationId) return;
@@ -121,35 +121,52 @@ function triggerAgent(prompt: string, label: string) {
121
121
  log.warn(`[scheduler] Failed to create conversation for ${label}: ${err.message}`);
122
122
  }
123
123
 
124
+ let fullResponse = '';
125
+
124
126
  startFluxyAgentQuery(convId, prompt, model, (type, eventData) => {
125
127
  if (type === 'bot:response') {
126
- // Scan for <Message>...</Message> tags
127
- const messageRegex = /<Message>([\s\S]*?)<\/Message>/g;
128
- let match;
129
- while ((match = messageRegex.exec(eventData.content)) !== null) {
130
- const messageContent = match[1].trim();
131
- log.info(`[scheduler] Agent message: ${messageContent.slice(0, 80)}...`);
132
- broadcastFluxy('bot:autonomous-message', {
133
- content: messageContent,
134
- source: label,
135
- timestamp: new Date().toISOString(),
136
- });
137
- // TODO: push notification for PWA
138
- }
139
-
140
- // Persist bot response to DB
141
- if (dbConvId) {
142
- workerApi(`/api/conversations/${dbConvId}/messages`, 'POST', {
143
- role: 'assistant',
144
- content: eventData.content,
145
- meta: { model, source: label },
146
- }).catch((err: any) => {
147
- log.warn(`[scheduler] DB persist error: ${err.message}`);
148
- });
149
- }
128
+ fullResponse = eventData.content || '';
150
129
  }
151
130
 
152
131
  if (type === 'bot:done') {
132
+ // Extract <Message> blocks after agent turn completes
133
+ if (fullResponse) {
134
+ const messageRegex = /<Message(?:\s+([^>]*))?>(([\s\S]*?))<\/Message>/g;
135
+ let match;
136
+ while ((match = messageRegex.exec(fullResponse)) !== null) {
137
+ const attrs = match[1] || '';
138
+ const messageContent = match[2].trim();
139
+
140
+ // Parse optional title and priority attributes
141
+ const titleMatch = attrs.match(/title="([^"]*)"/);
142
+ const priorityMatch = attrs.match(/priority="([^"]*)"/);
143
+
144
+ log.info(`[scheduler] Agent message: ${messageContent.slice(0, 80)}...`);
145
+ broadcastFluxy('bot:autonomous-message', {
146
+ content: messageContent,
147
+ source: label,
148
+ title: titleMatch?.[1],
149
+ priority: priorityMatch?.[1],
150
+ timestamp: new Date().toISOString(),
151
+ });
152
+ // TODO: push notification for PWA
153
+ }
154
+
155
+ // Strip <Message> blocks from stored response
156
+ const cleanedResponse = fullResponse.replace(/<Message(?:\s+[^>]*)?>[\s\S]*?<\/Message>/g, '').trim();
157
+
158
+ // Persist cleaned bot response to DB
159
+ if (dbConvId && cleanedResponse) {
160
+ workerApi(`/api/conversations/${dbConvId}/messages`, 'POST', {
161
+ role: 'assistant',
162
+ content: cleanedResponse,
163
+ meta: { model, source: label },
164
+ }).catch((err: any) => {
165
+ log.warn(`[scheduler] DB persist error: ${err.message}`);
166
+ });
167
+ }
168
+ }
169
+
153
170
  log.info(`[scheduler] ${label} agent query complete`);
154
171
  if (eventData.usedFileTools) {
155
172
  log.info(`[scheduler] File tools used — restarting backend`);
@@ -104,7 +104,7 @@ When you receive a `<PULSE/>` tag, it means the background process triggered you
104
104
  2. **Check the workspace.** Any broken routes? Stale data? Code that needs cleanup? Problems you noticed earlier but didn't fix?
105
105
  3. **Be proactive.** Think about what would help or impress your human. Maybe research a topic they mentioned. Maybe organize something messy. Maybe prepare for something you know is coming.
106
106
  4. **Rate importance 0–10** based on what you know about your human:
107
- - **8+**: Send a message immediately using `<Message>Your markdown message here<Message/>`
107
+ - **8+**: Send a message immediately using `<Message>Your markdown message here</Message>`. You can optionally add title and priority: `<Message title="Build Error" priority="high">details here</Message>`
108
108
  - **Below 8**: Note it in your daily memory. Discuss later when they talk to you.
109
109
 
110
110
  **Track what you check** so you don't repeat the same things every pulse. Rotate through different tasks.