fluxy-bot 0.3.21 → 0.3.23

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.3.21",
3
+ "version": "0.3.23",
4
4
  "description": "Self-hosted AI bot — run your own AI assistant from anywhere",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/shared/paths.ts CHANGED
@@ -4,6 +4,7 @@ import { fileURLToPath } from 'url';
4
4
 
5
5
  export const PKG_DIR = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
6
6
  export const DATA_DIR = path.join(os.homedir(), '.fluxy');
7
+ export const WORKSPACE_DIR = path.join(PKG_DIR, 'workspace');
7
8
 
8
9
  const cfName = process.platform === 'win32' ? 'cloudflared.exe' : 'cloudflared';
9
10
 
@@ -12,8 +13,8 @@ export const paths = {
12
13
  db: path.join(DATA_DIR, 'memory.db'),
13
14
  widgetJs: path.join(PKG_DIR, 'supervisor', 'widget.js'),
14
15
  cloudflared: path.join(DATA_DIR, 'bin', cfName),
15
- files: path.join(DATA_DIR, 'files'),
16
- filesAudio: path.join(DATA_DIR, 'files', 'audio'),
17
- filesImages: path.join(DATA_DIR, 'files', 'images'),
18
- filesDocuments: path.join(DATA_DIR, 'files', 'documents'),
16
+ files: path.join(WORKSPACE_DIR, 'files'),
17
+ filesAudio: path.join(WORKSPACE_DIR, 'files', 'audio'),
18
+ filesImages: path.join(WORKSPACE_DIR, 'files', 'images'),
19
+ filesDocuments: path.join(WORKSPACE_DIR, 'files', 'documents'),
19
20
  };
@@ -0,0 +1,50 @@
1
+ import fs from 'fs';
2
+ import crypto from 'crypto';
3
+ import { paths } from '../shared/paths.js';
4
+
5
+ export interface SavedFile {
6
+ type: 'image' | 'document';
7
+ name: string;
8
+ mediaType: string;
9
+ relPath: string;
10
+ absPath: string;
11
+ }
12
+
13
+ export function ensureFileDirs(): void {
14
+ fs.mkdirSync(paths.filesAudio, { recursive: true });
15
+ fs.mkdirSync(paths.filesImages, { recursive: true });
16
+ fs.mkdirSync(paths.filesDocuments, { recursive: true });
17
+ }
18
+
19
+ export function saveAttachment(att: { type: 'image' | 'file'; name: string; mediaType: string; data: string }): SavedFile {
20
+ const category = att.type === 'image' ? 'images' : 'documents';
21
+
22
+ const now = new Date();
23
+ const ts = now.toISOString().replace(/[-:T]/g, '').slice(0, 14);
24
+ const stamp = `${ts.slice(0, 8)}_${ts.slice(8, 14)}`;
25
+ const rand = crypto.randomBytes(3).toString('hex');
26
+
27
+ // Extract extension from original name or mediaType
28
+ const extFromName = att.name?.includes('.') ? att.name.split('.').pop()! : '';
29
+ const extFromMime: Record<string, string> = {
30
+ 'image/png': 'png', 'image/jpeg': 'jpg', 'image/gif': 'gif', 'image/webp': 'webp',
31
+ 'application/pdf': 'pdf', 'text/plain': 'txt', 'text/csv': 'csv',
32
+ };
33
+ const ext = extFromName || extFromMime[att.mediaType] || 'bin';
34
+
35
+ const filename = `${stamp}_${rand}.${ext}`;
36
+ const relPath = `files/${category}/${filename}`;
37
+
38
+ const dir = category === 'images' ? paths.filesImages : paths.filesDocuments;
39
+ const absPath = `${dir}/${filename}`;
40
+
41
+ fs.writeFileSync(absPath, Buffer.from(att.data, 'base64'));
42
+
43
+ return {
44
+ type: att.type === 'image' ? 'image' : 'document',
45
+ name: att.name,
46
+ mediaType: att.mediaType,
47
+ relPath,
48
+ absPath,
49
+ };
50
+ }
@@ -8,6 +8,7 @@ import fs from 'fs';
8
8
  import path from 'path';
9
9
  import { log } from '../shared/logger.js';
10
10
  import { DATA_DIR, PKG_DIR } from '../shared/paths.js';
