@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 +1 -1
- package/src/agent.js +417 -0
- package/src/chat.js +412 -99
- package/src/cli.js +88 -0
- package/src/config.js +187 -10
- package/src/file-parser.js +48 -1
- package/src/git.js +41 -0
- package/src/ui/theme.js +202 -5
- package/test/agent.test.js +62 -0
- package/test/config.test.js +27 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@krishivpb60/aether-ai-cli",
|
|
3
|
-
"version": "1.1.
|
|
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
|
+
}
|