omni-agent-cli 2.0.0
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 +161 -0
- package/index.js +364 -0
- package/package.json +30 -0
- package/src/agent.js +183 -0
- package/src/config.js +338 -0
- package/src/model-fetcher.js +83 -0
- package/src/providers.js +232 -0
- package/src/stats.js +65 -0
- package/src/tools.js +366 -0
- package/src/ui.js +320 -0
package/src/providers.js
ADDED
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import axios from 'axios';
|
|
2
|
+
import { TOOL_DEFINITIONS } from './tools.js';
|
|
3
|
+
|
|
4
|
+
// ─── Providers that DON'T support parallel_tool_calls param ──────────────────
|
|
5
|
+
const NO_PARALLEL = new Set([
|
|
6
|
+
'groq', 'together', 'ionet', 'qwen', 'mistral',
|
|
7
|
+
'cohere', 'ollama', 'custom', 'deepseek',
|
|
8
|
+
]);
|
|
9
|
+
|
|
10
|
+
// ─── Providers that DON'T support tools/function-calling at all ───────────────
|
|
11
|
+
const NO_TOOLS = new Set([
|
|
12
|
+
// add provider keys here if needed
|
|
13
|
+
]);
|
|
14
|
+
|
|
15
|
+
// ─── Providers that use max_completion_tokens instead of max_tokens ───────────
|
|
16
|
+
const MAX_COMPLETION_TOKENS = new Set(['openai']);
|
|
17
|
+
|
|
18
|
+
// ─── Models that don't support temperature ────────────────────────────────────
|
|
19
|
+
const NO_TEMPERATURE_MODELS = ['o1', 'o1-mini', 'o1-preview', 'o3', 'o3-mini', 'o4-mini'];
|
|
20
|
+
|
|
21
|
+
function toOpenAITools(tools) {
|
|
22
|
+
return tools.map((t) => ({
|
|
23
|
+
type: 'function',
|
|
24
|
+
function: { name: t.name, description: t.description, parameters: t.parameters },
|
|
25
|
+
}));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function toAnthropicTools(tools) {
|
|
29
|
+
return tools.map((t) => ({
|
|
30
|
+
name: t.name,
|
|
31
|
+
description: t.description,
|
|
32
|
+
input_schema: t.parameters,
|
|
33
|
+
}));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// ─── Parse detailed error from provider response ──────────────────────────────
|
|
37
|
+
function parseProviderError(err, providerKey) {
|
|
38
|
+
if (!err.response) {
|
|
39
|
+
return `Network error: ${err.message}`;
|
|
40
|
+
}
|
|
41
|
+
const status = err.response.status;
|
|
42
|
+
const data = err.response.data;
|
|
43
|
+
|
|
44
|
+
const detail =
|
|
45
|
+
data?.error?.message ||
|
|
46
|
+
data?.error?.msg ||
|
|
47
|
+
data?.message ||
|
|
48
|
+
data?.detail ||
|
|
49
|
+
(typeof data === 'string' ? data.slice(0, 200) : null) ||
|
|
50
|
+
err.message;
|
|
51
|
+
|
|
52
|
+
if (status === 400) return `Bad request (400): ${detail}\n → Check model name, API key format, or tool support for "${providerKey}"`;
|
|
53
|
+
if (status === 401) return `Unauthorized (401): Invalid API key for "${providerKey}"`;
|
|
54
|
+
if (status === 403) return `Forbidden (403): ${detail}`;
|
|
55
|
+
if (status === 404) return `Not found (404): Model or endpoint not found — ${detail}`;
|
|
56
|
+
if (status === 422) return `Invalid params (422): ${detail}`;
|
|
57
|
+
if (status === 429) return `Rate limited (429): Too many requests — wait and retry`;
|
|
58
|
+
if (status === 500) return `Server error (500): Provider side issue — ${detail}`;
|
|
59
|
+
return `HTTP ${status}: ${detail}`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ─── OpenAI-compatible call ───────────────────────────────────────────────────
|
|
63
|
+
async function callOpenAI(messages, config) {
|
|
64
|
+
const key = config.providerKey || 'custom';
|
|
65
|
+
|
|
66
|
+
const headers = {
|
|
67
|
+
'Content-Type': 'application/json',
|
|
68
|
+
Authorization: `Bearer ${config.apiKey}`,
|
|
69
|
+
};
|
|
70
|
+
if (key === 'openrouter') {
|
|
71
|
+
headers['HTTP-Referer'] = 'https://github.com/omni-agent';
|
|
72
|
+
headers['X-Title'] = 'OmniAgent';
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const noTools = NO_TOOLS.has(key);
|
|
76
|
+
const noTemp = NO_TEMPERATURE_MODELS.some(m => config.model?.startsWith(m));
|
|
77
|
+
const maxKey = MAX_COMPLETION_TOKENS.has(key) ? 'max_completion_tokens' : 'max_tokens';
|
|
78
|
+
|
|
79
|
+
const body = {
|
|
80
|
+
model: config.model,
|
|
81
|
+
messages,
|
|
82
|
+
[maxKey]: config.maxTokens || 8192,
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
// Temperature — skip for reasoning models
|
|
86
|
+
if (!noTemp) body.temperature = config.temperature ?? 0.3;
|
|
87
|
+
|
|
88
|
+
// Tools
|
|
89
|
+
if (!noTools) {
|
|
90
|
+
body.tools = toOpenAITools(TOOL_DEFINITIONS);
|
|
91
|
+
body.tool_choice = 'auto';
|
|
92
|
+
// Only add parallel_tool_calls for providers that support it
|
|
93
|
+
if (!NO_PARALLEL.has(key)) {
|
|
94
|
+
body.parallel_tool_calls = true;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
let response;
|
|
99
|
+
try {
|
|
100
|
+
response = await axios.post(`${config.baseURL}/chat/completions`, body, {
|
|
101
|
+
headers,
|
|
102
|
+
timeout: 120000,
|
|
103
|
+
});
|
|
104
|
+
} catch (err) {
|
|
105
|
+
throw new Error(parseProviderError(err, key));
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const msg = response.data.choices?.[0]?.message || {};
|
|
109
|
+
const usage = response.data.usage || {};
|
|
110
|
+
|
|
111
|
+
// Parse tool_calls safely
|
|
112
|
+
const toolCalls = [];
|
|
113
|
+
for (const tc of msg.tool_calls || []) {
|
|
114
|
+
try {
|
|
115
|
+
toolCalls.push({
|
|
116
|
+
id: tc.id || `call_${Date.now()}_${Math.random().toString(36).slice(2)}`,
|
|
117
|
+
name: tc.function.name,
|
|
118
|
+
input: JSON.parse(tc.function.arguments),
|
|
119
|
+
});
|
|
120
|
+
} catch {
|
|
121
|
+
// skip malformed tool call
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return {
|
|
126
|
+
content: msg.content || '',
|
|
127
|
+
toolCalls,
|
|
128
|
+
usage: {
|
|
129
|
+
inputTokens: usage.prompt_tokens || 0,
|
|
130
|
+
outputTokens: usage.completion_tokens || 0,
|
|
131
|
+
},
|
|
132
|
+
stopReason: response.data.choices?.[0]?.finish_reason || 'stop',
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// ─── Anthropic call ───────────────────────────────────────────────────────────
|
|
137
|
+
async function callAnthropic(messages, config) {
|
|
138
|
+
let system = '';
|
|
139
|
+
const filtered = messages.filter((m) => {
|
|
140
|
+
if (m.role === 'system') { system = m.content; return false; }
|
|
141
|
+
return true;
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
const headers = {
|
|
145
|
+
'Content-Type': 'application/json',
|
|
146
|
+
'x-api-key': config.apiKey,
|
|
147
|
+
'anthropic-version': '2023-06-01',
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
const body = {
|
|
151
|
+
model: config.model,
|
|
152
|
+
system,
|
|
153
|
+
messages: filtered,
|
|
154
|
+
tools: toAnthropicTools(TOOL_DEFINITIONS),
|
|
155
|
+
max_tokens: config.maxTokens || 8192,
|
|
156
|
+
temperature: config.temperature ?? 0.3,
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
let response;
|
|
160
|
+
try {
|
|
161
|
+
response = await axios.post(`${config.baseURL}/v1/messages`, body, {
|
|
162
|
+
headers,
|
|
163
|
+
timeout: 120000,
|
|
164
|
+
});
|
|
165
|
+
} catch (err) {
|
|
166
|
+
throw new Error(parseProviderError(err, 'anthropic'));
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const data = response.data;
|
|
170
|
+
let textContent = '';
|
|
171
|
+
const toolCalls = [];
|
|
172
|
+
|
|
173
|
+
for (const block of data.content || []) {
|
|
174
|
+
if (block.type === 'text') textContent += block.text;
|
|
175
|
+
if (block.type === 'tool_use') toolCalls.push({ id: block.id, name: block.name, input: block.input });
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return {
|
|
179
|
+
content: textContent,
|
|
180
|
+
toolCalls,
|
|
181
|
+
usage: {
|
|
182
|
+
inputTokens: data.usage?.input_tokens || 0,
|
|
183
|
+
outputTokens: data.usage?.output_tokens || 0,
|
|
184
|
+
},
|
|
185
|
+
stopReason: data.stop_reason,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// ─── Build messages ────────────────────────────────────────────────────────────
|
|
190
|
+
export function buildToolResultMessage(format, toolCalls, results) {
|
|
191
|
+
if (format === 'anthropic') {
|
|
192
|
+
return {
|
|
193
|
+
role: 'user',
|
|
194
|
+
content: toolCalls.map((tc, i) => ({
|
|
195
|
+
type: 'tool_result',
|
|
196
|
+
tool_use_id: tc.id,
|
|
197
|
+
content: String(results[i] ?? ''),
|
|
198
|
+
})),
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
// OpenAI: tool messages WITHOUT name field (some providers reject it)
|
|
202
|
+
return toolCalls.map((tc, i) => ({
|
|
203
|
+
role: 'tool',
|
|
204
|
+
tool_call_id: tc.id,
|
|
205
|
+
content: String(results[i] ?? ''),
|
|
206
|
+
}));
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export function buildAssistantToolUseMessage(format, response) {
|
|
210
|
+
if (format === 'anthropic') {
|
|
211
|
+
const content = [];
|
|
212
|
+
if (response.content) content.push({ type: 'text', text: response.content });
|
|
213
|
+
for (const tc of response.toolCalls) {
|
|
214
|
+
content.push({ type: 'tool_use', id: tc.id, name: tc.name, input: tc.input });
|
|
215
|
+
}
|
|
216
|
+
return { role: 'assistant', content };
|
|
217
|
+
}
|
|
218
|
+
return {
|
|
219
|
+
role: 'assistant',
|
|
220
|
+
content: response.content || null,
|
|
221
|
+
tool_calls: response.toolCalls.map((tc) => ({
|
|
222
|
+
id: tc.id,
|
|
223
|
+
type: 'function',
|
|
224
|
+
function: { name: tc.name, arguments: JSON.stringify(tc.input) },
|
|
225
|
+
})),
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export async function callProvider(messages, config) {
|
|
230
|
+
if (config.format === 'anthropic') return callAnthropic(messages, config);
|
|
231
|
+
return callOpenAI(messages, config);
|
|
232
|
+
}
|
package/src/stats.js
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
export class SessionStats {
|
|
2
|
+
constructor() {
|
|
3
|
+
this.startTime = Date.now();
|
|
4
|
+
this.messages = { user: 0, assistant: 0 };
|
|
5
|
+
this.tokens = { input: 0, output: 0 };
|
|
6
|
+
this.toolCalls = {};
|
|
7
|
+
this.apiRequests = 0;
|
|
8
|
+
this.errors = 0;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
addMessage(role) {
|
|
12
|
+
this.messages[role] = (this.messages[role] || 0) + 1;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
addTokens(input, output) {
|
|
16
|
+
this.tokens.input += input;
|
|
17
|
+
this.tokens.output += output;
|
|
18
|
+
this.apiRequests++;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
addToolCall(toolName) {
|
|
22
|
+
this.toolCalls[toolName] = (this.toolCalls[toolName] || 0) + 1;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
addError() {
|
|
26
|
+
this.errors++;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
getStats() {
|
|
30
|
+
const elapsed = Date.now() - this.startTime;
|
|
31
|
+
const mins = Math.floor(elapsed / 60000);
|
|
32
|
+
const secs = Math.floor((elapsed % 60000) / 1000);
|
|
33
|
+
const totalMessages = this.messages.user + this.messages.assistant;
|
|
34
|
+
const totalTokens = this.tokens.input + this.tokens.output;
|
|
35
|
+
|
|
36
|
+
// Cost estimation (rough, per 1M tokens)
|
|
37
|
+
const INPUT_COST = 3.0; // $3 per 1M input tokens (rough average)
|
|
38
|
+
const OUTPUT_COST = 15.0; // $15 per 1M output tokens (rough average)
|
|
39
|
+
const estimatedCost = (
|
|
40
|
+
(this.tokens.input / 1_000_000) * INPUT_COST +
|
|
41
|
+
(this.tokens.output / 1_000_000) * OUTPUT_COST
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
const toolCallsList = Object.entries(this.toolCalls).sort((a, b) => b[1] - a[1]);
|
|
45
|
+
const totalToolCalls = toolCallsList.reduce((acc, [, v]) => acc + v, 0);
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
duration: `${mins}m ${secs}s`,
|
|
49
|
+
durationMs: elapsed,
|
|
50
|
+
messages: this.messages,
|
|
51
|
+
totalMessages,
|
|
52
|
+
tokens: this.tokens,
|
|
53
|
+
totalTokens,
|
|
54
|
+
apiRequests: this.apiRequests,
|
|
55
|
+
toolCalls: this.toolCalls,
|
|
56
|
+
totalToolCalls,
|
|
57
|
+
toolCallsList,
|
|
58
|
+
errors: this.errors,
|
|
59
|
+
estimatedCost: estimatedCost.toFixed(4),
|
|
60
|
+
tokensPerRequest: this.apiRequests > 0
|
|
61
|
+
? Math.round(totalTokens / this.apiRequests)
|
|
62
|
+
: 0,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
}
|
package/src/tools.js
ADDED
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
import {
|
|
2
|
+
readFileSync, writeFileSync, appendFileSync,
|
|
3
|
+
unlinkSync, mkdirSync, readdirSync, statSync,
|
|
4
|
+
copyFileSync, renameSync, existsSync,
|
|
5
|
+
} from 'fs';
|
|
6
|
+
import { execSync } from 'child_process';
|
|
7
|
+
import path from 'path';
|
|
8
|
+
import { glob } from 'glob';
|
|
9
|
+
|
|
10
|
+
// ─── Tool Definitions (OpenAI format, converted for Anthropic by provider) ───
|
|
11
|
+
|
|
12
|
+
export const TOOL_DEFINITIONS = [
|
|
13
|
+
{
|
|
14
|
+
name: 'list_directory',
|
|
15
|
+
description: 'List files and directories in a given path. Shows name, type, size, and modification time.',
|
|
16
|
+
parameters: {
|
|
17
|
+
type: 'object',
|
|
18
|
+
properties: {
|
|
19
|
+
path: { type: 'string', description: 'Directory path to list. Use "." for current working directory.' },
|
|
20
|
+
show_hidden: { type: 'boolean', description: 'Show hidden files (starting with dot). Default false.' },
|
|
21
|
+
},
|
|
22
|
+
required: ['path'],
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
name: 'read_file',
|
|
27
|
+
description: 'Read the contents of a file. Returns the full text content.',
|
|
28
|
+
parameters: {
|
|
29
|
+
type: 'object',
|
|
30
|
+
properties: {
|
|
31
|
+
path: { type: 'string', description: 'Absolute or relative path to the file.' },
|
|
32
|
+
encoding: { type: 'string', description: 'File encoding, default utf8. Use "base64" for binary files.' },
|
|
33
|
+
},
|
|
34
|
+
required: ['path'],
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
name: 'write_file',
|
|
39
|
+
description: 'Write content to a file. Creates the file if it does not exist, overwrites if it does.',
|
|
40
|
+
parameters: {
|
|
41
|
+
type: 'object',
|
|
42
|
+
properties: {
|
|
43
|
+
path: { type: 'string', description: 'Path to the file to write.' },
|
|
44
|
+
content: { type: 'string', description: 'Content to write to the file.' },
|
|
45
|
+
create_dirs: { type: 'boolean', description: 'Automatically create parent directories if missing. Default true.' },
|
|
46
|
+
},
|
|
47
|
+
required: ['path', 'content'],
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
name: 'append_to_file',
|
|
52
|
+
description: 'Append text to an existing file (or create it if missing).',
|
|
53
|
+
parameters: {
|
|
54
|
+
type: 'object',
|
|
55
|
+
properties: {
|
|
56
|
+
path: { type: 'string', description: 'Path to the file.' },
|
|
57
|
+
content: { type: 'string', description: 'Content to append.' },
|
|
58
|
+
},
|
|
59
|
+
required: ['path', 'content'],
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
name: 'patch_file',
|
|
64
|
+
description: 'Replace a specific string/block of text in a file with new content. Useful for editing specific parts without rewriting the whole file.',
|
|
65
|
+
parameters: {
|
|
66
|
+
type: 'object',
|
|
67
|
+
properties: {
|
|
68
|
+
path: { type: 'string', description: 'Path to the file.' },
|
|
69
|
+
old_text: { type: 'string', description: 'The exact text to find and replace. Must match exactly.' },
|
|
70
|
+
new_text: { type: 'string', description: 'The replacement text.' },
|
|
71
|
+
},
|
|
72
|
+
required: ['path', 'old_text', 'new_text'],
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
name: 'delete_path',
|
|
77
|
+
description: 'Delete a file or directory (directories are deleted recursively).',
|
|
78
|
+
parameters: {
|
|
79
|
+
type: 'object',
|
|
80
|
+
properties: {
|
|
81
|
+
path: { type: 'string', description: 'Path to file or directory to delete.' },
|
|
82
|
+
},
|
|
83
|
+
required: ['path'],
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
name: 'copy_path',
|
|
88
|
+
description: 'Copy a file or directory to a new location.',
|
|
89
|
+
parameters: {
|
|
90
|
+
type: 'object',
|
|
91
|
+
properties: {
|
|
92
|
+
source: { type: 'string', description: 'Source file path.' },
|
|
93
|
+
destination: { type: 'string', description: 'Destination path.' },
|
|
94
|
+
},
|
|
95
|
+
required: ['source', 'destination'],
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
name: 'move_path',
|
|
100
|
+
description: 'Move or rename a file or directory.',
|
|
101
|
+
parameters: {
|
|
102
|
+
type: 'object',
|
|
103
|
+
properties: {
|
|
104
|
+
source: { type: 'string', description: 'Source path.' },
|
|
105
|
+
destination: { type: 'string', description: 'Destination path.' },
|
|
106
|
+
},
|
|
107
|
+
required: ['source', 'destination'],
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
name: 'create_directory',
|
|
112
|
+
description: 'Create a directory and all necessary parent directories.',
|
|
113
|
+
parameters: {
|
|
114
|
+
type: 'object',
|
|
115
|
+
properties: {
|
|
116
|
+
path: { type: 'string', description: 'Path of directory to create.' },
|
|
117
|
+
},
|
|
118
|
+
required: ['path'],
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
name: 'get_file_info',
|
|
123
|
+
description: 'Get detailed metadata about a file or directory (size, permissions, dates, type).',
|
|
124
|
+
parameters: {
|
|
125
|
+
type: 'object',
|
|
126
|
+
properties: {
|
|
127
|
+
path: { type: 'string', description: 'Path to the file or directory.' },
|
|
128
|
+
},
|
|
129
|
+
required: ['path'],
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
name: 'find_files',
|
|
134
|
+
description: 'Find files matching a glob pattern recursively.',
|
|
135
|
+
parameters: {
|
|
136
|
+
type: 'object',
|
|
137
|
+
properties: {
|
|
138
|
+
pattern: { type: 'string', description: 'Glob pattern e.g. "**/*.js", "src/**/*.ts", "*.json"' },
|
|
139
|
+
cwd: { type: 'string', description: 'Directory to search in. Defaults to working directory.' },
|
|
140
|
+
ignore: { type: 'array', items: { type: 'string' }, description: 'Patterns to ignore, e.g. ["node_modules/**", ".git/**"]' },
|
|
141
|
+
},
|
|
142
|
+
required: ['pattern'],
|
|
143
|
+
},
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
name: 'search_in_files',
|
|
147
|
+
description: 'Search for text/regex in files. Returns matching file paths and lines.',
|
|
148
|
+
parameters: {
|
|
149
|
+
type: 'object',
|
|
150
|
+
properties: {
|
|
151
|
+
query: { type: 'string', description: 'Text or regex pattern to search for.' },
|
|
152
|
+
path: { type: 'string', description: 'Directory or file to search in.' },
|
|
153
|
+
file_pattern: { type: 'string', description: 'File extension filter e.g. "*.js". Default all files.' },
|
|
154
|
+
case_sensitive: { type: 'boolean', description: 'Case sensitive search. Default false.' },
|
|
155
|
+
max_results: { type: 'number', description: 'Max results to return. Default 50.' },
|
|
156
|
+
},
|
|
157
|
+
required: ['query', 'path'],
|
|
158
|
+
},
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
name: 'execute_command',
|
|
162
|
+
description: 'Execute a shell command in the working directory. Returns stdout and stderr.',
|
|
163
|
+
parameters: {
|
|
164
|
+
type: 'object',
|
|
165
|
+
properties: {
|
|
166
|
+
command: { type: 'string', description: 'Shell command to execute.' },
|
|
167
|
+
cwd: { type: 'string', description: 'Working directory for the command.' },
|
|
168
|
+
timeout: { type: 'number', description: 'Timeout in milliseconds. Default 30000 (30s).' },
|
|
169
|
+
},
|
|
170
|
+
required: ['command'],
|
|
171
|
+
},
|
|
172
|
+
},
|
|
173
|
+
];
|
|
174
|
+
|
|
175
|
+
// ─── Tool Executors ───────────────────────────────────────────────────────────
|
|
176
|
+
|
|
177
|
+
function resolvePath(p, workdir) {
|
|
178
|
+
if (path.isAbsolute(p)) return p;
|
|
179
|
+
return path.resolve(workdir, p);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function formatSize(bytes) {
|
|
183
|
+
if (bytes < 1024) return `${bytes}B`;
|
|
184
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
|
|
185
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function deleteDirRecursive(p) {
|
|
189
|
+
if (existsSync(p)) {
|
|
190
|
+
const stat = statSync(p);
|
|
191
|
+
if (stat.isDirectory()) {
|
|
192
|
+
for (const entry of readdirSync(p)) {
|
|
193
|
+
deleteDirRecursive(path.join(p, entry));
|
|
194
|
+
}
|
|
195
|
+
import('fs').then(({ rmdirSync }) => rmdirSync(p));
|
|
196
|
+
} else {
|
|
197
|
+
unlinkSync(p);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export async function executeTool(name, input, workdir) {
|
|
203
|
+
const wd = workdir || process.cwd();
|
|
204
|
+
|
|
205
|
+
try {
|
|
206
|
+
switch (name) {
|
|
207
|
+
case 'list_directory': {
|
|
208
|
+
const dirPath = resolvePath(input.path, wd);
|
|
209
|
+
const entries = readdirSync(dirPath);
|
|
210
|
+
const results = [];
|
|
211
|
+
for (const entry of entries) {
|
|
212
|
+
if (!input.show_hidden && entry.startsWith('.')) continue;
|
|
213
|
+
try {
|
|
214
|
+
const fullPath = path.join(dirPath, entry);
|
|
215
|
+
const stat = statSync(fullPath);
|
|
216
|
+
results.push({
|
|
217
|
+
name: entry,
|
|
218
|
+
type: stat.isDirectory() ? 'dir' : 'file',
|
|
219
|
+
size: stat.isDirectory() ? '-' : formatSize(stat.size),
|
|
220
|
+
modified: stat.mtime.toISOString().slice(0, 16).replace('T', ' '),
|
|
221
|
+
});
|
|
222
|
+
} catch {}
|
|
223
|
+
}
|
|
224
|
+
results.sort((a, b) => {
|
|
225
|
+
if (a.type !== b.type) return a.type === 'dir' ? -1 : 1;
|
|
226
|
+
return a.name.localeCompare(b.name);
|
|
227
|
+
});
|
|
228
|
+
const lines = results.map((e) =>
|
|
229
|
+
`${e.type === 'dir' ? '📁' : '📄'} ${e.name.padEnd(40)} ${e.size.padStart(8)} ${e.modified}`
|
|
230
|
+
);
|
|
231
|
+
return `Directory: ${dirPath}\n${'─'.repeat(72)}\n${lines.join('\n')}\n\nTotal: ${results.length} items`;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
case 'read_file': {
|
|
235
|
+
const filePath = resolvePath(input.path, wd);
|
|
236
|
+
const encoding = input.encoding || 'utf8';
|
|
237
|
+
const content = readFileSync(filePath, encoding);
|
|
238
|
+
const size = statSync(filePath).size;
|
|
239
|
+
return `File: ${filePath} (${formatSize(size)})\n${'─'.repeat(60)}\n${content}`;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
case 'write_file': {
|
|
243
|
+
const filePath = resolvePath(input.path, wd);
|
|
244
|
+
if (input.create_dirs !== false) {
|
|
245
|
+
mkdirSync(path.dirname(filePath), { recursive: true });
|
|
246
|
+
}
|
|
247
|
+
writeFileSync(filePath, input.content, 'utf8');
|
|
248
|
+
return `✅ Written ${formatSize(Buffer.byteLength(input.content))} to: ${filePath}`;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
case 'append_to_file': {
|
|
252
|
+
const filePath = resolvePath(input.path, wd);
|
|
253
|
+
appendFileSync(filePath, input.content, 'utf8');
|
|
254
|
+
return `✅ Appended ${Buffer.byteLength(input.content)} bytes to: ${filePath}`;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
case 'patch_file': {
|
|
258
|
+
const filePath = resolvePath(input.path, wd);
|
|
259
|
+
let content = readFileSync(filePath, 'utf8');
|
|
260
|
+
if (!content.includes(input.old_text)) {
|
|
261
|
+
return `❌ Text not found in file. Make sure old_text matches exactly (check whitespace/newlines).`;
|
|
262
|
+
}
|
|
263
|
+
content = content.replace(input.old_text, input.new_text);
|
|
264
|
+
writeFileSync(filePath, content, 'utf8');
|
|
265
|
+
return `✅ Patched: ${filePath}`;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
case 'delete_path': {
|
|
269
|
+
const targetPath = resolvePath(input.path, wd);
|
|
270
|
+
if (!existsSync(targetPath)) return `⚠️ Path does not exist: ${targetPath}`;
|
|
271
|
+
const stat = statSync(targetPath);
|
|
272
|
+
if (stat.isDirectory()) {
|
|
273
|
+
execSync(`rm -rf "${targetPath}"`);
|
|
274
|
+
} else {
|
|
275
|
+
unlinkSync(targetPath);
|
|
276
|
+
}
|
|
277
|
+
return `✅ Deleted: ${targetPath}`;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
case 'copy_path': {
|
|
281
|
+
const src = resolvePath(input.source, wd);
|
|
282
|
+
const dst = resolvePath(input.destination, wd);
|
|
283
|
+
mkdirSync(path.dirname(dst), { recursive: true });
|
|
284
|
+
copyFileSync(src, dst);
|
|
285
|
+
return `✅ Copied: ${src} → ${dst}`;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
case 'move_path': {
|
|
289
|
+
const src = resolvePath(input.source, wd);
|
|
290
|
+
const dst = resolvePath(input.destination, wd);
|
|
291
|
+
mkdirSync(path.dirname(dst), { recursive: true });
|
|
292
|
+
renameSync(src, dst);
|
|
293
|
+
return `✅ Moved: ${src} → ${dst}`;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
case 'create_directory': {
|
|
297
|
+
const dirPath = resolvePath(input.path, wd);
|
|
298
|
+
mkdirSync(dirPath, { recursive: true });
|
|
299
|
+
return `✅ Created directory: ${dirPath}`;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
case 'get_file_info': {
|
|
303
|
+
const filePath = resolvePath(input.path, wd);
|
|
304
|
+
if (!existsSync(filePath)) return `❌ Path does not exist: ${filePath}`;
|
|
305
|
+
const stat = statSync(filePath);
|
|
306
|
+
return JSON.stringify({
|
|
307
|
+
path: filePath,
|
|
308
|
+
type: stat.isDirectory() ? 'directory' : 'file',
|
|
309
|
+
size: stat.size,
|
|
310
|
+
sizeHuman: formatSize(stat.size),
|
|
311
|
+
created: stat.birthtime.toISOString(),
|
|
312
|
+
modified: stat.mtime.toISOString(),
|
|
313
|
+
accessed: stat.atime.toISOString(),
|
|
314
|
+
permissions: (stat.mode & 0o777).toString(8),
|
|
315
|
+
isSymlink: stat.isSymbolicLink(),
|
|
316
|
+
}, null, 2);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
case 'find_files': {
|
|
320
|
+
const searchCwd = input.cwd ? resolvePath(input.cwd, wd) : wd;
|
|
321
|
+
const ignore = input.ignore || ['node_modules/**', '.git/**', 'dist/**', 'build/**'];
|
|
322
|
+
const files = await glob(input.pattern, { cwd: searchCwd, ignore, dot: false });
|
|
323
|
+
if (files.length === 0) return `No files found matching: ${input.pattern}`;
|
|
324
|
+
return `Found ${files.length} file(s):\n${files.sort().map((f) => ` ${f}`).join('\n')}`;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
case 'search_in_files': {
|
|
328
|
+
const searchPath = resolvePath(input.path, wd);
|
|
329
|
+
const flags = input.case_sensitive ? '' : '-i';
|
|
330
|
+
const filePattern = input.file_pattern ? `--include="${input.file_pattern}"` : '';
|
|
331
|
+
const maxR = input.max_results || 50;
|
|
332
|
+
const cmd = `grep -rn ${flags} ${filePattern} "${input.query}" "${searchPath}" 2>/dev/null | head -${maxR}`;
|
|
333
|
+
try {
|
|
334
|
+
const output = execSync(cmd, { encoding: 'utf8', timeout: 10000 });
|
|
335
|
+
const lines = output.trim().split('\n').filter(Boolean);
|
|
336
|
+
return lines.length > 0
|
|
337
|
+
? `Found ${lines.length} match(es):\n${lines.join('\n')}`
|
|
338
|
+
: `No matches found for: ${input.query}`;
|
|
339
|
+
} catch {
|
|
340
|
+
return `No matches found for: ${input.query}`;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
case 'execute_command': {
|
|
345
|
+
const cmdWd = input.cwd ? resolvePath(input.cwd, wd) : wd;
|
|
346
|
+
const timeout = input.timeout || 30000;
|
|
347
|
+
try {
|
|
348
|
+
const output = execSync(input.command, {
|
|
349
|
+
cwd: cmdWd,
|
|
350
|
+
encoding: 'utf8',
|
|
351
|
+
timeout,
|
|
352
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
353
|
+
});
|
|
354
|
+
return output || '(command completed with no output)';
|
|
355
|
+
} catch (err) {
|
|
356
|
+
return `Exit code ${err.status || 1}:\nSTDOUT: ${err.stdout || ''}\nSTDERR: ${err.stderr || err.message}`;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
default:
|
|
361
|
+
return `❌ Unknown tool: ${name}`;
|
|
362
|
+
}
|
|
363
|
+
} catch (err) {
|
|
364
|
+
return `❌ Tool error: ${err.message}`;
|
|
365
|
+
}
|
|
366
|
+
}
|