@phren/agent 0.0.1 → 0.0.3

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.
@@ -3,36 +3,45 @@ import { AnthropicProvider } from "./anthropic.js";
3
3
  import { OllamaProvider } from "./ollama.js";
4
4
  import { CodexProvider } from "./codex.js";
5
5
  import { hasCodexToken } from "./codex-auth.js";
6
- export function resolveProvider(overrideProvider, overrideModel) {
6
+ import { lookupMaxOutputTokens } from "../cost.js";
7
+ export function resolveProvider(overrideProvider, overrideModel, overrideMaxOutput) {
7
8
  const explicit = overrideProvider ?? process.env.PHREN_AGENT_PROVIDER;
9
+ // Resolve max output tokens: CLI override > model lookup > default 8192
10
+ const resolveLimit = (model) => overrideMaxOutput ?? lookupMaxOutputTokens(model);
8
11
  if (explicit === "openrouter" || (!explicit && process.env.OPENROUTER_API_KEY)) {
9
12
  const key = process.env.OPENROUTER_API_KEY;
10
13
  if (!key)
11
14
  throw new Error("OPENROUTER_API_KEY is required for OpenRouter provider.");
12
- return new OpenRouterProvider(key, overrideModel);
15
+ const model = overrideModel ?? "anthropic/claude-sonnet-4-20250514";
16
+ return new OpenRouterProvider(key, overrideModel, undefined, resolveLimit(model));
13
17
  }
14
18
  if (explicit === "anthropic" || (!explicit && process.env.ANTHROPIC_API_KEY)) {
15
19
  const key = process.env.ANTHROPIC_API_KEY;
16
20
  if (!key)
17
21
  throw new Error("ANTHROPIC_API_KEY is required for Anthropic provider.");
18
- return new AnthropicProvider(key, overrideModel);
22
+ const model = overrideModel ?? "claude-sonnet-4-20250514";
23
+ return new AnthropicProvider(key, overrideModel, resolveLimit(model));
19
24
  }
20
25
  if (explicit === "openai" || (!explicit && process.env.OPENAI_API_KEY)) {
21
26
  const key = process.env.OPENAI_API_KEY;
22
27
  if (!key)
23
28
  throw new Error("OPENAI_API_KEY is required for OpenAI provider.");
24
- return new OpenAiProvider(key, overrideModel);
29
+ const model = overrideModel ?? "gpt-4o";
30
+ return new OpenAiProvider(key, overrideModel, undefined, resolveLimit(model));
25
31
  }
26
32
  // Codex: uses your ChatGPT subscription directly — no API key, no middleman
27
33
  if (explicit === "codex" || (!explicit && hasCodexToken())) {
28
- return new CodexProvider(overrideModel);
34
+ const model = overrideModel ?? "gpt-5.2-codex";
35
+ return new CodexProvider(overrideModel, resolveLimit(model));
29
36
  }
30
37
  if (explicit === "ollama" || (!explicit && process.env.PHREN_OLLAMA_URL && process.env.PHREN_OLLAMA_URL !== "off")) {
31
- return new OllamaProvider(overrideModel, process.env.PHREN_OLLAMA_URL);
38
+ const model = overrideModel ?? "qwen2.5-coder:14b";
39
+ return new OllamaProvider(overrideModel, process.env.PHREN_OLLAMA_URL, resolveLimit(model));
32
40
  }
33
41
  // Last resort: try Ollama at default URL
34
42
  if (!explicit) {
35
- return new OllamaProvider(overrideModel);
43
+ const model = overrideModel ?? "qwen2.5-coder:14b";
44
+ return new OllamaProvider(overrideModel, undefined, resolveLimit(model));
36
45
  }
37
46
  throw new Error(`Unknown provider "${explicit}". Supported: openrouter, anthropic, openai, codex, ollama.\n` +
38
47
  "Set one of: OPENROUTER_API_KEY, ANTHROPIC_API_KEY, OPENAI_API_KEY, or run 'phren-agent auth login' for Codex.");
package/dist/repl.js CHANGED
@@ -5,9 +5,9 @@ import * as path from "node:path";
5
5
  import * as os from "node:os";
6
6
  import { createSession, runTurn } from "./agent-loop.js";
7
7
  import { handleCommand } from "./commands.js";
8
+ import { loadInputMode, saveInputMode, savePermissionMode } from "./settings.js";
8
9
  const HISTORY_DIR = path.join(os.homedir(), ".phren-agent");
9
10
  const HISTORY_FILE = path.join(HISTORY_DIR, "repl-history.txt");
10
- const SETTINGS_FILE = path.join(HISTORY_DIR, "settings.json");
11
11
  const MAX_HISTORY = 500;
12
12
  const CYAN = "\x1b[36m";
13
13
  const RED = "\x1b[31m";
@@ -30,41 +30,6 @@ function saveHistory(lines) {
30
30
  }
31
31
  catch { /* ignore */ }
32
32
  }
33
- function loadInputMode() {
34
- try {
35
- const data = JSON.parse(fs.readFileSync(SETTINGS_FILE, "utf-8"));
36
- if (data.inputMode === "queue")
37
- return "queue";
38
- }
39
- catch { /* ignore */ }
40
- return "steering";
41
- }
42
- function saveInputMode(mode) {
43
- try {
44
- fs.mkdirSync(HISTORY_DIR, { recursive: true });
45
- let data = {};
46
- try {
47
- data = JSON.parse(fs.readFileSync(SETTINGS_FILE, "utf-8"));
48
- }
49
- catch { /* fresh */ }
50
- data.inputMode = mode;
51
- fs.writeFileSync(SETTINGS_FILE, JSON.stringify(data, null, 2) + "\n");
52
- }
53
- catch { /* ignore */ }
54
- }
55
- function savePermissionMode(mode) {
56
- try {
57
- fs.mkdirSync(HISTORY_DIR, { recursive: true });
58
- let data = {};
59
- try {
60
- data = JSON.parse(fs.readFileSync(SETTINGS_FILE, "utf-8"));
61
- }
62
- catch { /* fresh */ }
63
- data.permissionMode = mode;
64
- fs.writeFileSync(SETTINGS_FILE, JSON.stringify(data, null, 2) + "\n");
65
- }
66
- catch { /* ignore */ }
67
- }
68
33
  export async function startRepl(config) {
69
34
  const contextLimit = config.provider.contextWindow ?? 200_000;
70
35
  const session = createSession(contextLimit);
@@ -0,0 +1,42 @@
1
+ /** Shared settings persistence for agent TUI and REPL. */
2
+ import * as fs from "fs";
3
+ import * as path from "path";
4
+ import * as os from "os";
5
+ export const SETTINGS_FILE = path.join(os.homedir(), ".phren-agent", "settings.json");
6
+ export function loadInputMode() {
7
+ try {
8
+ const data = JSON.parse(fs.readFileSync(SETTINGS_FILE, "utf-8"));
9
+ if (data.inputMode === "queue")
10
+ return "queue";
11
+ }
12
+ catch { }
13
+ return "steering";
14
+ }
15
+ export function saveInputMode(mode) {
16
+ try {
17
+ const dir = path.dirname(SETTINGS_FILE);
18
+ fs.mkdirSync(dir, { recursive: true });
19
+ let data = {};
20
+ try {
21
+ data = JSON.parse(fs.readFileSync(SETTINGS_FILE, "utf-8"));
22
+ }
23
+ catch { }
24
+ data.inputMode = mode;
25
+ fs.writeFileSync(SETTINGS_FILE, JSON.stringify(data, null, 2) + "\n");
26
+ }
27
+ catch { }
28
+ }
29
+ export function savePermissionMode(mode) {
30
+ try {
31
+ const dir = path.dirname(SETTINGS_FILE);
32
+ fs.mkdirSync(dir, { recursive: true });
33
+ let data = {};
34
+ try {
35
+ data = JSON.parse(fs.readFileSync(SETTINGS_FILE, "utf-8"));
36
+ }
37
+ catch { }
38
+ data.permissionMode = mode;
39
+ fs.writeFileSync(SETTINGS_FILE, JSON.stringify(data, null, 2) + "\n");
40
+ }
41
+ catch { }
42
+ }
@@ -1,5 +1,7 @@
1
1
  import * as fs from "fs";
2
+ import * as path from "path";
2
3
  import { encodeDiffPayload } from "../multi/diff-renderer.js";
4
+ import { checkSensitivePath, validatePath } from "../permissions/sandbox.js";
3
5
  export const editFileTool = {
4
6
  name: "edit_file",
5
7
  description: "Edit a file by replacing an exact string match. Preferred over write_file for modifying existing files — only changes what's needed. The old_string must appear exactly once; include surrounding lines for uniqueness if needed.",
@@ -16,6 +18,17 @@ export const editFileTool = {
16
18
  const filePath = input.path;
17
19
  const oldStr = input.old_string;
18
20
  const newStr = input.new_string;
21
+ // Defense-in-depth: sensitive path check
22
+ const resolved = path.resolve(filePath);
23
+ const sensitive = checkSensitivePath(resolved);
24
+ if (sensitive.sensitive) {
25
+ return { output: `Access denied: ${sensitive.reason}`, is_error: true };
26
+ }
27
+ // Defense-in-depth: sandbox check
28
+ const sandboxResult = validatePath(filePath, process.cwd(), []);
29
+ if (!sandboxResult.ok) {
30
+ return { output: `Path outside sandbox: ${sandboxResult.error}`, is_error: true };
31
+ }
19
32
  if (!fs.existsSync(filePath))
20
33
  return { output: `File not found: ${filePath}`, is_error: true };
21
34
  const oldContent = fs.readFileSync(filePath, "utf-8");
package/dist/tools/git.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { execFileSync } from "child_process";
2
+ import { validatePath } from "../permissions/sandbox.js";
2
3
  function git(args, cwd) {
3
4
  return execFileSync("git", args, {
4
5
  cwd,
@@ -20,6 +21,10 @@ export const gitStatusTool = {
20
21
  },
21
22
  async execute(input) {
22
23
  const cwd = input.cwd || process.cwd();
24
+ const cwdCheck = validatePath(cwd, process.cwd(), []);
25
+ if (!cwdCheck.ok) {
26
+ return { output: `Git cwd outside sandbox: ${cwdCheck.error}`, is_error: true };
27
+ }
23
28
  try {
24
29
  const output = git(["status", "--short"], cwd);
25
30
  return { output: output || "(working tree clean)" };
@@ -43,6 +48,10 @@ export const gitDiffTool = {
43
48
  },
44
49
  async execute(input) {
45
50
  const cwd = input.cwd || process.cwd();
51
+ const cwdCheck = validatePath(cwd, process.cwd(), []);
52
+ if (!cwdCheck.ok) {
53
+ return { output: `Git cwd outside sandbox: ${cwdCheck.error}`, is_error: true };
54
+ }
46
55
  const args = ["diff"];
47
56
  if (input.cached)
48
57
  args.push("--cached");
@@ -75,6 +84,10 @@ export const gitCommitTool = {
75
84
  },
76
85
  async execute(input) {
77
86
  const cwd = input.cwd || process.cwd();
87
+ const cwdCheck = validatePath(cwd, process.cwd(), []);
88
+ if (!cwdCheck.ok) {
89
+ return { output: `Git cwd outside sandbox: ${cwdCheck.error}`, is_error: true };
90
+ }
78
91
  const message = input.message;
79
92
  const files = input.files || [];
80
93
  try {
@@ -1,6 +1,7 @@
1
1
  import * as fs from "fs";
2
2
  import * as path from "path";
3
3
  import { encodeDiffPayload } from "../multi/diff-renderer.js";
4
+ import { checkSensitivePath, validatePath } from "../permissions/sandbox.js";
4
5
  export const writeFileTool = {
5
6
  name: "write_file",
6
7
  description: "Write content to a file, creating parent directories as needed. Use for new files only — prefer edit_file for modifying existing files. Overwrites existing content entirely.",
@@ -15,6 +16,17 @@ export const writeFileTool = {
15
16
  async execute(input) {
16
17
  const filePath = input.path;
17
18
  const content = input.content;
19
+ // Defense-in-depth: sensitive path check
20
+ const resolved = path.resolve(filePath);
21
+ const sensitive = checkSensitivePath(resolved);
22
+ if (sensitive.sensitive) {
23
+ return { output: `Access denied: ${sensitive.reason}`, is_error: true };
24
+ }
25
+ // Defense-in-depth: sandbox check
26
+ const sandboxResult = validatePath(filePath, process.cwd(), []);
27
+ if (!sandboxResult.ok) {
28
+ return { output: `Path outside sandbox: ${sandboxResult.error}`, is_error: true };
29
+ }
18
30
  const oldContent = fs.existsSync(filePath) ? fs.readFileSync(filePath, "utf-8") : "";
19
31
  fs.mkdirSync(path.dirname(filePath), { recursive: true });
20
32
  fs.writeFileSync(filePath, content);
package/dist/tui.js CHANGED
@@ -8,21 +8,25 @@ import { createSession, runTurn } from "./agent-loop.js";
8
8
  import { handleCommand } from "./commands.js";
9
9
  import { renderMarkdown } from "./multi/markdown.js";
10
10
  import { decodeDiffPayload, renderInlineDiff, DIFF_MARKER } from "./multi/diff-renderer.js";
11
- import * as fs from "fs";
12
- import * as path from "path";
13
11
  import * as os from "os";
12
+ import { loadInputMode, saveInputMode, savePermissionMode } from "./settings.js";
14
13
  // ── ANSI helpers ─────────────────────────────────────────────────────────────
15
14
  const ESC = "\x1b[";
16
15
  const s = {
17
16
  reset: `${ESC}0m`,
18
17
  bold: (t) => `${ESC}1m${t}${ESC}0m`,
19
18
  dim: (t) => `${ESC}2m${t}${ESC}0m`,
19
+ italic: (t) => `${ESC}3m${t}${ESC}0m`,
20
20
  cyan: (t) => `${ESC}36m${t}${ESC}0m`,
21
21
  green: (t) => `${ESC}32m${t}${ESC}0m`,
22
22
  yellow: (t) => `${ESC}33m${t}${ESC}0m`,
23
23
  red: (t) => `${ESC}31m${t}${ESC}0m`,
24
+ blue: (t) => `${ESC}34m${t}${ESC}0m`,
25
+ magenta: (t) => `${ESC}35m${t}${ESC}0m`,
24
26
  gray: (t) => `${ESC}90m${t}${ESC}0m`,
25
27
  invert: (t) => `${ESC}7m${t}${ESC}0m`,
28
+ // Gradient-style brand text
29
+ brand: (t) => `${ESC}1;35m${t}${ESC}0m`,
26
30
  };
27
31
  function cols() {
28
32
  return process.stdout.columns || 80;
@@ -38,23 +42,28 @@ const PERMISSION_LABELS = {
38
42
  "auto-confirm": "auto",
39
43
  "full-auto": "full-auto",
40
44
  };
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
45
  // ── Status bar ───────────────────────────────────────────────────────────────
50
46
  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}`)} `;
47
+ const modeLabel = permMode ? PERMISSION_LABELS[permMode] : "";
48
+ const agentTag = agentCount && agentCount > 0 ? ` A${agentCount}` : "";
49
+ // Left: brand + provider + project
50
+ const parts = [" phren", provider];
51
+ if (project)
52
+ parts.push(project);
53
+ const left = parts.join(" · ");
54
+ // Right: mode + agents + cost + turns
55
+ const rightParts = [];
56
+ if (modeLabel)
57
+ rightParts.push(modeLabel);
58
+ if (agentTag)
59
+ rightParts.push(agentTag.trim());
60
+ if (cost)
61
+ rightParts.push(cost);
62
+ rightParts.push(`T${turns}`);
63
+ const right = rightParts.join(" ") + " ";
55
64
  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));
65
+ const pad = Math.max(0, w - left.length - right.length);
66
+ return s.invert(left + " ".repeat(pad) + right);
58
67
  }
59
68
  function stripAnsi(t) {
60
69
  return t.replace(/\x1b\[[0-9;?]*[ -/]*[@-~]/g, "");
@@ -70,17 +79,34 @@ function formatDuration(ms) {
70
79
  const secs = Math.round((ms % 60_000) / 1000);
71
80
  return `${mins}m ${secs}s`;
72
81
  }
82
+ function formatToolInput(name, input) {
83
+ // Show the most relevant input field for each tool type
84
+ switch (name) {
85
+ case "read_file": return input.file_path ?? "";
86
+ case "write_file": return input.file_path ?? "";
87
+ case "edit_file": return input.file_path ?? "";
88
+ case "shell": return (input.command ?? "").slice(0, 60);
89
+ case "glob": return input.pattern ?? "";
90
+ case "grep": return `/${input.pattern ?? ""}/ ${input.path ?? ""}`;
91
+ case "git_commit": return (input.message ?? "").slice(0, 50);
92
+ case "phren_search": return input.query ?? "";
93
+ case "phren_add_finding": return (input.finding ?? "").slice(0, 50);
94
+ default: return JSON.stringify(input).slice(0, 60);
95
+ }
96
+ }
73
97
  function renderToolCall(name, input, output, isError, durationMs) {
74
- const inputPreview = JSON.stringify(input).slice(0, 80);
98
+ const preview = formatToolInput(name, input);
75
99
  const dur = formatDuration(durationMs);
76
- const icon = isError ? s.red("✗") : s.green("");
77
- const header = s.dim(` ${name}(${inputPreview})`) + ` ${icon} ${s.dim(dur)}`;
100
+ const icon = isError ? s.red("✗") : s.green("");
101
+ const header = ` ${icon} ${s.bold(name)} ${s.gray(preview)} ${s.dim(dur)}`;
78
102
  // Compact: show first 3 lines only, with overflow count
79
103
  const allLines = output.split("\n").filter(Boolean);
104
+ if (allLines.length === 0)
105
+ return header;
80
106
  const shown = allLines.slice(0, COMPACT_LINES);
81
- const body = shown.map((l) => s.dim(`${l.slice(0, cols() - 6)}`)).join("\n");
107
+ const body = shown.map((l) => s.dim(` ${l.slice(0, cols() - 6)}`)).join("\n");
82
108
  const overflow = allLines.length - COMPACT_LINES;
83
- const more = overflow > 0 ? `\n${s.dim(` [+${overflow} lines]`)}` : "";
109
+ const more = overflow > 0 ? `\n${s.dim(` ... +${overflow} lines`)}` : "";
84
110
  return `${header}\n${body}${more}`;
85
111
  }
86
112
  // ── Menu mode helpers ────────────────────────────────────────────────────────
@@ -155,8 +181,10 @@ export async function startTui(config, spawner) {
155
181
  }
156
182
  // Print prompt
157
183
  function prompt() {
158
- const modeTag = inputMode === "steering" ? s.dim("[steer]") : s.dim("[queue]");
159
- w.write(`\n${s.cyan("phren>")} ${modeTag} `);
184
+ const mode = config.registry.permissionConfig.mode;
185
+ const modeIcon = mode === "full-auto" ? "●" : mode === "auto-confirm" ? "◐" : "○";
186
+ const modeColor = mode === "full-auto" ? s.yellow : mode === "auto-confirm" ? s.green : s.cyan;
187
+ w.write(`\n${modeColor(modeIcon)} ${s.dim("▸")} `);
160
188
  }
161
189
  // Terminal cleanup: restore state on exit
162
190
  function cleanupTerminal() {
@@ -175,7 +203,45 @@ export async function startTui(config, spawner) {
175
203
  w.write(`${ESC}1;1H`); // move to top
176
204
  statusBar();
177
205
  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"));
206
+ // Startup banner
207
+ const project = config.phrenCtx?.project;
208
+ const cwd = process.cwd().replace(os.homedir(), "~");
209
+ const permMode = config.registry.permissionConfig.mode;
210
+ const modeColor = permMode === "full-auto" ? s.yellow : permMode === "auto-confirm" ? s.green : s.cyan;
211
+ // Try to show the phren character art alongside info
212
+ let artLines = [];
213
+ try {
214
+ const { PHREN_ART } = await import("@phren/cli/phren-art");
215
+ artLines = PHREN_ART.filter((l) => l.trim());
216
+ }
217
+ catch { /* art not available */ }
218
+ if (artLines.length > 0) {
219
+ // Art on left, info on right
220
+ const info = [
221
+ `${s.brand("◆ phren agent")} ${s.dim("v0.0.1")}`,
222
+ `${s.dim(config.provider.name)}${project ? s.dim(` · ${project}`) : ""}`,
223
+ `${s.dim(cwd)}`,
224
+ ``,
225
+ `${modeColor(`${permMode === "full-auto" ? "●" : permMode === "auto-confirm" ? "◐" : "○"} ${permMode}`)} ${s.dim("permissions (shift+tab to cycle)")}`,
226
+ ``,
227
+ `${s.dim("Tab")} memory ${s.dim("Shift+Tab")} perms ${s.dim("/help")} cmds ${s.dim("Ctrl+D")} exit`,
228
+ ];
229
+ const maxArtWidth = 26; // phren art is ~24 chars wide
230
+ for (let i = 0; i < Math.max(artLines.length, info.length); i++) {
231
+ const artPart = i < artLines.length ? artLines[i] : "";
232
+ const infoPart = i < info.length ? info[i] : "";
233
+ const artPadded = artPart + " ".repeat(Math.max(0, maxArtWidth - stripAnsi(artPart).length));
234
+ w.write(`${artPadded}${infoPart}\n`);
235
+ }
236
+ }
237
+ else {
238
+ // Fallback: text-only banner
239
+ w.write(`\n ${s.brand("◆ phren agent")} ${s.dim("v0.0.1")}\n`);
240
+ w.write(` ${s.dim(config.provider.name)}${project ? s.dim(` · ${project}`) : ""} ${s.dim(cwd)}\n`);
241
+ w.write(` ${modeColor(`${permMode === "full-auto" ? "●" : permMode === "auto-confirm" ? "◐" : "○"} ${permMode}`)} ${s.dim("permissions (shift+tab to cycle)")}\n\n`);
242
+ w.write(` ${s.dim("Tab")} memory ${s.dim("Shift+Tab")} perms ${s.dim("/help")} cmds ${s.dim("Ctrl+D")} exit\n\n`);
243
+ }
244
+ w.write("\n");
179
245
  }
180
246
  // Raw stdin for steering
181
247
  if (process.stdin.isTTY) {
@@ -255,7 +321,9 @@ export async function startTui(config, spawner) {
255
321
  const next = nextPermissionMode(current);
256
322
  config.registry.setPermissions({ ...config.registry.permissionConfig, mode: next });
257
323
  savePermissionMode(next);
258
- w.write(s.yellow(` [mode: ${next}]\n`));
324
+ const modeColor = next === "full-auto" ? s.yellow : next === "auto-confirm" ? s.green : s.cyan;
325
+ const modeIcon = next === "full-auto" ? "●" : next === "auto-confirm" ? "◐" : "○";
326
+ w.write(` ${modeColor(`${modeIcon} ${next}`)}\n`);
259
327
  statusBar();
260
328
  if (!running)
261
329
  prompt();
@@ -360,9 +428,11 @@ export async function startTui(config, spawner) {
360
428
  if (!text.endsWith("\n"))
361
429
  w.write("\n");
362
430
  },
363
- onToolStart: (name, _input, _count) => {
431
+ onToolStart: (name, input, count) => {
364
432
  flushTextBuffer();
365
- w.write(s.dim(` ⠋ ${name}...\r`));
433
+ const preview = formatToolInput(name, input);
434
+ const countLabel = count > 1 ? s.dim(` (${count} tools)`) : "";
435
+ w.write(`${ESC}2K ${s.dim("◌")} ${s.gray(name)} ${s.dim(preview)}${countLabel}\r`);
366
436
  },
367
437
  onToolEnd: (name, input, output, isError, dur) => {
368
438
  w.write(`${ESC}2K\r`);
@@ -386,7 +456,7 @@ export async function startTui(config, spawner) {
386
456
  };
387
457
  async function runAgentTurn(userInput) {
388
458
  running = true;
389
- w.write(s.dim(" Thinking...\r"));
459
+ w.write(`${ESC}2K ${s.dim(" thinking...")}\r`);
390
460
  try {
391
461
  await runTurn(userInput, session, config, tuiHooks);
392
462
  statusBar();
@@ -410,42 +480,3 @@ export async function startTui(config, spawner) {
410
480
  prompt();
411
481
  return done;
412
482
  }
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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@phren/agent",
3
- "version": "0.0.1",
3
+ "version": "0.0.3",
4
4
  "description": "Coding agent with persistent memory — powered by phren",
5
5
  "type": "module",
6
6
  "bin": {
@@ -12,12 +12,8 @@
12
12
  "files": [
13
13
  "dist"
14
14
  ],
15
- "scripts": {
16
- "build": "tsc -p tsconfig.json",
17
- "test": "echo 'Run tests from repo root: pnpm -w test'"
18
- },
19
15
  "dependencies": {
20
- "@phren/cli": "workspace:*"
16
+ "@phren/cli": "0.0.58"
21
17
  },
22
18
  "engines": {
23
19
  "node": ">=20.0.0"
@@ -35,5 +31,9 @@
35
31
  "type": "git",
36
32
  "url": "git+https://github.com/alaarab/phren.git",
37
33
  "directory": "packages/agent"
34
+ },
35
+ "scripts": {
36
+ "build": "tsc -p tsconfig.json",
37
+ "test": "echo 'Run tests from repo root: pnpm -w test'"
38
38
  }
39
- }
39
+ }
@@ -1,32 +0,0 @@
1
- /**
2
- * Progress indicators for terminal output.
3
- */
4
- const FILLED = "█";
5
- const EMPTY = "░";
6
- const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
7
- /** Render a progress bar: `[████░░░░] 50%` */
8
- export function renderProgressBar(current, total, width = 20) {
9
- if (total <= 0)
10
- return `[${"░".repeat(width)}] 0%`;
11
- const pct = Math.min(1, Math.max(0, current / total));
12
- const filled = Math.round(pct * width);
13
- const empty = width - filled;
14
- const percent = Math.round(pct * 100);
15
- return `[${FILLED.repeat(filled)}${EMPTY.repeat(empty)}] ${percent}%`;
16
- }
17
- /** Get a braille spinner frame by index (wraps automatically). */
18
- export function renderSpinnerFrame(frame) {
19
- return SPINNER_FRAMES[frame % SPINNER_FRAMES.length];
20
- }
21
- /** Format elapsed time from a start timestamp: `2.1s` or `1m 30s`. */
22
- export function renderElapsed(startMs) {
23
- const elapsed = Date.now() - startMs;
24
- if (elapsed < 1000)
25
- return `${elapsed}ms`;
26
- const secs = elapsed / 1000;
27
- if (secs < 60)
28
- return `${secs.toFixed(1)}s`;
29
- const mins = Math.floor(secs / 60);
30
- const remSecs = Math.round(secs % 60);
31
- return `${mins}m ${remSecs}s`;
32
- }