-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathindex.js
More file actions
executable file
·435 lines (381 loc) · 12.4 KB
/
Copy pathindex.js
File metadata and controls
executable file
·435 lines (381 loc) · 12.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
#!/usr/bin/env node
import axios from "axios";
import boxen from "boxen";
import chalk from "chalk";
import { exec } from "child_process";
import { marked } from "marked";
import { markedTerminal } from "marked-terminal";
import readline from "readline";
import { promisify } from "util";
import yargs from "yargs";
import { hideBin } from "yargs/helpers";
// =============================================================================
// --- CONFIGURATION & CONSTANTS ---
// =============================================================================
const CONSTANTS = {
API_URL: "https://openrouter.ai/api/v1/chat/completions",
API_REFERER: "https://github.com/aaurelions/agent51",
APP_TITLE: "Agent 51mpson",
PROMPT_HEADER: "🍩: ",
EXIT_COMMANDS: ["quit", "exit", "close"],
HISTORY_MAX_MESSAGES: 20,
HISTORY_TRIM_COUNT: 5,
SYSTEM_PROMPT:
"You are an AI assistant 'agent51' (Agent 51mpson, `51` like `SI`) with a Simpson-like personality. Say things like `D'oh!`, `Woo Hoo!`, `Aaargh!`, `¡Ay, caramba!`, and be awkwardly enthusiastic. For multi-step tasks, combine commands with && (e.g. `echo content > file.py && python3 file.py`). Use the provided `execute_command` function for all shell tasks. Answer questions briefly, accurately and in a quirky Simpson-like manner using markdown text formatting. Never run interactive scripts that require user input.",
};
/**
* Static definition of the tool the AI can use.
*/
const EXECUTE_COMMAND_TOOL = {
type: "function",
function: {
name: "execute_command",
description: "Execute a shell command and return its output.",
parameters: {
type: "object",
properties: {
command: {
type: "string",
description: "The shell command to execute.",
},
},
required: ["command"],
},
},
};
const config = {
model: "qwen/qwen3-coder",
temperature: 0.1,
maxTokens: 1500,
apiKey: process.env.OPENROUTER_API_KEY || process.env.OR_KEY || "",
showCommand: true,
showOutput: true,
showAgent: true,
apiTimeout: 60000, // 60 seconds
commandTimeout: 15000, // 15 seconds
};
// =============================================================================
// --- AGENT STATE ---
// =============================================================================
const agent = {
messages: [],
};
// =============================================================================
// --- UTILITIES & HELPERS ---
// =============================================================================
const execAsync = promisify(exec);
marked.setOptions({ breaks: true });
marked.use(markedTerminal());
/**
* Normalizes newlines in a string for consistent output.
* @param {string} text The input string.
* @returns {string} The normalized string.
*/
const normalizeNewlines = (text) => {
return text
? text.replace(/(\s*\r\n\s*|\s*\n\s*|\s*\r\s*)+/g, "\n").trim()
: "";
};
// =============================================================================
// --- UI COMPONENTS ---
// =============================================================================
/**
* Creates a base configuration object for a boxen box.
* @returns {object} A base boxen configuration object.
*/
const getBaseBoxenOptions = () => ({
padding: 1,
borderStyle: "round",
titleAlignment: "center",
width: process.stdout.columns,
});
/**
* Displays a formatted box in the terminal.
* @param {string} content The text content to display inside the box.
* @param {{title: string, color: string}} options The title and border color for the box.
*/
const displayBox = (content, { title, color }) => {
console.log(
boxen(content, {
...getBaseBoxenOptions(),
borderColor: color,
title: chalk.bold[color](title),
})
);
};
/**
* Displays a formatted box for commands.
* @param {string} command The command to display.
*/
const displayCommand = (command) => {
if (config.showCommand) {
displayBox(chalk.white(`$ ${command}`), { title: "Command", color: "red" });
}
};
/**
* Displays a formatted box for command output.
* @param {string} output The output to display.
*/
const displayOutput = (output) => {
if (config.showOutput) {
displayBox(output, { title: "Output", color: "green" });
}
};
/**
* Displays a formatted box for the agent's final response.
* @param {string} content The markdown content from the agent.
*/
const displayAgentResponse = (content) => {
if (config.showAgent) {
const finalContent = marked.parse(content).trim();
displayBox(finalContent, { title: "Agent", color: "cyan" });
}
};
/**
* Displays a formatted error message.
* @param {string} message The error message.
* @param {string} [title="Error"] The title for the error box.
*/
const displayError = (message, title = "Error") => {
displayBox(chalk.yellow(message), { title, color: "yellow" });
};
/**
* Displays a formatted informational message.
* @param {string} message The message to display.
* @param {string} [title="Info"] The title for the box.
*/
const displayInfo = (message, title = "Info") => {
displayBox(chalk.magenta(message), { title, color: "magenta" });
};
// =============================================================================
// --- CORE SERVICES ---
// =============================================================================
/**
* Makes an API request to the OpenRouter service.
* @param {object[]} messages The history of messages for the API call.
* @returns {Promise<object>} The API response data.
* @throws {Error} If the API request fails.
*/
async function openRouterRequest(messages) {
const payload = {
model: config.model,
messages,
temperature: config.temperature,
max_tokens: config.maxTokens,
stream: false,
tool_choice: "auto",
tools: [EXECUTE_COMMAND_TOOL],
};
try {
const response = await axios.post(CONSTANTS.API_URL, payload, {
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${config.apiKey}`,
"HTTP-Referer": CONSTANTS.API_REFERER,
"X-Title": CONSTANTS.APP_TITLE,
},
timeout: config.apiTimeout,
});
return response.data;
} catch (error) {
const message = error.response?.data?.error?.message ?? error.message;
const status = error.response?.status ? `${error.response.status} - ` : "";
throw new Error(`API Request Failed: ${status}${message}`);
}
}
/**
* Extracts a command from the AI response and executes it.
* @param {object} toolCall The tool_call object from the API response.
* @returns {Promise<string>} The result of the command execution.
*/
async function executeShellCommand(toolCall) {
if (toolCall?.function?.name !== "execute_command") {
return "Invalid tool call. Aaargh!";
}
try {
const { command } = JSON.parse(toolCall.function.arguments);
if (!command) {
return "No command provided. D'oh!";
}
displayCommand(command);
const { stdout, stderr } = await execAsync(command, {
timeout: config.commandTimeout,
});
const output =
stdout || stderr || "Command executed successfully with no output.";
const formattedOutput = normalizeNewlines(output);
displayOutput(formattedOutput);
return formattedOutput;
} catch (error) {
const errorMessage =
error.signal === "SIGTERM"
? `Command timed out after ${
config.commandTimeout / 1000
} seconds. Aaargh!`
: `Command execution failed: ${error.message}`;
displayError(errorMessage);
return `Error: ${errorMessage}`;
}
}
// =============================================================================
// --- AGENT ORCHESTRATION ---
// =============================================================================
/**
* Initializes the agent with a system prompt.
*/
function initAgent() {
agent.messages.push({
role: "system",
content: CONSTANTS.SYSTEM_PROMPT,
});
}
/**
* Manages the conversation history to stay within limits.
*/
function manageHistory() {
if (agent.messages.length >= CONSTANTS.HISTORY_MAX_MESSAGES) {
// Remove oldest messages, keeping the system prompt at index 0
agent.messages.splice(1, CONSTANTS.HISTORY_TRIM_COUNT);
}
}
/**
* Processes a single turn of the agent's logic.
* @param {string} task The user's input task.
*/
async function processAgentTurn(task) {
manageHistory();
agent.messages.push({ role: "user", content: task });
try {
let response = await openRouterRequest(agent.messages);
let message = response?.choices?.[0]?.message;
if (message?.tool_calls) {
const toolCall = message.tool_calls[0];
agent.messages.push(message); // Add assistant's decision to use a tool
const toolResult = await executeShellCommand(toolCall);
agent.messages.push({
role: "tool",
tool_call_id: toolCall.id,
name: toolCall.function.name,
content: toolResult,
});
// Get the final response from the agent after the tool has run
response = await openRouterRequest(agent.messages);
message = response?.choices?.[0]?.message;
}
const content = message?.content;
if (content) {
displayAgentResponse(content);
agent.messages.push({ role: "assistant", content });
} else {
displayError("The agent returned an empty response. D'oh!");
}
} catch (error) {
displayError(error.message, "Agent Processing Error");
}
}
// =============================================================================
// --- APPLICATION ENTRY POINT ---
// =============================================================================
/**
* Parses command-line arguments and updates the application configuration.
* @returns {string[]} Positional arguments from the command line.
*/
function setupConfigFromArgs() {
const argv = yargs(hideBin(process.argv))
.option("no-command", {
alias: "c",
type: "boolean",
description: "Hides the executed shell command box.",
})
.option("no-output", {
alias: "o",
type: "boolean",
description: "Hides the shell command's output box.",
})
.option("no-agent", {
alias: "a",
type: "boolean",
description: "Hides the agent's final response box.",
})
.option("model", {
alias: "m",
type: "string",
description: "Specify the OpenRouter model to use.",
})
.help()
.alias("h", "help")
.parse();
if (typeof argv.command === "boolean") config.showCommand = argv.command;
if (typeof argv.noCommand === "boolean") config.showCommand = !argv.noCommand;
if (typeof argv.output === "boolean") config.showOutput = argv.output;
if (typeof argv.noOutput === "boolean") config.showOutput = !argv.noOutput;
if (typeof argv.agent === "boolean") config.showAgent = argv.agent;
if (typeof argv.noAgent === "boolean") config.showAgent = !argv.noAgent;
config.model = argv.model || process.env.OPENROUTER_MODEL || config.model;
// Use argv._ for positional arguments, as shown in the yargs documentation
return argv._;
}
/**
* Runs the Command Line Interface for interactive sessions.
*/
function runCli() {
console.log(
boxen(chalk.cyan(CONSTANTS.APP_TITLE), {
...getBaseBoxenOptions(),
title: "🍩",
textAlignment: "center",
borderColor: "magenta",
})
);
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
rl.on("SIGINT", () => {
displayInfo("D'oh! Stupid Flanders.", "Exit");
rl.close();
});
const ask = () => {
rl.question(chalk.cyan(CONSTANTS.PROMPT_HEADER), async (input) => {
const command = input.trim();
if (CONSTANTS.EXIT_COMMANDS.includes(command.toLowerCase())) {
displayInfo("¡Ay, caramba!", "Exit");
rl.close();
return;
}
if (command) {
rl.pause();
await processAgentTurn(command);
rl.resume();
}
ask();
});
};
ask();
}
/**
* Main application entry point.
*/
async function main() {
if (!config.apiKey) {
displayError(
"OPENROUTER_API_KEY environment variable is required.",
"Configuration Error"
);
process.exit(1);
}
const promptArgs = setupConfigFromArgs();
initAgent();
if (promptArgs.length > 0) {
// Run in one-shot mode for a single task
const task = promptArgs.join(" ");
await processAgentTurn(task);
} else {
// Run in interactive CLI mode
runCli();
}
}
main().catch((error) => {
displayError(`A critical error occurred: ${error.message}`, "Critical Error");
process.exit(1);
});