@krishivpb60/aether-ai-cli 1.3.3 → 1.3.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/HIGHLIGHTS.md CHANGED
@@ -1,3 +1,16 @@
1
+ # Aether CLI v1.3.5 Highlights
2
+ - **Visual Telemetry Dashboard HUD (`aether dashboard` / `aether telemetry`)**:
3
+ - Adds a local zero-dependency Web Server hosting a cyberpunk observability dashboard HUD.
4
+ - Displays real-time request latencies, query success rates, model token distributions, and active failover mesh topologies.
5
+ - Persistent storage preserves historical metrics across CLI executions in `~/.aether/telemetry.json`.
6
+ - Offline-compatible custom SVG chart engine allows telemetry visualization without an internet connection.
7
+
8
+ # Aether CLI v1.3.4 Highlights
9
+ - **AI-Powered Workspace Search & Code Indexer (`/search`)**:
10
+ - Adds `/search <query>` slash command to scan all workspace text files for keyword matches, showing line numbers and code snippets.
11
+ - Supports `/search --ai <query>` to run a semantic search using the active AI reasoning model.
12
+ - Automatically ignores binaries, files exceeding 250KB, and build/dependency/git directories.
13
+
1
14
  # Aether CLI v1.3.3 Highlights
2
15
  - **Codex & Claude Code Slash Commands**: Added 7 new advanced developer experience (DX) commands:
3
16
  - `/review`: Analyze staged/unstaged git changes and stream an AI-powered code review.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@krishivpb60/aether-ai-cli",
3
- "version": "1.3.3",
3
+ "version": "1.3.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/src/ai/router.js CHANGED
@@ -12,6 +12,7 @@ import {
12
12
  callCohere,
13
13
  } from "./universal.js";
14
14
  import { estimateTokens, recordTokenUsage } from "./tokens.js";
15
+ import { recordLatency } from "./telemetry.js";
15
16
 
