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,400 @@
|
|
|
1
|
+
// ═══════════════════════════════════════════════════════════
|
|
2
|
+
// Krims Code AI CLI — Secure Configuration Management
|
|
3
|
+
// Stores user API keys locally at ~/.krims-code/config.json
|
|
4
|
+
// Supports ALL AI providers (13+ and growing)
|
|
5
|
+
// ═══════════════════════════════════════════════════════════
|
|
6
|
+
|
|
7
|
+
import { readFile, writeFile, mkdir, unlink, access } from "node:fs/promises";
|
|
8
|
+
import {
|
|
9
|
+
existsSync,
|
|
10
|
+
readdirSync,
|
|
11
|
+
statSync,
|
|
12
|
+
mkdirSync,
|
|
13
|
+
readFileSync,
|
|
14
|
+
writeFileSync,
|
|
15
|
+
unlinkSync
|
|
16
|
+
} from "node:fs";
|
|
17
|
+
import { join } from "node:path";
|
|
18
|
+
import { homedir } from "node:os";
|
|
19
|
+
import { getAllConfigKeys } from "./ai/providers.js";
|
|
20
|
+
|
|
21
|
+
const CONFIG_DIR = existsSync(join(homedir(), ".krims"))
|
|
22
|
+
? join(homedir(), ".krims")
|
|
23
|
+
: (existsSync(join(homedir(), ".krims-code"))
|
|
24
|
+
? join(homedir(), ".krims-code")
|
|
25
|
+
: join(homedir(), ".krims"));
|
|
26
|
+
const CONFIG_FILE = join(CONFIG_DIR, "config.json");
|
|
27
|
+
|
|
28
|
+
const SENSITIVE_PATTERNS = ["KEY", "TOKEN", "SECRET"];
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Returns the full path to the config file.
|
|
32
|
+
* @returns {string}
|
|
33
|
+
*/
|
|
34
|
+
export function getConfigPath() {
|
|
35
|
+
return CONFIG_FILE;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Loads the config from disk.
|
|
40
|
+
* @returns {Promise<object>} Parsed config or empty object
|
|
41
|
+
*/
|
|
42
|
+
export async function loadConfig() {
|
|
43
|
+
try {
|
|
44
|
+
const raw = await readFile(CONFIG_FILE, "utf-8");
|
|
45
|
+
return JSON.parse(raw);
|
|
46
|
+
} catch {
|
|
47
|
+
return {};
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Saves config to disk, creating the directory if needed.
|
|
53
|
+
* @param {object} config - The config object to write
|
|
54
|
+
*/
|
|
55
|
+
export async function saveConfig(config) {
|
|
56
|
+
try {
|
|
57
|
+
await mkdir(CONFIG_DIR, { recursive: true });
|
|
58
|
+
await writeFile(CONFIG_FILE, JSON.stringify(config, null, 2), "utf-8");
|
|
59
|
+
} catch (err) {
|
|
60
|
+
throw new Error(`Failed to save config: ${err.message}`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Gets a single config value.
|
|
66
|
+
* @param {string} key
|
|
67
|
+
* @returns {Promise<string|undefined>}
|
|
68
|
+
*/
|
|
69
|
+
export async function getConfigValue(key) {
|
|
70
|
+
const config = await loadConfig();
|
|
71
|
+
return config[key];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Sets a single config value.
|
|
76
|
+
* @param {string} key
|
|
77
|
+
* @param {string} value
|
|
78
|
+
*/
|
|
79
|
+
export async function setConfigValue(key, value) {
|
|
80
|
+
const config = await loadConfig();
|
|
81
|
+
config[key] = value;
|
|
82
|
+
await saveConfig(config);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Deletes a single config key.
|
|
87
|
+
* @param {string} key
|
|
88
|
+
*/
|
|
89
|
+
export async function deleteConfigValue(key) {
|
|
90
|
+
const config = await loadConfig();
|
|
91
|
+
delete config[key];
|
|
92
|
+
await saveConfig(config);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Deletes the entire config file.
|
|
97
|
+
*/
|
|
98
|
+
export async function resetConfig() {
|
|
99
|
+
try {
|
|
100
|
+
await unlink(CONFIG_FILE);
|
|
101
|
+
} catch {
|
|
102
|
+
// File may not exist
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Lists all config keys with sensitive values masked.
|
|
108
|
+
* @returns {Promise<object>} Config with sensitive values masked
|
|
109
|
+
*/
|
|
110
|
+
export async function listConfig() {
|
|
111
|
+
const config = await loadConfig();
|
|
112
|
+
const masked = {};
|
|
113
|
+
|
|
114
|
+
for (const [key, value] of Object.entries(config)) {
|
|
115
|
+
const isSensitive = SENSITIVE_PATTERNS.some((p) => key.toUpperCase().includes(p));
|
|
116
|
+
if (isSensitive && typeof value === "string" && value.length > 8) {
|
|
117
|
+
masked[key] = value.slice(0, 6) + "•••" + value.slice(-3);
|
|
118
|
+
} else {
|
|
119
|
+
masked[key] = value;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return masked;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Returns the full flat config object for the AI router.
|
|
128
|
+
* Merges the config file with environment variables (config file takes precedence).
|
|
129
|
+
* @returns {Promise<object>}
|
|
130
|
+
*/
|
|
131
|
+
export async function getAIConfig() {
|
|
132
|
+
const config = await loadConfig();
|
|
133
|
+
const allKeys = getAllConfigKeys();
|
|
134
|
+
|
|
135
|
+
// Merge: config file values override env vars
|
|
136
|
+
const merged = {};
|
|
137
|
+
for (const key of allKeys) {
|
|
138
|
+
merged[key] = config[key] || process.env[key] || "";
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Also pass through any custom model overrides and extra keys
|
|
142
|
+
for (const [key, value] of Object.entries(config)) {
|
|
143
|
+
if (!merged[key]) {
|
|
144
|
+
merged[key] = value;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Check env vars for anything the config didn't have
|
|
149
|
+
for (const [key, value] of Object.entries(process.env)) {
|
|
150
|
+
if (key.endsWith("_API_KEY") && !merged[key]) {
|
|
151
|
+
merged[key] = value;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return merged;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Checks if the config file exists.
|
|
160
|
+
* @returns {Promise<boolean>}
|
|
161
|
+
*/
|
|
162
|
+
export async function configExists() {
|
|
163
|
+
try {
|
|
164
|
+
await access(CONFIG_FILE);
|
|
165
|
+
return true;
|
|
166
|
+
} catch {
|
|
167
|
+
return false;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Checks if the given key is a valid/recognized config key.
|
|
173
|
+
* Accepts any *_API_KEY, *_MODEL, and known keys.
|
|
174
|
+
* @param {string} key
|
|
175
|
+
* @returns {boolean}
|
|
176
|
+
*/
|
|
177
|
+
export function isValidConfigKey(key) {
|
|
178
|
+
const upper = key.toUpperCase();
|
|
179
|
+
// Accept any API key or model override
|
|
180
|
+
const allowedSpecialKeys = [
|
|
181
|
+
"THEME", "CUSTOM_COMMANDS", "AUTOPILOT",
|
|
182
|
+
"AUTO_UPDATE", "SHOW_HIGHLIGHTS", "LAST_UPDATE_CHECK", "LAST_NOTIFIED_VERSION",
|
|
183
|
+
"SHOW_TOKENS", "DIAGNOSE_CMD", "NERD_FONTS"
|
|
184
|
+
];
|
|
185
|
+
if (upper.endsWith("_API_KEY") || upper.endsWith("_API_KEYS") || upper.endsWith("_MODEL") || allowedSpecialKeys.includes(upper)) {
|
|
186
|
+
return true;
|
|
187
|
+
}
|
|
188
|
+
// Accept known config keys
|
|
189
|
+
const knownKeys = getAllConfigKeys();
|
|
190
|
+
return knownKeys.includes(upper);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const HISTORY_DIR = join(CONFIG_DIR, "history");
|
|
194
|
+
const LEGACY_HISTORY_FILE = join(CONFIG_DIR, "history.json");
|
|
195
|
+
|
|
196
|
+
let currentSessionFile = null;
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Gets the current active session file path, initializing it if necessary.
|
|
200
|
+
* @returns {string}
|
|
201
|
+
*/
|
|
202
|
+
export function getSessionFile() {
|
|
203
|
+
if (currentSessionFile) {
|
|
204
|
+
return currentSessionFile;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
try {
|
|
208
|
+
if (!existsSync(HISTORY_DIR)) {
|
|
209
|
+
mkdirSync(HISTORY_DIR, { recursive: true });
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const files = readdirSync(HISTORY_DIR).filter(
|
|
213
|
+
(f) => f.startsWith("session_") && f.endsWith(".json")
|
|
214
|
+
);
|
|
215
|
+
|
|
216
|
+
if (files.length > 0) {
|
|
217
|
+
// Sort files descending by modification time
|
|
218
|
+
files.sort((a, b) => {
|
|
219
|
+
return statSync(join(HISTORY_DIR, b)).mtimeMs - statSync(join(HISTORY_DIR, a)).mtimeMs;
|
|
220
|
+
});
|
|
221
|
+
currentSessionFile = join(HISTORY_DIR, files[0]);
|
|
222
|
+
} else {
|
|
223
|
+
// If legacy history file exists, migrate it
|
|
224
|
+
if (existsSync(LEGACY_HISTORY_FILE)) {
|
|
225
|
+
const timestamp = statSync(LEGACY_HISTORY_FILE).mtimeMs || Date.now();
|
|
226
|
+
currentSessionFile = join(HISTORY_DIR, `session_${timestamp}.json`);
|
|
227
|
+
try {
|
|
228
|
+
const raw = readFileSync(LEGACY_HISTORY_FILE, "utf-8");
|
|
229
|
+
const legacyData = JSON.parse(raw);
|
|
230
|
+
const sessionData = {
|
|
231
|
+
mode: "titan",
|
|
232
|
+
timestamp,
|
|
233
|
+
messages: Array.isArray(legacyData) ? legacyData : (legacyData.messages || []),
|
|
234
|
+
};
|
|
235
|
+
writeFileSync(currentSessionFile, JSON.stringify(sessionData, null, 2), "utf-8");
|
|
236
|
+
try {
|
|
237
|
+
unlinkSync(LEGACY_HISTORY_FILE);
|
|
238
|
+
} catch {
|
|
239
|
+
// ignore unlink error
|
|
240
|
+
}
|
|
241
|
+
} catch {
|
|
242
|
+
// ignore parsing error, start new
|
|
243
|
+
startNewSession();
|
|
244
|
+
}
|
|
245
|
+
} else {
|
|
246
|
+
startNewSession();
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
} catch {
|
|
250
|
+
startNewSession();
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
return currentSessionFile;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Starts a new chat session.
|
|
258
|
+
* @returns {string} Path to the new session file
|
|
259
|
+
*/
|
|
260
|
+
export function startNewSession() {
|
|
261
|
+
const timestamp = Date.now();
|
|
262
|
+
currentSessionFile = join(HISTORY_DIR, `session_${timestamp}.json`);
|
|
263
|
+
try {
|
|
264
|
+
if (!existsSync(HISTORY_DIR)) {
|
|
265
|
+
mkdirSync(HISTORY_DIR, { recursive: true });
|
|
266
|
+
}
|
|
267
|
+
const sessionData = {
|
|
268
|
+
mode: "titan",
|
|
269
|
+
timestamp,
|
|
270
|
+
messages: [],
|
|
271
|
+
};
|
|
272
|
+
writeFileSync(currentSessionFile, JSON.stringify(sessionData, null, 2), "utf-8");
|
|
273
|
+
} catch {
|
|
274
|
+
// Fail silently
|
|
275
|
+
}
|
|
276
|
+
return currentSessionFile;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Lists all session logs.
|
|
281
|
+
* @returns {Array} List of sessions with metadata
|
|
282
|
+
*/
|
|
283
|
+
export function listSessions() {
|
|
284
|
+
try {
|
|
285
|
+
if (!existsSync(HISTORY_DIR)) {
|
|
286
|
+
mkdirSync(HISTORY_DIR, { recursive: true });
|
|
287
|
+
}
|
|
288
|
+
const files = readdirSync(HISTORY_DIR).filter(
|
|
289
|
+
(f) => f.startsWith("session_") && f.endsWith(".json")
|
|
290
|
+
);
|
|
291
|
+
const sessions = [];
|
|
292
|
+
|
|
293
|
+
for (const file of files) {
|
|
294
|
+
const fullPath = join(HISTORY_DIR, file);
|
|
295
|
+
try {
|
|
296
|
+
const raw = readFileSync(fullPath, "utf-8");
|
|
297
|
+
const data = JSON.parse(raw);
|
|
298
|
+
const messages = Array.isArray(data) ? data : (data.messages || []);
|
|
299
|
+
const mode = Array.isArray(data) ? "titan" : (data.mode || "titan");
|
|
300
|
+
const timestamp = Array.isArray(data)
|
|
301
|
+
? (statSync(fullPath).mtimeMs || Date.now())
|
|
302
|
+
: (data.timestamp || statSync(fullPath).mtimeMs || Date.now());
|
|
303
|
+
|
|
304
|
+
sessions.push({
|
|
305
|
+
file: fullPath,
|
|
306
|
+
filename: file,
|
|
307
|
+
timestamp,
|
|
308
|
+
mode,
|
|
309
|
+
messages,
|
|
310
|
+
});
|
|
311
|
+
} catch {
|
|
312
|
+
// ignore corrupt files
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
sessions.sort((a, b) => b.timestamp - a.timestamp);
|
|
317
|
+
return sessions;
|
|
318
|
+
} catch {
|
|
319
|
+
return [];
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Switches the active session file.
|
|
325
|
+
* @param {string} sessionFile
|
|
326
|
+
*/
|
|
327
|
+
export function switchSession(sessionFile) {
|
|
328
|
+
currentSessionFile = sessionFile;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Loads chat history from disk.
|
|
333
|
+
* @returns {Promise<Array>} List of chat exchanges
|
|
334
|
+
*/
|
|
335
|
+
export async function loadHistory() {
|
|
336
|
+
try {
|
|
337
|
+
const file = getSessionFile();
|
|
338
|
+
const raw = readFileSync(file, "utf-8");
|
|
339
|
+
const data = JSON.parse(raw);
|
|
340
|
+
return Array.isArray(data) ? data : (data.messages || []);
|
|
341
|
+
} catch {
|
|
342
|
+
return [];
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Saves chat history to disk.
|
|
348
|
+
* @param {Array} history - List of chat exchanges to save
|
|
349
|
+
* @param {string} [mode] - Current mode name
|
|
350
|
+
*/
|
|
351
|
+
export async function saveHistory(history, mode) {
|
|
352
|
+
try {
|
|
353
|
+
const file = getSessionFile();
|
|
354
|
+
let timestamp = Date.now();
|
|
355
|
+
const match = file.match(/session_(\d+)\.json$/);
|
|
356
|
+
if (match) {
|
|
357
|
+
timestamp = parseInt(match[1], 10);
|
|
358
|
+
}
|
|
359
|
+
const trimmed = history.slice(-50);
|
|
360
|
+
|
|
361
|
+
let finalMode = mode;
|
|
362
|
+
if (!finalMode) {
|
|
363
|
+
try {
|
|
364
|
+
const raw = readFileSync(file, "utf-8");
|
|
365
|
+
const data = JSON.parse(raw);
|
|
366
|
+
if (!Array.isArray(data)) {
|
|
367
|
+
finalMode = data.mode;
|
|
368
|
+
}
|
|
369
|
+
} catch {
|
|
370
|
+
// file might not exist yet
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
if (!finalMode) {
|
|
374
|
+
finalMode = "titan";
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
const sessionData = {
|
|
378
|
+
mode: finalMode,
|
|
379
|
+
timestamp,
|
|
380
|
+
messages: trimmed,
|
|
381
|
+
};
|
|
382
|
+
|
|
383
|
+
writeFileSync(file, JSON.stringify(sessionData, null, 2), "utf-8");
|
|
384
|
+
} catch {
|
|
385
|
+
// Fail silently to not block chat
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* Deletes the current chat history file.
|
|
391
|
+
*/
|
|
392
|
+
export async function clearHistory() {
|
|
393
|
+
try {
|
|
394
|
+
const file = getSessionFile();
|
|
395
|
+
unlinkSync(file);
|
|
396
|
+
currentSessionFile = null;
|
|
397
|
+
} catch {
|
|
398
|
+
// File may not exist
|
|
399
|
+
}
|
|
400
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
// ═══════════════════════════════════════════════════════════
|
|
2
|
+
// Krims Code AI CLI — Cyberpunk Web Telemetry Dashboard Server
|
|
3
|
+
// Lightweight server using node:http to host dashboard diagnostics
|
|
4
|
+
// ═══════════════════════════════════════════════════════════
|
|
5
|
+
|
|
6
|
+
import http from "node:http";
|
|
7
|
+
import fs from "node:fs";
|
|
8
|
+
import path from "node:path";
|
|
9
|
+
import { fileURLToPath } from "node:url";
|
|
10
|
+
import { getTelemetryData } from "./ai/telemetry.js";
|
|
11
|
+
import { getAIConfig } from "./config.js";
|
|
12
|
+
|
|
13
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
14
|
+
const __dirname = path.dirname(__filename);
|
|
15
|
+
|
|
16
|
+
let server = null;
|
|
17
|
+
let currentPort = 4567;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Starts the telemetry HTTP server.
|
|
21
|
+
* If the preferred port is occupied, automatically increments and tries the next port.
|
|
22
|
+
* @param {number} preferredPort
|
|
23
|
+
* @returns {Promise<{ port: number, server: http.Server }>}
|
|
24
|
+
*/
|
|
25
|
+
export async function startDashboardServer(preferredPort = 4567) {
|
|
26
|
+
return new Promise((resolve, reject) => {
|
|
27
|
+
let port = preferredPort;
|
|
28
|
+
|
|
29
|
+
function attemptListen() {
|
|
30
|
+
server = http.createServer(async (req, res) => {
|
|
31
|
+
const url = new URL(req.url, `http://${req.headers.host}`);
|
|
32
|
+
|
|
33
|
+
// CORS headers
|
|
34
|
+
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
35
|
+
res.setHeader("Access-Control-Allow-Methods", "GET, OPTIONS");
|
|
36
|
+
res.setHeader("Access-Control-Allow-Headers", "Content-Type");
|
|
37
|
+
|
|
38
|
+
if (req.method === "OPTIONS") {
|
|
39
|
+
res.writeHead(200);
|
|
40
|
+
res.end();
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Serve HTML dashboard
|
|
45
|
+
if (url.pathname === "/" || url.pathname === "/index.html") {
|
|
46
|
+
try {
|
|
47
|
+
const htmlPath = path.join(__dirname, "ui", "dashboard.html");
|
|
48
|
+
const html = fs.readFileSync(htmlPath, "utf8");
|
|
49
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
50
|
+
res.end(html);
|
|
51
|
+
} catch (err) {
|
|
52
|
+
res.writeHead(500, { "Content-Type": "text/plain" });
|
|
53
|
+
res.end("Error loading telemetry dashboard UI: " + err.message);
|
|
54
|
+
}
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// API Endpoint: /api/telemetry
|
|
59
|
+
if (url.pathname === "/api/telemetry") {
|
|
60
|
+
try {
|
|
61
|
+
const config = await getAIConfig();
|
|
62
|
+
const data = getTelemetryData(config);
|
|
63
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
64
|
+
res.end(JSON.stringify(data));
|
|
65
|
+
} catch (err) {
|
|
66
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
67
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
68
|
+
}
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Default 404
|
|
73
|
+
res.writeHead(404, { "Content-Type": "text/plain" });
|
|
74
|
+
res.end("Not Found");
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
server.on("error", (err) => {
|
|
78
|
+
if (err.code === "EADDRINUSE") {
|
|
79
|
+
port++;
|
|
80
|
+
attemptListen();
|
|
81
|
+
} else {
|
|
82
|
+
reject(err);
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
server.listen(port, () => {
|
|
87
|
+
currentPort = port;
|
|
88
|
+
resolve({ port, server });
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
attemptListen();
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Stops the dashboard server if active.
|
|
98
|
+
*/
|
|
99
|
+
export function stopDashboardServer() {
|
|
100
|
+
if (server) {
|
|
101
|
+
server.close();
|
|
102
|
+
server = null;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Gets the active port.
|
|
108
|
+
* @returns {number}
|
|
109
|
+
*/
|
|
110
|
+
export function getDashboardPort() {
|
|
111
|
+
return currentPort;
|
|
112
|
+
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
// ═══════════════════════════════════════════════════════════
|
|
2
|
+
// Krims Code AI CLI — File Parser & Context Injector
|
|
3
|
+
// ═══════════════════════════════════════════════════════════
|
|
4
|
+
|
|
5
|
+
import { readFile, stat } from "node:fs/promises";
|
|
6
|
+
import { readdirSync, statSync } from "node:fs";
|
|
7
|
+
import { resolve, extname, basename, join, relative } from "node:path";
|
|
8
|
+
|
|
9
|
+
const MAX_CONTENT_LENGTH = 30000;
|
|
10
|
+
|
|
11
|
+
const SUPPORTED_EXTENSIONS = new Set([
|
|
12
|
+
".txt", ".md", ".json", ".csv", ".js", ".jsx", ".ts", ".tsx",
|
|
13
|
+
".html", ".css", ".py", ".log", ".yaml", ".yml", ".xml",
|
|
14
|
+
".toml", ".env", ".sh", ".bat", ".ps1", ".sql", ".rs",
|
|
15
|
+
".go", ".java", ".c", ".cpp", ".h", ".rb", ".php",
|
|
16
|
+
".swift", ".kt", ".dart", ".vue", ".svelte",
|
|
17
|
+
]);
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Reads and parses a file for context injection.
|
|
21
|
+
* @param {string} filePath - Path to the file (absolute or relative)
|
|
22
|
+
* @returns {Promise<{ name: string, content: string, size: number, extension: string }>}
|
|
23
|
+
*/
|
|
24
|
+
export async function parseFile(filePath) {
|
|
25
|
+
let lineRange = null;
|
|
26
|
+
let cleanFilePath = filePath;
|
|
27
|
+
|
|
28
|
+
// Match ending with :start-end or :line (e.g. :10-50 or :100)
|
|
29
|
+
const match = filePath.match(/:(\d+)(?:-(\d+))?$/);
|
|
30
|
+
if (match) {
|
|
31
|
+
cleanFilePath = filePath.slice(0, match.index);
|
|
32
|
+
const start = parseInt(match[1], 10);
|
|
33
|
+
const end = match[2] ? parseInt(match[2], 10) : start;
|
|
34
|
+
lineRange = { start, end };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const resolved = resolve(cleanFilePath);
|
|
38
|
+
const ext = extname(resolved).toLowerCase();
|
|
39
|
+
let name = basename(resolved);
|
|
40
|
+
|
|
41
|
+
// Validate extension
|
|
42
|
+
if (!SUPPORTED_EXTENSIONS.has(ext)) {
|
|
43
|
+
const supported = [...SUPPORTED_EXTENSIONS].join(", ");
|
|
44
|
+
throw new Error(
|
|
45
|
+
`Unsupported file type: "${ext}"\nSupported types: ${supported}`
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Check file exists and get size
|
|
50
|
+
let fileStats;
|
|
51
|
+
try {
|
|
52
|
+
fileStats = await stat(resolved);
|
|
53
|
+
} catch {
|
|
54
|
+
throw new Error(`File not found: ${resolved}`);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (!fileStats.isFile()) {
|
|
58
|
+
throw new Error(`Not a file: ${resolved}`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Read file content
|
|
62
|
+
let content;
|
|
63
|
+
try {
|
|
64
|
+
content = await readFile(resolved, "utf-8");
|
|
65
|
+
} catch (err) {
|
|
66
|
+
throw new Error(`Cannot read file: ${err.message}`);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
let finalSize = fileStats.size;
|
|
70
|
+
|
|
71
|
+
if (lineRange) {
|
|
72
|
+
const lines = content.split(/\r?\n/);
|
|
73
|
+
const startIdx = Math.max(0, lineRange.start - 1);
|
|
74
|
+
const endIdx = Math.min(lines.length, lineRange.end);
|
|
75
|
+
content = lines.slice(startIdx, endIdx).join("\n");
|
|
76
|
+
name += `:${lineRange.start}-${lineRange.end}`;
|
|
77
|
+
finalSize = Buffer.byteLength(content, "utf-8");
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Trim if too long
|
|
81
|
+
if (content.length > MAX_CONTENT_LENGTH) {
|
|
82
|
+
content = content.slice(0, MAX_CONTENT_LENGTH) +
|
|
83
|
+
`\n\n[... truncated at ${MAX_CONTENT_LENGTH.toLocaleString()} characters]`;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return {
|
|
87
|
+
name,
|
|
88
|
+
content: content.trim(),
|
|
89
|
+
size: finalSize,
|
|
90
|
+
extension: ext,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Formats parsed file data into a context string for prompt injection.
|
|
96
|
+
* @param {{ name: string, content: string, size: number, extension: string }} fileData
|
|
97
|
+
* @returns {string}
|
|
98
|
+
*/
|
|
99
|
+
export function formatContext(fileData) {
|
|
100
|
+
return [
|
|
101
|
+
`[Context File: ${fileData.name} (${formatBytes(fileData.size)}, ${fileData.extension})]`,
|
|
102
|
+
"---",
|
|
103
|
+
fileData.content,
|
|
104
|
+
"---",
|
|
105
|
+
`[End of ${fileData.name}]`,
|
|
106
|
+
].join("\n");
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Formats bytes into a human-readable string.
|
|
111
|
+
* @param {number} bytes
|
|
112
|
+
* @returns {string}
|
|
113
|
+
*/
|
|
114
|
+
function formatBytes(bytes) {
|
|
115
|
+
if (bytes < 1024) return `${bytes}B`;
|
|
116
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
|
|
117
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const EXCLUDE_DIRS = new Set([
|
|
121
|
+
"node_modules", ".git", ".agents", "build", "dist", ".github",
|
|
122
|
+
"Krims Code_pip", "krims_code_agent_cli.egg-info", "krims_code_cli.egg-info"
|
|
123
|
+
]);
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Recursively scans baseDir and returns a list of supported files.
|
|
127
|
+
* @param {string} baseDir - Directory to scan
|
|
128
|
+
* @returns {string[]} List of relative file paths
|
|
129
|
+
*/
|
|
130
|
+
export function scanWorkspaceFiles(baseDir) {
|
|
131
|
+
const files = [];
|
|
132
|
+
|
|
133
|
+
function recurse(dir) {
|
|
134
|
+
let entries;
|
|
135
|
+
try {
|
|
136
|
+
entries = readdirSync(dir);
|
|
137
|
+
} catch {
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
for (const entry of entries) {
|
|
142
|
+
if (EXCLUDE_DIRS.has(entry)) continue;
|
|
143
|
+
const fullPath = join(dir, entry);
|
|
144
|
+
let stats;
|
|
145
|
+
try {
|
|
146
|
+
stats = statSync(fullPath);
|
|
147
|
+
} catch {
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (stats.isDirectory()) {
|
|
152
|
+
recurse(fullPath);
|
|
153
|
+
} else if (stats.isFile()) {
|
|
154
|
+
const ext = extname(entry).toLowerCase();
|
|
155
|
+
if (SUPPORTED_EXTENSIONS.has(ext)) {
|
|
156
|
+
files.push(relative(baseDir, fullPath));
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
recurse(baseDir);
|
|
163
|
+
return files.sort();
|
|
164
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { exec } from "node:child_process";
|
|
2
|
+
import { promisify } from "node:util";
|
|
3
|
+
|
|
4
|
+
const execAsync = promisify(exec);
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Checks if inside a git repository and returns the staged or unstaged diff.
|
|
8
|
+
* @returns {Promise<{ diff: string, isStaged: boolean }>}
|
|
9
|
+
*/
|
|
10
|
+
export async function getGitDiff() {
|
|
11
|
+
try {
|
|
12
|
+
await execAsync("git rev-parse --is-inside-work-tree");
|
|
13
|
+
} catch {
|
|
14
|
+
throw new Error("Not a git repository (or git is not installed).");
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Try staged changes first
|
|
18
|
+
const { stdout: staged } = await execAsync("git diff --cached");
|
|
19
|
+
if (staged.trim()) {
|
|
20
|
+
return { diff: staged.trim(), isStaged: true };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Fallback to unstaged changes
|
|
24
|
+
const { stdout: unstaged } = await execAsync("git diff");
|
|
25
|
+
if (unstaged.trim()) {
|
|
26
|
+
return { diff: unstaged.trim(), isStaged: false };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return { diff: "", isStaged: false };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Executes a git commit with the specified message.
|
|
34
|
+
* @param {string} message - The commit message
|
|
35
|
+
* @returns {Promise<string>} stdout output of the git commit command
|
|
36
|
+
*/
|
|
37
|
+
export async function runGitCommit(message) {
|
|
38
|
+
const escaped = message.replace(/"/g, '\\"');
|
|
39
|
+
const { stdout } = await execAsync(`git commit -m "${escaped}"`);
|
|
40
|
+
return stdout.trim();
|
|
41
|
+
}
|