@krishivpb60/aether-ai-cli 1.1.6 → 1.1.8

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@krishivpb60/aether-ai-cli",
3
- "version": "1.1.6",
3
+ "version": "1.1.8",
4
4
  "description": "Aether Core AI — A cyberpunk command-line AI assistant with multi-mode reasoning, 12-node failover mesh, file context injection, and offline fallbacks.",
5
5
  "main": "src/cli.js",
6
6
  "bin": {
package/src/agent.js ADDED
@@ -0,0 +1,417 @@
1
+ // ═══════════════════════════════════════════════════════════
2
+ // AETHER AI CLI — Agent Autopilot Engine
3
+ // Safe Command Execution, File Sandbox, DuckDuckGo Search
4
+ // ═══════════════════════════════════════════════════════════
5
+
6
+ import { resolve, relative, isAbsolute, dirname } from "node:path";
7
+ import { readFileSync, writeFileSync, mkdirSync } from "node:fs";
8
+ import { exec } from "node:child_process";
9
+ import { promisify } from "node:util";
10
+ import chalk from "chalk";
11
+
12
+ const execAsync = promisify(exec);
13
+
14
+ export const AGENT_INSTRUCTIONS = `
15
+ SYSTEM AGENT CAPABILITIES:
16
+ You can interact with the user's terminal and files by outputting special command blocks in your responses. You can output multiple command blocks in one turn. The CLI will execute them, show you the result, and let you continue.
17
+ Supported commands:
18
+ 1. To read a file: [READ_FILE: path/to/file.ext]
19
+ 2. To write/create a file:
20
+ [WRITE_FILE: path/to/file.ext]
21
+ <content>
22
+ [END_WRITE]
23
+ 3. To run a terminal command: [RUN_COMMAND: your command here]
24
+ 4. To search the web: [SEARCH_WEB: search query here]
25
+
26
+ Rules:
27
+ - Before running modifying commands or reading private files, check the user's permission level.
28
+ - Always output clean, direct commands.
29
+ - Do not explain these command blocks; just output them when you need to perform the action.
30
+ `;
31
+
32
+ /**
33
+ * Checks if a command is safe (read-only/inspection).
34
+ * @param {string} cmd
35
+ * @returns {boolean}
36
+ */
37
+ export function isSafeCommand(cmd) {
38
+ const safePatterns = [
39
+ /^git\s+(status|diff|log|branch|show)/i,
40
+ /^(ls|dir|pwd)(\s+|$)/i,
41
+ /^(cat|type|head|tail)(\s+|$)/i,
42
+ /^(npm|yarn|pnpm)\s+test(\s+|$)/i,
43
+ /^(node|npm|git|yarn|pnpm|python|pip)\s+(--version|-v)(\s+|$)/i,
44
+ ];
45
+ return safePatterns.some((pattern) => pattern.test(cmd.trim()));
46
+ }
47
+
48
+ /**
49
+ * Checks if a target path is inside the current working directory.
50
+ * @param {string} path
51
+ * @returns {boolean}
52
+ */
53
+ export function isInsideWorkspace(path) {
54
+ const ws = resolve(process.cwd());
55
+ const target = resolve(path);
56
+ const rel = relative(ws, target);
57
+ return !rel.startsWith("..") && !isAbsolute(rel);
58
+ }
59
+
60
+ /**
61
+ * Free web search via DuckDuckGo HTML scraping.
62
+ * @param {string} query
63
+ * @returns {Promise<Array>} List of search results
64
+ */
65
+ export async function searchDuckDuckGo(query) {
66
+ try {
67
+ const response = await fetch(
68
+ `https://html.duckduckgo.com/html/?q=${encodeURIComponent(query)}`,
69
+ {
70
+ headers: {
71
+ "User-Agent":
72
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
73
+ },
74
+ }
75
+ );
76
+ if (!response.ok) {
77
+ throw new Error(`DuckDuckGo returned status ${response.status}`);
78
+ }
79
+ const html = await response.text();
80
+
81
+ const results = [];
82
+ const matches = [...html.matchAll(/<a class="result__a"[^>]* href="([^"]+)"[^>]*>([\s\S]*?)<\/a>/g)];
83
+ const snippets = [...html.matchAll(/<a class="result__snippet"[^>]*>([\s\S]*?)<\/a>/g)];
84
+
85
+ for (let i = 0; i < Math.min(5, matches.length); i++) {
86
+ let url = matches[i][1];
87
+ const title = matches[i][2].replace(/<[^>]*>/g, "").trim();
88
+ const snippet = snippets[i]
89
+ ? snippets[i][1].replace(/<[^>]*>/g, "").trim()
90
+ : "";
91
+
92
+ if (url.includes("uddg=")) {
93
+ const urlMatch = url.match(/uddg=([^&]+)/);
94
+ if (urlMatch) {
95
+ url = decodeURIComponent(urlMatch[1]);
96
+ }
97
+ }
98
+ if (url.startsWith("//")) {
99
+ url = "https:" + url;
100
+ }
101
+
102
+ results.push({ title, url, snippet });
103
+ }
104
+ return results;
105
+ } catch (err) {
106
+ throw new Error(`DuckDuckGo search failed: ${err.message}`);
107
+ }
108
+ }
109
+
110
+ /**
111
+ * Sequentially parses and executes all agent tool blocks in the text.
112
+ * @param {string} text - AI response
113
+ * @param {object} aiConfig - Flat config object
114
+ * @param {object} rl - Readline interface
115
+ * @returns {Promise<Array>} List of execution results
116
+ */
117
+ export async function processAgentBlocks(text, aiConfig, rl) {
118
+ let index = 0;
119
+ const results = [];
120
+
121
+ while (true) {
122
+ const runMatch = text.indexOf("[RUN_COMMAND:", index);
123
+ const readMatch = text.indexOf("[READ_FILE:", index);
124
+ const searchMatch = text.indexOf("[SEARCH_WEB:", index);
125
+ const writeMatch = text.indexOf("[WRITE_FILE:", index);
126
+
127
+ const matches = [
128
+ { type: "RUN_COMMAND", pos: runMatch },
129
+ { type: "READ_FILE", pos: readMatch },
130
+ { type: "SEARCH_WEB", pos: searchMatch },
131
+ { type: "WRITE_FILE", pos: writeMatch },
132
+ ].filter((m) => m.pos !== -1);
133
+
134
+ if (matches.length === 0) break;
135
+
136
+ // Process the earliest tag in the text
137
+ matches.sort((a, b) => a.pos - b.pos);
138
+ const nextTool = matches[0];
139
+
140
+ if (nextTool.type === "WRITE_FILE") {
141
+ const startTag = "[WRITE_FILE:";
142
+ const startIdx = nextTool.pos;
143
+ const endTagIdx = text.indexOf("]", startIdx);
144
+ if (endTagIdx === -1) {
145
+ index = startIdx + startTag.length;
146
+ continue;
147
+ }
148
+ const filePath = text.substring(startIdx + startTag.length, endTagIdx).trim();
149
+ const endWriteIdx = text.indexOf("[END_WRITE]", endTagIdx);
150
+ if (endWriteIdx === -1) {
151
+ index = endTagIdx + 1;
152
+ continue;
153
+ }
154
+ const content = text.substring(endTagIdx + 1, endWriteIdx);
155
+
156
+ const result = await executeTool("WRITE_FILE", filePath, content, aiConfig, rl);
157
+ results.push(result);
158
+ index = endWriteIdx + "[END_WRITE]".length;
159
+ } else {
160
+ const tag = `[${nextTool.type}:`;
161
+ const startIdx = nextTool.pos;
162
+ const endIdx = text.indexOf("]", startIdx);
163
+ if (endIdx === -1) {
164
+ index = startIdx + tag.length;
165
+ continue;
166
+ }
167
+ const arg = text.substring(startIdx + tag.length, endIdx).trim();
168
+
169
+ const result = await executeTool(nextTool.type, arg, "", aiConfig, rl);
170
+ results.push(result);
171
+ index = endIdx + 1;
172
+ }
173
+ }
174
+
175
+ return results;
176
+ }
177
+
178
+ /**
179
+ * Executes a single tool based on autopilot config settings.
180
+ * @param {string} type - Tool type
181
+ * @param {string} arg - Tool argument
182
+ * @param {string} content - Write file content (if applicable)
183
+ * @param {object} aiConfig - Flat config object
184
+ * @param {object} rl - Readline interface
185
+ */
186
+ export async function executeTool(type, arg, content, aiConfig, rl) {
187
+ const autopilot = (aiConfig.AUTOPILOT || "off").toLowerCase();
188
+
189
+ if (type === "READ_FILE") {
190
+ const filePath = arg;
191
+ const inside = isInsideWorkspace(filePath);
192
+
193
+ let allowed = false;
194
+ if (
195
+ autopilot === "machine" ||
196
+ autopilot === "workspace" ||
197
+ (autopilot === "safe" && inside)
198
+ ) {
199
+ allowed = true;
200
+ } else {
201
+ rl.pause();
202
+ const answer = await new Promise((resolve) => {
203
+ rl.question(
204
+ chalk.yellow(
205
+ `\n⚠️ AI wants to read file: "${filePath}". Allow? [y/N]: `
206
+ ),
207
+ resolve
208
+ );
209
+ });
210
+ rl.resume();
211
+ allowed =
212
+ answer.toLowerCase().trim() === "y" ||
213
+ answer.toLowerCase().trim() === "yes";
214
+ }
215
+
216
+ if (!allowed) {
217
+ return {
218
+ tool: "READ_FILE",
219
+ arg,
220
+ success: false,
221
+ error: "Access denied by user.",
222
+ };
223
+ }
224
+
225
+ try {
226
+ const fileContent = readFileSync(resolve(filePath), "utf-8");
227
+ return {
228
+ tool: "READ_FILE",
229
+ arg,
230
+ success: true,
231
+ content: fileContent,
232
+ };
233
+ } catch (err) {
234
+ return {
235
+ tool: "READ_FILE",
236
+ arg,
237
+ success: false,
238
+ error: err.message,
239
+ };
240
+ }
241
+ }
242
+
243
+ if (type === "WRITE_FILE") {
244
+ const filePath = arg;
245
+ const inside = isInsideWorkspace(filePath);
246
+
247
+ let allowed = false;
248
+ if (autopilot === "machine" || (autopilot === "workspace" && inside)) {
249
+ allowed = true;
250
+ } else {
251
+ rl.pause();
252
+ const answer = await new Promise((resolve) => {
253
+ rl.question(
254
+ chalk.yellow(
255
+ `\n⚠️ AI wants to write file: "${filePath}". Allow? [y/N]: `
256
+ ),
257
+ resolve
258
+ );
259
+ });
260
+ rl.resume();
261
+ allowed =
262
+ answer.toLowerCase().trim() === "y" ||
263
+ answer.toLowerCase().trim() === "yes";
264
+ }
265
+
266
+ if (!allowed) {
267
+ return {
268
+ tool: "WRITE_FILE",
269
+ arg,
270
+ success: false,
271
+ error: "Access denied by user.",
272
+ };
273
+ }
274
+
275
+ try {
276
+ const fullPath = resolve(filePath);
277
+ mkdirSync(dirname(fullPath), { recursive: true });
278
+ writeFileSync(fullPath, content, "utf-8");
279
+ return {
280
+ tool: "WRITE_FILE",
281
+ arg,
282
+ success: true,
283
+ message: "File written successfully.",
284
+ };
285
+ } catch (err) {
286
+ return {
287
+ tool: "WRITE_FILE",
288
+ arg,
289
+ success: false,
290
+ error: err.message,
291
+ };
292
+ }
293
+ }
294
+
295
+ if (type === "RUN_COMMAND") {
296
+ const command = arg;
297
+ const safe = isSafeCommand(command);
298
+
299
+ let allowed = false;
300
+ if (
301
+ autopilot === "machine" ||
302
+ (autopilot === "safe" && safe) ||
303
+ (autopilot === "workspace" && safe)
304
+ ) {
305
+ allowed = true;
306
+ } else {
307
+ rl.pause();
308
+ const answer = await new Promise((resolve) => {
309
+ rl.question(
310
+ chalk.yellow(
311
+ `\n⚠️ AI wants to run terminal command: "${command}". Allow? [y/N/always]: `
312
+ ),
313
+ resolve
314
+ );
315
+ });
316
+ rl.resume();
317
+
318
+ const cleanAnswer = answer.toLowerCase().trim();
319
+ if (cleanAnswer === "always") {
320
+ const { setConfigValue } = await import("./config.js");
321
+ await setConfigValue("AUTOPILOT", "safe");
322
+ aiConfig.AUTOPILOT = "safe";
323
+ console.log(chalk.green(`\n✓ Autopilot enabled (Safe mode).`));
324
+ allowed = true;
325
+ } else {
326
+ allowed = cleanAnswer === "y" || cleanAnswer === "yes";
327
+ }
328
+ }
329
+
330
+ if (!allowed) {
331
+ return {
332
+ tool: "RUN_COMMAND",
333
+ arg,
334
+ success: false,
335
+ error: "Execution denied by user.",
336
+ };
337
+ }
338
+
339
+ console.log(chalk.cyan(`\n⚡ Running command: ${command}`));
340
+ try {
341
+ const { stdout, stderr } = await execAsync(command);
342
+ return {
343
+ tool: "RUN_COMMAND",
344
+ arg,
345
+ success: true,
346
+ stdout,
347
+ stderr,
348
+ };
349
+ } catch (err) {
350
+ return {
351
+ tool: "RUN_COMMAND",
352
+ arg,
353
+ success: false,
354
+ error: err.message,
355
+ stdout: err.stdout,
356
+ stderr: err.stderr,
357
+ };
358
+ }
359
+ }
360
+
361
+ if (type === "SEARCH_WEB") {
362
+ const query = arg;
363
+
364
+ let allowed = false;
365
+ if (autopilot !== "off") {
366
+ allowed = true;
367
+ } else {
368
+ rl.pause();
369
+ const answer = await new Promise((resolve) => {
370
+ rl.question(
371
+ chalk.yellow(
372
+ `\n⚠️ AI wants to search the web for: "${query}". Allow? [y/N]: `
373
+ ),
374
+ resolve
375
+ );
376
+ });
377
+ rl.resume();
378
+ allowed =
379
+ answer.toLowerCase().trim() === "y" ||
380
+ answer.toLowerCase().trim() === "yes";
381
+ }
382
+
383
+ if (!allowed) {
384
+ return {
385
+ tool: "SEARCH_WEB",
386
+ arg,
387
+ success: false,
388
+ error: "Search denied by user.",
389
+ };
390
+ }
391
+
392
+ console.log(chalk.cyan(`\n🔍 Searching web: "${query}"`));
393
+ try {
394
+ const results = await searchDuckDuckGo(query);
395
+ return {
396
+ tool: "SEARCH_WEB",
397
+ arg,
398
+ success: true,
399
+ results,
400
+ };
401
+ } catch (err) {
402
+ return {
403
+ tool: "SEARCH_WEB",
404
+ arg,
405
+ success: false,
406
+ error: err.message,
407
+ };
408
+ }
409
+ }
410
+
411
+ return {
412
+ tool: type,
413
+ arg,
414
+ success: false,
415
+ error: "Unknown tool type.",
416
+ };
417
+ }