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 +1 -1
- package/shared/paths.ts +5 -4
- package/supervisor/file-saver.ts +50 -0
- package/supervisor/fluxy-agent.ts +19 -3
- package/supervisor/index.ts +25 -3
- package/worker/index.ts +2 -2
- package/workspace/client/public/fluxy-icon-192.png +0 -0
- package/workspace/client/public/fluxy-icon-512.png +0 -0
- package/worker/file-storage.ts +0 -25
package/package.json
CHANGED
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(
|
|
16
|
-
filesAudio: path.join(
|
|
17
|
-
filesImages: path.join(
|
|
18
|
-
filesDocuments: path.join(
|
|
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
|
-
|
|
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) :
|
|
112
|
+
attachments?.length ? buildMultiPartPrompt(prompt, attachments, savedFiles) : plainPrompt;
|
|
97
113
|
|
|
98
114
|
try {
|
|
99
115
|
const claudeQuery = query({
|
package/supervisor/index.ts
CHANGED
|
@@ -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
|
|
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
|
|
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 '
|
|
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()));
|
|
Binary file
|
|
Binary file
|
package/worker/file-storage.ts
DELETED
|
@@ -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
|
-
}
|