16
17
  /**
17
18
  * Routes a prompt through the universal AI failover mesh.
@@ -31,11 +32,14 @@ export async function routePrompt(prompt, systemPrompt, config, onToken, history
31
32
  // ── Node 0: Local Math Solver ───────────────────────────
32
33
  const mathExpr = detectMathExpression(prompt);
33
34
  if (mathExpr) {
35
+ const startTime = performance.now();
34
36
  const mathResult = solveMath(mathExpr);
35
37
  if (mathResult) {
38
+ const latencyMs = performance.now() - startTime;
36
39
  const pTokens = estimateTokens(systemPrompt + prompt);
37
40
  const cTokens = estimateTokens(mathResult.text);
38
41
  const usage = recordTokenUsage("local-math", pTokens, cTokens);
42
+ recordLatency("local", "math-solver", latencyMs, pTokens, cTokens, true);
39
43
  return { ...mathResult, provider: "local", node: 0, usage };
40
44
  }
41
45
  }
@@ -57,10 +61,13 @@ export async function routePrompt(prompt, systemPrompt, config, onToken, history
57
61
 
58
62
  // ── No providers configured → Krylo ────────────────────
59
63
  if (active.length === 0) {
64
+ const startTime = performance.now();
60
65
  const kryloReply = generateKryloReply(prompt);
66
+ const latencyMs = performance.now() - startTime;
61
67
  const pTokens = estimateTokens(systemPrompt + prompt);
62
68
  const cTokens = estimateTokens(kryloReply.text);
63
69
  const usage = recordTokenUsage("krylo-local", pTokens, cTokens);
70
+ recordLatency("krylo-fallback", "local", latencyMs, pTokens, cTokens, true);
64
71
  return { ...kryloReply, provider: "krylo-fallback", node: 0, usage };
65
72
  }
66
73
 
@@ -69,6 +76,7 @@ export async function routePrompt(prompt, systemPrompt, config, onToken, history
69
76
  let nodeIndex = 1;
70
77
 
71
78
  for (const { id, provider, apiKey } of active) {
79
+ const startTime = performance.now();
72
80
  try {
73
81
  const model = config[`${id.toUpperCase()}_MODEL`] || provider.defaultModel;
74
82
  let result;
@@ -103,22 +111,31 @@ export async function routePrompt(prompt, systemPrompt, config, onToken, history
103
111
  );
104
112
  }
105
113
 
114
+ const latencyMs = performance.now() - startTime;
106
115
  const pTokens = estimateTokens(systemPrompt + prompt + history.map(h => h.content).join(""));
107
116
  const cTokens = estimateTokens(result.text);
108
117
  const usage = recordTokenUsage(result.model, pTokens, cTokens);
109
118
 
119
+ recordLatency(provider.name, result.model, latencyMs, pTokens, cTokens, true);
120
+
110
121
  return { ...result, node: nodeIndex, usage };
111
122
  } catch (err) {
123
+ const latencyMs = performance.now() - startTime;
124
+ const pTokens = estimateTokens(systemPrompt + prompt + history.map(h => h.content).join(""));
125
+ recordLatency(provider.name, "unknown", latencyMs, pTokens, 0, false);
112
126
  errors.push(`[Node ${nodeIndex} ${provider.name}] ${err.message}`);
113
127
  nodeIndex++;
114
128
  }
115
129
  }
116
130
 
117
131
  // ── Final Fallback: Krylo Companion ─────────────────────
132
+ const startTimeKrylo = performance.now();
118
133
  const kryloReply = generateKryloReply(prompt);
134
+ const latencyMsKrylo = performance.now() - startTimeKrylo;
119
135
  const pTokens = estimateTokens(systemPrompt + prompt + history.map(h => h.content).join(""));
120
136
  const cTokens = estimateTokens(kryloReply.text);
121
137
  const usage = recordTokenUsage("krylo-local", pTokens, cTokens);
138
+ recordLatency("krylo-fallback", "local", latencyMsKrylo, pTokens, cTokens, true);
122
139
  return {
123
140
  ...kryloReply,
124
141
  provider: "krylo-fallback",
@@ -0,0 +1,94 @@
1
+ import { readdirSync, statSync, readFileSync } from "node:fs";
2
+ import { join, sep, relative } from "node:path";
3
+
4
+ const IGNORE_DIRS = new Set([
5
+ ".git",
6
+ "node_modules",
7
+ "build",
8
+ "dist",
9
+ ".agents",
10
+ "aether_ai_agent_cli.egg-info",
11
+ "aether_ai_cli.egg-info",
12
+ "temp-test-home",
13
+ "temp-test-home-updater",
14
+ "temp-test-home-search"
15
+ ]);
16
+
17
+ const BINARY_EXTS = new Set([
18
+ ".png", ".jpg", ".jpeg", ".gif", ".ico", ".webp",
19
+ ".zip", ".tar", ".gz", ".rar", ".7z",
20
+ ".pdf", ".exe", ".dll", ".so", ".dylib", ".bin",
21
+ ".mp3", ".mp4", ".wav", ".avi", ".mov",
22
+ ".woff", ".woff2", ".ttf", ".eot"
23
+ ]);
24
+
25
+ /**
26
+ * Recursively walks a directory and yields all text files.
27
+ * @param {string} dir - Directory to scan
28
+ * @param {string} rootDir - Root directory for ignore matching
29
+ * @returns {string[]} List of absolute paths
30
+ */
31
+ export function crawlDirectory(dir, rootDir = dir) {
32
+ let files = [];
33
+ let entries = [];
34
+ try {
35
+ entries = readdirSync(dir);
36
+ } catch {
37
+ return files;
38
+ }
39
+
40
+ for (const entry of entries) {
41
+ if (IGNORE_DIRS.has(entry)) continue;
42
+ const fullPath = join(dir, entry);
43
+ try {
44
+ const stat = statSync(fullPath);
45
+ if (stat.isDirectory()) {
46
+ files = files.concat(crawlDirectory(fullPath, rootDir));
47
+ } else if (stat.isFile()) {
48
+ if (stat.size > 250 * 1024) continue; // Skip files larger than 250KB
49
+
50
+ const dotIdx = entry.lastIndexOf(".");
51
+ const ext = dotIdx !== -1 ? entry.slice(dotIdx).toLowerCase() : "";
52
+ if (BINARY_EXTS.has(ext)) continue;
53
+
54
+ files.push(fullPath);
55
+ }
56
+ } catch {
57
+ // Ignore stat failures
58
+ }
59
+ }
60
+ return files;
61
+ }
62
+
63
+ /**
64
+ * Searches all text files in the workspace for occurrences of query.
65
+ * @param {string} query - Query string
66
+ * @param {string} rootDir - The workspace root path
67
+ * @returns {Array<{ filePath: string, relativePath: string, lineNumber: number, lineContent: string }>}
68
+ */
69
+ export function workspaceSearch(query, rootDir = process.cwd()) {
70
+ const files = crawlDirectory(rootDir, rootDir);
71
+ const results = [];
72
+ const lowerQuery = query.toLowerCase().trim();
73
+
74
+ for (const file of files) {
75
+ try {
76
+ const content = readFileSync(file, "utf8");
77
+ const lines = content.split(/\r?\n/);
78
+ for (let i = 0; i < lines.length; i++) {
79
+ const line = lines[i];
80
+ if (line.toLowerCase().includes(lowerQuery)) {
81
+ results.push({
82
+ filePath: file,
83
+ relativePath: relative(rootDir, file),
84
+ lineNumber: i + 1,
85
+ lineContent: line.trim()
86
+ });
87
+ }
88
+ }
89
+ } catch {
90
+ // Ignore read failures
91
+ }
92
+ }
93
+ return results;
94
+ }
@@ -0,0 +1,125 @@
1
+ // ═══════════════════════════════════════════════════════════
2
+ // AETHER AI CLI — Telemetry & Latency Logger
3
+ // Logs request latencies, prompt speeds, and meshes provider logs.
4
+ // ═══════════════════════════════════════════════════════════
5
+
6
+ import { readFileSync, writeFileSync, existsSync, mkdirSync, unlinkSync } from "node:fs";
7
+ import { join } from "node:path";
8
+ import { homedir } from "node:os";
9
+ import { getSessionTokenStats, getBreakdownByModel } from "./tokens.js";
10
+ import { getActiveProviders, PROVIDERS } from "./providers.js";
11
+ import { listSessions } from "../config.js";
12
+
13
+ const CONFIG_DIR = join(homedir(), ".aether");
14
+ const TELEMETRY_FILE = join(CONFIG_DIR, "telemetry.json");
15
+
16
+ /**
17
+ * Loads telemetry logs from disk.
18
+ * @returns {Array}
19
+ */
20
+ function loadTelemetryFromDisk() {
21
+ try {
22
+ if (existsSync(TELEMETRY_FILE)) {
23
+ const raw = readFileSync(TELEMETRY_FILE, "utf-8");
24
+ return JSON.parse(raw);
25
+ }
26
+ } catch {
27
+ // ignore
28
+ }
29
+ return [];
30
+ }
31
+
32
+ /**
33
+ * Saves telemetry logs to disk.
34
+ * @param {Array} logs
35
+ */
36
+ function saveTelemetryToDisk(logs) {
37
+ try {
38
+ if (!existsSync(CONFIG_DIR)) {
39
+ mkdirSync(CONFIG_DIR, { recursive: true });
40
+ }
41
+ writeFileSync(TELEMETRY_FILE, JSON.stringify(logs, null, 2), "utf-8");
42
+ } catch {
43
+ // ignore
44
+ }
45
+ }
46
+
47
+ const latencyLogs = loadTelemetryFromDisk();
48
+
49
+ /**
50
+ * Records telemetry metrics for an AI call.
51
+ * @param {string} provider - The provider name
52
+ * @param {string} model - The model name
53
+ * @param {number} latencyMs - The request latency in ms
54
+ * @param {number} promptTokens - Prompt token count
55
+ * @param {number} completionTokens - Completion token count
56
+ * @param {boolean} success - Whether the call succeeded
57
+ */
58
+ export function recordLatency(provider, model, latencyMs, promptTokens, completionTokens, success) {
59
+ latencyLogs.push({
60
+ timestamp: new Date().toISOString(),
61
+ provider: provider || "unknown",
62
+ model: model || "unknown",
63
+ latencyMs: Math.round(latencyMs),
64
+ promptTokens: promptTokens || 0,
65
+ completionTokens: completionTokens || 0,
66
+ success: !!success,
67
+ });
68
+
69
+ // Limit to last 100 entries for historical visualization
70
+ if (latencyLogs.length > 100) {
71
+ latencyLogs.shift();
72
+ }
73
+
74
+ saveTelemetryToDisk(latencyLogs);
75
+ }
76
+
77
+ /**
78
+ * Returns raw latency records.
79
+ * @returns {Array}
80
+ */
81
+ export function getLatencyLogs() {
82
+ return latencyLogs;
83
+ }
84
+
85
+ /**
86
+ * Clears all telemetry logs.
87
+ */
88
+ export function clearTelemetryLogs() {
89
+ latencyLogs.length = 0;
90
+ try {
91
+ if (existsSync(TELEMETRY_FILE)) {
92
+ unlinkSync(TELEMETRY_FILE);
93
+ }
94
+ } catch {
95
+ // ignore
96
+ }
97
+ }
98
+
99
+ /**
100
+ * Gathers all metrics needed for the web dashboard visualization.
101
+ * @param {object} config - Aether configuration object
102
+ * @returns {object}
103
+ */
104
+ export function getTelemetryData(config = {}) {
105
+ // Map active status for all providers in our registry
106
+ const mesh = Object.entries(PROVIDERS).map(([id, provider]) => {
107
+ const isConfigured = !!config[provider.key];
108
+ return {
109
+ id,
110
+ name: provider.name,
111
+ configured: isConfigured,
112
+ defaultModel: provider.defaultModel,
113
+ tier: provider.tier,
114
+ description: provider.description,
115
+ };
116
+ });
117
+
118
+ return {
119
+ tokenStats: getSessionTokenStats(),
120
+ modelBreakdown: getBreakdownByModel(),
121
+ latencyLogs: [...latencyLogs],
122
+ meshStructure: mesh,
123
+ sessions: listSessions(),
124
+ };
125
+ }