@thejeetsingh/kalcode 2.0.0

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.
package/src/config.ts ADDED
@@ -0,0 +1,61 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
2
+ import { join } from "path";
3
+ import { homedir } from "os";
4
+ import type { Config } from "./types.js";
5
+ import { DEFAULT_MODEL, AVAILABLE_MODELS } from "./constants.js";
6
+
7
+ const CONFIG_DIR = join(homedir(), ".kalcode");
8
+ const CONFIG_FILE = join(CONFIG_DIR, "config.json");
9
+
10
+ const KNOWN_MODEL_IDS = new Set(AVAILABLE_MODELS.map(m => m.id));
11
+
12
+ export function loadConfig(): Config {
13
+ if (!existsSync(CONFIG_FILE)) {
14
+ return { apiKey: process.env.NVIDIA_NIM_KEY || "", model: DEFAULT_MODEL };
15
+ }
16
+ try {
17
+ const raw = readFileSync(CONFIG_FILE, "utf-8");
18
+ const parsed = JSON.parse(raw);
19
+ let model = parsed.model || DEFAULT_MODEL;
20
+
21
+ // Migrate old OpenRouter model IDs
22
+ if (model.includes(":free") || model.includes("openrouter/")) {
23
+ model = DEFAULT_MODEL;
24
+ }
25
+
26
+ // Ignore old OpenRouter keys
27
+ const apiKey = (parsed.apiKey && !parsed.apiKey.startsWith("sk-or-"))
28
+ ? parsed.apiKey
29
+ : (process.env.NVIDIA_NIM_KEY || "");
30
+ return { apiKey, model };
31
+ } catch {
32
+ return { apiKey: "", model: DEFAULT_MODEL };
33
+ }
34
+ }
35
+
36
+ export function saveConfig(config: Config): void {
37
+ if (!existsSync(CONFIG_DIR)) {
38
+ mkdirSync(CONFIG_DIR, { recursive: true });
39
+ }
40
+ writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2) + "\n");
41
+ }
42
+
43
+ export function getApiKey(): string {
44
+ return loadConfig().apiKey;
45
+ }
46
+
47
+ export function setApiKey(key: string): void {
48
+ const config = loadConfig();
49
+ config.apiKey = key;
50
+ saveConfig(config);
51
+ }
52
+
53
+ export function getModel(): string {
54
+ return loadConfig().model;
55
+ }
56
+
57
+ export function setModel(model: string): void {
58
+ const config = loadConfig();
59
+ config.model = model;
60
+ saveConfig(config);
61
+ }
@@ -0,0 +1,58 @@
1
+ export const VERSION = "2.0.0";
2
+
3
+ export const API_URL = "https://integrate.api.nvidia.com/v1/chat/completions";
4
+
5
+ export const DEFAULT_MODEL = "qwen/qwen3-coder-480b-a35b-instruct";
6
+
7
+ export const AVAILABLE_MODELS: { id: string; name: string; params: string }[] = [
8
+ { id: "qwen/qwen3-coder-480b-a35b-instruct", name: "Qwen3 Coder 480B", params: "480B MoE" },
9
+ { id: "mistralai/devstral-2-123b-instruct-2512", name: "Devstral 2 123B", params: "123B" },
10
+ { id: "deepseek-ai/deepseek-v3.2", name: "DeepSeek V3.2", params: "685B" },
11
+ { id: "mistralai/mistral-large-3-675b-instruct-2512", name: "Mistral Large 3", params: "675B" },
12
+ { id: "qwen/qwen3.5-397b-a17b", name: "Qwen 3.5 397B", params: "397B MoE" },
13
+ { id: "moonshotai/kimi-k2-instruct", name: "Kimi K2", params: "1T MoE" },
14
+ { id: "z-ai/glm5", name: "GLM 5", params: "744B MoE" },
15
+ { id: "nvidia/llama-3.1-nemotron-ultra-253b-v1", name: "Nemotron Ultra", params: "253B" },
16
+ { id: "meta/llama-3.1-405b-instruct", name: "Llama 3.1 405B", params: "405B" },
17
+ { id: "minimaxai/minimax-m2.5", name: "MiniMax M2.5", params: "230B" },
18
+ ];
19
+
20
+ export const MAX_LOOP_ITERATIONS = 50;
21
+ export const MAX_OUTPUT_BYTES = 50 * 1024;
22
+ export const DEFAULT_COMMAND_TIMEOUT = 30_000;
23
+ export const MAX_COMMAND_TIMEOUT = 45_000;
24
+ export const MAX_FILE_SIZE = 1024 * 1024;
25
+ export const DEFAULT_LINE_LIMIT = 2000;
26
+ export const MAX_GREP_RESULTS = 50;
27
+ export const MAX_GLOB_RESULTS = 1000;
28
+ export const MAX_DIR_ENTRIES = 500;
29
+ export const MAX_DIR_DEPTH = 3;
30
+ export const RATE_LIMIT_WINDOW = 60_000;
31
+ export const RATE_LIMIT_MAX = 60;
32
+ export const MAX_RETRIES = 3;
33
+ export const RETRY_BASE_DELAY = 5_000;
34
+
35
+ export function buildSystemPrompt(cwd: string): string {
36
+ return `You are kalcode, a CLI coding agent. You help users with software engineering tasks.
37
+
38
+ IMPORTANT: Your working directory is ${cwd}
39
+ ALL file operations MUST be relative to or inside this directory.
40
+ NEVER create or modify files outside of ${cwd} unless the user explicitly provides an absolute path elsewhere.
41
+
42
+ Tools available:
43
+ - readFile: Read file contents (line numbers included)
44
+ - writeFile: Create/overwrite files
45
+ - editFile: Search/replace edits to existing files
46
+ - runCommand: Execute shell commands (30s timeout)
47
+ - grep: Search file contents with regex
48
+ - glob: Find files by glob pattern
49
+ - listDirectory: List directory contents
50
+
51
+ Rules:
52
+ - Read before editing. Use editFile for surgical changes, writeFile for new files.
53
+ - Be concise. Lead with actions, not explanations.
54
+ - Use relative paths from cwd. For tool calls, prefix paths with ${cwd}/ if using absolute paths.
55
+ - When done, briefly summarize what you changed.
56
+ - For dangerous operations (rm, git push, etc), warn the user first.
57
+ - Do NOT explore directories outside the working directory.`;
58
+ }
package/src/git/git.ts ADDED
@@ -0,0 +1,86 @@
1
+ import { existsSync } from "fs";
2
+ import { join } from "path";
3
+
4
+ export function isGitRepo(cwd: string = process.cwd()): boolean {
5
+ return existsSync(join(cwd, ".git"));
6
+ }
7
+
8
+ async function run(args: string[], cwd?: string): Promise<{ ok: boolean; stdout: string; stderr: string }> {
9
+ try {
10
+ const proc = Bun.spawn(["git", ...args], {
11
+ stdout: "pipe",
12
+ stderr: "pipe",
13
+ cwd: cwd || process.cwd(),
14
+ });
15
+ const [stdoutBuf, stderrBuf] = await Promise.all([
16
+ new Response(proc.stdout).arrayBuffer(),
17
+ new Response(proc.stderr).arrayBuffer(),
18
+ ]);
19
+ const exitCode = await proc.exited;
20
+ return {
21
+ ok: exitCode === 0,
22
+ stdout: new TextDecoder().decode(stdoutBuf).trim(),
23
+ stderr: new TextDecoder().decode(stderrBuf).trim(),
24
+ };
25
+ } catch (err) {
26
+ return { ok: false, stdout: "", stderr: String(err) };
27
+ }
28
+ }
29
+
30
+ export async function gitStatus(): Promise<string> {
31
+ const r = await run(["status", "--short"]);
32
+ return r.ok ? (r.stdout || "Clean working tree") : `git error: ${r.stderr}`;
33
+ }
34
+
35
+ export async function gitDiff(staged = false): Promise<string> {
36
+ const args = staged ? ["diff", "--staged"] : ["diff"];
37
+ const r = await run(args);
38
+ return r.ok ? (r.stdout || "No changes") : `git error: ${r.stderr}`;
39
+ }
40
+
41
+ export async function gitDiffSummary(): Promise<string> {
42
+ const r = await run(["diff", "--stat"]);
43
+ const rs = await run(["diff", "--staged", "--stat"]);
44
+ let out = "";
45
+ if (r.stdout) out += `Unstaged:\n${r.stdout}\n`;
46
+ if (rs.stdout) out += `Staged:\n${rs.stdout}`;
47
+ return out || "No changes";
48
+ }
49
+
50
+ export async function gitLog(count = 5): Promise<string> {
51
+ const r = await run(["log", `--oneline`, `-${count}`]);
52
+ return r.ok ? (r.stdout || "No commits yet") : `git error: ${r.stderr}`;
53
+ }
54
+
55
+ export async function gitCommit(message: string): Promise<{ ok: boolean; output: string }> {
56
+ // Stage all changes
57
+ await run(["add", "-A"]);
58
+ const r = await run(["commit", "-m", message]);
59
+ return { ok: r.ok, output: r.ok ? r.stdout : r.stderr };
60
+ }
61
+
62
+ export async function gitUndo(): Promise<{ ok: boolean; output: string }> {
63
+ // Check if there's a commit to undo
64
+ const log = await run(["log", "--oneline", "-1"]);
65
+ if (!log.ok || !log.stdout) {
66
+ return { ok: false, output: "No commits to undo" };
67
+ }
68
+
69
+ const r = await run(["reset", "--soft", "HEAD~1"]);
70
+ if (r.ok) {
71
+ // Also unstage
72
+ await run(["reset", "HEAD"]);
73
+ return { ok: true, output: `Undid: ${log.stdout}` };
74
+ }
75
+ return { ok: false, output: r.stderr };
76
+ }
77
+
78
+ export async function gitCurrentBranch(): Promise<string> {
79
+ const r = await run(["branch", "--show-current"]);
80
+ return r.ok ? r.stdout : "unknown";
81
+ }
82
+
83
+ export async function hasPendingChanges(): Promise<boolean> {
84
+ const r = await run(["status", "--porcelain"]);
85
+ return r.ok && r.stdout.length > 0;
86
+ }
package/src/index.ts ADDED
@@ -0,0 +1,403 @@
1
+ import { parseArgs } from "util";
2
+ import { writeFileSync } from "fs";
3
+ import { join } from "path";
4
+ import chalk from "chalk";
5
+ import { VERSION, AVAILABLE_MODELS } from "./constants.js";
6
+ import { loadConfig, setApiKey, setModel, saveConfig } from "./config.js";
7
+ import { initHistory, clearHistory, getLastUserMessage, removeLastExchange } from "./agent/history.js";
8
+ import { runAgentLoop, setCompact, getCompact, setAskMode, getAskMode, interruptAgent } from "./agent/loop.js";
9
+ import { renderWelcome, renderHelp, renderError, renderSeparator, renderHints } from "./ui/terminal.js";
10
+ import { pickModel } from "./ui/model-picker.js";
11
+ import { createInput } from "./ui/input.js";
12
+ import { getSkills, searchSkills, renderSkillsList } from "./ui/skills.js";
13
+ import { setPermissionLevel, getPermissionLevel } from "./agent/permissions.js";
14
+ import { addFileToContext, dropFileFromContext, listContextFiles, clearContextFiles } from "./agent/context.js";
15
+ import { getMemoryFileName } from "./agent/memory.js";
16
+ import { isGitRepo, gitUndo, gitDiffSummary, gitLog, gitStatus, gitCommit } from "./git/git.js";
17
+ import * as readline from "readline";
18
+
19
+ const REPL_COMMANDS: { cmd: string; desc: string }[] = [
20
+ { cmd: "/help", desc: "Show help" },
21
+ { cmd: "/clear", desc: "Clear conversation + context" },
22
+ { cmd: "/retry", desc: "Retry last message" },
23
+ { cmd: "/model", desc: "Show/switch model" },
24
+ { cmd: "/compact", desc: "Toggle compact output" },
25
+ { cmd: "/ask", desc: "Toggle read-only mode" },
26
+ { cmd: "/auto", desc: "Toggle auto-accept permissions" },
27
+ { cmd: "/skills", desc: "List skills" },
28
+ { cmd: "/add", desc: "Add file to context" },
29
+ { cmd: "/drop", desc: "Remove file from context" },
30
+ { cmd: "/files", desc: "List context files" },
31
+ { cmd: "/diff", desc: "Show uncommitted changes" },
32
+ { cmd: "/status", desc: "Git status" },
33
+ { cmd: "/log", desc: "Recent commits" },
34
+ { cmd: "/undo", desc: "Undo last commit" },
35
+ { cmd: "/commit", desc: "Commit all changes" },
36
+ { cmd: "/init", desc: "Create KALCODE.md" },
37
+ { cmd: "/exit", desc: "Quit" },
38
+ { cmd: "/quit", desc: "Quit" },
39
+ { cmd: "/q", desc: "Quit" },
40
+ ];
41
+
42
+ export async function main(): Promise<void> {
43
+ const { values, positionals } = parseArgs({
44
+ args: Bun.argv.slice(2),
45
+ options: {
46
+ help: { type: "boolean", short: "h" },
47
+ version: { type: "boolean", short: "v" },
48
+ model: { type: "string", short: "m" },
49
+ "set-key": { type: "boolean" },
50
+ "set-model": { type: "string" },
51
+ compact: { type: "boolean" },
52
+ "auto-accept": { type: "boolean" },
53
+ ask: { type: "boolean" },
54
+ },
55
+ allowPositionals: true,
56
+ strict: true,
57
+ });
58
+
59
+ if (values.version) {
60
+ console.log(`kalcode v${VERSION}`);
61
+ return;
62
+ }
63
+
64
+ if (values.help) {
65
+ printHelp();
66
+ return;
67
+ }
68
+
69
+ if (values["set-key"]) {
70
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
71
+ const key = await new Promise<string>((resolve) => {
72
+ rl.question(chalk.dim(" NVIDIA NIM API key: "), (ans) => { rl.close(); resolve(ans.trim()); });
73
+ });
74
+ if (key) { setApiKey(key); console.log(chalk.green(" ✓ Saved to ~/.kalcode/config.json")); }
75
+ else { console.log(chalk.yellow(" No key provided.")); }
76
+ return;
77
+ }
78
+
79
+ if (values["set-model"]) {
80
+ setModel(values["set-model"]);
81
+ console.log(chalk.green(` ✓ Default model: ${values["set-model"]}`));
82
+ return;
83
+ }
84
+
85
+ const config = loadConfig();
86
+ if (values.model) config.model = values.model;
87
+ if (values.compact) setCompact(true);
88
+ if (values["auto-accept"]) setPermissionLevel("auto");
89
+ if (values.ask) setAskMode(true);
90
+ const proxyUrl = (process.env.KALCODE_PROXY_URL || "").trim();
91
+ const proxyMode = proxyUrl.length > 0;
92
+
93
+ // API key check (skip when using proxy mode)
94
+ if (!proxyMode && !config.apiKey) {
95
+ console.log("");
96
+ console.log(chalk.yellow(" No API key found."));
97
+ console.log(chalk.dim(" Get one at https://build.nvidia.com (NVIDIA NIM)"));
98
+ console.log("");
99
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
100
+ const key = await new Promise<string>((resolve) => {
101
+ rl.question(chalk.dim(" NVIDIA NIM API key: "), (ans) => { rl.close(); resolve(ans.trim()); });
102
+ });
103
+ if (!key) { console.log(chalk.red(" API key required.")); return; }
104
+ setApiKey(key);
105
+ config.apiKey = key;
106
+ console.log(chalk.green(" ✓ Saved."));
107
+ console.log("");
108
+ }
109
+
110
+ await initHistory();
111
+
112
+ // Single-shot mode
113
+ if (positionals.length > 0) {
114
+ await runAgentLoop(config.apiKey, config.model, positionals.join(" "));
115
+ return;
116
+ }
117
+
118
+ // ─── Interactive REPL ─────────────────────────────────────────────
119
+ renderWelcome(config.model);
120
+
121
+ // Status context
122
+ const statusParts: string[] = [];
123
+ const memFile = getMemoryFileName(process.cwd());
124
+ if (memFile) statusParts.push(`memory: ${memFile}`);
125
+ if (isGitRepo()) statusParts.push("git: repo detected");
126
+ if (proxyMode) statusParts.push("api: proxy");
127
+ if (statusParts.length > 0) {
128
+ console.log(chalk.dim(` ${statusParts.join(" · ")}`));
129
+ }
130
+
131
+ renderSeparator();
132
+ renderHints();
133
+
134
+ // Ctrl+C handling
135
+ let agentRunning = false;
136
+ process.on("SIGINT", () => {
137
+ if (agentRunning) {
138
+ interruptAgent();
139
+ } else {
140
+ console.log(chalk.dim("\n Goodbye."));
141
+ process.exit(0);
142
+ }
143
+ });
144
+
145
+ const input = createInput(REPL_COMMANDS.map((c) => c.cmd));
146
+
147
+ while (true) {
148
+ let userInput: string;
149
+ try {
150
+ userInput = await input.prompt();
151
+ } catch {
152
+ await new Promise(r => setTimeout(r, 50));
153
+ continue;
154
+ }
155
+
156
+ if (!userInput) continue;
157
+
158
+ if (userInput.startsWith("/")) {
159
+ const parts = userInput.split(/\s+/);
160
+ const rawCmd = parts[0]!.toLowerCase();
161
+ const arg = parts.slice(1).join(" ");
162
+ const commandNames = REPL_COMMANDS.map((c) => c.cmd);
163
+
164
+ if (rawCmd === "/") {
165
+ renderSlashCommandMenu(commandNames);
166
+ continue;
167
+ }
168
+
169
+ let cmd = rawCmd;
170
+ if (!commandNames.includes(rawCmd)) {
171
+ const matches = commandNames.filter((c) => c.startsWith(rawCmd));
172
+ if (matches.length === 1) {
173
+ cmd = matches[0]!;
174
+ console.log(chalk.dim(` → ${rawCmd} resolved to ${cmd}`));
175
+ } else if (matches.length > 1) {
176
+ renderSlashCommandMenu(matches);
177
+ continue;
178
+ } else {
179
+ console.log(chalk.dim(` Unknown command: ${rawCmd}. Type / for command list.`));
180
+ continue;
181
+ }
182
+ }
183
+
184
+ switch (cmd) {
185
+ case "/exit": case "/quit": case "/q":
186
+ console.log(chalk.dim(" Goodbye."));
187
+ input.close();
188
+ return;
189
+
190
+ case "/help":
191
+ renderHelp();
192
+ continue;
193
+
194
+ case "/clear":
195
+ clearHistory();
196
+ clearContextFiles();
197
+ console.log(chalk.dim(" Conversation cleared."));
198
+ continue;
199
+
200
+ case "/retry": {
201
+ const lastMsg = getLastUserMessage();
202
+ if (!lastMsg) { console.log(chalk.dim(" Nothing to retry.")); continue; }
203
+ removeLastExchange();
204
+ console.log(chalk.dim(` Retrying: ${lastMsg.slice(0, 60)}${lastMsg.length > 60 ? "…" : ""}`));
205
+ // Close readline before agent run so permission keypresses do not leak into prompt input.
206
+ input.close();
207
+ agentRunning = true;
208
+ await runAgentLoop(config.apiKey, config.model, lastMsg);
209
+ agentRunning = false;
210
+ continue;
211
+ }
212
+
213
+ case "/compact":
214
+ setCompact(!getCompact());
215
+ console.log(chalk.dim(` Compact: ${getCompact() ? "on" : "off"}`));
216
+ continue;
217
+
218
+ case "/ask":
219
+ setAskMode(!getAskMode());
220
+ console.log(chalk.dim(` Read-only mode: ${getAskMode() ? "on" : "off"}`));
221
+ continue;
222
+
223
+ case "/auto":
224
+ setPermissionLevel(getPermissionLevel() === "auto" ? "ask" : "auto");
225
+ console.log(chalk.dim(` Auto-accept: ${getPermissionLevel() === "auto" ? "on" : "off"}`));
226
+ continue;
227
+
228
+ case "/model":
229
+ if (arg) {
230
+ config.model = arg;
231
+ saveConfig(config);
232
+ console.log(chalk.green(` ✓ Model: ${arg}`));
233
+ } else {
234
+ // Close readline before opening raw-mode picker to avoid keypress buffering issues.
235
+ input.close();
236
+ const picked = await pickModel(AVAILABLE_MODELS, config.model);
237
+ if (picked && picked !== config.model) {
238
+ config.model = picked;
239
+ saveConfig(config);
240
+ console.log(chalk.green(` ✓ Switched to ${picked}`));
241
+ } else if (picked) {
242
+ console.log(chalk.dim(` Already using ${picked}`));
243
+ } else {
244
+ console.log(chalk.dim(" Cancelled."));
245
+ }
246
+ }
247
+ continue;
248
+
249
+ case "/skills": {
250
+ const skills = arg ? searchSkills(arg) : getSkills();
251
+ renderSkillsList(skills, arg);
252
+ continue;
253
+ }
254
+
255
+ case "/add":
256
+ if (!arg) { console.log(chalk.dim(" Usage: /add <file>")); continue; }
257
+ console.log(chalk.dim(` ${addFileToContext(arg)}`));
258
+ continue;
259
+
260
+ case "/drop":
261
+ if (!arg) { console.log(chalk.dim(" Usage: /drop <file>")); continue; }
262
+ console.log(chalk.dim(` ${dropFileFromContext(arg)}`));
263
+ continue;
264
+
265
+ case "/files": {
266
+ const files = listContextFiles();
267
+ if (files.length === 0) { console.log(chalk.dim(" No files in context. Use /add <file>")); }
268
+ else { files.forEach((f) => console.log(chalk.dim(` · ${f}`))); }
269
+ continue;
270
+ }
271
+
272
+ case "/diff":
273
+ if (!isGitRepo()) { console.log(chalk.dim(" Not a git repo.")); continue; }
274
+ console.log(chalk.dim(await gitDiffSummary()));
275
+ continue;
276
+
277
+ case "/status":
278
+ if (!isGitRepo()) { console.log(chalk.dim(" Not a git repo.")); continue; }
279
+ console.log(chalk.dim(await gitStatus()));
280
+ continue;
281
+
282
+ case "/log":
283
+ if (!isGitRepo()) { console.log(chalk.dim(" Not a git repo.")); continue; }
284
+ console.log(chalk.dim(await gitLog()));
285
+ continue;
286
+
287
+ case "/undo":
288
+ if (!isGitRepo()) { console.log(chalk.dim(" Not a git repo.")); continue; }
289
+ const undoResult = await gitUndo();
290
+ console.log(undoResult.ok ? chalk.green(` ✓ ${undoResult.output}`) : chalk.red(` ✗ ${undoResult.output}`));
291
+ continue;
292
+
293
+ case "/commit": {
294
+ if (!isGitRepo()) { console.log(chalk.dim(" Not a git repo.")); continue; }
295
+ const msg = arg || "kalcode changes";
296
+ const commitResult = await gitCommit(msg);
297
+ console.log(commitResult.ok ? chalk.green(` ✓ ${commitResult.output}`) : chalk.red(` ✗ ${commitResult.output}`));
298
+ continue;
299
+ }
300
+
301
+ case "/init": {
302
+ const path = join(process.cwd(), "KALCODE.md");
303
+ const template = `# Project Conventions
304
+
305
+ ## Tech Stack
306
+ <!-- e.g., TypeScript, React, Node.js -->
307
+
308
+ ## Architecture
309
+ <!-- Brief description of project structure -->
310
+
311
+ ## Coding Style
312
+ <!-- e.g., Use functional components, prefer const, etc. -->
313
+
314
+ ## Important Files
315
+ <!-- Key files the agent should know about -->
316
+
317
+ ## Rules
318
+ <!-- Any rules the agent should follow -->
319
+ `;
320
+ writeFileSync(path, template);
321
+ console.log(chalk.green(" ✓ Created KALCODE.md — edit it with your project conventions"));
322
+ continue;
323
+ }
324
+
325
+ default:
326
+ console.log(chalk.dim(` Unknown command: ${cmd}. Type /help`));
327
+ continue;
328
+ }
329
+ }
330
+
331
+ // Close readline before agent run so permission keypresses do not leak into prompt input.
332
+ input.close();
333
+ agentRunning = true;
334
+ await runAgentLoop(config.apiKey, config.model, userInput);
335
+ agentRunning = false;
336
+ }
337
+
338
+ input.close();
339
+ }
340
+
341
+ function printHelp(): void {
342
+ console.log(`
343
+ ${chalk.bold("kalcode")} — CLI coding agent powered by NVIDIA NIM
344
+
345
+ ${chalk.bold("Usage:")}
346
+ kalcode [options] [prompt]
347
+
348
+ ${chalk.bold("Options:")}
349
+ -h, --help Show this help
350
+ -v, --version Show version
351
+ -m, --model <id> Use a specific model
352
+ --set-key Set NVIDIA NIM API key
353
+ --set-model <id> Set default model
354
+ --compact Compact output
355
+ --auto-accept Skip permission prompts
356
+ --ask Read-only mode (no writes)
357
+
358
+ ${chalk.bold("REPL commands:")}
359
+ /help Show help
360
+ /model [id] Show or switch model
361
+ /clear Clear conversation + context
362
+ /retry Retry last message
363
+ /compact Toggle compact output
364
+ /ask Toggle read-only mode
365
+ /auto Toggle auto-accept permissions
366
+ /skills [query] List skills (filter optional)
367
+ /add <file> Add file to context
368
+ /drop <file> Remove file from context
369
+ /files List context files
370
+
371
+ ${chalk.bold("Git commands:")}
372
+ /diff Show uncommitted changes
373
+ /status Git status
374
+ /log Recent commits
375
+ /undo Undo last commit (soft reset)
376
+ /commit [msg] Commit all changes
377
+
378
+ ${chalk.bold("Project:")}
379
+ /init Create KALCODE.md conventions file
380
+ /exit Quit
381
+
382
+ ${chalk.bold("Models (NVIDIA NIM):")}
383
+ ${AVAILABLE_MODELS.map(m => ` ${chalk.dim(m.name.padEnd(22))} ${m.id}`).join("\n")}
384
+
385
+ ${chalk.bold("Examples:")}
386
+ kalcode "fix the bug in main.ts"
387
+ kalcode -m nvidia/llama-3.1-405b-instruct "explain this"
388
+ kalcode --ask "explain this codebase"
389
+ / Show all slash commands
390
+ /he Auto-resolve to /help when unique
391
+ `);
392
+ }
393
+
394
+ function renderSlashCommandMenu(commands: string[]): void {
395
+ console.log("");
396
+ console.log(chalk.bold(" Slash commands"));
397
+ for (const cmd of commands) {
398
+ const def = REPL_COMMANDS.find((c) => c.cmd === cmd);
399
+ if (!def) continue;
400
+ console.log(` ${chalk.bold(cmd.padEnd(12))}${chalk.dim(def.desc)}`);
401
+ }
402
+ console.log("");
403
+ }