@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.
Files changed (48) hide show
  1. package/dist/agent-loop/index.js +214 -0
  2. package/dist/agent-loop/stream.js +124 -0
  3. package/dist/agent-loop/types.js +13 -0
  4. package/dist/agent-loop.js +7 -326
  5. package/dist/commands/info.js +146 -0
  6. package/dist/commands/memory.js +165 -0
  7. package/dist/commands/model.js +138 -0
  8. package/dist/commands/session.js +213 -0
  9. package/dist/commands.js +25 -297
  10. package/dist/config.js +6 -2
  11. package/dist/index.js +10 -4
  12. package/dist/mcp-client.js +11 -7
  13. package/dist/multi/multi-commands.js +170 -0
  14. package/dist/multi/multi-events.js +81 -0
  15. package/dist/multi/multi-render.js +146 -0
  16. package/dist/multi/pane.js +28 -0
  17. package/dist/multi/spawner.js +3 -2
  18. package/dist/multi/tui-multi.js +39 -454
  19. package/dist/permissions/allowlist.js +2 -2
  20. package/dist/permissions/shell-safety.js +8 -0
  21. package/dist/providers/anthropic.js +72 -33
  22. package/dist/providers/codex.js +121 -60
  23. package/dist/providers/openai-compat.js +6 -1
  24. package/dist/repl.js +2 -2
  25. package/dist/system-prompt.js +24 -26
  26. package/dist/tools/glob.js +30 -6
  27. package/dist/tools/shell.js +5 -2
  28. package/dist/tui/ansi.js +48 -0
  29. package/dist/tui/components/AgentMessage.js +5 -0
  30. package/dist/tui/components/App.js +70 -0
  31. package/dist/tui/components/Banner.js +44 -0
  32. package/dist/tui/components/ChatMessage.js +23 -0
  33. package/dist/tui/components/InputArea.js +23 -0
  34. package/dist/tui/components/Separator.js +7 -0
  35. package/dist/tui/components/StatusBar.js +25 -0
  36. package/dist/tui/components/SteerQueue.js +7 -0
  37. package/dist/tui/components/StreamingText.js +5 -0
  38. package/dist/tui/components/ThinkingIndicator.js +20 -0
  39. package/dist/tui/components/ToolCall.js +11 -0
  40. package/dist/tui/components/UserMessage.js +5 -0
  41. package/dist/tui/hooks/useKeyboardShortcuts.js +89 -0
  42. package/dist/tui/hooks/useSlashCommands.js +52 -0
  43. package/dist/tui/index.js +5 -0
  44. package/dist/tui/ink-entry.js +271 -0
  45. package/dist/tui/menu-mode.js +86 -0
  46. package/dist/tui/tool-render.js +43 -0
  47. package/dist/tui.js +378 -252
  48. package/package.json +9 -2
