@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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Jeet Singh
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,142 @@
1
+ # kalcode
2
+
3
+ CLI coding agent powered by NVIDIA NIM.
4
+
5
+ ## Install
6
+
7
+ Global install:
8
+
9
+ ```bash
10
+ npm i -g @thejeetsingh/kalcode
11
+ ```
12
+
13
+ Run without global install:
14
+
15
+ ```bash
16
+ npx @thejeetsingh/kalcode --version
17
+ ```
18
+
19
+ ## Usage
20
+
21
+ ```bash
22
+ kalcode "your prompt here"
23
+ ```
24
+
25
+ ## Publish to npm
26
+
27
+ 1. Login:
28
+
29
+ ```bash
30
+ npm login
31
+ ```
32
+
33
+ 2. Check what will be published:
34
+
35
+ ```bash
36
+ npm pack --dry-run
37
+ ```
38
+
39
+ 3. Publish:
40
+
41
+ ```bash
42
+ npm publish --access public
43
+ ```
44
+
45
+ 4. Verify:
46
+
47
+ ```bash
48
+ npm view @thejeetsingh/kalcode version
49
+ ```
50
+
51
+ ## Deployment and Secrets
52
+
53
+ - Do not hardcode API keys in source files.
54
+ - Set `NVIDIA_NIM_KEY` in your shell or deployment environment.
55
+ - Use `kalcode --set-key` to store the key in `~/.kalcode/config.json` for local development.
56
+ - TLS verification is on by default. Do not disable it in production.
57
+ - `KALCODE_INSECURE_TLS=1` is available only as a local debugging escape hatch.
58
+
59
+ ## Secure Proxy Mode (share access without sharing your NVIDIA key)
60
+
61
+ Run the backend proxy with your NVIDIA key on a server you control:
62
+
63
+ ```bash
64
+ export NVIDIA_NIM_KEY="your-real-provider-key"
65
+ export KALCODE_PROXY_TOKEN="choose-a-long-random-token"
66
+ export PORT=8787
67
+ bun run proxy
68
+ ```
69
+
70
+ Client users run `kalcode` with proxy settings (no NVIDIA key needed on client):
71
+
72
+ ```bash
73
+ export KALCODE_PROXY_URL="https://your-proxy-domain"
74
+ export KALCODE_PROXY_TOKEN="same-token-as-server"
75
+ kalcode
76
+ ```
77
+
78
+ Optional server hardening vars:
79
+
80
+ - `KALCODE_PROXY_RATE_LIMIT_PER_MIN` - per-client request cap (default `120`)
81
+ - `HOST` - bind host (default `0.0.0.0`)
82
+ - `PORT` - bind port (default `8787`)
83
+
84
+ Health check endpoint:
85
+
86
+ ```bash
87
+ curl https://your-proxy-domain/health
88
+ ```
89
+
90
+ ## Vercel Deployment (serverless)
91
+
92
+ This repo now includes Vercel serverless endpoints:
93
+
94
+ - `POST /v1/chat/completions` (rewritten to `/api/v1/chat/completions`)
95
+ - `GET /health` (rewritten to `/api/health`)
96
+
97
+ ### 1) Deploy
98
+
99
+ - Import the `kalcode` folder as a Vercel project.
100
+ - Framework preset: **Other**.
101
+ - Build command: leave default/empty.
102
+
103
+ ### 2) Set Vercel environment variables
104
+
105
+ - `NVIDIA_NIM_KEY` = your provider key (server-only)
106
+ - `KALCODE_PROXY_TOKEN` = long random token for client auth
107
+
108
+ ### 3) Configure clients
109
+
110
+ ```bash
111
+ export KALCODE_PROXY_URL="https://<your-vercel-project>.vercel.app"
112
+ export KALCODE_PROXY_TOKEN="<same-token-as-vercel-env>"
113
+ kalcode
114
+ ```
115
+
116
+ ### 4) Verify
117
+
118
+ ```bash
119
+ curl https://<your-vercel-project>.vercel.app/health
120
+ ```
121
+
122
+ ## Options
123
+
124
+ - `-h, --help` - Show help
125
+ - `-v, --version` - Show version
126
+ - `-m, --model <id>` - Use a specific model
127
+ - `--set-key` - Set NVIDIA NIM API key
128
+ - `--compact` - Compact output
129
+ - `--auto-accept` - Skip permission prompts
130
+ - `--ask` - Read-only mode
131
+
132
+ ## REPL Commands
133
+
134
+ - `/help` - Show help
135
+ - `/model` - Show or switch model
136
+ - `/clear` - Clear conversation
137
+ - `/retry` - Retry last message
138
+ - `/compact` - Toggle compact output
139
+ - `/ask` - Toggle read-only mode
140
+ - `/skills` - List skills
141
+ - `/` - Show slash commands
142
+ - `/exit` - Quit
package/api/health.ts ADDED
@@ -0,0 +1,10 @@
1
+ export const runtime = "edge";
2
+
3
+ export default async function handler(): Promise<Response> {
4
+ return new Response(JSON.stringify({ ok: true }), {
5
+ status: 200,
6
+ headers: {
7
+ "Content-Type": "application/json",
8
+ },
9
+ });
10
+ }
@@ -0,0 +1,59 @@
1
+ const API_URL = "https://integrate.api.nvidia.com/v1/chat/completions";
2
+
3
+ export const runtime = "edge";
4
+
5
+ function json(status: number, payload: Record<string, unknown>): Response {
6
+ return new Response(JSON.stringify(payload), {
7
+ status,
8
+ headers: {
9
+ "Content-Type": "application/json",
10
+ },
11
+ });
12
+ }
13
+
14
+ function getBearerToken(req: Request): string {
15
+ const auth = req.headers.get("authorization") || "";
16
+ const m = auth.match(/^Bearer\s+(.+)$/i);
17
+ return m?.[1]?.trim() || "";
18
+ }
19
+
20
+ export default async function handler(req: Request): Promise<Response> {
21
+ if (req.method !== "POST") {
22
+ return json(405, { error: { message: "Method not allowed." } });
23
+ }
24
+
25
+ const serverNimKey = process.env.NVIDIA_NIM_KEY || "";
26
+ const requiredProxyToken = process.env.KALCODE_PROXY_TOKEN || "";
27
+
28
+ if (!serverNimKey) {
29
+ return json(500, { error: { message: "Server missing NVIDIA_NIM_KEY." } });
30
+ }
31
+
32
+ if (requiredProxyToken) {
33
+ const incoming = getBearerToken(req);
34
+ if (!incoming || incoming !== requiredProxyToken) {
35
+ return json(401, { error: { message: "Unauthorized." } });
36
+ }
37
+ }
38
+
39
+ const body = await req.text();
40
+ const upstream = await fetch(API_URL, {
41
+ method: "POST",
42
+ headers: {
43
+ "Content-Type": "application/json",
44
+ Authorization: `Bearer ${serverNimKey}`,
45
+ },
46
+ body,
47
+ });
48
+
49
+ const headers = new Headers();
50
+ const contentType = upstream.headers.get("content-type");
51
+ if (contentType) headers.set("Content-Type", contentType);
52
+ const cacheControl = upstream.headers.get("cache-control");
53
+ if (cacheControl) headers.set("Cache-Control", cacheControl);
54
+
55
+ return new Response(upstream.body, {
56
+ status: upstream.status,
57
+ headers,
58
+ });
59
+ }
package/bin/kalcode.ts ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env bun
2
+ // Optional local-dev escape hatch for TLS issues. Never enable in production.
3
+ if (process.env.KALCODE_INSECURE_TLS === "1") {
4
+ process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
5
+ console.warn("Warning: insecure TLS mode enabled (KALCODE_INSECURE_TLS=1).");
6
+ }
7
+
8
+ import { main } from "../src/index.js";
9
+
10
+ main().catch((err) => {
11
+ console.error("Fatal error:", err);
12
+ process.exit(1);
13
+ });
14
+
package/package.json ADDED
@@ -0,0 +1,56 @@
1
+ {
2
+ "name": "@thejeetsingh/kalcode",
3
+ "version": "2.0.0",
4
+ "description": "CLI coding agent powered by NVIDIA NIM",
5
+ "author": "Jeet Singh",
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/TheJeetSingh/Kalcode.git"
10
+ },
11
+ "homepage": "https://github.com/TheJeetSingh/Kalcode#readme",
12
+ "bugs": {
13
+ "url": "https://github.com/TheJeetSingh/Kalcode/issues"
14
+ },
15
+ "keywords": [
16
+ "cli",
17
+ "coding-assistant",
18
+ "ai",
19
+ "nvidia",
20
+ "nim",
21
+ "terminal"
22
+ ],
23
+ "type": "module",
24
+ "bin": {
25
+ "kalcode": "./bin/kalcode.ts"
26
+ },
27
+ "files": [
28
+ "bin",
29
+ "src",
30
+ "api",
31
+ "README.md",
32
+ "LICENSE",
33
+ "tsconfig.json",
34
+ "vercel.json"
35
+ ],
36
+ "engines": {
37
+ "bun": ">=1.1.0"
38
+ },
39
+ "publishConfig": {
40
+ "access": "public"
41
+ },
42
+ "scripts": {
43
+ "start": "bun run bin/kalcode.ts",
44
+ "dev": "bun --watch run bin/kalcode.ts",
45
+ "proxy": "bun run src/proxy/server.ts",
46
+ "proxy:dev": "bun --watch run src/proxy/server.ts",
47
+ "typecheck": "tsc --noEmit"
48
+ },
49
+ "dependencies": {
50
+ "chalk": "^5.4.0"
51
+ },
52
+ "devDependencies": {
53
+ "@types/bun": "latest",
54
+ "typescript": "^5.7.0"
55
+ }
56
+ }
@@ -0,0 +1,62 @@
1
+ import { readFileSync, existsSync, statSync } from "fs";
2
+ import { resolve, relative } from "path";
3
+ import { MAX_FILE_SIZE } from "../constants.js";
4
+
5
+ const addedFiles = new Map<string, string>();
6
+
7
+ export function addFileToContext(filePath: string): string {
8
+ const abs = resolve(filePath);
9
+ if (!existsSync(abs)) return `File not found: ${filePath}`;
10
+ const stat = statSync(abs);
11
+ if (stat.isDirectory()) return `Cannot add directory: ${filePath}`;
12
+ if (stat.size > MAX_FILE_SIZE) return `File too large: ${filePath}`;
13
+
14
+ try {
15
+ const content = readFileSync(abs, "utf-8");
16
+ const rel = relative(process.cwd(), abs);
17
+ addedFiles.set(rel, content);
18
+ return `Added ${rel} (${content.split("\n").length} lines)`;
19
+ } catch (err) {
20
+ return `Error reading ${filePath}: ${err}`;
21
+ }
22
+ }
23
+
24
+ export function dropFileFromContext(filePath: string): string {
25
+ const abs = resolve(filePath);
26
+ const rel = relative(process.cwd(), abs);
27
+ if (addedFiles.has(rel)) {
28
+ addedFiles.delete(rel);
29
+ return `Dropped ${rel}`;
30
+ }
31
+ // Try matching by the raw input too
32
+ if (addedFiles.has(filePath)) {
33
+ addedFiles.delete(filePath);
34
+ return `Dropped ${filePath}`;
35
+ }
36
+ return `Not in context: ${filePath}`;
37
+ }
38
+
39
+ export function getContextFiles(): Map<string, string> {
40
+ return addedFiles;
41
+ }
42
+
43
+ export function listContextFiles(): string[] {
44
+ return Array.from(addedFiles.keys());
45
+ }
46
+
47
+ export function clearContextFiles(): void {
48
+ addedFiles.clear();
49
+ }
50
+
51
+ export function buildContextBlock(): string {
52
+ if (addedFiles.size === 0) return "";
53
+ let block = "\n\nFiles in context (provided by user for reference):\n";
54
+ for (const [path, content] of addedFiles) {
55
+ const lines = content.split("\n");
56
+ const preview = lines.length > 200
57
+ ? lines.slice(0, 200).join("\n") + `\n... (${lines.length - 200} more lines)`
58
+ : content;
59
+ block += `\n--- ${path} ---\n${preview}\n`;
60
+ }
61
+ return block;
62
+ }
@@ -0,0 +1,70 @@
1
+ import type { Message } from "../types.js";
2
+ import { buildSystemPrompt } from "../constants.js";
3
+ import { loadProjectMemory } from "./memory.js";
4
+ import { buildContextBlock } from "./context.js";
5
+ import { isGitRepo, gitCurrentBranch } from "../git/git.js";
6
+
7
+ let messages: Message[] = [];
8
+
9
+ export async function initHistory(): Promise<void> {
10
+ let systemContent = buildSystemPrompt(process.cwd());
11
+
12
+ // Load project memory
13
+ const memory = loadProjectMemory(process.cwd());
14
+ if (memory) {
15
+ systemContent += `\n\nProject conventions (from KALCODE.md):\n${memory}`;
16
+ }
17
+
18
+ // Git context
19
+ if (isGitRepo()) {
20
+ const branch = await gitCurrentBranch();
21
+ systemContent += `\n\nGit: on branch "${branch}"`;
22
+ }
23
+
24
+ messages = [{ role: "system", content: systemContent }];
25
+ }
26
+
27
+ export function addMessage(msg: Message): void {
28
+ messages.push(msg);
29
+ }
30
+
31
+ export function getMessages(): Message[] {
32
+ // Inject context files into the last user message if present
33
+ const contextBlock = buildContextBlock();
34
+ if (!contextBlock) return messages;
35
+
36
+ // Clone and append context to the system message
37
+ const result = [...messages];
38
+ if (result.length > 0 && result[0]!.role === "system") {
39
+ result[0] = { ...result[0]!, content: result[0]!.content + contextBlock };
40
+ }
41
+ return result;
42
+ }
43
+
44
+ export function getLastUserMessage(): string | null {
45
+ for (let i = messages.length - 1; i >= 0; i--) {
46
+ if (messages[i]!.role === "user") {
47
+ return messages[i]!.content as string;
48
+ }
49
+ }
50
+ return null;
51
+ }
52
+
53
+ export function removeLastExchange(): void {
54
+ for (let i = messages.length - 1; i >= 0; i--) {
55
+ if (messages[i]!.role === "user") {
56
+ messages = messages.slice(0, i);
57
+ return;
58
+ }
59
+ }
60
+ }
61
+
62
+ export function clearHistory(): void {
63
+ // Re-init is async now, but we can just reset messages
64
+ const systemMsg = messages.length > 0 ? messages[0]! : { role: "system" as const, content: "" };
65
+ messages = [systemMsg];
66
+ }
67
+
68
+ export function getMessageCount(): number {
69
+ return messages.filter(m => m.role !== "system").length;
70
+ }