@phren/agent 0.1.2 → 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 +9 -2
- package/dist/commands.js +351 -4
- package/dist/config.js +6 -2
- package/dist/index.js +1 -0
- package/dist/multi/spawner.js +3 -2
- 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 +24 -26
- package/dist/tools/shell.js +5 -2
- package/dist/tui.js +288 -31
- package/package.json +2 -2
package/dist/providers/codex.js
CHANGED
|
@@ -194,73 +194,129 @@ export class CodexProvider {
|
|
|
194
194
|
body.tools = toResponsesTools(tools);
|
|
195
195
|
body.tool_choice = "auto";
|
|
196
196
|
}
|
|
197
|
-
|
|
198
|
-
|
|
197
|
+
// Use WebSocket for true token-by-token streaming (matches Codex CLI behavior).
|
|
198
|
+
// The HTTP SSE endpoint batches the entire response before flushing.
|
|
199
|
+
yield* this.chatStreamWs(accessToken, body);
|
|
200
|
+
}
|
|
201
|
+
/** WebSocket streaming — sends request, yields deltas as they arrive. */
|
|
202
|
+
async *chatStreamWs(accessToken, body) {
|
|
203
|
+
const wsUrl = CODEX_API.replace(/^https:/, "wss:").replace(/^http:/, "ws:");
|
|
204
|
+
// Queue for events received from the WebSocket before the consumer pulls them
|
|
205
|
+
const queue = [];
|
|
206
|
+
let resolve = null;
|
|
207
|
+
let done = false;
|
|
208
|
+
const push = (item) => {
|
|
209
|
+
queue.push(item);
|
|
210
|
+
if (resolve) {
|
|
211
|
+
resolve();
|
|
212
|
+
resolve = null;
|
|
213
|
+
}
|
|
214
|
+
};
|
|
215
|
+
// Node.js (undici) WebSocket accepts headers in the second argument object,
|
|
216
|
+
// but the DOM typings only allow string | string[]. Cast to bypass.
|
|
217
|
+
const ws = new WebSocket(wsUrl, {
|
|
199
218
|
headers: {
|
|
200
|
-
"Content-Type": "application/json",
|
|
201
219
|
Authorization: `Bearer ${accessToken}`,
|
|
202
220
|
},
|
|
203
|
-
body: JSON.stringify(body),
|
|
204
221
|
});
|
|
205
|
-
if (!res.ok) {
|
|
206
|
-
const text = await res.text();
|
|
207
|
-
throw new Error(`Codex API error ${res.status}: ${text}`);
|
|
208
|
-
}
|
|
209
|
-
// Parse SSE stream
|
|
210
|
-
if (!res.body)
|
|
211
|
-
throw new Error("Provider returned empty response body");
|
|
212
|
-
const reader = res.body.getReader();
|
|
213
|
-
const decoder = new TextDecoder();
|
|
214
|
-
let buffer = "";
|
|
215
222
|
let activeToolCallId = "";
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
223
|
+
ws.addEventListener("open", () => {
|
|
224
|
+
// Wrap the request body in a response.create envelope (Codex WS protocol)
|
|
225
|
+
const wsRequest = { type: "response.create", ...body };
|
|
226
|
+
ws.send(JSON.stringify(wsRequest));
|
|
227
|
+
});
|
|
228
|
+
ws.addEventListener("message", (evt) => {
|
|
229
|
+
const data = typeof evt.data === "string" ? evt.data : String(evt.data);
|
|
230
|
+
let event;
|
|
231
|
+
try {
|
|
232
|
+
event = JSON.parse(data);
|
|
233
|
+
}
|
|
234
|
+
catch {
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
const type = event.type;
|
|
238
|
+
// Handle server-side errors
|
|
239
|
+
if (type === "error") {
|
|
240
|
+
const err = event.error;
|
|
241
|
+
const msg = err?.message ?? "Codex WebSocket error";
|
|
242
|
+
const status = event.status;
|
|
243
|
+
push(new Error(`Codex WS error${status ? ` ${status}` : ""}: ${msg}`));
|
|
244
|
+
done = true;
|
|
230
245
|
try {
|
|
231
|
-
|
|
232
|
-
}
|
|
233
|
-
catch {
|
|
234
|
-
continue;
|
|
246
|
+
ws.close();
|
|
235
247
|
}
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
248
|
+
catch { /* ignore */ }
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
if (type === "response.output_text.delta") {
|
|
252
|
+
const delta = event.delta;
|
|
253
|
+
if (delta)
|
|
254
|
+
push({ type: "text_delta", text: delta });
|
|
255
|
+
}
|
|
256
|
+
else if (type === "response.output_item.added") {
|
|
257
|
+
if (event.item?.type === "function_call") {
|
|
258
|
+
const item = event.item;
|
|
259
|
+
activeToolCallId = item.call_id;
|
|
260
|
+
push({ type: "tool_use_start", id: activeToolCallId, name: item.name });
|
|
246
261
|
}
|
|
247
|
-
|
|
248
|
-
|
|
262
|
+
}
|
|
263
|
+
else if (type === "response.function_call_arguments.delta") {
|
|
264
|
+
push({ type: "tool_use_delta", id: activeToolCallId, json: event.delta });
|
|
265
|
+
}
|
|
266
|
+
else if (type === "response.function_call_arguments.done") {
|
|
267
|
+
push({ type: "tool_use_end", id: activeToolCallId });
|
|
268
|
+
}
|
|
269
|
+
else if (type === "response.completed") {
|
|
270
|
+
const response = event.response;
|
|
271
|
+
const usage = response?.usage;
|
|
272
|
+
const output = response?.output;
|
|
273
|
+
const hasToolCalls = output?.some((o) => o.type === "function_call");
|
|
274
|
+
push({
|
|
275
|
+
type: "done",
|
|
276
|
+
stop_reason: hasToolCalls ? "tool_use" : "end_turn",
|
|
277
|
+
usage: usage ? { input_tokens: usage.input_tokens ?? 0, output_tokens: usage.output_tokens ?? 0 } : undefined,
|
|
278
|
+
});
|
|
279
|
+
done = true;
|
|
280
|
+
try {
|
|
281
|
+
ws.close();
|
|
249
282
|
}
|
|
250
|
-
|
|
251
|
-
|
|
283
|
+
catch { /* ignore */ }
|
|
284
|
+
}
|
|
285
|
+
});
|
|
286
|
+
ws.addEventListener("error", () => {
|
|
287
|
+
if (!done) {
|
|
288
|
+
push(new Error("Codex WebSocket connection error"));
|
|
289
|
+
done = true;
|
|
290
|
+
}
|
|
291
|
+
});
|
|
292
|
+
ws.addEventListener("close", () => {
|
|
293
|
+
if (!done) {
|
|
294
|
+
push(new Error("Codex WebSocket closed before response.completed"));
|
|
295
|
+
done = true;
|
|
296
|
+
}
|
|
297
|
+
});
|
|
298
|
+
// Async iteration: drain the queue, wait for new events
|
|
299
|
+
try {
|
|
300
|
+
while (true) {
|
|
301
|
+
while (queue.length > 0) {
|
|
302
|
+
const item = queue.shift();
|
|
303
|
+
if (item instanceof Error)
|
|
304
|
+
throw item;
|
|
305
|
+
yield item;
|
|
306
|
+
if (item.type === "done")
|
|
307
|
+
return;
|
|
252
308
|
}
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
};
|
|
309
|
+
if (done)
|
|
310
|
+
return;
|
|
311
|
+
await new Promise((r) => { resolve = r; });
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
finally {
|
|
315
|
+
if (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING) {
|
|
316
|
+
try {
|
|
317
|
+
ws.close();
|
|
263
318
|
}
|
|
319
|
+
catch { /* ignore */ }
|
|
264
320
|
}
|
|
265
321
|
}
|
|
266
322
|
}
|
package/dist/repl.js
CHANGED
|
@@ -82,7 +82,7 @@ export async function startRepl(config) {
|
|
|
82
82
|
rl.prompt();
|
|
83
83
|
continue;
|
|
84
84
|
}
|
|
85
|
-
if (handleCommand(trimmed, { session, contextLimit, undoStack: [] })) {
|
|
85
|
+
if (handleCommand(trimmed, { session, contextLimit, undoStack: [], phrenCtx: config.phrenCtx })) {
|
|
86
86
|
rl.prompt();
|
|
87
87
|
continue;
|
|
88
88
|
}
|
|
@@ -118,7 +118,7 @@ export async function startRepl(config) {
|
|
|
118
118
|
process.stderr.write(`${YELLOW}Input mode: ${inputMode}${RESET}\n`);
|
|
119
119
|
}
|
|
120
120
|
else {
|
|
121
|
-
handleCommand(queued, { session, contextLimit, undoStack: [] });
|
|
121
|
+
handleCommand(queued, { session, contextLimit, undoStack: [], phrenCtx: config.phrenCtx });
|
|
122
122
|
}
|
|
123
123
|
break;
|
|
124
124
|
}
|
package/dist/system-prompt.js
CHANGED
|
@@ -1,37 +1,35 @@
|
|
|
1
1
|
export function buildSystemPrompt(phrenContext, priorSummary, providerInfo) {
|
|
2
2
|
const modelNote = providerInfo ? ` You are running on ${providerInfo.name}${providerInfo.model ? ` (model: ${providerInfo.model})` : ""}.` : "";
|
|
3
3
|
const parts = [
|
|
4
|
-
`You are phren-agent,
|
|
4
|
+
`You are phren-agent, an autonomous coding agent with persistent memory.${modelNote}`,
|
|
5
5
|
"",
|
|
6
|
-
"##
|
|
7
|
-
"
|
|
8
|
-
"
|
|
9
|
-
"
|
|
10
|
-
"4. **Verify** — Run tests and linters via `shell` after edits. Check `git_diff` to review your changes.",
|
|
11
|
-
"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.",
|
|
12
|
-
"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.",
|
|
13
10
|
"",
|
|
14
|
-
"##
|
|
15
|
-
"
|
|
16
|
-
"
|
|
17
|
-
"
|
|
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.",
|
|
18
18
|
"",
|
|
19
|
-
"##
|
|
20
|
-
"
|
|
21
|
-
"- `
|
|
22
|
-
"- `
|
|
23
|
-
"- `
|
|
24
|
-
"- `
|
|
25
|
-
"- `phren hooks enable <tool>` — enable hooks for claude/copilot/cursor/codex",
|
|
26
|
-
"- `phren doctor --fix` — diagnose and self-heal",
|
|
27
|
-
"- `phren status` — check health",
|
|
28
|
-
"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`",
|
|
29
25
|
"",
|
|
30
|
-
"##
|
|
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.",
|
|
31
31
|
"- Never write secrets, API keys, or PII to files or findings.",
|
|
32
|
-
"-
|
|
33
|
-
"- Keep shell commands safe. No `rm -rf`, no `sudo`, no destructive operations.",
|
|
34
|
-
"- 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.",
|
|
35
33
|
];
|
|
36
34
|
if (priorSummary) {
|
|
37
35
|
parts.push("", `## Last session\n${priorSummary}`);
|
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,
|