@krishivpb60/aether-ai-cli 1.1.4 → 1.1.6
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 +9 -1
- package/package.json +1 -1
- package/setup.py +23 -12
- package/src/chat.js +7 -3
- package/src/cli.js +13 -3
- package/src/ui/theme.js +122 -0
- package/test/ux.test.js +31 -1
package/README.md
CHANGED
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
|
|
47
47
|
## 🚀 Quick Start
|
|
48
48
|
|
|
49
|
-
### Install globally
|
|
49
|
+
### Install globally via npm
|
|
50
50
|
|
|
51
51
|
```bash
|
|
52
52
|
npm install -g @krishivpb60/aether-ai-cli
|
|
@@ -58,6 +58,14 @@ npm install -g @krishivpb60/aether-ai-cli
|
|
|
58
58
|
npx @krishivpb60/aether-ai-cli chat
|
|
59
59
|
```
|
|
60
60
|
|
|
61
|
+
### Or install via pip (Python wrapper)
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
pip install aether-ai-agent-cli
|
|
65
|
+
# Run via terminal:
|
|
66
|
+
aether-pip chat
|
|
67
|
+
```
|
|
68
|
+
|
|
61
69
|
### Setup (Interactive Wizard)
|
|
62
70
|
|
|
63
71
|
```bash
|
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.6",
|
|
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/setup.py
CHANGED
|
@@ -1,19 +1,30 @@
|
|
|
1
1
|
from setuptools import setup, find_packages
|
|
2
2
|
import os
|
|
3
3
|
import shutil
|
|
4
|
+
import json
|
|
5
|
+
|
|
6
|
+
# Read version from package.json
|
|
7
|
+
pkg_json_path = 'package.json'
|
|
8
|
+
if not os.path.exists(pkg_json_path):
|
|
9
|
+
pkg_json_path = os.path.join('aether_pip', 'node_project', 'package.json')
|
|
10
|
+
|
|
11
|
+
with open(pkg_json_path, 'r', encoding='utf-8') as f:
|
|
12
|
+
pkg_data = json.load(f)
|
|
13
|
+
version = pkg_data.get('version', '1.0.0')
|
|
4
14
|
|
|
5
15
|
# Copy Node project files into aether_pip/node_project for clean packaging
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
16
|
+
if os.path.exists('bin') and os.path.exists('src'):
|
|
17
|
+
dest_dir = os.path.join('aether_pip', 'node_project')
|
|
18
|
+
if os.path.exists(dest_dir):
|
|
19
|
+
shutil.rmtree(dest_dir)
|
|
20
|
+
os.makedirs(dest_dir)
|
|
10
21
|
|
|
11
|
-
# Copy directories
|
|
12
|
-
shutil.copytree('bin', os.path.join(dest_dir, 'bin'))
|
|
13
|
-
shutil.copytree('src', os.path.join(dest_dir, 'src'))
|
|
14
|
-
shutil.copyfile('package.json', os.path.join(dest_dir, 'package.json'))
|
|
15
|
-
if os.path.exists('package-lock.json'):
|
|
16
|
-
|
|
22
|
+
# Copy directories
|
|
23
|
+
shutil.copytree('bin', os.path.join(dest_dir, 'bin'))
|
|
24
|
+
shutil.copytree('src', os.path.join(dest_dir, 'src'))
|
|
25
|
+
shutil.copyfile('package.json', os.path.join(dest_dir, 'package.json'))
|
|
26
|
+
if os.path.exists('package-lock.json'):
|
|
27
|
+
shutil.copyfile('package-lock.json', os.path.join(dest_dir, 'package-lock.json'))
|
|
17
28
|
|
|
18
29
|
def package_files(directory):
|
|
19
30
|
paths = []
|
|
@@ -24,8 +35,8 @@ def package_files(directory):
|
|
|
24
35
|
return paths
|
|
25
36
|
|
|
26
37
|
setup(
|
|
27
|
-
name="aether-ai-cli",
|
|
28
|
-
version=
|
|
38
|
+
name="aether-ai-agent-cli",
|
|
39
|
+
version=version,
|
|
29
40
|
author="Krishiv PB",
|
|
30
41
|
author_email="krylobloxyt@gmail.com",
|
|
31
42
|
description="Aether Core AI v110 — Universal AI Gateway CLI (Python Wrapper)",
|
package/src/chat.js
CHANGED
|
@@ -20,6 +20,8 @@ import {
|
|
|
20
20
|
bullet,
|
|
21
21
|
modeBadge,
|
|
22
22
|
clearStreamedText,
|
|
23
|
+
StreamFilter,
|
|
24
|
+
stripCodeFences,
|
|
23
25
|
getActiveTheme,
|
|
24
26
|
setTheme,
|
|
25
27
|
getThemesList
|
|
@@ -228,19 +230,21 @@ export async function startChat(options = {}) {
|
|
|
228
230
|
|
|
229
231
|
let hasStartedStreaming = false;
|
|
230
232
|
let streamedText = "";
|
|
233
|
+
const filter = new StreamFilter(process.stdout.write.bind(process.stdout));
|
|
231
234
|
const onToken = (token) => {
|
|
232
235
|
if (!hasStartedStreaming) {
|
|
233
236
|
hasStartedStreaming = true;
|
|
234
237
|
firstTokenTime = Date.now();
|
|
235
238
|
spinner.stop();
|
|
236
239
|
}
|
|
237
|
-
|
|
240
|
+
filter.write(token);
|
|
238
241
|
streamedText += token;
|
|
239
242
|
};
|
|
240
243
|
|
|
241
244
|
try {
|
|
242
245
|
const result = await routePrompt(fullPrompt, currentMode.systemPrompt, aiConfig, onToken, history);
|
|
243
246
|
spinner.stop();
|
|
247
|
+
filter.flush();
|
|
244
248
|
|
|
245
249
|
// Store in history
|
|
246
250
|
history.push({ role: "user", content: originalInput, timestamp: new Date() });
|
|
@@ -257,7 +261,7 @@ export async function startChat(options = {}) {
|
|
|
257
261
|
await saveHistory(history);
|
|
258
262
|
|
|
259
263
|
if (hasStartedStreaming) {
|
|
260
|
-
clearStreamedText(
|
|
264
|
+
clearStreamedText(filter.filteredText);
|
|
261
265
|
}
|
|
262
266
|
|
|
263
267
|
// Display response
|
|
@@ -302,7 +306,7 @@ export async function startChat(options = {}) {
|
|
|
302
306
|
let match;
|
|
303
307
|
const fileWrites = [];
|
|
304
308
|
while ((match = writeRegex.exec(result.text)) !== null) {
|
|
305
|
-
fileWrites.push({ path: match[1].trim(), content: match[2] });
|
|
309
|
+
fileWrites.push({ path: match[1].trim(), content: stripCodeFences(match[2]) });
|
|
306
310
|
}
|
|
307
311
|
|
|
308
312
|
if (fileWrites.length > 0) {
|
package/src/cli.js
CHANGED
|
@@ -18,6 +18,8 @@ import {
|
|
|
18
18
|
keyValue,
|
|
19
19
|
bullet,
|
|
20
20
|
clearStreamedText,
|
|
21
|
+
StreamFilter,
|
|
22
|
+
stripCodeFences,
|
|
21
23
|
getActiveTheme,
|
|
22
24
|
setTheme,
|
|
23
25
|
getThemesList,
|
|
@@ -253,19 +255,27 @@ async function handleAsk(prompt, opts) {
|
|
|
253
255
|
|
|
254
256
|
let hasStartedStreaming = false;
|
|
255
257
|
let streamedText = "";
|
|
258
|
+
const filter = !opts.raw ? new StreamFilter(process.stdout.write.bind(process.stdout)) : null;
|
|
256
259
|
const onToken = (token) => {
|
|
257
260
|
if (!hasStartedStreaming) {
|
|
258
261
|
hasStartedStreaming = true;
|
|
259
262
|
firstTokenTime = Date.now();
|
|
260
263
|
spinner.stop();
|
|
261
264
|
}
|
|
262
|
-
|
|
265
|
+
if (filter) {
|
|
266
|
+
filter.write(token);
|
|
267
|
+
} else {
|
|
268
|
+
process.stdout.write(token);
|
|
269
|
+
}
|
|
263
270
|
streamedText += token;
|
|
264
271
|
};
|
|
265
272
|
|
|
266
273
|
try {
|
|
267
274
|
const result = await routePrompt(fullPrompt, mode.systemPrompt, aiConfig, onToken);
|
|
268
275
|
spinner.stop();
|
|
276
|
+
if (filter) {
|
|
277
|
+
filter.flush();
|
|
278
|
+
}
|
|
269
279
|
|
|
270
280
|
if (opts.raw) {
|
|
271
281
|
if (!hasStartedStreaming) {
|
|
@@ -277,7 +287,7 @@ async function handleAsk(prompt, opts) {
|
|
|
277
287
|
}
|
|
278
288
|
} else {
|
|
279
289
|
if (hasStartedStreaming) {
|
|
280
|
-
clearStreamedText(streamedText);
|
|
290
|
+
clearStreamedText(filter ? filter.filteredText : streamedText);
|
|
281
291
|
}
|
|
282
292
|
console.log("");
|
|
283
293
|
console.log(label.aether + " " + colors.dim(`via ${result.provider}${result.model ? ` (${result.model})` : ""} • Node ${result.node}`));
|
|
@@ -319,7 +329,7 @@ async function handleAsk(prompt, opts) {
|
|
|
319
329
|
let match;
|
|
320
330
|
const fileWrites = [];
|
|
321
331
|
while ((match = writeRegex.exec(result.text)) !== null) {
|
|
322
|
-
fileWrites.push({ path: match[1].trim(), content: match[2] });
|
|
332
|
+
fileWrites.push({ path: match[1].trim(), content: stripCodeFences(match[2]) });
|
|
323
333
|
}
|
|
324
334
|
|
|
325
335
|
if (fileWrites.length > 0) {
|
package/src/ui/theme.js
CHANGED
|
@@ -148,6 +148,107 @@ export function clearStreamedText(text) {
|
|
|
148
148
|
}
|
|
149
149
|
}
|
|
150
150
|
|
|
151
|
+
/**
|
|
152
|
+
* Filter stream tokens on the fly to suppress file write blocks
|
|
153
|
+
* and print a cleaner placeholder badge in real-time instead of raw code.
|
|
154
|
+
*/
|
|
155
|
+
export class StreamFilter {
|
|
156
|
+
constructor(writeFn) {
|
|
157
|
+
this.writeFn = writeFn;
|
|
158
|
+
this.buffer = "";
|
|
159
|
+
this.cursor = 0;
|
|
160
|
+
this.state = "NORMAL"; // "NORMAL", "COLLECTING_FILENAME", "SUPPRESSING"
|
|
161
|
+
this.filenameBuffer = "";
|
|
162
|
+
this.filteredText = "";
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
_write(text) {
|
|
166
|
+
this.writeFn(text);
|
|
167
|
+
this.filteredText += text;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
write(token) {
|
|
171
|
+
this.buffer += token;
|
|
172
|
+
this.process();
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
process() {
|
|
176
|
+
const writeTag = "[WRITE_FILE:";
|
|
177
|
+
const endTag = "[END_WRITE]";
|
|
178
|
+
|
|
179
|
+
while (this.cursor < this.buffer.length) {
|
|
180
|
+
if (this.state === "NORMAL") {
|
|
181
|
+
const nextIndex = this.buffer.indexOf(writeTag, this.cursor);
|
|
182
|
+
if (nextIndex !== -1) {
|
|
183
|
+
if (nextIndex > this.cursor) {
|
|
184
|
+
this._write(this.buffer.slice(this.cursor, nextIndex));
|
|
185
|
+
}
|
|
186
|
+
this.cursor = nextIndex + writeTag.length;
|
|
187
|
+
this.state = "COLLECTING_FILENAME";
|
|
188
|
+
this.filenameBuffer = "";
|
|
189
|
+
} else {
|
|
190
|
+
// Check for partial match of writeTag at the end of the buffer
|
|
191
|
+
let partialMatchLength = 0;
|
|
192
|
+
for (let i = 1; i < writeTag.length; i++) {
|
|
193
|
+
const part = writeTag.slice(0, i);
|
|
194
|
+
if (this.buffer.endsWith(part)) {
|
|
195
|
+
partialMatchLength = i;
|
|
196
|
+
break;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
const safeEnd = this.buffer.length - partialMatchLength;
|
|
200
|
+
if (safeEnd > this.cursor) {
|
|
201
|
+
this._write(this.buffer.slice(this.cursor, safeEnd));
|
|
202
|
+
this.cursor = safeEnd;
|
|
203
|
+
}
|
|
204
|
+
break; // Wait for more tokens
|
|
205
|
+
}
|
|
206
|
+
} else if (this.state === "COLLECTING_FILENAME") {
|
|
207
|
+
const closeIndex = this.buffer.indexOf("]", this.cursor);
|
|
208
|
+
if (closeIndex !== -1) {
|
|
209
|
+
this.filenameBuffer += this.buffer.slice(this.cursor, closeIndex);
|
|
210
|
+
const filename = this.filenameBuffer.trim();
|
|
211
|
+
this._write(`\n\n${colors.brand("⚡ [File creation request: " + filename + "]")}\n\n`);
|
|
212
|
+
this.cursor = closeIndex + 1;
|
|
213
|
+
this.state = "SUPPRESSING";
|
|
214
|
+
} else {
|
|
215
|
+
this.filenameBuffer += this.buffer.slice(this.cursor);
|
|
216
|
+
this.cursor = this.buffer.length;
|
|
217
|
+
break; // Wait for more tokens
|
|
218
|
+
}
|
|
219
|
+
} else if (this.state === "SUPPRESSING") {
|
|
220
|
+
const nextIndex = this.buffer.indexOf(endTag, this.cursor);
|
|
221
|
+
if (nextIndex !== -1) {
|
|
222
|
+
this.cursor = nextIndex + endTag.length;
|
|
223
|
+
this.state = "NORMAL";
|
|
224
|
+
} else {
|
|
225
|
+
// Check for partial match of endTag at the end of the buffer
|
|
226
|
+
let partialMatchLength = 0;
|
|
227
|
+
for (let i = 1; i < endTag.length; i++) {
|
|
228
|
+
const part = endTag.slice(0, i);
|
|
229
|
+
if (this.buffer.endsWith(part)) {
|
|
230
|
+
partialMatchLength = i;
|
|
231
|
+
break;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
const safeEnd = this.buffer.length - partialMatchLength;
|
|
235
|
+
if (safeEnd > this.cursor) {
|
|
236
|
+
this.cursor = safeEnd;
|
|
237
|
+
}
|
|
238
|
+
break; // Wait for more tokens
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
flush() {
|
|
245
|
+
if (this.state === "NORMAL" && this.cursor < this.buffer.length) {
|
|
246
|
+
this._write(this.buffer.slice(this.cursor));
|
|
247
|
+
this.cursor = this.buffer.length;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
151
252
|
// ── Theme State Management ────────────────────────────────
|
|
152
253
|
|
|
153
254
|
export function getActiveTheme() {
|
|
@@ -167,3 +268,24 @@ export function setTheme(themeName) {
|
|
|
167
268
|
export function getThemesList() {
|
|
168
269
|
return Object.keys(THEMES);
|
|
169
270
|
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Strips markdown code block fences (```lang ... ```) from a string if present.
|
|
274
|
+
* @param {string} content - Raw content extracted from file write blocks
|
|
275
|
+
* @returns {string} Cleaned content
|
|
276
|
+
*/
|
|
277
|
+
export function stripCodeFences(content) {
|
|
278
|
+
let cleaned = content.trim();
|
|
279
|
+
if (cleaned.startsWith("```")) {
|
|
280
|
+
const firstNewline = cleaned.indexOf("\n");
|
|
281
|
+
if (firstNewline !== -1) {
|
|
282
|
+
cleaned = cleaned.slice(firstNewline + 1);
|
|
283
|
+
} else {
|
|
284
|
+
cleaned = cleaned.slice(3);
|
|
285
|
+
}
|
|
286
|
+
if (cleaned.endsWith("```")) {
|
|
287
|
+
cleaned = cleaned.slice(0, -3);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
return cleaned.trim();
|
|
291
|
+
}
|
package/test/ux.test.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { test, beforeEach, afterEach } from "node:test";
|
|
2
2
|
import assert from "node:assert";
|
|
3
|
-
import { separator, clearStreamedText, getActiveTheme, setTheme, getThemesList } from "../src/ui/theme.js";
|
|
3
|
+
import { separator, clearStreamedText, StreamFilter, stripCodeFences, getActiveTheme, setTheme, getThemesList } from "../src/ui/theme.js";
|
|
4
4
|
import { createSpinner } from "../src/ui/spinner.js";
|
|
5
5
|
import { routePrompt } from "../src/ai/router.js";
|
|
6
6
|
import { getModeByName, MODES } from "../src/modes.js";
|
|
@@ -148,4 +148,34 @@ test("Cyberpunk UX and Streaming Suite", async (t) => {
|
|
|
148
148
|
const unknown = getModeByName("nonexistent-mode");
|
|
149
149
|
assert.strictEqual(unknown, null);
|
|
150
150
|
});
|
|
151
|
+
|
|
152
|
+
await t.test("StreamFilter should suppress file blocks but output other text", () => {
|
|
153
|
+
let output = "";
|
|
154
|
+
const filter = new StreamFilter((chunk) => {
|
|
155
|
+
output += chunk;
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
filter.write("Hello ");
|
|
159
|
+
filter.write("world! [WRITE_");
|
|
160
|
+
filter.write("FILE: test.txt]");
|
|
161
|
+
filter.write("This content is hidden\n");
|
|
162
|
+
filter.write("[END_WRITE] After block");
|
|
163
|
+
filter.flush();
|
|
164
|
+
|
|
165
|
+
assert.ok(output.includes("Hello world!"));
|
|
166
|
+
assert.ok(output.includes("After block"));
|
|
167
|
+
assert.ok(output.includes("File creation request: test.txt"));
|
|
168
|
+
assert.ok(!output.includes("This content is hidden"));
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
await t.test("stripCodeFences should clean code blocks with backticks", () => {
|
|
172
|
+
const jsBlock = "```javascript\nconsole.log('hi');\n```";
|
|
173
|
+
assert.strictEqual(stripCodeFences(jsBlock), "console.log('hi');");
|
|
174
|
+
|
|
175
|
+
const htmlBlock = "```html\n<div>hello</div>\n```";
|
|
176
|
+
assert.strictEqual(stripCodeFences(htmlBlock), "<div>hello</div>");
|
|
177
|
+
|
|
178
|
+
const noFenceBlock = "console.log('hi');";
|
|
179
|
+
assert.strictEqual(stripCodeFences(noFenceBlock), "console.log('hi');");
|
|
180
|
+
});
|
|
151
181
|
});
|