@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.
Files changed (61) hide show
  1. package/dist/agent-loop.js +328 -0
  2. package/dist/bin.js +3 -0
  3. package/dist/checkpoint.js +103 -0
  4. package/dist/commands.js +292 -0
  5. package/dist/config.js +139 -0
  6. package/dist/context/pruner.js +62 -0
  7. package/dist/context/token-counter.js +28 -0
  8. package/dist/cost.js +71 -0
  9. package/dist/index.js +284 -0
  10. package/dist/mcp-client.js +168 -0
  11. package/dist/memory/anti-patterns.js +69 -0
  12. package/dist/memory/auto-capture.js +72 -0
  13. package/dist/memory/context-flush.js +24 -0
  14. package/dist/memory/context.js +170 -0
  15. package/dist/memory/error-recovery.js +58 -0
  16. package/dist/memory/project-context.js +77 -0
  17. package/dist/memory/session.js +100 -0
  18. package/dist/multi/agent-colors.js +41 -0
  19. package/dist/multi/child-entry.js +173 -0
  20. package/dist/multi/coordinator.js +263 -0
  21. package/dist/multi/diff-renderer.js +175 -0
  22. package/dist/multi/markdown.js +96 -0
  23. package/dist/multi/presets.js +107 -0
  24. package/dist/multi/progress.js +32 -0
  25. package/dist/multi/spawner.js +219 -0
  26. package/dist/multi/tui-multi.js +626 -0
  27. package/dist/multi/types.js +7 -0
  28. package/dist/permissions/allowlist.js +61 -0
  29. package/dist/permissions/checker.js +111 -0
  30. package/dist/permissions/prompt.js +190 -0
  31. package/dist/permissions/sandbox.js +95 -0
  32. package/dist/permissions/shell-safety.js +74 -0
  33. package/dist/permissions/types.js +2 -0
  34. package/dist/plan.js +38 -0
  35. package/dist/providers/anthropic.js +170 -0
  36. package/dist/providers/codex-auth.js +197 -0
  37. package/dist/providers/codex.js +265 -0
  38. package/dist/providers/ollama.js +142 -0
  39. package/dist/providers/openai-compat.js +163 -0
  40. package/dist/providers/openrouter.js +116 -0
  41. package/dist/providers/resolve.js +39 -0
  42. package/dist/providers/retry.js +55 -0
  43. package/dist/providers/types.js +2 -0
  44. package/dist/repl.js +180 -0
  45. package/dist/spinner.js +46 -0
  46. package/dist/system-prompt.js +31 -0
  47. package/dist/tools/edit-file.js +31 -0
  48. package/dist/tools/git.js +98 -0
  49. package/dist/tools/glob.js +65 -0
  50. package/dist/tools/grep.js +108 -0
  51. package/dist/tools/lint-test.js +76 -0
  52. package/dist/tools/phren-finding.js +35 -0
  53. package/dist/tools/phren-search.js +44 -0
  54. package/dist/tools/phren-tasks.js +71 -0
  55. package/dist/tools/read-file.js +44 -0
  56. package/dist/tools/registry.js +46 -0
  57. package/dist/tools/shell.js +48 -0
  58. package/dist/tools/types.js +2 -0
  59. package/dist/tools/write-file.js +27 -0
  60. package/dist/tui.js +451 -0
  61. package/package.json +39 -0
