@krishivpb60/aether-ai-cli 1.1.3 → 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 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",
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
- dest_dir = os.path.join('aether_pip', 'node_project')
7
- if os.path.exists(dest_dir):
8
- shutil.rmtree(dest_dir)
9
- os.makedirs(dest_dir)
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
- shutil.copyfile('package-lock.json', os.path.join(dest_dir, 'package-lock.json'))
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="1.0.0",
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)",
@@ -196,16 +196,16 @@ export async function callGoogleGemini(prompt, systemPrompt, apiKey, model = "ge
196
196
  let buffer = "";
197
197
  let finishReason = "";
198
198
 
199
- let braceCount = 0;
200
- let jsonStart = -1;
201
- let inString = false;
202
- let escape = false;
203
-
204
199
  while (true) {
205
200
  const { done, value } = await reader.read();
206
201
  if (done) break;
207
202
  buffer += decoder.decode(value, { stream: true });
208
203
 
204
+ let braceCount = 0;
205
+ let jsonStart = -1;
206
+ let inString = false;
207
+ let escape = false;
208
+
209
209
  for (let i = 0; i < buffer.length; i++) {
210
210
  const char = buffer[i];
211
211
  if (inString) {
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
- process.stdout.write(token);
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(streamedText);
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
- process.stdout.write(token);
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
  });