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,101 @@
1
+ // ═══════════════════════════════════════════════════════════
2
+ // Krims Code AI CLI — Real-time Token & Telemetry Tracker
3
+ // Tracks input/output token counts and session usage metrics.
4
+ // ═══════════════════════════════════════════════════════════
5
+
6
+ // Session token accumulator
7
+ let sessionTokens = {
8
+ prompt: 0,
9
+ completion: 0,
10
+ total: 0,
11
+ exchanges: 0,
12
+ };
13
+
14
+ // Model-by-model breakdown
15
+ const modelBreakdown = {};
16
+
17
+ /**
18
+ * Heuristically estimates the token count of a string based on character length.
19
+ * Standard rule of thumb: 1 token ≈ 4 characters.
20
+ * @param {string} text
21
+ * @returns {number}
22
+ */
23
+ export function estimateTokens(text) {
24
+ if (!text) return 0;
25
+ return Math.ceil(text.length / 4);
26
+ }
27
+
28
+ /**
29
+ * Returns a copy of the current session token stats.
30
+ * @returns {{prompt: number, completion: number, total: number, exchanges: number}}
31
+ */
32
+ export function getSessionTokenStats() {
33
+ return { ...sessionTokens };
34
+ }
35
+
36
+ /**
37
+ * Resets all accumulated session token statistics and breakdowns.
38
+ */
39
+ export function resetSessionTokenStats() {
40
+ sessionTokens = {
41
+ prompt: 0,
42
+ completion: 0,
43
+ total: 0,
44
+ exchanges: 0,
45
+ };
46
+ // Clear object properties safely without losing reference
47
+ for (const key of Object.keys(modelBreakdown)) {
48
+ delete modelBreakdown[key];
49
+ }
50
+ }
51
+
52
+ /**
53
+ * Records token usage for a query.
54
+ * @param {string} model - The model name
55
+ * @param {number} promptTokens - Input prompt tokens count
56
+ * @param {number} completionTokens - Output completion tokens count
57
+ * @returns {{promptTokens: number, completionTokens: number, totalTokens: number}}
58
+ */
59
+ export function recordTokenUsage(model, promptTokens, completionTokens) {
60
+ const modelName = model || "unknown-model";
61
+ const totalTokens = promptTokens + completionTokens;
62
+
63
+ // Update session totals
64
+ sessionTokens.prompt += promptTokens;
65
+ sessionTokens.completion += completionTokens;
66
+ sessionTokens.total += totalTokens;
67
+ sessionTokens.exchanges += 1;
68
+
69
+ // Update model breakdown
70
+ if (!modelBreakdown[modelName]) {
71
+ modelBreakdown[modelName] = {
72
+ prompt: 0,
73
+ completion: 0,
74
+ total: 0,
75
+ exchanges: 0,
76
+ };
77
+ }
78
+ modelBreakdown[modelName].prompt += promptTokens;
79
+ modelBreakdown[modelName].completion += completionTokens;
80
+ modelBreakdown[modelName].total += totalTokens;
81
+ modelBreakdown[modelName].exchanges += 1;
82
+
83
+ return {
84
+ promptTokens,
85
+ completionTokens,
86
+ totalTokens,
87
+ };
88
+ }
89
+
90
+ /**
91
+ * Returns the model-by-model token usage breakdown.
92
+ * @returns {object}
93
+ */
94
+ export function getBreakdownByModel() {
95
+ // Deep clone modelBreakdown
96
+ const clone = {};
97
+ for (const [key, value] of Object.entries(modelBreakdown)) {
98
+ clone[key] = { ...value };
99
+ }
100
+ return clone;
101
+ }
@@ -0,0 +1,507 @@
1
+ // ═══════════════════════════════════════════════════════════
2
+ // Krims Code AI CLI — Universal API Caller
3
+ // Handles OpenAI-compatible, Google, Anthropic, and Cohere APIs
4
+ // ═══════════════════════════════════════════════════════════
5
+
6
+ /**
7
+ * Calls any OpenAI-compatible API (OpenAI, Groq, Together, Mistral, xAI, etc.)
8
+ * @param {string} prompt - User message
9
+ * @param {string} systemPrompt - System prompt
10
+ * @param {string} apiKey - API key
11
+ * @param {string} baseUrl - Full endpoint URL
12
+ * @param {string} model - Model identifier
13
+ * @param {string} providerName - For error messages
14
+ * @returns {Promise<{ text: string, provider: string, model: string }>}
15
+ */
16
+ export async function callOpenAICompatible(prompt, systemPrompt, apiKey, baseUrl, model, providerName, onToken, history = []) {
17
+ const isStreaming = typeof onToken === "function";
18
+ const formattedHistory = history.map(h => ({
19
+ role: h.role === "assistant" ? "assistant" : "user",
20
+ content: h.content
21
+ }));
22
+ const body = {
23
+ model,
24
+ messages: [
25
+ { role: "system", content: systemPrompt },
26
+ ...formattedHistory,
27
+ { role: "user", content: prompt },
28
+ ],
29
+ temperature: 0.7,
30
+ max_tokens: 4096,
31
+ ...(isStreaming ? { stream: true } : {}),
32
+ };
33
+
34
+ const headers = {
35
+ "Content-Type": "application/json",
36
+ Authorization: `Bearer ${apiKey}`,
37
+ };
38
+
39
+ // OpenRouter requires extra headers
40
+ if (baseUrl.includes("openrouter.ai")) {
41
+ headers["HTTP-Referer"] = "https://github.com/Krylo-60/krims-code-cli";
42
+ headers["X-Title"] = "Krims Code AI CLI";
43
+ }
44
+
45
+ const response = await fetch(baseUrl, {
46
+ method: "POST",
47
+ headers,
48
+ body: JSON.stringify(body),
49
+ });
50
+
51
+ if (!response.ok) {
52
+ const errorBody = await response.text().catch(() => "");
53
+ throw new Error(`${providerName} API error (${response.status}): ${response.statusText}. ${errorBody}`);
54
+ }
55
+
56
+ if (isStreaming && response.body && typeof response.body.getReader === "function") {
57
+ const reader = response.body.getReader();
58
+ const decoder = new TextDecoder();
59
+ let buffer = "";
60
+ let fullText = "";
61
+
62
+ while (true) {
63
+ const { done, value } = await reader.read();
64
+ if (done) break;
65
+ buffer += decoder.decode(value, { stream: true });
66
+
67
+ const lines = buffer.split("\n");
68
+ buffer = lines.pop(); // Keep the partial line
69
+
70
+ for (const line of lines) {
71
+ const trimmed = line.trim();
72
+ if (!trimmed) continue;
73
+ if (trimmed === "data: [DONE]") continue;
74
+ if (trimmed.startsWith("data: ")) {
75
+ try {
76
+ const data = JSON.parse(trimmed.slice(6));
77
+ const content = data?.choices?.[0]?.delta?.content || "";
78
+ if (content) {
79
+ onToken(content);
80
+ fullText += content;
81
+ }
82
+ } catch (e) {
83
+ // Ignore partial line errors
84
+ }
85
+ }
86
+ }
87
+ }
88
+ // Flush remaining buffer
89
+ if (buffer && buffer.startsWith("data: ")) {
90
+ try {
91
+ const data = JSON.parse(buffer.slice(6));
92
+ const content = data?.choices?.[0]?.delta?.content || "";
93
+ if (content) {
94
+ onToken(content);
95
+ fullText += content;
96
+ }
97
+ } catch (e) {}
98
+ }
99
+
100
+ if (!fullText) {
101
+ throw new Error(`${providerName} returned empty response`);
102
+ }
103
+ return { text: fullText, provider: providerName.toLowerCase(), model };
104
+ } else {
105
+ const data = await response.json();
106
+ const text = data?.choices?.[0]?.message?.content;
107
+
108
+ if (!text) {
109
+ throw new Error(`${providerName} returned empty response`);
110
+ }
111
+
112
+ return { text, provider: providerName.toLowerCase(), model };
113
+ }
114
+ }
115
+
116
+ /**
117
+ * Calls the Google Gemini API (non-OpenAI format).
118
+ * @param {string} prompt - User message
119
+ * @param {string} systemPrompt - System prompt
120
+ * @param {string} apiKey - Google API key
121
+ * @param {string} model - Model name
122
+ * @returns {Promise<{ text: string, provider: string, model: string }>}
123
+ */
124
+ export async function callGoogleGemini(prompt, systemPrompt, apiKey, model = "gemini-2.5-flash", onToken, history = []) {
125
+ const BASE = "https://generativelanguage.googleapis.com/v1beta/models";
126
+ const isStreaming = typeof onToken === "function";
127
+ let currentHistory = [...history];
128
+
129
+ if (isStreaming) {
130
+ let fullText = "";
131
+ let currentPrompt = prompt;
132
+ let continuations = 0;
133
+ const MAX = 3;
134
+
135
+ while (continuations <= MAX) {
136
+ const url = `${BASE}/${model}:streamGenerateContent?key=${apiKey}`;
137
+ const formattedHistory = currentHistory.map(h => ({
138
+ role: h.role === "assistant" ? "model" : "user",
139
+ parts: [{ text: h.content }]
140
+ }));
141
+ const body = {
142
+ systemInstruction: { parts: [{ text: systemPrompt }] },
143
+ contents: [
144
+ ...formattedHistory,
145
+ { role: "user", parts: [{ text: currentPrompt }] }
146
+ ],
147
+ generationConfig: { temperature: 0.7, maxOutputTokens: 8192 },
148
+ };
149
+
150
+ const response = await fetch(url, {
151
+ method: "POST",
152
+ headers: { "Content-Type": "application/json" },
153
+ body: JSON.stringify(body),
154
+ });
155
+
156
+ if (!response.ok) {
157
+ const errorBody = await response.text().catch(() => "");
158
+ throw new Error(`Gemini API error (${response.status}): ${response.statusText}. ${errorBody}`);
159
+ }
160
+
161
+ let streamedTextInThisTurn = "";
162
+
163
+ if (!response.body || typeof response.body.getReader !== "function") {
164
+ // Fallback to non-streaming if response body is not streamable (e.g. in unit tests)
165
+ const data = await response.json();
166
+ let chunkText = "";
167
+ let finishReason = "";
168
+ if (Array.isArray(data)) {
169
+ for (const chunk of data) {
170
+ const partText = chunk.candidates?.[0]?.content?.parts?.map((p) => p.text).filter(Boolean).join("") || "";
171
+ chunkText += partText;
172
+ finishReason = chunk.candidates?.[0]?.finishReason || finishReason;
173
+ }
174
+ } else {
175
+ chunkText = data.candidates?.[0]?.content?.parts?.map((p) => p.text).filter(Boolean).join("") || "";
176
+ finishReason = data.candidates?.[0]?.finishReason || finishReason;
177
+ }
178
+
179
+ if (chunkText) {
180
+ onToken(chunkText);
181
+ fullText += chunkText;
182
+ streamedTextInThisTurn += chunkText;
183
+ }
184
+
185
+ if (finishReason === "MAX_TOKENS" && continuations < MAX) {
186
+ currentHistory.push({ role: "user", content: currentPrompt });
187
+ currentHistory.push({ role: "assistant", content: streamedTextInThisTurn });
188
+ continuations++;
189
+ currentPrompt = "Continue your previous response from exactly where you left off.";
190
+ } else {
191
+ break;
192
+ }
193
+ } else {
194
+ const reader = response.body.getReader();
195
+ const decoder = new TextDecoder();
196
+ let buffer = "";
197
+ let finishReason = "";
198
+
199
+ while (true) {
200
+ const { done, value } = await reader.read();
201
+ if (done) break;
202
+ buffer += decoder.decode(value, { stream: true });
203
+
204
+ let braceCount = 0;
205
+ let jsonStart = -1;
206
+ let inString = false;
207
+ let escape = false;
208
+
209
+ for (let i = 0; i < buffer.length; i++) {
210
+ const char = buffer[i];
211
+ if (inString) {
212
+ if (escape) {
213
+ escape = false;
214
+ } else if (char === "\\") {
215
+ escape = true;
216
+ } else if (char === '"') {
217
+ inString = false;
218
+ }
219
+ } else {
220
+ if (char === '"') {
221
+ inString = true;
222
+ } else if (char === "{") {
223
+ if (braceCount === 0) {
224
+ jsonStart = i;
225
+ }
226
+ braceCount++;
227
+ } else if (char === "}") {
228
+ braceCount--;
229
+ if (braceCount === 0 && jsonStart !== -1) {
230
+ const jsonStr = buffer.slice(jsonStart, i + 1);
231
+ try {
232
+ const obj = JSON.parse(jsonStr);
233
+ const text = obj.candidates?.[0]?.content?.parts?.map((p) => p.text).filter(Boolean).join("") || "";
234
+ finishReason = obj.candidates?.[0]?.finishReason || finishReason;
235
+ if (text) {
236
+ onToken(text);
237
+ fullText += text;
238
+ streamedTextInThisTurn += text;
239
+ }
240
+ } catch (e) {
241
+ // Ignore parse errors
242
+ }
243
+ buffer = buffer.slice(i + 1);
244
+ i = -1;
245
+ jsonStart = -1;
246
+ }
247
+ }
248
+ }
249
+ }
250
+ }
251
+
252
+ if (finishReason === "MAX_TOKENS" && continuations < MAX) {
253
+ currentHistory.push({ role: "user", content: currentPrompt });
254
+ currentHistory.push({ role: "assistant", content: streamedTextInThisTurn });
255
+ continuations++;
256
+ currentPrompt = "Continue your previous response from exactly where you left off.";
257
+ } else {
258
+ break;
259
+ }
260
+ }
261
+ }
262
+
263
+ if (!fullText.trim()) throw new Error("Gemini returned empty response");
264
+ return { text: fullText, provider: "google", model };
265
+ } else {
266
+ let fullText = "";
267
+ let currentPrompt = prompt;
268
+ let continuations = 0;
269
+ const MAX = 3;
270
+
271
+ while (continuations <= MAX) {
272
+ const url = `${BASE}/${model}:generateContent?key=${apiKey}`;
273
+ const formattedHistory = currentHistory.map(h => ({
274
+ role: h.role === "assistant" ? "model" : "user",
275
+ parts: [{ text: h.content }]
276
+ }));
277
+ const body = {
278
+ systemInstruction: { parts: [{ text: systemPrompt }] },
279
+ contents: [
280
+ ...formattedHistory,
281
+ { role: "user", parts: [{ text: currentPrompt }] }
282
+ ],
283
+ generationConfig: { temperature: 0.7, maxOutputTokens: 8192 },
284
+ };
285
+
286
+ const response = await fetch(url, {
287
+ method: "POST",
288
+ headers: { "Content-Type": "application/json" },
289
+ body: JSON.stringify(body),
290
+ });
291
+
292
+ if (!response.ok) {
293
+ const errorBody = await response.text().catch(() => "");
294
+ throw new Error(`Gemini API error (${response.status}): ${response.statusText}. ${errorBody}`);
295
+ }
296
+
297
+ const data = await response.json();
298
+ if (data.promptFeedback?.blockReason) {
299
+ throw new Error(`Content blocked: ${data.promptFeedback.blockReason}`);
300
+ }
301
+
302
+ const candidate = data.candidates?.[0];
303
+ if (!candidate) throw new Error("Gemini returned no candidates");
304
+
305
+ const chunkText = candidate.content?.parts?.map((p) => p.text).filter(Boolean).join("") || "";
306
+ fullText += chunkText;
307
+
308
+ if (candidate.finishReason === "MAX_TOKENS" && continuations < MAX) {
309
+ currentHistory.push({ role: "user", content: currentPrompt });
310
+ currentHistory.push({ role: "assistant", content: chunkText });
311
+ continuations++;
312
+ currentPrompt = "Continue your previous response from exactly where you left off.";
313
+ } else {
314
+ break;
315
+ }
316
+ }
317
+
318
+ if (!fullText.trim()) throw new Error("Gemini returned empty response");
319
+ return { text: fullText, provider: "google", model };
320
+ }
321
+ }
322
+
323
+ /**
324
+ * Calls the Anthropic Claude API.
325
+ * @param {string} prompt - User message
326
+ * @param {string} systemPrompt - System prompt
327
+ * @param {string} apiKey - Anthropic API key
328
+ * @param {string} model - Model name
329
+ * @returns {Promise<{ text: string, provider: string, model: string }>}
330
+ */
331
+ export async function callAnthropic(prompt, systemPrompt, apiKey, model = "claude-sonnet-4-20250514", onToken, history = []) {
332
+ const url = "https://api.anthropic.com/v1/messages";
333
+ const isStreaming = typeof onToken === "function";
334
+ const formattedHistory = history.map(h => ({
335
+ role: h.role === "assistant" ? "assistant" : "user",
336
+ content: h.content
337
+ }));
338
+ const body = {
339
+ model,
340
+ max_tokens: 4096,
341
+ system: systemPrompt,
342
+ messages: [
343
+ ...formattedHistory,
344
+ { role: "user", content: prompt }
345
+ ],
346
+ ...(isStreaming ? { stream: true } : {}),
347
+ };
348
+
349
+ const response = await fetch(url, {
350
+ method: "POST",
351
+ headers: {
352
+ "Content-Type": "application/json",
353
+ "x-api-key": apiKey,
354
+ "anthropic-version": "2023-06-01",
355
+ },
356
+ body: JSON.stringify(body),
357
+ });
358
+
359
+ if (!response.ok) {
360
+ const errorBody = await response.text().catch(() => "");
361
+ throw new Error(`Anthropic API error (${response.status}): ${response.statusText}. ${errorBody}`);
362
+ }
363
+
364
+ if (isStreaming && response.body && typeof response.body.getReader === "function") {
365
+ const reader = response.body.getReader();
366
+ const decoder = new TextDecoder();
367
+ let buffer = "";
368
+ let fullText = "";
369
+
370
+ while (true) {
371
+ const { done, value } = await reader.read();
372
+ if (done) break;
373
+ buffer += decoder.decode(value, { stream: true });
374
+
375
+ const lines = buffer.split("\n");
376
+ buffer = lines.pop();
377
+
378
+ for (const line of lines) {
379
+ const trimmed = line.trim();
380
+ if (!trimmed) continue;
381
+ if (trimmed.startsWith("data: ")) {
382
+ try {
383
+ const data = JSON.parse(trimmed.slice(6));
384
+ if (data.type === "content_block_delta" && data.delta?.text) {
385
+ onToken(data.delta.text);
386
+ fullText += data.delta.text;
387
+ }
388
+ } catch (e) {
389
+ // Ignore partial JSON
390
+ }
391
+ }
392
+ }
393
+ }
394
+ // Flush remaining
395
+ if (buffer && buffer.startsWith("data: ")) {
396
+ try {
397
+ const data = JSON.parse(buffer.slice(6));
398
+ if (data.type === "content_block_delta" && data.delta?.text) {
399
+ onToken(data.delta.text);
400
+ fullText += data.delta.text;
401
+ }
402
+ } catch (e) {}
403
+ }
404
+
405
+ if (!fullText) throw new Error("Anthropic returned empty response");
406
+ return { text: fullText, provider: "anthropic", model };
407
+ } else {
408
+ const data = await response.json();
409
+ const text = data?.content?.[0]?.text;
410
+
411
+ if (!text) throw new Error("Anthropic returned empty response");
412
+ return { text, provider: "anthropic", model };
413
+ }
414
+ }
415
+
416
+ /**
417
+ * Calls the Cohere API (v2 chat format).
418
+ * @param {string} prompt - User message
419
+ * @param {string} systemPrompt - System prompt
420
+ * @param {string} apiKey - Cohere API key
421
+ * @param {string} model - Model name
422
+ * @returns {Promise<{ text: string, provider: string, model: string }>}
423
+ */
424
+ export async function callCohere(prompt, systemPrompt, apiKey, model = "command-r-plus", onToken, history = []) {
425
+ const url = "https://api.cohere.com/v2/chat";
426
+ const isStreaming = typeof onToken === "function";
427
+ const formattedHistory = history.map(h => ({
428
+ role: h.role === "assistant" ? "assistant" : "user",
429
+ content: h.content
430
+ }));
431
+ const body = {
432
+ model,
433
+ messages: [
434
+ { role: "system", content: systemPrompt },
435
+ ...formattedHistory,
436
+ { role: "user", content: prompt },
437
+ ],
438
+ ...(isStreaming ? { stream: true } : {}),
439
+ };
440
+
441
+ const response = await fetch(url, {
442
+ method: "POST",
443
+ headers: {
444
+ "Content-Type": "application/json",
445
+ Authorization: `Bearer ${apiKey}`,
446
+ },
447
+ body: JSON.stringify(body),
448
+ });
449
+
450
+ if (!response.ok) {
451
+ const errorBody = await response.text().catch(() => "");
452
+ throw new Error(`Cohere API error (${response.status}): ${response.statusText}. ${errorBody}`);
453
+ }
454
+
455
+ if (isStreaming && response.body && typeof response.body.getReader === "function") {
456
+ const reader = response.body.getReader();
457
+ const decoder = new TextDecoder();
458
+ let buffer = "";
459
+ let fullText = "";
460
+
461
+ while (true) {
462
+ const { done, value } = await reader.read();
463
+ if (done) break;
464
+ buffer += decoder.decode(value, { stream: true });
465
+
466
+ const lines = buffer.split("\n");
467
+ buffer = lines.pop();
468
+
469
+ for (const line of lines) {
470
+ const trimmed = line.trim();
471
+ if (!trimmed) continue;
472
+ if (trimmed.startsWith("data: ")) {
473
+ try {
474
+ const data = JSON.parse(trimmed.slice(6));
475
+ const content = data?.delta?.message?.content?.text || "";
476
+ if (content) {
477
+ onToken(content);
478
+ fullText += content;
479
+ }
480
+ } catch (e) {
481
+ // Ignore partial JSON
482
+ }
483
+ }
484
+ }
485
+ }
486
+ // Flush remaining
487
+ if (buffer && buffer.startsWith("data: ")) {
488
+ try {
489
+ const data = JSON.parse(buffer.slice(6));
490
+ const content = data?.delta?.message?.content?.text || "";
491
+ if (content) {
492
+ onToken(content);
493
+ fullText += content;
494
+ }
495
+ } catch (e) {}
496
+ }
497
+
498
+ if (!fullText) throw new Error("Cohere returned empty response");
499
+ return { text: fullText, provider: "cohere", model };
500
+ } else {
501
+ const data = await response.json();
502
+ const text = data?.message?.content?.[0]?.text;
503
+
504
+ if (!text) throw new Error("Cohere returned empty response");
505
+ return { text, provider: "cohere", model };
506
+ }
507
+ }
@@ -0,0 +1,50 @@
1
+ // ═══════════════════════════════════════════════════════════
2
+ // Krims Code AI CLI — xAI/Grok API Provider
3
+ // ═══════════════════════════════════════════════════════════
4
+
5
+ /**
6
+ * Sends a prompt to the xAI (Grok) API.
7
+ * @param {string} prompt - The user message
8
+ * @param {string} systemPrompt - System prompt for the mode
9
+ * @param {string} apiKey - xAI API key
10
+ * @param {string} [model='grok-2'] - Model name
11
+ * @returns {Promise<{ text: string, provider: string, model: string }>}
12
+ */
13
+ export async function callXai(prompt, systemPrompt, apiKey, model = "grok-2") {
14
+ const url = "https://api.x.ai/v1/chat/completions";
15
+
16
+ const body = {
17
+ model,
18
+ messages: [
19
+ { role: "system", content: systemPrompt },
20
+ { role: "user", content: prompt },
21
+ ],
22
+ temperature: 0.7,
23
+ max_tokens: 4096,
24
+ };
25
+
26
+ const response = await fetch(url, {
27
+ method: "POST",
28
+ headers: {
29
+ "Content-Type": "application/json",
30
+ Authorization: `Bearer ${apiKey}`,
31
+ },
32
+ body: JSON.stringify(body),
33
+ });
34
+
35
+ if (!response.ok) {
36
+ const errorBody = await response.text().catch(() => "");
37
+ throw new Error(
38
+ `xAI API error (${response.status}): ${response.statusText}. ${errorBody}`
39
+ );
40
+ }
41
+
42
+ const data = await response.json();
43
+ const text = data?.choices?.[0]?.message?.content;
44
+
45
+ if (!text) {
46
+ throw new Error("xAI API returned empty response");
47
+ }
48
+
49
+ return { text, provider: "xai", model };
50
+ }