fluxy-bot 0.4.27 → 0.4.29
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 -2
- package/supervisor/chat/OnboardWizard.tsx +9 -100
- package/supervisor/fluxy-agent.ts +57 -28
- package/supervisor/index.ts +21 -16
- package/worker/db.ts +9 -0
- package/worker/index.ts +7 -51
- package/worker/prompts/fluxy-system-prompt.txt +53 -19
- package/workspace/CRONS.json +1 -0
- package/workspace/PULSE.json +8 -0
- /package/{skills → workspace/skills}/code-reviewer/.claude-plugin/plugin.json +0 -0
- /package/{skills → workspace/skills}/code-reviewer/skills/code-reviewer/SKILL.md +0 -0
- /package/{skills → workspace/skills}/daily-standup/.claude-plugin/plugin.json +0 -0
- /package/{skills → workspace/skills}/daily-standup/skills/daily-standup/SKILL.md +0 -0
- /package/{skills → workspace/skills}/workspace-helper/.claude-plugin/plugin.json +0 -0
- /package/{skills → workspace/skills}/workspace-helper/skills/workspace-helper/SKILL.md +0 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fluxy-bot",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.29",
|
|
4
4
|
"description": "Self-hosted, self-evolving AI agent with its own dashboard.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -13,7 +13,6 @@
|
|
|
13
13
|
"supervisor/",
|
|
14
14
|
"worker/",
|
|
15
15
|
"shared/",
|
|
16
|
-
"skills/",
|
|
17
16
|
"workspace/",
|
|
18
17
|
"scripts/",
|
|
19
18
|
"vite.config.ts",
|
|
@@ -91,7 +91,7 @@ interface Props {
|
|
|
91
91
|
}
|
|
92
92
|
|
|
93
93
|
export default function OnboardWizard({ onComplete, isInitialSetup = false, onSave }: Props) {
|
|
94
|
-
const TOTAL_STEPS = isInitialSetup ?
|
|
94
|
+
const TOTAL_STEPS = isInitialSetup ? 7 : 6; // 0..5 normal, +step 6 "All Set" for initial
|
|
95
95
|
|
|
96
96
|
const [step, setStep] = useState(0);
|
|
97
97
|
const [userName, setUserName] = useState('');
|
|
@@ -146,12 +146,7 @@ export default function OnboardWizard({ onComplete, isInitialSetup = false, onSa
|
|
|
146
146
|
const [portalOldPassVerified, setPortalOldPassVerified] = useState(false);
|
|
147
147
|
const [portalVerifying, setPortalVerifying] = useState(false);
|
|
148
148
|
|
|
149
|
-
//
|
|
150
|
-
const [availableSkills, setAvailableSkills] = useState<{ id: string; name: string; description: string; enabled: boolean }[]>([]);
|
|
151
|
-
const [enabledSkills, setEnabledSkills] = useState<string[]>([]);
|
|
152
|
-
const [skillsLoading, setSkillsLoading] = useState(true);
|
|
153
|
-
|
|
154
|
-
// Whisper (step 6)
|
|
149
|
+
// Whisper (step 5)
|
|
155
150
|
const [whisperEnabled, setWhisperEnabled] = useState(false);
|
|
156
151
|
const [whisperKey, setWhisperKey] = useState('');
|
|
157
152
|
|
|
@@ -181,18 +176,6 @@ export default function OnboardWizard({ onComplete, isInitialSetup = false, onSa
|
|
|
181
176
|
prefillDone.current = true;
|
|
182
177
|
})
|
|
183
178
|
.catch(() => { prefillDone.current = true; });
|
|
184
|
-
|
|
185
|
-
// Fetch available skills
|
|
186
|
-
fetch('/api/skills')
|
|
187
|
-
.then((r) => r.json())
|
|
188
|
-
.then((data) => {
|
|
189
|
-
if (data.skills) {
|
|
190
|
-
setAvailableSkills(data.skills);
|
|
191
|
-
setEnabledSkills(data.skills.filter((s: any) => s.enabled).map((s: any) => s.id));
|
|
192
|
-
}
|
|
193
|
-
setSkillsLoading(false);
|
|
194
|
-
})
|
|
195
|
-
.catch(() => setSkillsLoading(false));
|
|
196
179
|
}, []);
|
|
197
180
|
|
|
198
181
|
// Check if Claude is already authenticated when selecting Anthropic
|
|
@@ -450,7 +433,7 @@ export default function OnboardWizard({ onComplete, isInitialSetup = false, onSa
|
|
|
450
433
|
|
|
451
434
|
/* ── Navigation ── */
|
|
452
435
|
|
|
453
|
-
// Steps: 0=Welcome, 1=Name, 2=Bot name + Handle, 3=Password, 4=Provider, 5=
|
|
436
|
+
// Steps: 0=Welcome, 1=Name, 2=Bot name + Handle, 3=Password, 4=Provider, 5=Whisper+Complete, 6=All Set (initial only)
|
|
454
437
|
const portalPassMatch = portalPass === portalPassConfirm;
|
|
455
438
|
const portalValid = portalPass.length >= 6 && portalPassMatch;
|
|
456
439
|
// When portal exists: can continue if no password fields touched, or old pass verified + new pass valid
|
|
@@ -465,8 +448,7 @@ export default function OnboardWizard({ onComplete, isInitialSetup = false, onSa
|
|
|
465
448
|
case 2: return registered;
|
|
466
449
|
case 3: return portalCanContinue;
|
|
467
450
|
case 4: return !!(provider && model && isConnected);
|
|
468
|
-
case 5: return true;
|
|
469
|
-
case 6: return true;
|
|
451
|
+
case 5: return true;
|
|
470
452
|
default: return false;
|
|
471
453
|
}
|
|
472
454
|
})();
|
|
@@ -490,7 +472,6 @@ export default function OnboardWizard({ onComplete, isInitialSetup = false, onSa
|
|
|
490
472
|
whisperKey: whisperEnabled ? whisperKey : '',
|
|
491
473
|
portalUser: portalUser.trim(),
|
|
492
474
|
portalPass,
|
|
493
|
-
enabledSkills,
|
|
494
475
|
};
|
|
495
476
|
try {
|
|
496
477
|
if (onSave) {
|
|
@@ -506,7 +487,7 @@ export default function OnboardWizard({ onComplete, isInitialSetup = false, onSa
|
|
|
506
487
|
}
|
|
507
488
|
if (isInitialSetup) {
|
|
508
489
|
setSaving(false);
|
|
509
|
-
setStep(
|
|
490
|
+
setStep(6);
|
|
510
491
|
} else {
|
|
511
492
|
onComplete();
|
|
512
493
|
}
|
|
@@ -1137,80 +1118,8 @@ export default function OnboardWizard({ onComplete, isInitialSetup = false, onSa
|
|
|
1137
1118
|
</div>
|
|
1138
1119
|
)}
|
|
1139
1120
|
|
|
1140
|
-
{/* ── Step 5:
|
|
1121
|
+
{/* ── Step 5: Whisper (optional) + Complete ── */}
|
|
1141
1122
|
{step === 5 && (
|
|
1142
|
-
<div>
|
|
1143
|
-
<h1 className="text-xl font-bold text-white tracking-tight">
|
|
1144
|
-
Skills
|
|
1145
|
-
</h1>
|
|
1146
|
-
<p className="text-white/40 text-[13px] mt-1.5 leading-relaxed">
|
|
1147
|
-
Choose which skills your agent can use. You can change these later.
|
|
1148
|
-
</p>
|
|
1149
|
-
|
|
1150
|
-
{skillsLoading ? (
|
|
1151
|
-
<div className="flex items-center justify-center py-10">
|
|
1152
|
-
<LoaderCircle className="h-5 w-5 animate-spin text-white/30" />
|
|
1153
|
-
</div>
|
|
1154
|
-
) : availableSkills.length === 0 ? (
|
|
1155
|
-
<div className="mt-5 bg-white/[0.02] border border-white/[0.06] rounded-xl px-4 py-3">
|
|
1156
|
-
<p className="text-white/40 text-[13px]">No skills found.</p>
|
|
1157
|
-
</div>
|
|
1158
|
-
) : (
|
|
1159
|
-
<div className="space-y-2.5 mt-5">
|
|
1160
|
-
{availableSkills.map((skill) => {
|
|
1161
|
-
const isEnabled = enabledSkills.includes(skill.id);
|
|
1162
|
-
return (
|
|
1163
|
-
<button
|
|
1164
|
-
key={skill.id}
|
|
1165
|
-
type="button"
|
|
1166
|
-
onClick={() => {
|
|
1167
|
-
setEnabledSkills((prev) =>
|
|
1168
|
-
isEnabled ? prev.filter((s) => s !== skill.id) : [...prev, skill.id]
|
|
1169
|
-
);
|
|
1170
|
-
}}
|
|
1171
|
-
className={`w-full rounded-xl border transition-all duration-200 p-4 text-left ${
|
|
1172
|
-
isEnabled
|
|
1173
|
-
? 'bg-white/[0.04] border-[#AF27E3]/40'
|
|
1174
|
-
: 'bg-transparent border-white/[0.06] hover:border-white/10 hover:bg-white/[0.02]'
|
|
1175
|
-
}`}
|
|
1176
|
-
>
|
|
1177
|
-
<div className="flex items-center gap-3.5">
|
|
1178
|
-
<div className="flex-1 min-w-0">
|
|
1179
|
-
<div className="text-[14px] font-medium text-white">{skill.name}</div>
|
|
1180
|
-
<div className="text-[12px] text-white/35 mt-0.5 leading-relaxed line-clamp-2">
|
|
1181
|
-
{skill.description}
|
|
1182
|
-
</div>
|
|
1183
|
-
</div>
|
|
1184
|
-
<div className={`w-10 h-[22px] rounded-full transition-colors duration-200 flex items-center px-0.5 shrink-0 ${
|
|
1185
|
-
isEnabled ? 'bg-gradient-brand' : 'bg-white/[0.08]'
|
|
1186
|
-
}`}>
|
|
1187
|
-
<div className={`w-[18px] h-[18px] rounded-full bg-white shadow-sm transition-transform duration-200 ${
|
|
1188
|
-
isEnabled ? 'translate-x-[18px]' : 'translate-x-0'
|
|
1189
|
-
}`} />
|
|
1190
|
-
</div>
|
|
1191
|
-
</div>
|
|
1192
|
-
</button>
|
|
1193
|
-
);
|
|
1194
|
-
})}
|
|
1195
|
-
</div>
|
|
1196
|
-
)}
|
|
1197
|
-
|
|
1198
|
-
<button
|
|
1199
|
-
onClick={next}
|
|
1200
|
-
className="w-full mt-5 py-3 bg-gradient-brand hover:opacity-90 text-white text-[14px] font-semibold rounded-full transition-colors flex items-center justify-center gap-2"
|
|
1201
|
-
>
|
|
1202
|
-
Continue
|
|
1203
|
-
<ArrowRight className="h-4 w-4" />
|
|
1204
|
-
</button>
|
|
1205
|
-
|
|
1206
|
-
<p className="text-center text-white/20 text-[11px] mt-2.5">
|
|
1207
|
-
{enabledSkills.length === 0 ? 'No skills selected — your agent will use its base capabilities.' : `${enabledSkills.length} skill${enabledSkills.length === 1 ? '' : 's'} selected`}
|
|
1208
|
-
</p>
|
|
1209
|
-
</div>
|
|
1210
|
-
)}
|
|
1211
|
-
|
|
1212
|
-
{/* ── Step 6: Whisper (optional) + Complete ── */}
|
|
1213
|
-
{step === 6 && (
|
|
1214
1123
|
<div>
|
|
1215
1124
|
<div className="flex items-center gap-2 mb-1">
|
|
1216
1125
|
<h1 className="text-xl font-bold text-white tracking-tight">
|
|
@@ -1296,8 +1205,8 @@ export default function OnboardWizard({ onComplete, isInitialSetup = false, onSa
|
|
|
1296
1205
|
</div>
|
|
1297
1206
|
)}
|
|
1298
1207
|
|
|
1299
|
-
{/* ── Step
|
|
1300
|
-
{step ===
|
|
1208
|
+
{/* ── Step 6: All Set (initial onboard only) ── */}
|
|
1209
|
+
{step === 6 && isInitialSetup && (
|
|
1301
1210
|
<div className="flex flex-col items-center text-center">
|
|
1302
1211
|
<div className="w-16 h-16 rounded-full bg-emerald-500/10 border border-emerald-500/20 flex items-center justify-center mb-5">
|
|
1303
1212
|
<Check className="h-8 w-8 text-emerald-400" />
|
|
@@ -1370,7 +1279,7 @@ export default function OnboardWizard({ onComplete, isInitialSetup = false, onSa
|
|
|
1370
1279
|
</AnimatePresence>
|
|
1371
1280
|
|
|
1372
1281
|
{/* Back button (hidden on All Set step) */}
|
|
1373
|
-
{step > 0 && step < TOTAL_STEPS - 1 && !(step ===
|
|
1282
|
+
{step > 0 && step < TOTAL_STEPS - 1 && !(step === 6 && isInitialSetup) && (
|
|
1374
1283
|
<div className="px-8 pb-5 -mt-3">
|
|
1375
1284
|
<button
|
|
1376
1285
|
onClick={back}
|
|
@@ -1,20 +1,22 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Lightweight Claude Agent SDK wrapper for the supervisor's fluxy chat.
|
|
3
|
-
*
|
|
3
|
+
* Fresh context per turn — memory files and conversation history are injected into the system prompt.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { query, type SDKMessage, type SDKUserMessage } from '@anthropic-ai/claude-agent-sdk';
|
|
7
7
|
import fs from 'fs';
|
|
8
8
|
import path from 'path';
|
|
9
9
|
import { log } from '../shared/logger.js';
|
|
10
|
-
import { PKG_DIR } from '../shared/paths.js';
|
|
10
|
+
import { PKG_DIR, WORKSPACE_DIR } from '../shared/paths.js';
|
|
11
11
|
import type { SavedFile } from './file-saver.js';
|
|
12
12
|
import { getClaudeAccessToken } from '../worker/claude-auth.js';
|
|
13
13
|
|
|
14
14
|
const PROMPT_FILE = path.join(import.meta.dirname, '..', 'worker', 'prompts', 'fluxy-system-prompt.txt');
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
|
|
16
|
+
export interface RecentMessage {
|
|
17
|
+
role: 'user' | 'assistant';
|
|
18
|
+
content: string;
|
|
19
|
+
}
|
|
18
20
|
|
|
19
21
|
interface ActiveQuery {
|
|
20
22
|
abortController: AbortController;
|
|
@@ -29,6 +31,33 @@ export interface AgentAttachment {
|
|
|
29
31
|
data: string; // base64
|
|
30
32
|
}
|
|
31
33
|
|
|
34
|
+
/** Read a memory file from workspace, returning '(empty)' if missing or empty */
|
|
35
|
+
function readMemoryFile(filename: string): string {
|
|
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
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Read all memory + config files and return their contents */
|
|
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
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Format recent messages as conversation history text */
|
|
56
|
+
function formatConversationHistory(messages: RecentMessage[]): string {
|
|
57
|
+
if (!messages.length) return '';
|
|
58
|
+
return messages.map((m) => `${m.role}: ${m.content}`).join('\n\n');
|
|
59
|
+
}
|
|
60
|
+
|
|
32
61
|
/** Build a multi-part prompt with attachments for the SDK */
|
|
33
62
|
function buildMultiPartPrompt(text: string, attachments: AgentAttachment[], savedFiles?: SavedFile[]): AsyncIterable<SDKUserMessage> {
|
|
34
63
|
return (async function* () {
|
|
@@ -82,7 +111,7 @@ function readSystemPrompt(botName = 'Fluxy', humanName = 'Human'): string {
|
|
|
82
111
|
|
|
83
112
|
/**
|
|
84
113
|
* Run an Agent SDK query for a fluxy chat conversation.
|
|
85
|
-
*
|
|
114
|
+
* Fresh context each turn — memory files and history are injected into the system prompt.
|
|
86
115
|
*/
|
|
87
116
|
export async function startFluxyAgentQuery(
|
|
88
117
|
conversationId: string,
|
|
@@ -92,7 +121,7 @@ export async function startFluxyAgentQuery(
|
|
|
92
121
|
attachments?: AgentAttachment[],
|
|
93
122
|
savedFiles?: SavedFile[],
|
|
94
123
|
names?: { botName: string; humanName: string },
|
|
95
|
-
|
|
124
|
+
recentMessages?: RecentMessage[],
|
|
96
125
|
): Promise<void> {
|
|
97
126
|
const oauthToken = await getClaudeAccessToken();
|
|
98
127
|
if (!oauthToken) {
|
|
@@ -101,8 +130,17 @@ export async function startFluxyAgentQuery(
|
|
|
101
130
|
}
|
|
102
131
|
|
|
103
132
|
const abortController = new AbortController();
|
|
104
|
-
const
|
|
105
|
-
const
|
|
133
|
+
const basePrompt = readSystemPrompt(names?.botName, names?.humanName);
|
|
134
|
+
const memoryFiles = readMemoryFiles();
|
|
135
|
+
|
|
136
|
+
// Build enriched system prompt with memory files and conversation history
|
|
137
|
+
let enrichedPrompt = basePrompt;
|
|
138
|
+
|
|
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}`;
|
|
140
|
+
|
|
141
|
+
if (recentMessages?.length) {
|
|
142
|
+
enrichedPrompt += `\n\n---\n# Recent Conversation\n${formatConversationHistory(recentMessages)}`;
|
|
143
|
+
}
|
|
106
144
|
|
|
107
145
|
activeQueries.set(conversationId, { abortController });
|
|
108
146
|
|
|
@@ -120,10 +158,16 @@ export async function startFluxyAgentQuery(
|
|
|
120
158
|
attachments?.length ? buildMultiPartPrompt(prompt, attachments, savedFiles) : plainPrompt;
|
|
121
159
|
|
|
122
160
|
try {
|
|
123
|
-
//
|
|
124
|
-
const
|
|
125
|
-
|
|
126
|
-
|
|
161
|
+
// Auto-discover all skill plugins in workspace/skills/ — any folder with a valid plugin.json is loaded
|
|
162
|
+
const skillsDir = path.join(PKG_DIR, 'workspace', 'skills');
|
|
163
|
+
const plugins: { type: 'local'; path: string }[] = [];
|
|
164
|
+
try {
|
|
165
|
+
for (const entry of fs.readdirSync(skillsDir, { withFileTypes: true })) {
|
|
166
|
+
if (entry.isDirectory() && fs.existsSync(path.join(skillsDir, entry.name, '.claude-plugin', 'plugin.json'))) {
|
|
167
|
+
plugins.push({ type: 'local' as const, path: path.join(skillsDir, entry.name) });
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
} catch {}
|
|
127
171
|
|
|
128
172
|
const claudeQuery = query({
|
|
129
173
|
prompt: sdkPrompt,
|
|
@@ -134,9 +178,8 @@ export async function startFluxyAgentQuery(
|
|
|
134
178
|
allowDangerouslySkipPermissions: true,
|
|
135
179
|
maxTurns: 50,
|
|
136
180
|
abortController,
|
|
137
|
-
systemPrompt:
|
|
181
|
+
systemPrompt: enrichedPrompt,
|
|
138
182
|
plugins: plugins.length ? plugins : undefined,
|
|
139
|
-
...(existingSessionId ? { resume: existingSessionId } : {}),
|
|
140
183
|
env: {
|
|
141
184
|
...process.env as Record<string, string>,
|
|
142
185
|
CLAUDE_CODE_OAUTH_TOKEN: oauthToken,
|
|
@@ -154,11 +197,6 @@ export async function startFluxyAgentQuery(
|
|
|
154
197
|
const assistantMsg = msg.message;
|
|
155
198
|
if (!assistantMsg?.content) break;
|
|
156
199
|
|
|
157
|
-
// Save session_id from first assistant message
|
|
158
|
-
if (msg.session_id && !sessions.has(conversationId)) {
|
|
159
|
-
sessions.set(conversationId, msg.session_id);
|
|
160
|
-
}
|
|
161
|
-
|
|
162
200
|
for (const block of assistantMsg.content) {
|
|
163
201
|
if (block.type === 'text' && block.text) {
|
|
164
202
|
// Add separator between text from different assistant turns
|
|
@@ -177,10 +215,6 @@ export async function startFluxyAgentQuery(
|
|
|
177
215
|
}
|
|
178
216
|
|
|
179
217
|
case 'result': {
|
|
180
|
-
if (msg.session_id) {
|
|
181
|
-
sessions.set(conversationId, msg.session_id);
|
|
182
|
-
}
|
|
183
|
-
|
|
184
218
|
if (fullText) {
|
|
185
219
|
onMessage('bot:response', { conversationId, content: fullText });
|
|
186
220
|
fullText = ''; // prevent duplicate
|
|
@@ -226,8 +260,3 @@ export function stopFluxyAgentQuery(conversationId: string): void {
|
|
|
226
260
|
activeQueries.delete(conversationId);
|
|
227
261
|
}
|
|
228
262
|
}
|
|
229
|
-
|
|
230
|
-
/** Clear a conversation's session (for "clear context") */
|
|
231
|
-
export function clearFluxySession(conversationId: string): void {
|
|
232
|
-
sessions.delete(conversationId);
|
|
233
|
-
}
|
package/supervisor/index.ts
CHANGED
|
@@ -12,7 +12,7 @@ import { startTunnel, stopTunnel, isTunnelAlive, restartTunnel } from './tunnel.
|
|
|
12
12
|
import { spawnWorker, stopWorker, getWorkerPort, isWorkerAlive } from './worker.js';
|
|
13
13
|
import { spawnBackend, stopBackend, getBackendPort, isBackendAlive, resetBackendRestarts } from './backend.js';
|
|
14
14
|
import { updateTunnelUrl, startHeartbeat, stopHeartbeat, disconnect } from '../shared/relay.js';
|
|
15
|
-
import { startFluxyAgentQuery, stopFluxyAgentQuery,
|
|
15
|
+
import { startFluxyAgentQuery, stopFluxyAgentQuery, type RecentMessage } from './fluxy-agent.js';
|
|
16
16
|
import { ensureFileDirs, saveAttachment, type SavedFile } from './file-saver.js';
|
|
17
17
|
import { startViteDevServers, stopViteDevServers } from './vite-dev.js';
|
|
18
18
|
import { execSync } from 'child_process';
|
|
@@ -146,7 +146,6 @@ export async function startSupervisor() {
|
|
|
146
146
|
'GET /api/portal/validate-token',
|
|
147
147
|
'GET /api/onboard/status',
|
|
148
148
|
'GET /api/health',
|
|
149
|
-
'GET /api/skills',
|
|
150
149
|
'POST /api/onboard',
|
|
151
150
|
];
|
|
152
151
|
|
|
@@ -422,19 +421,29 @@ export async function startSupervisor() {
|
|
|
422
421
|
log.warn(`[fluxy] DB persist error: ${err.message}`);
|
|
423
422
|
}
|
|
424
423
|
|
|
425
|
-
// Fetch agent/user names
|
|
424
|
+
// Fetch agent/user names and recent messages in parallel
|
|
426
425
|
let botName = 'Fluxy', humanName = 'Human';
|
|
426
|
+
let recentMessages: RecentMessage[] = [];
|
|
427
427
|
try {
|
|
428
|
-
const status = await
|
|
428
|
+
const [status, recentRaw] = await Promise.all([
|
|
429
|
+
workerApi('/api/onboard/status') as Promise<any>,
|
|
430
|
+
workerApi(`/api/conversations/${convId}/messages/recent?limit=20`) as Promise<any[]>,
|
|
431
|
+
]);
|
|
429
432
|
botName = status.agentName || 'Fluxy';
|
|
430
433
|
humanName = status.userName || 'Human';
|
|
431
|
-
} catch {}
|
|
432
434
|
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
435
|
+
// Filter to user/assistant only, exclude the last entry (current message already sent as SDK prompt)
|
|
436
|
+
if (Array.isArray(recentRaw)) {
|
|
437
|
+
const filtered = recentRaw
|
|
438
|
+
.filter((m: any) => m.role === 'user' || m.role === 'assistant');
|
|
439
|
+
// Slice off the last entry — it's the current user message
|
|
440
|
+
if (filtered.length > 0) {
|
|
441
|
+
recentMessages = filtered.slice(0, -1).map((m: any) => ({
|
|
442
|
+
role: m.role as 'user' | 'assistant',
|
|
443
|
+
content: m.content,
|
|
444
|
+
}));
|
|
445
|
+
}
|
|
446
|
+
}
|
|
438
447
|
} catch {}
|
|
439
448
|
|
|
440
449
|
// Start agent query
|
|
@@ -465,7 +474,7 @@ export async function startSupervisor() {
|
|
|
465
474
|
|
|
466
475
|
// Stream all events to every connected client
|
|
467
476
|
broadcastFluxy(type, eventData);
|
|
468
|
-
}, data.attachments, savedFiles, { botName, humanName },
|
|
477
|
+
}, data.attachments, savedFiles, { botName, humanName }, recentMessages);
|
|
469
478
|
})();
|
|
470
479
|
return;
|
|
471
480
|
}
|
|
@@ -506,11 +515,7 @@ export async function startSupervisor() {
|
|
|
506
515
|
if (msg.type === 'user:clear-context') {
|
|
507
516
|
(async () => {
|
|
508
517
|
try {
|
|
509
|
-
|
|
510
|
-
if (dbConvId) {
|
|
511
|
-
clearFluxySession(dbConvId);
|
|
512
|
-
clientConvs.delete(ws);
|
|
513
|
-
}
|
|
518
|
+
clientConvs.delete(ws);
|
|
514
519
|
await workerApi('/api/context/clear', 'POST');
|
|
515
520
|
} catch (err: any) {
|
|
516
521
|
log.warn(`[fluxy] Clear context error: ${err.message}`);
|
package/worker/db.ts
CHANGED
|
@@ -120,3 +120,12 @@ export function getSessionId(convId: string): string | null {
|
|
|
120
120
|
export function saveSessionId(convId: string, sessionId: string): void {
|
|
121
121
|
db.prepare('UPDATE conversations SET session_id = ? WHERE id = ?').run(sessionId, convId);
|
|
122
122
|
}
|
|
123
|
+
|
|
124
|
+
// Recent messages (for context injection)
|
|
125
|
+
export function getRecentMessages(convId: string, limit = 20) {
|
|
126
|
+
return db.prepare(`
|
|
127
|
+
SELECT * FROM (
|
|
128
|
+
SELECT * FROM messages WHERE conversation_id = ? ORDER BY created_at DESC LIMIT ?
|
|
129
|
+
) sub ORDER BY created_at ASC
|
|
130
|
+
`).all(convId, limit);
|
|
131
|
+
}
|
package/worker/index.ts
CHANGED
|
@@ -3,9 +3,9 @@ import crypto from 'crypto';
|
|
|
3
3
|
import fs from 'fs';
|
|
4
4
|
import path from 'path';
|
|
5
5
|
import { loadConfig, saveConfig } from '../shared/config.js';
|
|
6
|
-
import { paths, WORKSPACE_DIR
|
|
6
|
+
import { paths, WORKSPACE_DIR } from '../shared/paths.js';
|
|
7
7
|
import { log } from '../shared/logger.js';
|
|
8
|
-
import { initDb, closeDb, listConversations, createConversation, deleteConversation, getMessages, addMessage, getSetting, getAllSettings, setSetting, createSession, getSession, deleteExpiredSessions } from './db.js';
|
|
8
|
+
import { initDb, closeDb, listConversations, createConversation, deleteConversation, getMessages, addMessage, getSetting, getAllSettings, setSetting, createSession, getSession, deleteExpiredSessions, getRecentMessages } from './db.js';
|
|
9
9
|
import { startCodexOAuth, cancelCodexOAuth, getCodexAuthStatus, readCodexAccessToken } from './codex-auth.js';
|
|
10
10
|
import { startClaudeOAuth, exchangeClaudeCode, getClaudeAuthStatus, readClaudeAccessToken } from './claude-auth.js';
|
|
11
11
|
import { checkAvailability, registerHandle, releaseHandle, updateTunnelUrl, startHeartbeat, stopHeartbeat } from '../shared/relay.js';
|
|
@@ -55,6 +55,11 @@ app.post('/api/conversations/:id/messages', (req, res) => {
|
|
|
55
55
|
const msg = addMessage(req.params.id, role, content, meta);
|
|
56
56
|
res.json(msg);
|
|
57
57
|
});
|
|
58
|
+
app.get('/api/conversations/:id/messages/recent', (req, res) => {
|
|
59
|
+
const limit = parseInt(req.query.limit as string) || 20;
|
|
60
|
+
const msgs = getRecentMessages(req.params.id, Math.min(limit, 100));
|
|
61
|
+
res.json(msgs);
|
|
62
|
+
});
|
|
58
63
|
app.delete('/api/conversations/:id', (req, res) => { deleteConversation(req.params.id); res.json({ ok: true }); });
|
|
59
64
|
app.get('/api/settings', (_, res) => res.json(getAllSettings()));
|
|
60
65
|
app.put('/api/settings/:key', (req, res) => {
|
|
@@ -62,50 +67,6 @@ app.put('/api/settings/:key', (req, res) => {
|
|
|
62
67
|
res.json({ ok: true });
|
|
63
68
|
});
|
|
64
69
|
|
|
65
|
-
// ── Skills discovery ──
|
|
66
|
-
|
|
67
|
-
/** Parse YAML frontmatter from a SKILL.md file (minimal inline parser) */
|
|
68
|
-
function parseFrontmatter(content: string): Record<string, string> {
|
|
69
|
-
const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
70
|
-
if (!match) return {};
|
|
71
|
-
const result: Record<string, string> = {};
|
|
72
|
-
for (const line of match[1].split('\n')) {
|
|
73
|
-
const idx = line.indexOf(':');
|
|
74
|
-
if (idx > 0) {
|
|
75
|
-
result[line.slice(0, idx).trim()] = line.slice(idx + 1).trim();
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
return result;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
app.get('/api/skills', (_, res) => {
|
|
82
|
-
const skillsDir = path.join(PKG_DIR, 'skills');
|
|
83
|
-
const skills: { id: string; name: string; description: string; enabled: boolean }[] = [];
|
|
84
|
-
|
|
85
|
-
// Read enabled skills from DB (default: workspace-helper)
|
|
86
|
-
const raw = getSetting('enabled_skills');
|
|
87
|
-
const enabledSkills: string[] = raw ? JSON.parse(raw) : ['workspace-helper'];
|
|
88
|
-
|
|
89
|
-
try {
|
|
90
|
-
const entries = fs.readdirSync(skillsDir, { withFileTypes: true });
|
|
91
|
-
for (const entry of entries) {
|
|
92
|
-
if (!entry.isDirectory()) continue;
|
|
93
|
-
const skillMd = path.join(skillsDir, entry.name, 'skills', entry.name, 'SKILL.md');
|
|
94
|
-
if (!fs.existsSync(skillMd)) continue;
|
|
95
|
-
const content = fs.readFileSync(skillMd, 'utf-8');
|
|
96
|
-
const fm = parseFrontmatter(content);
|
|
97
|
-
skills.push({
|
|
98
|
-
id: entry.name,
|
|
99
|
-
name: fm.name || entry.name,
|
|
100
|
-
description: fm.description || '',
|
|
101
|
-
enabled: enabledSkills.includes(entry.name),
|
|
102
|
-
});
|
|
103
|
-
}
|
|
104
|
-
} catch {}
|
|
105
|
-
|
|
106
|
-
res.json({ skills });
|
|
107
|
-
});
|
|
108
|
-
|
|
109
70
|
// ── Current conversation (shared across devices) ──
|
|
110
71
|
|
|
111
72
|
app.get('/api/context/current', (_, res) => {
|
|
@@ -367,11 +328,6 @@ app.post('/api/onboard', (req, res) => {
|
|
|
367
328
|
setSetting('whisper_key', whisperKey);
|
|
368
329
|
}
|
|
369
330
|
|
|
370
|
-
// Save enabled skills
|
|
371
|
-
if (req.body.enabledSkills !== undefined) {
|
|
372
|
-
setSetting('enabled_skills', JSON.stringify(req.body.enabledSkills));
|
|
373
|
-
}
|
|
374
|
-
|
|
375
331
|
// Update bot and human names in FLUXY.md
|
|
376
332
|
// On first onboard: replaces $BOT / $HUMAN placeholders
|
|
377
333
|
// On re-onboard: replaces the previous names with the new ones
|
|
@@ -10,20 +10,13 @@ The workspace runs locally on your human's hardware. It's also a PWA, so they mi
|
|
|
10
10
|
|
|
11
11
|
---
|
|
12
12
|
|
|
13
|
-
#
|
|
13
|
+
# Context
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
Your memory files (MYSELF.md, MYHUMAN.md, MEMORY.md) are provided below in this system prompt. You already have their contents — do not re-read them with tools.
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
If recent conversation history is provided below, use it to maintain continuity. Respond naturally as if you remember the conversation.
|
|
18
18
|
|
|
19
|
-
|
|
20
|
-
2. **`MYHUMAN.md`** — who your human is: name, preferences, projects, what they care about.
|
|
21
|
-
3. **`MEMORY.md`** — your curated long-term memory. The distilled essence of everything worth keeping.
|
|
22
|
-
4. **`memory/YYYY-MM-DD.md`** — today's and yesterday's daily notes. Recent context and events.
|
|
23
|
-
|
|
24
|
-
Do not announce that you're reading memory. Do not say "Let me check my files first." Do not list what you found. Just read them and respond naturally, as if you've always known.
|
|
25
|
-
|
|
26
|
-
If any file is empty or missing, that's fine — you might be brand new. If `MYSELF.md` is bare, start a conversation to figure out who you are: your name, your vibe, what kind of agent you want to be. Make it natural, not an interrogation. Fill in the files as you learn.
|
|
19
|
+
You should still WRITE to all memory files whenever you learn something worth remembering. Your writes update the actual files on disk and will appear in your context on the next turn.
|
|
27
20
|
|
|
28
21
|
---
|
|
29
22
|
|
|
@@ -82,17 +75,35 @@ A thought you don't write down is a thought you'll never have again.
|
|
|
82
75
|
|
|
83
76
|
# PULSE and CRON
|
|
84
77
|
|
|
78
|
+
These are controlled by two JSON config files in your workspace. **Your human can ask you to change them** — when they do, edit the files directly. A background process reads these files and wakes you up accordingly.
|
|
79
|
+
|
|
85
80
|
## PULSE — `<PULSE/>`
|
|
86
81
|
|
|
87
|
-
|
|
82
|
+
**Config file:** `PULSE.json`
|
|
83
|
+
|
|
84
|
+
```json
|
|
85
|
+
{
|
|
86
|
+
"enabled": true,
|
|
87
|
+
"intervalMinutes": 30,
|
|
88
|
+
"quietHours": { "start": "23:00", "end": "07:00" }
|
|
89
|
+
}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Your human can ask you to:
|
|
93
|
+
- Change the interval ("check in every 15 minutes", "pulse every hour")
|
|
94
|
+
- Enable/disable pulse ("stop pulsing", "turn pulse back on")
|
|
95
|
+
- Adjust quiet hours ("don't wake me before 9am", "no quiet hours")
|
|
96
|
+
|
|
97
|
+
Just edit `PULSE.json` with the Write or Edit tool when asked.
|
|
88
98
|
|
|
89
|
-
|
|
99
|
+
When you receive a `<PULSE/>` tag, it means the background process triggered your periodic wake-up. You're on your own — no human initiated this.
|
|
90
100
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
101
|
+
**What to do on pulse:**
|
|
102
|
+
|
|
103
|
+
1. **Memory maintenance.** Review recent daily notes. Anything worth promoting to MEMORY.md? Anything in MEMORY.md that's stale? This is your equivalent of sleep consolidation — do it regularly.
|
|
104
|
+
2. **Check the workspace.** Any broken routes? Stale data? Code that needs cleanup? Problems you noticed earlier but didn't fix?
|
|
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
|
+
4. **Rate importance 0–10** based on what you know about your human:
|
|
96
107
|
- **8+**: Send a message immediately using `<Message>Your markdown message here<Message/>`
|
|
97
108
|
- **Below 8**: Note it in your daily memory. Discuss later when they talk to you.
|
|
98
109
|
|
|
@@ -102,7 +113,30 @@ Late at night, unless it's urgent — let them sleep.
|
|
|
102
113
|
|
|
103
114
|
## CRON — `<CRON/>`
|
|
104
115
|
|
|
105
|
-
|
|
116
|
+
**Config file:** `CRONS.json`
|
|
117
|
+
|
|
118
|
+
An array of scheduled tasks:
|
|
119
|
+
|
|
120
|
+
```json
|
|
121
|
+
[
|
|
122
|
+
{
|
|
123
|
+
"id": "daily-summary",
|
|
124
|
+
"schedule": "0 9 * * *",
|
|
125
|
+
"task": "Write a daily summary of yesterday's notes",
|
|
126
|
+
"enabled": true
|
|
127
|
+
}
|
|
128
|
+
]
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Your human can ask you to:
|
|
132
|
+
- Add a cron ("every morning at 9, summarize my notes")
|
|
133
|
+
- Remove or disable a cron ("stop the daily summary")
|
|
134
|
+
- Change a schedule ("move the summary to 8am")
|
|
135
|
+
- List active crons ("what's scheduled?")
|
|
136
|
+
|
|
137
|
+
Just edit `CRONS.json` with the Write or Edit tool when asked. Each cron needs: `id` (unique slug), `schedule` (cron expression), `task` (what to do), `enabled` (boolean).
|
|
138
|
+
|
|
139
|
+
When you receive a `<CRON/>` tag, it includes context about which cron triggered. Execute the task, save results to the appropriate files, finish your turn.
|
|
106
140
|
|
|
107
141
|
Notify your human only if importance is 7+ — otherwise log results silently.
|
|
108
142
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
[]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|