termux-dev 1.0.2
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/LICENSE +21 -0
- package/README.md +290 -0
- package/assets/banner.svg +33 -0
- package/assets/preview.png +0 -0
- package/bin/devx.js +2 -0
- package/dist/cli/clipboard.js +136 -0
- package/dist/cli/files.js +93 -0
- package/dist/cli/index.js +1506 -0
- package/dist/cli/markdown.js +147 -0
- package/dist/cli/prompt.js +553 -0
- package/dist/cli/providers.js +892 -0
- package/dist/cli/server.js +137 -0
- package/dist/cli/updater.js +245 -0
- package/dist/core/history.js +121 -0
- package/dist/core/loop.js +164 -0
- package/dist/core/memory.js +68 -0
- package/dist/core/models.js +72 -0
- package/dist/core/pricing.js +65 -0
- package/dist/core/session.js +129 -0
- package/dist/core/snapshot.js +88 -0
- package/dist/core/types.js +1 -0
- package/dist/permissions/guard.js +104 -0
- package/dist/prompts/builder.js +69 -0
- package/dist/providers/index.js +7 -0
- package/dist/providers/openai.js +318 -0
- package/dist/tools/bash.js +51 -0
- package/dist/tools/diagnostics.js +63 -0
- package/dist/tools/fs.js +185 -0
- package/dist/tools/index.js +17 -0
- package/dist/tools/packages.js +80 -0
- package/dist/tools/plan.js +52 -0
- package/dist/tools/questions.js +101 -0
- package/dist/tools/search.js +90 -0
- package/dist/tools/web.js +155 -0
- package/package.json +64 -0
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
import { calculateCost } from '../core/pricing.js';
|
|
2
|
+
export class OpenAIProvider {
|
|
3
|
+
baseUrl;
|
|
4
|
+
apiKey;
|
|
5
|
+
model;
|
|
6
|
+
id = 'openai-compatible';
|
|
7
|
+
constructor(baseUrl, apiKey, model) {
|
|
8
|
+
this.baseUrl = baseUrl;
|
|
9
|
+
this.apiKey = apiKey;
|
|
10
|
+
this.model = model;
|
|
11
|
+
}
|
|
12
|
+
buildPayload(request, stream) {
|
|
13
|
+
const totalMsgs = request.messages.length;
|
|
14
|
+
let lastImageMsgIdx = -1;
|
|
15
|
+
for (let i = totalMsgs - 1; i >= 0; i--) {
|
|
16
|
+
if (request.messages[i].images && request.messages[i].images.length > 0) {
|
|
17
|
+
lastImageMsgIdx = i;
|
|
18
|
+
break;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
const payload = {
|
|
22
|
+
model: this.model,
|
|
23
|
+
stream,
|
|
24
|
+
messages: request.messages.map((m, idx) => {
|
|
25
|
+
let content = m.content || "";
|
|
26
|
+
if (m.images && m.images.length > 0) {
|
|
27
|
+
if (idx === lastImageMsgIdx) {
|
|
28
|
+
content = [
|
|
29
|
+
{ type: 'text', text: m.content || "" },
|
|
30
|
+
...m.images.map(img => ({
|
|
31
|
+
type: 'image_url',
|
|
32
|
+
image_url: { url: img.dataUrl }
|
|
33
|
+
}))
|
|
34
|
+
];
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
const imgRef = m.images.map(img => `[Attached Image: ${img.path}]`).join(' ');
|
|
38
|
+
content = m.content ? `${m.content}\n${imgRef}` : imgRef;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
const msg = { role: m.role, content };
|
|
42
|
+
if (m.name)
|
|
43
|
+
msg.name = m.name;
|
|
44
|
+
if (m.tool_calls)
|
|
45
|
+
msg.tool_calls = m.tool_calls.map(tc => ({
|
|
46
|
+
id: tc.id,
|
|
47
|
+
type: 'function',
|
|
48
|
+
function: { name: tc.name, arguments: JSON.stringify(tc.arguments) }
|
|
49
|
+
}));
|
|
50
|
+
if (m.tool_call_id)
|
|
51
|
+
msg.tool_call_id = m.tool_call_id;
|
|
52
|
+
return msg;
|
|
53
|
+
}),
|
|
54
|
+
};
|
|
55
|
+
if (request.tools && request.tools.length > 0) {
|
|
56
|
+
payload.tools = request.tools.map(t => ({
|
|
57
|
+
type: 'function',
|
|
58
|
+
function: t
|
|
59
|
+
}));
|
|
60
|
+
}
|
|
61
|
+
if (stream) {
|
|
62
|
+
payload.stream_options = { include_usage: true };
|
|
63
|
+
}
|
|
64
|
+
return payload;
|
|
65
|
+
}
|
|
66
|
+
async chat(request) {
|
|
67
|
+
const url = `${this.baseUrl}/chat/completions`;
|
|
68
|
+
const payload = this.buildPayload(request, false);
|
|
69
|
+
const headers = {
|
|
70
|
+
'Content-Type': 'application/json',
|
|
71
|
+
};
|
|
72
|
+
if (this.apiKey) {
|
|
73
|
+
headers['Authorization'] = `Bearer ${this.apiKey}`;
|
|
74
|
+
}
|
|
75
|
+
const res = await fetch(url, {
|
|
76
|
+
method: 'POST',
|
|
77
|
+
headers,
|
|
78
|
+
body: JSON.stringify(payload),
|
|
79
|
+
signal: request.signal
|
|
80
|
+
});
|
|
81
|
+
if (!res.ok) {
|
|
82
|
+
const errText = await res.text();
|
|
83
|
+
let errMsg = errText;
|
|
84
|
+
try {
|
|
85
|
+
const parsed = JSON.parse(errText);
|
|
86
|
+
if (parsed.error?.message) {
|
|
87
|
+
errMsg = parsed.error.message;
|
|
88
|
+
}
|
|
89
|
+
else if (parsed.message) {
|
|
90
|
+
errMsg = parsed.message;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
catch { }
|
|
94
|
+
throw new Error(`Provider HTTP Error ${res.status}: ${errMsg}`);
|
|
95
|
+
}
|
|
96
|
+
const data = await res.json();
|
|
97
|
+
if (data.error) {
|
|
98
|
+
const msg = data.error.message || data.error.code || JSON.stringify(data.error);
|
|
99
|
+
throw new Error(`Provider Error: ${msg}`);
|
|
100
|
+
}
|
|
101
|
+
const choice = data.choices?.[0]?.message || {};
|
|
102
|
+
let usage;
|
|
103
|
+
if (data.usage) {
|
|
104
|
+
const promptTokens = data.usage.prompt_tokens || 0;
|
|
105
|
+
const completionTokens = data.usage.completion_tokens || 0;
|
|
106
|
+
const totalTokens = data.usage.total_tokens || (promptTokens + completionTokens);
|
|
107
|
+
const cost = typeof data.usage.total_cost === 'number'
|
|
108
|
+
? data.usage.total_cost
|
|
109
|
+
: calculateCost(this.model, promptTokens, completionTokens);
|
|
110
|
+
usage = {
|
|
111
|
+
promptTokens,
|
|
112
|
+
completionTokens,
|
|
113
|
+
totalTokens,
|
|
114
|
+
cost
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
return {
|
|
118
|
+
content: choice.content || null,
|
|
119
|
+
toolCalls: choice.tool_calls ? choice.tool_calls.map((tc) => ({
|
|
120
|
+
id: tc.id,
|
|
121
|
+
name: tc.function.name,
|
|
122
|
+
arguments: JSON.parse(tc.function.arguments)
|
|
123
|
+
})) : undefined,
|
|
124
|
+
usage
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
async *chatStream(request) {
|
|
128
|
+
const url = `${this.baseUrl}/chat/completions`;
|
|
129
|
+
const payload = this.buildPayload(request, true);
|
|
130
|
+
const headers = {
|
|
131
|
+
'Content-Type': 'application/json',
|
|
132
|
+
};
|
|
133
|
+
if (this.apiKey) {
|
|
134
|
+
headers['Authorization'] = `Bearer ${this.apiKey}`;
|
|
135
|
+
}
|
|
136
|
+
const res = await fetch(url, {
|
|
137
|
+
method: 'POST',
|
|
138
|
+
headers,
|
|
139
|
+
body: JSON.stringify(payload),
|
|
140
|
+
signal: request.signal
|
|
141
|
+
});
|
|
142
|
+
if (!res.ok) {
|
|
143
|
+
const errText = await res.text();
|
|
144
|
+
let errMsg = errText;
|
|
145
|
+
try {
|
|
146
|
+
const parsed = JSON.parse(errText);
|
|
147
|
+
if (parsed.error?.message) {
|
|
148
|
+
errMsg = parsed.error.message;
|
|
149
|
+
}
|
|
150
|
+
else if (parsed.message) {
|
|
151
|
+
errMsg = parsed.message;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
catch { }
|
|
155
|
+
throw new Error(`Provider HTTP Error ${res.status}: ${errMsg}`);
|
|
156
|
+
}
|
|
157
|
+
const reader = res.body?.getReader();
|
|
158
|
+
if (!reader) {
|
|
159
|
+
return await this.chat(request);
|
|
160
|
+
}
|
|
161
|
+
if (request.signal) {
|
|
162
|
+
request.signal.addEventListener('abort', () => {
|
|
163
|
+
try {
|
|
164
|
+
reader.cancel().catch(() => { });
|
|
165
|
+
}
|
|
166
|
+
catch { }
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
const decoder = new TextDecoder();
|
|
170
|
+
let buffer = '';
|
|
171
|
+
let accumulatedContent = '';
|
|
172
|
+
let accumulatedReasoning = '';
|
|
173
|
+
let inThinkTag = false;
|
|
174
|
+
const toolMap = new Map();
|
|
175
|
+
let usageData = null;
|
|
176
|
+
try {
|
|
177
|
+
while (true) {
|
|
178
|
+
if (request.signal?.aborted)
|
|
179
|
+
break;
|
|
180
|
+
const { done, value } = await reader.read();
|
|
181
|
+
if (done || request.signal?.aborted)
|
|
182
|
+
break;
|
|
183
|
+
buffer += decoder.decode(value, { stream: true });
|
|
184
|
+
const lines = buffer.split('\n');
|
|
185
|
+
buffer = lines.pop() || '';
|
|
186
|
+
for (const line of lines) {
|
|
187
|
+
const trimmed = line.trim();
|
|
188
|
+
if (!trimmed || trimmed.startsWith(':'))
|
|
189
|
+
continue;
|
|
190
|
+
if (trimmed === 'data: [DONE]')
|
|
191
|
+
continue;
|
|
192
|
+
if (trimmed.startsWith('data: ')) {
|
|
193
|
+
try {
|
|
194
|
+
const json = JSON.parse(trimmed.slice(6));
|
|
195
|
+
if (json.error) {
|
|
196
|
+
const errMsg = json.error.message || json.error.code || JSON.stringify(json.error);
|
|
197
|
+
throw new Error(`Provider Stream Error: ${errMsg}`);
|
|
198
|
+
}
|
|
199
|
+
if (json.usage) {
|
|
200
|
+
usageData = json.usage;
|
|
201
|
+
}
|
|
202
|
+
const choice = json.choices?.[0];
|
|
203
|
+
if (!choice)
|
|
204
|
+
continue;
|
|
205
|
+
const delta = choice.delta || {};
|
|
206
|
+
// 1. Check reasoning_content (DeepSeek-R1 / OpenRouter / etc.)
|
|
207
|
+
const reasoningText = delta.reasoning_content || delta.reasoning || '';
|
|
208
|
+
if (reasoningText) {
|
|
209
|
+
accumulatedReasoning += reasoningText;
|
|
210
|
+
yield { type: 'reasoning_delta', delta: reasoningText };
|
|
211
|
+
}
|
|
212
|
+
// 2. Check content (and handle <think> tags if embedded in content)
|
|
213
|
+
if (delta.content) {
|
|
214
|
+
const text = delta.content;
|
|
215
|
+
if (text.includes('<think>')) {
|
|
216
|
+
inThinkTag = true;
|
|
217
|
+
const parts = text.split('<think>');
|
|
218
|
+
if (parts[0]) {
|
|
219
|
+
accumulatedContent += parts[0];
|
|
220
|
+
yield { type: 'content_delta', delta: parts[0] };
|
|
221
|
+
}
|
|
222
|
+
if (parts[1]) {
|
|
223
|
+
accumulatedReasoning += parts[1];
|
|
224
|
+
yield { type: 'reasoning_delta', delta: parts[1] };
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
else if (text.includes('</think>')) {
|
|
228
|
+
inThinkTag = false;
|
|
229
|
+
const parts = text.split('</think>');
|
|
230
|
+
if (parts[0]) {
|
|
231
|
+
accumulatedReasoning += parts[0];
|
|
232
|
+
yield { type: 'reasoning_delta', delta: parts[0] };
|
|
233
|
+
}
|
|
234
|
+
if (parts[1]) {
|
|
235
|
+
accumulatedContent += parts[1];
|
|
236
|
+
yield { type: 'content_delta', delta: parts[1] };
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
else if (inThinkTag) {
|
|
240
|
+
accumulatedReasoning += text;
|
|
241
|
+
yield { type: 'reasoning_delta', delta: text };
|
|
242
|
+
}
|
|
243
|
+
else {
|
|
244
|
+
accumulatedContent += text;
|
|
245
|
+
yield { type: 'content_delta', delta: text };
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
// 3. Accumulate tool calls and yield live progress
|
|
249
|
+
if (delta.tool_calls) {
|
|
250
|
+
for (const tc of delta.tool_calls) {
|
|
251
|
+
const idx = tc.index ?? 0;
|
|
252
|
+
const existing = toolMap.get(idx) || { id: '', name: '', argsStr: '' };
|
|
253
|
+
if (tc.id)
|
|
254
|
+
existing.id = tc.id;
|
|
255
|
+
if (tc.function?.name)
|
|
256
|
+
existing.name += tc.function.name;
|
|
257
|
+
if (tc.function?.arguments)
|
|
258
|
+
existing.argsStr += tc.function.arguments;
|
|
259
|
+
toolMap.set(idx, existing);
|
|
260
|
+
yield {
|
|
261
|
+
type: 'tool_generating',
|
|
262
|
+
name: existing.name || 'tool',
|
|
263
|
+
bytes: existing.argsStr.length
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
catch (e) {
|
|
269
|
+
if (e.message && e.message.startsWith('Provider')) {
|
|
270
|
+
throw e;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
finally {
|
|
278
|
+
reader.releaseLock();
|
|
279
|
+
}
|
|
280
|
+
const toolCalls = Array.from(toolMap.values()).map(tc => {
|
|
281
|
+
let parsedArgs = {};
|
|
282
|
+
try {
|
|
283
|
+
parsedArgs = JSON.parse(tc.argsStr);
|
|
284
|
+
}
|
|
285
|
+
catch {
|
|
286
|
+
parsedArgs = {};
|
|
287
|
+
}
|
|
288
|
+
return {
|
|
289
|
+
id: tc.id || `call_${Math.random().toString(36).substring(2, 9)}`,
|
|
290
|
+
name: tc.name,
|
|
291
|
+
arguments: parsedArgs
|
|
292
|
+
};
|
|
293
|
+
});
|
|
294
|
+
if (!accumulatedContent && !accumulatedReasoning && toolCalls.length === 0) {
|
|
295
|
+
throw new Error(`Provider returned empty response (0 tokens generated). Please check your model '${this.model}', balance, or API key.`);
|
|
296
|
+
}
|
|
297
|
+
let usage;
|
|
298
|
+
const promptTokens = usageData?.prompt_tokens || Math.ceil(JSON.stringify(request.messages).length / 3.5);
|
|
299
|
+
const completionTokens = usageData?.completion_tokens || Math.ceil((accumulatedContent.length + accumulatedReasoning.length) / 3.5);
|
|
300
|
+
const totalTokens = usageData?.total_tokens || (promptTokens + completionTokens);
|
|
301
|
+
const cost = typeof usageData?.total_cost === 'number'
|
|
302
|
+
? usageData.total_cost
|
|
303
|
+
: calculateCost(this.model, promptTokens, completionTokens);
|
|
304
|
+
usage = {
|
|
305
|
+
promptTokens,
|
|
306
|
+
completionTokens,
|
|
307
|
+
totalTokens,
|
|
308
|
+
cost
|
|
309
|
+
};
|
|
310
|
+
const finalResponse = {
|
|
311
|
+
content: accumulatedContent || null,
|
|
312
|
+
toolCalls: toolCalls.length > 0 ? toolCalls : undefined,
|
|
313
|
+
usage
|
|
314
|
+
};
|
|
315
|
+
yield { type: 'done', response: finalResponse };
|
|
316
|
+
return finalResponse;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { spawn } from 'child_process';
|
|
2
|
+
export const bashTool = {
|
|
3
|
+
name: 'bash',
|
|
4
|
+
definition: {
|
|
5
|
+
name: 'bash',
|
|
6
|
+
description: 'Execute a bash command',
|
|
7
|
+
parameters: {
|
|
8
|
+
type: 'object',
|
|
9
|
+
properties: {
|
|
10
|
+
command: { type: 'string' }
|
|
11
|
+
},
|
|
12
|
+
required: ['command']
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
validateArgs(args) {
|
|
16
|
+
if (!args.command || typeof args.command !== 'string')
|
|
17
|
+
throw new Error('command is required');
|
|
18
|
+
},
|
|
19
|
+
async execute(args) {
|
|
20
|
+
return new Promise((resolve, reject) => {
|
|
21
|
+
const proc = spawn(args.command, { shell: true });
|
|
22
|
+
let output = '';
|
|
23
|
+
const timeout = setTimeout(() => {
|
|
24
|
+
proc.kill();
|
|
25
|
+
resolve(output + '\n[Process killed due to timeout]');
|
|
26
|
+
}, 30000);
|
|
27
|
+
proc.stdout.on('data', (data) => {
|
|
28
|
+
output += data.toString();
|
|
29
|
+
if (output.length > 20000) {
|
|
30
|
+
output = output.substring(0, 20000) + '\n[Output truncated]';
|
|
31
|
+
proc.kill();
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
proc.stderr.on('data', (data) => {
|
|
35
|
+
output += data.toString();
|
|
36
|
+
if (output.length > 20000) {
|
|
37
|
+
output = output.substring(0, 20000) + '\n[Output truncated]';
|
|
38
|
+
proc.kill();
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
proc.on('close', (code) => {
|
|
42
|
+
clearTimeout(timeout);
|
|
43
|
+
resolve(`${output}\n[Exit code: ${code}]`);
|
|
44
|
+
});
|
|
45
|
+
proc.on('error', (err) => {
|
|
46
|
+
clearTimeout(timeout);
|
|
47
|
+
reject(new Error(`Failed to start process: ${err.message}`));
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
};
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { exec } from 'child_process';
|
|
2
|
+
import fsSync from 'fs';
|
|
3
|
+
export const diagnoseCodeTool = {
|
|
4
|
+
name: 'diagnose_code',
|
|
5
|
+
definition: {
|
|
6
|
+
name: 'diagnose_code',
|
|
7
|
+
description: 'Diagnose and verify code for syntax, type errors, or lint issues across the project. Use after modifying code to ensure everything compiles cleanly and without errors.',
|
|
8
|
+
parameters: {
|
|
9
|
+
type: 'object',
|
|
10
|
+
properties: {
|
|
11
|
+
file: { type: 'string', description: 'Optional specific file to check (checks whole project if omitted)' }
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
validateArgs() { },
|
|
16
|
+
async execute(args) {
|
|
17
|
+
return new Promise((resolve) => {
|
|
18
|
+
let cmd = '';
|
|
19
|
+
if (args.file) {
|
|
20
|
+
if (args.file.endsWith('.ts') || args.file.endsWith('.tsx')) {
|
|
21
|
+
if (fsSync.existsSync('tsconfig.json')) {
|
|
22
|
+
cmd = 'npx tsc --noEmit';
|
|
23
|
+
}
|
|
24
|
+
else {
|
|
25
|
+
cmd = `node --check ${args.file}`;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
else if (args.file.endsWith('.js') || args.file.endsWith('.mjs') || args.file.endsWith('.cjs')) {
|
|
29
|
+
cmd = `node --check ${args.file}`;
|
|
30
|
+
}
|
|
31
|
+
else if (args.file.endsWith('.py')) {
|
|
32
|
+
cmd = `python -m py_compile ${args.file}`;
|
|
33
|
+
}
|
|
34
|
+
else if (args.file.endsWith('.rs')) {
|
|
35
|
+
cmd = 'cargo check';
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
if (!cmd) {
|
|
39
|
+
if (fsSync.existsSync('tsconfig.json')) {
|
|
40
|
+
cmd = 'npx tsc --noEmit';
|
|
41
|
+
}
|
|
42
|
+
else if (fsSync.existsSync('Cargo.toml')) {
|
|
43
|
+
cmd = 'cargo check';
|
|
44
|
+
}
|
|
45
|
+
else if (fsSync.existsSync('package.json')) {
|
|
46
|
+
cmd = 'npm test -- --passWithNoTests';
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
return resolve('No automated checker found for this workspace. Manual review looks good.');
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
exec(cmd, (err, stdout, stderr) => {
|
|
53
|
+
const out = (stdout + '\n' + stderr).trim();
|
|
54
|
+
if (!err) {
|
|
55
|
+
resolve('✅ Diagnostics passed with 0 errors! Code is clean and valid.');
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
resolve(`❌ Diagnostic errors found:\n${out.slice(0, 3000)}`);
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
};
|
package/dist/tools/fs.js
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { globalSnapshotManager } from '../core/snapshot.js';
|
|
4
|
+
export const readFileTool = {
|
|
5
|
+
name: 'read_file',
|
|
6
|
+
definition: {
|
|
7
|
+
name: 'read_file',
|
|
8
|
+
description: 'Read contents of a file',
|
|
9
|
+
parameters: {
|
|
10
|
+
type: 'object',
|
|
11
|
+
properties: {
|
|
12
|
+
path: { type: 'string' }
|
|
13
|
+
},
|
|
14
|
+
required: ['path']
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
validateArgs(args) {
|
|
18
|
+
if (!args.path || typeof args.path !== 'string')
|
|
19
|
+
throw new Error('path is required');
|
|
20
|
+
},
|
|
21
|
+
async execute(args) {
|
|
22
|
+
try {
|
|
23
|
+
return await fs.readFile(args.path, 'utf8');
|
|
24
|
+
}
|
|
25
|
+
catch (err) {
|
|
26
|
+
throw new Error(`Failed to read file: ${err.message}`);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
export const writeFileTool = {
|
|
31
|
+
name: 'write_file',
|
|
32
|
+
definition: {
|
|
33
|
+
name: 'write_file',
|
|
34
|
+
description: 'Write content to a file (creates new file or overwrites existing file)',
|
|
35
|
+
parameters: {
|
|
36
|
+
type: 'object',
|
|
37
|
+
properties: {
|
|
38
|
+
path: { type: 'string' },
|
|
39
|
+
content: { type: 'string' }
|
|
40
|
+
},
|
|
41
|
+
required: ['path', 'content']
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
validateArgs(args) {
|
|
45
|
+
if (!args.path || typeof args.path !== 'string')
|
|
46
|
+
throw new Error('path is required');
|
|
47
|
+
if (typeof args.content !== 'string')
|
|
48
|
+
throw new Error('content is required');
|
|
49
|
+
},
|
|
50
|
+
async execute(args) {
|
|
51
|
+
try {
|
|
52
|
+
await globalSnapshotManager.recordFileBeforeChange(args.path);
|
|
53
|
+
const parentDir = path.dirname(args.path);
|
|
54
|
+
if (parentDir && parentDir !== '.' && parentDir !== '/') {
|
|
55
|
+
await fs.mkdir(parentDir, { recursive: true });
|
|
56
|
+
}
|
|
57
|
+
let oldLines = 0;
|
|
58
|
+
let existed = false;
|
|
59
|
+
try {
|
|
60
|
+
const oldContent = await fs.readFile(args.path, 'utf8');
|
|
61
|
+
oldLines = oldContent.split('\n').length;
|
|
62
|
+
existed = true;
|
|
63
|
+
}
|
|
64
|
+
catch { }
|
|
65
|
+
await fs.writeFile(args.path, args.content, 'utf8');
|
|
66
|
+
const newLines = args.content.split('\n').length;
|
|
67
|
+
if (!existed) {
|
|
68
|
+
return `Successfully created ${args.path} (+${newLines} lines)`;
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
const diff = newLines - oldLines;
|
|
72
|
+
const diffStr = diff >= 0 ? `+${diff}` : `${diff}`;
|
|
73
|
+
return `Successfully updated ${args.path} (${diffStr} lines, ${newLines} total)`;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
catch (err) {
|
|
77
|
+
throw new Error(`Failed to write file: ${err.message}`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
export const editFileTool = {
|
|
82
|
+
name: 'edit_file',
|
|
83
|
+
definition: {
|
|
84
|
+
name: 'edit_file',
|
|
85
|
+
description: 'Edit a specific block of text in an existing file by replacing old text with new text',
|
|
86
|
+
parameters: {
|
|
87
|
+
type: 'object',
|
|
88
|
+
properties: {
|
|
89
|
+
path: { type: 'string', description: 'Path to the file to edit' },
|
|
90
|
+
target: { type: 'string', description: 'Exact string or block of code to replace' },
|
|
91
|
+
replacement: { type: 'string', description: 'New string or block of code to replace with' }
|
|
92
|
+
},
|
|
93
|
+
required: ['path', 'target', 'replacement']
|
|
94
|
+
}
|
|
95
|
+
},
|
|
96
|
+
validateArgs(args) {
|
|
97
|
+
if (!args.path || typeof args.path !== 'string')
|
|
98
|
+
throw new Error('path is required');
|
|
99
|
+
if (typeof args.target !== 'string')
|
|
100
|
+
throw new Error('target is required');
|
|
101
|
+
if (typeof args.replacement !== 'string')
|
|
102
|
+
throw new Error('replacement is required');
|
|
103
|
+
},
|
|
104
|
+
async execute(args) {
|
|
105
|
+
try {
|
|
106
|
+
await globalSnapshotManager.recordFileBeforeChange(args.path);
|
|
107
|
+
const oldContent = await fs.readFile(args.path, 'utf8');
|
|
108
|
+
let target = args.target;
|
|
109
|
+
let content = oldContent;
|
|
110
|
+
if (!content.includes(target)) {
|
|
111
|
+
const normalizedContent = content.replace(/\r\n/g, '\n');
|
|
112
|
+
const normalizedTarget = target.replace(/\r\n/g, '\n');
|
|
113
|
+
if (normalizedContent.includes(normalizedTarget)) {
|
|
114
|
+
content = normalizedContent;
|
|
115
|
+
target = normalizedTarget;
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
118
|
+
throw new Error(`Target text to replace was not found in ${args.path}. Ensure exact indentation and characters match.`);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
const newContent = content.replace(target, args.replacement);
|
|
122
|
+
await fs.writeFile(args.path, newContent, 'utf8');
|
|
123
|
+
const removedLines = target.split('\n').length;
|
|
124
|
+
const addedLines = args.replacement.split('\n').length;
|
|
125
|
+
return `Successfully edited ${args.path} (+${addedLines} -${removedLines} lines)`;
|
|
126
|
+
}
|
|
127
|
+
catch (err) {
|
|
128
|
+
throw new Error(`Failed to edit file: ${err.message}`);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
export const listDirTool = {
|
|
133
|
+
name: 'list_dir',
|
|
134
|
+
definition: {
|
|
135
|
+
name: 'list_dir',
|
|
136
|
+
description: 'List contents of a directory',
|
|
137
|
+
parameters: {
|
|
138
|
+
type: 'object',
|
|
139
|
+
properties: {
|
|
140
|
+
path: { type: 'string' }
|
|
141
|
+
},
|
|
142
|
+
required: ['path']
|
|
143
|
+
}
|
|
144
|
+
},
|
|
145
|
+
validateArgs(args) {
|
|
146
|
+
if (!args.path || typeof args.path !== 'string')
|
|
147
|
+
throw new Error('path is required');
|
|
148
|
+
},
|
|
149
|
+
async execute(args) {
|
|
150
|
+
try {
|
|
151
|
+
const files = await fs.readdir(args.path, { withFileTypes: true });
|
|
152
|
+
return files.map(f => `${f.isDirectory() ? '[DIR]' : '[FILE]'} ${f.name}`).join('\n');
|
|
153
|
+
}
|
|
154
|
+
catch (err) {
|
|
155
|
+
throw new Error(`Failed to list directory: ${err.message}`);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
export const mkdirTool = {
|
|
160
|
+
name: 'mkdir',
|
|
161
|
+
definition: {
|
|
162
|
+
name: 'mkdir',
|
|
163
|
+
description: 'Create a directory',
|
|
164
|
+
parameters: {
|
|
165
|
+
type: 'object',
|
|
166
|
+
properties: {
|
|
167
|
+
path: { type: 'string' }
|
|
168
|
+
},
|
|
169
|
+
required: ['path']
|
|
170
|
+
}
|
|
171
|
+
},
|
|
172
|
+
validateArgs(args) {
|
|
173
|
+
if (!args.path || typeof args.path !== 'string')
|
|
174
|
+
throw new Error('path is required');
|
|
175
|
+
},
|
|
176
|
+
async execute(args) {
|
|
177
|
+
try {
|
|
178
|
+
await fs.mkdir(args.path, { recursive: true });
|
|
179
|
+
return `Created directory ${args.path}`;
|
|
180
|
+
}
|
|
181
|
+
catch (err) {
|
|
182
|
+
throw new Error(`Failed to create directory: ${err.message}`);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { readFileTool, writeFileTool, editFileTool, listDirTool, mkdirTool } from './fs.js';
|
|
2
|
+
import { bashTool } from './bash.js';
|
|
3
|
+
import { searchTool } from './search.js';
|
|
4
|
+
import { askQuestionsTool } from './questions.js';
|
|
5
|
+
import { webSearchTool, fetchUrlTool } from './web.js';
|
|
6
|
+
import { diagnoseCodeTool } from './diagnostics.js';
|
|
7
|
+
import { installPackageTool } from './packages.js';
|
|
8
|
+
import { saveMemoryTool } from '../core/memory.js';
|
|
9
|
+
import { planReadyTool, lastPlanReady, resetPlanReady } from './plan.js';
|
|
10
|
+
export function getTools(planMode) {
|
|
11
|
+
const baseTools = [readFileTool, listDirTool, searchTool, askQuestionsTool, webSearchTool, fetchUrlTool, saveMemoryTool, planReadyTool];
|
|
12
|
+
if (planMode) {
|
|
13
|
+
return baseTools;
|
|
14
|
+
}
|
|
15
|
+
return [...baseTools, writeFileTool, editFileTool, mkdirTool, bashTool, diagnoseCodeTool, installPackageTool];
|
|
16
|
+
}
|
|
17
|
+
export { webSearchTool, fetchUrlTool, diagnoseCodeTool, installPackageTool, saveMemoryTool, planReadyTool, lastPlanReady, resetPlanReady };
|