@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 +21 -0
- package/README.md +142 -0
- package/api/health.ts +10 -0
- package/api/v1/chat/completions.ts +59 -0
- package/bin/kalcode.ts +14 -0
- package/package.json +56 -0
- package/src/agent/context.ts +62 -0
- package/src/agent/history.ts +70 -0
- package/src/agent/loop.ts +282 -0
- package/src/agent/memory.ts +26 -0
- package/src/agent/permissions.ts +84 -0
- package/src/agent/text-tool-parser.ts +71 -0
- package/src/api/client.ts +110 -0
- package/src/api/stream-parser.ts +109 -0
- package/src/config.ts +61 -0
- package/src/constants.ts +58 -0
- package/src/git/git.ts +86 -0
- package/src/index.ts +403 -0
- package/src/proxy/server.ts +128 -0
- package/src/tools/edit-file.ts +97 -0
- package/src/tools/glob-tool.ts +59 -0
- package/src/tools/grep.ts +96 -0
- package/src/tools/list-directory.ts +101 -0
- package/src/tools/read-file.ts +71 -0
- package/src/tools/registry.ts +41 -0
- package/src/tools/run-command.ts +99 -0
- package/src/tools/write-file.ts +42 -0
- package/src/types.ts +68 -0
- package/src/ui/input.ts +60 -0
- package/src/ui/model-picker.ts +92 -0
- package/src/ui/skills-picker.ts +113 -0
- package/src/ui/skills.ts +152 -0
- package/src/ui/spinner.ts +56 -0
- package/src/ui/stream-renderer.ts +69 -0
- package/src/ui/terminal.ts +337 -0
- package/tsconfig.json +15 -0
- package/vercel.json +12 -0
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,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
|
+
}
|