aela-ai 1.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 ADDED
@@ -0,0 +1,86 @@
1
+ # 🦇 Aela - Your Terminal AI Sidekick
2
+
3
+ ![Aela the Axolotl](image.png)
4
+
5
+ > An agentic, terminal-based AI assistant powered by OpenRouter.
6
+
7
+ Aela is a lightweight but powerful agentic CLI tool. It brings autonomous AI capabilities directly into your terminal, allowing you to converse with LLMs that can read your local files, write code, and execute bash commands on your behalf.
8
+
9
+ Whether you need to analyze a Git repository, refactor an entire directory, or scaffold a new project, Aela acts as your personal terminal sidekick.
10
+
11
+ ## ✨ Features
12
+
13
+ - **Agentic Capabilities:** Aela doesn't just chat. He has direct access to read/write files, execute bash commands, fetch full webpages, search the web (via Tavily), and even pause to ask you for clarification mid-task.
14
+ - **Interactive REPL Mode:** Drop into a continuous, multi-turn chat session with persistent memory (`aela repl`).
15
+ - **Beautiful Theming:** A polished, color-coded terminal UX built with `picocolors`.
16
+ - **Model Agnostic:** Powered by OpenRouter, you can instantly switch between the best models (e.g., GPT-4, Claude 3.5 Sonnet, Llama 3) directly from the CLI.
17
+ - **Zero-Friction Setup:** Automatically prompts and securely stores your API keys locally (`~/.aela/config.json`) on first run. No messy environment variables required.
18
+ - **Lightweight & Fast:** Built entirely in TypeScript with a minimal footprint.
19
+
20
+ ## 🚀 Installation
21
+
22
+ You can install Aela globally on your system using npm:
23
+
24
+ ```bash
25
+ npm install -g .
26
+ ```
27
+
28
+ *Note: You may need to run `npm run build` if the `dist/` directory is not already compiled.*
29
+
30
+ ## 💻 Usage
31
+
32
+ Simply type `aela` followed by your prompt for one-shot execution, or type `aela repl` to drop into a continuous chat session with persistent memory. No flags necessary.
33
+
34
+ ```bash
35
+ # Start an interactive REPL session
36
+ aela repl
37
+
38
+ # Read files and summarize
39
+ aela read package.json and summarize what this project does
40
+
41
+ # Search and analyze codebase
42
+ aela find all TODO comments in my codebase
43
+
44
+ # Write code and scaffold
45
+ aela create a new python script that scrapes a website
46
+
47
+ # Search the Web
48
+ aela Search the web for the latest news about OpenAI
49
+
50
+ # Fetch and Read Webpages
51
+ aela Go to https://news.ycombinator.com and list the top 3 stories right now
52
+ ```
53
+
54
+ To see the interactive welcome screen and all available commands, simply run:
55
+ ```bash
56
+ aela help
57
+ ```
58
+
59
+ ## ⚙️ Configuration
60
+
61
+ Aela allows you to easily configure your preferences on the fly.
62
+
63
+ - **Select Model:** Fetch and choose from a dynamic list of available OpenRouter models:
64
+ ```bash
65
+ aela config model
66
+ ```
67
+ - **Set API Key (OpenRouter):**
68
+ ```bash
69
+ aela config apiKey <your-key>
70
+ ```
71
+ - **Set Web Search API Key (Tavily):**
72
+ ```bash
73
+ aela config tavilyApiKey <your-key>
74
+ ```
75
+ - **Set Max Tokens:** (Useful to prevent OpenRouter 402 Credit limit errors)
76
+ ```bash
77
+ aela config maxTokens 4000
78
+ ```
79
+
80
+ ## 🛠️ Built With
81
+ - [TypeScript](https://www.typescriptlang.org/)
82
+ - [Node.js](https://nodejs.org/)
83
+ - [OpenAI Node SDK](https://github.com/openai/openai-node) (configured for OpenRouter)
84
+
85
+ ---
86
+ *Built as a lightweight alternative to full-scale agentic terminal environments.*
@@ -0,0 +1,152 @@
1
+ #!/usr/bin/env node
2
+ import OpenAI from "openai";
3
+ import * as fs from "fs";
4
+ import { exec as execCallback } from "child_process";
5
+ import { promisify } from "util";
6
+ const exec = promisify(execCallback);
7
+ async function main() {
8
+ const [, , flag, prompt] = process.argv;
9
+ const apiKey = process.env.OPENROUTER_API_KEY;
10
+ const baseURL = process.env.OPENROUTER_BASE_URL ?? "https://openrouter.ai/api/v1";
11
+ if (!apiKey) {
12
+ throw new Error("OPENROUTER_API_KEY is not set");
13
+ }
14
+ if (flag !== "-p" || !prompt) {
15
+ throw new Error("error: -p flag is required");
16
+ }
17
+ const client = new OpenAI({
18
+ apiKey: apiKey,
19
+ baseURL: baseURL,
20
+ });
21
+ const messages = [
22
+ { role: "user", content: prompt }
23
+ ];
24
+ const tools = [{
25
+ "type": "function",
26
+ "function": {
27
+ "name": "Read",
28
+ "description": "Read and return the contents of a file",
29
+ "parameters": {
30
+ "type": "object",
31
+ "properties": {
32
+ "file_path": {
33
+ "type": "string",
34
+ "description": "The path to the file to read"
35
+ }
36
+ },
37
+ "required": ["file_path"]
38
+ }
39
+ }
40
+ },
41
+ {
42
+ "type": "function",
43
+ "function": {
44
+ "name": "Write",
45
+ "description": "Write content to a file",
46
+ "parameters": {
47
+ "type": "object",
48
+ "required": ["file_path", "content"],
49
+ "properties": {
50
+ "file_path": {
51
+ "type": "string",
52
+ "description": "The path of the file to write to"
53
+ },
54
+ "content": {
55
+ "type": "string",
56
+ "description": "The content to write to the file"
57
+ }
58
+ }
59
+ }
60
+ }
61
+ },
62
+ {
63
+ "type": "function",
64
+ "function": {
65
+ "name": "Bash",
66
+ "description": "Execute a shell command",
67
+ "parameters": {
68
+ "type": "object",
69
+ "required": ["command"],
70
+ "properties": {
71
+ "command": {
72
+ "type": "string",
73
+ "description": "The command to execute"
74
+ }
75
+ }
76
+ }
77
+ }
78
+ }];
79
+ while (true) {
80
+ const response = await client.chat.completions.create({
81
+ model: "anthropic/claude-haiku-4.5",
82
+ messages: messages,
83
+ max_completion_tokens: 7000,
84
+ tools: tools
85
+ });
86
+ if (!response.choices || response.choices.length === 0) {
87
+ throw new Error("no choices in response");
88
+ }
89
+ const message = response.choices[0].message;
90
+ messages.push(message);
91
+ if (!message.tool_calls || message.tool_calls.length === 0) {
92
+ if (message.content) {
93
+ console.log(message.content);
94
+ }
95
+ break;
96
+ }
97
+ for (const toolCall of message.tool_calls) {
98
+ if (toolCall.type === "function" && toolCall.function.name === "Read") {
99
+ const args = JSON.parse(toolCall.function.arguments);
100
+ let content = "";
101
+ try {
102
+ content = fs.readFileSync(args.file_path, "utf-8");
103
+ }
104
+ catch (err) {
105
+ content = `Error reading file: ${err.message}`;
106
+ }
107
+ messages.push({
108
+ role: "tool",
109
+ tool_call_id: toolCall.id,
110
+ content: content
111
+ });
112
+ }
113
+ else if (toolCall.type === "function" && toolCall.function.name === "Write") {
114
+ const args = JSON.parse(toolCall.function.arguments);
115
+ try {
116
+ fs.writeFileSync(args.file_path, args.content);
117
+ messages.push({
118
+ role: "tool",
119
+ tool_call_id: toolCall.id,
120
+ content: "File written successfully"
121
+ });
122
+ }
123
+ catch (err) {
124
+ messages.push({
125
+ role: "tool",
126
+ tool_call_id: toolCall.id,
127
+ content: `Error writing file: ${err.message}`
128
+ });
129
+ }
130
+ }
131
+ else if (toolCall.type === "function" && toolCall.function.name === "Bash") {
132
+ const args = JSON.parse(toolCall.function.arguments);
133
+ try {
134
+ const { stdout, stderr } = await exec(args.command);
135
+ messages.push({
136
+ role: "tool",
137
+ tool_call_id: toolCall.id,
138
+ content: stdout || stderr || "Command executed successfully"
139
+ });
140
+ }
141
+ catch (err) {
142
+ messages.push({
143
+ role: "tool",
144
+ tool_call_id: toolCall.id,
145
+ content: `Error executing command: ${err.message}`
146
+ });
147
+ }
148
+ }
149
+ }
150
+ }
151
+ }
152
+ main();
package/dist/main.js ADDED
@@ -0,0 +1,487 @@
1
+ #!/usr/bin/env node
2
+ import OpenAI from "openai";
3
+ import * as fs from "fs";
4
+ import { exec as execCallback } from "child_process";
5
+ import { promisify } from "util";
6
+ import * as os from "os";
7
+ import * as path from "path";
8
+ import * as readline from "readline";
9
+ import pc from "picocolors";
10
+ const exec = promisify(execCallback);
11
+ const AELA_LOGO = `
12
+ ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
13
+ Aela the Axolotl⠀⠀⠀⠀⢀⡏⠈⢱⠀⠀⡖⠲⣀⠀⠀⠀⠀⠀⠀⠀⠀
14
+ ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢠⠋⠹⡇⠀⡸⢠⠞⠳⠆⠈⡆⠀⠀⠀⠀⠀⠀⠀
15
+ ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣠⠤⠤⠤⠤⠤⢬⣇⢀⣿⣚⢳⡏⠀⢰⠃⡴⠛⢦⠀⠀⠀⠀⠀⠀
16
+ ⠀⡠⣄⢠⠒⣄⠐⢄⠀⠀⣠⠴⠋⠁⠀⠀⠀⠀⠀⠀⠀⠈⠻⣿⣸⡟⢣⣠⣿⣯⣤⡔⠃⠀⠀⠀⠀⠀⠀
17
+ ⠘⣇⠈⢻⡀⠸⡄⠈⣆⠞⠁⠀⠀⠀⠀⠀⠀⠀⣶⣶⣄⡀⠀⠙⠿⣿⣿⣻⡿⠋⢹⠟⠉⡗⠂⠀⠀⠀⠀
18
+ ⢴⠚⠢⢤⣿⣧⣽⣶⣏⡀⠀⠀⠀⠀⠀⠀⣀⠀⠘⠿⡭⢯⠆⠐⢲⣿⣾⣿⢁⣶⣏⡠⠞⢳⠉⢩⠏⠀⠀
19
+ ⠈⡗⠒⣿⡈⣿⡍⣿⣿⣷⠀⣀⣴⣻⣶⠋⠉⠀⠀⠀⠀⠀⠀⠀⠀⢠⡾⠻⠿⣍⠉⣴⠒⠋⢀⠇⠀⠆⠀
20
+ ⢠⠽⠦⠈⣳⣌⣷⣿⠷⠟⠀⠀⠀⠀⠀⠀⠀⠀⣠⢶⡶⢤⣀⠀⢀⡼⠙⣶⣤⠟⠓⠋⠀⠀⠸⡀⠀⢦⠀
21
+ ⠘⠂⣤⡔⠛⢯⣙⣿⣆⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⢧⡃⠀⠈⠙⠛⠓⠒⠛⠦⣀⠀⠀⠀⠀⠀⣇⠀⠘⡀
22
+ ⠀⠸⢅⣙⠶⢲⠟⠻⢿⡷⣄⣀⠀⠀⠀⠀⠀⠀⠀⠈⠙⠂⠀⠀⠀⠀⠀⠀⠀⠈⠉⠉⣳⠀⢀⡏⠀⢠⠇
23
+ ⠀⠀⠀⠈⠀⠸⠤⠚⠛⠁⢾⠋⠉⠉⠉⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣀⣴⢛⣉⠴⠛⠀⢀⡞⠀
24
+ ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠙⠒⠒⠦⠴⠦⠶⢤⣀⠀⠀⠀⠀⠀⠀⠀⢠⠿⣍⡉⠁⠀⠀⣀⡤⠊⠀⠀
25
+ ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠙⠲⠦⣄⣀⣀⡤⠴⠒⠚⠋⠉⠉⠉⠁⠀⠀⠀⠀
26
+ `;
27
+ async function promptInput(query) {
28
+ const rl = readline.createInterface({
29
+ input: process.stdin,
30
+ output: process.stdout,
31
+ });
32
+ return new Promise((resolve) => rl.question(query, (ans) => {
33
+ rl.close();
34
+ resolve(ans);
35
+ }));
36
+ }
37
+ function getConfigPath() {
38
+ const configDir = path.join(os.homedir(), ".aela");
39
+ if (!fs.existsSync(configDir)) {
40
+ fs.mkdirSync(configDir, { recursive: true });
41
+ }
42
+ return path.join(configDir, "config.json");
43
+ }
44
+ function getHistoryPath() {
45
+ const configDir = path.join(os.homedir(), ".aela");
46
+ if (!fs.existsSync(configDir)) {
47
+ fs.mkdirSync(configDir, { recursive: true });
48
+ }
49
+ return path.join(configDir, "history.json");
50
+ }
51
+ function loadHistory() {
52
+ const historyFile = getHistoryPath();
53
+ if (fs.existsSync(historyFile)) {
54
+ try {
55
+ return JSON.parse(fs.readFileSync(historyFile, "utf-8"));
56
+ }
57
+ catch (e) {
58
+ return [];
59
+ }
60
+ }
61
+ return [];
62
+ }
63
+ function saveHistory(messages) {
64
+ const historyFile = getHistoryPath();
65
+ const history = messages.filter(m => m.role !== "system").slice(-20);
66
+ fs.writeFileSync(historyFile, JSON.stringify(history, null, 2));
67
+ }
68
+ function loadConfig() {
69
+ const configFile = getConfigPath();
70
+ if (fs.existsSync(configFile)) {
71
+ try {
72
+ return JSON.parse(fs.readFileSync(configFile, "utf-8"));
73
+ }
74
+ catch (e) {
75
+ return {};
76
+ }
77
+ }
78
+ return {};
79
+ }
80
+ function saveConfig(config) {
81
+ const configFile = getConfigPath();
82
+ fs.writeFileSync(configFile, JSON.stringify(config, null, 2));
83
+ }
84
+ async function getValidConfig() {
85
+ let config = loadConfig();
86
+ if (!config.apiKey) {
87
+ console.log(pc.cyan(AELA_LOGO));
88
+ const key = await promptInput(pc.green("Please enter your OpenRouter API key: "));
89
+ config.apiKey = key.trim();
90
+ saveConfig(config);
91
+ console.log(pc.green("API Key saved to ~/.aela/config.json\n"));
92
+ }
93
+ return config;
94
+ }
95
+ async function main() {
96
+ const isConfigCommand = process.argv[2] === "config";
97
+ if (isConfigCommand) {
98
+ const key = process.argv[3];
99
+ let value = process.argv[4];
100
+ if (!key) {
101
+ console.log("Usage: aela config <key> [value]");
102
+ console.log("Supported keys: apiKey, model, maxTokens, tavilyApiKey");
103
+ return;
104
+ }
105
+ if (key === "model" && !value) {
106
+ console.log(pc.dim("Fetching available models from OpenRouter..."));
107
+ const configObj = await getValidConfig();
108
+ const apiKey = configObj.apiKey;
109
+ try {
110
+ const response = await fetch("https://openrouter.ai/api/v1/models", {
111
+ headers: {
112
+ "Authorization": `Bearer ${apiKey}`
113
+ }
114
+ });
115
+ const data = await response.json();
116
+ const models = data.data.map((m) => m.id);
117
+ console.log(pc.yellow(pc.bold("\nSelect a model:")));
118
+ models.forEach((m, i) => console.log(`${pc.cyan(`${i + 1}.`)} ${m}`));
119
+ const choice = await promptInput(pc.green("Enter the number of the model: "));
120
+ const index = parseInt(choice, 10) - 1;
121
+ if (index >= 0 && index < models.length) {
122
+ value = models[index];
123
+ }
124
+ else {
125
+ console.log(pc.red("Invalid choice."));
126
+ return;
127
+ }
128
+ }
129
+ catch (e) {
130
+ console.error(pc.red("Failed to fetch models from OpenRouter. " + e.message));
131
+ return;
132
+ }
133
+ }
134
+ else if (!value) {
135
+ console.log(pc.red(`Value is required for ${key}`));
136
+ return;
137
+ }
138
+ if (key === "maxTokens") {
139
+ value = parseInt(value, 10);
140
+ }
141
+ const config = loadConfig();
142
+ config[key] = value;
143
+ saveConfig(config);
144
+ console.log(pc.green(`Successfully updated ${key} to ${value} in ~/.aela/config.json`));
145
+ return;
146
+ }
147
+ const prompt = process.argv.slice(2).join(" ").trim();
148
+ if (process.argv.length <= 2 || prompt === "" || prompt === "help" || prompt === "--help" || prompt === "-h") {
149
+ console.log(pc.cyan(AELA_LOGO));
150
+ console.log(pc.green(pc.bold("Welcome to Aela!")));
151
+ console.log(pc.yellow("\nAvailable Commands:"));
152
+ console.log(" aela <your prompt> - Chat with Aela");
153
+ console.log(" aela repl - Start an interactive REPL session with history");
154
+ console.log(" aela config model - Select from a dropdown of available OpenRouter models");
155
+ console.log(" aela config maxTokens <number> - Set max completion tokens (e.g. 4000)");
156
+ console.log(" aela config apiKey <key> - Set your OpenRouter API key");
157
+ console.log(" aela config tavilyApiKey <key> - Set your Tavily API key for web search");
158
+ console.log(" aela help - Show this help message");
159
+ console.log(pc.yellow("\nExample Usage:"));
160
+ console.log(" aela Summarize the top 3 stories on https://news.ycombinator.com");
161
+ console.log(" aela Search the web for the best restaurants in my favorite city");
162
+ console.log(" aela Read package.json and tell me if any dependencies are outdated");
163
+ console.log(" aela config model");
164
+ return;
165
+ }
166
+ const config = await getValidConfig();
167
+ const apiKey = config.apiKey;
168
+ const modelName = config.model || "anthropic/claude-haiku-4.5";
169
+ const maxTokens = config.maxTokens || 4000;
170
+ const baseURL = process.env.OPENROUTER_BASE_URL ?? "https://openrouter.ai/api/v1";
171
+ const client = new OpenAI({
172
+ apiKey: apiKey,
173
+ baseURL: baseURL,
174
+ });
175
+ const systemPrompt = `You are Aela, a helpful terminal-based AI assistant.
176
+ The current date and time is: ${new Date().toLocaleString()}.
177
+ You are running on: ${os.type()} ${os.release()} (${os.arch()}).
178
+ Use your tools to help the user. If they ask about current time or dates, you can use the time provided above.
179
+ IMPORTANT: If you need to ask the user a clarifying question or request permission, you MUST use the "AskUser" tool. Do not just output the question as text, because the program will exit immediately and you will not get an answer.`;
180
+ const messages = [
181
+ { role: "system", content: systemPrompt },
182
+ { role: "user", content: prompt }
183
+ ];
184
+ const tools = [{
185
+ "type": "function",
186
+ "function": {
187
+ "name": "Read",
188
+ "description": "Read and return the contents of a file",
189
+ "parameters": {
190
+ "type": "object",
191
+ "properties": {
192
+ "file_path": {
193
+ "type": "string",
194
+ "description": "The path to the file to read"
195
+ }
196
+ },
197
+ "required": ["file_path"]
198
+ }
199
+ }
200
+ },
201
+ {
202
+ "type": "function",
203
+ "function": {
204
+ "name": "Write",
205
+ "description": "Write content to a file",
206
+ "parameters": {
207
+ "type": "object",
208
+ "required": ["file_path", "content"],
209
+ "properties": {
210
+ "file_path": {
211
+ "type": "string",
212
+ "description": "The path of the file to write to"
213
+ },
214
+ "content": {
215
+ "type": "string",
216
+ "description": "The content to write to the file"
217
+ }
218
+ }
219
+ }
220
+ }
221
+ },
222
+ {
223
+ "type": "function",
224
+ "function": {
225
+ "name": "Bash",
226
+ "description": "Execute a shell command",
227
+ "parameters": {
228
+ "type": "object",
229
+ "required": ["command"],
230
+ "properties": {
231
+ "command": {
232
+ "type": "string",
233
+ "description": "The command to execute"
234
+ }
235
+ }
236
+ }
237
+ }
238
+ },
239
+ {
240
+ "type": "function",
241
+ "function": {
242
+ "name": "SearchWeb",
243
+ "description": "Search the web for information using Tavily API. Useful for current events, facts, and finding documentation.",
244
+ "parameters": {
245
+ "type": "object",
246
+ "required": ["query"],
247
+ "properties": {
248
+ "query": {
249
+ "type": "string",
250
+ "description": "The search query"
251
+ }
252
+ }
253
+ }
254
+ }
255
+ },
256
+ {
257
+ "type": "function",
258
+ "function": {
259
+ "name": "AskUser",
260
+ "description": "Pause execution and ask the user a question to get clarification or permission.",
261
+ "parameters": {
262
+ "type": "object",
263
+ "required": ["question"],
264
+ "properties": {
265
+ "question": {
266
+ "type": "string",
267
+ "description": "The question to ask the user"
268
+ }
269
+ }
270
+ }
271
+ }
272
+ },
273
+ {
274
+ "type": "function",
275
+ "function": {
276
+ "name": "FetchURL",
277
+ "description": "Fetch the raw text content of a URL.",
278
+ "parameters": {
279
+ "type": "object",
280
+ "required": ["url"],
281
+ "properties": {
282
+ "url": {
283
+ "type": "string",
284
+ "description": "The URL to fetch"
285
+ }
286
+ }
287
+ }
288
+ }
289
+ }];
290
+ async function runAgent(client, modelName, messages, maxTokens, tools, config) {
291
+ while (true) {
292
+ let response;
293
+ try {
294
+ response = await client.chat.completions.create({
295
+ model: modelName,
296
+ messages: messages,
297
+ max_completion_tokens: maxTokens,
298
+ tools: tools
299
+ });
300
+ }
301
+ catch (e) {
302
+ if (e.status === 402) {
303
+ console.error(`\nError: Not enough credits for this request.`);
304
+ console.error(`You requested up to ${maxTokens} max_tokens, which exceeds your available balance.`);
305
+ console.error(`Try lowering your maxTokens limit by running:`);
306
+ console.error(` aela config maxTokens <number>\n`);
307
+ process.exit(1);
308
+ }
309
+ throw e;
310
+ }
311
+ if (!response.choices || response.choices.length === 0) {
312
+ throw new Error("no choices in response");
313
+ }
314
+ const message = response.choices[0].message;
315
+ messages.push(message);
316
+ if (!message.tool_calls || message.tool_calls.length === 0) {
317
+ if (message.content) {
318
+ console.log(pc.cyan(message.content));
319
+ }
320
+ break;
321
+ }
322
+ for (const toolCall of message.tool_calls) {
323
+ if (toolCall.type === "function" && toolCall.function.name === "Read") {
324
+ const args = JSON.parse(toolCall.function.arguments);
325
+ let content = "";
326
+ try {
327
+ content = fs.readFileSync(args.file_path, "utf-8");
328
+ }
329
+ catch (err) {
330
+ content = `Error reading file: ${err.message}`;
331
+ }
332
+ messages.push({
333
+ role: "tool",
334
+ tool_call_id: toolCall.id,
335
+ content: content
336
+ });
337
+ }
338
+ else if (toolCall.type === "function" && toolCall.function.name === "Write") {
339
+ const args = JSON.parse(toolCall.function.arguments);
340
+ try {
341
+ fs.writeFileSync(args.file_path, args.content);
342
+ messages.push({
343
+ role: "tool",
344
+ tool_call_id: toolCall.id,
345
+ content: "File written successfully"
346
+ });
347
+ }
348
+ catch (err) {
349
+ messages.push({
350
+ role: "tool",
351
+ tool_call_id: toolCall.id,
352
+ content: `Error writing file: ${err.message}`
353
+ });
354
+ }
355
+ }
356
+ else if (toolCall.type === "function" && toolCall.function.name === "Bash") {
357
+ const args = JSON.parse(toolCall.function.arguments);
358
+ try {
359
+ const { stdout, stderr } = await exec(args.command);
360
+ messages.push({
361
+ role: "tool",
362
+ tool_call_id: toolCall.id,
363
+ content: stdout || stderr || "Command executed successfully"
364
+ });
365
+ }
366
+ catch (err) {
367
+ messages.push({
368
+ role: "tool",
369
+ tool_call_id: toolCall.id,
370
+ content: `Error executing command: ${err.message}`
371
+ });
372
+ }
373
+ }
374
+ else if (toolCall.type === "function" && toolCall.function.name === "SearchWeb") {
375
+ const args = JSON.parse(toolCall.function.arguments);
376
+ if (!config.tavilyApiKey) {
377
+ messages.push({
378
+ role: "tool",
379
+ tool_call_id: toolCall.id,
380
+ content: "Error: tavilyApiKey is not configured. Please tell the user to run 'aela config tavilyApiKey <their-tavily-api-key>' to enable web search."
381
+ });
382
+ }
383
+ else {
384
+ try {
385
+ const searchResponse = await fetch("https://api.tavily.com/search", {
386
+ method: "POST",
387
+ headers: {
388
+ "Content-Type": "application/json"
389
+ },
390
+ body: JSON.stringify({
391
+ api_key: config.tavilyApiKey,
392
+ query: args.query,
393
+ search_depth: "basic",
394
+ include_answer: true
395
+ })
396
+ });
397
+ const searchData = await searchResponse.json();
398
+ let content = "";
399
+ if (searchData.answer) {
400
+ content += `Answer: ${searchData.answer}\n\n`;
401
+ }
402
+ if (searchData.results && searchData.results.length > 0) {
403
+ content += "Sources:\n" + searchData.results.map((r) => `- ${r.title} (${r.url}): ${r.content}`).join("\n");
404
+ }
405
+ else {
406
+ content += "No search results found.";
407
+ }
408
+ messages.push({
409
+ role: "tool",
410
+ tool_call_id: toolCall.id,
411
+ content: content || "No relevant info found."
412
+ });
413
+ }
414
+ catch (err) {
415
+ messages.push({
416
+ role: "tool",
417
+ tool_call_id: toolCall.id,
418
+ content: `Error executing web search: ${err.message}`
419
+ });
420
+ }
421
+ }
422
+ }
423
+ else if (toolCall.type === "function" && toolCall.function.name === "AskUser") {
424
+ const args = JSON.parse(toolCall.function.arguments);
425
+ const answer = await promptInput(`\n${pc.yellow(pc.bold("[Aela asks]:"))} ${pc.yellow(args.question)}\n${pc.dim("Your answer: ")}`);
426
+ messages.push({
427
+ role: "tool",
428
+ tool_call_id: toolCall.id,
429
+ content: answer
430
+ });
431
+ }
432
+ else if (toolCall.type === "function" && toolCall.function.name === "FetchURL") {
433
+ const args = JSON.parse(toolCall.function.arguments);
434
+ try {
435
+ const response = await fetch(args.url);
436
+ let text = await response.text();
437
+ text = text.replace(/<style[^>]*>[\s\S]*?<\/style>/gi, '')
438
+ .replace(/<script[^>]*>[\s\S]*?<\/script>/gi, '')
439
+ .replace(/<[^>]+>/g, ' ')
440
+ .replace(/\s+/g, ' ')
441
+ .trim();
442
+ messages.push({
443
+ role: "tool",
444
+ tool_call_id: toolCall.id,
445
+ content: text.slice(0, 10000)
446
+ });
447
+ }
448
+ catch (err) {
449
+ messages.push({
450
+ role: "tool",
451
+ tool_call_id: toolCall.id,
452
+ content: `Error fetching URL: ${err.message}`
453
+ });
454
+ }
455
+ }
456
+ }
457
+ }
458
+ }
459
+ if (prompt === "repl") {
460
+ console.log(pc.cyan(AELA_LOGO));
461
+ console.log(pc.green(pc.bold("Welcome to Aela REPL!")) + " " + pc.dim("Type 'exit' to quit.\n"));
462
+ let messages = loadHistory();
463
+ if (messages.length === 0 || messages[0].role !== "system") {
464
+ messages.unshift({ role: "system", content: systemPrompt });
465
+ }
466
+ else {
467
+ messages[0].content = systemPrompt;
468
+ }
469
+ while (true) {
470
+ const userInput = await promptInput(pc.green(pc.bold("You: ")));
471
+ if (userInput.trim().toLowerCase() === "exit" || userInput.trim() === "") {
472
+ break;
473
+ }
474
+ messages.push({ role: "user", content: userInput });
475
+ await runAgent(client, modelName, messages, maxTokens, tools, config);
476
+ saveHistory(messages);
477
+ }
478
+ }
479
+ else {
480
+ const messages = [
481
+ { role: "system", content: systemPrompt },
482
+ { role: "user", content: prompt }
483
+ ];
484
+ await runAgent(client, modelName, messages, maxTokens, tools, config);
485
+ }
486
+ }
487
+ main();
package/image.png ADDED
Binary file
package/package.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "aela-ai",
3
+ "version": "1.0.0",
4
+ "description": "An agentic, terminal-based AI assistant with file access, web search, and persistent REPL mode.",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/ABHIGYAN-MOHANTA/aela.git"
10
+ },
11
+ "keywords": [
12
+ "cli",
13
+ "ai",
14
+ "agent",
15
+ "terminal",
16
+ "openrouter",
17
+ "llm",
18
+ "tavily"
19
+ ],
20
+ "files": [
21
+ "dist",
22
+ "image.png",
23
+ "README.md"
24
+ ],
25
+ "bin": {
26
+ "aela": "./dist/main.js"
27
+ },
28
+ "scripts": {
29
+ "dev": "bun run app/main.ts",
30
+ "build": "tsc"
31
+ },
32
+ "dependencies": {
33
+ "openai": "^6.16.0",
34
+ "picocolors": "^1.1.1"
35
+ },
36
+ "devDependencies": {
37
+ "@types/bun": "latest",
38
+ "@types/node": "latest",
39
+ "typescript": "latest"
40
+ }
41
+ }