@devang0907/agent-dev 0.1.2 → 0.1.4
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/README.md +12 -2
- package/dist/agent/loop.d.ts +7 -0
- package/dist/agent/loop.js +146 -30
- package/dist/agent/platform.d.ts +10 -0
- package/dist/agent/platform.js +74 -0
- package/dist/agent/session.d.ts +8 -1
- package/dist/agent/session.js +19 -0
- package/dist/agent/tools/index.d.ts +1 -0
- package/dist/agent/tools/index.js +3 -0
- package/dist/agent/tools/read.js +15 -17
- package/dist/agent/tools/search.d.ts +5 -0
- package/dist/agent/tools/search.js +188 -0
- package/dist/agent/tools/shell.d.ts +1 -0
- package/dist/agent/tools/shell.js +63 -0
- package/dist/modes/print-mode.js +15 -0
- package/dist/providers/groq.js +7 -81
- package/dist/providers/openai-compat.d.ts +10 -0
- package/dist/providers/openai-compat.js +212 -0
- package/dist/providers/openrouter-free.js +7 -81
- package/dist/ui/ApiKeyPrompt.js +2 -3
- package/dist/ui/App.d.ts +1 -0
- package/dist/ui/App.js +115 -36
- package/dist/ui/ChatView.d.ts +2 -1
- package/dist/ui/ChatView.js +14 -4
- package/dist/ui/CommandApprovalPrompt.d.ts +11 -0
- package/dist/ui/CommandApprovalPrompt.js +15 -0
- package/dist/ui/Editor.d.ts +2 -1
- package/dist/ui/Editor.js +7 -3
- package/dist/ui/ModelSelector.js +1 -1
- package/dist/ui/SettingsView.d.ts +3 -1
- package/dist/ui/SettingsView.js +15 -9
- package/dist/ui/layout.d.ts +6 -0
- package/dist/ui/layout.js +22 -0
- package/dist/ui/scroll.d.ts +3 -0
- package/dist/ui/scroll.js +4 -0
- package/dist/ui/theme.js +1 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# agent-dev
|
|
2
2
|
|
|
3
|
-
A minimal pi-like terminal coding agent with an Ink UI. Chat with an AI that can read, write, edit files, and run
|
|
3
|
+
A minimal pi-like terminal coding agent with an Ink UI. Chat with an AI that can read, write, edit files, search the web, and run shell commands (with your approval).
|
|
4
4
|
|
|
5
5
|
## Quick start
|
|
6
6
|
|
|
@@ -51,7 +51,17 @@ Config and sessions are stored in `~/.agent-dev/`.
|
|
|
51
51
|
|
|
52
52
|
## Tools
|
|
53
53
|
|
|
54
|
-
The agent has
|
|
54
|
+
The agent has five built-in tools:
|
|
55
|
+
|
|
56
|
+
| Tool | Description |
|
|
57
|
+
|------|-------------|
|
|
58
|
+
| `read` | Read a file in the project directory |
|
|
59
|
+
| `write` | Create or overwrite a file |
|
|
60
|
+
| `edit` | Replace text in a file |
|
|
61
|
+
| `web_search` | Search the internet (DuckDuckGo, no extra API key) |
|
|
62
|
+
| `bash` | Run a shell command — **requires your approval** before execution |
|
|
63
|
+
|
|
64
|
+
File operations are restricted to the current working directory. When the agent proposes a shell command, the UI prompts you to approve (`y`) or deny (`n` / Esc).
|
|
55
65
|
|
|
56
66
|
## License
|
|
57
67
|
|
package/dist/agent/loop.d.ts
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import type { ChatMessage, Model, ToolCall } from "../providers/types.js";
|
|
2
2
|
import type { Settings } from "../config/settings.js";
|
|
3
|
+
export interface PermissionRequest {
|
|
4
|
+
toolCallId: string;
|
|
5
|
+
name: string;
|
|
6
|
+
args: Record<string, unknown>;
|
|
7
|
+
command: string;
|
|
8
|
+
}
|
|
3
9
|
export type AgentEvent = {
|
|
4
10
|
type: "message_start";
|
|
5
11
|
role: "assistant";
|
|
@@ -28,5 +34,6 @@ export interface AgentLoopOptions {
|
|
|
28
34
|
systemPrompt?: string;
|
|
29
35
|
signal?: AbortSignal;
|
|
30
36
|
onEvent: (event: AgentEvent) => void;
|
|
37
|
+
onPermissionRequest?: (request: PermissionRequest) => Promise<boolean>;
|
|
31
38
|
}
|
|
32
39
|
export declare function runAgentLoop(options: AgentLoopOptions): Promise<ChatMessage[]>;
|
package/dist/agent/loop.js
CHANGED
|
@@ -1,8 +1,111 @@
|
|
|
1
|
+
import { normalizeToolCalls, parseMalformedToolCalls, extractFailedGeneration, } from "../providers/openai-compat.js";
|
|
1
2
|
import { streamChat } from "../providers/registry.js";
|
|
2
|
-
import { getToolDefinitions, executeTool } from "./tools/index.js";
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
import { getToolDefinitions, executeTool, PERMISSION_REQUIRED_TOOLS } from "./tools/index.js";
|
|
4
|
+
import { getPlatformContext } from "./platform.js";
|
|
5
|
+
const MAX_TOOL_ROUNDS = 6;
|
|
6
|
+
const MAX_SAME_TOOL_CALLS = 2;
|
|
7
|
+
const DEFAULT_SYSTEM_PROMPT = `You are a helpful coding assistant with access to tools: read, write, edit, bash, and web_search.
|
|
8
|
+
When the user asks you to create or modify files, call write or edit once with the full file content, then reply briefly to confirm.
|
|
9
|
+
Use web_search when you need current information, documentation, or facts from the internet.
|
|
10
|
+
Shell commands via bash require user approval before they run — propose the exact command you need.
|
|
11
|
+
Never run long-lived dev servers (npm run dev, npm start) — they time out. Use npm run build to verify, then tell the user how to start the dev server locally.
|
|
12
|
+
Do NOT call the same tool repeatedly with the same arguments. One successful write is enough.
|
|
13
|
+
When calling tools, use the function-calling API with valid JSON arguments only (e.g. web_search: {"query": "search terms"}).
|
|
14
|
+
|
|
15
|
+
${getPlatformContext()}`;
|
|
16
|
+
function systemPromptForModel(model, base = DEFAULT_SYSTEM_PROMPT) {
|
|
17
|
+
if (model.provider === "groq") {
|
|
18
|
+
return `${base}\nFor Groq: never output <function=...> text — use structured tool calls with JSON arguments.`;
|
|
19
|
+
}
|
|
20
|
+
return base;
|
|
21
|
+
}
|
|
22
|
+
function isToolUseFailedError(message) {
|
|
23
|
+
return /Failed to call a function|tool_use_failed|failed_generation/i.test(message);
|
|
24
|
+
}
|
|
25
|
+
function hadSuccessfulToolResults(context) {
|
|
26
|
+
return context.some((m) => m.role === "tool" && m.content.length > 0 && !m.content.startsWith("Error:"));
|
|
27
|
+
}
|
|
28
|
+
function toolSignature(name, args) {
|
|
29
|
+
return `${name}:${JSON.stringify(args)}`;
|
|
30
|
+
}
|
|
31
|
+
function dedupeToolCalls(toolCalls) {
|
|
32
|
+
const seen = new Set();
|
|
33
|
+
return toolCalls.filter((tc) => {
|
|
34
|
+
const key = `${tc.name}:${tc.arguments}`;
|
|
35
|
+
if (seen.has(key))
|
|
36
|
+
return false;
|
|
37
|
+
seen.add(key);
|
|
38
|
+
return true;
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
function resolveToolCalls(content, toolCalls, error) {
|
|
42
|
+
const normalized = dedupeToolCalls(normalizeToolCalls(toolCalls.filter((tc) => tc.name)));
|
|
43
|
+
if (normalized.length > 0)
|
|
44
|
+
return normalized;
|
|
45
|
+
const fromContent = dedupeToolCalls(normalizeToolCalls(parseMalformedToolCalls(content)));
|
|
46
|
+
if (fromContent.length > 0)
|
|
47
|
+
return fromContent;
|
|
48
|
+
if (error) {
|
|
49
|
+
const failed = extractFailedGeneration(error);
|
|
50
|
+
if (failed) {
|
|
51
|
+
return dedupeToolCalls(normalizeToolCalls(parseMalformedToolCalls(failed)));
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return [];
|
|
55
|
+
}
|
|
56
|
+
async function runToolBatch(uniqueCalls, context, workdir, callCounts, onEvent, onPermissionRequest) {
|
|
57
|
+
let stopAfterBatch = false;
|
|
58
|
+
for (const tc of uniqueCalls) {
|
|
59
|
+
onEvent({ type: "tool_call", toolCall: tc });
|
|
60
|
+
let args = {};
|
|
61
|
+
try {
|
|
62
|
+
args = JSON.parse(tc.arguments || "{}");
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
args = {};
|
|
66
|
+
}
|
|
67
|
+
const sig = toolSignature(tc.name, args);
|
|
68
|
+
const prev = callCounts.get(sig) ?? 0;
|
|
69
|
+
callCounts.set(sig, prev + 1);
|
|
70
|
+
if (prev >= MAX_SAME_TOOL_CALLS) {
|
|
71
|
+
const skip = "Skipped — already executed this action.";
|
|
72
|
+
onEvent({ type: "tool_result", toolCallId: tc.id, name: tc.name, result: skip });
|
|
73
|
+
context.push({ role: "tool", content: skip, toolCallId: tc.id, name: tc.name });
|
|
74
|
+
stopAfterBatch = true;
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
let result;
|
|
78
|
+
const needsPermission = PERMISSION_REQUIRED_TOOLS.has(tc.name);
|
|
79
|
+
if (needsPermission && onPermissionRequest) {
|
|
80
|
+
const approved = await onPermissionRequest({
|
|
81
|
+
toolCallId: tc.id,
|
|
82
|
+
name: tc.name,
|
|
83
|
+
args,
|
|
84
|
+
command: String(args.command ?? ""),
|
|
85
|
+
});
|
|
86
|
+
result = approved
|
|
87
|
+
? await executeTool(tc.name, args, workdir)
|
|
88
|
+
: "Command execution denied by user.";
|
|
89
|
+
}
|
|
90
|
+
else if (needsPermission) {
|
|
91
|
+
result = "Command execution denied — permission handler not available.";
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
result = await executeTool(tc.name, args, workdir);
|
|
95
|
+
}
|
|
96
|
+
onEvent({ type: "tool_result", toolCallId: tc.id, name: tc.name, result });
|
|
97
|
+
context.push({
|
|
98
|
+
role: "tool",
|
|
99
|
+
content: result,
|
|
100
|
+
toolCallId: tc.id,
|
|
101
|
+
name: tc.name,
|
|
102
|
+
});
|
|
103
|
+
if (!result.startsWith("Error:") && (tc.name === "write" || tc.name === "edit")) {
|
|
104
|
+
stopAfterBatch = true;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return stopAfterBatch;
|
|
108
|
+
}
|
|
6
109
|
async function collectStream(model, messages, settings, systemPrompt, signal, onEvent) {
|
|
7
110
|
const tools = getToolDefinitions();
|
|
8
111
|
let content = "";
|
|
@@ -32,51 +135,64 @@ async function collectStream(model, messages, settings, systemPrompt, signal, on
|
|
|
32
135
|
tc.arguments += event.argumentsDelta;
|
|
33
136
|
}
|
|
34
137
|
else if (event.type === "error") {
|
|
35
|
-
return { content, toolCalls:
|
|
138
|
+
return { content, toolCalls: Array.from(toolCallMap.values()), error: event.message };
|
|
36
139
|
}
|
|
37
140
|
}
|
|
38
|
-
const toolCalls = Array.from(toolCallMap.values()).filter((tc) => tc.name);
|
|
141
|
+
const toolCalls = normalizeToolCalls(Array.from(toolCallMap.values()).filter((tc) => tc.name));
|
|
39
142
|
return { content, toolCalls };
|
|
40
143
|
}
|
|
144
|
+
function finishGracefully(context, content, onEvent) {
|
|
145
|
+
const msg = content.trim() || "Done — changes saved successfully.";
|
|
146
|
+
if (!content.trim()) {
|
|
147
|
+
onEvent({ type: "text_delta", delta: msg });
|
|
148
|
+
}
|
|
149
|
+
context.push({ role: "assistant", content: msg });
|
|
150
|
+
onEvent({ type: "turn_end" });
|
|
151
|
+
}
|
|
41
152
|
export async function runAgentLoop(options) {
|
|
42
|
-
const { model, messages, settings, workdir, systemPrompt = DEFAULT_SYSTEM_PROMPT, signal, onEvent, } = options;
|
|
153
|
+
const { model, messages, settings, workdir, systemPrompt = DEFAULT_SYSTEM_PROMPT, signal, onEvent, onPermissionRequest, } = options;
|
|
43
154
|
const context = [...messages];
|
|
155
|
+
const callCounts = new Map();
|
|
156
|
+
const effectivePrompt = systemPromptForModel(model, systemPrompt);
|
|
157
|
+
let toolRound = 0;
|
|
44
158
|
while (true) {
|
|
45
159
|
if (signal?.aborted)
|
|
46
160
|
break;
|
|
161
|
+
toolRound++;
|
|
162
|
+
if (toolRound > MAX_TOOL_ROUNDS) {
|
|
163
|
+
finishGracefully(context, "Done — stopped after too many tool calls.", onEvent);
|
|
164
|
+
break;
|
|
165
|
+
}
|
|
47
166
|
onEvent({ type: "message_start", role: "assistant" });
|
|
48
|
-
const { content, toolCalls, error } = await collectStream(model, context, settings,
|
|
49
|
-
|
|
167
|
+
const { content, toolCalls, error } = await collectStream(model, context, settings, effectivePrompt, signal, onEvent);
|
|
168
|
+
const uniqueCalls = resolveToolCalls(content, toolCalls, error);
|
|
169
|
+
if (error && uniqueCalls.length === 0) {
|
|
170
|
+
if (isToolUseFailedError(error) && hadSuccessfulToolResults(context)) {
|
|
171
|
+
finishGracefully(context, content, onEvent);
|
|
172
|
+
break;
|
|
173
|
+
}
|
|
50
174
|
onEvent({ type: "error", message: error });
|
|
51
175
|
break;
|
|
52
176
|
}
|
|
53
177
|
const assistantMsg = {
|
|
54
178
|
role: "assistant",
|
|
55
|
-
content,
|
|
56
|
-
toolCalls:
|
|
179
|
+
content: error ? "" : content,
|
|
180
|
+
toolCalls: uniqueCalls.length > 0 ? uniqueCalls : undefined,
|
|
57
181
|
};
|
|
58
182
|
context.push(assistantMsg);
|
|
59
|
-
if (
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
}
|
|
63
|
-
for (const tc of toolCalls) {
|
|
64
|
-
onEvent({ type: "tool_call", toolCall: tc });
|
|
65
|
-
let args = {};
|
|
66
|
-
try {
|
|
67
|
-
args = JSON.parse(tc.arguments || "{}");
|
|
183
|
+
if (uniqueCalls.length === 0) {
|
|
184
|
+
if (error) {
|
|
185
|
+
onEvent({ type: "error", message: error });
|
|
68
186
|
}
|
|
69
|
-
|
|
70
|
-
|
|
187
|
+
else {
|
|
188
|
+
onEvent({ type: "turn_end" });
|
|
71
189
|
}
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
name: tc.name,
|
|
79
|
-
});
|
|
190
|
+
break;
|
|
191
|
+
}
|
|
192
|
+
const stopAfterBatch = await runToolBatch(uniqueCalls, context, workdir, callCounts, onEvent, onPermissionRequest);
|
|
193
|
+
if (stopAfterBatch) {
|
|
194
|
+
finishGracefully(context, content, onEvent);
|
|
195
|
+
break;
|
|
80
196
|
}
|
|
81
197
|
}
|
|
82
198
|
return context.slice(messages.length);
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export interface ShellConfig {
|
|
2
|
+
executable: string;
|
|
3
|
+
args: string[];
|
|
4
|
+
name: string;
|
|
5
|
+
supportsAndAnd: boolean;
|
|
6
|
+
}
|
|
7
|
+
export declare function getShellConfig(): ShellConfig;
|
|
8
|
+
/** Adapt common Unix-isms and command chaining for the host shell. */
|
|
9
|
+
export declare function normalizeCommand(command: string, shell: ShellConfig): string;
|
|
10
|
+
export declare function getPlatformContext(): string;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { arch, platform, release } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
function findPwsh() {
|
|
5
|
+
const candidates = [
|
|
6
|
+
process.env.PWSH_PATH,
|
|
7
|
+
join(process.env.ProgramFiles ?? "C:\\Program Files", "PowerShell", "7", "pwsh.exe"),
|
|
8
|
+
join(process.env["ProgramFiles(x86)"] ?? "", "PowerShell", "7", "pwsh.exe"),
|
|
9
|
+
].filter(Boolean);
|
|
10
|
+
for (const candidate of candidates) {
|
|
11
|
+
if (existsSync(candidate))
|
|
12
|
+
return candidate;
|
|
13
|
+
}
|
|
14
|
+
return undefined;
|
|
15
|
+
}
|
|
16
|
+
export function getShellConfig() {
|
|
17
|
+
if (platform() === "win32") {
|
|
18
|
+
const pwsh = findPwsh();
|
|
19
|
+
if (pwsh) {
|
|
20
|
+
return {
|
|
21
|
+
executable: pwsh,
|
|
22
|
+
name: "PowerShell 7+ (pwsh)",
|
|
23
|
+
supportsAndAnd: true,
|
|
24
|
+
args: ["-NoProfile", "-ExecutionPolicy", "Bypass", "-Command"],
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
return {
|
|
28
|
+
executable: "powershell.exe",
|
|
29
|
+
name: "Windows PowerShell",
|
|
30
|
+
supportsAndAnd: false,
|
|
31
|
+
args: ["-NoProfile", "-ExecutionPolicy", "Bypass", "-Command"],
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
return {
|
|
35
|
+
executable: process.env.SHELL ?? "/bin/bash",
|
|
36
|
+
name: "bash",
|
|
37
|
+
supportsAndAnd: true,
|
|
38
|
+
args: ["-lc"],
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
/** Adapt common Unix-isms and command chaining for the host shell. */
|
|
42
|
+
export function normalizeCommand(command, shell) {
|
|
43
|
+
let cmd = command.trim();
|
|
44
|
+
if (!cmd)
|
|
45
|
+
return cmd;
|
|
46
|
+
if (platform() === "win32") {
|
|
47
|
+
if (!shell.supportsAndAnd) {
|
|
48
|
+
cmd = cmd.replace(/\s&&\s/g, "; ");
|
|
49
|
+
}
|
|
50
|
+
cmd = cmd
|
|
51
|
+
.replace(/\bmkdir -p\s+/g, "New-Item -ItemType Directory -Force -Path ")
|
|
52
|
+
.replace(/\brm -rf\s+/g, "Remove-Item -Recurse -Force ")
|
|
53
|
+
.replace(/\brm -r\s+/g, "Remove-Item -Recurse -Force ")
|
|
54
|
+
.replace(/\btouch\s+/g, "New-Item -ItemType File -Force ");
|
|
55
|
+
}
|
|
56
|
+
return cmd;
|
|
57
|
+
}
|
|
58
|
+
export function getPlatformContext() {
|
|
59
|
+
const shell = getShellConfig();
|
|
60
|
+
const lines = [
|
|
61
|
+
`Platform: ${platform()} ${arch()} (${release()})`,
|
|
62
|
+
`Shell: ${shell.name}`,
|
|
63
|
+
`Working directory: ${process.cwd()}`,
|
|
64
|
+
];
|
|
65
|
+
if (platform() === "win32") {
|
|
66
|
+
lines.push("This agent runs on Windows. Use PowerShell-compatible commands.", shell.supportsAndAnd
|
|
67
|
+
? "You may chain commands with ; or &&."
|
|
68
|
+
: "Chain commands with ; (&& is NOT supported in Windows PowerShell 5).", "Examples: New-Item -ItemType Directory -Force todo-app; Set-Location todo-app", "For npx/npm scaffolding, always use non-interactive flags (--yes, -y, --defaults) and set CI=1.", "Do not use mkdir -p, rm -rf, or touch — use PowerShell equivalents or the write tool.", "Never run dev servers (npm run dev, npm start, next dev) via bash — they run forever and will time out.", "To verify a web app, use npm run build. Tell the user to run the dev server in their own terminal.", "Do not run npm audit fix unless the user explicitly asks.");
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
lines.push("Use bash/sh syntax. Chain commands with && or ;.", "For npx/npm scaffolding, use non-interactive flags (--yes, -y) to avoid prompts.", "Never run dev servers (npm run dev, npm start) via bash — they run forever.", "To verify a web app, use npm run build. Tell the user to run the dev server separately.");
|
|
72
|
+
}
|
|
73
|
+
return lines.join("\n");
|
|
74
|
+
}
|
package/dist/agent/session.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { EventEmitter } from "node:events";
|
|
2
2
|
import type { ChatMessage, Model } from "../providers/types.js";
|
|
3
3
|
import type { Settings } from "../config/settings.js";
|
|
4
|
-
import { type AgentEvent } from "./loop.js";
|
|
4
|
+
import { type AgentEvent, type PermissionRequest } from "./loop.js";
|
|
5
5
|
import { SessionManager } from "../session/manager.js";
|
|
6
6
|
export type SessionEvent = AgentEvent | {
|
|
7
7
|
type: "user_message";
|
|
@@ -9,6 +9,9 @@ export type SessionEvent = AgentEvent | {
|
|
|
9
9
|
} | {
|
|
10
10
|
type: "model_changed";
|
|
11
11
|
model: Model;
|
|
12
|
+
} | {
|
|
13
|
+
type: "permission_request";
|
|
14
|
+
request: PermissionRequest;
|
|
12
15
|
};
|
|
13
16
|
export declare class AgentSession extends EventEmitter {
|
|
14
17
|
private messages;
|
|
@@ -18,6 +21,7 @@ export declare class AgentSession extends EventEmitter {
|
|
|
18
21
|
private sessionManager;
|
|
19
22
|
private abortController?;
|
|
20
23
|
private running;
|
|
24
|
+
private pendingPermission?;
|
|
21
25
|
constructor(settings: Settings, sessionManager: SessionManager, workdir: string, initialModel?: Model);
|
|
22
26
|
getModel(): Model;
|
|
23
27
|
getSettings(): Settings;
|
|
@@ -26,6 +30,9 @@ export declare class AgentSession extends EventEmitter {
|
|
|
26
30
|
setModel(model: Model): void;
|
|
27
31
|
updateSettings(settings: Settings): void;
|
|
28
32
|
abort(): void;
|
|
33
|
+
respondToPermission(approved: boolean): void;
|
|
34
|
+
private resolvePermission;
|
|
35
|
+
private requestCommandPermission;
|
|
29
36
|
prompt(content: string): Promise<void>;
|
|
30
37
|
newSession(): void;
|
|
31
38
|
getAvailableModels(): Model[];
|
package/dist/agent/session.js
CHANGED
|
@@ -11,6 +11,7 @@ export class AgentSession extends EventEmitter {
|
|
|
11
11
|
sessionManager;
|
|
12
12
|
abortController;
|
|
13
13
|
running = false;
|
|
14
|
+
pendingPermission;
|
|
14
15
|
constructor(settings, sessionManager, workdir, initialModel) {
|
|
15
16
|
super();
|
|
16
17
|
this.settings = settings;
|
|
@@ -49,6 +50,23 @@ export class AgentSession extends EventEmitter {
|
|
|
49
50
|
}
|
|
50
51
|
abort() {
|
|
51
52
|
this.abortController?.abort();
|
|
53
|
+
this.resolvePermission(false);
|
|
54
|
+
}
|
|
55
|
+
respondToPermission(approved) {
|
|
56
|
+
this.resolvePermission(approved);
|
|
57
|
+
}
|
|
58
|
+
resolvePermission(approved) {
|
|
59
|
+
const resolve = this.pendingPermission;
|
|
60
|
+
if (resolve) {
|
|
61
|
+
this.pendingPermission = undefined;
|
|
62
|
+
resolve(approved);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
requestCommandPermission(request) {
|
|
66
|
+
return new Promise((resolve) => {
|
|
67
|
+
this.pendingPermission = resolve;
|
|
68
|
+
this.emit("event", { type: "permission_request", request });
|
|
69
|
+
});
|
|
52
70
|
}
|
|
53
71
|
async prompt(content) {
|
|
54
72
|
if (this.running)
|
|
@@ -67,6 +85,7 @@ export class AgentSession extends EventEmitter {
|
|
|
67
85
|
workdir: this.workdir,
|
|
68
86
|
signal: this.abortController.signal,
|
|
69
87
|
onEvent: (event) => this.emit("event", event),
|
|
88
|
+
onPermissionRequest: (request) => this.requestCommandPermission(request),
|
|
70
89
|
});
|
|
71
90
|
for (const msg of newMessages) {
|
|
72
91
|
if (msg.role !== "user") {
|
|
@@ -4,5 +4,6 @@ export interface AgentTool {
|
|
|
4
4
|
execute: (args: Record<string, unknown>, workdir: string) => Promise<string>;
|
|
5
5
|
}
|
|
6
6
|
export declare const BUILTIN_TOOLS: AgentTool[];
|
|
7
|
+
export declare const PERMISSION_REQUIRED_TOOLS: Set<string>;
|
|
7
8
|
export declare function getToolDefinitions(): ToolDefinition[];
|
|
8
9
|
export declare function executeTool(name: string, args: Record<string, unknown>, workdir: string): Promise<string>;
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import { readTool, writeTool, editTool, bashTool, executeRead, executeWrite, executeEdit, executeBash, } from "./read.js";
|
|
2
|
+
import { webSearchTool, executeWebSearch } from "./search.js";
|
|
2
3
|
export const BUILTIN_TOOLS = [
|
|
3
4
|
{ definition: readTool, execute: (args, wd) => executeRead(args, wd) },
|
|
4
5
|
{ definition: writeTool, execute: (args, wd) => executeWrite(args, wd) },
|
|
5
6
|
{ definition: editTool, execute: (args, wd) => executeEdit(args, wd) },
|
|
6
7
|
{ definition: bashTool, execute: (args, wd) => executeBash(args, wd) },
|
|
8
|
+
{ definition: webSearchTool, execute: (args) => executeWebSearch(args) },
|
|
7
9
|
];
|
|
10
|
+
export const PERMISSION_REQUIRED_TOOLS = new Set(["bash"]);
|
|
8
11
|
export function getToolDefinitions() {
|
|
9
12
|
return BUILTIN_TOOLS.map((t) => t.definition);
|
|
10
13
|
}
|
package/dist/agent/tools/read.js
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { readFileSync, writeFileSync, existsSync } from "node:fs";
|
|
2
2
|
import { resolve, isAbsolute } from "node:path";
|
|
3
|
+
import { platform as osPlatform } from "node:os";
|
|
4
|
+
import { getShellConfig } from "../platform.js";
|
|
5
|
+
import { executeShellCommand } from "./shell.js";
|
|
3
6
|
const DEFAULT_WORKDIR = process.cwd();
|
|
4
7
|
function resolvePath(path, workdir = DEFAULT_WORKDIR) {
|
|
5
8
|
return isAbsolute(path) ? path : resolve(workdir, path);
|
|
@@ -20,6 +23,7 @@ export const readTool = {
|
|
|
20
23
|
path: { type: "string", description: "File path relative to project root" },
|
|
21
24
|
},
|
|
22
25
|
required: ["path"],
|
|
26
|
+
additionalProperties: false,
|
|
23
27
|
},
|
|
24
28
|
};
|
|
25
29
|
export async function executeRead(args, workdir = DEFAULT_WORKDIR) {
|
|
@@ -41,6 +45,7 @@ export const writeTool = {
|
|
|
41
45
|
content: { type: "string", description: "Content to write" },
|
|
42
46
|
},
|
|
43
47
|
required: ["path", "content"],
|
|
48
|
+
additionalProperties: false,
|
|
44
49
|
},
|
|
45
50
|
};
|
|
46
51
|
export async function executeWrite(args, workdir = DEFAULT_WORKDIR) {
|
|
@@ -60,6 +65,7 @@ export const editTool = {
|
|
|
60
65
|
new_string: { type: "string", description: "Replacement string" },
|
|
61
66
|
},
|
|
62
67
|
required: ["path", "old_string", "new_string"],
|
|
68
|
+
additionalProperties: false,
|
|
63
69
|
},
|
|
64
70
|
};
|
|
65
71
|
export async function executeEdit(args, workdir = DEFAULT_WORKDIR) {
|
|
@@ -78,31 +84,23 @@ export async function executeEdit(args, workdir = DEFAULT_WORKDIR) {
|
|
|
78
84
|
}
|
|
79
85
|
export const bashTool = {
|
|
80
86
|
name: "bash",
|
|
81
|
-
description:
|
|
87
|
+
description: osPlatform() === "win32"
|
|
88
|
+
? "Run a PowerShell command in the project directory (Windows). Chain with ; not &&. Use non-interactive flags for npx/npm."
|
|
89
|
+
: "Run a bash shell command in the project directory",
|
|
82
90
|
parameters: {
|
|
83
91
|
type: "object",
|
|
84
92
|
properties: {
|
|
85
93
|
command: { type: "string", description: "Shell command to execute" },
|
|
86
94
|
},
|
|
87
95
|
required: ["command"],
|
|
96
|
+
additionalProperties: false,
|
|
88
97
|
},
|
|
89
98
|
};
|
|
90
99
|
export async function executeBash(args, workdir = DEFAULT_WORKDIR) {
|
|
91
|
-
const
|
|
92
|
-
const
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
const { stdout, stderr } = await execAsync(args.command, {
|
|
96
|
-
cwd: workdir,
|
|
97
|
-
timeout: 120000,
|
|
98
|
-
maxBuffer: 1024 * 1024,
|
|
99
|
-
});
|
|
100
|
-
const out = stdout + (stderr ? `\n${stderr}` : "");
|
|
101
|
-
return out.trim() || "(no output)";
|
|
102
|
-
}
|
|
103
|
-
catch (err) {
|
|
104
|
-
const e = err;
|
|
105
|
-
const out = (e.stdout ?? "") + (e.stderr ? `\n${e.stderr}` : "");
|
|
106
|
-
return out.trim() || (e.message ?? "Command failed");
|
|
100
|
+
const shell = getShellConfig();
|
|
101
|
+
const result = await executeShellCommand(args.command, workdir);
|
|
102
|
+
if (result.startsWith("Error:")) {
|
|
103
|
+
return `${result}\n(shell: ${shell.name})`;
|
|
107
104
|
}
|
|
105
|
+
return result;
|
|
108
106
|
}
|