@phren/agent 0.0.1
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.js +328 -0
- package/dist/bin.js +3 -0
- package/dist/checkpoint.js +103 -0
- package/dist/commands.js +292 -0
- package/dist/config.js +139 -0
- package/dist/context/pruner.js +62 -0
- package/dist/context/token-counter.js +28 -0
- package/dist/cost.js +71 -0
- package/dist/index.js +284 -0
- package/dist/mcp-client.js +168 -0
- package/dist/memory/anti-patterns.js +69 -0
- package/dist/memory/auto-capture.js +72 -0
- package/dist/memory/context-flush.js +24 -0
- package/dist/memory/context.js +170 -0
- package/dist/memory/error-recovery.js +58 -0
- package/dist/memory/project-context.js +77 -0
- package/dist/memory/session.js +100 -0
- package/dist/multi/agent-colors.js +41 -0
- package/dist/multi/child-entry.js +173 -0
- package/dist/multi/coordinator.js +263 -0
- package/dist/multi/diff-renderer.js +175 -0
- package/dist/multi/markdown.js +96 -0
- package/dist/multi/presets.js +107 -0
- package/dist/multi/progress.js +32 -0
- package/dist/multi/spawner.js +219 -0
- package/dist/multi/tui-multi.js +626 -0
- package/dist/multi/types.js +7 -0
- package/dist/permissions/allowlist.js +61 -0
- package/dist/permissions/checker.js +111 -0
- package/dist/permissions/prompt.js +190 -0
- package/dist/permissions/sandbox.js +95 -0
- package/dist/permissions/shell-safety.js +74 -0
- package/dist/permissions/types.js +2 -0
- package/dist/plan.js +38 -0
- package/dist/providers/anthropic.js +170 -0
- package/dist/providers/codex-auth.js +197 -0
- package/dist/providers/codex.js +265 -0
- package/dist/providers/ollama.js +142 -0
- package/dist/providers/openai-compat.js +163 -0
- package/dist/providers/openrouter.js +116 -0
- package/dist/providers/resolve.js +39 -0
- package/dist/providers/retry.js +55 -0
- package/dist/providers/types.js +2 -0
- package/dist/repl.js +180 -0
- package/dist/spinner.js +46 -0
- package/dist/system-prompt.js +31 -0
- package/dist/tools/edit-file.js +31 -0
- package/dist/tools/git.js +98 -0
- package/dist/tools/glob.js +65 -0
- package/dist/tools/grep.js +108 -0
- package/dist/tools/lint-test.js +76 -0
- package/dist/tools/phren-finding.js +35 -0
- package/dist/tools/phren-search.js +44 -0
- package/dist/tools/phren-tasks.js +71 -0
- package/dist/tools/read-file.js +44 -0
- package/dist/tools/registry.js +46 -0
- package/dist/tools/shell.js +48 -0
- package/dist/tools/types.js +2 -0
- package/dist/tools/write-file.js +27 -0
- package/dist/tui.js +451 -0
- package/package.json +39 -0
package/dist/tui.js
ADDED
|
@@ -0,0 +1,451 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Terminal UI for phren-agent — streaming chat with inline tool calls.
|
|
3
|
+
* Dual-mode: Chat (LLM conversation) and Menu (navigable memory browser).
|
|
4
|
+
* Tab toggles between modes. Raw stdin for steering support.
|
|
5
|
+
*/
|
|
6
|
+
import * as readline from "node:readline";
|
|
7
|
+
import { createSession, runTurn } from "./agent-loop.js";
|
|
8
|
+
import { handleCommand } from "./commands.js";
|
|
9
|
+
import { renderMarkdown } from "./multi/markdown.js";
|
|
10
|
+
import { decodeDiffPayload, renderInlineDiff, DIFF_MARKER } from "./multi/diff-renderer.js";
|
|
11
|
+
import * as fs from "fs";
|
|
12
|
+
import * as path from "path";
|
|
13
|
+
import * as os from "os";
|
|
14
|
+
// ── ANSI helpers ─────────────────────────────────────────────────────────────
|
|
15
|
+
const ESC = "\x1b[";
|
|
16
|
+
const s = {
|
|
17
|
+
reset: `${ESC}0m`,
|
|
18
|
+
bold: (t) => `${ESC}1m${t}${ESC}0m`,
|
|
19
|
+
dim: (t) => `${ESC}2m${t}${ESC}0m`,
|
|
20
|
+
cyan: (t) => `${ESC}36m${t}${ESC}0m`,
|
|
21
|
+
green: (t) => `${ESC}32m${t}${ESC}0m`,
|
|
22
|
+
yellow: (t) => `${ESC}33m${t}${ESC}0m`,
|
|
23
|
+
red: (t) => `${ESC}31m${t}${ESC}0m`,
|
|
24
|
+
gray: (t) => `${ESC}90m${t}${ESC}0m`,
|
|
25
|
+
invert: (t) => `${ESC}7m${t}${ESC}0m`,
|
|
26
|
+
};
|
|
27
|
+
function cols() {
|
|
28
|
+
return process.stdout.columns || 80;
|
|
29
|
+
}
|
|
30
|
+
// ── Permission mode helpers ─────────────────────────────────────────────────
|
|
31
|
+
const PERMISSION_MODES = ["suggest", "auto-confirm", "full-auto"];
|
|
32
|
+
function nextPermissionMode(current) {
|
|
33
|
+
const idx = PERMISSION_MODES.indexOf(current);
|
|
34
|
+
return PERMISSION_MODES[(idx + 1) % PERMISSION_MODES.length];
|
|
35
|
+
}
|
|
36
|
+
const PERMISSION_LABELS = {
|
|
37
|
+
"suggest": "suggest",
|
|
38
|
+
"auto-confirm": "auto",
|
|
39
|
+
"full-auto": "full-auto",
|
|
40
|
+
};
|
|
41
|
+
function formatPermissionMode(mode) {
|
|
42
|
+
const label = PERMISSION_LABELS[mode];
|
|
43
|
+
switch (mode) {
|
|
44
|
+
case "suggest": return s.cyan(`[${label}]`);
|
|
45
|
+
case "auto-confirm": return s.green(`[${label}]`);
|
|
46
|
+
case "full-auto": return s.yellow(`[${label}]`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
// ── Status bar ───────────────────────────────────────────────────────────────
|
|
50
|
+
function renderStatusBar(provider, project, turns, cost, permMode, agentCount) {
|
|
51
|
+
const modeStr = permMode ? ` ${PERMISSION_LABELS[permMode]}` : "";
|
|
52
|
+
const agentTag = agentCount && agentCount > 0 ? ` ${s.dim(`A${agentCount}`)}` : "";
|
|
53
|
+
const left = ` ${s.bold("phren-agent")} ${s.dim("·")} ${provider}${project ? ` ${s.dim("·")} ${project}` : ""}`;
|
|
54
|
+
const right = `${modeStr}${agentTag} ${cost ? cost + " " : ""}${s.dim(`T${turns}`)} `;
|
|
55
|
+
const w = cols();
|
|
56
|
+
const pad = Math.max(0, w - stripAnsi(left).length - stripAnsi(right).length);
|
|
57
|
+
return s.invert(stripAnsi(left) + " ".repeat(pad) + stripAnsi(right));
|
|
58
|
+
}
|
|
59
|
+
function stripAnsi(t) {
|
|
60
|
+
return t.replace(/\x1b\[[0-9;?]*[ -/]*[@-~]/g, "");
|
|
61
|
+
}
|
|
62
|
+
// ── Tool call rendering ──────────────────────────────────────────────────────
|
|
63
|
+
const COMPACT_LINES = 3;
|
|
64
|
+
function formatDuration(ms) {
|
|
65
|
+
if (ms < 1000)
|
|
66
|
+
return `${ms}ms`;
|
|
67
|
+
if (ms < 60_000)
|
|
68
|
+
return `${(ms / 1000).toFixed(1)}s`;
|
|
69
|
+
const mins = Math.floor(ms / 60_000);
|
|
70
|
+
const secs = Math.round((ms % 60_000) / 1000);
|
|
71
|
+
return `${mins}m ${secs}s`;
|
|
72
|
+
}
|
|
73
|
+
function renderToolCall(name, input, output, isError, durationMs) {
|
|
74
|
+
const inputPreview = JSON.stringify(input).slice(0, 80);
|
|
75
|
+
const dur = formatDuration(durationMs);
|
|
76
|
+
const icon = isError ? s.red("✗") : s.green("✓");
|
|
77
|
+
const header = s.dim(` ${name}(${inputPreview})`) + ` ${icon} ${s.dim(dur)}`;
|
|
78
|
+
// Compact: show first 3 lines only, with overflow count
|
|
79
|
+
const allLines = output.split("\n").filter(Boolean);
|
|
80
|
+
const shown = allLines.slice(0, COMPACT_LINES);
|
|
81
|
+
const body = shown.map((l) => s.dim(` │ ${l.slice(0, cols() - 6)}`)).join("\n");
|
|
82
|
+
const overflow = allLines.length - COMPACT_LINES;
|
|
83
|
+
const more = overflow > 0 ? `\n${s.dim(` │ [+${overflow} lines]`)}` : "";
|
|
84
|
+
return `${header}\n${body}${more}`;
|
|
85
|
+
}
|
|
86
|
+
// ── Menu mode helpers ────────────────────────────────────────────────────────
|
|
87
|
+
let menuMod = null;
|
|
88
|
+
async function loadMenuModule() {
|
|
89
|
+
if (!menuMod) {
|
|
90
|
+
try {
|
|
91
|
+
menuMod = await import("@phren/cli/shell/render-api");
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
menuMod = null;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return menuMod;
|
|
98
|
+
}
|
|
99
|
+
// ── Main TUI ─────────────────────────────────────────────────────────────────
|
|
100
|
+
export async function startTui(config, spawner) {
|
|
101
|
+
const contextLimit = config.provider.contextWindow ?? 200_000;
|
|
102
|
+
const session = createSession(contextLimit);
|
|
103
|
+
const w = process.stdout;
|
|
104
|
+
const isTTY = process.stdout.isTTY;
|
|
105
|
+
let inputMode = loadInputMode();
|
|
106
|
+
let pendingInput = null;
|
|
107
|
+
let running = false;
|
|
108
|
+
let inputLine = "";
|
|
109
|
+
let costStr = "";
|
|
110
|
+
// ── Dual-mode state ─────────────────────────────────────────────────────
|
|
111
|
+
let tuiMode = "chat";
|
|
112
|
+
let menuState = {
|
|
113
|
+
view: "Projects",
|
|
114
|
+
project: config.phrenCtx?.project ?? undefined,
|
|
115
|
+
cursor: 0,
|
|
116
|
+
scroll: 0,
|
|
117
|
+
};
|
|
118
|
+
let menuListCount = 0;
|
|
119
|
+
let menuFilterActive = false;
|
|
120
|
+
let menuFilterBuf = "";
|
|
121
|
+
// ── Menu rendering ─────────────────────────────────────────────────────
|
|
122
|
+
async function renderMenu() {
|
|
123
|
+
const mod = await loadMenuModule();
|
|
124
|
+
if (!mod || !config.phrenCtx)
|
|
125
|
+
return;
|
|
126
|
+
const result = await mod.renderMenuFrame(config.phrenCtx.phrenPath, config.phrenCtx.profile, menuState);
|
|
127
|
+
menuListCount = result.listCount;
|
|
128
|
+
// Full-screen write: single write to avoid flicker
|
|
129
|
+
w.write(`${ESC}?25l${ESC}H${ESC}2J${result.output}${ESC}?25h`);
|
|
130
|
+
}
|
|
131
|
+
function enterMenuMode() {
|
|
132
|
+
if (!config.phrenCtx) {
|
|
133
|
+
w.write(s.yellow(" phren not configured — menu unavailable\n"));
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
tuiMode = "menu";
|
|
137
|
+
menuState.project = config.phrenCtx.project ?? menuState.project;
|
|
138
|
+
w.write("\x1b[?1049h"); // enter alternate screen
|
|
139
|
+
renderMenu();
|
|
140
|
+
}
|
|
141
|
+
function exitMenuMode() {
|
|
142
|
+
tuiMode = "chat";
|
|
143
|
+
menuFilterActive = false;
|
|
144
|
+
menuFilterBuf = "";
|
|
145
|
+
w.write("\x1b[?1049l"); // leave alternate screen (restores chat)
|
|
146
|
+
statusBar();
|
|
147
|
+
prompt();
|
|
148
|
+
}
|
|
149
|
+
// Print status bar
|
|
150
|
+
function statusBar() {
|
|
151
|
+
if (!isTTY)
|
|
152
|
+
return;
|
|
153
|
+
const bar = renderStatusBar(config.provider.name, config.phrenCtx?.project ?? null, session.turns, costStr, config.registry.permissionConfig.mode, spawner?.listAgents().length);
|
|
154
|
+
w.write(`${ESC}s${ESC}H${bar}${ESC}u`); // save cursor, move to top, print, restore
|
|
155
|
+
}
|
|
156
|
+
// Print prompt
|
|
157
|
+
function prompt() {
|
|
158
|
+
const modeTag = inputMode === "steering" ? s.dim("[steer]") : s.dim("[queue]");
|
|
159
|
+
w.write(`\n${s.cyan("phren>")} ${modeTag} `);
|
|
160
|
+
}
|
|
161
|
+
// Terminal cleanup: restore state on exit
|
|
162
|
+
function cleanupTerminal() {
|
|
163
|
+
w.write("\x1b[?1049l"); // leave alt screen if active
|
|
164
|
+
if (process.stdin.isTTY) {
|
|
165
|
+
try {
|
|
166
|
+
process.stdin.setRawMode(false);
|
|
167
|
+
}
|
|
168
|
+
catch { }
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
process.on("exit", cleanupTerminal);
|
|
172
|
+
// Setup: alternate screen not needed — just reserve top line for status
|
|
173
|
+
if (isTTY) {
|
|
174
|
+
w.write("\n"); // make room for status bar
|
|
175
|
+
w.write(`${ESC}1;1H`); // move to top
|
|
176
|
+
statusBar();
|
|
177
|
+
w.write(`${ESC}2;1H`); // move below status bar
|
|
178
|
+
w.write(s.dim("phren-agent TUI. Tab: memory browser Shift+Tab: permissions /help: commands Ctrl+D: exit\n"));
|
|
179
|
+
}
|
|
180
|
+
// Raw stdin for steering
|
|
181
|
+
if (process.stdin.isTTY) {
|
|
182
|
+
readline.emitKeypressEvents(process.stdin);
|
|
183
|
+
process.stdin.setRawMode(true);
|
|
184
|
+
}
|
|
185
|
+
let resolve = null;
|
|
186
|
+
const done = new Promise((r) => { resolve = r; });
|
|
187
|
+
// ── Menu keypress handler ───────────────────────────────────────────────
|
|
188
|
+
async function handleMenuKeypress(key) {
|
|
189
|
+
// Filter input mode: capture text for / search
|
|
190
|
+
if (menuFilterActive) {
|
|
191
|
+
if (key.name === "escape") {
|
|
192
|
+
menuFilterActive = false;
|
|
193
|
+
menuFilterBuf = "";
|
|
194
|
+
menuState = { ...menuState, filter: undefined, cursor: 0, scroll: 0 };
|
|
195
|
+
renderMenu();
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
if (key.name === "return") {
|
|
199
|
+
menuFilterActive = false;
|
|
200
|
+
menuState = { ...menuState, filter: menuFilterBuf || undefined, cursor: 0, scroll: 0 };
|
|
201
|
+
menuFilterBuf = "";
|
|
202
|
+
renderMenu();
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
if (key.name === "backspace") {
|
|
206
|
+
menuFilterBuf = menuFilterBuf.slice(0, -1);
|
|
207
|
+
menuState = { ...menuState, filter: menuFilterBuf || undefined, cursor: 0 };
|
|
208
|
+
renderMenu();
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
if (key.sequence && !key.ctrl && !key.meta) {
|
|
212
|
+
menuFilterBuf += key.sequence;
|
|
213
|
+
menuState = { ...menuState, filter: menuFilterBuf, cursor: 0 };
|
|
214
|
+
renderMenu();
|
|
215
|
+
}
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
// "/" starts filter input
|
|
219
|
+
if (key.sequence === "/") {
|
|
220
|
+
menuFilterActive = true;
|
|
221
|
+
menuFilterBuf = "";
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
const mod = await loadMenuModule();
|
|
225
|
+
if (!mod) {
|
|
226
|
+
exitMenuMode();
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
const newState = mod.handleMenuKey(menuState, key.name ?? "", menuListCount, config.phrenCtx?.phrenPath, config.phrenCtx?.profile);
|
|
230
|
+
if (newState === null) {
|
|
231
|
+
exitMenuMode();
|
|
232
|
+
}
|
|
233
|
+
else {
|
|
234
|
+
menuState = newState;
|
|
235
|
+
renderMenu();
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
// ── Keypress router ────────────────────────────────────────────────────
|
|
239
|
+
process.stdin.on("keypress", (_ch, key) => {
|
|
240
|
+
if (!key)
|
|
241
|
+
return;
|
|
242
|
+
// Ctrl+D — always exit
|
|
243
|
+
if (key.ctrl && key.name === "d") {
|
|
244
|
+
if (tuiMode === "menu")
|
|
245
|
+
w.write("\x1b[?1049l"); // leave alt screen
|
|
246
|
+
if (process.stdin.isTTY)
|
|
247
|
+
process.stdin.setRawMode(false);
|
|
248
|
+
w.write(s.dim("\nSession ended.\n"));
|
|
249
|
+
resolve(session);
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
// Shift+Tab — cycle permission mode (works in chat mode, not during filter)
|
|
253
|
+
if (key.shift && key.name === "tab" && !menuFilterActive && tuiMode === "chat") {
|
|
254
|
+
const current = config.registry.permissionConfig.mode;
|
|
255
|
+
const next = nextPermissionMode(current);
|
|
256
|
+
config.registry.setPermissions({ ...config.registry.permissionConfig, mode: next });
|
|
257
|
+
savePermissionMode(next);
|
|
258
|
+
w.write(s.yellow(` [mode: ${next}]\n`));
|
|
259
|
+
statusBar();
|
|
260
|
+
if (!running)
|
|
261
|
+
prompt();
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
// Tab — toggle mode (not during agent run or filter)
|
|
265
|
+
if (key.name === "tab" && !menuFilterActive) {
|
|
266
|
+
if (tuiMode === "chat" && !running) {
|
|
267
|
+
enterMenuMode();
|
|
268
|
+
}
|
|
269
|
+
else if (tuiMode === "menu") {
|
|
270
|
+
exitMenuMode();
|
|
271
|
+
}
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
// Route to mode-specific handler
|
|
275
|
+
if (tuiMode === "menu") {
|
|
276
|
+
handleMenuKeypress(key);
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
// ── Chat mode keys ──────────────────────────────────────────────────
|
|
280
|
+
// Ctrl+C — cancel current or clear line
|
|
281
|
+
if (key.ctrl && key.name === "c") {
|
|
282
|
+
if (running) {
|
|
283
|
+
pendingInput = null;
|
|
284
|
+
w.write(s.yellow("\n [interrupted]\n"));
|
|
285
|
+
}
|
|
286
|
+
else {
|
|
287
|
+
inputLine = "";
|
|
288
|
+
w.write("\n");
|
|
289
|
+
prompt();
|
|
290
|
+
}
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
// Enter — submit
|
|
294
|
+
if (key.name === "return") {
|
|
295
|
+
const line = inputLine.trim();
|
|
296
|
+
inputLine = "";
|
|
297
|
+
w.write("\n");
|
|
298
|
+
if (!line) {
|
|
299
|
+
prompt();
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
// Slash commands
|
|
303
|
+
if (line === "/mode") {
|
|
304
|
+
inputMode = inputMode === "steering" ? "queue" : "steering";
|
|
305
|
+
saveInputMode(inputMode);
|
|
306
|
+
w.write(s.yellow(` Input mode: ${inputMode}\n`));
|
|
307
|
+
prompt();
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
if (handleCommand(line, { session, contextLimit, undoStack: [] })) {
|
|
311
|
+
prompt();
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
// If agent is running, buffer input
|
|
315
|
+
if (running) {
|
|
316
|
+
pendingInput = line;
|
|
317
|
+
const label = inputMode === "steering" ? "steering" : "queued";
|
|
318
|
+
w.write(s.dim(` ↳ ${label}: "${line.slice(0, 60)}"\n`));
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
// Run agent turn
|
|
322
|
+
runAgentTurn(line);
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
// Backspace
|
|
326
|
+
if (key.name === "backspace") {
|
|
327
|
+
if (inputLine.length > 0) {
|
|
328
|
+
inputLine = inputLine.slice(0, -1);
|
|
329
|
+
w.write("\b \b");
|
|
330
|
+
}
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
// Regular character
|
|
334
|
+
if (key.sequence && !key.ctrl && !key.meta) {
|
|
335
|
+
inputLine += key.sequence;
|
|
336
|
+
w.write(key.sequence);
|
|
337
|
+
}
|
|
338
|
+
});
|
|
339
|
+
// TUI hooks — render streaming text with markdown, compact tool output
|
|
340
|
+
let textBuffer = "";
|
|
341
|
+
function flushTextBuffer() {
|
|
342
|
+
if (!textBuffer)
|
|
343
|
+
return;
|
|
344
|
+
w.write(renderMarkdown(textBuffer));
|
|
345
|
+
textBuffer = "";
|
|
346
|
+
}
|
|
347
|
+
const tuiHooks = {
|
|
348
|
+
onTextDelta: (text) => {
|
|
349
|
+
textBuffer += text;
|
|
350
|
+
// Flush on paragraph boundaries (double newline) or single newline for streaming feel
|
|
351
|
+
if (textBuffer.includes("\n\n") || textBuffer.endsWith("\n")) {
|
|
352
|
+
flushTextBuffer();
|
|
353
|
+
}
|
|
354
|
+
},
|
|
355
|
+
onTextDone: () => {
|
|
356
|
+
flushTextBuffer();
|
|
357
|
+
},
|
|
358
|
+
onTextBlock: (text) => {
|
|
359
|
+
w.write(renderMarkdown(text));
|
|
360
|
+
if (!text.endsWith("\n"))
|
|
361
|
+
w.write("\n");
|
|
362
|
+
},
|
|
363
|
+
onToolStart: (name, _input, _count) => {
|
|
364
|
+
flushTextBuffer();
|
|
365
|
+
w.write(s.dim(` ⠋ ${name}...\r`));
|
|
366
|
+
},
|
|
367
|
+
onToolEnd: (name, input, output, isError, dur) => {
|
|
368
|
+
w.write(`${ESC}2K\r`);
|
|
369
|
+
const diffData = (name === "edit_file" || name === "write_file") ? decodeDiffPayload(output) : null;
|
|
370
|
+
const cleanOutput = diffData ? output.slice(0, output.indexOf(DIFF_MARKER)) : output;
|
|
371
|
+
w.write(renderToolCall(name, input, cleanOutput, isError, dur) + "\n");
|
|
372
|
+
if (diffData) {
|
|
373
|
+
w.write(renderInlineDiff(diffData.oldContent, diffData.newContent, diffData.filePath) + "\n");
|
|
374
|
+
}
|
|
375
|
+
},
|
|
376
|
+
onStatus: (msg) => w.write(s.dim(msg)),
|
|
377
|
+
getSteeringInput: () => {
|
|
378
|
+
if (pendingInput && inputMode === "steering") {
|
|
379
|
+
const steer = pendingInput;
|
|
380
|
+
pendingInput = null;
|
|
381
|
+
w.write(s.yellow(` ↳ steering: ${steer}\n`));
|
|
382
|
+
return steer;
|
|
383
|
+
}
|
|
384
|
+
return null;
|
|
385
|
+
},
|
|
386
|
+
};
|
|
387
|
+
async function runAgentTurn(userInput) {
|
|
388
|
+
running = true;
|
|
389
|
+
w.write(s.dim(" ⠋ Thinking...\r"));
|
|
390
|
+
try {
|
|
391
|
+
await runTurn(userInput, session, config, tuiHooks);
|
|
392
|
+
statusBar();
|
|
393
|
+
}
|
|
394
|
+
catch (err) {
|
|
395
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
396
|
+
w.write(s.red(` Error: ${msg}\n`));
|
|
397
|
+
}
|
|
398
|
+
running = false;
|
|
399
|
+
// Process queued input
|
|
400
|
+
if (pendingInput) {
|
|
401
|
+
const queued = pendingInput;
|
|
402
|
+
pendingInput = null;
|
|
403
|
+
runAgentTurn(queued);
|
|
404
|
+
}
|
|
405
|
+
else {
|
|
406
|
+
prompt();
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
// Initial prompt
|
|
410
|
+
prompt();
|
|
411
|
+
return done;
|
|
412
|
+
}
|
|
413
|
+
// ── Settings persistence ─────────────────────────────────────────────────────
|
|
414
|
+
const SETTINGS_FILE = path.join(os.homedir(), ".phren-agent", "settings.json");
|
|
415
|
+
function loadInputMode() {
|
|
416
|
+
try {
|
|
417
|
+
const data = JSON.parse(fs.readFileSync(SETTINGS_FILE, "utf-8"));
|
|
418
|
+
if (data.inputMode === "queue")
|
|
419
|
+
return "queue";
|
|
420
|
+
}
|
|
421
|
+
catch { }
|
|
422
|
+
return "steering";
|
|
423
|
+
}
|
|
424
|
+
function saveInputMode(mode) {
|
|
425
|
+
try {
|
|
426
|
+
const dir = path.dirname(SETTINGS_FILE);
|
|
427
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
428
|
+
let data = {};
|
|
429
|
+
try {
|
|
430
|
+
data = JSON.parse(fs.readFileSync(SETTINGS_FILE, "utf-8"));
|
|
431
|
+
}
|
|
432
|
+
catch { }
|
|
433
|
+
data.inputMode = mode;
|
|
434
|
+
fs.writeFileSync(SETTINGS_FILE, JSON.stringify(data, null, 2) + "\n");
|
|
435
|
+
}
|
|
436
|
+
catch { }
|
|
437
|
+
}
|
|
438
|
+
function savePermissionMode(mode) {
|
|
439
|
+
try {
|
|
440
|
+
const dir = path.dirname(SETTINGS_FILE);
|
|
441
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
442
|
+
let data = {};
|
|
443
|
+
try {
|
|
444
|
+
data = JSON.parse(fs.readFileSync(SETTINGS_FILE, "utf-8"));
|
|
445
|
+
}
|
|
446
|
+
catch { }
|
|
447
|
+
data.permissionMode = mode;
|
|
448
|
+
fs.writeFileSync(SETTINGS_FILE, JSON.stringify(data, null, 2) + "\n");
|
|
449
|
+
}
|
|
450
|
+
catch { }
|
|
451
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@phren/agent",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Coding agent with persistent memory — powered by phren",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"phren-agent": "dist/bin.js"
|
|
8
|
+
},
|
|
9
|
+
"exports": {
|
|
10
|
+
".": "./dist/index.js"
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"dist"
|
|
14
|
+
],
|
|
15
|
+
"scripts": {
|
|
16
|
+
"build": "tsc -p tsconfig.json",
|
|
17
|
+
"test": "echo 'Run tests from repo root: pnpm -w test'"
|
|
18
|
+
},
|
|
19
|
+
"dependencies": {
|
|
20
|
+
"@phren/cli": "workspace:*"
|
|
21
|
+
},
|
|
22
|
+
"engines": {
|
|
23
|
+
"node": ">=20.0.0"
|
|
24
|
+
},
|
|
25
|
+
"keywords": [
|
|
26
|
+
"agent",
|
|
27
|
+
"coding-agent",
|
|
28
|
+
"tui",
|
|
29
|
+
"phren",
|
|
30
|
+
"memory"
|
|
31
|
+
],
|
|
32
|
+
"author": "Ala Arab",
|
|
33
|
+
"license": "MIT",
|
|
34
|
+
"repository": {
|
|
35
|
+
"type": "git",
|
|
36
|
+
"url": "git+https://github.com/alaarab/phren.git",
|
|
37
|
+
"directory": "packages/agent"
|
|
38
|
+
}
|
|
39
|
+
}
|