@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 +13 -0
- package/package.json +1 -1
- package/src/ai/router.js +17 -0
- package/src/ai/search.js +94 -0
- package/src/ai/telemetry.js +125 -0
- package/src/chat.js +2184 -1617
- package/src/cli.js +31 -0
- package/src/dashboard.js +112 -0
- package/src/telemetry-server.js +855 -0
- package/src/ui/dashboard.html +834 -0
- package/test/autopilot-debug.test.js +91 -0
- package/test/git-tui.test.js +94 -0
- package/test/search.test.js +52 -0
- package/test/telemetry.test.js +104 -0
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
|
+
"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",
|
package/src/ai/search.js
ADDED
|
@@ -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
|
+
}
|