@yemi33/minions 0.1.252 → 0.1.254
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 +10 -0
- package/dashboard/js/command-center.js +52 -30
- package/dashboard.js +50 -0
- package/engine/llm.js +73 -0
- package/engine.js +4 -3
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.254 (2026-04-03)
|
|
4
|
+
|
|
5
|
+
### Features
|
|
6
|
+
- streaming CC responses — text appears as it arrives via SSE
|
|
7
|
+
|
|
8
|
+
## 0.1.253 (2026-04-03)
|
|
9
|
+
|
|
10
|
+
### Fixes
|
|
11
|
+
- sanitize dispatch ID in temp filenames for Windows compatibility
|
|
12
|
+
|
|
3
13
|
## 0.1.252 (2026-04-03)
|
|
4
14
|
|
|
5
15
|
### Features
|
|
@@ -171,45 +171,67 @@ async function _ccDoSend(message, skipUserMsg) {
|
|
|
171
171
|
}, 500);
|
|
172
172
|
|
|
173
173
|
try {
|
|
174
|
-
|
|
175
|
-
const res = await fetch('/api/command-center', {
|
|
174
|
+
// Stream response via SSE — shows text as it arrives
|
|
175
|
+
const res = await fetch('/api/command-center/stream', {
|
|
176
176
|
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
177
|
-
body: JSON.stringify({ message
|
|
178
|
-
signal:
|
|
177
|
+
body: JSON.stringify({ message }),
|
|
178
|
+
signal: AbortSignal.timeout(960000)
|
|
179
179
|
});
|
|
180
|
-
const data = await res.json();
|
|
181
180
|
|
|
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>' : ''));
|
|
181
|
+
if (!res.ok) {
|
|
182
|
+
clearInterval(ccTimer);
|
|
183
|
+
thinking.remove();
|
|
184
|
+
const errText = await res.text();
|
|
185
|
+
ccAddMessage('assistant', '<span style="color:var(--red)">' + escHtml(errText || 'CC error') + '</span>' +
|
|
186
|
+
(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
187
|
return;
|
|
190
188
|
}
|
|
191
189
|
|
|
192
|
-
//
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
190
|
+
// Create streaming message bubble
|
|
191
|
+
clearInterval(ccTimer);
|
|
192
|
+
thinking.remove();
|
|
193
|
+
const streamDiv = document.createElement('div');
|
|
194
|
+
streamDiv.className = 'cc-msg assistant';
|
|
195
|
+
streamDiv.innerHTML = '<span style="color:var(--muted);font-size:11px">Thinking...</span>';
|
|
196
|
+
document.getElementById('cc-messages').appendChild(streamDiv);
|
|
197
|
+
let streamedText = '';
|
|
198
|
+
|
|
199
|
+
const reader = res.body.getReader();
|
|
200
|
+
const decoder = new TextDecoder();
|
|
201
|
+
let buf = '';
|
|
202
|
+
|
|
203
|
+
while (true) {
|
|
204
|
+
const { done, value } = await reader.read();
|
|
205
|
+
if (done) break;
|
|
206
|
+
buf += decoder.decode(value, { stream: true });
|
|
207
|
+
const lines = buf.split('\n');
|
|
208
|
+
buf = lines.pop();
|
|
209
|
+
for (const line of lines) {
|
|
210
|
+
if (!line.startsWith('data: ')) continue;
|
|
211
|
+
try {
|
|
212
|
+
const evt = JSON.parse(line.slice(6));
|
|
213
|
+
if (evt.type === 'chunk') {
|
|
214
|
+
streamedText = evt.text; // each chunk is the full text so far for this turn
|
|
215
|
+
streamDiv.innerHTML = renderMd(streamedText);
|
|
216
|
+
const msgs = document.getElementById('cc-messages');
|
|
217
|
+
if (msgs.scrollHeight - msgs.scrollTop - msgs.clientHeight < 150) msgs.scrollTop = msgs.scrollHeight;
|
|
218
|
+
} else if (evt.type === 'done') {
|
|
219
|
+
// Final result — replace with rendered markdown + actions
|
|
220
|
+
const ccElapsed = Math.round((Date.now() - ccStartTime) / 1000);
|
|
221
|
+
streamDiv.innerHTML = renderMd(evt.text || streamedText || '') +
|
|
222
|
+
'<div style="font-size:9px;color:var(--muted);margin-top:6px;display:flex;justify-content:flex-end;padding-right:30px">' + ccElapsed + 's</div>';
|
|
223
|
+
_ccMessages.push({ role: 'assistant', html: streamDiv.innerHTML });
|
|
224
|
+
if (evt.sessionId) { _ccSessionId = evt.sessionId; ccSaveState(); ccUpdateSessionIndicator(); }
|
|
225
|
+
if (evt.actions && evt.actions.length > 0) {
|
|
226
|
+
for (const action of evt.actions) { await ccExecuteAction(action); }
|
|
227
|
+
}
|
|
228
|
+
} else if (evt.type === 'error') {
|
|
229
|
+
streamDiv.innerHTML = '<span style="color:var(--red)">' + escHtml(evt.error) + '</span>';
|
|
230
|
+
}
|
|
231
|
+
} catch { /* incomplete JSON */ }
|
|
202
232
|
}
|
|
203
|
-
_ccSessionId = data.sessionId;
|
|
204
|
-
ccSaveState();
|
|
205
|
-
ccUpdateSessionIndicator();
|
|
206
233
|
}
|
|
207
234
|
|
|
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
235
|
// Execute actions
|
|
214
236
|
if (data.actions && data.actions.length > 0) {
|
|
215
237
|
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/engine.js
CHANGED
|
@@ -431,10 +431,11 @@ function spawnAgent(dispatchItem, config) {
|
|
|
431
431
|
// Write prompt and system prompt to temp files (avoids shell escaping issues)
|
|
432
432
|
const tmpDir = path.join(ENGINE_DIR, 'tmp');
|
|
433
433
|
if (!fs.existsSync(tmpDir)) fs.mkdirSync(tmpDir, { recursive: true });
|
|
434
|
-
const
|
|
434
|
+
const safeId = id.replace(/[:\\/*?"<>|]/g, '-');
|
|
435
|
+
const promptPath = path.join(tmpDir, `prompt-${safeId}.md`);
|
|
435
436
|
safeWrite(promptPath, fullTaskPrompt);
|
|
436
437
|
|
|
437
|
-
const sysPromptPath = path.join(tmpDir, `sysprompt-${
|
|
438
|
+
const sysPromptPath = path.join(tmpDir, `sysprompt-${safeId}.md`);
|
|
438
439
|
safeWrite(sysPromptPath, systemPrompt);
|
|
439
440
|
|
|
440
441
|
// Build claude CLI args
|
|
@@ -555,7 +556,7 @@ function spawnAgent(dispatchItem, config) {
|
|
|
555
556
|
|
|
556
557
|
// Write new prompt with steering message
|
|
557
558
|
const steerPrompt = `Message from your human teammate:\n\n${steerMsg}\n\nRespond to this, then continue working on your current task.`;
|
|
558
|
-
const steerPromptPath = path.join(ENGINE_DIR, 'tmp', `prompt-steer-${
|
|
559
|
+
const steerPromptPath = path.join(ENGINE_DIR, 'tmp', `prompt-steer-${safeId}.md`);
|
|
559
560
|
safeWrite(steerPromptPath, steerPrompt);
|
|
560
561
|
|
|
561
562
|
// Build resume args
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yemi33/minions",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.254",
|
|
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"
|