@yemi33/minions 0.1.253 → 0.1.255
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/CHANGELOG.md +8 -0
- package/dashboard/js/command-center.js +56 -31
- package/dashboard.js +50 -0
- package/engine/llm.js +73 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,8 @@ let _ccMessages = JSON.parse(localStorage.getItem('cc-messages') || '[]');
|
|
|
5
5
|
let _ccOpen = false;
|
|
6
6
|
let _ccSending = false;
|
|
7
7
|
let _ccQueue = [];
|
|
8
|
+
// Clear stale sending state on page load — SSE streams don't survive refresh
|
|
9
|
+
try { localStorage.removeItem('cc-sending'); } catch {}
|
|
8
10
|
|
|
9
11
|
function toggleCommandCenter() {
|
|
10
12
|
_ccOpen = !_ccOpen;
|
|
@@ -40,7 +42,8 @@ function ccRestoreMessages() {
|
|
|
40
42
|
// Restore "thinking" indicator if CC was mid-request when page refreshed
|
|
41
43
|
try {
|
|
42
44
|
const sendingState = JSON.parse(localStorage.getItem('cc-sending') || 'null');
|
|
43
|
-
|
|
45
|
+
// Only restore sending state if very recent (< 10s) — page refresh kills the SSE stream
|
|
46
|
+
if (sendingState?.sending && (Date.now() - sendingState.startedAt) < 10000) {
|
|
44
47
|
_ccSending = true;
|
|
45
48
|
const elapsed = Date.now() - sendingState.startedAt;
|
|
46
49
|
const thinking = document.createElement('div');
|
|
@@ -171,45 +174,67 @@ async function _ccDoSend(message, skipUserMsg) {
|
|
|
171
174
|
}, 500);
|
|
172
175
|
|
|
173
176
|
try {
|
|
174
|
-
|
|
175
|
-
const res = await fetch('/api/command-center', {
|
|
177
|
+
// Stream response via SSE — shows text as it arrives
|
|
178
|
+
const res = await fetch('/api/command-center/stream', {
|
|
176
179
|
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
177
|
-
body: JSON.stringify({ message
|
|
178
|
-
signal:
|
|
180
|
+
body: JSON.stringify({ message }),
|
|
181
|
+
signal: AbortSignal.timeout(960000)
|
|
179
182
|
});
|
|
180
|
-
const data = await res.json();
|
|
181
183
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
(isBusy ? ' <button onclick="ccNewSession()" style="margin-top:4px;padding:3px 10px;background:var(--surface2);border:1px solid var(--border);border-radius:4px;color:var(--blue);cursor:pointer;font-size:10px">Reset CC</button>' : ''));
|
|
184
|
+
if (!res.ok) {
|
|
185
|
+
clearInterval(ccTimer);
|
|
186
|
+
thinking.remove();
|
|
187
|
+
const errText = await res.text();
|
|
188
|
+
ccAddMessage('assistant', '<span style="color:var(--red)">' + escHtml(errText || 'CC error') + '</span>' +
|
|
189
|
+
(errText.includes('busy') ? ' <button onclick="ccNewSession()" style="margin-top:4px;padding:3px 10px;background:var(--surface2);border:1px solid var(--border);border-radius:4px;color:var(--blue);cursor:pointer;font-size:10px">Reset CC</button>' : ''));
|
|
189
190
|
return;
|
|
190
191
|
}
|
|
191
192
|
|
|
192
|
-
//
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
193
|
+
// Create streaming message bubble
|
|
194
|
+
clearInterval(ccTimer);
|
|
195
|
+
thinking.remove();
|
|
196
|
+
const streamDiv = document.createElement('div');
|
|
197
|
+
streamDiv.className = 'cc-msg assistant';
|
|
198
|
+
streamDiv.innerHTML = '<span style="color:var(--muted);font-size:11px">Thinking...</span>';
|
|
199
|
+
document.getElementById('cc-messages').appendChild(streamDiv);
|
|
200
|
+
let streamedText = '';
|
|
201
|
+
|
|
202
|
+
const reader = res.body.getReader();
|
|
203
|
+
const decoder = new TextDecoder();
|
|
204
|
+
let buf = '';
|
|
205
|
+
|
|
206
|
+
while (true) {
|
|
207
|
+
const { done, value } = await reader.read();
|
|
208
|
+
if (done) break;
|
|
209
|
+
buf += decoder.decode(value, { stream: true });
|
|
210
|
+
const lines = buf.split('\n');
|
|
211
|
+
buf = lines.pop();
|
|
212
|
+
for (const line of lines) {
|
|
213
|
+
if (!line.startsWith('data: ')) continue;
|
|
214
|
+
try {
|
|
215
|
+
const evt = JSON.parse(line.slice(6));
|
|
216
|
+
if (evt.type === 'chunk') {
|
|
217
|
+
streamedText = evt.text; // each chunk is the full text so far for this turn
|
|
218
|
+
streamDiv.innerHTML = renderMd(streamedText);
|
|
219
|
+
const msgs = document.getElementById('cc-messages');
|
|
220
|
+
if (msgs.scrollHeight - msgs.scrollTop - msgs.clientHeight < 150) msgs.scrollTop = msgs.scrollHeight;
|
|
221
|
+
} else if (evt.type === 'done') {
|
|
222
|
+
// Final result — replace with rendered markdown + actions
|
|
223
|
+
const ccElapsed = Math.round((Date.now() - ccStartTime) / 1000);
|
|
224
|
+
streamDiv.innerHTML = renderMd(evt.text || streamedText || '') +
|
|
225
|
+
'<div style="font-size:9px;color:var(--muted);margin-top:6px;display:flex;justify-content:flex-end;padding-right:30px">' + ccElapsed + 's</div>';
|
|
226
|
+
_ccMessages.push({ role: 'assistant', html: streamDiv.innerHTML });
|
|
227
|
+
if (evt.sessionId) { _ccSessionId = evt.sessionId; ccSaveState(); ccUpdateSessionIndicator(); }
|
|
228
|
+
if (evt.actions && evt.actions.length > 0) {
|
|
229
|
+
for (const action of evt.actions) { await ccExecuteAction(action); }
|
|
230
|
+
}
|
|
231
|
+
} else if (evt.type === 'error') {
|
|
232
|
+
streamDiv.innerHTML = '<span style="color:var(--red)">' + escHtml(evt.error) + '</span>';
|
|
233
|
+
}
|
|
234
|
+
} catch { /* incomplete JSON */ }
|
|
202
235
|
}
|
|
203
|
-
_ccSessionId = data.sessionId;
|
|
204
|
-
ccSaveState();
|
|
205
|
-
ccUpdateSessionIndicator();
|
|
206
236
|
}
|
|
207
237
|
|
|
208
|
-
// Render markdown response
|
|
209
|
-
const ccElapsed = Math.round((Date.now() - ccStartTime) / 1000);
|
|
210
|
-
const rendered = renderMd(data.text || '');
|
|
211
|
-
ccAddMessage('assistant', rendered + '<div style="font-size:9px;color:var(--muted);margin-top:6px;display:flex;justify-content:flex-end;padding-right:30px">' + ccElapsed + 's</div>');
|
|
212
|
-
|
|
213
238
|
// Execute actions
|
|
214
239
|
if (data.actions && data.actions.length > 0) {
|
|
215
240
|
for (const action of data.actions) {
|
package/dashboard.js
CHANGED
|
@@ -2980,6 +2980,55 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
2980
2980
|
} catch (e) { ccInFlight = false; return jsonReply(res, 500, { error: e.message }); }
|
|
2981
2981
|
}
|
|
2982
2982
|
|
|
2983
|
+
async function handleCommandCenterStream(req, res) {
|
|
2984
|
+
if (checkRateLimit('command-center', 10)) { res.statusCode = 429; res.end('Rate limited'); return; }
|
|
2985
|
+
try {
|
|
2986
|
+
const body = await readBody(req);
|
|
2987
|
+
if (!body.message) { res.statusCode = 400; res.end('message required'); return; }
|
|
2988
|
+
if (ccInFlight && (Date.now() - ccInFlightSince) < CC_INFLIGHT_TIMEOUT_MS) {
|
|
2989
|
+
res.statusCode = 429; res.end('CC busy'); return;
|
|
2990
|
+
}
|
|
2991
|
+
if (ccInFlight) console.log('[CC-stream] Auto-releasing stuck guard');
|
|
2992
|
+
ccInFlight = true;
|
|
2993
|
+
ccInFlightSince = Date.now();
|
|
2994
|
+
|
|
2995
|
+
res.writeHead(200, { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive' });
|
|
2996
|
+
|
|
2997
|
+
const sessionId = ccSessionValid() ? ccSession.sessionId : null;
|
|
2998
|
+
const preamble = buildCCStatePreamble();
|
|
2999
|
+
const prompt = preamble + '\n\n---\n\n' + body.message;
|
|
3000
|
+
|
|
3001
|
+
const { callLLMStreaming, trackEngineUsage: trackUsage } = require('./engine/llm');
|
|
3002
|
+
const result = await callLLMStreaming(prompt, CC_STATIC_SYSTEM_PROMPT, {
|
|
3003
|
+
timeout: 900000, label: 'command-center', model: 'sonnet', maxTurns: 50,
|
|
3004
|
+
allowedTools: 'Bash,Read,Write,Edit,Glob,Grep,WebFetch,WebSearch',
|
|
3005
|
+
sessionId,
|
|
3006
|
+
onChunk: (text) => {
|
|
3007
|
+
try { res.write('data: ' + JSON.stringify({ type: 'chunk', text }) + '\n\n'); } catch {}
|
|
3008
|
+
}
|
|
3009
|
+
});
|
|
3010
|
+
trackUsage('command-center', result.usage);
|
|
3011
|
+
|
|
3012
|
+
// Update session
|
|
3013
|
+
const now = Date.now();
|
|
3014
|
+
if (result.sessionId) {
|
|
3015
|
+
ccSession = { sessionId: result.sessionId, createdAt: ccSession.createdAt || now, lastActiveAt: now, turnCount: (ccSession.turnCount || 0) + 1 };
|
|
3016
|
+
safeWrite(path.join(ENGINE_DIR, 'cc-session.json'), ccSession);
|
|
3017
|
+
}
|
|
3018
|
+
|
|
3019
|
+
// Send final result with actions
|
|
3020
|
+
const { text: displayText, actions } = parseCCActions(result.text);
|
|
3021
|
+
res.write('data: ' + JSON.stringify({ type: 'done', text: displayText, actions, sessionId: ccSession.sessionId }) + '\n\n');
|
|
3022
|
+
res.end();
|
|
3023
|
+
ccInFlight = false;
|
|
3024
|
+
ccInFlightSince = 0;
|
|
3025
|
+
} catch (e) {
|
|
3026
|
+
ccInFlight = false;
|
|
3027
|
+
try { res.write('data: ' + JSON.stringify({ type: 'error', error: e.message }) + '\n\n'); } catch {}
|
|
3028
|
+
try { res.end(); } catch {}
|
|
3029
|
+
}
|
|
3030
|
+
}
|
|
3031
|
+
|
|
2983
3032
|
async function handleSchedulesList(req, res) {
|
|
2984
3033
|
reloadConfig();
|
|
2985
3034
|
const schedules = CONFIG.schedules || [];
|
|
@@ -3473,6 +3522,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
3473
3522
|
// Command Center
|
|
3474
3523
|
{ method: 'POST', path: '/api/command-center/new-session', desc: 'Clear active CC session', handler: handleCommandCenterNewSession },
|
|
3475
3524
|
{ method: 'POST', path: '/api/command-center', desc: 'Conversational command center with full minions context', params: 'message, sessionId?', handler: handleCommandCenter },
|
|
3525
|
+
{ method: 'POST', path: '/api/command-center/stream', desc: 'Streaming CC — SSE with text chunks as they arrive', params: 'message', handler: handleCommandCenterStream },
|
|
3476
3526
|
|
|
3477
3527
|
// Schedules
|
|
3478
3528
|
{ method: 'POST', path: '/api/schedules/parse-natural', desc: 'Parse natural language schedule text into cron expression', params: 'text', handler: handleSchedulesParseNatural },
|
package/engine/llm.js
CHANGED
|
@@ -108,8 +108,81 @@ function isResumeSessionStillValid(result) {
|
|
|
108
108
|
return false;
|
|
109
109
|
}
|
|
110
110
|
|
|
111
|
+
/**
|
|
112
|
+
* Streaming variant of callLLM — emits text chunks via onChunk callback.
|
|
113
|
+
* Returns the same result object as callLLM when the process completes.
|
|
114
|
+
* onChunk(text) is called for each assistant text block as it arrives.
|
|
115
|
+
*/
|
|
116
|
+
function callLLMStreaming(promptText, sysPromptText, { timeout = 120000, label = 'llm', model = 'sonnet', maxTurns = 1, allowedTools = '', sessionId = null, onChunk = () => {} } = {}) {
|
|
117
|
+
return new Promise((resolve) => {
|
|
118
|
+
const id = uid();
|
|
119
|
+
const tmpDir = path.join(ENGINE_DIR, 'tmp');
|
|
120
|
+
if (!require('fs').existsSync(tmpDir)) require('fs').mkdirSync(tmpDir, { recursive: true });
|
|
121
|
+
const promptPath = path.join(tmpDir, `${label}-prompt-${id}.md`);
|
|
122
|
+
const sysPath = path.join(tmpDir, `${label}-sys-${id}.md`);
|
|
123
|
+
safeWrite(promptPath, promptText);
|
|
124
|
+
safeWrite(sysPath, sysPromptText || '');
|
|
125
|
+
|
|
126
|
+
const spawnScript = path.join(ENGINE_DIR, 'spawn-agent.js');
|
|
127
|
+
const args = [
|
|
128
|
+
spawnScript, promptPath, sysPath,
|
|
129
|
+
'--output-format', 'stream-json', '--max-turns', String(maxTurns), '--model', model,
|
|
130
|
+
'--verbose',
|
|
131
|
+
];
|
|
132
|
+
if (allowedTools) args.push('--allowedTools', allowedTools);
|
|
133
|
+
args.push('--permission-mode', 'bypassPermissions');
|
|
134
|
+
if (sessionId) args.push('--resume', sessionId);
|
|
135
|
+
|
|
136
|
+
const proc = runFile(process.execPath, args, { cwd: MINIONS_DIR, stdio: ['pipe', 'pipe', 'pipe'], env: cleanChildEnv() });
|
|
137
|
+
|
|
138
|
+
let stdout = '';
|
|
139
|
+
let stderr = '';
|
|
140
|
+
let lineBuf = '';
|
|
141
|
+
|
|
142
|
+
proc.stdout.on('data', d => {
|
|
143
|
+
const chunk = d.toString();
|
|
144
|
+
stdout += chunk;
|
|
145
|
+
lineBuf += chunk;
|
|
146
|
+
// Parse complete lines for streaming text
|
|
147
|
+
const lines = lineBuf.split('\n');
|
|
148
|
+
lineBuf = lines.pop(); // keep incomplete line in buffer
|
|
149
|
+
for (const line of lines) {
|
|
150
|
+
const trimmed = line.trim();
|
|
151
|
+
if (!trimmed || !trimmed.startsWith('{')) continue;
|
|
152
|
+
try {
|
|
153
|
+
const obj = JSON.parse(trimmed);
|
|
154
|
+
if (obj.type === 'assistant' && obj.message?.content) {
|
|
155
|
+
for (const block of obj.message.content) {
|
|
156
|
+
if (block.type === 'text' && block.text) onChunk(block.text);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
} catch { /* incomplete JSON or non-JSON line */ }
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
proc.stderr.on('data', d => { stderr += d.toString(); });
|
|
163
|
+
|
|
164
|
+
const timer = setTimeout(() => { try { proc.kill('SIGTERM'); } catch {} }, timeout);
|
|
165
|
+
|
|
166
|
+
proc.on('close', (code) => {
|
|
167
|
+
clearTimeout(timer);
|
|
168
|
+
safeUnlink(promptPath);
|
|
169
|
+
safeUnlink(sysPath);
|
|
170
|
+
const parsed = parseStreamJsonOutput(stdout);
|
|
171
|
+
resolve({ text: parsed.text || '', usage: parsed.usage, sessionId: parsed.sessionId || null, code, stderr, raw: stdout });
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
proc.on('error', (err) => {
|
|
175
|
+
clearTimeout(timer);
|
|
176
|
+
safeUnlink(promptPath);
|
|
177
|
+
safeUnlink(sysPath);
|
|
178
|
+
resolve({ text: '', usage: null, sessionId: null, code: 1, stderr: err.message, raw: '' });
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
|
|
111
183
|
module.exports = {
|
|
112
184
|
callLLM,
|
|
185
|
+
callLLMStreaming,
|
|
113
186
|
trackEngineUsage,
|
|
114
187
|
isResumeSessionStillValid,
|
|
115
188
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yemi33/minions",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.255",
|
|
4
4
|
"description": "Multi-agent AI dev team that runs from ~/.minions/ — five autonomous agents share a single engine, dashboard, and knowledge base",
|
|
5
5
|
"bin": {
|
|
6
6
|
"minions": "bin/minions.js"
|