@yemi33/minions 0.1.577 → 0.1.579

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 CHANGED
@@ -1,5 +1,15 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.579 (2026-04-08)
4
+
5
+ ### Fixes
6
+ - CC response renderer HTML-escape before innerHTML injection (closes #506) (#525)
7
+
8
+ ## 0.1.578 (2026-04-08)
9
+
10
+ ### Other
11
+ - refactor: extract _spawnProcess helper, fix dep fetch error tracking, cleanup file leak
12
+
3
13
  ## 0.1.577 (2026-04-08)
4
14
 
5
15
  ### Other
package/dashboard.html CHANGED
@@ -4732,10 +4732,11 @@ async function _ccDoSend(message, skipUserMsg) {
4732
4732
  ccUpdateSessionIndicator();
4733
4733
  }
4734
4734
 
4735
- // Render markdown-ish response
4735
+ // Render markdown-ish response (HTML-escape first to prevent XSS + broken rendering)
4736
4736
  const ccElapsed = Math.round((Date.now() - ccStartTime) / 1000);
4737
- const rendered = (data.text || '').replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
4738
- .replace(/`([^`]+)`/g, '<code style="background:var(--surface);padding:1px 4px;border-radius:3px;font-size:11px">$1</code>')
4737
+ const safe = escHtml(data.text || '');
4738
+ const rendered = safe.replace(/\*\*([^*\n]+)\*\*/g, '<strong>$1</strong>')
4739
+ .replace(/`([^`\n]+)`/g, '<code style="background:var(--surface);padding:1px 4px;border-radius:3px;font-size:11px">$1</code>')
4739
4740
  .replace(/\n/g, '<br>');
4740
4741
  ccAddMessage('assistant', rendered + '<div style="font-size:9px;color:var(--muted);margin-top:4px;text-align:right">' + ccElapsed + 's</div>');
4741
4742
 
package/engine/llm.js CHANGED
@@ -68,54 +68,62 @@ function _buildCliArgs({ model, maxTurns, allowedTools, effort, sessionId, sysPr
68
68
  return args;
69
69
  }
70
70
 
71
+ /**
72
+ * Spawn a claude CLI process. Returns { proc, cleanupFiles } or null if binary not cached.
73
+ * When direct=true, spawns claude CLI directly (fewer syscalls). Otherwise uses spawn-agent.js.
74
+ */
75
+ function _spawnProcess(promptText, sysPromptText, { direct, label, model, maxTurns, allowedTools, effort, sessionId }) {
76
+ const fs = require('fs');
77
+ const id = uid();
78
+ const tmpDir = path.join(ENGINE_DIR, 'tmp');
79
+ if (!fs.existsSync(tmpDir)) fs.mkdirSync(tmpDir, { recursive: true });
80
+
81
+ const cleanupFiles = [];
82
+ const resolved = direct ? _resolveClaudeBin() : null;
83
+
84
+ if (resolved) {
85
+ let sysTmpPath = null;
86
+ if (!sessionId && sysPromptText) {
87
+ sysTmpPath = path.join(tmpDir, `direct-sys-${id}.md`);
88
+ fs.writeFileSync(sysTmpPath, sysPromptText);
89
+ cleanupFiles.push(sysTmpPath);
90
+ }
91
+ const cliArgs = _buildCliArgs({ model, maxTurns, allowedTools, effort, sessionId, sysPromptFile: sysTmpPath });
92
+ const proc = resolved.native
93
+ ? runFile(resolved.bin, cliArgs, { cwd: MINIONS_DIR, stdio: ['pipe', 'pipe', 'pipe'], env: cleanChildEnv() })
94
+ : runFile(process.execPath, [resolved.bin, ...cliArgs], { cwd: MINIONS_DIR, stdio: ['pipe', 'pipe', 'pipe'], env: cleanChildEnv() });
95
+ try { proc.stdin.write(promptText); proc.stdin.end(); } catch { /* broken pipe */ }
96
+ return { proc, cleanupFiles };
97
+ }
98
+
99
+ // Indirect: use spawn-agent.js
100
+ const promptPath = path.join(tmpDir, `${label}-prompt-${id}.md`);
101
+ const sysPath = path.join(tmpDir, `${label}-sys-${id}.md`);
102
+ safeWrite(promptPath, promptText);
103
+ safeWrite(sysPath, sysPromptText || '');
104
+ cleanupFiles.push(promptPath, sysPath);
105
+
106
+ const spawnScript = path.join(ENGINE_DIR, 'spawn-agent.js');
107
+ const args = [
108
+ spawnScript, promptPath, sysPath,
109
+ '--output-format', 'stream-json', '--max-turns', String(maxTurns), '--model', model,
110
+ '--verbose',
111
+ ];
112
+ if (allowedTools) args.push('--allowedTools', allowedTools);
113
+ if (effort) args.push('--effort', effort);
114
+ args.push('--permission-mode', 'bypassPermissions');
115
+ if (sessionId) args.push('--resume', sessionId);
116
+
117
+ const proc = runFile(process.execPath, args, { cwd: MINIONS_DIR, stdio: ['pipe', 'pipe', 'pipe'], env: cleanChildEnv() });
118
+ return { proc, cleanupFiles };
119
+ }
120
+
71
121
  // ── Core LLM Call ───────────────────────────────────────────────────────────
72
122
 
73
123
  function callLLM(promptText, sysPromptText, { timeout = 120000, label = 'llm', model = 'sonnet', maxTurns = 1, allowedTools = '', sessionId = null, effort = null, direct = false } = {}) {
74
124
  return new Promise((resolve) => {
75
125
  const _startMs = Date.now();
76
- const fs = require('fs');
77
- const id = uid();
78
- const tmpDir = path.join(ENGINE_DIR, 'tmp');
79
- if (!fs.existsSync(tmpDir)) fs.mkdirSync(tmpDir, { recursive: true });
80
-
81
- let proc;
82
- const cleanupFiles = [];
83
- const resolved = direct ? _resolveClaudeBin() : null;
84
-
85
- if (resolved) {
86
- // Direct spawn: skip spawn-agent.js — fewer file syscalls, no extra process
87
- let sysTmpPath = null;
88
- if (!sessionId && sysPromptText) {
89
- sysTmpPath = path.join(tmpDir, `direct-sys-${id}.md`);
90
- fs.writeFileSync(sysTmpPath, sysPromptText);
91
- cleanupFiles.push(sysTmpPath);
92
- }
93
- const cliArgs = _buildCliArgs({ model, maxTurns, allowedTools, effort, sessionId, sysPromptFile: sysTmpPath });
94
- proc = resolved.native
95
- ? runFile(resolved.bin, cliArgs, { cwd: MINIONS_DIR, stdio: ['pipe', 'pipe', 'pipe'], env: cleanChildEnv() })
96
- : runFile(process.execPath, [resolved.bin, ...cliArgs], { cwd: MINIONS_DIR, stdio: ['pipe', 'pipe', 'pipe'], env: cleanChildEnv() });
97
- try { proc.stdin.write(promptText); proc.stdin.end(); } catch { /* broken pipe */ }
98
- } else {
99
- // Indirect: use spawn-agent.js (for agent dispatches or if binary not cached)
100
- const promptPath = path.join(tmpDir, `${label}-prompt-${id}.md`);
101
- const sysPath = path.join(tmpDir, `${label}-sys-${id}.md`);
102
- safeWrite(promptPath, promptText);
103
- safeWrite(sysPath, sysPromptText || '');
104
- cleanupFiles.push(promptPath, sysPath);
105
-
106
- const spawnScript = path.join(ENGINE_DIR, 'spawn-agent.js');
107
- const args = [
108
- spawnScript, promptPath, sysPath,
109
- '--output-format', 'stream-json', '--max-turns', String(maxTurns), '--model', model,
110
- '--verbose',
111
- ];
112
- if (allowedTools) args.push('--allowedTools', allowedTools);
113
- if (effort) args.push('--effort', effort);
114
- args.push('--permission-mode', 'bypassPermissions');
115
- if (sessionId) args.push('--resume', sessionId);
116
-
117
- proc = runFile(process.execPath, args, { cwd: MINIONS_DIR, stdio: ['pipe', 'pipe', 'pipe'], env: cleanChildEnv() });
118
- }
126
+ const { proc, cleanupFiles } = _spawnProcess(promptText, sysPromptText, { direct, label, model, maxTurns, allowedTools, effort, sessionId });
119
127
 
120
128
  let stdout = '';
121
129
  let stderr = '';
@@ -167,47 +175,7 @@ function callLLMStreaming(promptText, sysPromptText, { timeout = 120000, label =
167
175
  let _abort = null;
168
176
  const promise = new Promise((resolve) => {
169
177
  const _startMs = Date.now();
170
- const fs = require('fs');
171
- const id = uid();
172
- const tmpDir = path.join(ENGINE_DIR, 'tmp');
173
- if (!fs.existsSync(tmpDir)) fs.mkdirSync(tmpDir, { recursive: true });
174
-
175
- let proc;
176
- const cleanupFiles = [];
177
- const resolved = direct ? _resolveClaudeBin() : null;
178
-
179
- if (resolved) {
180
- let sysTmpPath = null;
181
- if (!sessionId && sysPromptText) {
182
- sysTmpPath = path.join(tmpDir, `direct-sys-${id}.md`);
183
- fs.writeFileSync(sysTmpPath, sysPromptText);
184
- cleanupFiles.push(sysTmpPath);
185
- }
186
- const cliArgs = _buildCliArgs({ model, maxTurns, allowedTools, effort, sessionId, sysPromptFile: sysTmpPath });
187
- proc = resolved.native
188
- ? runFile(resolved.bin, cliArgs, { cwd: MINIONS_DIR, stdio: ['pipe', 'pipe', 'pipe'], env: cleanChildEnv() })
189
- : runFile(process.execPath, [resolved.bin, ...cliArgs], { cwd: MINIONS_DIR, stdio: ['pipe', 'pipe', 'pipe'], env: cleanChildEnv() });
190
- try { proc.stdin.write(promptText); proc.stdin.end(); } catch { /* broken pipe */ }
191
- } else {
192
- const promptPath = path.join(tmpDir, `${label}-prompt-${id}.md`);
193
- const sysPath = path.join(tmpDir, `${label}-sys-${id}.md`);
194
- safeWrite(promptPath, promptText);
195
- safeWrite(sysPath, sysPromptText || '');
196
- cleanupFiles.push(promptPath, sysPath);
197
-
198
- const spawnScript = path.join(ENGINE_DIR, 'spawn-agent.js');
199
- const args = [
200
- spawnScript, promptPath, sysPath,
201
- '--output-format', 'stream-json', '--max-turns', String(maxTurns), '--model', model,
202
- '--verbose',
203
- ];
204
- if (allowedTools) args.push('--allowedTools', allowedTools);
205
- if (effort) args.push('--effort', effort);
206
- args.push('--permission-mode', 'bypassPermissions');
207
- if (sessionId) args.push('--resume', sessionId);
208
-
209
- proc = runFile(process.execPath, args, { cwd: MINIONS_DIR, stdio: ['pipe', 'pipe', 'pipe'], env: cleanChildEnv() });
210
- }
178
+ const { proc, cleanupFiles } = _spawnProcess(promptText, sysPromptText, { direct, label, model, maxTurns, allowedTools, effort, sessionId });
211
179
 
212
180
  _abort = () => { shared.killImmediate(proc); };
213
181
 
package/engine.js CHANGED
@@ -292,6 +292,7 @@ async function spawnAgent(dispatchItem, config) {
292
292
  safeWrite(promptPath, fullTaskPrompt);
293
293
  const sysPromptPath = path.join(tmpDir, `sysprompt-${safeId}.md`);
294
294
  safeWrite(sysPromptPath, systemPrompt);
295
+ const _cleanupPromptFiles = () => { safeUnlink(promptPath); safeUnlink(sysPromptPath); };
295
296
 
296
297
  if (branchName) {
297
298
  const wtSuffix = id ? id.split('-').pop() : shared.uid();
@@ -440,11 +441,11 @@ async function spawnAgent(dispatchItem, config) {
440
441
  execAsync(`git fetch origin "${depBranch}"`, { ..._gitOpts, cwd: rootDir }).then(() => depBranch)
441
442
  )
442
443
  );
443
- for (const r of fetchResults) {
444
- if (r.status === 'rejected') {
445
- const failedBranch = fetchable.find(d => r.reason?.message?.includes(d.branch))?.branch || 'unknown';
444
+ for (let i = 0; i < fetchResults.length; i++) {
445
+ if (fetchResults[i].status === 'rejected') {
446
+ const failedBranch = fetchable[i].branch;
446
447
  _failedRefCache.add(failedBranch);
447
- log('warn', `Failed to fetch dependency ${failedBranch}: ${r.reason?.message}`);
448
+ log('warn', `Failed to fetch dependency ${failedBranch}: ${fetchResults[i].reason?.message}`);
448
449
  depMergeFailed = true;
449
450
  }
450
451
  }
@@ -462,6 +463,7 @@ async function spawnAgent(dispatchItem, config) {
462
463
  }
463
464
  }
464
465
  if (depMergeFailed) {
466
+ _cleanupPromptFiles();
465
467
  completeDispatch(id, DISPATCH_RESULT.ERROR, `Dependency merge failed — will retry next tick`);
466
468
  return;
467
469
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.577",
3
+ "version": "0.1.579",
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"