@@ -0,0 +1,81 @@
1
+ import { appendToPane, flushPartial } from "./pane.js";
2
+ import { getAgentStyle } from "./agent-colors.js";
3
+ import { decodeDiffPayload, renderInlineDiff, DIFF_MARKER } from "./diff-renderer.js";
4
+ import { s, formatToolStart, formatToolEnd } from "./multi-render.js";
5
+ export function wireSpawnerEvents(spawner, ctx) {
6
+ spawner.on("text_delta", (agentId, text) => {
7
+ const pane = ctx.getOrCreatePane(agentId);
8
+ appendToPane(pane, text);
9
+ if (agentId === ctx.getSelectedId())
10
+ ctx.render();
11
+ });
12
+ spawner.on("text_block", (agentId, text) => {
13
+ const pane = ctx.getOrCreatePane(agentId);
14
+ appendToPane(pane, text + "\n");
15
+ if (agentId === ctx.getSelectedId())
16
+ ctx.render();
17
+ });
18
+ spawner.on("tool_start", (agentId, toolName, input) => {
19
+ const pane = ctx.getOrCreatePane(agentId);
20
+ flushPartial(pane);
21
+ appendToPane(pane, formatToolStart(toolName, input) + "\n");
22
+ if (agentId === ctx.getSelectedId())
23
+ ctx.render();
24
+ });
25
+ spawner.on("tool_end", (agentId, toolName, input, output, isError, durationMs) => {
26
+ const pane = ctx.getOrCreatePane(agentId);
27
+ flushPartial(pane);
28
+ const diffData = (toolName === "edit_file" || toolName === "write_file") ? decodeDiffPayload(output) : null;
29
+ const cleanOutput = diffData ? output.slice(0, output.indexOf(DIFF_MARKER)) : output;
30
+ appendToPane(pane, formatToolEnd(toolName, input, cleanOutput, isError, durationMs) + "\n");
31
+ if (diffData) {
32
+ appendToPane(pane, renderInlineDiff(diffData.oldContent, diffData.newContent, diffData.filePath) + "\n");
33
+ }
34
+ if (agentId === ctx.getSelectedId())
35
+ ctx.render();
36
+ });
37
+ spawner.on("status", (agentId, message) => {
38
+ const pane = ctx.getOrCreatePane(agentId);
39
+ appendToPane(pane, s.dim(message) + "\n");
40
+ if (agentId === ctx.getSelectedId())
41
+ ctx.render();
42
+ });
43
+ spawner.on("done", (agentId, result) => {
44
+ const pane = ctx.getOrCreatePane(agentId);
45
+ flushPartial(pane);
46
+ const style = getAgentStyle(pane.index);
47
+ appendToPane(pane, "\n" + style.color(`--- ${style.icon} Agent completed ---`) + "\n");
48
+ appendToPane(pane, s.dim(` Turns: ${result.turns} Tool calls: ${result.toolCalls}${result.totalCost ? ` Cost: ${result.totalCost}` : ""}`) + "\n");
49
+ ctx.render();
50
+ });
51
+ spawner.on("error", (agentId, error) => {
52
+ const pane = ctx.getOrCreatePane(agentId);
53
+ flushPartial(pane);
54
+ const style = getAgentStyle(pane.index);
55
+ appendToPane(pane, "\n" + style.color(`--- ${style.icon} Error: ${error} ---`) + "\n");
56
+ ctx.render();
57
+ });
58
+ spawner.on("exit", (agentId, code) => {
59
+ const pane = ctx.getOrCreatePane(agentId);
60
+ if (code !== null && code !== 0) {
61
+ appendToPane(pane, s.dim(` Process exited with code ${code}`) + "\n");
62
+ }
63
+ ctx.render();
64
+ });
65
+ spawner.on("message", (from, to, content) => {
66
+ const senderPane = ctx.panes.get(from);
67
+ if (senderPane) {
68
+ flushPartial(senderPane);
69
+ const toName = ctx.panes.get(to)?.name ?? to;
70
+ appendToPane(senderPane, s.yellow(`[${senderPane.name} -> ${toName}] ${content}`) + "\n");
71
+ }
72
+ const recipientPane = ctx.panes.get(to);
73
+ if (recipientPane) {
74
+ flushPartial(recipientPane);
75
+ const fromName = senderPane?.name ?? from;
76
+ appendToPane(recipientPane, s.yellow(`[${fromName} -> ${recipientPane.name}] ${content}`) + "\n");
77
+ }
78
+ if (from === ctx.getSelectedId() || to === ctx.getSelectedId())
79
+ ctx.render();
80
+ });
81
+ }
@@ -0,0 +1,146 @@
1
+ import { getAgentStyle, formatAgentName } from "./agent-colors.js";
2
+ // ── ANSI helpers (mirrors tui.ts pattern) ────────────────────────────────────
3
+ export const ESC = "\x1b[";
4
+ export const s = {
5
+ reset: `${ESC}0m`,
6
+ bold: (t) => `${ESC}1m${t}${ESC}0m`,
7
+ dim: (t) => `${ESC}2m${t}${ESC}0m`,
8
+ cyan: (t) => `${ESC}36m${t}${ESC}0m`,
9
+ green: (t) => `${ESC}32m${t}${ESC}0m`,
10
+ yellow: (t) => `${ESC}33m${t}${ESC}0m`,
11
+ red: (t) => `${ESC}31m${t}${ESC}0m`,
12
+ gray: (t) => `${ESC}90m${t}${ESC}0m`,
13
+ white: (t) => `${ESC}37m${t}${ESC}0m`,
14
+ bgGreen: (t) => `${ESC}42m${t}${ESC}0m`,
15
+ bgRed: (t) => `${ESC}41m${t}${ESC}0m`,
16
+ bgGray: (t) => `${ESC}100m${t}${ESC}0m`,
17
+ bgCyan: (t) => `${ESC}46m${t}${ESC}0m`,
18
+ bgYellow: (t) => `${ESC}43m${t}${ESC}0m`,
19
+ invert: (t) => `${ESC}7m${t}${ESC}0m`,
20
+ };
21
+ function stripAnsi(t) {
22
+ return t.replace(/\x1b\[[0-9;?]*[ -/]*[@-~]/g, "");
23
+ }
24
+ function cols() {
25
+ return process.stdout.columns || 80;
26
+ }
27
+ function rows() {
28
+ return process.stdout.rows || 24;
29
+ }
30
+ export function statusColor(status) {
31
+ switch (status) {
32
+ case "starting": return s.yellow;
33
+ case "running": return s.green;
34
+ case "done": return s.gray;
35
+ case "error": return s.red;
36
+ case "cancelled": return s.gray;
37
+ }
38
+ }
39
+ export function formatToolStart(toolName, input) {
40
+ const preview = JSON.stringify(input).slice(0, 60);
41
+ return s.dim(` > ${toolName}(${preview})...`);
42
+ }
43
+ export function formatToolEnd(toolName, input, output, isError, durationMs) {
44
+ const dur = durationMs < 1000 ? `${durationMs}ms` : `${(durationMs / 1000).toFixed(1)}s`;
45
+ const icon = isError ? s.red("x") : s.green("ok");
46
+ const preview = JSON.stringify(input).slice(0, 50);
47
+ const header = s.dim(` ${toolName}(${preview})`) + ` ${icon} ${s.dim(dur)}`;
48
+ const allLines = output.split("\n");
49
+ const w = cols();
50
+ const body = allLines.slice(0, 4).map((l) => s.dim(` | ${l.slice(0, w - 6)}`)).join("\n");
51
+ const more = allLines.length > 4 ? `\n${s.dim(` | ... (${allLines.length} lines)`)}` : "";
52
+ return `${header}\n${body}${more}`;
53
+ }
54
+ export function renderTopBar(spawner, panes, selectedId) {
55
+ const w_ = cols();
56
+ const agents = spawner.listAgents();
57
+ const tabs = [];
58
+ for (let i = 0; i < agents.length; i++) {
59
+ const a = agents[i];
60
+ const isSel = a.id === selectedId;
61
+ const pane = panes.get(a.id);
62
+ const paneIdx = pane?.index ?? i;
63
+ const stColor = statusColor(a.status);
64
+ const agentLabel = formatAgentName(pane?.name ?? a.task.slice(0, 12), paneIdx);
65
+ const statusTag = stColor(a.status);
66
+ const raw = ` ${i + 1}:${stripAnsi(agentLabel)} [${stripAnsi(statusTag)}] `;
67
+ const colored = ` ${i + 1}:${agentLabel} [${statusTag}] `;
68
+ const tab = isSel ? s.invert(raw) : colored;
69
+ tabs.push(tab);
70
+ }
71
+ if (agents.length === 0) {
72
+ tabs.push(s.dim(" no agents "));
73
+ }
74
+ const title = s.bold(" phren-multi ");
75
+ const tabStr = tabs.join(s.dim("|"));
76
+ const line = title + s.dim("|") + tabStr;
77
+ const pad = Math.max(0, w_ - stripAnsi(line).length);
78
+ return s.invert(stripAnsi(title)) + s.dim("|") + tabStr + " ".repeat(pad);
79
+ }
80
+ export function renderMainArea(panes, selectedId, scrollOffset) {
81
+ const availRows = rows() - 3; // top bar + bottom bar + input line
82
+ if (availRows < 1)
83
+ return [];
84
+ if (!selectedId || !panes.has(selectedId)) {
85
+ const emptyMsg = s.dim(" No agent selected. Use /spawn <name> <task> to create one.");
86
+ const lines = [emptyMsg];
87
+ while (lines.length < availRows)
88
+ lines.push("");
89
+ return lines;
90
+ }
91
+ const pane = panes.get(selectedId);
92
+ // Include partial line if any
93
+ const allLines = [...pane.lines];
94
+ if (pane.partial)
95
+ allLines.push(pane.partial);
96
+ // Apply scroll offset
97
+ const totalLines = allLines.length;
98
+ let start = Math.max(0, totalLines - availRows - scrollOffset);
99
+ let end = start + availRows;
100
+ if (end > totalLines) {
101
+ end = totalLines;
102
+ start = Math.max(0, end - availRows);
103
+ }
104
+ const visible = allLines.slice(start, end);
105
+ const w_ = cols();
106
+ const output = [];
107
+ const paneStyle = getAgentStyle(pane.index);
108
+ const linePrefix = paneStyle.color(paneStyle.icon) + " ";
109
+ const prefixLen = 2; // icon + space
110
+ for (const line of visible) {
111
+ output.push(linePrefix + line.slice(0, w_ - prefixLen));
112
+ }
113
+ // Pad remaining rows
114
+ while (output.length < availRows)
115
+ output.push("");
116
+ return output;
117
+ }
118
+ export function renderBottomBar(spawner) {
119
+ const w_ = cols();
120
+ const agentCount = spawner.listAgents().length;
121
+ const runningCount = spawner.getAgentsByStatus("running").length;
122
+ const left = ` Agents: ${agentCount} (${runningCount} running)`;
123
+ const right = `1-9:select Ctrl+</>:cycle /spawn /list /kill /broadcast Ctrl+D:exit `;
124
+ const pad = Math.max(0, w_ - left.length - right.length);
125
+ return s.invert(left + " ".repeat(pad) + right);
126
+ }
127
+ export function render(w, spawner, panes, selectedId, scrollOffset, inputLine) {
128
+ // Hide cursor, move to top, clear screen
129
+ w.write(`${ESC}?25l${ESC}H${ESC}2J`);
130
+ // Top bar
131
+ w.write(renderTopBar(spawner, panes, selectedId));
132
+ w.write("\n");
133
+ // Main area
134
+ const mainLines = renderMainArea(panes, selectedId, scrollOffset);
135
+ for (const line of mainLines) {
136
+ w.write(line + "\n");
137
+ }
138
+ // Bottom bar
139
+ w.write(renderBottomBar(spawner));
140
+ w.write("\n");
141
+ // Input line
142
+ const prompt = s.cyan("multi> ");
143
+ w.write(prompt + inputLine);
144
+ // Show cursor
145
+ w.write(`${ESC}?25h`);
146
+ }
@@ -0,0 +1,28 @@
1
+ export const MAX_SCROLLBACK = 1000;
2
+ let nextPaneIndex = 0;
3
+ export function resetPaneIndex() {
4
+ nextPaneIndex = 0;
5
+ }
6
+ export function createPane(agentId, name) {
7
+ return { agentId, name, index: nextPaneIndex++, lines: [], partial: "" };
8
+ }
9
+ export function appendToPane(pane, text) {
10
+ // Merge with partial line buffer
11
+ const combined = pane.partial + text;
12
+ const parts = combined.split("\n");
13
+ // Everything except the last segment is a complete line
14
+ for (let i = 0; i < parts.length - 1; i++) {
15
+ pane.lines.push(parts[i]);
16
+ }
17
+ pane.partial = parts[parts.length - 1];
18
+ // Enforce scrollback cap
19
+ if (pane.lines.length > MAX_SCROLLBACK) {
20
+ pane.lines.splice(0, pane.lines.length - MAX_SCROLLBACK);
21
+ }
22
+ }
23
+ export function flushPartial(pane) {
24
+ if (pane.partial) {
25
+ pane.lines.push(pane.partial);
26
+ pane.partial = "";
27
+ }
28
+ }
@@ -28,6 +28,7 @@ const ENV_FORWARD_KEYS = [
28
28
  "PHREN_PROFILE",
29
29
  "PHREN_DEBUG",
30
30
  "HOME",
31
+ "USERPROFILE",
31
32
  "PATH",
32
33
  "NODE_EXTRA_CA_CERTS",
33
34
  ];
@@ -160,7 +161,7 @@ export class AgentSpawner extends EventEmitter {
160
161
  // Give it a moment to clean up, then force kill
161
162
  setTimeout(() => {
162
163
  if (this.processes.has(agentId)) {
163
- child.kill("SIGTERM");
164
+ child.kill();
164
165
  }
165
166
  }, 5000);
166
167
  const agent = this.agents.get(agentId);
@@ -207,7 +208,7 @@ export class AgentSpawner extends EventEmitter {
207
208
  setTimeout(() => {
208
209
  // Force kill remaining
209
210
  for (const [id, child] of this.processes) {
210
- child.kill("SIGKILL");
211
+ child.kill();
211
212
  this.processes.delete(id);
212
213
  }
213
214
  resolve();