@phren/agent 0.1.2 → 0.1.4
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/dist/agent-loop/index.js +214 -0
- package/dist/agent-loop/stream.js +124 -0
- package/dist/agent-loop/types.js +13 -0
- package/dist/agent-loop.js +7 -326
- package/dist/commands/info.js +146 -0
- package/dist/commands/memory.js +165 -0
- package/dist/commands/model.js +138 -0
- package/dist/commands/session.js +213 -0
- package/dist/commands.js +25 -297
- package/dist/config.js +6 -2
- package/dist/index.js +10 -4
- package/dist/mcp-client.js +11 -7
- package/dist/multi/multi-commands.js +170 -0
- package/dist/multi/multi-events.js +81 -0
- package/dist/multi/multi-render.js +146 -0
- package/dist/multi/pane.js +28 -0
- package/dist/multi/spawner.js +3 -2
- package/dist/multi/tui-multi.js +39 -454
- package/dist/permissions/allowlist.js +2 -2
- package/dist/permissions/shell-safety.js +8 -0
- package/dist/providers/anthropic.js +72 -33
- package/dist/providers/codex.js +121 -60
- package/dist/providers/openai-compat.js +6 -1
- package/dist/repl.js +2 -2
- package/dist/system-prompt.js +24 -26
- package/dist/tools/glob.js +30 -6
- package/dist/tools/shell.js +5 -2
- package/dist/tui/ansi.js +48 -0
- package/dist/tui/components/AgentMessage.js +5 -0
- package/dist/tui/components/App.js +70 -0
- package/dist/tui/components/Banner.js +44 -0
- package/dist/tui/components/ChatMessage.js +23 -0
- package/dist/tui/components/InputArea.js +23 -0
- package/dist/tui/components/Separator.js +7 -0
- package/dist/tui/components/StatusBar.js +25 -0
- package/dist/tui/components/SteerQueue.js +7 -0
- package/dist/tui/components/StreamingText.js +5 -0
- package/dist/tui/components/ThinkingIndicator.js +20 -0
- package/dist/tui/components/ToolCall.js +11 -0
- package/dist/tui/components/UserMessage.js +5 -0
- package/dist/tui/hooks/useKeyboardShortcuts.js +89 -0
- package/dist/tui/hooks/useSlashCommands.js +52 -0
- package/dist/tui/index.js +5 -0
- package/dist/tui/ink-entry.js +271 -0
- package/dist/tui/menu-mode.js +86 -0
- package/dist/tui/tool-render.js +43 -0
- package/dist/tui.js +378 -252
- package/package.json +9 -2
package/dist/multi/tui-multi.js
CHANGED
|
@@ -1,104 +1,9 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Multiplexed TUI for multi-agent orchestration.
|
|
3
|
-
*
|
|
4
|
-
* Full alternate-screen terminal with:
|
|
5
|
-
* - Top bar: agent tabs with status color coding
|
|
6
|
-
* - Main area: scrollback buffer for the selected agent
|
|
7
|
-
* - Bottom bar: input line + keyboard hints
|
|
8
|
-
*
|
|
9
|
-
* Keyboard:
|
|
10
|
-
* 1-9 Select agent pane by index
|
|
11
|
-
* Ctrl+Left/Right Cycle between agent panes
|
|
12
|
-
* Enter Send input / execute command
|
|
13
|
-
* /spawn <n> <t> Spawn a new agent
|
|
14
|
-
* /list List all agents
|
|
15
|
-
* /kill <name> Terminate an agent
|
|
16
|
-
* /broadcast <msg> Send message to all agents
|
|
17
|
-
* Ctrl+D Exit (kills all agents)
|
|
18
|
-
*/
|
|
1
|
+
/** Multiplexed TUI for multi-agent orchestration. */
|
|
19
2
|
import * as readline from "node:readline";
|
|
20
|
-
import {
|
|
21
|
-
import {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
const s = {
|
|
25
|
-
reset: `${ESC}0m`,
|
|
26
|
-
bold: (t) => `${ESC}1m${t}${ESC}0m`,
|
|
27
|
-
dim: (t) => `${ESC}2m${t}${ESC}0m`,
|
|
28
|
-
cyan: (t) => `${ESC}36m${t}${ESC}0m`,
|
|
29
|
-
green: (t) => `${ESC}32m${t}${ESC}0m`,
|
|
30
|
-
yellow: (t) => `${ESC}33m${t}${ESC}0m`,
|
|
31
|
-
red: (t) => `${ESC}31m${t}${ESC}0m`,
|
|
32
|
-
gray: (t) => `${ESC}90m${t}${ESC}0m`,
|
|
33
|
-
white: (t) => `${ESC}37m${t}${ESC}0m`,
|
|
34
|
-
bgGreen: (t) => `${ESC}42m${t}${ESC}0m`,
|
|
35
|
-
bgRed: (t) => `${ESC}41m${t}${ESC}0m`,
|
|
36
|
-
bgGray: (t) => `${ESC}100m${t}${ESC}0m`,
|
|
37
|
-
bgCyan: (t) => `${ESC}46m${t}${ESC}0m`,
|
|
38
|
-
bgYellow: (t) => `${ESC}43m${t}${ESC}0m`,
|
|
39
|
-
invert: (t) => `${ESC}7m${t}${ESC}0m`,
|
|
40
|
-
};
|
|
41
|
-
function stripAnsi(t) {
|
|
42
|
-
return t.replace(/\x1b\[[0-9;?]*[ -/]*[@-~]/g, "");
|
|
43
|
-
}
|
|
44
|
-
function cols() {
|
|
45
|
-
return process.stdout.columns || 80;
|
|
46
|
-
}
|
|
47
|
-
function rows() {
|
|
48
|
-
return process.stdout.rows || 24;
|
|
49
|
-
}
|
|
50
|
-
// ── Pane buffer ──────────────────────────────────────────────────────────────
|
|
51
|
-
const MAX_SCROLLBACK = 1000;
|
|
52
|
-
let nextPaneIndex = 0;
|
|
53
|
-
function createPane(agentId, name) {
|
|
54
|
-
return { agentId, name, index: nextPaneIndex++, lines: [], partial: "" };
|
|
55
|
-
}
|
|
56
|
-
function appendToPane(pane, text) {
|
|
57
|
-
// Merge with partial line buffer
|
|
58
|
-
const combined = pane.partial + text;
|
|
59
|
-
const parts = combined.split("\n");
|
|
60
|
-
// Everything except the last segment is a complete line
|
|
61
|
-
for (let i = 0; i < parts.length - 1; i++) {
|
|
62
|
-
pane.lines.push(parts[i]);
|
|
63
|
-
}
|
|
64
|
-
pane.partial = parts[parts.length - 1];
|
|
65
|
-
// Enforce scrollback cap
|
|
66
|
-
if (pane.lines.length > MAX_SCROLLBACK) {
|
|
67
|
-
pane.lines.splice(0, pane.lines.length - MAX_SCROLLBACK);
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
function flushPartial(pane) {
|
|
71
|
-
if (pane.partial) {
|
|
72
|
-
pane.lines.push(pane.partial);
|
|
73
|
-
pane.partial = "";
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
// ── Status color ─────────────────────────────────────────────────────────────
|
|
77
|
-
function statusColor(status) {
|
|
78
|
-
switch (status) {
|
|
79
|
-
case "starting": return s.yellow;
|
|
80
|
-
case "running": return s.green;
|
|
81
|
-
case "done": return s.gray;
|
|
82
|
-
case "error": return s.red;
|
|
83
|
-
case "cancelled": return s.gray;
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
// ── Tool call formatting ─────────────────────────────────────────────────────
|
|
87
|
-
function formatToolStart(toolName, input) {
|
|
88
|
-
const preview = JSON.stringify(input).slice(0, 60);
|
|
89
|
-
return s.dim(` > ${toolName}(${preview})...`);
|
|
90
|
-
}
|
|
91
|
-
function formatToolEnd(toolName, input, output, isError, durationMs) {
|
|
92
|
-
const dur = durationMs < 1000 ? `${durationMs}ms` : `${(durationMs / 1000).toFixed(1)}s`;
|
|
93
|
-
const icon = isError ? s.red("x") : s.green("ok");
|
|
94
|
-
const preview = JSON.stringify(input).slice(0, 50);
|
|
95
|
-
const header = s.dim(` ${toolName}(${preview})`) + ` ${icon} ${s.dim(dur)}`;
|
|
96
|
-
const allLines = output.split("\n");
|
|
97
|
-
const w = cols();
|
|
98
|
-
const body = allLines.slice(0, 4).map((l) => s.dim(` | ${l.slice(0, w - 6)}`)).join("\n");
|
|
99
|
-
const more = allLines.length > 4 ? `\n${s.dim(` | ... (${allLines.length} lines)`)}` : "";
|
|
100
|
-
return `${header}\n${body}${more}`;
|
|
101
|
-
}
|
|
3
|
+
import { MAX_SCROLLBACK, createPane, appendToPane } from "./pane.js";
|
|
4
|
+
import { ESC, s, render } from "./multi-render.js";
|
|
5
|
+
import { handleSlashCommand } from "./multi-commands.js";
|
|
6
|
+
import { wireSpawnerEvents } from "./multi-events.js";
|
|
102
7
|
// ── Main TUI ─────────────────────────────────────────────────────────────────
|
|
103
8
|
export async function startMultiTui(spawner, config) {
|
|
104
9
|
const w = process.stdout;
|
|
@@ -130,7 +35,7 @@ export async function startMultiTui(spawner, config) {
|
|
|
130
35
|
if (panes.has(agentId)) {
|
|
131
36
|
selectedId = agentId;
|
|
132
37
|
scrollOffset = 0;
|
|
133
|
-
|
|
38
|
+
doRender();
|
|
134
39
|
}
|
|
135
40
|
}
|
|
136
41
|
function selectByIndex(index) {
|
|
@@ -149,349 +54,29 @@ export async function startMultiTui(spawner, config) {
|
|
|
149
54
|
next = 0;
|
|
150
55
|
selectAgent(agentOrder[next]);
|
|
151
56
|
}
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
const tab = isSel ? s.invert(raw) : colored;
|
|
168
|
-
tabs.push(tab);
|
|
169
|
-
}
|
|
170
|
-
if (agents.length === 0) {
|
|
171
|
-
tabs.push(s.dim(" no agents "));
|
|
172
|
-
}
|
|
173
|
-
const title = s.bold(" phren-multi ");
|
|
174
|
-
const tabStr = tabs.join(s.dim("|"));
|
|
175
|
-
const line = title + s.dim("|") + tabStr;
|
|
176
|
-
const pad = Math.max(0, w_ - stripAnsi(line).length);
|
|
177
|
-
return s.invert(stripAnsi(title)) + s.dim("|") + tabStr + " ".repeat(pad);
|
|
178
|
-
}
|
|
179
|
-
function renderMainArea() {
|
|
180
|
-
const availRows = rows() - 3; // top bar + bottom bar + input line
|
|
181
|
-
if (availRows < 1)
|
|
182
|
-
return [];
|
|
183
|
-
if (!selectedId || !panes.has(selectedId)) {
|
|
184
|
-
const emptyMsg = s.dim(" No agent selected. Use /spawn <name> <task> to create one.");
|
|
185
|
-
const lines = [emptyMsg];
|
|
186
|
-
while (lines.length < availRows)
|
|
187
|
-
lines.push("");
|
|
188
|
-
return lines;
|
|
189
|
-
}
|
|
190
|
-
const pane = panes.get(selectedId);
|
|
191
|
-
// Include partial line if any
|
|
192
|
-
const allLines = [...pane.lines];
|
|
193
|
-
if (pane.partial)
|
|
194
|
-
allLines.push(pane.partial);
|
|
195
|
-
// Apply scroll offset
|
|
196
|
-
const totalLines = allLines.length;
|
|
197
|
-
let start = Math.max(0, totalLines - availRows - scrollOffset);
|
|
198
|
-
let end = start + availRows;
|
|
199
|
-
if (end > totalLines) {
|
|
200
|
-
end = totalLines;
|
|
201
|
-
start = Math.max(0, end - availRows);
|
|
202
|
-
}
|
|
203
|
-
const visible = allLines.slice(start, end);
|
|
204
|
-
const w_ = cols();
|
|
205
|
-
const output = [];
|
|
206
|
-
const paneStyle = getAgentStyle(pane.index);
|
|
207
|
-
const linePrefix = paneStyle.color(paneStyle.icon) + " ";
|
|
208
|
-
const prefixLen = 2; // icon + space
|
|
209
|
-
for (const line of visible) {
|
|
210
|
-
output.push(linePrefix + line.slice(0, w_ - prefixLen));
|
|
211
|
-
}
|
|
212
|
-
// Pad remaining rows
|
|
213
|
-
while (output.length < availRows)
|
|
214
|
-
output.push("");
|
|
215
|
-
return output;
|
|
216
|
-
}
|
|
217
|
-
function renderBottomBar() {
|
|
218
|
-
const w_ = cols();
|
|
219
|
-
const agentCount = spawner.listAgents().length;
|
|
220
|
-
const runningCount = spawner.getAgentsByStatus("running").length;
|
|
221
|
-
const left = ` Agents: ${agentCount} (${runningCount} running)`;
|
|
222
|
-
const right = `1-9:select Ctrl+</>:cycle /spawn /list /kill /broadcast Ctrl+D:exit `;
|
|
223
|
-
const pad = Math.max(0, w_ - left.length - right.length);
|
|
224
|
-
return s.invert(left + " ".repeat(pad) + right);
|
|
225
|
-
}
|
|
226
|
-
function renderInputLine() {
|
|
227
|
-
const prompt = s.cyan("multi> ");
|
|
228
|
-
return prompt + inputLine;
|
|
229
|
-
}
|
|
230
|
-
function render() {
|
|
231
|
-
// Hide cursor, move to top, clear screen
|
|
232
|
-
w.write(`${ESC}?25l${ESC}H${ESC}2J`);
|
|
233
|
-
// Top bar
|
|
234
|
-
w.write(renderTopBar());
|
|
235
|
-
w.write("\n");
|
|
236
|
-
// Main area
|
|
237
|
-
const mainLines = renderMainArea();
|
|
238
|
-
for (const line of mainLines) {
|
|
239
|
-
w.write(line + "\n");
|
|
240
|
-
}
|
|
241
|
-
// Bottom bar
|
|
242
|
-
w.write(renderBottomBar());
|
|
243
|
-
w.write("\n");
|
|
244
|
-
// Input line
|
|
245
|
-
w.write(renderInputLine());
|
|
246
|
-
// Show cursor
|
|
247
|
-
w.write(`${ESC}?25h`);
|
|
57
|
+
function doRender() {
|
|
58
|
+
render(w, spawner, panes, selectedId, scrollOffset, inputLine);
|
|
59
|
+
}
|
|
60
|
+
// ── Command context for slash commands ────────────────────────────────
|
|
61
|
+
function getCmdCtx() {
|
|
62
|
+
return {
|
|
63
|
+
spawner,
|
|
64
|
+
config,
|
|
65
|
+
panes,
|
|
66
|
+
agentOrder,
|
|
67
|
+
selectedId,
|
|
68
|
+
setSelectedId: (id) => { selectedId = id; },
|
|
69
|
+
getOrCreatePane,
|
|
70
|
+
render: doRender,
|
|
71
|
+
};
|
|
248
72
|
}
|
|
249
73
|
// ── Spawner event wiring ───────────────────────────────────────────────
|
|
250
|
-
spawner
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
});
|
|
256
|
-
spawner.on("text_block", (agentId, text) => {
|
|
257
|
-
const pane = getOrCreatePane(agentId);
|
|
258
|
-
appendToPane(pane, text + "\n");
|
|
259
|
-
if (agentId === selectedId)
|
|
260
|
-
render();
|
|
261
|
-
});
|
|
262
|
-
spawner.on("tool_start", (agentId, toolName, input) => {
|
|
263
|
-
const pane = getOrCreatePane(agentId);
|
|
264
|
-
flushPartial(pane);
|
|
265
|
-
appendToPane(pane, formatToolStart(toolName, input) + "\n");
|
|
266
|
-
if (agentId === selectedId)
|
|
267
|
-
render();
|
|
268
|
-
});
|
|
269
|
-
spawner.on("tool_end", (agentId, toolName, input, output, isError, durationMs) => {
|
|
270
|
-
const pane = getOrCreatePane(agentId);
|
|
271
|
-
flushPartial(pane);
|
|
272
|
-
const diffData = (toolName === "edit_file" || toolName === "write_file") ? decodeDiffPayload(output) : null;
|
|
273
|
-
const cleanOutput = diffData ? output.slice(0, output.indexOf(DIFF_MARKER)) : output;
|
|
274
|
-
appendToPane(pane, formatToolEnd(toolName, input, cleanOutput, isError, durationMs) + "\n");
|
|
275
|
-
if (diffData) {
|
|
276
|
-
appendToPane(pane, renderInlineDiff(diffData.oldContent, diffData.newContent, diffData.filePath) + "\n");
|
|
277
|
-
}
|
|
278
|
-
if (agentId === selectedId)
|
|
279
|
-
render();
|
|
280
|
-
});
|
|
281
|
-
spawner.on("status", (agentId, message) => {
|
|
282
|
-
const pane = getOrCreatePane(agentId);
|
|
283
|
-
appendToPane(pane, s.dim(message) + "\n");
|
|
284
|
-
if (agentId === selectedId)
|
|
285
|
-
render();
|
|
286
|
-
});
|
|
287
|
-
spawner.on("done", (agentId, result) => {
|
|
288
|
-
const pane = getOrCreatePane(agentId);
|
|
289
|
-
flushPartial(pane);
|
|
290
|
-
const style = getAgentStyle(pane.index);
|
|
291
|
-
appendToPane(pane, "\n" + style.color(`--- ${style.icon} Agent completed ---`) + "\n");
|
|
292
|
-
appendToPane(pane, s.dim(` Turns: ${result.turns} Tool calls: ${result.toolCalls}${result.totalCost ? ` Cost: ${result.totalCost}` : ""}`) + "\n");
|
|
293
|
-
render();
|
|
294
|
-
});
|
|
295
|
-
spawner.on("error", (agentId, error) => {
|
|
296
|
-
const pane = getOrCreatePane(agentId);
|
|
297
|
-
flushPartial(pane);
|
|
298
|
-
const style = getAgentStyle(pane.index);
|
|
299
|
-
appendToPane(pane, "\n" + style.color(`--- ${style.icon} Error: ${error} ---`) + "\n");
|
|
300
|
-
render();
|
|
301
|
-
});
|
|
302
|
-
spawner.on("exit", (agentId, code) => {
|
|
303
|
-
const pane = getOrCreatePane(agentId);
|
|
304
|
-
if (code !== null && code !== 0) {
|
|
305
|
-
appendToPane(pane, s.dim(` Process exited with code ${code}`) + "\n");
|
|
306
|
-
}
|
|
307
|
-
render();
|
|
74
|
+
wireSpawnerEvents(spawner, {
|
|
75
|
+
panes,
|
|
76
|
+
getOrCreatePane,
|
|
77
|
+
getSelectedId: () => selectedId,
|
|
78
|
+
render: doRender,
|
|
308
79
|
});
|
|
309
|
-
spawner.on("message", (from, to, content) => {
|
|
310
|
-
// Show in sender's pane
|
|
311
|
-
const senderPane = panes.get(from);
|
|
312
|
-
if (senderPane) {
|
|
313
|
-
flushPartial(senderPane);
|
|
314
|
-
const toName = panes.get(to)?.name ?? to;
|
|
315
|
-
appendToPane(senderPane, s.yellow(`[${senderPane.name} -> ${toName}] ${content}`) + "\n");
|
|
316
|
-
}
|
|
317
|
-
// Show in recipient's pane
|
|
318
|
-
const recipientPane = panes.get(to);
|
|
319
|
-
if (recipientPane) {
|
|
320
|
-
flushPartial(recipientPane);
|
|
321
|
-
const fromName = senderPane?.name ?? from;
|
|
322
|
-
appendToPane(recipientPane, s.yellow(`[${fromName} -> ${recipientPane.name}] ${content}`) + "\n");
|
|
323
|
-
}
|
|
324
|
-
if (from === selectedId || to === selectedId)
|
|
325
|
-
render();
|
|
326
|
-
});
|
|
327
|
-
// ── Slash command handling ─────────────────────────────────────────────
|
|
328
|
-
function handleSlashCommand(line) {
|
|
329
|
-
const parts = line.split(/\s+/);
|
|
330
|
-
const cmd = parts[0].toLowerCase();
|
|
331
|
-
if (cmd === "/spawn") {
|
|
332
|
-
const name = parts[1];
|
|
333
|
-
const task = parts.slice(2).join(" ");
|
|
334
|
-
if (!name || !task) {
|
|
335
|
-
appendToSystem("Usage: /spawn <name> <task>");
|
|
336
|
-
return true;
|
|
337
|
-
}
|
|
338
|
-
const opts = {
|
|
339
|
-
task,
|
|
340
|
-
cwd: process.cwd(),
|
|
341
|
-
provider: config.provider.name,
|
|
342
|
-
permissions: "auto-confirm",
|
|
343
|
-
verbose: config.verbose,
|
|
344
|
-
};
|
|
345
|
-
const agentId = spawner.spawn(opts);
|
|
346
|
-
const pane = getOrCreatePane(agentId);
|
|
347
|
-
pane.name = name;
|
|
348
|
-
appendToPane(pane, s.cyan(`Spawned agent "${name}" (${agentId}): ${task}`) + "\n");
|
|
349
|
-
selectAgent(agentId);
|
|
350
|
-
return true;
|
|
351
|
-
}
|
|
352
|
-
if (cmd === "/list") {
|
|
353
|
-
const agents = spawner.listAgents();
|
|
354
|
-
if (agents.length === 0) {
|
|
355
|
-
appendToSystem("No agents.");
|
|
356
|
-
}
|
|
357
|
-
else {
|
|
358
|
-
const lines = ["Agents:"];
|
|
359
|
-
for (let i = 0; i < agents.length; i++) {
|
|
360
|
-
const a = agents[i];
|
|
361
|
-
const pane = panes.get(a.id);
|
|
362
|
-
const name = pane?.name ?? a.id;
|
|
363
|
-
const color = statusColor(a.status);
|
|
364
|
-
const elapsed = a.finishedAt
|
|
365
|
-
? `${((a.finishedAt - a.startedAt) / 1000).toFixed(1)}s`
|
|
366
|
-
: `${((Date.now() - a.startedAt) / 1000).toFixed(0)}s`;
|
|
367
|
-
lines.push(` ${i + 1}. ${name} [${color(a.status)}] ${s.dim(elapsed)} — ${a.task.slice(0, 50)}`);
|
|
368
|
-
}
|
|
369
|
-
appendToSystem(lines.join("\n"));
|
|
370
|
-
}
|
|
371
|
-
return true;
|
|
372
|
-
}
|
|
373
|
-
if (cmd === "/kill") {
|
|
374
|
-
const target = parts[1];
|
|
375
|
-
if (!target) {
|
|
376
|
-
appendToSystem("Usage: /kill <name|index>");
|
|
377
|
-
return true;
|
|
378
|
-
}
|
|
379
|
-
const agentId = resolveAgentTarget(target);
|
|
380
|
-
if (!agentId) {
|
|
381
|
-
appendToSystem(`Agent "${target}" not found.`);
|
|
382
|
-
return true;
|
|
383
|
-
}
|
|
384
|
-
const ok = spawner.cancel(agentId);
|
|
385
|
-
const pane = getOrCreatePane(agentId);
|
|
386
|
-
if (ok) {
|
|
387
|
-
appendToPane(pane, s.yellow("\n--- Cancelled ---\n"));
|
|
388
|
-
}
|
|
389
|
-
else {
|
|
390
|
-
appendToSystem(`Agent "${target}" is not running.`);
|
|
391
|
-
}
|
|
392
|
-
render();
|
|
393
|
-
return true;
|
|
394
|
-
}
|
|
395
|
-
if (cmd === "/broadcast") {
|
|
396
|
-
const msg = parts.slice(1).join(" ");
|
|
397
|
-
if (!msg) {
|
|
398
|
-
appendToSystem("Usage: /broadcast <message>");
|
|
399
|
-
return true;
|
|
400
|
-
}
|
|
401
|
-
const agents = spawner.listAgents();
|
|
402
|
-
let sent = 0;
|
|
403
|
-
for (const a of agents) {
|
|
404
|
-
if (a.status === "running") {
|
|
405
|
-
const pane = getOrCreatePane(a.id);
|
|
406
|
-
appendToPane(pane, s.yellow(`[broadcast] ${msg}`) + "\n");
|
|
407
|
-
sent++;
|
|
408
|
-
}
|
|
409
|
-
}
|
|
410
|
-
appendToSystem(`Broadcast sent to ${sent} running agent(s).`);
|
|
411
|
-
return true;
|
|
412
|
-
}
|
|
413
|
-
if (cmd === "/msg") {
|
|
414
|
-
const target = parts[1];
|
|
415
|
-
const msg = parts.slice(2).join(" ");
|
|
416
|
-
if (!target || !msg) {
|
|
417
|
-
appendToSystem("Usage: /msg <agent> <text>");
|
|
418
|
-
return true;
|
|
419
|
-
}
|
|
420
|
-
const agentId = resolveAgentTarget(target);
|
|
421
|
-
if (!agentId) {
|
|
422
|
-
appendToSystem(`Agent "${target}" not found.`);
|
|
423
|
-
return true;
|
|
424
|
-
}
|
|
425
|
-
const ok = spawner.sendToAgent(agentId, msg, "user");
|
|
426
|
-
if (ok) {
|
|
427
|
-
const recipientPane = getOrCreatePane(agentId);
|
|
428
|
-
flushPartial(recipientPane);
|
|
429
|
-
appendToPane(recipientPane, s.yellow(`[user -> ${recipientPane.name}] ${msg}`) + "\n");
|
|
430
|
-
if (selectedId && selectedId !== agentId && panes.has(selectedId)) {
|
|
431
|
-
const curPane = panes.get(selectedId);
|
|
432
|
-
flushPartial(curPane);
|
|
433
|
-
appendToPane(curPane, s.yellow(`[user -> ${recipientPane.name}] ${msg}`) + "\n");
|
|
434
|
-
}
|
|
435
|
-
}
|
|
436
|
-
else {
|
|
437
|
-
appendToSystem(`Agent "${target}" is not running.`);
|
|
438
|
-
}
|
|
439
|
-
render();
|
|
440
|
-
return true;
|
|
441
|
-
}
|
|
442
|
-
if (cmd === "/help") {
|
|
443
|
-
appendToSystem([
|
|
444
|
-
"Commands:",
|
|
445
|
-
" /spawn <name> <task> — Spawn a new agent",
|
|
446
|
-
" /list — List all agents",
|
|
447
|
-
" /kill <name|index> — Terminate an agent",
|
|
448
|
-
" /msg <agent> <text> — Send direct message to an agent",
|
|
449
|
-
" /broadcast <msg> — Send to all running agents",
|
|
450
|
-
" /help — Show this help",
|
|
451
|
-
"",
|
|
452
|
-
"Keys:",
|
|
453
|
-
" 1-9 — Select agent by number",
|
|
454
|
-
" Ctrl+Left/Right — Cycle agents",
|
|
455
|
-
" PageUp/PageDown — Scroll output",
|
|
456
|
-
" Ctrl+D — Exit (kills all)",
|
|
457
|
-
].join("\n"));
|
|
458
|
-
return true;
|
|
459
|
-
}
|
|
460
|
-
return false;
|
|
461
|
-
}
|
|
462
|
-
function resolveAgentTarget(target) {
|
|
463
|
-
// Try numeric index (1-based)
|
|
464
|
-
const idx = parseInt(target, 10);
|
|
465
|
-
if (!isNaN(idx) && idx >= 1 && idx <= agentOrder.length) {
|
|
466
|
-
return agentOrder[idx - 1];
|
|
467
|
-
}
|
|
468
|
-
// Try name match
|
|
469
|
-
for (const [id, pane] of panes) {
|
|
470
|
-
if (pane.name === target)
|
|
471
|
-
return id;
|
|
472
|
-
}
|
|
473
|
-
// Try agent ID
|
|
474
|
-
if (spawner.getAgent(target))
|
|
475
|
-
return target;
|
|
476
|
-
return null;
|
|
477
|
-
}
|
|
478
|
-
function appendToSystem(text) {
|
|
479
|
-
if (!selectedId || !panes.has(selectedId)) {
|
|
480
|
-
// Create a virtual system pane
|
|
481
|
-
const pane = createPane("_system", "system");
|
|
482
|
-
panes.set("_system", pane);
|
|
483
|
-
if (!agentOrder.includes("_system"))
|
|
484
|
-
agentOrder.push("_system");
|
|
485
|
-
selectedId = "_system";
|
|
486
|
-
appendToPane(pane, text + "\n");
|
|
487
|
-
}
|
|
488
|
-
else {
|
|
489
|
-
const pane = panes.get(selectedId);
|
|
490
|
-
flushPartial(pane);
|
|
491
|
-
appendToPane(pane, text + "\n");
|
|
492
|
-
}
|
|
493
|
-
render();
|
|
494
|
-
}
|
|
495
80
|
// ── Terminal setup ─────────────────────────────────────────────────────
|
|
496
81
|
// Enter alternate screen
|
|
497
82
|
w.write("\x1b[?1049h");
|
|
@@ -531,7 +116,7 @@ export async function startMultiTui(spawner, config) {
|
|
|
531
116
|
if (key.ctrl && key.name === "c") {
|
|
532
117
|
if (inputLine.length > 0) {
|
|
533
118
|
inputLine = "";
|
|
534
|
-
|
|
119
|
+
doRender();
|
|
535
120
|
}
|
|
536
121
|
else {
|
|
537
122
|
shutdown();
|
|
@@ -554,15 +139,15 @@ export async function startMultiTui(spawner, config) {
|
|
|
554
139
|
}
|
|
555
140
|
// Page Up/Down — scroll
|
|
556
141
|
if (key.name === "pageup") {
|
|
557
|
-
const availRows = rows
|
|
142
|
+
const availRows = (process.stdout.rows || 24) - 3;
|
|
558
143
|
scrollOffset = Math.min(scrollOffset + Math.floor(availRows / 2), MAX_SCROLLBACK);
|
|
559
|
-
|
|
144
|
+
doRender();
|
|
560
145
|
return;
|
|
561
146
|
}
|
|
562
147
|
if (key.name === "pagedown") {
|
|
563
|
-
const availRows = rows
|
|
148
|
+
const availRows = (process.stdout.rows || 24) - 3;
|
|
564
149
|
scrollOffset = Math.max(0, scrollOffset - Math.floor(availRows / 2));
|
|
565
|
-
|
|
150
|
+
doRender();
|
|
566
151
|
return;
|
|
567
152
|
}
|
|
568
153
|
// Enter — submit input
|
|
@@ -570,12 +155,12 @@ export async function startMultiTui(spawner, config) {
|
|
|
570
155
|
const line = inputLine.trim();
|
|
571
156
|
inputLine = "";
|
|
572
157
|
if (!line) {
|
|
573
|
-
|
|
158
|
+
doRender();
|
|
574
159
|
return;
|
|
575
160
|
}
|
|
576
161
|
if (line.startsWith("/")) {
|
|
577
|
-
if (handleSlashCommand(line)) {
|
|
578
|
-
|
|
162
|
+
if (handleSlashCommand(line, getCmdCtx())) {
|
|
163
|
+
doRender();
|
|
579
164
|
return;
|
|
580
165
|
}
|
|
581
166
|
}
|
|
@@ -584,25 +169,25 @@ export async function startMultiTui(spawner, config) {
|
|
|
584
169
|
const pane = panes.get(selectedId);
|
|
585
170
|
appendToPane(pane, s.cyan(`> ${line}`) + "\n");
|
|
586
171
|
}
|
|
587
|
-
|
|
172
|
+
doRender();
|
|
588
173
|
return;
|
|
589
174
|
}
|
|
590
175
|
// Backspace
|
|
591
176
|
if (key.name === "backspace") {
|
|
592
177
|
if (inputLine.length > 0) {
|
|
593
178
|
inputLine = inputLine.slice(0, -1);
|
|
594
|
-
|
|
179
|
+
doRender();
|
|
595
180
|
}
|
|
596
181
|
return;
|
|
597
182
|
}
|
|
598
183
|
// Regular character input
|
|
599
184
|
if (key.sequence && !key.ctrl && !key.meta) {
|
|
600
185
|
inputLine += key.sequence;
|
|
601
|
-
|
|
186
|
+
doRender();
|
|
602
187
|
}
|
|
603
188
|
});
|
|
604
189
|
// Handle terminal resize
|
|
605
|
-
process.stdout.on("resize", () =>
|
|
190
|
+
process.stdout.on("resize", () => doRender());
|
|
606
191
|
// Register panes for any agents that already exist
|
|
607
192
|
for (const agent of spawner.listAgents()) {
|
|
608
193
|
getOrCreatePane(agent.id);
|
|
@@ -610,6 +195,6 @@ export async function startMultiTui(spawner, config) {
|
|
|
610
195
|
if (agentOrder.length > 0) {
|
|
611
196
|
selectedId = agentOrder[0];
|
|
612
197
|
}
|
|
613
|
-
|
|
198
|
+
doRender();
|
|
614
199
|
});
|
|
615
200
|
}
|
|
@@ -33,8 +33,8 @@ export function isAllowed(toolName, input) {
|
|
|
33
33
|
return false;
|
|
34
34
|
if (entry.pattern === "*")
|
|
35
35
|
return true;
|
|
36
|
-
// For file paths: exact match or child path
|
|
37
|
-
if (pattern.startsWith(entry.pattern))
|
|
36
|
+
// For file paths: exact match or child path (boundary-aware to prevent prefix collisions)
|
|
37
|
+
if (pattern === entry.pattern || pattern.startsWith(entry.pattern.endsWith("/") ? entry.pattern : entry.pattern + "/"))
|
|
38
38
|
return true;
|
|
39
39
|
// For shell commands: match the binary name
|
|
40
40
|
return entry.pattern === pattern;
|
|
@@ -11,6 +11,14 @@ const DANGEROUS_PATTERNS = [
|
|
|
11
11
|
{ pattern: /\bnohup\b/i, reason: "Detached process may outlive session", severity: "block" },
|
|
12
12
|
{ pattern: /\bdisown\b/i, reason: "Detached process may outlive session", severity: "block" },
|
|
13
13
|
{ pattern: /\bsetsid\b/i, reason: "Detached process may outlive session", severity: "block" },
|
|
14
|
+
// Block: Windows-specific destructive commands
|
|
15
|
+
{ pattern: /\bformat\s+[a-z]:/i, reason: "Disk format command", severity: "block" },
|
|
16
|
+
{ pattern: /\bdel\s+\/[sq]/i, reason: "Recursive or quiet delete", severity: "block" },
|
|
17
|
+
{ pattern: /\brd\s+\/s/i, reason: "Recursive directory removal", severity: "block" },
|
|
18
|
+
{ pattern: /\brmdir\s+\/s/i, reason: "Recursive directory removal", severity: "block" },
|
|
19
|
+
{ pattern: /\breg\s+delete\b/i, reason: "Registry deletion", severity: "block" },
|
|
20
|
+
{ pattern: /\bpowershell\b.*\b-enc\b/i, reason: "Encoded PowerShell command (obfuscation)", severity: "block" },
|
|
21
|
+
{ pattern: /\bcmd\b.*\/c.*\bdel\s+\/[sq]/i, reason: "Recursive or quiet delete via cmd", severity: "block" },
|
|
14
22
|
// Warn: potentially dangerous
|
|
15
23
|
{ pattern: /\beval\b/i, reason: "Dynamic code execution via eval", severity: "warn" },
|
|
16
24
|
{ pattern: /\$\(.*\)/, reason: "Command substitution", severity: "warn" },
|