@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/system-prompt.js
CHANGED
|
@@ -1,36 +1,35 @@
|
|
|
1
|
-
export function buildSystemPrompt(phrenContext, priorSummary) {
|
|
1
|
+
export function buildSystemPrompt(phrenContext, priorSummary, providerInfo) {
|
|
2
|
+
const modelNote = providerInfo ? ` You are running on ${providerInfo.name}${providerInfo.model ? ` (model: ${providerInfo.model})` : ""}.` : "";
|
|
2
3
|
const parts = [
|
|
3
|
-
`You are phren-agent,
|
|
4
|
+
`You are phren-agent, an autonomous coding agent with persistent memory.${modelNote}`,
|
|
4
5
|
"",
|
|
5
|
-
"##
|
|
6
|
-
"
|
|
7
|
-
"
|
|
8
|
-
"
|
|
9
|
-
"4. **Verify** — Run tests and linters via `shell` after edits. Check `git_diff` to review your changes.",
|
|
10
|
-
"5. **Remember** — Save non-obvious discoveries with `phren_add_finding`: tricky bugs, architecture decisions, gotchas, workarounds. Skip obvious things — only save what would help a future session.",
|
|
11
|
-
"6. **Report** — Explain what you did concisely. Mention files changed and why.",
|
|
6
|
+
"## Core Behavior",
|
|
7
|
+
"ACT IMMEDIATELY. When the user asks you to do something, DO IT. Don't describe what you're going to do — just do it. Use your tools without asking permission. Read files, search code, make edits, run commands. Only ask clarifying questions when the request is genuinely ambiguous.",
|
|
8
|
+
"",
|
|
9
|
+
"You have persistent memory via phren. Past decisions, discovered patterns, and project context are searchable across sessions. Use this to avoid repeating mistakes.",
|
|
12
10
|
"",
|
|
13
|
-
"##
|
|
14
|
-
"
|
|
15
|
-
"
|
|
16
|
-
"
|
|
11
|
+
"## Workflow",
|
|
12
|
+
"1. **Search memory first** — `phren_search` for relevant past findings before starting work.",
|
|
13
|
+
"2. **Read before writing** — `glob` to find files, `grep` to locate symbols, `read_file` to understand code.",
|
|
14
|
+
"3. **Make changes** — `edit_file` for surgical edits, `write_file` for new files only.",
|
|
15
|
+
"4. **Verify** — `shell` to run tests/linters, `git_diff` to review changes.",
|
|
16
|
+
"5. **Save learnings** — `phren_add_finding` for non-obvious discoveries (bugs, architecture decisions, gotchas). Skip obvious stuff.",
|
|
17
|
+
"6. **Report concisely** — what changed and why. No fluff.",
|
|
17
18
|
"",
|
|
18
|
-
"##
|
|
19
|
-
"
|
|
20
|
-
"- `
|
|
21
|
-
"- `
|
|
22
|
-
"- `
|
|
23
|
-
"- `
|
|
24
|
-
"- `phren hooks enable <tool>` — enable hooks for claude/copilot/cursor/codex",
|
|
25
|
-
"- `phren doctor --fix` — diagnose and self-heal",
|
|
26
|
-
"- `phren status` — check health",
|
|
27
|
-
"If the user asks you to configure phren, set up a project, or fix their install, use the shell tool to run these commands.",
|
|
19
|
+
"## Tools You Have",
|
|
20
|
+
"- File I/O: `read_file`, `write_file`, `edit_file`",
|
|
21
|
+
"- Search: `glob`, `grep`, `web_search`, `web_fetch`",
|
|
22
|
+
"- System: `shell` (run commands, cd, build, test)",
|
|
23
|
+
"- Git: `git_status`, `git_diff`, `git_commit`",
|
|
24
|
+
"- Memory: `phren_search`, `phren_add_finding`, `phren_get_tasks`, `phren_complete_task`, `phren_add_task`",
|
|
28
25
|
"",
|
|
29
|
-
"##
|
|
26
|
+
"## Important",
|
|
27
|
+
"- Be direct and concise. Lead with the answer, not the reasoning.",
|
|
28
|
+
"- Call multiple tools in parallel when they're independent.",
|
|
29
|
+
"- NEVER ask 'should I read the file?' or 'would you like me to...' — just call the tool. If permission is needed, the system will prompt the user automatically. You don't handle permissions.",
|
|
30
|
+
"- Don't describe your plan unless asked. Execute immediately.",
|
|
30
31
|
"- Never write secrets, API keys, or PII to files or findings.",
|
|
31
|
-
"-
|
|
32
|
-
"- Keep shell commands safe. No `rm -rf`, no `sudo`, no destructive operations.",
|
|
33
|
-
"- If unsure, say so. Don't guess at behavior you can verify by reading code or running tests.",
|
|
32
|
+
"- You ARE phren-agent. You can run `phren` CLI commands via shell to configure yourself.",
|
|
34
33
|
];
|
|
35
34
|
if (priorSummary) {
|
|
36
35
|
parts.push("", `## Last session\n${priorSummary}`);
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { addTasks } from "@phren/cli/data/tasks";
|
|
2
|
+
export function createPhrenAddTaskTool(ctx, sessionId) {
|
|
3
|
+
return {
|
|
4
|
+
name: "phren_add_task",
|
|
5
|
+
description: "Add a new task to the phren task list for a project. " +
|
|
6
|
+
"Use this to track work items discovered during execution that should be addressed later. " +
|
|
7
|
+
"Good tasks: TODOs found in code, follow-up work, bugs to fix, tech debt. " +
|
|
8
|
+
"Bad tasks: obvious next steps, tasks you'll complete in this session.",
|
|
9
|
+
input_schema: {
|
|
10
|
+
type: "object",
|
|
11
|
+
properties: {
|
|
12
|
+
item: {
|
|
13
|
+
type: "string",
|
|
14
|
+
description: "Task description. Be specific — include file paths, function names, or error context.",
|
|
15
|
+
},
|
|
16
|
+
project: {
|
|
17
|
+
type: "string",
|
|
18
|
+
description: "Project name. Omit to use current project.",
|
|
19
|
+
},
|
|
20
|
+
priority: {
|
|
21
|
+
type: "string",
|
|
22
|
+
enum: ["high", "medium", "low"],
|
|
23
|
+
description: "Task priority. Default: medium.",
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
required: ["item"],
|
|
27
|
+
},
|
|
28
|
+
async execute(input) {
|
|
29
|
+
const item = input.item;
|
|
30
|
+
const project = input.project || ctx.project;
|
|
31
|
+
const priority = input.priority || "medium";
|
|
32
|
+
if (!project)
|
|
33
|
+
return { output: "No project context. Specify a project name.", is_error: true };
|
|
34
|
+
try {
|
|
35
|
+
// Format with priority prefix if not medium
|
|
36
|
+
const taskText = priority !== "medium" ? `[${priority}] ${item}` : item;
|
|
37
|
+
const result = addTasks(ctx.phrenPath, project, [taskText]);
|
|
38
|
+
if (result.ok) {
|
|
39
|
+
return { output: `Task added to ${project}: ${item}` };
|
|
40
|
+
}
|
|
41
|
+
return { output: result.error ?? "Failed to add task.", is_error: true };
|
|
42
|
+
}
|
|
43
|
+
catch (err) {
|
|
44
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
45
|
+
return { output: `Failed: ${msg}`, is_error: true };
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
}
|
package/dist/tools/shell.js
CHANGED
|
@@ -5,7 +5,7 @@ const MAX_TIMEOUT_MS = 120_000;
|
|
|
5
5
|
const MAX_OUTPUT_BYTES = 100_000;
|
|
6
6
|
export const shellTool = {
|
|
7
7
|
name: "shell",
|
|
8
|
-
description: "Run a shell command
|
|
8
|
+
description: "Run a shell command and return stdout + stderr. Uses bash on Unix, cmd.exe on Windows. Use for: running tests, linters, build commands, git operations, and exploring the environment. Prefer specific tools (read_file, glob, grep) over shell equivalents when available.",
|
|
9
9
|
input_schema: {
|
|
10
10
|
type: "object",
|
|
11
11
|
properties: {
|
|
@@ -24,8 +24,11 @@ export const shellTool = {
|
|
|
24
24
|
if (!safety.safe && safety.severity === "block") {
|
|
25
25
|
return { output: `Blocked: ${safety.reason}`, is_error: true };
|
|
26
26
|
}
|
|
27
|
+
const isWindows = process.platform === "win32";
|
|
28
|
+
const shell = isWindows ? "cmd" : "bash";
|
|
29
|
+
const shellArgs = isWindows ? ["/c", command] : ["-c", command];
|
|
27
30
|
try {
|
|
28
|
-
const output = execFileSync(
|
|
31
|
+
const output = execFileSync(shell, shellArgs, {
|
|
29
32
|
cwd,
|
|
30
33
|
encoding: "utf-8",
|
|
31
34
|
timeout,
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
export function createWebFetchTool() {
|
|
2
|
+
return {
|
|
3
|
+
name: "web_fetch",
|
|
4
|
+
description: "Fetch a URL and return its text content. Use for reading documentation, API references, or web pages. Returns plain text (HTML tags stripped). Max 50KB response.",
|
|
5
|
+
input_schema: {
|
|
6
|
+
type: "object",
|
|
7
|
+
properties: {
|
|
8
|
+
url: { type: "string", description: "The URL to fetch." },
|
|
9
|
+
max_length: { type: "number", description: "Max response length in characters. Default: 50000." },
|
|
10
|
+
},
|
|
11
|
+
required: ["url"],
|
|
12
|
+
},
|
|
13
|
+
async execute(input) {
|
|
14
|
+
const url = input.url;
|
|
15
|
+
const maxLen = input.max_length || 50_000;
|
|
16
|
+
try {
|
|
17
|
+
const res = await fetch(url, {
|
|
18
|
+
headers: { "User-Agent": "phren-agent/0.1" },
|
|
19
|
+
signal: AbortSignal.timeout(15_000),
|
|
20
|
+
});
|
|
21
|
+
if (!res.ok)
|
|
22
|
+
return { output: `HTTP ${res.status}: ${res.statusText}`, is_error: true };
|
|
23
|
+
let text = await res.text();
|
|
24
|
+
// Strip HTML tags for readability
|
|
25
|
+
text = text.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, "");
|
|
26
|
+
text = text.replace(/<style[^>]*>[\s\S]*?<\/style>/gi, "");
|
|
27
|
+
text = text.replace(/<[^>]+>/g, " ");
|
|
28
|
+
text = text.replace(/\s{2,}/g, " ").trim();
|
|
29
|
+
if (text.length > maxLen) {
|
|
30
|
+
text = text.slice(0, maxLen) + `\n\n[truncated at ${maxLen} chars]`;
|
|
31
|
+
}
|
|
32
|
+
return { output: text };
|
|
33
|
+
}
|
|
34
|
+
catch (err) {
|
|
35
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
36
|
+
return { output: `Fetch failed: ${msg}`, is_error: true };
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
export function createWebSearchTool() {
|
|
2
|
+
return {
|
|
3
|
+
name: "web_search",
|
|
4
|
+
description: "Search the web for documentation, error messages, library APIs, or any technical information. " +
|
|
5
|
+
"Returns a list of search results with titles, URLs, and snippets. " +
|
|
6
|
+
"Use this when you need external information not available in the codebase or phren knowledge base.",
|
|
7
|
+
input_schema: {
|
|
8
|
+
type: "object",
|
|
9
|
+
properties: {
|
|
10
|
+
query: {
|
|
11
|
+
type: "string",
|
|
12
|
+
description: "Search query. Be specific — include error messages, library names, or version numbers.",
|
|
13
|
+
},
|
|
14
|
+
limit: {
|
|
15
|
+
type: "number",
|
|
16
|
+
description: "Max results to return. Default: 5.",
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
required: ["query"],
|
|
20
|
+
},
|
|
21
|
+
async execute(input) {
|
|
22
|
+
const query = input.query;
|
|
23
|
+
const limit = Math.min(input.limit || 5, 10);
|
|
24
|
+
try {
|
|
25
|
+
const results = await searchDuckDuckGo(query, limit);
|
|
26
|
+
if (results.length === 0) {
|
|
27
|
+
return { output: "No search results found." };
|
|
28
|
+
}
|
|
29
|
+
const formatted = results.map((r, i) => `${i + 1}. **${r.title}**\n ${r.url}\n ${r.snippet}`).join("\n\n");
|
|
30
|
+
return { output: formatted };
|
|
31
|
+
}
|
|
32
|
+
catch (err) {
|
|
33
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
34
|
+
return { output: `Search failed: ${msg}`, is_error: true };
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
async function searchDuckDuckGo(query, limit) {
|
|
40
|
+
const encoded = encodeURIComponent(query);
|
|
41
|
+
const url = `https://html.duckduckgo.com/html/?q=${encoded}`;
|
|
42
|
+
const res = await fetch(url, {
|
|
43
|
+
headers: {
|
|
44
|
+
"User-Agent": "phren-agent/0.1 (search tool)",
|
|
45
|
+
"Accept": "text/html",
|
|
46
|
+
},
|
|
47
|
+
signal: AbortSignal.timeout(10_000),
|
|
48
|
+
});
|
|
49
|
+
if (!res.ok) {
|
|
50
|
+
throw new Error(`Search returned HTTP ${res.status}`);
|
|
51
|
+
}
|
|
52
|
+
const html = await res.text();
|
|
53
|
+
return parseSearchResults(html, limit);
|
|
54
|
+
}
|
|
55
|
+
function parseSearchResults(html, limit) {
|
|
56
|
+
const results = [];
|
|
57
|
+
// DuckDuckGo HTML results are in <div class="result"> blocks
|
|
58
|
+
// Extract links and snippets using regex (no DOM parser dependency)
|
|
59
|
+
const resultBlocks = html.match(/<div class="links_main[\s\S]*?<\/div>\s*<\/div>/gi) || [];
|
|
60
|
+
for (const block of resultBlocks) {
|
|
61
|
+
if (results.length >= limit)
|
|
62
|
+
break;
|
|
63
|
+
// Extract URL from the result link
|
|
64
|
+
const urlMatch = block.match(/<a[^>]*class="result__a"[^>]*href="([^"]*)"[^>]*>/i);
|
|
65
|
+
const titleMatch = block.match(/<a[^>]*class="result__a"[^>]*>([\s\S]*?)<\/a>/i);
|
|
66
|
+
const snippetMatch = block.match(/<a[^>]*class="result__snippet"[^>]*>([\s\S]*?)<\/a>/i);
|
|
67
|
+
if (!urlMatch || !titleMatch)
|
|
68
|
+
continue;
|
|
69
|
+
let href = urlMatch[1];
|
|
70
|
+
// DuckDuckGo wraps URLs through their redirect — extract the actual URL
|
|
71
|
+
const uddgMatch = href.match(/uddg=([^&]+)/);
|
|
72
|
+
if (uddgMatch) {
|
|
73
|
+
href = decodeURIComponent(uddgMatch[1]);
|
|
74
|
+
}
|
|
75
|
+
const title = stripHtml(titleMatch[1]).trim();
|
|
76
|
+
const snippet = snippetMatch ? stripHtml(snippetMatch[1]).trim() : "";
|
|
77
|
+
if (title && href) {
|
|
78
|
+
results.push({ title, url: href, snippet });
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return results;
|
|
82
|
+
}
|
|
83
|
+
function stripHtml(html) {
|
|
84
|
+
return html
|
|
85
|
+
.replace(/<[^>]+>/g, "")
|
|
86
|
+
.replace(/&/g, "&")
|
|
87
|
+
.replace(/</g, "<")
|
|
88
|
+
.replace(/>/g, ">")
|
|
89
|
+
.replace(/"/g, '"')
|
|
90
|
+
.replace(/'/g, "'")
|
|
91
|
+
.replace(/ /g, " ")
|
|
92
|
+
.replace(/\s{2,}/g, " ");
|
|
93
|
+
}
|