package/dist/index.js ADDED
@@ -0,0 +1,284 @@
1
+ #!/usr/bin/env node
2
+ import { parseArgs, printHelp } from "./config.js";
3
+ import { resolveProvider } from "./providers/resolve.js";
4
+ import { ToolRegistry } from "./tools/registry.js";
5
+ import { readFileTool } from "./tools/read-file.js";
6
+ import { writeFileTool } from "./tools/write-file.js";
7
+ import { editFileTool } from "./tools/edit-file.js";
8
+ import { shellTool } from "./tools/shell.js";
9
+ import { globTool } from "./tools/glob.js";
10
+ import { grepTool } from "./tools/grep.js";
11
+ import { createPhrenSearchTool } from "./tools/phren-search.js";
12
+ import { createPhrenFindingTool } from "./tools/phren-finding.js";
13
+ import { createPhrenGetTasksTool, createPhrenCompleteTaskTool } from "./tools/phren-tasks.js";
14
+ import { gitStatusTool, gitDiffTool, gitCommitTool } from "./tools/git.js";
15
+ import { buildPhrenContext, buildContextSnippet } from "./memory/context.js";
16
+ import { startSession, endSession, getPriorSummary, saveSessionMessages, loadLastSessionMessages } from "./memory/session.js";
17
+ import { loadProjectContext, evolveProjectContext } from "./memory/project-context.js";
18
+ import { buildSystemPrompt } from "./system-prompt.js";
19
+ import { runAgent, createSession, runTurn } from "./agent-loop.js";
20
+ import { createCostTracker } from "./cost.js";
21
+ import { codexLogin, codexLogout } from "./providers/codex-auth.js";
22
+ import { createCheckpoint } from "./checkpoint.js";
23
+ import { detectLintCommand, detectTestCommand } from "./tools/lint-test.js";
24
+ import { connectMcpServers, loadMcpConfig, parseMcpInline } from "./mcp-client.js";
25
+ const VERSION = "0.0.1";
26
+ /**
27
+ * Run the agent CLI with the given argv tokens.
28
+ * Called from `phren agent ...` or directly via `phren-agent ...`.
29
+ */
30
+ export async function runAgentCli(raw) {
31
+ // Handle auth subcommands before normal arg parsing
32
+ if (raw[0] === "auth") {
33
+ if (raw[1] === "login") {
34
+ await codexLogin();
35
+ process.exit(0);
36
+ }
37
+ if (raw[1] === "logout") {
38
+ codexLogout();
39
+ process.exit(0);
40
+ }
41
+ console.error("Usage: phren-agent auth login|logout");
42
+ process.exit(1);
43
+ }
44
+ const args = parseArgs(raw);
45
+ if (args.help) {
46
+ printHelp();
47
+ process.exit(0);
48
+ }
49
+ if (args.version) {
50
+ console.log(`phren-agent v${VERSION}`);
51
+ process.exit(0);
52
+ }
53
+ if (!args.task && !args.interactive && !args.multi && !args.team) {
54
+ console.error("Usage: phren-agent <task>\nRun phren-agent --help for more info.");
55
+ process.exit(1);
56
+ }
57
+ // Resolve LLM provider
58
+ let provider;
59
+ try {
60
+ provider = resolveProvider(args.provider, args.model);
61
+ }
62
+ catch (err) {
63
+ console.error(err instanceof Error ? err.message : String(err));
64
+ process.exit(1);
65
+ }
66
+ if (args.verbose) {
67
+ process.stderr.write(`Provider: ${provider.name}\n`);
68
+ }
69
+ // Build phren context
70
+ const phrenCtx = await buildPhrenContext(args.project);
71
+ let contextSnippet = "";
72
+ let priorSummary = null;
73
+ let sessionId = null;
74
+ if (phrenCtx) {
75
+ if (args.verbose) {
76
+ process.stderr.write(`Phren: ${phrenCtx.phrenPath} (project: ${phrenCtx.project ?? "none"})\n`);
77
+ }
78
+ contextSnippet = await buildContextSnippet(phrenCtx, args.task);
79
+ priorSummary = getPriorSummary(phrenCtx);
80
+ sessionId = startSession(phrenCtx);
81
+ // Load evolved project context for warm start
82
+ const projectCtx = loadProjectContext(phrenCtx);
83
+ if (projectCtx) {
84
+ contextSnippet += `\n\n## Agent context (${phrenCtx.project})\n\n${projectCtx}`;
85
+ }
86
+ }
87
+ const systemPrompt = buildSystemPrompt(contextSnippet, priorSummary);
88
+ // Dry run: print system prompt and exit
89
+ if (args.dryRun) {
90
+ console.log("=== System Prompt ===");
91
+ console.log(systemPrompt);
92
+ console.log("\n=== Task ===");
93
+ console.log(args.task);
94
+ process.exit(0);
95
+ }
96
+ // Register tools
97
+ const registry = new ToolRegistry();
98
+ registry.setPermissions({
99
+ mode: args.permissions,
100
+ allowedPaths: [],
101
+ projectRoot: process.cwd(),
102
+ });
103
+ registry.register(readFileTool);
104
+ registry.register(writeFileTool);
105
+ registry.register(editFileTool);
106
+ registry.register(shellTool);
107
+ registry.register(globTool);
108
+ registry.register(grepTool);
109
+ if (phrenCtx) {
110
+ registry.register(createPhrenSearchTool(phrenCtx));
111
+ registry.register(createPhrenFindingTool(phrenCtx, sessionId));
112
+ registry.register(createPhrenGetTasksTool(phrenCtx));
113
+ registry.register(createPhrenCompleteTaskTool(phrenCtx, sessionId));
114
+ }
115
+ // Git tools
116
+ registry.register(gitStatusTool);
117
+ registry.register(gitDiffTool);
118
+ registry.register(gitCommitTool);
119
+ // MCP server connections
120
+ let mcpCleanup;
121
+ const mcpServers = {};
122
+ if (args.mcpConfig) {
123
+ Object.assign(mcpServers, loadMcpConfig(args.mcpConfig));
124
+ }
125
+ for (let idx = 0; idx < args.mcp.length; idx++) {
126
+ const entry = parseMcpInline(args.mcp[idx]);
127
+ mcpServers[`mcp-${idx}`] = entry;
128
+ }
129
+ if (Object.keys(mcpServers).length > 0) {
130
+ const { tools: mcpTools, cleanup } = await connectMcpServers(mcpServers, args.verbose);
131
+ mcpCleanup = cleanup;
132
+ for (const tool of mcpTools)
133
+ registry.register(tool);
134
+ }
135
+ // Build cost tracker from model info
136
+ const modelName = args.model ?? provider.name;
137
+ const costTracker = createCostTracker(modelName, args.budget);
138
+ // Build lint/test config from CLI flags or auto-detect
139
+ const cwd = process.cwd();
140
+ const lintCmd = args.lintCmd ?? detectLintCommand(cwd);
141
+ const testCmd = args.testCmd ?? detectTestCommand(cwd);
142
+ const lintTestConfig = (lintCmd || testCmd) ? { lintCmd: lintCmd ?? undefined, testCmd: testCmd ?? undefined } : undefined;
143
+ if (args.verbose && lintTestConfig) {
144
+ if (lintTestConfig.lintCmd)
145
+ process.stderr.write(`Lint: ${lintTestConfig.lintCmd}\n`);
146
+ if (lintTestConfig.testCmd)
147
+ process.stderr.write(`Test: ${lintTestConfig.testCmd}\n`);
148
+ }
149
+ const agentConfig = {
150
+ provider,
151
+ registry,
152
+ systemPrompt,
153
+ maxTurns: args.maxTurns,
154
+ verbose: args.verbose,
155
+ phrenCtx,
156
+ costTracker,
157
+ plan: args.plan,
158
+ lintTestConfig,
159
+ };
160
+ // Multi-agent TUI mode
161
+ if (args.multi || args.team) {
162
+ const { AgentSpawner } = await import("./multi/spawner.js");
163
+ const { startMultiTui } = await import("./multi/tui-multi.js");
164
+ const spawner = new AgentSpawner();
165
+ process.on("SIGINT", async () => {
166
+ await spawner.shutdown();
167
+ process.exit(130);
168
+ });
169
+ await startMultiTui(spawner, agentConfig);
170
+ await spawner.shutdown();
171
+ if (phrenCtx && sessionId) {
172
+ endSession(phrenCtx, sessionId, "Multi-agent session ended");
173
+ }
174
+ mcpCleanup?.();
175
+ return;
176
+ }
177
+ // Interactive mode — TUI if terminal, fallback to REPL if not
178
+ if (args.interactive) {
179
+ const isTTY = process.stdout.isTTY && process.stdin.isTTY;
180
+ const session = isTTY
181
+ ? await (await import("./tui.js")).startTui(agentConfig)
182
+ : await (await import("./repl.js")).startRepl(agentConfig);
183
+ // Flush anti-patterns at session end
184
+ if (phrenCtx) {
185
+ try {
186
+ await session.antiPatterns.flushAntiPatterns(phrenCtx);
187
+ }
188
+ catch { /* best effort */ }
189
+ try {
190
+ await evolveProjectContext(phrenCtx, provider, session.messages);
191
+ }
192
+ catch { /* best effort */ }
193
+ }
194
+ if (phrenCtx && sessionId) {
195
+ const lastText = session.messages.length > 0 ? "Interactive session ended" : "Empty session";
196
+ endSession(phrenCtx, sessionId, lastText);
197
+ }
198
+ mcpCleanup?.();
199
+ return;
200
+ }
201
+ // Create initial checkpoint before agent starts
202
+ const initCheckpoint = createCheckpoint(cwd, "pre-agent");
203
+ if (args.verbose && initCheckpoint) {
204
+ process.stderr.write(`Checkpoint: ${initCheckpoint.slice(0, 8)}\n`);
205
+ }
206
+ // SIGINT handler: offer rollback
207
+ process.on("SIGINT", () => {
208
+ process.stderr.write("\nInterrupted. Use --resume to continue later.\n");
209
+ if (process.stdin.isTTY) {
210
+ try {
211
+ process.stdin.setRawMode(false);
212
+ }
213
+ catch { }
214
+ }
215
+ mcpCleanup?.();
216
+ if (phrenCtx && sessionId) {
217
+ endSession(phrenCtx, sessionId, "Interrupted by user");
218
+ }
219
+ process.exit(130);
220
+ });
221
+ // One-shot mode
222
+ try {
223
+ let result;
224
+ if (args.resume && phrenCtx) {
225
+ // Resume: load previous messages and continue
226
+ const prevMessages = loadLastSessionMessages(phrenCtx.phrenPath);
227
+ if (prevMessages && prevMessages.length > 0) {
228
+ if (args.verbose)
229
+ process.stderr.write(`Resuming session with ${prevMessages.length} messages\n`);
230
+ const contextLimit = provider.contextWindow ?? 200_000;
231
+ const session = createSession(contextLimit);
232
+ session.messages = prevMessages;
233
+ const turnResult = await runTurn("Continuing where we left off. Please review the conversation and continue with the task.", session, agentConfig);
234
+ // Save messages for future resume
235
+ saveSessionMessages(phrenCtx.phrenPath, sessionId, session.messages);
236
+ result = {
237
+ finalText: turnResult.text,
238
+ turns: turnResult.turns,
239
+ toolCalls: turnResult.toolCalls,
240
+ totalCost: agentConfig.costTracker?.formatCost(),
241
+ messages: session.messages,
242
+ };
243
+ }
244
+ else {
245
+ process.stderr.write("No previous session to resume.\n");
246
+ result = await runAgent(args.task, agentConfig);
247
+ }
248
+ }
249
+ else {
250
+ result = await runAgent(args.task, agentConfig);
251
+ }
252
+ if (args.verbose) {
253
+ const costStr = result.totalCost ? `, ${result.totalCost}` : "";
254
+ process.stderr.write(`\nDone: ${result.turns} turns, ${result.toolCalls} tool calls${costStr}\n`);
255
+ }
256
+ // End session with summary + memory intelligence
257
+ if (phrenCtx && sessionId) {
258
+ const summary = result.finalText.slice(0, 500);
259
+ endSession(phrenCtx, sessionId, summary);
260
+ // Save messages for resume
261
+ saveSessionMessages(phrenCtx.phrenPath, sessionId, result.messages);
262
+ // Evolve project context via lightweight LLM reflection
263
+ try {
264
+ await evolveProjectContext(phrenCtx, provider, result.messages);
265
+ }
266
+ catch { /* best effort */ }
267
+ }
268
+ }
269
+ catch (err) {
270
+ console.error(err instanceof Error ? err.message : String(err));
271
+ if (phrenCtx && sessionId) {
272
+ endSession(phrenCtx, sessionId, `Error: ${err instanceof Error ? err.message : String(err)}`);
273
+ }
274
+ mcpCleanup?.();
275
+ process.exit(1);
276
+ }
277
+ mcpCleanup?.();
278
+ }
279
+ // When run directly (phren-agent binary), parse from process.argv
280
+ const isDirectRun = process.argv[1]?.endsWith("/agent/index.js") ||
281
+ process.argv[1]?.endsWith("/agent/index.ts");
282
+ if (isDirectRun) {
283
+ runAgentCli(process.argv.slice(2));
284
+ }
@@ -0,0 +1,168 @@
1
+ /**
2
+ * MCP client — connects to MCP servers, discovers tools, wraps them as AgentTools.
3
+ * Uses stdio transport (spawns server as child process).
4
+ */
5
+ import { spawn } from "child_process";
6
+ import * as fs from "fs";
7
+ import * as readline from "readline";
8
+ /** Active MCP server connection. */
9
+ class McpConnection {
10
+ proc;
11
+ nextId = 1;
12
+ pending = new Map();
13
+ rl;
14
+ name;
15
+ constructor(name, config) {
16
+ this.name = name;
17
+ const env = { ...process.env, ...config.env };
18
+ this.proc = spawn(config.command, config.args ?? [], {
19
+ stdio: ["pipe", "pipe", "pipe"],
20
+ env,
21
+ });
22
+ this.rl = readline.createInterface({ input: this.proc.stdout });
23
+ this.rl.on("line", (line) => {
24
+ try {
25
+ const msg = JSON.parse(line);
26
+ if (msg.id !== undefined && this.pending.has(msg.id)) {
27
+ const { resolve, reject } = this.pending.get(msg.id);
28
+ this.pending.delete(msg.id);
29
+ if (msg.error)
30
+ reject(new Error(`MCP error: ${msg.error.message}`));
31
+ else
32
+ resolve(msg.result);
33
+ }
34
+ }
35
+ catch { /* ignore non-JSON lines */ }
36
+ });
37
+ this.proc.on("error", (err) => {
38
+ for (const { reject } of this.pending.values())
39
+ reject(err);
40
+ this.pending.clear();
41
+ });
42
+ }
43
+ send(method, params) {
44
+ return new Promise((resolve, reject) => {
45
+ const id = this.nextId++;
46
+ const msg = { jsonrpc: "2.0", id, method, params };
47
+ this.pending.set(id, { resolve, reject });
48
+ this.proc.stdin.write(JSON.stringify(msg) + "\n");
49
+ // Timeout after 30s
50
+ setTimeout(() => {
51
+ if (this.pending.has(id)) {
52
+ this.pending.delete(id);
53
+ reject(new Error(`MCP call ${method} timed out (30s)`));
54
+ }
55
+ }, 30_000);
56
+ });
57
+ }
58
+ async initialize() {
59
+ await this.send("initialize", {
60
+ protocolVersion: "2024-11-05",
61
+ capabilities: {},
62
+ clientInfo: { name: "phren-agent", version: "0.0.1" },
63
+ });
64
+ // Send initialized notification (no id)
65
+ this.proc.stdin.write(JSON.stringify({ jsonrpc: "2.0", method: "notifications/initialized" }) + "\n");
66
+ }
67
+ async listTools() {
68
+ const result = await this.send("tools/list");
69
+ return result?.tools ?? [];
70
+ }
71
+ async callTool(name, args) {
72
+ return await this.send("tools/call", { name, arguments: args });
73
+ }
74
+ close() {
75
+ try {
76
+ this.proc.stdin.end();
77
+ }
78
+ catch { /* ignore */ }
79
+ try {
80
+ this.proc.kill();
81
+ }
82
+ catch { /* ignore */ }
83
+ this.rl.close();
84
+ for (const { reject } of this.pending.values())
85
+ reject(new Error("Connection closed"));
86
+ this.pending.clear();
87
+ }
88
+ }
89
+ /** Wrap an MCP tool as an AgentTool. */
90
+ function wrapMcpTool(conn, def) {
91
+ return {
92
+ name: `mcp_${conn.name}_${def.name}`,
93
+ description: `[${conn.name}] ${def.description ?? def.name}`,
94
+ input_schema: def.inputSchema ?? { type: "object", properties: {} },
95
+ async execute(input) {
96
+ try {
97
+ const result = await conn.callTool(def.name, input);
98
+ const text = result.content
99
+ ?.map((c) => c.text ?? JSON.stringify(c))
100
+ .join("\n") ?? "OK";
101
+ return { output: text };
102
+ }
103
+ catch (err) {
104
+ const msg = err instanceof Error ? err.message : String(err);
105
+ return { output: `MCP error: ${msg}`, is_error: true };
106
+ }
107
+ },
108
+ };
109
+ }
110
+ /** Connect to MCP servers and return their tools as AgentTools. */
111
+ export async function connectMcpServers(servers, verbose = false) {
112
+ const connections = [];
113
+ const tools = [];
114
+ for (const [name, config] of Object.entries(servers)) {
115
+ try {
116
+ if (verbose)
117
+ process.stderr.write(`Connecting to MCP server: ${name}...\n`);
118
+ const conn = new McpConnection(name, config);
119
+ await conn.initialize();
120
+ const mcpTools = await conn.listTools();
121
+ for (const def of mcpTools) {
122
+ tools.push(wrapMcpTool(conn, def));
123
+ }
124
+ connections.push(conn);
125
+ if (verbose)
126
+ process.stderr.write(` ${name}: ${mcpTools.length} tools\n`);
127
+ }
128
+ catch (err) {
129
+ const msg = err instanceof Error ? err.message : String(err);
130
+ process.stderr.write(`Failed to connect to MCP server "${name}": ${msg}\n`);
131
+ }
132
+ }
133
+ return {
134
+ tools,
135
+ cleanup: () => { for (const conn of connections)
136
+ conn.close(); },
137
+ };
138
+ }
139
+ /** Load MCP server config from a JSON file (same format as Claude Code's mcpServers). */
140
+ export function loadMcpConfig(configPath) {
141
+ if (!fs.existsSync(configPath))
142
+ return {};
143
+ try {
144
+ const raw = JSON.parse(fs.readFileSync(configPath, "utf-8"));
145
+ // Support both { mcpServers: {...} } and direct { serverName: {...} } formats
146
+ const servers = raw.mcpServers ?? raw;
147
+ const result = {};
148
+ for (const [name, entry] of Object.entries(servers)) {
149
+ const e = entry;
150
+ if (e.command && typeof e.command === "string") {
151
+ result[name] = {
152
+ command: e.command,
153
+ args: Array.isArray(e.args) ? e.args : undefined,
154
+ env: typeof e.env === "object" && e.env !== null ? e.env : undefined,
155
+ };
156
+ }
157
+ }
158
+ return result;
159
+ }
160
+ catch {
161
+ return {};
162
+ }
163
+ }
164
+ /** Parse --mcp "command args..." into an McpConfigEntry. */
165
+ export function parseMcpInline(spec) {
166
+ const parts = spec.split(/\s+/);
167
+ return { command: parts[0], args: parts.slice(1) };
168
+ }
@@ -0,0 +1,69 @@
1
+ import { addFinding } from "@phren/cli/core/finding";
2
+ export class AntiPatternTracker {
3
+ attempts = [];
4
+ /** Record a tool execution result. */
5
+ recordAttempt(name, input, succeeded, output) {
6
+ this.attempts.push({
7
+ name,
8
+ input: JSON.stringify(input).slice(0, 300),
9
+ succeeded,
10
+ output: output.slice(0, 300),
11
+ timestamp: Date.now(),
12
+ });
13
+ }
14
+ /**
15
+ * Extract anti-patterns: find cases where a tool failed then later
16
+ * succeeded with different input (same tool name).
17
+ */
18
+ extractAntiPatterns() {
19
+ const patterns = [];
20
+ const seen = new Set();
21
+ for (let i = 0; i < this.attempts.length; i++) {
22
+ const fail = this.attempts[i];
23
+ if (fail.succeeded)
24
+ continue;
25
+ // Look for a later success with same tool name
26
+ for (let j = i + 1; j < this.attempts.length; j++) {
27
+ const success = this.attempts[j];
28
+ if (success.name !== fail.name || !success.succeeded)
29
+ continue;
30
+ if (success.input === fail.input)
31
+ continue; // Same input, not an anti-pattern
32
+ const key = `${fail.name}:${fail.input}`;
33
+ if (seen.has(key))
34
+ break;
35
+ seen.add(key);
36
+ patterns.push({
37
+ tool: fail.name,
38
+ failedInput: fail.input,
39
+ failedOutput: fail.output,
40
+ succeededInput: success.input,
41
+ });
42
+ break;
43
+ }
44
+ }
45
+ return patterns;
46
+ }
47
+ /**
48
+ * Save top 3 anti-patterns as findings at session end.
49
+ */
50
+ async flushAntiPatterns(ctx) {
51
+ if (!ctx.project)
52
+ return 0;
53
+ const patterns = this.extractAntiPatterns().slice(0, 3);
54
+ if (patterns.length === 0)
55
+ return 0;
56
+ let saved = 0;
57
+ try {
58
+ for (const p of patterns) {
59
+ const finding = `[anti-pattern] ${p.tool}: failed with ${p.failedInput.slice(0, 100)} (${p.failedOutput.slice(0, 80)}), succeeded with ${p.succeededInput.slice(0, 100)}`;
60
+ await addFinding(ctx.phrenPath, ctx.project, finding);
61
+ saved++;
62
+ }
63
+ }
64
+ catch {
65
+ // best effort
66
+ }
67
+ return saved;
68
+ }
69
+ }
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Auto-capture: extract findings from error tool results.
3
+ * Runs after each turn on error results only.
4
+ */
5
+ import * as crypto from "crypto";
6
+ import { addFinding } from "@phren/cli/core/finding";
7
+ const SESSION_CAP = 10;
8
+ const COOLDOWN_MS = 30_000;
9
+ /** Patterns that indicate capturable knowledge in error output. */
10
+ const CAPTURE_PATTERNS = [
11
+ { pattern: /ENOENT.*no such file/i, label: "missing-file" },
12
+ { pattern: /EACCES|permission denied/i, label: "permission-error" },
13
+ { pattern: /Cannot find module/i, label: "missing-module" },
14
+ { pattern: /ERR_MODULE_NOT_FOUND/i, label: "missing-module" },
15
+ { pattern: /ECONNREFUSED|ETIMEDOUT/i, label: "connection-error" },
16
+ { pattern: /port\s+\d+\s+(already|in use)/i, label: "port-conflict" },
17
+ { pattern: /deprecated/i, label: "deprecation" },
18
+ { pattern: /version\s+mismatch|incompatible/i, label: "version-mismatch" },
19
+ { pattern: /out of memory|heap|OOM/i, label: "memory-issue" },
20
+ { pattern: /syntax\s*error/i, label: "syntax-error" },
21
+ { pattern: /type\s*error.*is not a function/i, label: "type-error" },
22
+ { pattern: /config(uration)?\s+(not found|missing|invalid)/i, label: "config-issue" },
23
+ { pattern: /https?:\/\/\S+:\d+/i, label: "service-endpoint" },
24
+ { pattern: /env\s+var(iable)?.*not set|missing.*env/i, label: "env-var" },
25
+ ];
26
+ export function createCaptureState() {
27
+ return { captured: 0, hashes: new Set(), lastCaptureTime: 0 };
28
+ }
29
+ function hashText(text) {
30
+ return crypto.createHash("sha256").update(text).digest("hex").slice(0, 16);
31
+ }
32
+ /**
33
+ * Analyze error output and auto-capture relevant findings.
34
+ * Returns the number of findings captured this call.
35
+ */
36
+ export async function analyzeAndCapture(ctx, errorOutput, state) {
37
+ if (!ctx.project)
38
+ return 0;
39
+ if (state.captured >= SESSION_CAP)
40
+ return 0;
41
+ const now = Date.now();
42
+ if (now - state.lastCaptureTime < COOLDOWN_MS)
43
+ return 0;
44
+ let captured = 0;
45
+ for (const { pattern, label } of CAPTURE_PATTERNS) {
46
+ if (state.captured + captured >= SESSION_CAP)
47
+ break;
48
+ const match = errorOutput.match(pattern);
49
+ if (!match)
50
+ continue;
51
+ // Build a concise finding from the matched line
52
+ const lines = errorOutput.split("\n");
53
+ const matchedLine = lines.find((l) => pattern.test(l))?.trim() ?? match[0];
54
+ const finding = `[auto-capture:${label}] ${matchedLine.slice(0, 200)}`;
55
+ const hash = hashText(finding);
56
+ if (state.hashes.has(hash))
57
+ continue;
58
+ try {
59
+ await addFinding(ctx.phrenPath, ctx.project, finding);
60
+ state.hashes.add(hash);
61
+ captured++;
62
+ }
63
+ catch {
64
+ // best effort
65
+ }
66
+ }
67
+ if (captured > 0) {
68
+ state.captured += captured;
69
+ state.lastCaptureTime = now;
70
+ }
71
+ return captured;
72
+ }
@@ -0,0 +1,24 @@
1
+ import { estimateTokens, estimateMessageTokens } from "../context/token-counter.js";
2
+ export function createFlushConfig(contextLimit) {
3
+ return { contextLimit, triggered: false };
4
+ }
5
+ const FLUSH_PROMPT = "Before continuing, briefly summarize the key decisions, patterns, and discoveries from this conversation so far. " +
6
+ "Focus on non-obvious findings that would be valuable to remember in future sessions. " +
7
+ "Be concise — 3-5 bullet points maximum.";
8
+ /**
9
+ * Check if a context flush should be injected.
10
+ * Returns the flush message to inject, or null if not needed.
11
+ * Only triggers once per session.
12
+ */
13
+ export function checkFlushNeeded(systemPrompt, messages, config) {
14
+ if (config.triggered)
15
+ return null;
16
+ const systemTokens = estimateTokens(systemPrompt);
17
+ const msgTokens = estimateMessageTokens(messages);
18
+ const usage = (systemTokens + msgTokens) / config.contextLimit;
19
+ if (usage >= 0.75) {
20
+ config.triggered = true;
21
+ return FLUSH_PROMPT;
22
+ }
23
+ return null;
24
+ }