11
+ import type { SavedFile } from './file-saver.js';
11
12
  import { getClaudeAccessToken } from '../worker/claude-auth.js';
12
13
 
13
14
  const PROMPT_FILE = path.join(import.meta.dirname, '..', 'worker', 'prompts', 'fluxy-system-prompt.txt');
@@ -29,7 +30,7 @@ export interface AgentAttachment {
29
30
  }
30
31
 
31
32
  /** Build a multi-part prompt with attachments for the SDK */
32
- function buildMultiPartPrompt(text: string, attachments: AgentAttachment[]): AsyncIterable<SDKUserMessage> {
33
+ function buildMultiPartPrompt(text: string, attachments: AgentAttachment[], savedFiles?: SavedFile[]): AsyncIterable<SDKUserMessage> {
33
34
  return (async function* () {
34
35
  const content: any[] = [];
35
36
 
@@ -47,7 +48,14 @@ function buildMultiPartPrompt(text: string, attachments: AgentAttachment[]): Asy
47
48
  }
48
49
  }
49
50
 
50
- content.push({ type: 'text', text: text || '(attached files)' });
51
+ // Append local file paths so Claude can reference them with tools
52
+ let promptText = text || '(attached files)';
53
+ if (savedFiles?.length) {
54
+ const lines = savedFiles.map((f) => `- ${f.name} -> ${f.relPath}`);
55
+ promptText += `\n\n[Attached files saved to disk]\n${lines.join('\n')}\nYou can read or reference these files using the paths above (relative to your cwd).`;
56
+ }
57
+
58
+ content.push({ type: 'text', text: promptText });
51
59
 
52
60
  yield {
53
61
  type: 'user' as const,
@@ -76,6 +84,7 @@ export async function startFluxyAgentQuery(
76
84
  model: string,
77
85
  onMessage: (type: string, data: any) => void,
78
86
  attachments?: AgentAttachment[],
87
+ savedFiles?: SavedFile[],
79
88
  ): Promise<void> {
80
89
  const oauthToken = await getClaudeAccessToken();
81
90
  if (!oauthToken) {
@@ -92,8 +101,15 @@ export async function startFluxyAgentQuery(
92
101
  let fullText = '';
93
102
  const usedTools = new Set<string>();
94
103
 
104
+ // If there are saved files but no inline attachments, append path info to plain text prompt
105
+ let plainPrompt = prompt;
106
+ if (savedFiles?.length && !attachments?.length) {
107
+ const lines = savedFiles.map((f) => `- ${f.name} -> ${f.relPath}`);
108
+ plainPrompt += `\n\n[Attached files saved to disk]\n${lines.join('\n')}\nYou can read or reference these files using the paths above (relative to your cwd).`;
109
+ }
110
+
95
111
  const sdkPrompt: string | AsyncIterable<SDKUserMessage> =
96
- attachments?.length ? buildMultiPartPrompt(prompt, attachments) : prompt;
112
+ attachments?.length ? buildMultiPartPrompt(prompt, attachments, savedFiles) : plainPrompt;
97
113
 
98
114
  try {
99
115
  const claudeQuery = query({
@@ -13,6 +13,7 @@ import { spawnWorker, stopWorker, getWorkerPort, isWorkerAlive } from './worker.
13
13
  import { spawnBackend, stopBackend, getBackendPort, isBackendAlive, resetBackendRestarts } from './backend.js';
14
14
  import { updateTunnelUrl, startHeartbeat, stopHeartbeat, disconnect } from '../shared/relay.js';
15
15
  import { startFluxyAgentQuery, stopFluxyAgentQuery, clearFluxySession } from './fluxy-agent.js';
16
+ import { ensureFileDirs, saveAttachment, type SavedFile } from './file-saver.js';
16
17
  import { startViteDevServers, stopViteDevServers } from './vite-dev.js';
17
18
  import { execSync } from 'child_process';
18
19
 
@@ -61,6 +62,9 @@ export async function startSupervisor() {
61
62
  const vitePorts = await startViteDevServers(config.port);
62
63
  console.log(`[supervisor] Vite ready — dashboard :${vitePorts.dashboard}`);
63
64
 
65
+ // Ensure file storage dirs exist
66
+ ensureFileDirs();
67
+
64
68
  // Fluxy's AI brain
65
69
  let ai: AiProvider | null = null;
66
70
  if (config.ai.provider && (config.ai.apiKey || config.ai.provider === 'ollama')) {
@@ -380,9 +384,27 @@ export async function startSupervisor() {
380
384
  }
381
385
  convId = dbConvId!;
382
386
 
383
- // Save user message to DB
387
+ // Save attachments to disk
388
+ let savedFiles: SavedFile[] = [];
389
+ if (data.attachments?.length) {
390
+ for (const att of data.attachments) {
391
+ try {
392
+ savedFiles.push(saveAttachment(att));
393
+ } catch (err: any) {
394
+ log.warn(`[fluxy] File save error: ${err.message}`);
395
+ }
396
+ }
397
+ }
398
+
399
+ // Save user message to DB (include attachment metadata)
400
+ const meta: any = { model: freshConfig.ai.model };
401
+ if (savedFiles.length) {
402
+ meta.attachments = savedFiles.map((f) => ({
403
+ type: f.type, name: f.name, mediaType: f.mediaType, path: f.relPath,
404
+ }));
405
+ }
384
406
  await workerApi(`/api/conversations/${convId}/messages`, 'POST', {
385
- role: 'user', content, meta: { model: freshConfig.ai.model },
407
+ role: 'user', content, meta,
386
408
  });
387
409
 
388
410
  // Broadcast user message to other clients
@@ -430,7 +452,7 @@ export async function startSupervisor() {
430
452
  if (ws.readyState === WebSocket.OPEN) {
431
453
  ws.send(JSON.stringify({ type, data: eventData }));
432
454
  }
433
- }, data.attachments);
455
+ }, data.attachments, savedFiles);
434
456
  })();
435
457
  return;
436
458
  }
package/worker/index.ts CHANGED
@@ -7,7 +7,7 @@ import { initDb, closeDb, listConversations, createConversation, deleteConversat
7
7
  import { startCodexOAuth, cancelCodexOAuth, getCodexAuthStatus, readCodexAccessToken } from './codex-auth.js';
8
8
  import { startClaudeOAuth, exchangeClaudeCode, getClaudeAuthStatus, readClaudeAccessToken } from './claude-auth.js';
9
9
  import { checkAvailability, registerHandle, releaseHandle, updateTunnelUrl, startHeartbeat, stopHeartbeat } from '../shared/relay.js';
10
- import { ensureFileDirs } from './file-storage.js';
10
+ import { ensureFileDirs } from '../supervisor/file-saver.js';
11
11
 
12
12
  // ── Password hashing (scrypt) ──
13
13
 
@@ -34,7 +34,7 @@ ensureFileDirs();
34
34
 
35
35
  // Express
36
36
  const app = express();
37
- app.use(express.json());
37
+ app.use(express.json({ limit: '10mb' }));
38
38
 
39
39
  app.get('/api/health', (_, res) => res.json({ status: 'ok' }));
40
40
  app.get('/api/conversations', (_, res) => res.json(listConversations()));
@@ -1,25 +0,0 @@
1
- import fs from 'fs';
2
- import crypto from 'crypto';
3
- import { paths } from '../shared/paths.js';
4
-
5
- export function ensureFileDirs(): void {
6
- fs.mkdirSync(paths.filesAudio, { recursive: true });
7
- fs.mkdirSync(paths.filesImages, { recursive: true });
8
- fs.mkdirSync(paths.filesDocuments, { recursive: true });
9
- }
10
-
11
- export function saveFile(base64: string, category: 'audio' | 'images' | 'documents', ext: string): string {
12
- const now = new Date();
13
- const ts = now.toISOString().replace(/[-:T]/g, '').slice(0, 14); // YYYYMMDD_HHmmss → YYYYMMDDHHmmss
14
- const stamp = `${ts.slice(0, 8)}_${ts.slice(8, 14)}`;
15
- const rand = crypto.randomBytes(3).toString('hex');
16
- const filename = `${stamp}_${rand}.${ext}`;
17
- const relPath = `${category}/${filename}`;
18
-
19
- const dir = category === 'audio' ? paths.filesAudio
20
- : category === 'images' ? paths.filesImages
21
- : paths.filesDocuments;
22
-
23
- fs.writeFileSync(`${dir}/${filename}`, Buffer.from(base64, 'base64'));
24
- return relPath;
25
- }