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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fluxy-bot",
3
- "version": "0.4.27",
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 ? 8 : 7; // 0..6 normal, +step 7 "All Set" for initial
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
- // Skills (step 5)
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=Skills, 6=Whisper+Complete, 7=All Set (initial only)
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; // skills — 0 skills is valid
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(7);
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: Skills ── */}
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 7: All Set (initial onboard only) ── */}
1300
- {step === 7 && isInitialSetup && (
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 === 7 && isInitialSetup) && (
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
- * No DB dependencysessions are tracked in-memory.
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
- // In-memory session tracking (conversationId → sessionId)
17
- const sessions = new Map<string, string>();
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
- * Streams events back via onMessage callback.
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
- enabledSkills?: string[],
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 existingSessionId = sessions.get(conversationId);
105
- const customPrompt = readSystemPrompt(names?.botName, names?.humanName);
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
- // Build plugins array from enabled skills — each skill is its own plugin root
124
- const plugins = (enabledSkills || [])
125
- .filter(name => fs.existsSync(path.join(PKG_DIR, 'skills', name, '.claude-plugin', 'plugin.json')))
126
- .map(name => ({ type: 'local' as const, path: path.join(PKG_DIR, 'skills', name) }));
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: customPrompt,
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
- }
@@ -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, clearFluxySession } from './fluxy-agent.js';
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 for system prompt substitution
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 workerApi('/api/onboard/status') as any;
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
- // Fetch enabled skills for dynamic plugin loading
434
- let enabledSkills: string[] = ['workspace-helper'];
435
- try {
436
- const settings = await workerApi('/api/settings') as Record<string, string>;
437
- if (settings.enabled_skills) enabledSkills = JSON.parse(settings.enabled_skills);
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 }, enabledSkills);
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
- const dbConvId = clientConvs.get(ws);
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, PKG_DIR } from '../shared/paths.js';
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
- # Startup Sequence
13
+ # Context
14
14
 
15
- Every session starts blank. You wake up with no memory of anything. That's normalyour files are how you remember.
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
- Before responding to anything, silently read your memory files in this order:
17
+ If recent conversation history is provided below, use it to maintain continuity. Respond naturally as if you remember the conversation.
18
18
 
19
- 1. **`MYSELF.md`** your identity, personality, and operating manual. This is who you are.
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
- Periodic autonomous wake-up (roughly every 30 minutes). You're on your own — no human initiated this.
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
- **What to do:**
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
- 1. **Read your files.** Check daily notes, MEMORY.md, see what's going on.
92
- 2. **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.
93
- 3. **Check the workspace.** Any broken routes? Stale data? Code that needs cleanup? Problems you noticed earlier but didn't fix?
94
- 4. **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.
95
- 5. **Rate importance 0–10** based on what you know about your human:
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
- Scheduled task with context. A cron always has a purpose and you'll know what to do. Execute the task, save results to the appropriate files, finish your turn.
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
+ []
@@ -0,0 +1,8 @@
1
+ {
2
+ "enabled": true,
3
+ "intervalMinutes": 30,
4
+ "quietHours": {
5
+ "start": "23:00",
6
+ "end": "07:00"
7
+ }
8
+ }