@ayoxx/kundex 0.1.7
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/README.md +36 -0
- package/bin/kundex.mjs +18 -0
- package/package.json +33 -0
- package/src/config.ts +40 -0
- package/src/index.ts +41 -0
- package/src/login.ts +21 -0
- package/src/repl.ts +97 -0
- package/src/sdk.ts +178 -0
- package/src/tools.ts +105 -0
package/README.md
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# Kundex CLI & SDK
|
|
2
|
+
|
|
3
|
+
The Kundex terminal AI coding agent — usable as a global CLI (`kundex`) or as
|
|
4
|
+
an SDK (`import { ... } from "kundex"`).
|
|
5
|
+
|
|
6
|
+
## Install
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
npm install -g kundex
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## Usage
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
kundex login # store your kdx_live_... API key
|
|
16
|
+
kundex # start an interactive agent session
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Publishing (maintainers)
|
|
20
|
+
|
|
21
|
+
This export ships a GitHub Actions workflow at
|
|
22
|
+
`.github/workflows/publish-cli.yml` that publishes to npm whenever you push a
|
|
23
|
+
git tag matching `cli-v*` (e.g. `v0.2.0`):
|
|
24
|
+
|
|
25
|
+
1. Push this directory to a GitHub repo.
|
|
26
|
+
2. In the repo's Settings > Secrets and variables > Actions, add an
|
|
27
|
+
`NPM_TOKEN` secret (an npm automation token with publish rights).
|
|
28
|
+
3. Bump `version` in `package.json`, commit, then:
|
|
29
|
+
```bash
|
|
30
|
+
git tag v0.2.0
|
|
31
|
+
git push origin v0.2.0
|
|
32
|
+
```
|
|
33
|
+
4. The workflow installs dependencies, type-checks, and runs `npm publish`.
|
|
34
|
+
|
|
35
|
+
Update the `repository` field in `package.json` to point at your repo before
|
|
36
|
+
publishing.
|
package/bin/kundex.mjs
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Thin launcher: re-execs node with tsx's ESM loader registered via --import,
|
|
3
|
+
// so we can run the TypeScript source directly without a separate build
|
|
4
|
+
// step, then hands off to the real entrypoint.
|
|
5
|
+
import { spawnSync } from "node:child_process";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
|
|
9
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
10
|
+
const entry = path.join(here, "..", "src", "index.ts");
|
|
11
|
+
|
|
12
|
+
const result = spawnSync(
|
|
13
|
+
process.execPath,
|
|
14
|
+
["--import", "tsx/esm", entry, ...process.argv.slice(2)],
|
|
15
|
+
{ stdio: "inherit" },
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
process.exit(result.status ?? 0);
|
package/package.json
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@ayoxx/kundex",
|
|
3
|
+
"version": "0.1.7",
|
|
4
|
+
"description": "Kundex — a terminal-based AI coding agent CLI and SDK, powered by Groq.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"kundex": "./bin/kundex.mjs"
|
|
8
|
+
},
|
|
9
|
+
"exports": {
|
|
10
|
+
".": "./src/sdk.ts"
|
|
11
|
+
},
|
|
12
|
+
"scripts": {
|
|
13
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
14
|
+
"start": "node ./bin/kundex.mjs"
|
|
15
|
+
},
|
|
16
|
+
"dependencies": {
|
|
17
|
+
"tsx": "^4.20.6"
|
|
18
|
+
},
|
|
19
|
+
"devDependencies": {
|
|
20
|
+
"@types/node": "^22.15.29",
|
|
21
|
+
"typescript": "^5.9.2"
|
|
22
|
+
},
|
|
23
|
+
"repository": {
|
|
24
|
+
"type": "git",
|
|
25
|
+
"url": "https://github.com/ayoistooslick/kundex.git"
|
|
26
|
+
},
|
|
27
|
+
"license": "MIT",
|
|
28
|
+
"files": [
|
|
29
|
+
"bin",
|
|
30
|
+
"src",
|
|
31
|
+
"README.md"
|
|
32
|
+
]
|
|
33
|
+
}
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
|
|
5
|
+
export interface KundexConfig {
|
|
6
|
+
apiKey: string | null;
|
|
7
|
+
baseUrl: string | null;
|
|
8
|
+
defaultModel: string | null;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const CONFIG_DIR = path.join(os.homedir(), ".kundex");
|
|
12
|
+
const CONFIG_PATH = path.join(CONFIG_DIR, "config.json");
|
|
13
|
+
|
|
14
|
+
const EMPTY_CONFIG: KundexConfig = {
|
|
15
|
+
apiKey: null,
|
|
16
|
+
baseUrl: null,
|
|
17
|
+
defaultModel: null,
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export function loadConfig(): KundexConfig {
|
|
21
|
+
try {
|
|
22
|
+
const raw = fs.readFileSync(CONFIG_PATH, "utf-8");
|
|
23
|
+
return { ...EMPTY_CONFIG, ...JSON.parse(raw) };
|
|
24
|
+
} catch {
|
|
25
|
+
return { ...EMPTY_CONFIG };
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function saveConfig(config: Partial<KundexConfig>): KundexConfig {
|
|
30
|
+
const merged = { ...loadConfig(), ...config };
|
|
31
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
32
|
+
fs.writeFileSync(CONFIG_PATH, JSON.stringify(merged, null, 2));
|
|
33
|
+
return merged;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export const KUNDEX_BASE_URL = "https://kundex-api.onrender.com";
|
|
37
|
+
|
|
38
|
+
export function resolveBaseUrl(_config: KundexConfig): string | null {
|
|
39
|
+
return KUNDEX_BASE_URL;
|
|
40
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { loadConfig, resolveBaseUrl } from "./config";
|
|
2
|
+
import { runLogin } from "./login";
|
|
3
|
+
import { runRepl } from "./repl";
|
|
4
|
+
|
|
5
|
+
async function main(): Promise<void> {
|
|
6
|
+
const [, , command] = process.argv;
|
|
7
|
+
|
|
8
|
+
if (command === "login") {
|
|
9
|
+
await runLogin();
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
if (command === "help" || command === "--help" || command === "-h") {
|
|
14
|
+
console.log(
|
|
15
|
+
[
|
|
16
|
+
"kundeX — terminal AI coding agent.",
|
|
17
|
+
"",
|
|
18
|
+
"Usage:",
|
|
19
|
+
" kundex login Save your API key and API base URL",
|
|
20
|
+
" kundex Start the agent REPL in the current directory",
|
|
21
|
+
].join("\n"),
|
|
22
|
+
);
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const config = loadConfig();
|
|
27
|
+
const baseUrl = resolveBaseUrl(config);
|
|
28
|
+
|
|
29
|
+
if (!config.apiKey || !baseUrl) {
|
|
30
|
+
console.error('Not logged in. Run "kundex login" first.');
|
|
31
|
+
process.exitCode = 1;
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
await runRepl(config, config.apiKey, baseUrl);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
main().catch((err) => {
|
|
39
|
+
console.error(err instanceof Error ? err.message : err);
|
|
40
|
+
process.exitCode = 1;
|
|
41
|
+
});
|
package/src/login.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import readline from "node:readline/promises";
|
|
2
|
+
import { saveConfig } from "./config";
|
|
3
|
+
|
|
4
|
+
export async function runLogin(): Promise<void> {
|
|
5
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
6
|
+
|
|
7
|
+
try {
|
|
8
|
+
const apiKeyAnswer = await rl.question("Kundex API key (from the dashboard's API Keys page): ");
|
|
9
|
+
const apiKey = apiKeyAnswer.trim();
|
|
10
|
+
if (!apiKey) {
|
|
11
|
+
console.error("An API key is required.");
|
|
12
|
+
process.exitCode = 1;
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
saveConfig({ apiKey });
|
|
17
|
+
console.log("Saved. Run `kundex` in a project directory to start the agent.");
|
|
18
|
+
} finally {
|
|
19
|
+
rl.close();
|
|
20
|
+
}
|
|
21
|
+
}
|
package/src/repl.ts
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import readline from "node:readline/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { execSync } from "node:child_process";
|
|
4
|
+
import { Kundex, type AgentTurn } from "./sdk";
|
|
5
|
+
import { executeToolCall } from "./tools";
|
|
6
|
+
import type { KundexConfig } from "./config";
|
|
7
|
+
|
|
8
|
+
const HELP = `Commands:
|
|
9
|
+
/help Show this help
|
|
10
|
+
/model Show the model this session is using
|
|
11
|
+
/clear Start a new agent session
|
|
12
|
+
/exit Quit
|
|
13
|
+
Anything else is sent to the agent as a message.`;
|
|
14
|
+
|
|
15
|
+
function safeGitStatus(cwd: string): string | undefined {
|
|
16
|
+
try {
|
|
17
|
+
return execSync("git status --porcelain=v1 -b", { cwd, timeout: 5000 }).toString();
|
|
18
|
+
} catch {
|
|
19
|
+
return undefined;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async function handleTurn(
|
|
24
|
+
kundex: Kundex,
|
|
25
|
+
cwd: string,
|
|
26
|
+
sessionId: number,
|
|
27
|
+
turn: AgentTurn,
|
|
28
|
+
): Promise<void> {
|
|
29
|
+
let current = turn;
|
|
30
|
+
|
|
31
|
+
while (!current.done && current.toolCalls && current.toolCalls.length > 0) {
|
|
32
|
+
for (const call of current.toolCalls) {
|
|
33
|
+
console.log(`\n\u2192 ${call.name}(${call.arguments})`);
|
|
34
|
+
const { result, isError } = await executeToolCall(cwd, call);
|
|
35
|
+
console.log(isError ? ` error: ${result}` : ` ${result.slice(0, 2000)}`);
|
|
36
|
+
|
|
37
|
+
current = await kundex.agent.submitToolResult({
|
|
38
|
+
sessionId,
|
|
39
|
+
toolCallId: call.id,
|
|
40
|
+
result,
|
|
41
|
+
isError,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (current.reply) {
|
|
47
|
+
console.log(`\nkundex: ${current.reply}\n`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export async function runRepl(config: KundexConfig, apiKey: string, baseUrl: string): Promise<void> {
|
|
52
|
+
const cwd = process.cwd();
|
|
53
|
+
const kundex = new Kundex({ apiKey, baseUrl });
|
|
54
|
+
const model = config.defaultModel ?? "openai/gpt-oss-120b";
|
|
55
|
+
|
|
56
|
+
console.log(`Kundex agent — cwd: ${cwd}, model: ${model}`);
|
|
57
|
+
console.log("Type /help for commands.\n");
|
|
58
|
+
|
|
59
|
+
let session = await kundex.agent.createSession({ cwd, model });
|
|
60
|
+
|
|
61
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
62
|
+
|
|
63
|
+
try {
|
|
64
|
+
for (;;) {
|
|
65
|
+
const input = (await rl.question("> ")).trim();
|
|
66
|
+
if (!input) continue;
|
|
67
|
+
|
|
68
|
+
if (input === "/exit") break;
|
|
69
|
+
if (input === "/help") {
|
|
70
|
+
console.log(HELP);
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
if (input === "/model") {
|
|
74
|
+
console.log(`Model: ${session.model}`);
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
if (input === "/clear") {
|
|
78
|
+
session = await kundex.agent.createSession({ cwd, model });
|
|
79
|
+
console.log("Started a new session.");
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
try {
|
|
84
|
+
const turn = await kundex.agent.sendMessage({
|
|
85
|
+
sessionId: session.id,
|
|
86
|
+
message: input,
|
|
87
|
+
context: { gitStatus: safeGitStatus(cwd), openFiles: [] },
|
|
88
|
+
});
|
|
89
|
+
await handleTurn(kundex, cwd, session.id, turn);
|
|
90
|
+
} catch (err) {
|
|
91
|
+
console.error(`Error: ${(err as Error).message}`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
} finally {
|
|
95
|
+
rl.close();
|
|
96
|
+
}
|
|
97
|
+
}
|
package/src/sdk.ts
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
export interface KundexChatMessage {
|
|
2
|
+
role: "system" | "user" | "assistant" | "tool";
|
|
3
|
+
content?: string | null;
|
|
4
|
+
name?: string | null;
|
|
5
|
+
tool_call_id?: string | null;
|
|
6
|
+
tool_calls?: Array<{ id: string; name: string; arguments: string }> | null;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface KundexToolDefinition {
|
|
10
|
+
name: string;
|
|
11
|
+
description: string;
|
|
12
|
+
parametersJsonSchema: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface ChatUsage {
|
|
16
|
+
promptTokens: number;
|
|
17
|
+
completionTokens: number;
|
|
18
|
+
totalTokens: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface ModelInfo {
|
|
22
|
+
id: string;
|
|
23
|
+
name: string;
|
|
24
|
+
contextWindow: number;
|
|
25
|
+
inputPricePerMTok: number;
|
|
26
|
+
outputPricePerMTok: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface AgentTurn {
|
|
30
|
+
sessionId: number;
|
|
31
|
+
reply: string | null;
|
|
32
|
+
toolCalls: Array<{ id: string; name: string; arguments: string }> | null;
|
|
33
|
+
done: boolean;
|
|
34
|
+
usage: ChatUsage | null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface KundexOptions {
|
|
38
|
+
apiKey: string;
|
|
39
|
+
baseUrl: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Minimal programmatic SDK for the Kundex API. Used by the CLI and
|
|
44
|
+
* importable directly for building custom integrations:
|
|
45
|
+
*
|
|
46
|
+
* import { Kundex } from "kundex";
|
|
47
|
+
* const kundex = new Kundex({ apiKey, baseUrl });
|
|
48
|
+
* const result = await kundex.chat.create({ model, messages });
|
|
49
|
+
*/
|
|
50
|
+
export class Kundex {
|
|
51
|
+
private apiKey: string;
|
|
52
|
+
private baseUrl: string;
|
|
53
|
+
|
|
54
|
+
constructor(opts: KundexOptions) {
|
|
55
|
+
if (!opts.apiKey) throw new Error("Kundex: apiKey is required");
|
|
56
|
+
if (!opts.baseUrl) throw new Error("Kundex: baseUrl is required");
|
|
57
|
+
this.apiKey = opts.apiKey;
|
|
58
|
+
this.baseUrl = opts.baseUrl.replace(/\/+$/, "");
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
private async request<T>(pathname: string, body: unknown): Promise<T> {
|
|
62
|
+
const res = await fetch(`${this.baseUrl}${pathname}`, {
|
|
63
|
+
method: "POST",
|
|
64
|
+
headers: {
|
|
65
|
+
"content-type": "application/json",
|
|
66
|
+
authorization: `Bearer ${this.apiKey}`,
|
|
67
|
+
},
|
|
68
|
+
body: JSON.stringify(body),
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
if (!res.ok) {
|
|
72
|
+
const text = await res.text().catch(() => "");
|
|
73
|
+
throw new Error(`Kundex API error (${res.status}): ${text || res.statusText}`);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return (await res.json()) as T;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
models = {
|
|
80
|
+
list: async (): Promise<ModelInfo[]> => {
|
|
81
|
+
const res = await fetch(`${this.baseUrl}/v1/models`);
|
|
82
|
+
if (!res.ok) throw new Error(`Kundex API error (${res.status})`);
|
|
83
|
+
return (await res.json()) as ModelInfo[];
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
chat = {
|
|
88
|
+
/** Non-streaming chat completion. */
|
|
89
|
+
create: (params: {
|
|
90
|
+
model: string;
|
|
91
|
+
messages: KundexChatMessage[];
|
|
92
|
+
temperature?: number;
|
|
93
|
+
tools?: KundexToolDefinition[];
|
|
94
|
+
}) =>
|
|
95
|
+
this.request<{
|
|
96
|
+
id: string;
|
|
97
|
+
model: string;
|
|
98
|
+
message: KundexChatMessage;
|
|
99
|
+
finishReason: string;
|
|
100
|
+
usage: ChatUsage;
|
|
101
|
+
}>("/v1/chat/completions", { ...params, stream: false }),
|
|
102
|
+
|
|
103
|
+
/** Streaming chat completion; invokes onDelta as tokens arrive. */
|
|
104
|
+
stream: async (
|
|
105
|
+
params: {
|
|
106
|
+
model: string;
|
|
107
|
+
messages: KundexChatMessage[];
|
|
108
|
+
temperature?: number;
|
|
109
|
+
tools?: KundexToolDefinition[];
|
|
110
|
+
},
|
|
111
|
+
onDelta: (text: string) => void,
|
|
112
|
+
): Promise<{ finishReason: string; usage: ChatUsage | null }> => {
|
|
113
|
+
const res = await fetch(`${this.baseUrl}/v1/chat/completions`, {
|
|
114
|
+
method: "POST",
|
|
115
|
+
headers: {
|
|
116
|
+
"content-type": "application/json",
|
|
117
|
+
authorization: `Bearer ${this.apiKey}`,
|
|
118
|
+
},
|
|
119
|
+
body: JSON.stringify({ ...params, stream: true }),
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
if (!res.ok || !res.body) {
|
|
123
|
+
const text = await res.text().catch(() => "");
|
|
124
|
+
throw new Error(`Kundex API error (${res.status}): ${text || res.statusText}`);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const reader = res.body.getReader();
|
|
128
|
+
const decoder = new TextDecoder();
|
|
129
|
+
let buffer = "";
|
|
130
|
+
let finishReason = "stop";
|
|
131
|
+
let usage: ChatUsage | null = null;
|
|
132
|
+
|
|
133
|
+
for (;;) {
|
|
134
|
+
const { done, value } = await reader.read();
|
|
135
|
+
if (done) break;
|
|
136
|
+
buffer += decoder.decode(value, { stream: true });
|
|
137
|
+
const lines = buffer.split("\n");
|
|
138
|
+
buffer = lines.pop() ?? "";
|
|
139
|
+
|
|
140
|
+
for (const line of lines) {
|
|
141
|
+
const trimmed = line.trim();
|
|
142
|
+
if (!trimmed.startsWith("data:")) continue;
|
|
143
|
+
const payload = trimmed.slice(5).trim();
|
|
144
|
+
if (!payload) continue;
|
|
145
|
+
const json = JSON.parse(payload);
|
|
146
|
+
if (json.delta) onDelta(json.delta);
|
|
147
|
+
if (json.done) {
|
|
148
|
+
finishReason = json.finishReason ?? finishReason;
|
|
149
|
+
usage = json.usage ?? usage;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return { finishReason, usage };
|
|
155
|
+
},
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
agent = {
|
|
159
|
+
createSession: (params: { cwd: string; model?: string }) =>
|
|
160
|
+
this.request<{ id: number; cwd: string; model: string; createdAt: string }>(
|
|
161
|
+
"/v1/agent/session",
|
|
162
|
+
params,
|
|
163
|
+
),
|
|
164
|
+
|
|
165
|
+
sendMessage: (params: {
|
|
166
|
+
sessionId: number;
|
|
167
|
+
message: string;
|
|
168
|
+
context?: { fileTree?: string; gitStatus?: string; openFiles?: string[] };
|
|
169
|
+
}) => this.request<AgentTurn>("/v1/agent/message", params),
|
|
170
|
+
|
|
171
|
+
submitToolResult: (params: {
|
|
172
|
+
sessionId: number;
|
|
173
|
+
toolCallId: string;
|
|
174
|
+
result: string;
|
|
175
|
+
isError?: boolean;
|
|
176
|
+
}) => this.request<AgentTurn>("/v1/agent/tool-result", params),
|
|
177
|
+
};
|
|
178
|
+
}
|
package/src/tools.ts
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { exec } from "node:child_process";
|
|
4
|
+
import readline from "node:readline/promises";
|
|
5
|
+
|
|
6
|
+
export interface ToolCall {
|
|
7
|
+
id: string;
|
|
8
|
+
name: string;
|
|
9
|
+
arguments: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface ToolResult {
|
|
13
|
+
result: string;
|
|
14
|
+
isError: boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async function confirm(prompt: string): Promise<boolean> {
|
|
18
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
19
|
+
try {
|
|
20
|
+
const answer = await rl.question(`${prompt} [y/N] `);
|
|
21
|
+
return answer.trim().toLowerCase().startsWith("y");
|
|
22
|
+
} finally {
|
|
23
|
+
rl.close();
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function resolveInCwd(cwd: string, relPath: string): string {
|
|
28
|
+
const resolved = path.resolve(cwd, relPath);
|
|
29
|
+
if (!resolved.startsWith(path.resolve(cwd))) {
|
|
30
|
+
throw new Error("Path escapes the working directory");
|
|
31
|
+
}
|
|
32
|
+
return resolved;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function readFileTool(cwd: string, args: { path: string }): Promise<string> {
|
|
36
|
+
const full = resolveInCwd(cwd, args.path);
|
|
37
|
+
return fs.readFile(full, "utf-8");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async function writeFileTool(cwd: string, args: { path: string; content: string }): Promise<string> {
|
|
41
|
+
const approved = await confirm(`Write ${args.content.length} bytes to "${args.path}"?`);
|
|
42
|
+
if (!approved) return "User declined to write this file.";
|
|
43
|
+
const full = resolveInCwd(cwd, args.path);
|
|
44
|
+
await fs.mkdir(path.dirname(full), { recursive: true });
|
|
45
|
+
await fs.writeFile(full, args.content, "utf-8");
|
|
46
|
+
return `Wrote ${args.content.length} bytes to ${args.path}`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function runCommandTool(cwd: string, args: { command: string }): Promise<string> {
|
|
50
|
+
const approved = await confirm(`Run shell command: "${args.command}"?`);
|
|
51
|
+
if (!approved) return "User declined to run this command.";
|
|
52
|
+
return new Promise((resolve) => {
|
|
53
|
+
exec(args.command, { cwd, timeout: 60_000, maxBuffer: 1024 * 1024 }, (err, stdout, stderr) => {
|
|
54
|
+
if (err) {
|
|
55
|
+
resolve(`Command failed (${err.message}):\n${stdout}\n${stderr}`);
|
|
56
|
+
} else {
|
|
57
|
+
resolve(stdout || stderr || "(no output)");
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function gitTool(cwd: string, args: { args: string[] }): Promise<string> {
|
|
64
|
+
const isMutating = !["status", "diff", "log", "show", "branch"].includes(args.args[0]);
|
|
65
|
+
if (isMutating) {
|
|
66
|
+
const approved = await confirm(`Run: git ${args.args.join(" ")}?`);
|
|
67
|
+
if (!approved) return "User declined to run this git command.";
|
|
68
|
+
}
|
|
69
|
+
return new Promise((resolve) => {
|
|
70
|
+
exec(`git ${args.args.join(" ")}`, { cwd, timeout: 30_000, maxBuffer: 1024 * 1024 }, (err, stdout, stderr) => {
|
|
71
|
+
if (err) {
|
|
72
|
+
resolve(`git command failed (${err.message}):\n${stdout}\n${stderr}`);
|
|
73
|
+
} else {
|
|
74
|
+
resolve(stdout || stderr || "(no output)");
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Executes a single tool call locally, matching the server's AGENT_TOOLS contract. */
|
|
81
|
+
export async function executeToolCall(cwd: string, call: ToolCall): Promise<ToolResult> {
|
|
82
|
+
let args: any;
|
|
83
|
+
try {
|
|
84
|
+
args = JSON.parse(call.arguments);
|
|
85
|
+
} catch {
|
|
86
|
+
return { result: "Invalid JSON arguments from model.", isError: true };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
try {
|
|
90
|
+
switch (call.name) {
|
|
91
|
+
case "read_file":
|
|
92
|
+
return { result: await readFileTool(cwd, args), isError: false };
|
|
93
|
+
case "write_file":
|
|
94
|
+
return { result: await writeFileTool(cwd, args), isError: false };
|
|
95
|
+
case "run_command":
|
|
96
|
+
return { result: await runCommandTool(cwd, args), isError: false };
|
|
97
|
+
case "git":
|
|
98
|
+
return { result: await gitTool(cwd, args), isError: false };
|
|
99
|
+
default:
|
|
100
|
+
return { result: `Unknown tool: ${call.name}`, isError: true };
|
|
101
|
+
}
|
|
102
|
+
} catch (err) {
|
|
103
|
+
return { result: (err as Error).message, isError: true };
|
|
104
|
+
}
|
|
105
|
+
}
|