@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/commands.js
CHANGED
|
@@ -1,17 +1,10 @@
|
|
|
1
|
-
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import { formatProviderList, formatModelAddHelp, addCustomModel, removeCustomModel } from "./multi/provider-manager.js";
|
|
1
|
+
// Sub-module handlers
|
|
2
|
+
import { helpCommand, turnsCommand, clearCommand, cwdCommand, filesCommand, costCommand, planCommand, undoCommand, contextCommand } from "./commands/info.js";
|
|
3
|
+
import { sessionCommand, historyCommand, compactCommand, diffCommand, gitCommand } from "./commands/session.js";
|
|
4
|
+
import { memCommand, askCommand } from "./commands/memory.js";
|
|
5
|
+
import { modelCommand, providerCommand, presetCommand } from "./commands/model.js";
|
|
7
6
|
const DIM = "\x1b[2m";
|
|
8
|
-
const BOLD = "\x1b[1m";
|
|
9
|
-
const CYAN = "\x1b[36m";
|
|
10
|
-
const GREEN = "\x1b[32m";
|
|
11
|
-
const RED = "\x1b[31m";
|
|
12
|
-
const YELLOW = "\x1b[33m";
|
|
13
7
|
const RESET = "\x1b[0m";
|
|
14
|
-
const HISTORY_MAX_LINES = 5;
|
|
15
8
|
export function createCommandContext(session, contextLimit) {
|
|
16
9
|
return {
|
|
17
10
|
session,
|
|
@@ -19,227 +12,33 @@ export function createCommandContext(session, contextLimit) {
|
|
|
19
12
|
undoStack: [],
|
|
20
13
|
};
|
|
21
14
|
}
|
|
22
|
-
/** Truncate text to N lines, appending [+M lines] if overflow. */
|
|
23
|
-
function truncateText(text, maxLines) {
|
|
24
|
-
const lines = text.split("\n");
|
|
25
|
-
if (lines.length <= maxLines)
|
|
26
|
-
return text;
|
|
27
|
-
const overflow = lines.length - maxLines;
|
|
28
|
-
return lines.slice(0, maxLines).join("\n") + `\n${DIM}[+${overflow} lines]${RESET}`;
|
|
29
|
-
}
|
|
30
15
|
/**
|
|
31
16
|
* Try to handle a slash command. Returns true if the input was a command.
|
|
17
|
+
* Returns a Promise<boolean> for async commands like /ask.
|
|
32
18
|
*/
|
|
33
19
|
export function handleCommand(input, ctx) {
|
|
34
20
|
const parts = input.trim().split(/\s+/);
|
|
35
21
|
const name = parts[0];
|
|
36
22
|
switch (name) {
|
|
37
|
-
case "/help":
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
return true;
|
|
57
|
-
case "/model": {
|
|
58
|
-
const sub = parts[1]?.toLowerCase();
|
|
59
|
-
// /model add <id> [provider=X] [context=N] [reasoning=X]
|
|
60
|
-
if (sub === "add") {
|
|
61
|
-
const modelId = parts[2];
|
|
62
|
-
if (!modelId) {
|
|
63
|
-
process.stderr.write(formatModelAddHelp() + "\n");
|
|
64
|
-
return true;
|
|
65
|
-
}
|
|
66
|
-
let provider = ctx.providerName ?? "openrouter";
|
|
67
|
-
let contextWindow = 128_000;
|
|
68
|
-
let reasoning = null;
|
|
69
|
-
const reasoningRange = [];
|
|
70
|
-
for (const arg of parts.slice(3)) {
|
|
71
|
-
const [k, v] = arg.split("=", 2);
|
|
72
|
-
if (k === "provider")
|
|
73
|
-
provider = v;
|
|
74
|
-
else if (k === "context")
|
|
75
|
-
contextWindow = parseInt(v, 10) || 128_000;
|
|
76
|
-
else if (k === "reasoning") {
|
|
77
|
-
reasoning = v;
|
|
78
|
-
reasoningRange.push("low", "medium", "high");
|
|
79
|
-
if (v === "max")
|
|
80
|
-
reasoningRange.push("max");
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
addCustomModel(modelId, provider, { contextWindow, reasoning, reasoningRange });
|
|
84
|
-
process.stderr.write(`${GREEN}→ Added ${modelId} to ${provider}${RESET}\n`);
|
|
85
|
-
return true;
|
|
86
|
-
}
|
|
87
|
-
// /model remove <id>
|
|
88
|
-
if (sub === "remove" || sub === "rm") {
|
|
89
|
-
const modelId = parts[2];
|
|
90
|
-
if (!modelId) {
|
|
91
|
-
process.stderr.write(`${DIM}Usage: /model remove <model-id>${RESET}\n`);
|
|
92
|
-
return true;
|
|
93
|
-
}
|
|
94
|
-
const ok = removeCustomModel(modelId);
|
|
95
|
-
process.stderr.write(ok ? `${GREEN}→ Removed ${modelId}${RESET}\n` : `${DIM}Model "${modelId}" not found in custom models.${RESET}\n`);
|
|
96
|
-
return true;
|
|
97
|
-
}
|
|
98
|
-
// /model (no sub) — interactive picker
|
|
99
|
-
if (!ctx.providerName) {
|
|
100
|
-
process.stderr.write(`${DIM}Provider not configured. Start with --provider to set one.${RESET}\n`);
|
|
101
|
-
return true;
|
|
102
|
-
}
|
|
103
|
-
showModelPicker(ctx.providerName, ctx.currentModel, process.stdout).then((result) => {
|
|
104
|
-
if (result && ctx.onModelChange) {
|
|
105
|
-
ctx.onModelChange(result);
|
|
106
|
-
const reasoningLabel = result.reasoning ? ` (reasoning: ${result.reasoning})` : "";
|
|
107
|
-
process.stderr.write(`${GREEN}→ ${result.model}${reasoningLabel}${RESET}\n`);
|
|
108
|
-
}
|
|
109
|
-
else if (result) {
|
|
110
|
-
process.stderr.write(`${DIM}Model selected: ${result.model} — restart to apply.${RESET}\n`);
|
|
111
|
-
}
|
|
112
|
-
});
|
|
113
|
-
return true;
|
|
114
|
-
}
|
|
115
|
-
case "/provider": {
|
|
116
|
-
process.stderr.write(formatProviderList());
|
|
117
|
-
return true;
|
|
118
|
-
}
|
|
119
|
-
case "/turns": {
|
|
120
|
-
const tokens = estimateMessageTokens(ctx.session.messages);
|
|
121
|
-
const pct = ctx.contextLimit > 0 ? ((tokens / ctx.contextLimit) * 100).toFixed(1) : "?";
|
|
122
|
-
const costLine = ctx.costTracker ? ` Cost: $${ctx.costTracker.totalCost.toFixed(4)}` : "";
|
|
123
|
-
process.stderr.write(`${DIM}Turns: ${ctx.session.turns} Tool calls: ${ctx.session.toolCalls} ` +
|
|
124
|
-
`Messages: ${ctx.session.messages.length} Tokens: ~${tokens} (${pct}%)${costLine}${RESET}\n`);
|
|
125
|
-
return true;
|
|
126
|
-
}
|
|
127
|
-
case "/clear":
|
|
128
|
-
ctx.session.messages.length = 0;
|
|
129
|
-
ctx.session.turns = 0;
|
|
130
|
-
ctx.session.toolCalls = 0;
|
|
131
|
-
ctx.undoStack.length = 0;
|
|
132
|
-
process.stderr.write(`${DIM}Conversation cleared.${RESET}\n`);
|
|
133
|
-
return true;
|
|
134
|
-
case "/cost": {
|
|
135
|
-
const ct = ctx.costTracker;
|
|
136
|
-
if (ct) {
|
|
137
|
-
process.stderr.write(`${DIM}Tokens — input: ${ct.inputTokens} output: ${ct.outputTokens} est. cost: $${ct.totalCost.toFixed(4)}${RESET}\n`);
|
|
138
|
-
}
|
|
139
|
-
else {
|
|
140
|
-
process.stderr.write(`${DIM}Cost tracking not available.${RESET}\n`);
|
|
141
|
-
}
|
|
142
|
-
return true;
|
|
143
|
-
}
|
|
144
|
-
case "/plan": {
|
|
145
|
-
const tools = [];
|
|
146
|
-
for (const msg of ctx.session.messages) {
|
|
147
|
-
if (typeof msg.content !== "string") {
|
|
148
|
-
for (const block of msg.content) {
|
|
149
|
-
if (block.type === "tool_use") {
|
|
150
|
-
tools.push(block.name);
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
if (tools.length === 0) {
|
|
156
|
-
process.stderr.write(`${DIM}No tool calls yet.${RESET}\n`);
|
|
157
|
-
}
|
|
158
|
-
else {
|
|
159
|
-
process.stderr.write(`${DIM}Tool calls (${tools.length}): ${tools.join(" → ")}${RESET}\n`);
|
|
160
|
-
}
|
|
161
|
-
return true;
|
|
162
|
-
}
|
|
163
|
-
case "/undo": {
|
|
164
|
-
if (ctx.session.messages.length < 2) {
|
|
165
|
-
process.stderr.write(`${DIM}Nothing to undo.${RESET}\n`);
|
|
166
|
-
return true;
|
|
167
|
-
}
|
|
168
|
-
// Remove messages back to the previous user message
|
|
169
|
-
let removed = 0;
|
|
170
|
-
while (ctx.session.messages.length > 0) {
|
|
171
|
-
const last = ctx.session.messages.pop();
|
|
172
|
-
removed++;
|
|
173
|
-
if (last?.role === "user" && typeof last.content === "string")
|
|
174
|
-
break;
|
|
175
|
-
}
|
|
176
|
-
process.stderr.write(`${DIM}Undid ${removed} messages.${RESET}\n`);
|
|
177
|
-
return true;
|
|
178
|
-
}
|
|
179
|
-
case "/history": {
|
|
180
|
-
const msgs = ctx.session.messages;
|
|
181
|
-
if (msgs.length === 0) {
|
|
182
|
-
process.stderr.write(`${DIM}No messages yet.${RESET}\n`);
|
|
183
|
-
return true;
|
|
184
|
-
}
|
|
185
|
-
const arg = parts[1];
|
|
186
|
-
const isFull = arg === "full";
|
|
187
|
-
const count = isFull ? msgs.length : Math.min(parseInt(arg, 10) || 10, msgs.length);
|
|
188
|
-
const slice = msgs.slice(-count);
|
|
189
|
-
const tokens = estimateMessageTokens(msgs);
|
|
190
|
-
const pct = ctx.contextLimit > 0 ? ((tokens / ctx.contextLimit) * 100).toFixed(1) : "?";
|
|
191
|
-
process.stderr.write(`${DIM}── History (${slice.length}/${msgs.length} messages, ~${tokens} tokens, ${pct}% context) ──${RESET}\n`);
|
|
192
|
-
for (const msg of slice) {
|
|
193
|
-
if (msg.role === "user") {
|
|
194
|
-
if (typeof msg.content === "string") {
|
|
195
|
-
const truncated = truncateText(msg.content, isFull ? Infinity : HISTORY_MAX_LINES);
|
|
196
|
-
process.stderr.write(`\n${CYAN}${BOLD}You:${RESET} ${truncated}\n`);
|
|
197
|
-
}
|
|
198
|
-
else {
|
|
199
|
-
// Tool results
|
|
200
|
-
for (const block of msg.content) {
|
|
201
|
-
if (block.type === "tool_result") {
|
|
202
|
-
const icon = block.is_error ? `${RED}✗${RESET}` : `${GREEN}✓${RESET}`;
|
|
203
|
-
const preview = (block.content ?? "").slice(0, 80).replace(/\n/g, " ");
|
|
204
|
-
process.stderr.write(`${DIM} ${icon} tool_result ${preview}${preview.length >= 80 ? "..." : ""}${RESET}\n`);
|
|
205
|
-
}
|
|
206
|
-
else if (block.type === "text") {
|
|
207
|
-
process.stderr.write(`${DIM} ${block.text.slice(0, 100)}${RESET}\n`);
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
else if (msg.role === "assistant") {
|
|
213
|
-
if (typeof msg.content === "string") {
|
|
214
|
-
const rendered = isFull ? renderMarkdown(msg.content) : truncateText(msg.content, HISTORY_MAX_LINES);
|
|
215
|
-
process.stderr.write(`\n${GREEN}${BOLD}Agent:${RESET}\n${rendered}\n`);
|
|
216
|
-
}
|
|
217
|
-
else {
|
|
218
|
-
for (const block of msg.content) {
|
|
219
|
-
if (block.type === "text") {
|
|
220
|
-
const text = block.text;
|
|
221
|
-
const rendered = isFull ? renderMarkdown(text) : truncateText(text, HISTORY_MAX_LINES);
|
|
222
|
-
process.stderr.write(`\n${GREEN}${BOLD}Agent:${RESET}\n${rendered}\n`);
|
|
223
|
-
}
|
|
224
|
-
else if (block.type === "tool_use") {
|
|
225
|
-
const tb = block;
|
|
226
|
-
const inputPreview = JSON.stringify(tb.input).slice(0, 60);
|
|
227
|
-
process.stderr.write(`${YELLOW} ⚡ ${tb.name}${RESET}${DIM}(${inputPreview})${RESET}\n`);
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
process.stderr.write(`${DIM}── end ──${RESET}\n`);
|
|
234
|
-
return true;
|
|
235
|
-
}
|
|
236
|
-
case "/compact": {
|
|
237
|
-
const before = ctx.session.messages.length;
|
|
238
|
-
ctx.session.messages = pruneMessages(ctx.session.messages, { contextLimit: ctx.contextLimit, keepRecentTurns: 4 });
|
|
239
|
-
const after = ctx.session.messages.length;
|
|
240
|
-
process.stderr.write(`${DIM}Compacted: ${before} → ${after} messages.${RESET}\n`);
|
|
241
|
-
return true;
|
|
242
|
-
}
|
|
23
|
+
case "/help": return helpCommand(parts, ctx);
|
|
24
|
+
case "/turns": return turnsCommand(parts, ctx);
|
|
25
|
+
case "/clear": return clearCommand(parts, ctx);
|
|
26
|
+
case "/cwd": return cwdCommand(parts, ctx);
|
|
27
|
+
case "/files": return filesCommand(parts, ctx);
|
|
28
|
+
case "/cost": return costCommand(parts, ctx);
|
|
29
|
+
case "/plan": return planCommand(parts, ctx);
|
|
30
|
+
case "/undo": return undoCommand(parts, ctx);
|
|
31
|
+
case "/context": return contextCommand(parts, ctx);
|
|
32
|
+
case "/model": return modelCommand(parts, ctx);
|
|
33
|
+
case "/provider": return providerCommand(parts, ctx);
|
|
34
|
+
case "/preset": return presetCommand(parts, ctx);
|
|
35
|
+
case "/session": return sessionCommand(parts, ctx);
|
|
36
|
+
case "/history": return historyCommand(parts, ctx);
|
|
37
|
+
case "/compact": return compactCommand(parts, ctx);
|
|
38
|
+
case "/diff": return diffCommand(parts, ctx);
|
|
39
|
+
case "/git": return gitCommand(parts, ctx);
|
|
40
|
+
case "/mem": return memCommand(parts, ctx);
|
|
41
|
+
case "/ask": return askCommand(parts, ctx);
|
|
243
42
|
case "/spawn": {
|
|
244
43
|
if (!ctx.spawner) {
|
|
245
44
|
process.stderr.write(`${DIM}Spawner not available. Start with --multi or --team to enable.${RESET}\n`);
|
|
@@ -275,77 +74,6 @@ export function handleCommand(input, ctx) {
|
|
|
275
74
|
}
|
|
276
75
|
return true;
|
|
277
76
|
}
|
|
278
|
-
case "/preset": {
|
|
279
|
-
const sub = parts[1]?.toLowerCase();
|
|
280
|
-
if (!sub || sub === "list") {
|
|
281
|
-
const all = listPresets();
|
|
282
|
-
if (all.length === 0) {
|
|
283
|
-
process.stderr.write(`${DIM}No presets.${RESET}\n`);
|
|
284
|
-
}
|
|
285
|
-
else {
|
|
286
|
-
const lines = all.map((p) => ` ${formatPreset(p.name, p.preset, p.builtin)}`);
|
|
287
|
-
process.stderr.write(`${DIM}Presets:\n${lines.join("\n")}${RESET}\n`);
|
|
288
|
-
}
|
|
289
|
-
return true;
|
|
290
|
-
}
|
|
291
|
-
if (sub === "save") {
|
|
292
|
-
// /preset save <name> [provider=X] [model=X] [permissions=X] [max-turns=N] [budget=N] [plan]
|
|
293
|
-
const presetName = parts[2];
|
|
294
|
-
if (!presetName) {
|
|
295
|
-
process.stderr.write(`${DIM}Usage: /preset save <name> [provider=X] [model=X] [permissions=X] [max-turns=N] [budget=N] [plan]${RESET}\n`);
|
|
296
|
-
return true;
|
|
297
|
-
}
|
|
298
|
-
const preset = {};
|
|
299
|
-
for (const arg of parts.slice(3)) {
|
|
300
|
-
const [k, v] = arg.split("=", 2);
|
|
301
|
-
if (k === "provider")
|
|
302
|
-
preset.provider = v;
|
|
303
|
-
else if (k === "model")
|
|
304
|
-
preset.model = v;
|
|
305
|
-
else if (k === "permissions")
|
|
306
|
-
preset.permissions = v;
|
|
307
|
-
else if (k === "max-turns")
|
|
308
|
-
preset.maxTurns = parseInt(v, 10) || undefined;
|
|
309
|
-
else if (k === "budget")
|
|
310
|
-
preset.budget = v === "none" ? null : parseFloat(v) || undefined;
|
|
311
|
-
else if (k === "plan")
|
|
312
|
-
preset.plan = true;
|
|
313
|
-
}
|
|
314
|
-
try {
|
|
315
|
-
savePreset(presetName, preset);
|
|
316
|
-
process.stderr.write(`${DIM}Saved preset "${presetName}".${RESET}\n`);
|
|
317
|
-
}
|
|
318
|
-
catch (err) {
|
|
319
|
-
process.stderr.write(`${DIM}${err instanceof Error ? err.message : String(err)}${RESET}\n`);
|
|
320
|
-
}
|
|
321
|
-
return true;
|
|
322
|
-
}
|
|
323
|
-
if (sub === "delete") {
|
|
324
|
-
const presetName = parts[2];
|
|
325
|
-
if (!presetName) {
|
|
326
|
-
process.stderr.write(`${DIM}Usage: /preset delete <name>${RESET}\n`);
|
|
327
|
-
return true;
|
|
328
|
-
}
|
|
329
|
-
try {
|
|
330
|
-
const ok = deletePreset(presetName);
|
|
331
|
-
process.stderr.write(`${DIM}${ok ? `Deleted "${presetName}".` : `Preset "${presetName}" not found.`}${RESET}\n`);
|
|
332
|
-
}
|
|
333
|
-
catch (err) {
|
|
334
|
-
process.stderr.write(`${DIM}${err instanceof Error ? err.message : String(err)}${RESET}\n`);
|
|
335
|
-
}
|
|
336
|
-
return true;
|
|
337
|
-
}
|
|
338
|
-
// /preset <name> — show preset details (use --preset <name> on CLI to apply at startup)
|
|
339
|
-
const preset = loadPreset(sub);
|
|
340
|
-
if (!preset) {
|
|
341
|
-
process.stderr.write(`${DIM}Preset "${sub}" not found. Use /preset list to see available presets.${RESET}\n`);
|
|
342
|
-
}
|
|
343
|
-
else {
|
|
344
|
-
const isBuiltin = ["fast", "careful", "yolo"].includes(sub);
|
|
345
|
-
process.stderr.write(`${DIM}${formatPreset(sub, preset, isBuiltin)}\nUse: phren-agent --preset ${sub} <task>${RESET}\n`);
|
|
346
|
-
}
|
|
347
|
-
return true;
|
|
348
|
-
}
|
|
349
77
|
case "/exit":
|
|
350
78
|
case "/quit":
|
|
351
79
|
case "/q":
|
package/dist/config.js
CHANGED
|
@@ -11,7 +11,8 @@ Options:
|
|
|
11
11
|
--max-output <n> Max output tokens per response (default: auto per model)
|
|
12
12
|
--budget <dollars> Max spend in USD (aborts when exceeded)
|
|
13
13
|
--plan Plan mode: show plan before executing tools
|
|
14
|
-
--permissions <mode> Permission mode: suggest, auto-confirm, full-auto
|
|
14
|
+
--permissions <mode> Permission mode: suggest (default), auto-confirm, full-auto
|
|
15
|
+
--yolo Full-auto permissions — no confirmations (alias for --permissions full-auto)
|
|
15
16
|
--interactive, -i Interactive REPL mode (multi-turn conversation)
|
|
16
17
|
--resume Resume last session's conversation
|
|
17
18
|
--lint-cmd <cmd> Override auto-detected lint command
|
|
@@ -46,7 +47,7 @@ Examples:
|
|
|
46
47
|
export function parseArgs(argv) {
|
|
47
48
|
const args = {
|
|
48
49
|
task: "",
|
|
49
|
-
permissions: "
|
|
50
|
+
permissions: "suggest",
|
|
50
51
|
maxTurns: 50,
|
|
51
52
|
budget: null,
|
|
52
53
|
plan: false,
|
|
@@ -120,6 +121,9 @@ export function parseArgs(argv) {
|
|
|
120
121
|
else if (arg === "--budget" && argv[i + 1]) {
|
|
121
122
|
args.budget = parseFloat(argv[++i]) || null;
|
|
122
123
|
}
|
|
124
|
+
else if (arg === "--yolo") {
|
|
125
|
+
args.permissions = "full-auto";
|
|
126
|
+
}
|
|
123
127
|
else if (arg === "--permissions" && argv[i + 1]) {
|
|
124
128
|
const mode = argv[++i];
|
|
125
129
|
if (mode === "suggest" || mode === "auto-confirm" || mode === "full-auto") {
|
package/dist/index.js
CHANGED
|
@@ -165,6 +165,7 @@ export async function runAgentCli(raw) {
|
|
|
165
165
|
costTracker,
|
|
166
166
|
plan: args.plan,
|
|
167
167
|
lintTestConfig,
|
|
168
|
+
sessionId,
|
|
168
169
|
};
|
|
169
170
|
// Multi-agent TUI mode
|
|
170
171
|
if (args.multi || args.team) {
|
|
@@ -183,12 +184,17 @@ export async function runAgentCli(raw) {
|
|
|
183
184
|
mcpCleanup?.();
|
|
184
185
|
return;
|
|
185
186
|
}
|
|
186
|
-
// Interactive mode — TUI if
|
|
187
|
+
// Interactive mode — Ink TUI if available, legacy TUI fallback, REPL if not TTY
|
|
187
188
|
if (args.interactive) {
|
|
188
189
|
const isTTY = process.stdout.isTTY && process.stdin.isTTY;
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
190
|
+
let session;
|
|
191
|
+
if (!isTTY) {
|
|
192
|
+
session = await (await import("./repl.js")).startRepl(agentConfig);
|
|
193
|
+
}
|
|
194
|
+
else {
|
|
195
|
+
// Ink TUI only — no legacy fallback
|
|
196
|
+
session = await (await import("./tui/ink-entry.js")).startInkTui(agentConfig);
|
|
197
|
+
}
|
|
192
198
|
// Flush anti-patterns at session end
|
|
193
199
|
if (phrenCtx) {
|
|
194
200
|
try {
|
package/dist/mcp-client.js
CHANGED
|
@@ -24,7 +24,8 @@ class McpConnection {
|
|
|
24
24
|
try {
|
|
25
25
|
const msg = JSON.parse(line);
|
|
26
26
|
if (msg.id !== undefined && this.pending.has(msg.id)) {
|
|
27
|
-
const { resolve, reject } = this.pending.get(msg.id);
|
|
27
|
+
const { resolve, reject, timer } = this.pending.get(msg.id);
|
|
28
|
+
clearTimeout(timer);
|
|
28
29
|
this.pending.delete(msg.id);
|
|
29
30
|
if (msg.error)
|
|
30
31
|
reject(new Error(`MCP error: ${msg.error.message}`));
|
|
@@ -35,8 +36,10 @@ class McpConnection {
|
|
|
35
36
|
catch { /* ignore non-JSON lines */ }
|
|
36
37
|
});
|
|
37
38
|
this.proc.on("error", (err) => {
|
|
38
|
-
for (const { reject } of this.pending.values())
|
|
39
|
+
for (const { reject, timer } of this.pending.values()) {
|
|
40
|
+
clearTimeout(timer);
|
|
39
41
|
reject(err);
|
|
42
|
+
}
|
|
40
43
|
this.pending.clear();
|
|
41
44
|
});
|
|
42
45
|
}
|
|
@@ -44,15 +47,14 @@ class McpConnection {
|
|
|
44
47
|
return new Promise((resolve, reject) => {
|
|
45
48
|
const id = this.nextId++;
|
|
46
49
|
const msg = { jsonrpc: "2.0", id, method, params };
|
|
47
|
-
|
|
48
|
-
this.proc.stdin.write(JSON.stringify(msg) + "\n");
|
|
49
|
-
// Timeout after 30s
|
|
50
|
-
setTimeout(() => {
|
|
50
|
+
const timer = setTimeout(() => {
|
|
51
51
|
if (this.pending.has(id)) {
|
|
52
52
|
this.pending.delete(id);
|
|
53
53
|
reject(new Error(`MCP call ${method} timed out (30s)`));
|
|
54
54
|
}
|
|
55
55
|
}, 30_000);
|
|
56
|
+
this.pending.set(id, { resolve, reject, timer });
|
|
57
|
+
this.proc.stdin.write(JSON.stringify(msg) + "\n");
|
|
56
58
|
});
|
|
57
59
|
}
|
|
58
60
|
async initialize() {
|
|
@@ -81,8 +83,10 @@ class McpConnection {
|
|
|
81
83
|
}
|
|
82
84
|
catch { /* ignore */ }
|
|
83
85
|
this.rl.close();
|
|
84
|
-
for (const { reject } of this.pending.values())
|
|
86
|
+
for (const { reject, timer } of this.pending.values()) {
|
|
87
|
+
clearTimeout(timer);
|
|
85
88
|
reject(new Error("Connection closed"));
|
|
89
|
+
}
|
|
86
90
|
this.pending.clear();
|
|
87
91
|
}
|
|
88
92
|
}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { createPane, appendToPane, flushPartial } from "./pane.js";
|
|
2
|
+
import { s, statusColor } from "./multi-render.js";
|
|
3
|
+
function resolveAgentTarget(target, agentOrder, panes, spawner) {
|
|
4
|
+
// Try numeric index (1-based)
|
|
5
|
+
const idx = parseInt(target, 10);
|
|
6
|
+
if (!isNaN(idx) && idx >= 1 && idx <= agentOrder.length) {
|
|
7
|
+
return agentOrder[idx - 1];
|
|
8
|
+
}
|
|
9
|
+
// Try name match
|
|
10
|
+
for (const [id, pane] of panes) {
|
|
11
|
+
if (pane.name === target)
|
|
12
|
+
return id;
|
|
13
|
+
}
|
|
14
|
+
// Try agent ID
|
|
15
|
+
if (spawner.getAgent(target))
|
|
16
|
+
return target;
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
function appendToSystem(ctx, text) {
|
|
20
|
+
if (!ctx.selectedId || !ctx.panes.has(ctx.selectedId)) {
|
|
21
|
+
// Create a virtual system pane
|
|
22
|
+
const pane = createPane("_system", "system");
|
|
23
|
+
ctx.panes.set("_system", pane);
|
|
24
|
+
if (!ctx.agentOrder.includes("_system"))
|
|
25
|
+
ctx.agentOrder.push("_system");
|
|
26
|
+
ctx.setSelectedId("_system");
|
|
27
|
+
appendToPane(pane, text + "\n");
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
const pane = ctx.panes.get(ctx.selectedId);
|
|
31
|
+
flushPartial(pane);
|
|
32
|
+
appendToPane(pane, text + "\n");
|
|
33
|
+
}
|
|
34
|
+
ctx.render();
|
|
35
|
+
}
|
|
36
|
+
export function handleSlashCommand(line, ctx) {
|
|
37
|
+
const parts = line.split(/\s+/);
|
|
38
|
+
const cmd = parts[0].toLowerCase();
|
|
39
|
+
if (cmd === "/spawn") {
|
|
40
|
+
const name = parts[1];
|
|
41
|
+
const task = parts.slice(2).join(" ");
|
|
42
|
+
if (!name || !task) {
|
|
43
|
+
appendToSystem(ctx, "Usage: /spawn <name> <task>");
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
const opts = {
|
|
47
|
+
task,
|
|
48
|
+
cwd: process.cwd(),
|
|
49
|
+
provider: ctx.config.provider.name,
|
|
50
|
+
permissions: "auto-confirm",
|
|
51
|
+
verbose: ctx.config.verbose,
|
|
52
|
+
};
|
|
53
|
+
const agentId = ctx.spawner.spawn(opts);
|
|
54
|
+
const pane = ctx.getOrCreatePane(agentId);
|
|
55
|
+
pane.name = name;
|
|
56
|
+
appendToPane(pane, s.cyan(`Spawned agent "${name}" (${agentId}): ${task}`) + "\n");
|
|
57
|
+
ctx.setSelectedId(agentId);
|
|
58
|
+
ctx.render();
|
|
59
|
+
return true;
|
|
60
|
+
}
|
|
61
|
+
if (cmd === "/list") {
|
|
62
|
+
const agents = ctx.spawner.listAgents();
|
|
63
|
+
if (agents.length === 0) {
|
|
64
|
+
appendToSystem(ctx, "No agents.");
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
const lines = ["Agents:"];
|
|
68
|
+
for (let i = 0; i < agents.length; i++) {
|
|
69
|
+
const a = agents[i];
|
|
70
|
+
const pane = ctx.panes.get(a.id);
|
|
71
|
+
const name = pane?.name ?? a.id;
|
|
72
|
+
const color = statusColor(a.status);
|
|
73
|
+
const elapsed = a.finishedAt
|
|
74
|
+
? `${((a.finishedAt - a.startedAt) / 1000).toFixed(1)}s`
|
|
75
|
+
: `${((Date.now() - a.startedAt) / 1000).toFixed(0)}s`;
|
|
76
|
+
lines.push(` ${i + 1}. ${name} [${color(a.status)}] ${s.dim(elapsed)} — ${a.task.slice(0, 50)}`);
|
|
77
|
+
}
|
|
78
|
+
appendToSystem(ctx, lines.join("\n"));
|
|
79
|
+
}
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
if (cmd === "/kill") {
|
|
83
|
+
const target = parts[1];
|
|
84
|
+
if (!target) {
|
|
85
|
+
appendToSystem(ctx, "Usage: /kill <name|index>");
|
|
86
|
+
return true;
|
|
87
|
+
}
|
|
88
|
+
const agentId = resolveAgentTarget(target, ctx.agentOrder, ctx.panes, ctx.spawner);
|
|
89
|
+
if (!agentId) {
|
|
90
|
+
appendToSystem(ctx, `Agent "${target}" not found.`);
|
|
91
|
+
return true;
|
|
92
|
+
}
|
|
93
|
+
const ok = ctx.spawner.cancel(agentId);
|
|
94
|
+
const pane = ctx.getOrCreatePane(agentId);
|
|
95
|
+
if (ok) {
|
|
96
|
+
appendToPane(pane, s.yellow("\n--- Cancelled ---\n"));
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
appendToSystem(ctx, `Agent "${target}" is not running.`);
|
|
100
|
+
}
|
|
101
|
+
ctx.render();
|
|
102
|
+
return true;
|
|
103
|
+
}
|
|
104
|
+
if (cmd === "/broadcast") {
|
|
105
|
+
const msg = parts.slice(1).join(" ");
|
|
106
|
+
if (!msg) {
|
|
107
|
+
appendToSystem(ctx, "Usage: /broadcast <message>");
|
|
108
|
+
return true;
|
|
109
|
+
}
|
|
110
|
+
const agents = ctx.spawner.listAgents();
|
|
111
|
+
let sent = 0;
|
|
112
|
+
for (const a of agents) {
|
|
113
|
+
if (a.status === "running") {
|
|
114
|
+
const pane = ctx.getOrCreatePane(a.id);
|
|
115
|
+
appendToPane(pane, s.yellow(`[broadcast] ${msg}`) + "\n");
|
|
116
|
+
sent++;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
appendToSystem(ctx, `Broadcast sent to ${sent} running agent(s).`);
|
|
120
|
+
return true;
|
|
121
|
+
}
|
|
122
|
+
if (cmd === "/msg") {
|
|
123
|
+
const target = parts[1];
|
|
124
|
+
const msg = parts.slice(2).join(" ");
|
|
125
|
+
if (!target || !msg) {
|
|
126
|
+
appendToSystem(ctx, "Usage: /msg <agent> <text>");
|
|
127
|
+
return true;
|
|
128
|
+
}
|
|
129
|
+
const agentId = resolveAgentTarget(target, ctx.agentOrder, ctx.panes, ctx.spawner);
|
|
130
|
+
if (!agentId) {
|
|
131
|
+
appendToSystem(ctx, `Agent "${target}" not found.`);
|
|
132
|
+
return true;
|
|
133
|
+
}
|
|
134
|
+
const ok = ctx.spawner.sendToAgent(agentId, msg, "user");
|
|
135
|
+
if (ok) {
|
|
136
|
+
const recipientPane = ctx.getOrCreatePane(agentId);
|
|
137
|
+
flushPartial(recipientPane);
|
|
138
|
+
appendToPane(recipientPane, s.yellow(`[user -> ${recipientPane.name}] ${msg}`) + "\n");
|
|
139
|
+
if (ctx.selectedId && ctx.selectedId !== agentId && ctx.panes.has(ctx.selectedId)) {
|
|
140
|
+
const curPane = ctx.panes.get(ctx.selectedId);
|
|
141
|
+
flushPartial(curPane);
|
|
142
|
+
appendToPane(curPane, s.yellow(`[user -> ${recipientPane.name}] ${msg}`) + "\n");
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
appendToSystem(ctx, `Agent "${target}" is not running.`);
|
|
147
|
+
}
|
|
148
|
+
ctx.render();
|
|
149
|
+
return true;
|
|
150
|
+
}
|
|
151
|
+
if (cmd === "/help") {
|
|
152
|
+
appendToSystem(ctx, [
|
|
153
|
+
"Commands:",
|
|
154
|
+
" /spawn <name> <task> — Spawn a new agent",
|
|
155
|
+
" /list — List all agents",
|
|
156
|
+
" /kill <name|index> — Terminate an agent",
|
|
157
|
+
" /msg <agent> <text> — Send direct message to an agent",
|
|
158
|
+
" /broadcast <msg> — Send to all running agents",
|
|
159
|
+
" /help — Show this help",
|
|
160
|
+
"",
|
|
161
|
+
"Keys:",
|
|
162
|
+
" 1-9 — Select agent by number",
|
|
163
|
+
" Ctrl+Left/Right — Cycle agents",
|
|
164
|
+
" PageUp/PageDown — Scroll output",
|
|
165
|
+
" Ctrl+D — Exit (kills all)",
|
|
166
|
+
].join("\n"));
|
|
167
|
+
return true;
|
|
168
|
+
}
|
|
169
|
+
return false;
|
|
170
|
+
}
|