@krishivpb60/aether-ai-cli 1.1.4 → 1.1.5
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 +5 -2
- package/src/cli.js +11 -2
- package/src/ui/theme.js +101 -0
- package/test/ux.test.js +20 -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.5",
|
|
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,7 @@ import {
|
|
|
20
20
|
bullet,
|
|
21
21
|
modeBadge,
|
|
22
22
|
clearStreamedText,
|
|
23
|
+
StreamFilter,
|
|
23
24
|
getActiveTheme,
|
|
24
25
|
setTheme,
|
|
25
26
|
getThemesList
|
|
@@ -228,19 +229,21 @@ export async function startChat(options = {}) {
|
|
|
228
229
|
|
|
229
230
|
let hasStartedStreaming = false;
|
|
230
231
|
let streamedText = "";
|
|
232
|
+
const filter = new StreamFilter(process.stdout.write.bind(process.stdout));
|
|
231
233
|
const onToken = (token) => {
|
|
232
234
|
if (!hasStartedStreaming) {
|
|
233
235
|
hasStartedStreaming = true;
|
|
234
236
|
firstTokenTime = Date.now();
|
|
235
237
|
spinner.stop();
|
|
236
238
|
}
|
|
237
|
-
|
|
239
|
+
filter.write(token);
|
|
238
240
|
streamedText += token;
|
|
239
241
|
};
|
|
240
242
|
|
|
241
243
|
try {
|
|
242
244
|
const result = await routePrompt(fullPrompt, currentMode.systemPrompt, aiConfig, onToken, history);
|
|
243
245
|
spinner.stop();
|
|
246
|
+
filter.flush();
|
|
244
247
|
|
|
245
248
|
// Store in history
|
|
246
249
|
history.push({ role: "user", content: originalInput, timestamp: new Date() });
|
|
@@ -257,7 +260,7 @@ export async function startChat(options = {}) {
|
|
|
257
260
|
await saveHistory(history);
|
|
258
261
|
|
|
259
262
|
if (hasStartedStreaming) {
|
|
260
|
-
clearStreamedText(
|
|
263
|
+
clearStreamedText(filter.filteredText);
|
|
261
264
|
}
|
|
262
265
|
|
|
263
266
|
// Display response
|
package/src/cli.js
CHANGED
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
keyValue,
|
|
19
19
|
bullet,
|
|
20
20
|
clearStreamedText,
|
|
21
|
+
StreamFilter,
|
|
21
22
|
getActiveTheme,
|
|
22
23
|
setTheme,
|
|
23
24
|
getThemesList,
|
|
@@ -253,19 +254,27 @@ async function handleAsk(prompt, opts) {
|
|
|
253
254
|
|
|
254
255
|
let hasStartedStreaming = false;
|
|
255
256
|
let streamedText = "";
|
|
257
|
+
const filter = !opts.raw ? new StreamFilter(process.stdout.write.bind(process.stdout)) : null;
|
|
256
258
|
const onToken = (token) => {
|
|
257
259
|
if (!hasStartedStreaming) {
|
|
258
260
|
hasStartedStreaming = true;
|
|
259
261
|
firstTokenTime = Date.now();
|
|
260
262
|
spinner.stop();
|
|
261
263
|
}
|
|
262
|
-
|
|
264
|
+
if (filter) {
|
|
265
|
+
filter.write(token);
|
|
266
|
+
} else {
|
|
267
|
+
process.stdout.write(token);
|
|
268
|
+
}
|
|
263
269
|
streamedText += token;
|
|
264
270
|
};
|
|
265
271
|
|
|
266
272
|
try {
|
|
267
273
|
const result = await routePrompt(fullPrompt, mode.systemPrompt, aiConfig, onToken);
|
|
268
274
|
spinner.stop();
|
|
275
|
+
if (filter) {
|
|
276
|
+
filter.flush();
|
|
277
|
+
}
|
|
269
278
|
|
|
270
279
|
if (opts.raw) {
|
|
271
280
|
if (!hasStartedStreaming) {
|
|
@@ -277,7 +286,7 @@ async function handleAsk(prompt, opts) {
|
|
|
277
286
|
}
|
|
278
287
|
} else {
|
|
279
288
|
if (hasStartedStreaming) {
|
|
280
|
-
clearStreamedText(streamedText);
|
|
289
|
+
clearStreamedText(filter ? filter.filteredText : streamedText);
|
|
281
290
|
}
|
|
282
291
|
console.log("");
|
|
283
292
|
console.log(label.aether + " " + colors.dim(`via ${result.provider}${result.model ? ` (${result.model})` : ""} • Node ${result.node}`));
|
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() {
|
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, 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,23 @@ 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
|
+
});
|
|
151
170
|
});
|