@devang0907/agent-dev 0.1.3 → 0.1.5
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 +96 -42
- 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 +11 -17
- package/dist/agent/tools/search.d.ts +5 -0
- package/dist/agent/tools/search.js +229 -0
- package/dist/agent/tools/shell.d.ts +1 -0
- package/dist/agent/tools/shell.js +131 -0
- package/dist/modes/print-mode.js +15 -0
- package/dist/providers/openai-compat.d.ts +2 -0
- package/dist/providers/openai-compat.js +98 -0
- package/dist/ui/App.d.ts +1 -0
- package/dist/ui/App.js +67 -15
- package/dist/ui/ChatView.d.ts +2 -1
- package/dist/ui/ChatView.js +15 -7
- 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 +6 -2
- package/dist/ui/format-tool.d.ts +2 -0
- package/dist/ui/format-tool.js +25 -0
- 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,12 +1,23 @@
|
|
|
1
|
-
import { normalizeToolCalls } from "../providers/openai-compat.js";
|
|
1
|
+
import { normalizeToolCalls, parseMalformedToolCalls, extractFailedGeneration, } from "../providers/openai-compat.js";
|
|
2
2
|
import { streamChat } from "../providers/registry.js";
|
|
3
|
-
import { getToolDefinitions, executeTool } from "./tools/index.js";
|
|
3
|
+
import { getToolDefinitions, executeTool, PERMISSION_REQUIRED_TOOLS } from "./tools/index.js";
|
|
4
|
+
import { getPlatformContext } from "./platform.js";
|
|
4
5
|
const MAX_TOOL_ROUNDS = 6;
|
|
5
6
|
const MAX_SAME_TOOL_CALLS = 2;
|
|
6
|
-
const DEFAULT_SYSTEM_PROMPT = `You are a helpful coding assistant with access to tools: read, write, edit, and
|
|
7
|
+
const DEFAULT_SYSTEM_PROMPT = `You are a helpful coding assistant with access to tools: read, write, edit, bash, and web_search.
|
|
7
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 for news and current events. When headlines are returned, list them as a numbered list using the exact titles — do not give vague category summaries.
|
|
10
|
+
Shell commands via bash require user approval. Dev servers (npm run dev, npm start) run in the background and return a URL.
|
|
8
11
|
Do NOT call the same tool repeatedly with the same arguments. One successful write is enough.
|
|
9
|
-
|
|
12
|
+
When calling tools, use the function-calling API with valid JSON arguments only (e.g. web_search: {"query": "search terms"}).
|
|
13
|
+
|
|
14
|
+
${getPlatformContext()}`;
|
|
15
|
+
function systemPromptForModel(model, base = DEFAULT_SYSTEM_PROMPT) {
|
|
16
|
+
if (model.provider === "groq") {
|
|
17
|
+
return `${base}\nFor Groq: never output <function=...> text — use structured tool calls with JSON arguments.`;
|
|
18
|
+
}
|
|
19
|
+
return base;
|
|
20
|
+
}
|
|
10
21
|
function isToolUseFailedError(message) {
|
|
11
22
|
return /Failed to call a function|tool_use_failed|failed_generation/i.test(message);
|
|
12
23
|
}
|
|
@@ -26,6 +37,74 @@ function dedupeToolCalls(toolCalls) {
|
|
|
26
37
|
return true;
|
|
27
38
|
});
|
|
28
39
|
}
|
|
40
|
+
function resolveToolCalls(content, toolCalls, error) {
|
|
41
|
+
const normalized = dedupeToolCalls(normalizeToolCalls(toolCalls.filter((tc) => tc.name)));
|
|
42
|
+
if (normalized.length > 0)
|
|
43
|
+
return normalized;
|
|
44
|
+
const fromContent = dedupeToolCalls(normalizeToolCalls(parseMalformedToolCalls(content)));
|
|
45
|
+
if (fromContent.length > 0)
|
|
46
|
+
return fromContent;
|
|
47
|
+
if (error) {
|
|
48
|
+
const failed = extractFailedGeneration(error);
|
|
49
|
+
if (failed) {
|
|
50
|
+
return dedupeToolCalls(normalizeToolCalls(parseMalformedToolCalls(failed)));
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return [];
|
|
54
|
+
}
|
|
55
|
+
async function runToolBatch(uniqueCalls, context, workdir, callCounts, onEvent, onPermissionRequest) {
|
|
56
|
+
let stopAfterBatch = false;
|
|
57
|
+
for (const tc of uniqueCalls) {
|
|
58
|
+
onEvent({ type: "tool_call", toolCall: tc });
|
|
59
|
+
let args = {};
|
|
60
|
+
try {
|
|
61
|
+
args = JSON.parse(tc.arguments || "{}");
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
args = {};
|
|
65
|
+
}
|
|
66
|
+
const sig = toolSignature(tc.name, args);
|
|
67
|
+
const prev = callCounts.get(sig) ?? 0;
|
|
68
|
+
callCounts.set(sig, prev + 1);
|
|
69
|
+
if (prev >= MAX_SAME_TOOL_CALLS) {
|
|
70
|
+
const skip = "Skipped — already executed this action.";
|
|
71
|
+
onEvent({ type: "tool_result", toolCallId: tc.id, name: tc.name, result: skip });
|
|
72
|
+
context.push({ role: "tool", content: skip, toolCallId: tc.id, name: tc.name });
|
|
73
|
+
stopAfterBatch = true;
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
let result;
|
|
77
|
+
const needsPermission = PERMISSION_REQUIRED_TOOLS.has(tc.name);
|
|
78
|
+
if (needsPermission && onPermissionRequest) {
|
|
79
|
+
const approved = await onPermissionRequest({
|
|
80
|
+
toolCallId: tc.id,
|
|
81
|
+
name: tc.name,
|
|
82
|
+
args,
|
|
83
|
+
command: String(args.command ?? ""),
|
|
84
|
+
});
|
|
85
|
+
result = approved
|
|
86
|
+
? await executeTool(tc.name, args, workdir)
|
|
87
|
+
: "Command execution denied by user.";
|
|
88
|
+
}
|
|
89
|
+
else if (needsPermission) {
|
|
90
|
+
result = "Command execution denied — permission handler not available.";
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
result = await executeTool(tc.name, args, workdir);
|
|
94
|
+
}
|
|
95
|
+
onEvent({ type: "tool_result", toolCallId: tc.id, name: tc.name, result });
|
|
96
|
+
context.push({
|
|
97
|
+
role: "tool",
|
|
98
|
+
content: result,
|
|
99
|
+
toolCallId: tc.id,
|
|
100
|
+
name: tc.name,
|
|
101
|
+
});
|
|
102
|
+
if (!result.startsWith("Error:") && (tc.name === "write" || tc.name === "edit")) {
|
|
103
|
+
stopAfterBatch = true;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return stopAfterBatch;
|
|
107
|
+
}
|
|
29
108
|
async function collectStream(model, messages, settings, systemPrompt, signal, onEvent) {
|
|
30
109
|
const tools = getToolDefinitions();
|
|
31
110
|
let content = "";
|
|
@@ -55,7 +134,7 @@ async function collectStream(model, messages, settings, systemPrompt, signal, on
|
|
|
55
134
|
tc.arguments += event.argumentsDelta;
|
|
56
135
|
}
|
|
57
136
|
else if (event.type === "error") {
|
|
58
|
-
return { content, toolCalls:
|
|
137
|
+
return { content, toolCalls: Array.from(toolCallMap.values()), error: event.message };
|
|
59
138
|
}
|
|
60
139
|
}
|
|
61
140
|
const toolCalls = normalizeToolCalls(Array.from(toolCallMap.values()).filter((tc) => tc.name));
|
|
@@ -70,9 +149,10 @@ function finishGracefully(context, content, onEvent) {
|
|
|
70
149
|
onEvent({ type: "turn_end" });
|
|
71
150
|
}
|
|
72
151
|
export async function runAgentLoop(options) {
|
|
73
|
-
const { model, messages, settings, workdir, systemPrompt = DEFAULT_SYSTEM_PROMPT, signal, onEvent, } = options;
|
|
152
|
+
const { model, messages, settings, workdir, systemPrompt = DEFAULT_SYSTEM_PROMPT, signal, onEvent, onPermissionRequest, } = options;
|
|
74
153
|
const context = [...messages];
|
|
75
154
|
const callCounts = new Map();
|
|
155
|
+
const effectivePrompt = systemPromptForModel(model, systemPrompt);
|
|
76
156
|
let toolRound = 0;
|
|
77
157
|
while (true) {
|
|
78
158
|
if (signal?.aborted)
|
|
@@ -83,8 +163,9 @@ export async function runAgentLoop(options) {
|
|
|
83
163
|
break;
|
|
84
164
|
}
|
|
85
165
|
onEvent({ type: "message_start", role: "assistant" });
|
|
86
|
-
const { content, toolCalls, error } = await collectStream(model, context, settings,
|
|
87
|
-
|
|
166
|
+
const { content, toolCalls, error } = await collectStream(model, context, settings, effectivePrompt, signal, onEvent);
|
|
167
|
+
const uniqueCalls = resolveToolCalls(content, toolCalls, error);
|
|
168
|
+
if (error && uniqueCalls.length === 0) {
|
|
88
169
|
if (isToolUseFailedError(error) && hadSuccessfulToolResults(context)) {
|
|
89
170
|
finishGracefully(context, content, onEvent);
|
|
90
171
|
break;
|
|
@@ -92,49 +173,22 @@ export async function runAgentLoop(options) {
|
|
|
92
173
|
onEvent({ type: "error", message: error });
|
|
93
174
|
break;
|
|
94
175
|
}
|
|
95
|
-
const uniqueCalls = dedupeToolCalls(toolCalls);
|
|
96
176
|
const assistantMsg = {
|
|
97
177
|
role: "assistant",
|
|
98
|
-
content,
|
|
178
|
+
content: error ? "" : content,
|
|
99
179
|
toolCalls: uniqueCalls.length > 0 ? uniqueCalls : undefined,
|
|
100
180
|
};
|
|
101
181
|
context.push(assistantMsg);
|
|
102
182
|
if (uniqueCalls.length === 0) {
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
}
|
|
106
|
-
let stopAfterBatch = false;
|
|
107
|
-
for (const tc of uniqueCalls) {
|
|
108
|
-
onEvent({ type: "tool_call", toolCall: tc });
|
|
109
|
-
let args = {};
|
|
110
|
-
try {
|
|
111
|
-
args = JSON.parse(tc.arguments || "{}");
|
|
183
|
+
if (error) {
|
|
184
|
+
onEvent({ type: "error", message: error });
|
|
112
185
|
}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
}
|
|
116
|
-
const sig = toolSignature(tc.name, args);
|
|
117
|
-
const prev = callCounts.get(sig) ?? 0;
|
|
118
|
-
callCounts.set(sig, prev + 1);
|
|
119
|
-
if (prev >= MAX_SAME_TOOL_CALLS) {
|
|
120
|
-
const skip = "Skipped — already executed this action.";
|
|
121
|
-
onEvent({ type: "tool_result", toolCallId: tc.id, name: tc.name, result: skip });
|
|
122
|
-
context.push({ role: "tool", content: skip, toolCallId: tc.id, name: tc.name });
|
|
123
|
-
stopAfterBatch = true;
|
|
124
|
-
continue;
|
|
125
|
-
}
|
|
126
|
-
const result = await executeTool(tc.name, args, workdir);
|
|
127
|
-
onEvent({ type: "tool_result", toolCallId: tc.id, name: tc.name, result });
|
|
128
|
-
context.push({
|
|
129
|
-
role: "tool",
|
|
130
|
-
content: result,
|
|
131
|
-
toolCallId: tc.id,
|
|
132
|
-
name: tc.name,
|
|
133
|
-
});
|
|
134
|
-
if (!result.startsWith("Error:") && (tc.name === "write" || tc.name === "edit")) {
|
|
135
|
-
stopAfterBatch = true;
|
|
186
|
+
else {
|
|
187
|
+
onEvent({ type: "turn_end" });
|
|
136
188
|
}
|
|
189
|
+
break;
|
|
137
190
|
}
|
|
191
|
+
const stopAfterBatch = await runToolBatch(uniqueCalls, context, workdir, callCounts, onEvent, onPermissionRequest);
|
|
138
192
|
if (stopAfterBatch) {
|
|
139
193
|
finishGracefully(context, content, onEvent);
|
|
140
194
|
break;
|
|
@@ -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.", "Dev servers (npm run dev, next dev) start in the background via bash and return a localhost URL.", "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.", "Dev servers (npm run dev) start in the background via bash and return a localhost URL.");
|
|
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);
|
|
@@ -81,7 +84,9 @@ export async function executeEdit(args, workdir = DEFAULT_WORKDIR) {
|
|
|
81
84
|
}
|
|
82
85
|
export const bashTool = {
|
|
83
86
|
name: "bash",
|
|
84
|
-
description:
|
|
87
|
+
description: osPlatform() === "win32"
|
|
88
|
+
? "Run a PowerShell command. Dev servers (npm run dev) start in background. Chain with ; on Windows PowerShell 5."
|
|
89
|
+
: "Run a bash command. Dev servers (npm run dev) start in background and return a URL.",
|
|
85
90
|
parameters: {
|
|
86
91
|
type: "object",
|
|
87
92
|
properties: {
|
|
@@ -92,21 +97,10 @@ export const bashTool = {
|
|
|
92
97
|
},
|
|
93
98
|
};
|
|
94
99
|
export async function executeBash(args, workdir = DEFAULT_WORKDIR) {
|
|
95
|
-
const
|
|
96
|
-
const
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
const { stdout, stderr } = await execAsync(args.command, {
|
|
100
|
-
cwd: workdir,
|
|
101
|
-
timeout: 120000,
|
|
102
|
-
maxBuffer: 1024 * 1024,
|
|
103
|
-
});
|
|
104
|
-
const out = stdout + (stderr ? `\n${stderr}` : "");
|
|
105
|
-
return out.trim() || "(no output)";
|
|
106
|
-
}
|
|
107
|
-
catch (err) {
|
|
108
|
-
const e = err;
|
|
109
|
-
const out = (e.stdout ?? "") + (e.stderr ? `\n${e.stderr}` : "");
|
|
110
|
-
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})`;
|
|
111
104
|
}
|
|
105
|
+
return result;
|
|
112
106
|
}
|