Skip to content

Commit 8059b29

Browse files
committed
feat: implement CLI tool executor with OpenRouter integration
1 parent 50cae9b commit 8059b29

File tree

15 files changed

+1768
-0
lines changed

15 files changed

+1768
-0
lines changed

src/cli.ts

Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
#!/usr/bin/env node
2+
import * as path from 'path';
3+
import * as dotenv from 'dotenv';
4+
import { spawn } from 'child_process';
5+
import {
6+
ApiConfiguration,
7+
createApiClient,
8+
BaseToolExecutor,
9+
CliContextProvider,
10+
MessageParser,
11+
AVAILABLE_TOOLS,
12+
ToolResponse
13+
} from './lib';
14+
15+
dotenv.config();
16+
17+
class CliToolExecutor extends BaseToolExecutor {
18+
async executeCommand(command: string): Promise<[boolean, ToolResponse]> {
19+
return new Promise((resolve) => {
20+
const process = spawn(command, [], {
21+
shell: true,
22+
cwd: this.getCurrentWorkingDirectory()
23+
});
24+
25+
let output = '';
26+
let error = '';
27+
28+
process.stdout.on('data', (data) => {
29+
output += data.toString();
30+
console.log(data.toString());
31+
});
32+
33+
process.stderr.on('data', (data) => {
34+
error += data.toString();
35+
console.error(data.toString());
36+
});
37+
38+
process.on('close', (code) => {
39+
if (code !== 0) {
40+
resolve([true, 'Command failed with code ' + code + '\n' + error]);
41+
} else {
42+
resolve([false, output]);
43+
}
44+
});
45+
});
46+
}
47+
48+
getCurrentWorkingDirectory(): string {
49+
return process.cwd();
50+
}
51+
}
52+
53+
async function main() {
54+
const args = process.argv.slice(2);
55+
const task = args[0];
56+
57+
if (!task) {
58+
console.error('Usage: cline "My Task" [--tools]');
59+
process.exit(1);
60+
}
61+
62+
const apiKey = process.env.OPENROUTER_API_KEY;
63+
if (!apiKey) {
64+
console.error('Error: OPENROUTER_API_KEY environment variable is required');
65+
process.exit(1);
66+
}
67+
68+
const apiConfiguration: ApiConfiguration = {
69+
apiKey,
70+
apiModelId: 'anthropic/claude-3-sonnet-20240229',
71+
apiProvider: 'openrouter',
72+
};
73+
74+
const cwd = process.cwd();
75+
const toolExecutor = new CliToolExecutor(cwd);
76+
const contextProvider = new CliContextProvider(cwd);
77+
const messageParser = new MessageParser(AVAILABLE_TOOLS);
78+
const apiClient = createApiClient(apiConfiguration);
79+
80+
try {
81+
const envDetails = await contextProvider.getEnvironmentDetails(true);
82+
const toolDocs = AVAILABLE_TOOLS.map(tool => {
83+
const params = Object.entries(tool.parameters)
84+
.map(([name, param]) => '- ' + name + ': (' + (param.required ? 'required' : 'optional') + ') ' + param.description)
85+
.join('\n');
86+
return '## ' + tool.name + '\nDescription: ' + tool.description + '\nParameters:\n' + params;
87+
}).join('\n\n');
88+
89+
const systemPromptParts = [
90+
'You are Cline, a highly skilled software engineer.',
91+
'',
92+
'TOOLS',
93+
'',
94+
'You have access to the following tools that must be used with XML tags:',
95+
'',
96+
toolDocs,
97+
'',
98+
'RULES',
99+
'',
100+
'1. Use one tool at a time',
101+
'2. Wait for tool execution results before proceeding',
102+
'3. Handle errors appropriately',
103+
'4. Document your changes',
104+
'',
105+
'TASK',
106+
'',
107+
task
108+
];
109+
110+
const systemPrompt = systemPromptParts.join('\n');
111+
const history: Anthropic.MessageParam[] = [
112+
{ role: 'user', content: `<task>${task}</task><environment_details>${envDetails}</environment_details>` }
113+
];
114+
115+
console.log('Sending request to API...');
116+
for await (const chunk of apiClient.createMessage(systemPrompt, history)) {
117+
if (chunk.type === 'text' && chunk.text) {
118+
console.log('Received text:', chunk.text);
119+
const toolUse = messageParser.parseToolUse(chunk.text);
120+
if (toolUse) {
121+
console.log('Parsed tool use:', toolUse);
122+
}
123+
if (toolUse) {
124+
let error = false;
125+
let result = '';
126+
127+
switch (toolUse.name) {
128+
case 'write_to_file':
129+
[error, result] = await toolExecutor.writeFile(
130+
toolUse.params.path,
131+
toolUse.params.content,
132+
parseInt(toolUse.params.line_count)
133+
);
134+
break;
135+
case 'read_file':
136+
[error, result] = await toolExecutor.readFile(toolUse.params.path);
137+
break;
138+
case 'list_files':
139+
[error, result] = await toolExecutor.listFiles(
140+
toolUse.params.path,
141+
toolUse.params.recursive === 'true'
142+
);
143+
break;
144+
case 'search_files':
145+
[error, result] = await toolExecutor.searchFiles(
146+
toolUse.params.path,
147+
toolUse.params.regex,
148+
toolUse.params.file_pattern
149+
);
150+
break;
151+
case 'execute_command':
152+
[error, result] = await toolExecutor.executeCommand(toolUse.params.command);
153+
break;
154+
default:
155+
error = true;
156+
result = `Unknown tool: ${toolUse.name}`;
157+
}
158+
history.push(
159+
{ role: 'assistant', content: chunk.text },
160+
{ role: 'user', content: `[${toolUse.name}] Result: ${result}` }
161+
);
162+
} else {
163+
console.log(chunk.text);
164+
}
165+
} else if (chunk.type === 'usage') {
166+
// Log usage metrics if needed
167+
// console.log('Usage:', chunk);
168+
}
169+
}
170+
} catch (error) {
171+
console.error('Error:', error.message);
172+
process.exit(1);
173+
}
174+
}
175+
176+
main().catch(error => {
177+
console.error('Fatal error:', error);
178+
process.exit(1);
179+
});

0 commit comments

Comments
 (0)