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,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
+ }