@krishivpb60/aether-ai-cli 1.3.2 → 1.3.4
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 +16 -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 +72 -0
- package/src/chat.js +290 -2
- package/src/config.js +1 -1
- package/test/dx.test.js +10 -0
- package/test/search.test.js +52 -0
package/HIGHLIGHTS.md
CHANGED
|
@@ -1,3 +1,19 @@
|
|
|
1
|
+
# Aether CLI v1.3.4 Highlights
|
|
2
|
+
- **AI-Powered Workspace Search & Code Indexer (`/search`)**:
|
|
3
|
+
- Adds `/search <query>` slash command to scan all workspace text files for keyword matches, showing line numbers and code snippets.
|
|
4
|
+
- Supports `/search --ai <query>` to run a semantic search using the active AI reasoning model.
|
|
5
|
+
- Automatically ignores binaries, files exceeding 250KB, and build/dependency/git directories.
|
|
6
|
+
|
|
7
|
+
# Aether CLI v1.3.3 Highlights
|
|
8
|
+
- **Codex & Claude Code Slash Commands**: Added 7 new advanced developer experience (DX) commands:
|
|
9
|
+
- `/review`: Analyze staged/unstaged git changes and stream an AI-powered code review.
|
|
10
|
+
- `/diagnose [cmd]`: Run tests/builds and automatically debug any errors.
|
|
11
|
+
- `/explain <file>`: Explains design flow and patterns in code.
|
|
12
|
+
- `/refactor <file>`: Rewrites a target file to optimize it.
|
|
13
|
+
- `/bug <file>`: Scans a file to detect logical edge case failures.
|
|
14
|
+
- `/doc <file>`: Automatically writes documentation, inline comments, or JSDoc.
|
|
15
|
+
- `/translate <file> <lang>`: AI-translates file code into another target language.
|
|
16
|
+
|
|
1
17
|
# Aether CLI v1.3.2 Highlights
|
|
2
18
|
- **Manual Updater `/update`**: Added a new slash command `/update` to manually check the registry and force-upgrade Aether CLI to the latest version immediately, bypassing the 24-hour cache throttle.
|
|
3
19
|
|
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.4",
|
|
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,72 @@
|
|
|
1
|
+
// ═══════════════════════════════════════════════════════════
|
|
2
|
+
// AETHER AI CLI — Telemetry & Latency Logger
|
|
3
|
+
// Logs request latencies, prompt speeds, and meshes provider logs.
|
|
4
|
+
// ═══════════════════════════════════════════════════════════
|
|
5
|
+
|
|
6
|
+
import { getSessionTokenStats, getBreakdownByModel } from "./tokens.js";
|
|
7
|
+
import { getActiveProviders, PROVIDERS } from "./providers.js";
|
|
8
|
+
import { listSessions } from "../config.js";
|
|
9
|
+
|
|
10
|
+
const latencyLogs = [];
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Records telemetry metrics for an AI call.
|
|
14
|
+
* @param {string} provider - The provider name
|
|
15
|
+
* @param {string} model - The model name
|
|
16
|
+
* @param {number} latencyMs - The request latency in ms
|
|
17
|
+
* @param {number} promptTokens - Prompt token count
|
|
18
|
+
* @param {number} completionTokens - Completion token count
|
|
19
|
+
* @param {boolean} success - Whether the call succeeded
|
|
20
|
+
*/
|
|
21
|
+
export function recordLatency(provider, model, latencyMs, promptTokens, completionTokens, success) {
|
|
22
|
+
latencyLogs.push({
|
|
23
|
+
timestamp: new Date().toISOString(),
|
|
24
|
+
provider: provider || "unknown",
|
|
25
|
+
model: model || "unknown",
|
|
26
|
+
latencyMs: Math.round(latencyMs),
|
|
27
|
+
promptTokens: promptTokens || 0,
|
|
28
|
+
completionTokens: completionTokens || 0,
|
|
29
|
+
success: !!success,
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
// Limit to last 50 entries
|
|
33
|
+
if (latencyLogs.length > 50) {
|
|
34
|
+
latencyLogs.shift();
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Returns raw latency records.
|
|
40
|
+
* @returns {Array}
|
|
41
|
+
*/
|
|
42
|
+
export function getLatencyLogs() {
|
|
43
|
+
return latencyLogs;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Gathers all metrics needed for the web dashboard visualization.
|
|
48
|
+
* @param {object} config - Aether configuration object
|
|
49
|
+
* @returns {object}
|
|
50
|
+
*/
|
|
51
|
+
export function getTelemetryData(config = {}) {
|
|
52
|
+
// Map active status for all providers in our registry
|
|
53
|
+
const mesh = Object.entries(PROVIDERS).map(([id, provider]) => {
|
|
54
|
+
const isConfigured = !!config[provider.key];
|
|
55
|
+
return {
|
|
56
|
+
id,
|
|
57
|
+
name: provider.name,
|
|
58
|
+
configured: isConfigured,
|
|
59
|
+
defaultModel: provider.defaultModel,
|
|
60
|
+
tier: provider.tier,
|
|
61
|
+
description: provider.description,
|
|
62
|
+
};
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
return {
|
|
66
|
+
tokenStats: getSessionTokenStats(),
|
|
67
|
+
modelBreakdown: getBreakdownByModel(),
|
|
68
|
+
latencyLogs: [...latencyLogs],
|
|
69
|
+
meshStructure: mesh,
|
|
70
|
+
sessions: listSessions(),
|
|
71
|
+
};
|
|
72
|
+
}
|
package/src/chat.js
CHANGED
|
@@ -47,6 +47,7 @@ import { runMainframeHack } from "./ai/fallback.js";
|
|
|
47
47
|
import { AGENT_INSTRUCTIONS } from "./agent.js";
|
|
48
48
|
import { checkForUpdates } from "./updater.js";
|
|
49
49
|
import { getSessionTokenStats, getBreakdownByModel, resetSessionTokenStats } from "./ai/tokens.js";
|
|
50
|
+
import { getGitDiff } from "./git.js";
|
|
50
51
|
|
|
51
52
|
|
|
52
53
|
|
|
@@ -136,7 +137,9 @@ export async function startChat(options = {}) {
|
|
|
136
137
|
"/help", "/mode", "/modes", "/attach", "/files", "/clear",
|
|
137
138
|
"/providers", "/export", "/status", "/copy", "/exit", "/quit",
|
|
138
139
|
"/theme", "/themes", "/history-clear", "/game", "/abort", "/cmd", "/write",
|
|
139
|
-
"/commit", "/run", "/history", "/autopilot", "/tokens", "/update"
|
|
140
|
+
"/commit", "/run", "/history", "/autopilot", "/tokens", "/update",
|
|
141
|
+
"/review", "/diagnose", "/explain", "/refactor", "/bug", "/doc", "/translate",
|
|
142
|
+
"/search"
|
|
140
143
|
];
|
|
141
144
|
const customCmds = aiConfig.CUSTOM_COMMANDS || {};
|
|
142
145
|
const commands = [...builtIn, ...Object.keys(customCmds)];
|
|
@@ -424,7 +427,8 @@ export async function startChat(options = {}) {
|
|
|
424
427
|
"/providers", "/export", "/status", "/copy", "/exit", "/quit",
|
|
425
428
|
"/theme", "/themes", "/history-clear", "/game", "/abort", "/cmd",
|
|
426
429
|
"/guess", "/write", "/commit", "/run", "/history", "/autopilot", "/tokens",
|
|
427
|
-
"/update"
|
|
430
|
+
"/update", "/review", "/diagnose", "/explain", "/refactor", "/bug", "/doc",
|
|
431
|
+
"/translate", "/search"
|
|
428
432
|
];
|
|
429
433
|
|
|
430
434
|
const customCmds = aiConfig.CUSTOM_COMMANDS || {};
|
|
@@ -521,6 +525,26 @@ async function handleCommand(input, ctx) {
|
|
|
521
525
|
console.log("");
|
|
522
526
|
break;
|
|
523
527
|
|
|
528
|
+
case "/review":
|
|
529
|
+
await handleReviewCommand(ctx);
|
|
530
|
+
break;
|
|
531
|
+
|
|
532
|
+
case "/diagnose":
|
|
533
|
+
await handleDiagnoseCommand(args, ctx);
|
|
534
|
+
break;
|
|
535
|
+
|
|
536
|
+
case "/explain":
|
|
537
|
+
case "/refactor":
|
|
538
|
+
case "/bug":
|
|
539
|
+
case "/doc":
|
|
540
|
+
case "/translate":
|
|
541
|
+
await handleFileAICommand(cmd, args, ctx);
|
|
542
|
+
break;
|
|
543
|
+
|
|
544
|
+
case "/search":
|
|
545
|
+
await handleSearchCommand(args, ctx);
|
|
546
|
+
break;
|
|
547
|
+
|
|
524
548
|
case "/theme":
|
|
525
549
|
await handleThemeSwitch(args);
|
|
526
550
|
break;
|
|
@@ -620,6 +644,14 @@ function showHelp(aiConfig) {
|
|
|
620
644
|
console.log(keyValue("/write <filename>", "Extract last code block and save to file"));
|
|
621
645
|
console.log(keyValue("/commit", "Generate conventional commit message and commit changes"));
|
|
622
646
|
console.log(keyValue("/run <command>", "Execute a shell command interactively"));
|
|
647
|
+
console.log(keyValue("/review", "Run git diff and stream an AI code review"));
|
|
648
|
+
console.log(keyValue("/diagnose [cmd]", "Run build/tests and AI-debug any errors"));
|
|
649
|
+
console.log(keyValue("/explain <file>", "AI-explain the design and logic of a file"));
|
|
650
|
+
console.log(keyValue("/refactor <file>", "AI-refactor the code of a target file"));
|
|
651
|
+
console.log(keyValue("/bug <file>", "AI-audit a file to find potential logic bugs"));
|
|
652
|
+
console.log(keyValue("/doc <file>", "AI-generate documentation/docstrings for a file"));
|
|
653
|
+
console.log(keyValue("/translate <file> <lang>", "AI-translate code of a file to another language"));
|
|
654
|
+
console.log(keyValue("/search <query>", "Find matches in code files (use --ai for semantic search)"));
|
|
623
655
|
console.log(keyValue("/exit", "End session"));
|
|
624
656
|
|
|
625
657
|
if (aiConfig && aiConfig.CUSTOM_COMMANDS) {
|
|
@@ -1402,3 +1434,259 @@ async function handleTokensDisplay(ctx) {
|
|
|
1402
1434
|
console.log(" " + colors.accent("Total Tokens:") + colors.text(` Prompt: ${stats.prompt.toLocaleString()} | Completion: ${stats.completion.toLocaleString()} | Sum: `) + colors.brand.bold(stats.total.toLocaleString()));
|
|
1403
1435
|
console.log(separator("━") + "\n");
|
|
1404
1436
|
}
|
|
1437
|
+
|
|
1438
|
+
/**
|
|
1439
|
+
* Streams an AI query prompt and prints telemetry details at the end.
|
|
1440
|
+
*/
|
|
1441
|
+
async function executeAISpecialCommand(prompt, specialLabel, ctx) {
|
|
1442
|
+
const systemPrompt = ctx.currentMode.systemPrompt + "\n" + AGENT_INSTRUCTIONS;
|
|
1443
|
+
let hasStarted = false;
|
|
1444
|
+
let responseText = "";
|
|
1445
|
+
const queryStartTime = Date.now();
|
|
1446
|
+
let firstTokenTime = 0;
|
|
1447
|
+
|
|
1448
|
+
const onToken = (token) => {
|
|
1449
|
+
if (!hasStarted) {
|
|
1450
|
+
hasStarted = true;
|
|
1451
|
+
firstTokenTime = Date.now();
|
|
1452
|
+
process.stdout.write("\n" + label.aether + " " + colors.accent(specialLabel) + "\n" + separator("─") + "\n\n");
|
|
1453
|
+
}
|
|
1454
|
+
process.stdout.write(colors.success(token));
|
|
1455
|
+
responseText += token;
|
|
1456
|
+
};
|
|
1457
|
+
|
|
1458
|
+
const result = await routePrompt(prompt, systemPrompt, ctx.aiConfig, onToken);
|
|
1459
|
+
console.log("\n");
|
|
1460
|
+
|
|
1461
|
+
const elapsedSec = ((Date.now() - queryStartTime) / 1000).toFixed(1);
|
|
1462
|
+
let speedText = "";
|
|
1463
|
+
if (firstTokenTime > 0) {
|
|
1464
|
+
const streamElapsed = (Date.now() - firstTokenTime) / 1000;
|
|
1465
|
+
if (streamElapsed > 0.05) {
|
|
1466
|
+
const estimatedTokens = Math.max(1, Math.round(responseText.length / 4));
|
|
1467
|
+
const tps = (estimatedTokens / streamElapsed).toFixed(1);
|
|
1468
|
+
speedText = ` • ${tps} tok/s`;
|
|
1469
|
+
}
|
|
1470
|
+
}
|
|
1471
|
+
|
|
1472
|
+
const showTokens = ctx.aiConfig.SHOW_TOKENS !== "false";
|
|
1473
|
+
let tokensText = "";
|
|
1474
|
+
if (showTokens && result.usage) {
|
|
1475
|
+
const { promptTokens, completionTokens } = result.usage;
|
|
1476
|
+
tokensText = ` • ${promptTokens.toLocaleString()} in / ${completionTokens.toLocaleString()} out tokens`;
|
|
1477
|
+
}
|
|
1478
|
+
|
|
1479
|
+
console.log(separator("─"));
|
|
1480
|
+
console.log(
|
|
1481
|
+
" " + colors.dim(`Node ${result.node} • ${result.provider}`) +
|
|
1482
|
+
(result.model ? colors.dim(` • ${result.model}`) : "") +
|
|
1483
|
+
colors.dim(` • ${elapsedSec}s${speedText}`) +
|
|
1484
|
+
colors.dim(tokensText)
|
|
1485
|
+
);
|
|
1486
|
+
console.log("");
|
|
1487
|
+
}
|
|
1488
|
+
|
|
1489
|
+
/**
|
|
1490
|
+
* Handler for the /review command (git diff analysis).
|
|
1491
|
+
*/
|
|
1492
|
+
async function handleReviewCommand(ctx) {
|
|
1493
|
+
console.log("\n" + label.system + " " + colors.muted("Running git diff to fetch repository changes..."));
|
|
1494
|
+
try {
|
|
1495
|
+
const { diff, isStaged } = await getGitDiff();
|
|
1496
|
+
if (!diff) {
|
|
1497
|
+
console.log(label.system + " " + colors.success("✓ No changes detected in the repository to review.\n"));
|
|
1498
|
+
return;
|
|
1499
|
+
}
|
|
1500
|
+
|
|
1501
|
+
const specialLabel = `Reviewing ${isStaged ? "staged" : "unstaged"} changes...`;
|
|
1502
|
+
const prompt = `Review the following git diff. Identify potential bugs, logical issues, security concerns, performance problems, and recommend optimization or code cleanup. Keep it concise, practical, and highly technical:\n\n\`\`\`diff\n${diff}\n\`\`\``;
|
|
1503
|
+
|
|
1504
|
+
await executeAISpecialCommand(prompt, specialLabel, ctx);
|
|
1505
|
+
} catch (err) {
|
|
1506
|
+
console.log(label.system + " " + colors.danger(`Error: ${err.message}\n`));
|
|
1507
|
+
}
|
|
1508
|
+
}
|
|
1509
|
+
|
|
1510
|
+
/**
|
|
1511
|
+
* Handler for the /diagnose command (build & test diagnostics execution).
|
|
1512
|
+
*/
|
|
1513
|
+
async function handleDiagnoseCommand(args, ctx) {
|
|
1514
|
+
const defaultCmd = ctx.aiConfig.DIAGNOSE_CMD || "npm test";
|
|
1515
|
+
const cmdToRun = args.join(" ").trim() || defaultCmd;
|
|
1516
|
+
|
|
1517
|
+
console.log("\n" + label.system + " " + colors.muted(`Running diagnostics command: "${cmdToRun}"...`));
|
|
1518
|
+
|
|
1519
|
+
const spinner = createSpinner("Executing diagnostics").start();
|
|
1520
|
+
try {
|
|
1521
|
+
const { exec } = await import("node:child_process");
|
|
1522
|
+
const { promisify } = await import("node:util");
|
|
1523
|
+
const execAsync = promisify(exec);
|
|
1524
|
+
await execAsync(cmdToRun);
|
|
1525
|
+
spinner.succeed("Diagnostics complete!");
|
|
1526
|
+
console.log("\n" + label.system + " " + colors.success("✓ Diagnostics clean! Build and tests passed successfully.\n"));
|
|
1527
|
+
} catch (err) {
|
|
1528
|
+
spinner.fail("Diagnostics failed!");
|
|
1529
|
+
|
|
1530
|
+
const output = (err.stdout || "") + "\n" + (err.stderr || "");
|
|
1531
|
+
console.log("\n" + label.system + " " + colors.warning(`Diagnostics returned exit code ${err.code}.`));
|
|
1532
|
+
console.log(colors.muted("Analyzing compiler/test output logs...\n"));
|
|
1533
|
+
|
|
1534
|
+
const prompt = `The diagnostics command "${cmdToRun}" failed with exit code ${err.code}. Analyze the following stdout and stderr logs to determine the root cause, identify the files/lines causing the failure, and provide a step-by-step resolution and debugging plan:\n\n\`\`\`\n${output.slice(0, 15000)}\n\`\`\``;
|
|
1535
|
+
|
|
1536
|
+
await executeAISpecialCommand(prompt, "Analyzing diagnostics logs...", ctx);
|
|
1537
|
+
}
|
|
1538
|
+
}
|
|
1539
|
+
|
|
1540
|
+
/**
|
|
1541
|
+
* Handler for file analysis commands: /explain, /refactor, /bug, /doc, /translate.
|
|
1542
|
+
*/
|
|
1543
|
+
async function handleFileAICommand(cmdName, args, ctx) {
|
|
1544
|
+
const filePath = args[0];
|
|
1545
|
+
if (!filePath) {
|
|
1546
|
+
console.log("\n" + label.system + " " + colors.warning(`Usage: ${cmdName} <file_path>\n`));
|
|
1547
|
+
return;
|
|
1548
|
+
}
|
|
1549
|
+
|
|
1550
|
+
// Resolve path
|
|
1551
|
+
const resolvedPath = resolve(process.cwd(), filePath);
|
|
1552
|
+
|
|
1553
|
+
// Verify path is inside the workspace
|
|
1554
|
+
const { isInsideWorkspace } = await import("./agent.js");
|
|
1555
|
+
if (!isInsideWorkspace(resolvedPath)) {
|
|
1556
|
+
console.log("\n" + label.system + " " + colors.danger("Error: Path is outside the current workspace sandbox.\n"));
|
|
1557
|
+
return;
|
|
1558
|
+
}
|
|
1559
|
+
|
|
1560
|
+
if (!existsSync(resolvedPath)) {
|
|
1561
|
+
console.log("\n" + label.system + " " + colors.danger(`Error: File does not exist at "${filePath}"\n`));
|
|
1562
|
+
return;
|
|
1563
|
+
}
|
|
1564
|
+
|
|
1565
|
+
const stat = statSync(resolvedPath);
|
|
1566
|
+
if (stat.isDirectory()) {
|
|
1567
|
+
console.log("\n" + label.system + " " + colors.danger(`Error: "${filePath}" is a directory. File path required.\n`));
|
|
1568
|
+
return;
|
|
1569
|
+
}
|
|
1570
|
+
|
|
1571
|
+
if (stat.size > 150 * 1024) { // 150KB limit
|
|
1572
|
+
console.log("\n" + label.system + " " + colors.warning(`Warning: File "${filePath}" is too large (${Math.round(stat.size / 1024)}KB). Limits are 150KB to protect context limit.\n`));
|
|
1573
|
+
return;
|
|
1574
|
+
}
|
|
1575
|
+
|
|
1576
|
+
// Read file content
|
|
1577
|
+
let content;
|
|
1578
|
+
try {
|
|
1579
|
+
const { parseFile } = await import("./file-parser.js");
|
|
1580
|
+
const parsed = await parseFile(resolvedPath);
|
|
1581
|
+
content = parsed.content;
|
|
1582
|
+
} catch (err) {
|
|
1583
|
+
console.log("\n" + label.system + " " + colors.danger(`Error parsing file: ${err.message}\n`));
|
|
1584
|
+
return;
|
|
1585
|
+
}
|
|
1586
|
+
|
|
1587
|
+
let prompt = "";
|
|
1588
|
+
let labelText = "";
|
|
1589
|
+
|
|
1590
|
+
switch (cmdName.toLowerCase()) {
|
|
1591
|
+
case "/explain":
|
|
1592
|
+
labelText = `Explaining ${filePath}...`;
|
|
1593
|
+
prompt = `Explain the architecture, design patterns, logic flow, and purpose of the following code. Be clear, technical, and structured:\n\n\`\`\`\n${content}\n\`\`\``;
|
|
1594
|
+
break;
|
|
1595
|
+
case "/refactor":
|
|
1596
|
+
labelText = `Refactoring ${filePath}...`;
|
|
1597
|
+
prompt = `Suggest refactoring improvements for the following code. Focus on clean code design principles, optimization, readability, reducing complexity, and fixing potential logic bugs. Return both the refactored code block and explanations:\n\n\`\`\`\n${content}\n\`\`\``;
|
|
1598
|
+
break;
|
|
1599
|
+
case "/bug":
|
|
1600
|
+
labelText = `Auditing bugs in ${filePath}...`;
|
|
1601
|
+
prompt = `Perform a thorough static analysis and code review of the following code. Identify potential logical bugs, race conditions, edge case failures, performance bottlenecks, and security hazards. Suggest fixes:\n\n\`\`\`\n${content}\n\`\`\``;
|
|
1602
|
+
break;
|
|
1603
|
+
case "/doc":
|
|
1604
|
+
labelText = `Generating documentation for ${filePath}...`;
|
|
1605
|
+
prompt = `Generate comprehensive API documentation, JSDoc/docstrings, and comments for the following code. Ensure code parameters, return values, and types are documented:\n\n\`\`\`\n${content}\n\`\`\``;
|
|
1606
|
+
break;
|
|
1607
|
+
case "/translate":
|
|
1608
|
+
const targetLang = args[1];
|
|
1609
|
+
if (!targetLang) {
|
|
1610
|
+
console.log("\n" + label.system + " " + colors.warning(`Usage: /translate <file_path> <target_language>\n`));
|
|
1611
|
+
return;
|
|
1612
|
+
}
|
|
1613
|
+
labelText = `Translating ${filePath} to ${targetLang}...`;
|
|
1614
|
+
prompt = `Translate the following code into ${targetLang}. Return a clean, syntactically correct, and beautifully structured code block of the translated code:\n\n\`\`\`\n${content}\n\`\`\``;
|
|
1615
|
+
break;
|
|
1616
|
+
}
|
|
1617
|
+
|
|
1618
|
+
try {
|
|
1619
|
+
await executeAISpecialCommand(prompt, labelText, ctx);
|
|
1620
|
+
} catch (err) {
|
|
1621
|
+
console.log("\n" + label.system + " " + colors.danger(`Error: ${err.message}\n`));
|
|
1622
|
+
}
|
|
1623
|
+
}
|
|
1624
|
+
|
|
1625
|
+
/**
|
|
1626
|
+
* Handler for the /search command (workspace file crawler and AI semantic finder).
|
|
1627
|
+
*/
|
|
1628
|
+
async function handleSearchCommand(args, ctx) {
|
|
1629
|
+
const isAi = args[0] === "--ai";
|
|
1630
|
+
const queryArgs = isAi ? args.slice(1) : args;
|
|
1631
|
+
const query = queryArgs.join(" ").trim();
|
|
1632
|
+
|
|
1633
|
+
if (!query) {
|
|
1634
|
+
console.log("\n" + label.system + " " + colors.warning("Usage: /search [--ai] <query_string>\n"));
|
|
1635
|
+
return;
|
|
1636
|
+
}
|
|
1637
|
+
|
|
1638
|
+
const { workspaceSearch, crawlDirectory } = await import("./search.js");
|
|
1639
|
+
|
|
1640
|
+
if (isAi) {
|
|
1641
|
+
console.log("\n" + label.system + " " + colors.muted("Scanning workspace project tree for semantic search..."));
|
|
1642
|
+
const files = crawlDirectory(process.cwd());
|
|
1643
|
+
const { relative } = await import("node:path");
|
|
1644
|
+
const relativePaths = files.map((f) => relative(process.cwd(), f).replace(/\\/g, "/"));
|
|
1645
|
+
|
|
1646
|
+
// Construct semantic prompt
|
|
1647
|
+
const prompt = `Here is the directory structure / file listing of the current workspace:\n\n${relativePaths.slice(0, 100).join("\n")}\n\nBased on this file listing, identify and explain where the following logic or system is implemented, listing the relevant files: ${query}`;
|
|
1648
|
+
|
|
1649
|
+
await executeAISpecialCommand(prompt, `Semantic search: "${query}"`, ctx);
|
|
1650
|
+
return;
|
|
1651
|
+
}
|
|
1652
|
+
|
|
1653
|
+
console.log("\n" + label.system + " " + colors.muted(`Searching workspace for "${query}"...`));
|
|
1654
|
+
const results = workspaceSearch(query);
|
|
1655
|
+
|
|
1656
|
+
if (results.length === 0) {
|
|
1657
|
+
console.log("\n" + label.system + " " + colors.warning(`✓ No matches found for "${query}" in workspace.\n`));
|
|
1658
|
+
return;
|
|
1659
|
+
}
|
|
1660
|
+
|
|
1661
|
+
console.log("\n" + separator("━"));
|
|
1662
|
+
console.log(colors.accent.bold(` ★ WORKSPACE SEARCH RESULTS FOR "${query.toUpperCase()}" ★`));
|
|
1663
|
+
console.log(separator("─"));
|
|
1664
|
+
|
|
1665
|
+
// Print header
|
|
1666
|
+
console.log(
|
|
1667
|
+
colors.brand(" " + "File Path".padEnd(45) + "Line".padStart(6) + " " + "Preview")
|
|
1668
|
+
);
|
|
1669
|
+
console.log(colors.dim(" " + "─".repeat(80)));
|
|
1670
|
+
|
|
1671
|
+
// Display matches (limit to top 50 to prevent terminal overflow)
|
|
1672
|
+
const displayLimit = 50;
|
|
1673
|
+
const visibleResults = results.slice(0, displayLimit);
|
|
1674
|
+
|
|
1675
|
+
for (const match of visibleResults) {
|
|
1676
|
+
const truncatedPath = match.relativePath.length > 43 ? "..." + match.relativePath.slice(-40) : match.relativePath;
|
|
1677
|
+
const truncatedLine = match.lineContent.length > 50 ? match.lineContent.slice(0, 47) + "..." : match.lineContent;
|
|
1678
|
+
console.log(
|
|
1679
|
+
" " + colors.text(truncatedPath.padEnd(45)) +
|
|
1680
|
+
colors.brand(match.lineNumber.toString().padStart(6)) +
|
|
1681
|
+
" " + colors.muted(truncatedLine)
|
|
1682
|
+
);
|
|
1683
|
+
}
|
|
1684
|
+
|
|
1685
|
+
console.log(separator("─"));
|
|
1686
|
+
if (results.length > displayLimit) {
|
|
1687
|
+
console.log(" " + colors.warning(`⚠ Showing first ${displayLimit} of ${results.length} total matches.`));
|
|
1688
|
+
} else {
|
|
1689
|
+
console.log(" " + colors.success(`✓ Found ${results.length} matches across the workspace.`));
|
|
1690
|
+
}
|
|
1691
|
+
console.log(separator("━") + "\n");
|
|
1692
|
+
}
|
package/src/config.js
CHANGED
|
@@ -176,7 +176,7 @@ export function isValidConfigKey(key) {
|
|
|
176
176
|
const allowedSpecialKeys = [
|
|
177
177
|
"THEME", "CUSTOM_COMMANDS", "AUTOPILOT",
|
|
178
178
|
"AUTO_UPDATE", "SHOW_HIGHLIGHTS", "LAST_UPDATE_CHECK", "LAST_NOTIFIED_VERSION",
|
|
179
|
-
"SHOW_TOKENS"
|
|
179
|
+
"SHOW_TOKENS", "DIAGNOSE_CMD"
|
|
180
180
|
];
|
|
181
181
|
if (upper.endsWith("_API_KEY") || upper.endsWith("_API_KEYS") || upper.endsWith("_MODEL") || allowedSpecialKeys.includes(upper)) {
|
|
182
182
|
return true;
|
package/test/dx.test.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { test } from "node:test";
|
|
2
|
+
import assert from "node:assert";
|
|
3
|
+
import { isValidConfigKey } from "../src/config.js";
|
|
4
|
+
|
|
5
|
+
test("Developer Experience (DX) Commands Suite", async (t) => {
|
|
6
|
+
await t.test("isValidConfigKey whitelists DIAGNOSE_CMD correctly", () => {
|
|
7
|
+
assert.strictEqual(isValidConfigKey("DIAGNOSE_CMD"), true);
|
|
8
|
+
assert.strictEqual(isValidConfigKey("diagnose_cmd"), true);
|
|
9
|
+
});
|
|
10
|
+
});
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { test, before, after } from "node:test";
|
|
2
|
+
import assert from "node:assert";
|
|
3
|
+
import { mkdir, writeFile, rm } from "node:fs/promises";
|
|
4
|
+
import { join, relative } from "node:path";
|
|
5
|
+
import { workspaceSearch, crawlDirectory } from "../src/ai/search.js";
|
|
6
|
+
|
|
7
|
+
const testDir = join(process.cwd(), "temp-test-home-search");
|
|
8
|
+
|
|
9
|
+
test("Workspace Search Engine Suite", async (t) => {
|
|
10
|
+
before(async () => {
|
|
11
|
+
await mkdir(testDir, { recursive: true });
|
|
12
|
+
// Write test files
|
|
13
|
+
await writeFile(join(testDir, "file1.txt"), "hello world\nthis is a search test\nbye");
|
|
14
|
+
await writeFile(join(testDir, "file2.js"), "function test() {\n console.log('hello search');\n}");
|
|
15
|
+
await writeFile(join(testDir, "file3.png"), "binary data matches nothing");
|
|
16
|
+
|
|
17
|
+
const subDir = join(testDir, "subdir");
|
|
18
|
+
await mkdir(subDir, { recursive: true });
|
|
19
|
+
await writeFile(join(subDir, "file4.txt"), "nested matches here under search term");
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
after(async () => {
|
|
23
|
+
await rm(testDir, { recursive: true, force: true });
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
await t.test("crawlDirectory finds all text files and excludes binaries/ignored folders", () => {
|
|
27
|
+
const files = crawlDirectory(testDir, testDir);
|
|
28
|
+
const relativePaths = files.map((f) => relative(testDir, f).replace(/\\/g, "/"));
|
|
29
|
+
|
|
30
|
+
assert.ok(relativePaths.includes("file1.txt"));
|
|
31
|
+
assert.ok(relativePaths.includes("file2.js"));
|
|
32
|
+
assert.ok(relativePaths.includes("subdir/file4.txt"));
|
|
33
|
+
// Should exclude png
|
|
34
|
+
assert.strictEqual(relativePaths.includes("file3.png"), false);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
await t.test("workspaceSearch returns matching files, lines, and content", () => {
|
|
38
|
+
const results = workspaceSearch("search", testDir);
|
|
39
|
+
// Should match in file1.txt, file2.js, and subdir/file4.txt
|
|
40
|
+
assert.strictEqual(results.length, 3);
|
|
41
|
+
|
|
42
|
+
const file1Match = results.find((r) => r.relativePath.replace(/\\/g, "/") === "file1.txt");
|
|
43
|
+
assert.ok(file1Match);
|
|
44
|
+
assert.strictEqual(file1Match.lineNumber, 2);
|
|
45
|
+
assert.strictEqual(file1Match.lineContent, "this is a search test");
|
|
46
|
+
|
|
47
|
+
const jsMatch = results.find((r) => r.relativePath.replace(/\\/g, "/") === "file2.js");
|
|
48
|
+
assert.ok(jsMatch);
|
|
49
|
+
assert.strictEqual(jsMatch.lineNumber, 2);
|
|
50
|
+
assert.strictEqual(jsMatch.lineContent, "console.log('hello search');");
|
|
51
|
+
});
|
|
52
|
+
});
|