@phren/agent 0.1.1 → 0.1.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.
- package/dist/agent-loop.js +10 -5
- package/dist/checkpoint.js +0 -34
- package/dist/commands.js +351 -4
- package/dist/config.js +6 -2
- package/dist/index.js +12 -2
- package/dist/multi/model-picker.js +0 -2
- package/dist/multi/provider-manager.js +0 -23
- package/dist/multi/spawner.js +3 -2
- package/dist/multi/syntax-highlight.js +0 -1
- package/dist/multi/tui-multi.js +4 -6
- package/dist/permissions/allowlist.js +0 -4
- package/dist/permissions/privacy.js +248 -0
- package/dist/permissions/shell-safety.js +8 -0
- package/dist/providers/anthropic.js +68 -31
- package/dist/providers/codex.js +112 -56
- package/dist/repl.js +2 -2
- package/dist/system-prompt.js +26 -27
- package/dist/tools/phren-add-task.js +49 -0
- package/dist/tools/shell.js +5 -2
- package/dist/tools/web-fetch.js +40 -0
- package/dist/tools/web-search.js +93 -0
- package/dist/tui.js +381 -62
- package/package.json +2 -2
package/dist/agent-loop.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createSpinner, formatTurnHeader, formatToolCall } from "./spinner.js";
|
|
2
2
|
import { searchErrorRecovery } from "./memory/error-recovery.js";
|
|
3
3
|
import { shouldPrune, pruneMessages } from "./context/pruner.js";
|
|
4
|
+
import { estimateMessageTokens } from "./context/token-counter.js";
|
|
4
5
|
import { withRetry } from "./providers/retry.js";
|
|
5
6
|
import { createCaptureState, analyzeAndCapture } from "./memory/auto-capture.js";
|
|
6
7
|
import { AntiPatternTracker } from "./memory/anti-patterns.js";
|
|
@@ -9,7 +10,6 @@ import { injectPlanPrompt, requestPlanApproval } from "./plan.js";
|
|
|
9
10
|
import { detectLintCommand, detectTestCommand, runPostEditCheck } from "./tools/lint-test.js";
|
|
10
11
|
import { createCheckpoint } from "./checkpoint.js";
|
|
11
12
|
const MAX_TOOL_CONCURRENCY = 5;
|
|
12
|
-
const MAX_LINT_TEST_RETRIES = 3;
|
|
13
13
|
export function createSession(contextLimit) {
|
|
14
14
|
return {
|
|
15
15
|
messages: [],
|
|
@@ -76,7 +76,7 @@ async function consumeStream(stream, costTracker, onTextDelta) {
|
|
|
76
76
|
try {
|
|
77
77
|
input = JSON.parse(jsonStr);
|
|
78
78
|
}
|
|
79
|
-
catch
|
|
79
|
+
catch {
|
|
80
80
|
process.stderr.write(`\x1b[33m[warning] Malformed tool_use JSON for ${tool.name} (${tool.id}), skipping block\x1b[0m\n`);
|
|
81
81
|
continue;
|
|
82
82
|
}
|
|
@@ -105,7 +105,6 @@ export async function runTurn(userInput, session, config, hooks) {
|
|
|
105
105
|
const toolDefs = registry.getDefinitions();
|
|
106
106
|
const spinner = createSpinner();
|
|
107
107
|
const useStream = typeof provider.chatStream === "function";
|
|
108
|
-
const write = hooks?.onTextDelta ?? process.stdout.write.bind(process.stdout);
|
|
109
108
|
const status = hooks?.onStatus ?? ((msg) => process.stderr.write(msg));
|
|
110
109
|
// Plan mode: modify system prompt for first turn
|
|
111
110
|
let planPending = config.plan && session.turns === 0;
|
|
@@ -135,9 +134,15 @@ export async function runTurn(userInput, session, config, hooks) {
|
|
|
135
134
|
}
|
|
136
135
|
// Prune context if approaching limit
|
|
137
136
|
if (shouldPrune(systemPrompt, session.messages, { contextLimit })) {
|
|
137
|
+
const preCount = session.messages.length;
|
|
138
|
+
const preTokens = estimateMessageTokens(session.messages);
|
|
138
139
|
session.messages = pruneMessages(session.messages, { contextLimit, keepRecentTurns: 6 });
|
|
139
|
-
|
|
140
|
-
|
|
140
|
+
const postCount = session.messages.length;
|
|
141
|
+
const postTokens = estimateMessageTokens(session.messages);
|
|
142
|
+
const reduction = preTokens > 0 ? ((1 - postTokens / preTokens) * 100).toFixed(0) : "0";
|
|
143
|
+
const fmtPre = preTokens >= 1000 ? `${(preTokens / 1000).toFixed(1)}k` : String(preTokens);
|
|
144
|
+
const fmtPost = postTokens >= 1000 ? `${(postTokens / 1000).toFixed(1)}k` : String(postTokens);
|
|
145
|
+
status(`\x1b[2m[context pruned: ${preCount} → ${postCount} messages, ~${fmtPre} → ~${fmtPost} tokens, ${reduction}% reduction]\x1b[0m\n`);
|
|
141
146
|
}
|
|
142
147
|
// For plan mode first turn, pass empty tools so LLM can't call any
|
|
143
148
|
const turnTools = planPending ? [] : toolDefs;
|
package/dist/checkpoint.js
CHANGED
|
@@ -67,37 +67,3 @@ export function createCheckpoint(cwd, label) {
|
|
|
67
67
|
return null;
|
|
68
68
|
}
|
|
69
69
|
}
|
|
70
|
-
/** Rollback to a checkpoint by discarding current changes and applying the stash. */
|
|
71
|
-
export function rollbackToCheckpoint(cwd, ref) {
|
|
72
|
-
if (!isGitRepo(cwd))
|
|
73
|
-
return false;
|
|
74
|
-
try {
|
|
75
|
-
// Discard current working tree changes
|
|
76
|
-
execFileSync("git", ["checkout", "."], {
|
|
77
|
-
cwd,
|
|
78
|
-
encoding: "utf-8",
|
|
79
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
80
|
-
});
|
|
81
|
-
// Apply the stash ref
|
|
82
|
-
execFileSync("git", ["stash", "apply", ref], {
|
|
83
|
-
cwd,
|
|
84
|
-
encoding: "utf-8",
|
|
85
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
86
|
-
});
|
|
87
|
-
return true;
|
|
88
|
-
}
|
|
89
|
-
catch {
|
|
90
|
-
return false;
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
/** List stored checkpoints. */
|
|
94
|
-
export function listCheckpoints(cwd) {
|
|
95
|
-
return loadStore(cwd).checkpoints;
|
|
96
|
-
}
|
|
97
|
-
/** Get the latest checkpoint ref. */
|
|
98
|
-
export function getLatestCheckpoint(cwd) {
|
|
99
|
-
const store = loadStore(cwd);
|
|
100
|
-
return store.checkpoints.length > 0
|
|
101
|
-
? store.checkpoints[store.checkpoints.length - 1].ref
|
|
102
|
-
: null;
|
|
103
|
-
}
|
package/dist/commands.js
CHANGED
|
@@ -4,6 +4,16 @@ import { listPresets, loadPreset, savePreset, deletePreset, formatPreset } from
|
|
|
4
4
|
import { renderMarkdown } from "./multi/markdown.js";
|
|
5
5
|
import { showModelPicker } from "./multi/model-picker.js";
|
|
6
6
|
import { formatProviderList, formatModelAddHelp, addCustomModel, removeCustomModel } from "./multi/provider-manager.js";
|
|
7
|
+
import { execSync } from "node:child_process";
|
|
8
|
+
import * as fs from "node:fs";
|
|
9
|
+
import * as path from "node:path";
|
|
10
|
+
import * as os from "node:os";
|
|
11
|
+
import { saveSessionMessages } from "./memory/session.js";
|
|
12
|
+
import { buildIndex } from "@phren/cli/shared";
|
|
13
|
+
import { searchKnowledgeRows, rankResults } from "@phren/cli/shared/retrieval";
|
|
14
|
+
import { readFindings } from "@phren/cli/data/access";
|
|
15
|
+
import { readTasks } from "@phren/cli/data/tasks";
|
|
16
|
+
import { addFinding } from "@phren/cli/core/finding";
|
|
7
17
|
const DIM = "\x1b[2m";
|
|
8
18
|
const BOLD = "\x1b[1m";
|
|
9
19
|
const CYAN = "\x1b[36m";
|
|
@@ -19,6 +29,17 @@ export function createCommandContext(session, contextLimit) {
|
|
|
19
29
|
undoStack: [],
|
|
20
30
|
};
|
|
21
31
|
}
|
|
32
|
+
/** Format elapsed milliseconds as human-readable duration. */
|
|
33
|
+
function formatElapsed(ms) {
|
|
34
|
+
const secs = Math.floor(ms / 1000);
|
|
35
|
+
if (secs < 60)
|
|
36
|
+
return `${secs}s`;
|
|
37
|
+
const mins = Math.floor(secs / 60);
|
|
38
|
+
if (mins < 60)
|
|
39
|
+
return `${mins}m ${secs % 60}s`;
|
|
40
|
+
const hrs = Math.floor(mins / 60);
|
|
41
|
+
return `${hrs}h ${mins % 60}m`;
|
|
42
|
+
}
|
|
22
43
|
/** Truncate text to N lines, appending [+M lines] if overflow. */
|
|
23
44
|
function truncateText(text, maxLines) {
|
|
24
45
|
const lines = text.split("\n");
|
|
@@ -29,6 +50,7 @@ function truncateText(text, maxLines) {
|
|
|
29
50
|
}
|
|
30
51
|
/**
|
|
31
52
|
* Try to handle a slash command. Returns true if the input was a command.
|
|
53
|
+
* Returns a Promise<boolean> for async commands like /ask.
|
|
32
54
|
*/
|
|
33
55
|
export function handleCommand(input, ctx) {
|
|
34
56
|
const parts = input.trim().split(/\s+/);
|
|
@@ -42,15 +64,28 @@ export function handleCommand(input, ctx) {
|
|
|
42
64
|
/model remove <id> Remove a custom model
|
|
43
65
|
/provider Show configured providers + auth status
|
|
44
66
|
/turns Show turn and tool call counts
|
|
45
|
-
/clear Clear conversation history
|
|
67
|
+
/clear Clear conversation history and terminal screen
|
|
68
|
+
/cwd Show current working directory
|
|
69
|
+
/files Quick file tree (max depth 2, first 30 files)
|
|
46
70
|
/cost Show token usage and estimated cost
|
|
47
71
|
/plan Show conversation plan (tool calls so far)
|
|
48
72
|
/undo Undo last user message and response
|
|
49
73
|
/history [n|full] Show last N messages (default 10) with rich formatting
|
|
50
74
|
/compact Compact conversation to save context space
|
|
75
|
+
/context Show context window usage and provider info
|
|
51
76
|
/mode Toggle input mode (steering ↔ queue)
|
|
52
77
|
/spawn <name> <task> Spawn a background agent
|
|
53
78
|
/agents List running agents
|
|
79
|
+
/session Show session info (id, duration, stats)
|
|
80
|
+
/session save Save conversation checkpoint
|
|
81
|
+
/session export Export conversation as JSON
|
|
82
|
+
/diff [--staged] Show git diff with syntax highlighting
|
|
83
|
+
/git <cmd> Run common git commands (status, log, stash, stash pop)
|
|
84
|
+
/ask <question> Quick LLM query (no tools, not added to session)
|
|
85
|
+
/mem search <query> Search phren memory directly
|
|
86
|
+
/mem findings [project] Show recent findings
|
|
87
|
+
/mem tasks [project] Show tasks
|
|
88
|
+
/mem add <finding> Quick-add a finding
|
|
54
89
|
/preset [name|save|delete|list] Config presets
|
|
55
90
|
/exit Exit the REPL${RESET}\n`);
|
|
56
91
|
return true;
|
|
@@ -129,8 +164,32 @@ export function handleCommand(input, ctx) {
|
|
|
129
164
|
ctx.session.turns = 0;
|
|
130
165
|
ctx.session.toolCalls = 0;
|
|
131
166
|
ctx.undoStack.length = 0;
|
|
167
|
+
process.stdout.write("\x1b[2J\x1b[H"); // clear terminal screen
|
|
132
168
|
process.stderr.write(`${DIM}Conversation cleared.${RESET}\n`);
|
|
133
169
|
return true;
|
|
170
|
+
case "/cwd":
|
|
171
|
+
process.stderr.write(`${DIM}${process.cwd()}${RESET}\n`);
|
|
172
|
+
return true;
|
|
173
|
+
case "/files": {
|
|
174
|
+
try {
|
|
175
|
+
const countRaw = execSync("find . -type f -not -path '*/node_modules/*' -not -path '*/.git/*' -not -path '*/dist/*' | wc -l", { encoding: "utf-8", timeout: 5_000, cwd: process.cwd() }).trim();
|
|
176
|
+
const total = parseInt(countRaw, 10) || 0;
|
|
177
|
+
const listRaw = execSync("find . -maxdepth 2 -type f -not -path '*/node_modules/*' -not -path '*/.git/*' -not -path '*/dist/*' | sort | head -30", { encoding: "utf-8", timeout: 5_000, cwd: process.cwd() }).trim();
|
|
178
|
+
if (!listRaw) {
|
|
179
|
+
process.stderr.write(`${DIM}No files found.${RESET}\n`);
|
|
180
|
+
}
|
|
181
|
+
else {
|
|
182
|
+
const lines = listRaw.split("\n");
|
|
183
|
+
const label = total > lines.length ? `${total} files (showing first ${lines.length})` : `${total} files`;
|
|
184
|
+
process.stderr.write(`${DIM}${label}\n${listRaw}${RESET}\n`);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
catch (err) {
|
|
188
|
+
const e = err;
|
|
189
|
+
process.stderr.write(`${RED}${e.stderr || e.message || "find failed"}${RESET}\n`);
|
|
190
|
+
}
|
|
191
|
+
return true;
|
|
192
|
+
}
|
|
134
193
|
case "/cost": {
|
|
135
194
|
const ct = ctx.costTracker;
|
|
136
195
|
if (ct) {
|
|
@@ -234,10 +293,34 @@ export function handleCommand(input, ctx) {
|
|
|
234
293
|
return true;
|
|
235
294
|
}
|
|
236
295
|
case "/compact": {
|
|
237
|
-
const
|
|
296
|
+
const beforeCount = ctx.session.messages.length;
|
|
297
|
+
const beforeTokens = estimateMessageTokens(ctx.session.messages);
|
|
238
298
|
ctx.session.messages = pruneMessages(ctx.session.messages, { contextLimit: ctx.contextLimit, keepRecentTurns: 4 });
|
|
239
|
-
const
|
|
240
|
-
|
|
299
|
+
const afterCount = ctx.session.messages.length;
|
|
300
|
+
const afterTokens = estimateMessageTokens(ctx.session.messages);
|
|
301
|
+
const reduction = beforeTokens > 0 ? ((1 - afterTokens / beforeTokens) * 100).toFixed(0) : "0";
|
|
302
|
+
const fmtBefore = beforeTokens >= 1000 ? `${(beforeTokens / 1000).toFixed(1)}k` : String(beforeTokens);
|
|
303
|
+
const fmtAfter = afterTokens >= 1000 ? `${(afterTokens / 1000).toFixed(1)}k` : String(afterTokens);
|
|
304
|
+
process.stderr.write(`${DIM}Compacted: ${beforeCount} → ${afterCount} messages (~${fmtBefore} → ~${fmtAfter} tokens, ${reduction}% reduction)${RESET}\n`);
|
|
305
|
+
return true;
|
|
306
|
+
}
|
|
307
|
+
case "/context": {
|
|
308
|
+
const ctxTokens = estimateMessageTokens(ctx.session.messages);
|
|
309
|
+
const ctxPct = ctx.contextLimit > 0 ? (ctxTokens / ctx.contextLimit) * 100 : 0;
|
|
310
|
+
const ctxPctStr = ctxPct.toFixed(1);
|
|
311
|
+
const ctxWindowK = ctx.contextLimit >= 1000 ? `${(ctx.contextLimit / 1000).toFixed(0)}k` : String(ctx.contextLimit);
|
|
312
|
+
const ctxTokensStr = ctxTokens >= 1000 ? `~${(ctxTokens / 1000).toFixed(1)}k` : `~${ctxTokens}`;
|
|
313
|
+
// Progress bar: 10 chars wide
|
|
314
|
+
const filled = Math.round(ctxPct / 10);
|
|
315
|
+
const bar = "█".repeat(Math.min(filled, 10)) + "░".repeat(Math.max(10 - filled, 0));
|
|
316
|
+
const barColor = ctxPct > 80 ? RED : ctxPct > 50 ? YELLOW : GREEN;
|
|
317
|
+
const providerLabel = ctx.providerName ?? "unknown";
|
|
318
|
+
const modelLabel = ctx.currentModel ?? "default";
|
|
319
|
+
process.stderr.write(`${DIM} Messages: ${ctx.session.messages.length}\n` +
|
|
320
|
+
` Tokens: ${ctxTokensStr} / ${ctxWindowK} (${ctxPctStr}%)\n` +
|
|
321
|
+
` Provider: ${providerLabel} (${modelLabel})\n` +
|
|
322
|
+
` Context window: ${ctxWindowK}\n` +
|
|
323
|
+
` ${barColor}[${bar}]${RESET}${DIM} ${ctxPctStr}%${RESET}\n`);
|
|
241
324
|
return true;
|
|
242
325
|
}
|
|
243
326
|
case "/spawn": {
|
|
@@ -346,6 +429,270 @@ export function handleCommand(input, ctx) {
|
|
|
346
429
|
}
|
|
347
430
|
return true;
|
|
348
431
|
}
|
|
432
|
+
case "/session": {
|
|
433
|
+
const sub = parts[1]?.toLowerCase();
|
|
434
|
+
if (sub === "save") {
|
|
435
|
+
if (!ctx.phrenPath || !ctx.sessionId) {
|
|
436
|
+
process.stderr.write(`${DIM}No active phren session to save.${RESET}\n`);
|
|
437
|
+
return true;
|
|
438
|
+
}
|
|
439
|
+
try {
|
|
440
|
+
saveSessionMessages(ctx.phrenPath, ctx.sessionId, ctx.session.messages);
|
|
441
|
+
process.stderr.write(`${GREEN}→ Checkpoint saved (${ctx.session.messages.length} messages)${RESET}\n`);
|
|
442
|
+
}
|
|
443
|
+
catch (err) {
|
|
444
|
+
process.stderr.write(`${RED}${err instanceof Error ? err.message : String(err)}${RESET}\n`);
|
|
445
|
+
}
|
|
446
|
+
return true;
|
|
447
|
+
}
|
|
448
|
+
if (sub === "export") {
|
|
449
|
+
const exportDir = path.join(os.homedir(), ".phren-agent", "exports");
|
|
450
|
+
fs.mkdirSync(exportDir, { recursive: true });
|
|
451
|
+
const ts = new Date().toISOString().replace(/[:.]/g, "-");
|
|
452
|
+
const exportFile = path.join(exportDir, `session-${ts}.json`);
|
|
453
|
+
try {
|
|
454
|
+
fs.writeFileSync(exportFile, JSON.stringify(ctx.session.messages, null, 2) + "\n");
|
|
455
|
+
process.stderr.write(`${GREEN}→ Exported to ${exportFile}${RESET}\n`);
|
|
456
|
+
}
|
|
457
|
+
catch (err) {
|
|
458
|
+
process.stderr.write(`${RED}${err instanceof Error ? err.message : String(err)}${RESET}\n`);
|
|
459
|
+
}
|
|
460
|
+
return true;
|
|
461
|
+
}
|
|
462
|
+
// Default: show session info
|
|
463
|
+
const duration = ctx.startTime ? formatElapsed(Date.now() - ctx.startTime) : "unknown";
|
|
464
|
+
const lines = [];
|
|
465
|
+
if (ctx.sessionId)
|
|
466
|
+
lines.push(` Session: ${ctx.sessionId}`);
|
|
467
|
+
lines.push(` Turns: ${ctx.session.turns}`);
|
|
468
|
+
lines.push(` Tools: ${ctx.session.toolCalls}`);
|
|
469
|
+
lines.push(` Messages: ${ctx.session.messages.length}`);
|
|
470
|
+
lines.push(` Duration: ${duration}`);
|
|
471
|
+
// Read session state file for findings/tasks counters
|
|
472
|
+
if (ctx.phrenPath && ctx.sessionId) {
|
|
473
|
+
try {
|
|
474
|
+
const stateFile = path.join(ctx.phrenPath, ".runtime", "sessions", `session-${ctx.sessionId}.json`);
|
|
475
|
+
const state = JSON.parse(fs.readFileSync(stateFile, "utf-8"));
|
|
476
|
+
lines.push(` Findings: ${state.findingsAdded ?? 0}`);
|
|
477
|
+
lines.push(` Tasks: ${state.tasksCompleted ?? 0}`);
|
|
478
|
+
}
|
|
479
|
+
catch { /* session file may not exist */ }
|
|
480
|
+
}
|
|
481
|
+
process.stderr.write(`${DIM}${lines.join("\n")}${RESET}\n`);
|
|
482
|
+
return true;
|
|
483
|
+
}
|
|
484
|
+
case "/diff": {
|
|
485
|
+
const staged = parts.includes("--staged") || parts.includes("--cached");
|
|
486
|
+
const cmd = staged ? "git diff --staged" : "git diff";
|
|
487
|
+
try {
|
|
488
|
+
const raw = execSync(cmd, { encoding: "utf-8", timeout: 10_000, cwd: process.cwd() });
|
|
489
|
+
if (!raw.trim()) {
|
|
490
|
+
process.stderr.write(`${DIM}No ${staged ? "staged " : ""}changes.${RESET}\n`);
|
|
491
|
+
}
|
|
492
|
+
else {
|
|
493
|
+
const colored = raw.split("\n").map((line) => {
|
|
494
|
+
if (line.startsWith("diff --git"))
|
|
495
|
+
return `${BOLD}${line}${RESET}`;
|
|
496
|
+
if (line.startsWith("@@"))
|
|
497
|
+
return `${CYAN}${line}${RESET}`;
|
|
498
|
+
if (line.startsWith("+"))
|
|
499
|
+
return `${GREEN}${line}${RESET}`;
|
|
500
|
+
if (line.startsWith("-"))
|
|
501
|
+
return `${RED}${line}${RESET}`;
|
|
502
|
+
return line;
|
|
503
|
+
}).join("\n");
|
|
504
|
+
process.stderr.write(colored + "\n");
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
catch (err) {
|
|
508
|
+
const e = err;
|
|
509
|
+
process.stderr.write(`${RED}${e.stderr || e.message || "git diff failed"}${RESET}\n`);
|
|
510
|
+
}
|
|
511
|
+
return true;
|
|
512
|
+
}
|
|
513
|
+
case "/git": {
|
|
514
|
+
const sub = parts.slice(1).join(" ").trim();
|
|
515
|
+
if (!sub) {
|
|
516
|
+
process.stderr.write(`${DIM}Usage: /git <status|log|stash|stash pop>${RESET}\n`);
|
|
517
|
+
return true;
|
|
518
|
+
}
|
|
519
|
+
const allowed = {
|
|
520
|
+
"status": "git status",
|
|
521
|
+
"log": "git log --oneline -5",
|
|
522
|
+
"stash": "git stash",
|
|
523
|
+
"stash pop": "git stash pop",
|
|
524
|
+
};
|
|
525
|
+
const gitCmd = allowed[sub];
|
|
526
|
+
if (!gitCmd) {
|
|
527
|
+
process.stderr.write(`${DIM}Supported: /git status, /git log, /git stash, /git stash pop${RESET}\n`);
|
|
528
|
+
return true;
|
|
529
|
+
}
|
|
530
|
+
try {
|
|
531
|
+
const output = execSync(gitCmd, { encoding: "utf-8", timeout: 10_000, cwd: process.cwd() });
|
|
532
|
+
if (output.trim())
|
|
533
|
+
process.stderr.write(output.endsWith("\n") ? output : output + "\n");
|
|
534
|
+
else
|
|
535
|
+
process.stderr.write(`${DIM}(no output)${RESET}\n`);
|
|
536
|
+
}
|
|
537
|
+
catch (err) {
|
|
538
|
+
const e = err;
|
|
539
|
+
process.stderr.write(`${RED}${e.stderr || e.message || "git command failed"}${RESET}\n`);
|
|
540
|
+
}
|
|
541
|
+
return true;
|
|
542
|
+
}
|
|
543
|
+
case "/mem": {
|
|
544
|
+
const sub = parts[1]?.toLowerCase();
|
|
545
|
+
if (!ctx.phrenCtx) {
|
|
546
|
+
process.stderr.write(`${DIM}No phren context available.${RESET}\n`);
|
|
547
|
+
return true;
|
|
548
|
+
}
|
|
549
|
+
const pCtx = ctx.phrenCtx;
|
|
550
|
+
if (!sub || sub === "help") {
|
|
551
|
+
process.stderr.write(`${DIM}Usage:
|
|
552
|
+
/mem search <query> Search phren memory
|
|
553
|
+
/mem findings [project] Show recent findings
|
|
554
|
+
/mem tasks [project] Show tasks
|
|
555
|
+
/mem add <finding> Quick-add a finding${RESET}\n`);
|
|
556
|
+
return true;
|
|
557
|
+
}
|
|
558
|
+
if (sub === "search") {
|
|
559
|
+
const query = parts.slice(2).join(" ").trim();
|
|
560
|
+
if (!query) {
|
|
561
|
+
process.stderr.write(`${DIM}Usage: /mem search <query>${RESET}\n`);
|
|
562
|
+
return true;
|
|
563
|
+
}
|
|
564
|
+
return (async () => {
|
|
565
|
+
try {
|
|
566
|
+
const db = await buildIndex(pCtx.phrenPath, pCtx.profile);
|
|
567
|
+
const result = await searchKnowledgeRows(db, {
|
|
568
|
+
query,
|
|
569
|
+
maxResults: 10,
|
|
570
|
+
filterProject: pCtx.project || null,
|
|
571
|
+
filterType: null,
|
|
572
|
+
phrenPath: pCtx.phrenPath,
|
|
573
|
+
});
|
|
574
|
+
const ranked = rankResults(result.rows ?? [], query, null, pCtx.project || null, pCtx.phrenPath, db);
|
|
575
|
+
if (ranked.length === 0) {
|
|
576
|
+
process.stderr.write(`${DIM}No results found.${RESET}\n`);
|
|
577
|
+
}
|
|
578
|
+
else {
|
|
579
|
+
const lines = ranked.slice(0, 10).map((r, i) => {
|
|
580
|
+
const snippet = r.content?.slice(0, 200) ?? "";
|
|
581
|
+
return ` ${CYAN}${i + 1}.${RESET} ${DIM}[${r.project}/${r.filename}]${RESET} ${snippet}`;
|
|
582
|
+
});
|
|
583
|
+
process.stderr.write(lines.join("\n") + "\n");
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
catch (err) {
|
|
587
|
+
process.stderr.write(`${RED}Search failed: ${err instanceof Error ? err.message : String(err)}${RESET}\n`);
|
|
588
|
+
}
|
|
589
|
+
return true;
|
|
590
|
+
})();
|
|
591
|
+
}
|
|
592
|
+
if (sub === "findings") {
|
|
593
|
+
const project = parts[2] || pCtx.project;
|
|
594
|
+
if (!project) {
|
|
595
|
+
process.stderr.write(`${DIM}Usage: /mem findings <project>${RESET}\n`);
|
|
596
|
+
return true;
|
|
597
|
+
}
|
|
598
|
+
const result = readFindings(pCtx.phrenPath, project);
|
|
599
|
+
if (!result.ok) {
|
|
600
|
+
process.stderr.write(`${RED}${result.error}${RESET}\n`);
|
|
601
|
+
return true;
|
|
602
|
+
}
|
|
603
|
+
const items = result.data ?? [];
|
|
604
|
+
if (items.length === 0) {
|
|
605
|
+
process.stderr.write(`${DIM}No findings for ${project}.${RESET}\n`);
|
|
606
|
+
return true;
|
|
607
|
+
}
|
|
608
|
+
const recent = items.slice(-15);
|
|
609
|
+
const lines = recent.map((f) => ` ${DIM}${f.date}${RESET} ${f.text.slice(0, 120)}${f.text.length > 120 ? "..." : ""}`);
|
|
610
|
+
process.stderr.write(`${DIM}── Findings (${items.length} total, showing last ${recent.length}) ──${RESET}\n`);
|
|
611
|
+
process.stderr.write(lines.join("\n") + "\n");
|
|
612
|
+
return true;
|
|
613
|
+
}
|
|
614
|
+
if (sub === "tasks") {
|
|
615
|
+
const project = parts[2] || pCtx.project;
|
|
616
|
+
if (!project) {
|
|
617
|
+
process.stderr.write(`${DIM}Usage: /mem tasks <project>${RESET}\n`);
|
|
618
|
+
return true;
|
|
619
|
+
}
|
|
620
|
+
const result = readTasks(pCtx.phrenPath, project);
|
|
621
|
+
if (!result.ok) {
|
|
622
|
+
process.stderr.write(`${RED}${result.error}${RESET}\n`);
|
|
623
|
+
return true;
|
|
624
|
+
}
|
|
625
|
+
const sections = [];
|
|
626
|
+
for (const [section, items] of Object.entries(result.data.items)) {
|
|
627
|
+
if (section === "Done")
|
|
628
|
+
continue;
|
|
629
|
+
if (items.length === 0)
|
|
630
|
+
continue;
|
|
631
|
+
const lines = items.map((t) => {
|
|
632
|
+
const icon = t.checked ? `${GREEN}✓${RESET}` : `${DIM}○${RESET}`;
|
|
633
|
+
return ` ${icon} ${t.line}`;
|
|
634
|
+
});
|
|
635
|
+
sections.push(`${BOLD}${section}${RESET}\n${lines.join("\n")}`);
|
|
636
|
+
}
|
|
637
|
+
if (sections.length === 0) {
|
|
638
|
+
process.stderr.write(`${DIM}No active tasks for ${project}.${RESET}\n`);
|
|
639
|
+
}
|
|
640
|
+
else {
|
|
641
|
+
process.stderr.write(sections.join("\n") + "\n");
|
|
642
|
+
}
|
|
643
|
+
return true;
|
|
644
|
+
}
|
|
645
|
+
if (sub === "add") {
|
|
646
|
+
const finding = parts.slice(2).join(" ").trim();
|
|
647
|
+
if (!finding) {
|
|
648
|
+
process.stderr.write(`${DIM}Usage: /mem add <finding text>${RESET}\n`);
|
|
649
|
+
return true;
|
|
650
|
+
}
|
|
651
|
+
const project = pCtx.project;
|
|
652
|
+
if (!project) {
|
|
653
|
+
process.stderr.write(`${DIM}No project context. Cannot add finding without a project.${RESET}\n`);
|
|
654
|
+
return true;
|
|
655
|
+
}
|
|
656
|
+
const result = addFinding(pCtx.phrenPath, project, finding);
|
|
657
|
+
if (result.ok) {
|
|
658
|
+
process.stderr.write(`${GREEN}→ Finding saved to ${project}.${RESET}\n`);
|
|
659
|
+
}
|
|
660
|
+
else {
|
|
661
|
+
process.stderr.write(`${RED}${result.message ?? "Failed to save finding."}${RESET}\n`);
|
|
662
|
+
}
|
|
663
|
+
return true;
|
|
664
|
+
}
|
|
665
|
+
process.stderr.write(`${DIM}Unknown /mem subcommand: ${sub}. Try /mem help${RESET}\n`);
|
|
666
|
+
return true;
|
|
667
|
+
}
|
|
668
|
+
case "/ask": {
|
|
669
|
+
const question = parts.slice(1).join(" ").trim();
|
|
670
|
+
if (!question) {
|
|
671
|
+
process.stderr.write(`${DIM}Usage: /ask <question>${RESET}\n`);
|
|
672
|
+
return true;
|
|
673
|
+
}
|
|
674
|
+
if (!ctx.provider) {
|
|
675
|
+
process.stderr.write(`${DIM}Provider not available for /ask.${RESET}\n`);
|
|
676
|
+
return true;
|
|
677
|
+
}
|
|
678
|
+
const provider = ctx.provider;
|
|
679
|
+
const sysPrompt = ctx.systemPrompt ?? "You are a helpful assistant.";
|
|
680
|
+
return (async () => {
|
|
681
|
+
process.stderr.write(`${DIM}◆ quick answer (no tools):${RESET}\n`);
|
|
682
|
+
try {
|
|
683
|
+
const response = await provider.chat(sysPrompt, [{ role: "user", content: question }], []);
|
|
684
|
+
for (const block of response.content) {
|
|
685
|
+
if (block.type === "text") {
|
|
686
|
+
process.stderr.write(renderMarkdown(block.text) + "\n");
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
catch (err) {
|
|
691
|
+
process.stderr.write(`${RED}${err instanceof Error ? err.message : String(err)}${RESET}\n`);
|
|
692
|
+
}
|
|
693
|
+
return true;
|
|
694
|
+
})();
|
|
695
|
+
}
|
|
349
696
|
case "/exit":
|
|
350
697
|
case "/quit":
|
|
351
698
|
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
|
@@ -8,6 +8,9 @@ import { editFileTool } from "./tools/edit-file.js";
|
|
|
8
8
|
import { shellTool } from "./tools/shell.js";
|
|
9
9
|
import { globTool } from "./tools/glob.js";
|
|
10
10
|
import { grepTool } from "./tools/grep.js";
|
|
11
|
+
import { createWebFetchTool } from "./tools/web-fetch.js";
|
|
12
|
+
import { createWebSearchTool } from "./tools/web-search.js";
|
|
13
|
+
import { createPhrenAddTaskTool } from "./tools/phren-add-task.js";
|
|
11
14
|
import { createPhrenSearchTool } from "./tools/phren-search.js";
|
|
12
15
|
import { createPhrenFindingTool } from "./tools/phren-finding.js";
|
|
13
16
|
import { createPhrenGetTasksTool, createPhrenCompleteTaskTool } from "./tools/phren-tasks.js";
|
|
@@ -84,7 +87,10 @@ export async function runAgentCli(raw) {
|
|
|
84
87
|
contextSnippet += `\n\n## Agent context (${phrenCtx.project})\n\n${projectCtx}`;
|
|
85
88
|
}
|
|
86
89
|
}
|
|
87
|
-
const systemPrompt = buildSystemPrompt(contextSnippet, priorSummary
|
|
90
|
+
const systemPrompt = buildSystemPrompt(contextSnippet, priorSummary, {
|
|
91
|
+
name: provider.name,
|
|
92
|
+
model: provider.model,
|
|
93
|
+
});
|
|
88
94
|
// Dry run: print system prompt and exit
|
|
89
95
|
if (args.dryRun) {
|
|
90
96
|
console.log("=== System Prompt ===");
|
|
@@ -111,8 +117,11 @@ export async function runAgentCli(raw) {
|
|
|
111
117
|
registry.register(createPhrenFindingTool(phrenCtx, sessionId));
|
|
112
118
|
registry.register(createPhrenGetTasksTool(phrenCtx));
|
|
113
119
|
registry.register(createPhrenCompleteTaskTool(phrenCtx, sessionId));
|
|
120
|
+
registry.register(createPhrenAddTaskTool(phrenCtx, sessionId));
|
|
114
121
|
}
|
|
115
|
-
//
|
|
122
|
+
// Web tools
|
|
123
|
+
registry.register(createWebFetchTool());
|
|
124
|
+
registry.register(createWebSearchTool());
|
|
116
125
|
registry.register(gitStatusTool);
|
|
117
126
|
registry.register(gitDiffTool);
|
|
118
127
|
registry.register(gitCommitTool);
|
|
@@ -156,6 +165,7 @@ export async function runAgentCli(raw) {
|
|
|
156
165
|
costTracker,
|
|
157
166
|
plan: args.plan,
|
|
158
167
|
lintTestConfig,
|
|
168
|
+
sessionId,
|
|
159
169
|
};
|
|
160
170
|
// Multi-agent TUI mode
|
|
161
171
|
if (args.multi || args.team) {
|
|
@@ -85,7 +85,6 @@ export function showModelPicker(provider, currentModel, w) {
|
|
|
85
85
|
const m = models[i];
|
|
86
86
|
const selected = i === cursor;
|
|
87
87
|
const arrow = selected ? s.cyan("▸") : " ";
|
|
88
|
-
const label = selected ? s.bold(m.label) : s.dim(m.label);
|
|
89
88
|
const padded = m.label + " ".repeat(maxLabel - m.label.length);
|
|
90
89
|
const labelStr = selected ? s.bold(padded) : s.dim(padded);
|
|
91
90
|
const meter = renderReasoningMeter(reasoningState[i], m.reasoningRange);
|
|
@@ -97,7 +96,6 @@ export function showModelPicker(provider, currentModel, w) {
|
|
|
97
96
|
// Initial draw
|
|
98
97
|
drawPicker();
|
|
99
98
|
return new Promise((resolve) => {
|
|
100
|
-
const wasRaw = process.stdin.isRaw;
|
|
101
99
|
function onKey(_ch, key) {
|
|
102
100
|
if (!key)
|
|
103
101
|
return;
|
|
@@ -92,34 +92,11 @@ export function removeCustomModel(id) {
|
|
|
92
92
|
export function getCustomModels() {
|
|
93
93
|
return loadConfig().customModels;
|
|
94
94
|
}
|
|
95
|
-
/** Get all models for a provider (built-in + custom). */
|
|
96
|
-
export async function getAllModelsForProvider(provider, currentModel) {
|
|
97
|
-
// Import dynamically to avoid circular dep
|
|
98
|
-
const { getAvailableModels } = await import("./model-picker.js");
|
|
99
|
-
const builtIn = getAvailableModels(provider, currentModel);
|
|
100
|
-
// Add custom models for this provider
|
|
101
|
-
const custom = getCustomModels().filter((m) => m.provider === provider);
|
|
102
|
-
for (const c of custom) {
|
|
103
|
-
if (!builtIn.some((b) => b.id === c.id)) {
|
|
104
|
-
builtIn.push({
|
|
105
|
-
id: c.id,
|
|
106
|
-
provider: provider,
|
|
107
|
-
label: c.label + " ★",
|
|
108
|
-
reasoning: c.reasoning,
|
|
109
|
-
reasoningRange: c.reasoningRange,
|
|
110
|
-
contextWindow: c.contextWindow,
|
|
111
|
-
});
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
return builtIn;
|
|
115
|
-
}
|
|
116
95
|
// ── Format helpers for CLI display ──────────────────────────────────────────
|
|
117
96
|
const DIM = "\x1b[2m";
|
|
118
97
|
const BOLD = "\x1b[1m";
|
|
119
98
|
const GREEN = "\x1b[32m";
|
|
120
99
|
const RED = "\x1b[31m";
|
|
121
|
-
const CYAN = "\x1b[36m";
|
|
122
|
-
const YELLOW = "\x1b[33m";
|
|
123
100
|
const RESET = "\x1b[0m";
|
|
124
101
|
export function formatProviderList() {
|
|
125
102
|
const statuses = getProviderStatuses();
|
package/dist/multi/spawner.js
CHANGED
|
@@ -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(
|
|
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(
|
|
211
|
+
child.kill();
|
|
211
212
|
this.processes.delete(id);
|
|
212
213
|
}
|
|
213
214
|
resolve();
|