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.
- package/README.md +53 -84
- package/bin/cli.js +1717 -6
- package/components.json +18 -18
- package/package.json +98 -109
- package/postcss.config.js +3 -3
- package/scripts/install.ps1 +1 -1
- package/scripts/install.sh +1 -1
- package/scripts/postinstall.js +47 -58
- package/shared/ai.ts +118 -202
- package/shared/config.ts +37 -39
- package/shared/logger.ts +6 -14
- package/shared/paths.ts +10 -16
- package/shared/relay.ts +89 -98
- package/supervisor/backend.ts +77 -89
- package/supervisor/chat/ARCHITECTURE.md +13 -14
- package/supervisor/chat/OnboardWizard.tsx +2194 -3419
- package/supervisor/chat/fluxy-main.tsx +495 -633
- package/supervisor/chat/fluxy.html +24 -36
- package/supervisor/chat/onboard-main.tsx +8 -8
- package/supervisor/chat/onboard.html +10 -13
- package/supervisor/chat/src/components/Chat/AudioBubble.tsx +94 -108
- package/supervisor/chat/src/components/Chat/ChatView.tsx +14 -36
- package/supervisor/chat/src/components/Chat/ImageLightbox.tsx +71 -89
- package/supervisor/chat/src/components/Chat/InputBar.tsx +498 -728
- package/supervisor/chat/src/components/Chat/MessageBubble.tsx +154 -246
- package/supervisor/chat/src/components/Chat/MessageList.tsx +114 -138
- package/supervisor/chat/src/components/Chat/TypingIndicator.tsx +38 -50
- package/supervisor/chat/src/components/LoginScreen.tsx +245 -296
- package/supervisor/chat/src/hooks/useChat.ts +210 -259
- package/supervisor/chat/src/hooks/useFluxyChat.ts +232 -322
- package/supervisor/chat/src/hooks/useSpeechRecognition.ts +126 -148
- package/supervisor/chat/src/lib/auth.ts +20 -23
- package/supervisor/chat/src/lib/ws-client.ts +113 -125
- package/supervisor/chat/src/styles/globals.css +76 -85
- package/supervisor/file-saver.ts +39 -52
- package/supervisor/fluxy-agent.ts +213 -292
- package/supervisor/index.ts +895 -1263
- package/supervisor/scheduler.ts +237 -299
- package/supervisor/tunnel.ts +113 -167
- package/supervisor/vite-dev.ts +51 -61
- package/supervisor/widget.js +135 -147
- package/supervisor/worker.ts +45 -49
- package/tsconfig.json +18 -24
- package/vite.config.ts +48 -55
- package/vite.fluxy.config.ts +34 -34
- package/worker/claude-auth.ts +202 -265
- package/worker/codex-auth.ts +144 -181
- package/worker/db.ts +63 -191
- package/worker/index.ts +601 -842
- package/workspace/MYSELF.md +1 -1
- package/workspace/PULSE.json +6 -6
- package/workspace/backend/index.ts +16 -19
- package/workspace/client/index.html +47 -60
- package/workspace/client/public/manifest.json +21 -21
- package/workspace/client/public/sw.js +29 -31
- package/workspace/client/src/App.tsx +91 -136
- package/workspace/client/src/components/Dashboard/DashboardPage.tsx +68 -72
- package/workspace/client/src/components/ErrorBoundary.tsx +11 -11
- package/workspace/client/src/components/Layout/DashboardLayout.tsx +35 -35
- package/workspace/client/src/components/Layout/Footer.tsx +12 -12
- package/workspace/client/src/components/Layout/MobileNav.tsx +23 -23
- package/workspace/client/src/components/Layout/Sidebar.tsx +81 -81
- package/workspace/client/src/components/ui/avatar.tsx +85 -85
- package/workspace/client/src/components/ui/badge.tsx +40 -41
- package/workspace/client/src/components/ui/button.tsx +56 -57
- package/workspace/client/src/components/ui/card.tsx +75 -75
- package/workspace/client/src/components/ui/dialog.tsx +114 -117
- package/workspace/client/src/components/ui/dropdown-menu.tsx +191 -199
- package/workspace/client/src/components/ui/input.tsx +17 -17
- package/workspace/client/src/components/ui/scroll-area.tsx +47 -47
- package/workspace/client/src/components/ui/select.tsx +145 -151
- package/workspace/client/src/components/ui/separator.tsx +21 -21
- package/workspace/client/src/components/ui/sheet.tsx +102 -102
- package/workspace/client/src/components/ui/skeleton.tsx +10 -10
- package/workspace/client/src/components/ui/switch.tsx +26 -26
- package/workspace/client/src/components/ui/tabs.tsx +69 -69
- package/workspace/client/src/components/ui/textarea.tsx +14 -14
- package/workspace/client/src/components/ui/tooltip.tsx +37 -37
- package/workspace/client/src/lib/utils.ts +1 -1
- package/workspace/client/src/main.tsx +3 -3
- package/workspace/client/src/styles/globals.css +76 -85
- package/workspace/skills/code-reviewer/.claude-plugin/plugin.json +3 -3
- package/workspace/skills/code-reviewer/skills/code-reviewer/SKILL.md +0 -6
- package/workspace/skills/daily-standup/.claude-plugin/plugin.json +3 -3
- package/workspace/skills/daily-standup/skills/daily-standup/SKILL.md +0 -7
- package/workspace/skills/workspace-helper/.claude-plugin/plugin.json +3 -3
- package/workspace/skills/workspace-helper/skills/workspace-helper/SKILL.md +0 -2
- package/bin/cli.backup.js +0 -2138
- package/cli/commands/daemon.ts +0 -42
- package/cli/commands/init.ts +0 -32
- package/cli/commands/start.ts +0 -113
- package/cli/commands/tunnel.temp.ts +0 -206
- package/cli/commands/tunnel.ts +0 -227
- package/cli/commands/update.ts +0 -163
- package/cli/core/base-adapter.ts +0 -156
- package/cli/core/cloudflared.ts +0 -113
- package/cli/core/config.ts +0 -73
- package/cli/core/os-detector.ts +0 -43
- package/cli/core/server.ts +0 -109
- package/cli/core/types.ts +0 -15
- package/cli/index.ts +0 -72
- package/cli/platforms/darwin.ts +0 -146
- package/cli/platforms/index.ts +0 -21
- package/cli/platforms/linux.ts +0 -156
- package/cli/platforms/win32.ts +0 -34
- package/cli/utils/ui.ts +0 -37
|
@@ -3,11 +3,7 @@
|
|
|
3
3
|
* Fresh context per turn — memory files and conversation history are injected into the system prompt.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import {
|
|
7
|
-
query,
|
|
8
|
-
type SDKMessage,
|
|
9
|
-
type SDKUserMessage
|
|
10
|
-
} from '@anthropic-ai/claude-agent-sdk';
|
|
6
|
+
import { query, type SDKMessage, type SDKUserMessage } from '@anthropic-ai/claude-agent-sdk';
|
|
11
7
|
import fs from 'fs';
|
|
12
8
|
import path from 'path';
|
|
13
9
|
import { log } from '../shared/logger.js';
|
|
@@ -15,128 +11,102 @@ import { PKG_DIR, WORKSPACE_DIR } from '../shared/paths.js';
|
|
|
15
11
|
import type { SavedFile } from './file-saver.js';
|
|
16
12
|
import { getClaudeAccessToken } from '../worker/claude-auth.js';
|
|
17
13
|
|
|
18
|
-
const PROMPT_FILE = path.join(
|
|
19
|
-
import.meta.dirname,
|
|
20
|
-
'..',
|
|
21
|
-
'worker',
|
|
22
|
-
'prompts',
|
|
23
|
-
'fluxy-system-prompt.txt'
|
|
24
|
-
);
|
|
14
|
+
const PROMPT_FILE = path.join(import.meta.dirname, '..', 'worker', 'prompts', 'fluxy-system-prompt.txt');
|
|
25
15
|
|
|
26
16
|
export interface RecentMessage {
|
|
27
|
-
|
|
28
|
-
|
|
17
|
+
role: 'user' | 'assistant';
|
|
18
|
+
content: string;
|
|
29
19
|
}
|
|
30
20
|
|
|
31
21
|
interface ActiveQuery {
|
|
32
|
-
|
|
22
|
+
abortController: AbortController;
|
|
33
23
|
}
|
|
34
24
|
|
|
35
25
|
const activeQueries = new Map<string, ActiveQuery>();
|
|
36
26
|
|
|
37
27
|
export interface AgentAttachment {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
28
|
+
type: 'image' | 'file';
|
|
29
|
+
name: string;
|
|
30
|
+
mediaType: string;
|
|
31
|
+
data: string; // base64
|
|
42
32
|
}
|
|
43
33
|
|
|
44
34
|
/** Read a memory file from workspace, returning '(empty)' if missing or empty */
|
|
45
35
|
function readMemoryFile(filename: string): string {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
return '(empty)';
|
|
53
|
-
}
|
|
36
|
+
try {
|
|
37
|
+
const content = fs.readFileSync(path.join(WORKSPACE_DIR, filename), 'utf-8').trim();
|
|
38
|
+
return content || '(empty)';
|
|
39
|
+
} catch {
|
|
40
|
+
return '(empty)';
|
|
41
|
+
}
|
|
54
42
|
}
|
|
55
43
|
|
|
56
44
|
/** Read all memory + config files and return their contents */
|
|
57
|
-
function readMemoryFiles(): {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
myself: readMemoryFile('MYSELF.md'),
|
|
66
|
-
myhuman: readMemoryFile('MYHUMAN.md'),
|
|
67
|
-
memory: readMemoryFile('MEMORY.md'),
|
|
68
|
-
pulse: readMemoryFile('PULSE.json'),
|
|
69
|
-
crons: readMemoryFile('CRONS.json')
|
|
70
|
-
};
|
|
45
|
+
function readMemoryFiles(): { myself: string; myhuman: string; memory: string; pulse: string; crons: string } {
|
|
46
|
+
return {
|
|
47
|
+
myself: readMemoryFile('MYSELF.md'),
|
|
48
|
+
myhuman: readMemoryFile('MYHUMAN.md'),
|
|
49
|
+
memory: readMemoryFile('MEMORY.md'),
|
|
50
|
+
pulse: readMemoryFile('PULSE.json'),
|
|
51
|
+
crons: readMemoryFile('CRONS.json'),
|
|
52
|
+
};
|
|
71
53
|
}
|
|
72
54
|
|
|
73
55
|
/** Format recent messages as conversation history text */
|
|
74
56
|
function formatConversationHistory(messages: RecentMessage[]): string {
|
|
75
|
-
|
|
76
|
-
|
|
57
|
+
if (!messages.length) return '';
|
|
58
|
+
return messages.map((m) => `${m.role}: ${m.content}`).join('\n\n');
|
|
77
59
|
}
|
|
78
60
|
|
|
79
61
|
/** Build a multi-part prompt with attachments for the SDK */
|
|
80
|
-
function buildMultiPartPrompt(
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
)
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
});
|
|
98
|
-
} else {
|
|
99
|
-
content.push({
|
|
100
|
-
type: 'document',
|
|
101
|
-
source: {
|
|
102
|
-
type: 'base64',
|
|
103
|
-
media_type: att.mediaType,
|
|
104
|
-
data: att.data
|
|
105
|
-
}
|
|
106
|
-
});
|
|
107
|
-
}
|
|
108
|
-
}
|
|
62
|
+
function buildMultiPartPrompt(text: string, attachments: AgentAttachment[], savedFiles?: SavedFile[]): AsyncIterable<SDKUserMessage> {
|
|
63
|
+
return (async function* () {
|
|
64
|
+
const content: any[] = [];
|
|
65
|
+
|
|
66
|
+
for (const att of attachments) {
|
|
67
|
+
if (att.type === 'image') {
|
|
68
|
+
content.push({
|
|
69
|
+
type: 'image',
|
|
70
|
+
source: { type: 'base64', media_type: att.mediaType, data: att.data },
|
|
71
|
+
});
|
|
72
|
+
} else {
|
|
73
|
+
content.push({
|
|
74
|
+
type: 'document',
|
|
75
|
+
source: { type: 'base64', media_type: att.mediaType, data: att.data },
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
}
|
|
109
79
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
80
|
+
// Append local file paths so Claude can reference them with tools
|
|
81
|
+
let promptText = text || '(attached files)';
|
|
82
|
+
if (savedFiles?.length) {
|
|
83
|
+
const lines = savedFiles.map((f) => `- ${f.name} -> ${f.relPath}`);
|
|
84
|
+
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).`;
|
|
85
|
+
}
|
|
116
86
|
|
|
117
|
-
|
|
87
|
+
content.push({ type: 'text', text: promptText });
|
|
118
88
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
89
|
+
yield {
|
|
90
|
+
type: 'user' as const,
|
|
91
|
+
message: { role: 'user' as const, content },
|
|
92
|
+
parent_tool_use_id: null,
|
|
93
|
+
} as SDKUserMessage;
|
|
94
|
+
})();
|
|
125
95
|
}
|
|
126
96
|
|
|
127
97
|
/** Read the custom system prompt, replacing $BOT and $HUMAN placeholders */
|
|
128
98
|
function readSystemPrompt(botName = 'Fluxy', humanName = 'Human'): string {
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
}
|
|
135
|
-
return raw.replace(/\$BOT/g, botName).replace(/\$HUMAN/g, humanName);
|
|
136
|
-
} catch {
|
|
137
|
-
log.warn('System prompt file not found — using minimal fallback');
|
|
138
|
-
return `You are ${botName}, a helpful AI agent. Your human is ${humanName}.`;
|
|
99
|
+
try {
|
|
100
|
+
const raw = fs.readFileSync(PROMPT_FILE, 'utf-8').trim();
|
|
101
|
+
if (!raw) {
|
|
102
|
+
log.warn('System prompt file is empty — using minimal fallback');
|
|
103
|
+
return `You are ${botName}, a helpful AI agent. Your human is ${humanName}.`;
|
|
139
104
|
}
|
|
105
|
+
return raw.replace(/\$BOT/g, botName).replace(/\$HUMAN/g, humanName);
|
|
106
|
+
} catch {
|
|
107
|
+
log.warn('System prompt file not found — using minimal fallback');
|
|
108
|
+
return `You are ${botName}, a helpful AI agent. Your human is ${humanName}.`;
|
|
109
|
+
}
|
|
140
110
|
}
|
|
141
111
|
|
|
142
112
|
/**
|
|
@@ -144,223 +114,174 @@ function readSystemPrompt(botName = 'Fluxy', humanName = 'Human'): string {
|
|
|
144
114
|
* Fresh context each turn — memory files and history are injected into the system prompt.
|
|
145
115
|
*/
|
|
146
116
|
export async function startFluxyAgentQuery(
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
117
|
+
conversationId: string,
|
|
118
|
+
prompt: string,
|
|
119
|
+
model: string,
|
|
120
|
+
onMessage: (type: string, data: any) => void,
|
|
121
|
+
attachments?: AgentAttachment[],
|
|
122
|
+
savedFiles?: SavedFile[],
|
|
123
|
+
names?: { botName: string; humanName: string },
|
|
124
|
+
recentMessages?: RecentMessage[],
|
|
155
125
|
): Promise<void> {
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
});
|
|
162
|
-
return;
|
|
163
|
-
}
|
|
126
|
+
const oauthToken = await getClaudeAccessToken();
|
|
127
|
+
if (!oauthToken) {
|
|
128
|
+
onMessage('bot:error', { conversationId, error: 'Claude OAuth token not found. Please authenticate via the dashboard.' });
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
164
131
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
132
|
+
const abortController = new AbortController();
|
|
133
|
+
const basePrompt = readSystemPrompt(names?.botName, names?.humanName);
|
|
134
|
+
const memoryFiles = readMemoryFiles();
|
|
168
135
|
|
|
169
|
-
|
|
170
|
-
|
|
136
|
+
// Build enriched system prompt with memory files and conversation history
|
|
137
|
+
let enrichedPrompt = basePrompt;
|
|
171
138
|
|
|
172
|
-
|
|
139
|
+
enrichedPrompt += `\n\n---\n# Your Memory Files\n\n## MYSELF.md\n${memoryFiles.myself}\n\n## MYHUMAN.md\n${memoryFiles.myhuman}\n\n## MEMORY.md\n${memoryFiles.memory}\n\n---\n# Your Config Files\n\n## PULSE.json\n${memoryFiles.pulse}\n\n## CRONS.json\n${memoryFiles.crons}`;
|
|
173
140
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
141
|
+
if (recentMessages?.length) {
|
|
142
|
+
enrichedPrompt += `\n\n---\n# Recent Conversation\n${formatConversationHistory(recentMessages)}`;
|
|
143
|
+
}
|
|
177
144
|
|
|
178
|
-
|
|
145
|
+
activeQueries.set(conversationId, { abortController });
|
|
179
146
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
147
|
+
let fullText = '';
|
|
148
|
+
const usedTools = new Set<string>();
|
|
149
|
+
let stderrBuf = '';
|
|
183
150
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
151
|
+
// If there are saved files but no inline attachments, append path info to plain text prompt
|
|
152
|
+
let plainPrompt = prompt;
|
|
153
|
+
if (savedFiles?.length && !attachments?.length) {
|
|
154
|
+
const lines = savedFiles.map((f) => `- ${f.name} -> ${f.relPath}`);
|
|
155
|
+
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).`;
|
|
156
|
+
}
|
|
190
157
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
? buildMultiPartPrompt(prompt, attachments, savedFiles)
|
|
194
|
-
: plainPrompt;
|
|
158
|
+
const sdkPrompt: string | AsyncIterable<SDKUserMessage> =
|
|
159
|
+
attachments?.length ? buildMultiPartPrompt(prompt, attachments, savedFiles) : plainPrompt;
|
|
195
160
|
|
|
161
|
+
try {
|
|
162
|
+
// Auto-discover all skill plugins in workspace/skills/ — any folder with a valid plugin.json is loaded
|
|
163
|
+
const skillsDir = path.join(PKG_DIR, 'workspace', 'skills');
|
|
164
|
+
const plugins: { type: 'local'; path: string }[] = [];
|
|
196
165
|
try {
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
})) {
|
|
204
|
-
if (
|
|
205
|
-
entry.isDirectory() &&
|
|
206
|
-
fs.existsSync(
|
|
207
|
-
path.join(
|
|
208
|
-
skillsDir,
|
|
209
|
-
entry.name,
|
|
210
|
-
'.claude-plugin',
|
|
211
|
-
'plugin.json'
|
|
212
|
-
)
|
|
213
|
-
)
|
|
214
|
-
) {
|
|
215
|
-
plugins.push({
|
|
216
|
-
type: 'local' as const,
|
|
217
|
-
path: path.join(skillsDir, entry.name)
|
|
218
|
-
});
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
} catch {}
|
|
222
|
-
|
|
223
|
-
// Load MCP server config from workspace/MCP.json if it exists
|
|
224
|
-
// Format: { "server-name": { command, args, env }, ... } (object, not array)
|
|
225
|
-
let mcpServers: Record<string, any> | undefined;
|
|
226
|
-
try {
|
|
227
|
-
const mcpConfigPath = path.join(WORKSPACE_DIR, 'MCP.json');
|
|
228
|
-
const mcpConfig = JSON.parse(
|
|
229
|
-
fs.readFileSync(mcpConfigPath, 'utf-8')
|
|
230
|
-
);
|
|
231
|
-
if (
|
|
232
|
-
mcpConfig &&
|
|
233
|
-
typeof mcpConfig === 'object' &&
|
|
234
|
-
!Array.isArray(mcpConfig) &&
|
|
235
|
-
Object.keys(mcpConfig).length
|
|
236
|
-
) {
|
|
237
|
-
// Already in correct format: { "name": { command, args, env } }
|
|
238
|
-
mcpServers = mcpConfig;
|
|
239
|
-
} else if (Array.isArray(mcpConfig) && mcpConfig.length) {
|
|
240
|
-
// Legacy array format: merge all entries into a single object
|
|
241
|
-
mcpServers = Object.assign({}, ...mcpConfig);
|
|
242
|
-
}
|
|
243
|
-
if (mcpServers) {
|
|
244
|
-
const names = Object.keys(mcpServers).join(', ');
|
|
245
|
-
log.info(`Loaded MCP server(s): [${names}]`);
|
|
246
|
-
}
|
|
247
|
-
} catch {}
|
|
248
|
-
|
|
249
|
-
const claudeQuery = query({
|
|
250
|
-
prompt: sdkPrompt,
|
|
251
|
-
options: {
|
|
252
|
-
model,
|
|
253
|
-
cwd: WORKSPACE_DIR,
|
|
254
|
-
permissionMode: 'bypassPermissions',
|
|
255
|
-
allowDangerouslySkipPermissions: true,
|
|
256
|
-
maxTurns: 50,
|
|
257
|
-
abortController,
|
|
258
|
-
systemPrompt: enrichedPrompt,
|
|
259
|
-
plugins: plugins.length ? plugins : undefined,
|
|
260
|
-
mcpServers,
|
|
261
|
-
stderr: (chunk: string) => {
|
|
262
|
-
stderrBuf += chunk;
|
|
263
|
-
},
|
|
264
|
-
env: {
|
|
265
|
-
...(process.env as Record<string, string>),
|
|
266
|
-
CLAUDE_CODE_OAUTH_TOKEN: oauthToken,
|
|
267
|
-
CLAUDE_CODE_BUBBLEWRAP: '1'
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
});
|
|
166
|
+
for (const entry of fs.readdirSync(skillsDir, { withFileTypes: true })) {
|
|
167
|
+
if (entry.isDirectory() && fs.existsSync(path.join(skillsDir, entry.name, '.claude-plugin', 'plugin.json'))) {
|
|
168
|
+
plugins.push({ type: 'local' as const, path: path.join(skillsDir, entry.name) });
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
} catch {}
|
|
271
172
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
break;
|
|
173
|
+
// Load MCP server config from workspace/MCP.json if it exists
|
|
174
|
+
// Format: { "server-name": { command, args, env }, ... } (object, not array)
|
|
175
|
+
let mcpServers: Record<string, any> | undefined;
|
|
176
|
+
try {
|
|
177
|
+
const mcpConfigPath = path.join(WORKSPACE_DIR, 'MCP.json');
|
|
178
|
+
const mcpConfig = JSON.parse(fs.readFileSync(mcpConfigPath, 'utf-8'));
|
|
179
|
+
if (mcpConfig && typeof mcpConfig === 'object' && !Array.isArray(mcpConfig) && Object.keys(mcpConfig).length) {
|
|
180
|
+
// Already in correct format: { "name": { command, args, env } }
|
|
181
|
+
mcpServers = mcpConfig;
|
|
182
|
+
} else if (Array.isArray(mcpConfig) && mcpConfig.length) {
|
|
183
|
+
// Legacy array format: merge all entries into a single object
|
|
184
|
+
mcpServers = Object.assign({}, ...mcpConfig);
|
|
185
|
+
}
|
|
186
|
+
if (mcpServers) {
|
|
187
|
+
const names = Object.keys(mcpServers).join(', ');
|
|
188
|
+
log.info(`Loaded MCP server(s): [${names}]`);
|
|
189
|
+
}
|
|
190
|
+
} catch {}
|
|
191
|
+
|
|
192
|
+
const claudeQuery = query({
|
|
193
|
+
prompt: sdkPrompt,
|
|
194
|
+
options: {
|
|
195
|
+
model,
|
|
196
|
+
cwd: path.join(PKG_DIR, 'workspace'),
|
|
197
|
+
permissionMode: 'bypassPermissions',
|
|
198
|
+
allowDangerouslySkipPermissions: true,
|
|
199
|
+
maxTurns: 50,
|
|
200
|
+
abortController,
|
|
201
|
+
systemPrompt: enrichedPrompt,
|
|
202
|
+
plugins: plugins.length ? plugins : undefined,
|
|
203
|
+
mcpServers,
|
|
204
|
+
stderr: (chunk: string) => { stderrBuf += chunk; },
|
|
205
|
+
env: {
|
|
206
|
+
...process.env as Record<string, string>,
|
|
207
|
+
CLAUDE_CODE_OAUTH_TOKEN: oauthToken,
|
|
208
|
+
CLAUDE_CODE_BUBBLEWRAP: '1',
|
|
209
|
+
},
|
|
210
|
+
},
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
onMessage('bot:typing', { conversationId });
|
|
214
|
+
|
|
215
|
+
for await (const msg of claudeQuery) {
|
|
216
|
+
if (abortController.signal.aborted) break;
|
|
217
|
+
|
|
218
|
+
switch (msg.type) {
|
|
219
|
+
case 'assistant': {
|
|
220
|
+
const assistantMsg = msg.message;
|
|
221
|
+
if (!assistantMsg?.content) break;
|
|
222
|
+
|
|
223
|
+
for (const block of assistantMsg.content) {
|
|
224
|
+
if (block.type === 'text' && block.text) {
|
|
225
|
+
// Add separator between text from different assistant turns
|
|
226
|
+
if (fullText && !fullText.endsWith('\n')) {
|
|
227
|
+
fullText += '\n\n';
|
|
228
|
+
onMessage('bot:token', { conversationId, token: '\n\n' });
|
|
229
|
+
}
|
|
230
|
+
fullText += block.text;
|
|
231
|
+
onMessage('bot:token', { conversationId, token: block.text });
|
|
232
|
+
} else if (block.type === 'tool_use') {
|
|
233
|
+
usedTools.add(block.name);
|
|
234
|
+
onMessage('bot:tool', { conversationId, name: block.name, input: block.input });
|
|
335
235
|
}
|
|
236
|
+
}
|
|
237
|
+
break;
|
|
336
238
|
}
|
|
337
239
|
|
|
338
|
-
|
|
339
|
-
|
|
240
|
+
case 'result': {
|
|
241
|
+
if (fullText) {
|
|
340
242
|
onMessage('bot:response', { conversationId, content: fullText });
|
|
243
|
+
fullText = ''; // prevent duplicate
|
|
244
|
+
} else if (msg.subtype?.startsWith('error')) {
|
|
245
|
+
const errorText = (msg as any).errors?.join('; ') || 'Agent query failed';
|
|
246
|
+
onMessage('bot:error', { conversationId, error: errorText });
|
|
247
|
+
}
|
|
248
|
+
break;
|
|
341
249
|
}
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
} finally {
|
|
352
|
-
activeQueries.delete(conversationId);
|
|
353
|
-
const FILE_TOOLS = ['Write', 'Edit'];
|
|
354
|
-
const usedFileTools = FILE_TOOLS.some(t => usedTools.has(t));
|
|
355
|
-
onMessage('bot:done', { conversationId, usedFileTools });
|
|
250
|
+
|
|
251
|
+
case 'tool_progress':
|
|
252
|
+
onMessage('bot:tool', {
|
|
253
|
+
conversationId,
|
|
254
|
+
name: (msg as any).tool_name || 'working',
|
|
255
|
+
status: 'running',
|
|
256
|
+
});
|
|
257
|
+
break;
|
|
258
|
+
}
|
|
356
259
|
}
|
|
260
|
+
|
|
261
|
+
// If we accumulated text but didn't hit a result message, send what we have
|
|
262
|
+
if (fullText && !abortController.signal.aborted) {
|
|
263
|
+
onMessage('bot:response', { conversationId, content: fullText });
|
|
264
|
+
}
|
|
265
|
+
} catch (err: any) {
|
|
266
|
+
if (!abortController.signal.aborted) {
|
|
267
|
+
const detail = stderrBuf.trim();
|
|
268
|
+
const msg = detail ? `${err.message}\n\nCLI stderr:\n${detail}` : err.message;
|
|
269
|
+
log.warn(`Fluxy agent error (${conversationId}): ${msg}`);
|
|
270
|
+
onMessage('bot:error', { conversationId, error: msg });
|
|
271
|
+
}
|
|
272
|
+
} finally {
|
|
273
|
+
activeQueries.delete(conversationId);
|
|
274
|
+
const FILE_TOOLS = ['Write', 'Edit'];
|
|
275
|
+
const usedFileTools = FILE_TOOLS.some((t) => usedTools.has(t));
|
|
276
|
+
onMessage('bot:done', { conversationId, usedFileTools });
|
|
277
|
+
}
|
|
357
278
|
}
|
|
358
279
|
|
|
359
280
|
/** Stop an in-flight query */
|
|
360
281
|
export function stopFluxyAgentQuery(conversationId: string): void {
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
282
|
+
const q = activeQueries.get(conversationId);
|
|
283
|
+
if (q) {
|
|
284
|
+
q.abortController.abort();
|
|
285
|
+
activeQueries.delete(conversationId);
|
|
286
|
+
}
|
|
366
287
|
}
|