@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 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.4",
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
- 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)",
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
- process.stdout.write(token);
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(streamedText);
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
- process.stdout.write(token);
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
  });