krims-code-cli 1.5.2__py3-none-any.whl
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.
- krims_code_cli-1.5.2.dist-info/METADATA +336 -0
- krims_code_cli-1.5.2.dist-info/RECORD +43 -0
- krims_code_cli-1.5.2.dist-info/WHEEL +5 -0
- krims_code_cli-1.5.2.dist-info/entry_points.txt +2 -0
- krims_code_cli-1.5.2.dist-info/licenses/LICENSE +21 -0
- krims_code_cli-1.5.2.dist-info/top_level.txt +1 -0
- krims_pip/__init__.py +1 -0
- krims_pip/cli.py +50 -0
- krims_pip/node_project/bin/krims-code.js +10 -0
- krims_pip/node_project/package-lock.json +793 -0
- krims_pip/node_project/package.json +46 -0
- krims_pip/node_project/src/agent.js +415 -0
- krims_pip/node_project/src/ai/fallback.js +239 -0
- krims_pip/node_project/src/ai/google.js +87 -0
- krims_pip/node_project/src/ai/providers.js +203 -0
- krims_pip/node_project/src/ai/router.js +146 -0
- krims_pip/node_project/src/ai/search.js +94 -0
- krims_pip/node_project/src/ai/telemetry.js +125 -0
- krims_pip/node_project/src/ai/tokens.js +101 -0
- krims_pip/node_project/src/ai/universal.js +507 -0
- krims_pip/node_project/src/ai/xai.js +50 -0
- krims_pip/node_project/src/chat.js +2543 -0
- krims_pip/node_project/src/cli.js +632 -0
- krims_pip/node_project/src/commands/commit.js +157 -0
- krims_pip/node_project/src/commands/dashboard.js +59 -0
- krims_pip/node_project/src/commands/github.js +349 -0
- krims_pip/node_project/src/commands/index.js +67 -0
- krims_pip/node_project/src/commands/status.js +66 -0
- krims_pip/node_project/src/commands/teamwork-preview.js +326 -0
- krims_pip/node_project/src/commands/theme.js +42 -0
- krims_pip/node_project/src/commands/themes.js +30 -0
- krims_pip/node_project/src/config.js +400 -0
- krims_pip/node_project/src/dashboard.js +112 -0
- krims_pip/node_project/src/file-parser.js +164 -0
- krims_pip/node_project/src/git.js +41 -0
- krims_pip/node_project/src/mic.js +220 -0
- krims_pip/node_project/src/modes.js +92 -0
- krims_pip/node_project/src/telemetry-server.js +855 -0
- krims_pip/node_project/src/ui/banner.js +204 -0
- krims_pip/node_project/src/ui/dashboard.html +834 -0
- krims_pip/node_project/src/ui/spinner.js +43 -0
- krims_pip/node_project/src/ui/theme.js +624 -0
- krims_pip/node_project/src/updater.js +196 -0
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// ═══════════════════════════════════════════════════════════
|
|
2
|
+
// Krims Code AI CLI — Google Gemini API Provider
|
|
3
|
+
// ═══════════════════════════════════════════════════════════
|
|
4
|
+
|
|
5
|
+
const BASE_URL = "https://generativelanguage.googleapis.com/v1beta/models";
|
|
6
|
+
const MAX_CONTINUATIONS = 3;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Sends a prompt to the Google Gemini API.
|
|
10
|
+
* Handles continuation if finishReason is MAX_TOKENS.
|
|
11
|
+
* @param {string} prompt - The user message
|
|
12
|
+
* @param {string} systemPrompt - System prompt for the mode
|
|
13
|
+
* @param {string} apiKey - Google API key
|
|
14
|
+
* @param {string} [model='gemini-2.5-flash'] - Model name
|
|
15
|
+
* @returns {Promise<{ text: string, provider: string, model: string }>}
|
|
16
|
+
*/
|
|
17
|
+
export async function callGemini(prompt, systemPrompt, apiKey, model = "gemini-2.5-flash") {
|
|
18
|
+
let fullText = "";
|
|
19
|
+
let currentPrompt = prompt;
|
|
20
|
+
let continuations = 0;
|
|
21
|
+
|
|
22
|
+
while (continuations <= MAX_CONTINUATIONS) {
|
|
23
|
+
const url = `${BASE_URL}/${model}:generateContent?key=${apiKey}`;
|
|
24
|
+
|
|
25
|
+
const body = {
|
|
26
|
+
systemInstruction: {
|
|
27
|
+
parts: [{ text: systemPrompt }],
|
|
28
|
+
},
|
|
29
|
+
contents: [
|
|
30
|
+
{
|
|
31
|
+
role: "user",
|
|
32
|
+
parts: [{ text: currentPrompt }],
|
|
33
|
+
},
|
|
34
|
+
],
|
|
35
|
+
generationConfig: {
|
|
36
|
+
temperature: 0.7,
|
|
37
|
+
maxOutputTokens: 8192,
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const response = await fetch(url, {
|
|
42
|
+
method: "POST",
|
|
43
|
+
headers: { "Content-Type": "application/json" },
|
|
44
|
+
body: JSON.stringify(body),
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
if (!response.ok) {
|
|
48
|
+
const errorBody = await response.text().catch(() => "");
|
|
49
|
+
throw new Error(
|
|
50
|
+
`Gemini API error (${response.status}): ${response.statusText}. ${errorBody}`
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const data = await response.json();
|
|
55
|
+
|
|
56
|
+
// Check for blocked content
|
|
57
|
+
if (data.promptFeedback?.blockReason) {
|
|
58
|
+
throw new Error(`Content blocked: ${data.promptFeedback.blockReason}`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const candidate = data.candidates?.[0];
|
|
62
|
+
if (!candidate) {
|
|
63
|
+
throw new Error("Gemini API returned no candidates");
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const chunkText = candidate.content?.parts
|
|
67
|
+
?.map((p) => p.text)
|
|
68
|
+
.filter(Boolean)
|
|
69
|
+
.join("") || "";
|
|
70
|
+
|
|
71
|
+
fullText += chunkText;
|
|
72
|
+
|
|
73
|
+
// Check if the response was cut short
|
|
74
|
+
if (candidate.finishReason === "MAX_TOKENS" && continuations < MAX_CONTINUATIONS) {
|
|
75
|
+
continuations++;
|
|
76
|
+
currentPrompt = "Continue your previous response from exactly where you left off.";
|
|
77
|
+
} else {
|
|
78
|
+
break;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (!fullText.trim()) {
|
|
83
|
+
throw new Error("Gemini API returned empty response");
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return { text: fullText, provider: "google", model };
|
|
87
|
+
}
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
// ═══════════════════════════════════════════════════════════
|
|
2
|
+
// Krims Code AI CLI — Universal Provider Registry
|
|
3
|
+
// Supports ANY OpenAI-compatible API + custom providers
|
|
4
|
+
// ═══════════════════════════════════════════════════════════
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Registry of all supported AI providers.
|
|
8
|
+
* Each provider defines its API format, base URL, default model, and pricing tier.
|
|
9
|
+
*
|
|
10
|
+
* Providers with `format: "openai"` use the standard /v1/chat/completions endpoint.
|
|
11
|
+
* Providers with `format: "custom"` have their own handler in dedicated files.
|
|
12
|
+
*/
|
|
13
|
+
export const PROVIDERS = {
|
|
14
|
+
// ── Free Tier Providers ─────────────────────────────────
|
|
15
|
+
groq: {
|
|
16
|
+
name: "Groq",
|
|
17
|
+
key: "GROQ_API_KEY",
|
|
18
|
+
format: "openai",
|
|
19
|
+
baseUrl: "https://api.groq.com/openai/v1/chat/completions",
|
|
20
|
+
defaultModel: "llama-3.3-70b-versatile",
|
|
21
|
+
models: ["llama-3.3-70b-versatile", "llama-3.1-8b-instant", "mixtral-8x7b-32768", "gemma2-9b-it"],
|
|
22
|
+
tier: "free",
|
|
23
|
+
description: "Ultra-fast inference on Llama, Mixtral, Gemma (generous free tier)",
|
|
24
|
+
},
|
|
25
|
+
together: {
|
|
26
|
+
name: "Together AI",
|
|
27
|
+
key: "TOGETHER_API_KEY",
|
|
28
|
+
format: "openai",
|
|
29
|
+
baseUrl: "https://api.together.xyz/v1/chat/completions",
|
|
30
|
+
defaultModel: "meta-llama/Meta-Llama-3.1-70B-Instruct-Turbo",
|
|
31
|
+
models: ["meta-llama/Meta-Llama-3.1-70B-Instruct-Turbo", "mistralai/Mixtral-8x7B-Instruct-v0.1", "Qwen/Qwen2.5-72B-Instruct-Turbo"],
|
|
32
|
+
tier: "free",
|
|
33
|
+
description: "Open-source models with free credits on signup",
|
|
34
|
+
},
|
|
35
|
+
cerebras: {
|
|
36
|
+
name: "Cerebras",
|
|
37
|
+
key: "CEREBRAS_API_KEY",
|
|
38
|
+
format: "openai",
|
|
39
|
+
baseUrl: "https://api.cerebras.ai/v1/chat/completions",
|
|
40
|
+
defaultModel: "llama-3.3-70b",
|
|
41
|
+
models: ["llama-3.3-70b", "llama-3.1-8b"],
|
|
42
|
+
tier: "free",
|
|
43
|
+
description: "Fastest inference engine — free tier available",
|
|
44
|
+
},
|
|
45
|
+
|
|
46
|
+
// ── OpenAI-Compatible Providers ─────────────────────────
|
|
47
|
+
openai: {
|
|
48
|
+
name: "OpenAI",
|
|
49
|
+
key: "OPENAI_API_KEY",
|
|
50
|
+
format: "openai",
|
|
51
|
+
baseUrl: "https://api.openai.com/v1/chat/completions",
|
|
52
|
+
defaultModel: "gpt-4o",
|
|
53
|
+
models: ["gpt-4o", "gpt-4o-mini", "gpt-4-turbo", "gpt-3.5-turbo", "o1-mini", "o3-mini"],
|
|
54
|
+
tier: "paid",
|
|
55
|
+
description: "GPT-4o, GPT-4, o1 reasoning models",
|
|
56
|
+
},
|
|
57
|
+
mistral: {
|
|
58
|
+
name: "Mistral AI",
|
|
59
|
+
key: "MISTRAL_API_KEY",
|
|
60
|
+
format: "openai",
|
|
61
|
+
baseUrl: "https://api.mistral.ai/v1/chat/completions",
|
|
62
|
+
defaultModel: "mistral-large-latest",
|
|
63
|
+
models: ["mistral-large-latest", "mistral-medium-latest", "mistral-small-latest", "open-mistral-nemo"],
|
|
64
|
+
tier: "paid",
|
|
65
|
+
description: "Mistral Large, Medium, and open-weight models",
|
|
66
|
+
},
|
|
67
|
+
fireworks: {
|
|
68
|
+
name: "Fireworks AI",
|
|
69
|
+
key: "FIREWORKS_API_KEY",
|
|
70
|
+
format: "openai",
|
|
71
|
+
baseUrl: "https://api.fireworks.ai/inference/v1/chat/completions",
|
|
72
|
+
defaultModel: "accounts/fireworks/models/llama-v3p1-70b-instruct",
|
|
73
|
+
models: ["accounts/fireworks/models/llama-v3p1-70b-instruct", "accounts/fireworks/models/mixtral-8x7b-instruct"],
|
|
74
|
+
tier: "free",
|
|
75
|
+
description: "Fast open-source model hosting with free tier",
|
|
76
|
+
},
|
|
77
|
+
openrouter: {
|
|
78
|
+
name: "OpenRouter",
|
|
79
|
+
key: "OPENROUTER_API_KEY",
|
|
80
|
+
format: "openai",
|
|
81
|
+
baseUrl: "https://openrouter.ai/api/v1/chat/completions",
|
|
82
|
+
defaultModel: "meta-llama/llama-3.1-70b-instruct:free",
|
|
83
|
+
models: ["meta-llama/llama-3.1-70b-instruct:free", "google/gemini-2.5-flash-preview:free", "mistralai/mistral-7b-instruct:free", "anthropic/claude-sonnet-4", "openai/gpt-4o"],
|
|
84
|
+
tier: "free+paid",
|
|
85
|
+
description: "Gateway to 200+ models — many free options available",
|
|
86
|
+
},
|
|
87
|
+
deepseek: {
|
|
88
|
+
name: "DeepSeek",
|
|
89
|
+
key: "DEEPSEEK_API_KEY",
|
|
90
|
+
format: "openai",
|
|
91
|
+
baseUrl: "https://api.deepseek.com/v1/chat/completions",
|
|
92
|
+
defaultModel: "deepseek-chat",
|
|
93
|
+
models: ["deepseek-chat", "deepseek-reasoner"],
|
|
94
|
+
tier: "paid",
|
|
95
|
+
description: "DeepSeek-V3 and R1 reasoning model",
|
|
96
|
+
},
|
|
97
|
+
perplexity: {
|
|
98
|
+
name: "Perplexity",
|
|
99
|
+
key: "PERPLEXITY_API_KEY",
|
|
100
|
+
format: "openai",
|
|
101
|
+
baseUrl: "https://api.perplexity.ai/chat/completions",
|
|
102
|
+
defaultModel: "sonar",
|
|
103
|
+
models: ["sonar", "sonar-pro", "sonar-reasoning"],
|
|
104
|
+
tier: "paid",
|
|
105
|
+
description: "Search-augmented AI with real-time web access",
|
|
106
|
+
},
|
|
107
|
+
|
|
108
|
+
// ── Custom Format Providers ─────────────────────────────
|
|
109
|
+
xai: {
|
|
110
|
+
name: "xAI Grok",
|
|
111
|
+
key: "XAI_API_KEY",
|
|
112
|
+
format: "openai",
|
|
113
|
+
baseUrl: "https://api.x.ai/v1/chat/completions",
|
|
114
|
+
defaultModel: "grok-2",
|
|
115
|
+
models: ["grok-2", "grok-2-mini"],
|
|
116
|
+
tier: "paid",
|
|
117
|
+
description: "Grok-2 by xAI — witty, uncensored",
|
|
118
|
+
},
|
|
119
|
+
google: {
|
|
120
|
+
name: "Google Gemini",
|
|
121
|
+
key: "GOOGLE_API_KEY",
|
|
122
|
+
format: "custom-google",
|
|
123
|
+
baseUrl: "https://generativelanguage.googleapis.com/v1beta/models",
|
|
124
|
+
defaultModel: "gemini-2.5-flash",
|
|
125
|
+
models: ["gemini-2.5-flash", "gemini-2.5-pro", "gemini-2.0-flash", "gemini-1.5-pro"],
|
|
126
|
+
tier: "free+paid",
|
|
127
|
+
description: "Gemini 2.5 Flash/Pro — free tier with generous limits",
|
|
128
|
+
},
|
|
129
|
+
anthropic: {
|
|
130
|
+
name: "Anthropic Claude",
|
|
131
|
+
key: "ANTHROPIC_API_KEY",
|
|
132
|
+
format: "custom-anthropic",
|
|
133
|
+
baseUrl: "https://api.anthropic.com/v1/messages",
|
|
134
|
+
defaultModel: "claude-sonnet-4-20250514",
|
|
135
|
+
models: ["claude-sonnet-4-20250514", "claude-3-5-haiku-20241022", "claude-3-5-sonnet-20241022"],
|
|
136
|
+
tier: "paid",
|
|
137
|
+
description: "Claude Sonnet 4, Haiku — premium reasoning",
|
|
138
|
+
},
|
|
139
|
+
cohere: {
|
|
140
|
+
name: "Cohere",
|
|
141
|
+
key: "COHERE_API_KEY",
|
|
142
|
+
format: "custom-cohere",
|
|
143
|
+
baseUrl: "https://api.cohere.com/v2/chat",
|
|
144
|
+
defaultModel: "command-r-plus",
|
|
145
|
+
models: ["command-r-plus", "command-r", "command-light"],
|
|
146
|
+
tier: "free+paid",
|
|
147
|
+
description: "Command R+ — free tier for developers",
|
|
148
|
+
},
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Gets a flat list of all provider keys that can be configured.
|
|
153
|
+
* @returns {string[]}
|
|
154
|
+
*/
|
|
155
|
+
export function getAllConfigKeys() {
|
|
156
|
+
const keys = new Set();
|
|
157
|
+
for (const p of Object.values(PROVIDERS)) {
|
|
158
|
+
keys.add(p.key);
|
|
159
|
+
}
|
|
160
|
+
// Also include multi-key support for Google
|
|
161
|
+
keys.add("GOOGLE_API_KEYS");
|
|
162
|
+
return [...keys];
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Gets a provider by its config key name (e.g., "OPENAI_API_KEY" → openai provider).
|
|
167
|
+
* @param {string} configKey
|
|
168
|
+
* @returns {object|null}
|
|
169
|
+
*/
|
|
170
|
+
export function getProviderByKey(configKey) {
|
|
171
|
+
for (const [id, p] of Object.entries(PROVIDERS)) {
|
|
172
|
+
if (p.key === configKey) return { id, ...p };
|
|
173
|
+
}
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Gets all providers that have a valid key in the given config.
|
|
179
|
+
* @param {object} config - Config object with API keys
|
|
180
|
+
* @returns {Array<{ id: string, provider: object, apiKey: string }>}
|
|
181
|
+
*/
|
|
182
|
+
export function getActiveProviders(config) {
|
|
183
|
+
const active = [];
|
|
184
|
+
for (const [id, provider] of Object.entries(PROVIDERS)) {
|
|
185
|
+
const apiKey = config[provider.key];
|
|
186
|
+
if (apiKey) {
|
|
187
|
+
active.push({ id, provider, apiKey });
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
return active;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Groups providers by pricing tier for display.
|
|
195
|
+
* @returns {{ free: object[], paid: object[], mixed: object[] }}
|
|
196
|
+
*/
|
|
197
|
+
export function getProvidersByTier() {
|
|
198
|
+
const result = { free: [], "free+paid": [], paid: [] };
|
|
199
|
+
for (const [id, provider] of Object.entries(PROVIDERS)) {
|
|
200
|
+
result[provider.tier]?.push({ id, ...provider });
|
|
201
|
+
}
|
|
202
|
+
return result;
|
|
203
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
// ═══════════════════════════════════════════════════════════
|
|
2
|
+
// Krims Code AI CLI — Universal AI Router with Failover Mesh
|
|
3
|
+
// Routes through ALL configured providers automatically
|
|
4
|
+
// ═══════════════════════════════════════════════════════════
|
|
5
|
+
|
|
6
|
+
import { detectMathExpression, solveMath, generateOfflineReply } from "./fallback.js";
|
|
7
|
+
import { PROVIDERS, getActiveProviders } from "./providers.js";
|
|
8
|
+
import {
|
|
9
|
+
callOpenAICompatible,
|
|
10
|
+
callGoogleGemini,
|
|
11
|
+
callAnthropic,
|
|
12
|
+
callCohere,
|
|
13
|
+
} from "./universal.js";
|
|
14
|
+
import { estimateTokens, recordTokenUsage } from "./tokens.js";
|
|
15
|
+
import { recordLatency } from "./telemetry.js";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Routes a prompt through the universal AI failover mesh.
|
|
19
|
+
*
|
|
20
|
+
* Priority Order:
|
|
21
|
+
* 1. Local math solver (if pure math expression)
|
|
22
|
+
* 2. All configured providers, in order of priority
|
|
23
|
+
* 3. Google key rotation (if multiple keys)
|
|
24
|
+
* 4. Krylo companion fallback (if everything else fails)
|
|
25
|
+
*
|
|
26
|
+
* @param {string} prompt - The user prompt
|
|
27
|
+
* @param {string} systemPrompt - The mode system prompt
|
|
28
|
+
* @param {object} config - Flat config object with all API keys
|
|
29
|
+
* @returns {Promise<{ text: string, provider: string, model?: string, node: number, type?: string }>}
|
|
30
|
+
*/
|
|
31
|
+
export async function routePrompt(prompt, systemPrompt, config, onToken, history = []) {
|
|
32
|
+
// ── Node 0: Local Math Solver ───────────────────────────
|
|
33
|
+
const mathExpr = detectMathExpression(prompt);
|
|
34
|
+
if (mathExpr) {
|
|
35
|
+
const startTime = performance.now();
|
|
36
|
+
const mathResult = solveMath(mathExpr);
|
|
37
|
+
if (mathResult) {
|
|
38
|
+
const latencyMs = performance.now() - startTime;
|
|
39
|
+
const pTokens = estimateTokens(systemPrompt + prompt);
|
|
40
|
+
const cTokens = estimateTokens(mathResult.text);
|
|
41
|
+
const usage = recordTokenUsage("local-math", pTokens, cTokens);
|
|
42
|
+
recordLatency("local", "math-solver", latencyMs, pTokens, cTokens, true);
|
|
43
|
+
return { ...mathResult, provider: "local", node: 0, usage };
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// ── Gather all active providers ─────────────────────────
|
|
48
|
+
const active = getActiveProviders(config);
|
|
49
|
+
|
|
50
|
+
// Add extra Google keys for rotation
|
|
51
|
+
const googleExtraKeys = config.GOOGLE_API_KEYS;
|
|
52
|
+
if (googleExtraKeys) {
|
|
53
|
+
const extras = googleExtraKeys.split(",").map((k) => k.trim()).filter(Boolean);
|
|
54
|
+
for (const key of extras) {
|
|
55
|
+
// Avoid duplicates
|
|
56
|
+
if (!active.some((a) => a.id === "google" && a.apiKey === key)) {
|
|
57
|
+
active.push({ id: "google-extra", provider: PROVIDERS.google, apiKey: key });
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ── No providers configured → Offline ───────────────────
|
|
63
|
+
if (active.length === 0) {
|
|
64
|
+
const startTime = performance.now();
|
|
65
|
+
const offlineReply = generateOfflineReply(prompt);
|
|
66
|
+
const latencyMs = performance.now() - startTime;
|
|
67
|
+
const pTokens = estimateTokens(systemPrompt + prompt);
|
|
68
|
+
const cTokens = estimateTokens(offlineReply.text);
|
|
69
|
+
const usage = recordTokenUsage("offline-local", pTokens, cTokens);
|
|
70
|
+
recordLatency("offline-fallback", "local", latencyMs, pTokens, cTokens, true);
|
|
71
|
+
return { ...offlineReply, provider: "offline-fallback", node: 0, usage };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// ── Try each provider in order ──────────────────────────
|
|
75
|
+
const errors = [];
|
|
76
|
+
let nodeIndex = 1;
|
|
77
|
+
|
|
78
|
+
for (const { id, provider, apiKey } of active) {
|
|
79
|
+
const startTime = performance.now();
|
|
80
|
+
try {
|
|
81
|
+
const model = config[`${id.toUpperCase()}_MODEL`] || provider.defaultModel;
|
|
82
|
+
let result;
|
|
83
|
+
|
|
84
|
+
switch (provider.format) {
|
|
85
|
+
case "openai":
|
|
86
|
+
result = await callOpenAICompatible(
|
|
87
|
+
prompt, systemPrompt, apiKey,
|
|
88
|
+
provider.baseUrl, model, provider.name,
|
|
89
|
+
onToken, history
|
|
90
|
+
);
|
|
91
|
+
break;
|
|
92
|
+
|
|
93
|
+
case "custom-google":
|
|
94
|
+
result = await callGoogleGemini(prompt, systemPrompt, apiKey, model, onToken, history);
|
|
95
|
+
break;
|
|
96
|
+
|
|
97
|
+
case "custom-anthropic":
|
|
98
|
+
result = await callAnthropic(prompt, systemPrompt, apiKey, model, onToken, history);
|
|
99
|
+
break;
|
|
100
|
+
|
|
101
|
+
case "custom-cohere":
|
|
102
|
+
result = await callCohere(prompt, systemPrompt, apiKey, model, onToken, history);
|
|
103
|
+
break;
|
|
104
|
+
|
|
105
|
+
default:
|
|
106
|
+
// Treat unknown formats as OpenAI-compatible
|
|
107
|
+
result = await callOpenAICompatible(
|
|
108
|
+
prompt, systemPrompt, apiKey,
|
|
109
|
+
provider.baseUrl, model, provider.name,
|
|
110
|
+
onToken, history
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const latencyMs = performance.now() - startTime;
|
|
115
|
+
const pTokens = estimateTokens(systemPrompt + prompt + history.map(h => h.content).join(""));
|
|
116
|
+
const cTokens = estimateTokens(result.text);
|
|
117
|
+
const usage = recordTokenUsage(result.model, pTokens, cTokens);
|
|
118
|
+
|
|
119
|
+
recordLatency(provider.name, result.model, latencyMs, pTokens, cTokens, true);
|
|
120
|
+
|
|
121
|
+
return { ...result, node: nodeIndex, usage };
|
|
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);
|
|
126
|
+
errors.push(`[Node ${nodeIndex} ${provider.name}] ${err.message}`);
|
|
127
|
+
nodeIndex++;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// ── Final Fallback: Offline Fallback ────────────────────
|
|
132
|
+
const startTimeOffline = performance.now();
|
|
133
|
+
const offlineReply = generateOfflineReply(prompt, errors);
|
|
134
|
+
const latencyMsOffline = performance.now() - startTimeOffline;
|
|
135
|
+
const pTokens = estimateTokens(systemPrompt + prompt + history.map(h => h.content).join(""));
|
|
136
|
+
const cTokens = estimateTokens(offlineReply.text);
|
|
137
|
+
const usage = recordTokenUsage("offline-local", pTokens, cTokens);
|
|
138
|
+
recordLatency("offline-fallback", "local", latencyMsOffline, pTokens, cTokens, true);
|
|
139
|
+
return {
|
|
140
|
+
...offlineReply,
|
|
141
|
+
provider: "offline-fallback",
|
|
142
|
+
node: 0,
|
|
143
|
+
errors,
|
|
144
|
+
usage,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
@@ -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
|
+
"krims_code_agent_cli.egg-info",
|
|
11
|
+
"krims_code_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
|
+
// Krims Code 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(), ".krims-code");
|
|
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 - krims-code 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
|
+
}
|