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.
Files changed (43) hide show
  1. krims_code_cli-1.5.2.dist-info/METADATA +336 -0
  2. krims_code_cli-1.5.2.dist-info/RECORD +43 -0
  3. krims_code_cli-1.5.2.dist-info/WHEEL +5 -0
  4. krims_code_cli-1.5.2.dist-info/entry_points.txt +2 -0
  5. krims_code_cli-1.5.2.dist-info/licenses/LICENSE +21 -0
  6. krims_code_cli-1.5.2.dist-info/top_level.txt +1 -0
  7. krims_pip/__init__.py +1 -0
  8. krims_pip/cli.py +50 -0
  9. krims_pip/node_project/bin/krims-code.js +10 -0
  10. krims_pip/node_project/package-lock.json +793 -0
  11. krims_pip/node_project/package.json +46 -0
  12. krims_pip/node_project/src/agent.js +415 -0
  13. krims_pip/node_project/src/ai/fallback.js +239 -0
  14. krims_pip/node_project/src/ai/google.js +87 -0
  15. krims_pip/node_project/src/ai/providers.js +203 -0
  16. krims_pip/node_project/src/ai/router.js +146 -0
  17. krims_pip/node_project/src/ai/search.js +94 -0
  18. krims_pip/node_project/src/ai/telemetry.js +125 -0
  19. krims_pip/node_project/src/ai/tokens.js +101 -0
  20. krims_pip/node_project/src/ai/universal.js +507 -0
  21. krims_pip/node_project/src/ai/xai.js +50 -0
  22. krims_pip/node_project/src/chat.js +2543 -0
  23. krims_pip/node_project/src/cli.js +632 -0
  24. krims_pip/node_project/src/commands/commit.js +157 -0
  25. krims_pip/node_project/src/commands/dashboard.js +59 -0
  26. krims_pip/node_project/src/commands/github.js +349 -0
  27. krims_pip/node_project/src/commands/index.js +67 -0
  28. krims_pip/node_project/src/commands/status.js +66 -0
  29. krims_pip/node_project/src/commands/teamwork-preview.js +326 -0
  30. krims_pip/node_project/src/commands/theme.js +42 -0
  31. krims_pip/node_project/src/commands/themes.js +30 -0
  32. krims_pip/node_project/src/config.js +400 -0
  33. krims_pip/node_project/src/dashboard.js +112 -0
  34. krims_pip/node_project/src/file-parser.js +164 -0
  35. krims_pip/node_project/src/git.js +41 -0
  36. krims_pip/node_project/src/mic.js +220 -0
  37. krims_pip/node_project/src/modes.js +92 -0
  38. krims_pip/node_project/src/telemetry-server.js +855 -0
  39. krims_pip/node_project/src/ui/banner.js +204 -0
  40. krims_pip/node_project/src/ui/dashboard.html +834 -0
  41. krims_pip/node_project/src/ui/spinner.js +43 -0
  42. krims_pip/node_project/src/ui/theme.js +624 -0
  43. 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
+ }