@ayoxx/kundex 2.0.5 → 2.0.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/package.json +1 -1
- package/src/index.ts +9 -2
- package/src/repl.ts +24 -7
- package/src/tools.ts +224 -2
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { runLogin } from "./login";
|
|
|
3
3
|
import { runRepl } from "./repl";
|
|
4
4
|
import { Kundex } from "./sdk";
|
|
5
5
|
|
|
6
|
-
const VERSION = "2.0.
|
|
6
|
+
const VERSION = "2.0.7";
|
|
7
7
|
|
|
8
8
|
// ─── Help text ────────────────────────────────────────────────────────────────
|
|
9
9
|
|
|
@@ -335,6 +335,13 @@ async function main(): Promise<void> {
|
|
|
335
335
|
}
|
|
336
336
|
|
|
337
337
|
main().catch((e) => {
|
|
338
|
-
|
|
338
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
339
|
+
if (msg.includes("fetch failed") || msg.includes("ECONNREFUSED") || msg.includes("ENOTFOUND")) {
|
|
340
|
+
console.error(`\n${c.red} ✗ Could not connect to the API server.${c.reset}`);
|
|
341
|
+
console.error(`${c.dim} Check your internet connection and try again.${c.reset}`);
|
|
342
|
+
console.error(`${c.dim} Run "kundex login" if you haven't set up your API key.${c.reset}\n`);
|
|
343
|
+
} else {
|
|
344
|
+
console.error(`\n${c.red} ✗ ${msg}${c.reset}\n`);
|
|
345
|
+
}
|
|
339
346
|
process.exitCode = 1;
|
|
340
347
|
});
|
package/src/repl.ts
CHANGED
|
@@ -161,11 +161,19 @@ ${c.bold}Commands:${c.reset}
|
|
|
161
161
|
/clear Start a fresh agent session
|
|
162
162
|
/exit Quit kundex
|
|
163
163
|
|
|
164
|
+
${c.bold}Capabilities:${c.reset}
|
|
165
|
+
• Read, write, edit, and create files
|
|
166
|
+
• Run shell commands (npm, git, python, make, etc.)
|
|
167
|
+
• Search through codebases
|
|
168
|
+
• Search the web for documentation and solutions
|
|
169
|
+
• Fetch and read web pages
|
|
170
|
+
• Initialize git repos and make commits
|
|
171
|
+
• Install packages and dependencies
|
|
172
|
+
|
|
164
173
|
${c.bold}Tips:${c.reset}
|
|
165
|
-
•
|
|
166
|
-
•
|
|
167
|
-
• Responses stream in real-time
|
|
168
|
-
• Code blocks are syntax-highlighted in the terminal.
|
|
174
|
+
• Code blocks are syntax-highlighted automatically.
|
|
175
|
+
• The agent will ask for confirmation before writes and shell commands.
|
|
176
|
+
• Responses stream in real-time.
|
|
169
177
|
• Press Ctrl+C to abort a running request.
|
|
170
178
|
`;
|
|
171
179
|
|
|
@@ -245,10 +253,13 @@ async function handleTurn(
|
|
|
245
253
|
}
|
|
246
254
|
}
|
|
247
255
|
|
|
248
|
-
if (current.reply && !turn.toolCalls?.length) {
|
|
256
|
+
if (current.reply && current.reply.trim() && !turn.toolCalls?.length) {
|
|
249
257
|
process.stdout.write(`\n${c.dim}${"─".repeat(60)}${c.reset}\n\n`);
|
|
250
|
-
} else if (current.reply) {
|
|
258
|
+
} else if (current.reply && current.reply.trim()) {
|
|
251
259
|
printReply(current.reply);
|
|
260
|
+
} else if (turn.toolCalls?.length) {
|
|
261
|
+
// After tool calls, if no text was returned, just print the separator
|
|
262
|
+
process.stdout.write(`\n${c.dim}${"─".repeat(60)}${c.reset}\n\n`);
|
|
252
263
|
}
|
|
253
264
|
}
|
|
254
265
|
|
|
@@ -351,14 +362,20 @@ export async function runRepl(config: KundexConfig, apiKey: string, baseUrl: str
|
|
|
351
362
|
await handleTurn(kundex, cwd, session.id, turn, signal);
|
|
352
363
|
} else if (streamedAnyText) {
|
|
353
364
|
process.stdout.write(`\n${c.dim}${"─".repeat(60)}${c.reset}\n\n`);
|
|
354
|
-
} else if (turn.reply) {
|
|
365
|
+
} else if (turn.reply && turn.reply.trim()) {
|
|
355
366
|
printReply(turn.reply);
|
|
367
|
+
} else {
|
|
368
|
+
// Empty response — show a message so user knows something happened
|
|
369
|
+
process.stdout.write(`\n${c.dim} (no response — try rephrasing your message)${c.reset}\n\n`);
|
|
356
370
|
}
|
|
357
371
|
} catch (err) {
|
|
358
372
|
spinner.clear();
|
|
359
373
|
const e = err as Error;
|
|
360
374
|
if (e.name === "AbortError") {
|
|
361
375
|
// Already printed "⚡ Stopped" via sigintHandler
|
|
376
|
+
} else if (e.message.includes("fetch failed") || e.message.includes("ECONNREFUSED")) {
|
|
377
|
+
console.error(`\n${c.red} ✗ Could not connect to the API server.${c.reset}`);
|
|
378
|
+
console.error(`${c.dim} Check your internet connection and try again.${c.reset}\n`);
|
|
362
379
|
} else {
|
|
363
380
|
console.error(`\n${c.red} error: ${e.message}${c.reset}\n`);
|
|
364
381
|
}
|
package/src/tools.ts
CHANGED
|
@@ -33,6 +33,34 @@ function header(label: string, value: string) {
|
|
|
33
33
|
);
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
function printDiff(oldContent: string | null, newContent: string, filePath: string) {
|
|
37
|
+
const oldLines = oldContent ? oldContent.split("\n") : [];
|
|
38
|
+
const newLines = newContent.split("\n");
|
|
39
|
+
const maxLines = Math.max(oldLines.length, newLines.length);
|
|
40
|
+
const LIMIT = 40;
|
|
41
|
+
const shown = Math.min(maxLines, LIMIT);
|
|
42
|
+
|
|
43
|
+
console.log(`${c.bold}${c.white} ┌─ ${filePath} ─────────────────────────${c.reset}`);
|
|
44
|
+
for (let i = 0; i < shown; i++) {
|
|
45
|
+
const o = oldLines[i];
|
|
46
|
+
const n = newLines[i];
|
|
47
|
+
if (o === undefined) {
|
|
48
|
+
process.stdout.write(`${c.green} + ${(i + 1).toString().padStart(4)} ${n ?? ""}${c.reset}\n`);
|
|
49
|
+
} else if (n === undefined) {
|
|
50
|
+
process.stdout.write(`${c.red} - ${(i + 1).toString().padStart(4)} ${o ?? ""}${c.reset}\n`);
|
|
51
|
+
} else if (o !== n) {
|
|
52
|
+
process.stdout.write(`${c.red} - ${(i + 1).toString().padStart(4)} ${o}${c.reset}\n`);
|
|
53
|
+
process.stdout.write(`${c.green} + ${(i + 1).toString().padStart(4)} ${n}${c.reset}\n`);
|
|
54
|
+
} else {
|
|
55
|
+
process.stdout.write(`${c.dim} ${(i + 1).toString().padStart(4)} ${o}${c.reset}\n`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
if (maxLines > LIMIT) {
|
|
59
|
+
console.log(`${c.dim} ... ${maxLines - LIMIT} more lines omitted${c.reset}`);
|
|
60
|
+
}
|
|
61
|
+
console.log(`${c.bold}${c.white} └──────────────────────────────────────${c.reset}\n`);
|
|
62
|
+
}
|
|
63
|
+
|
|
36
64
|
async function confirm(prompt: string): Promise<boolean> {
|
|
37
65
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
38
66
|
try {
|
|
@@ -51,19 +79,108 @@ function resolveInCwd(cwd: string, relPath: string): string {
|
|
|
51
79
|
return resolved;
|
|
52
80
|
}
|
|
53
81
|
|
|
82
|
+
// ── File tools ────────────────────────────────────────────────────────────────
|
|
83
|
+
|
|
54
84
|
async function readFileTool(cwd: string, args: { path: string }): Promise<string> {
|
|
55
85
|
const full = resolveInCwd(cwd, args.path);
|
|
56
86
|
header("read_file", args.path);
|
|
57
87
|
return fs.readFile(full, "utf-8");
|
|
58
88
|
}
|
|
59
89
|
|
|
90
|
+
async function writeFileTool(cwd: string, args: { path: string; content: string }): Promise<string> {
|
|
91
|
+
const full = resolveInCwd(cwd, args.path);
|
|
92
|
+
const lines = args.content.split("\n").length;
|
|
93
|
+
const bytes = Buffer.byteLength(args.content, "utf-8");
|
|
94
|
+
|
|
95
|
+
console.log(`\n${c.bold}${c.magenta} ◆ write_file${c.reset} ${c.dim}${args.path}${c.reset}`);
|
|
96
|
+
console.log(`${c.dim} ${lines} lines · ${bytes} bytes${c.reset}`);
|
|
97
|
+
|
|
98
|
+
let oldContent: string | null = null;
|
|
99
|
+
try {
|
|
100
|
+
oldContent = await fs.readFile(full, "utf-8");
|
|
101
|
+
console.log(`${c.dim} (modifying existing file)${c.reset}`);
|
|
102
|
+
} catch {
|
|
103
|
+
console.log(`${c.dim} (creating new file)${c.reset}`);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
printDiff(oldContent, args.content, args.path);
|
|
107
|
+
|
|
108
|
+
const approved = await confirm(`Write ${bytes} bytes to "${args.path}"?`);
|
|
109
|
+
if (!approved) return "User declined to write this file.";
|
|
110
|
+
|
|
111
|
+
await fs.mkdir(path.dirname(full), { recursive: true });
|
|
112
|
+
await fs.writeFile(full, args.content, "utf-8");
|
|
113
|
+
|
|
114
|
+
console.log(`${c.green}${c.bold} ✓ Wrote ${args.path}${c.reset} ${c.dim}(${bytes} bytes)${c.reset}\n`);
|
|
115
|
+
return `Wrote ${bytes} bytes to ${args.path}`;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async function appendToFileTool(cwd: string, args: { path: string; content: string }): Promise<string> {
|
|
119
|
+
const full = resolveInCwd(cwd, args.path);
|
|
120
|
+
const bytes = Buffer.byteLength(args.content, "utf-8");
|
|
121
|
+
|
|
122
|
+
header("append_to_file", args.path);
|
|
123
|
+
|
|
124
|
+
const approved = await confirm(`Append ${bytes} bytes to "${args.path}"?`);
|
|
125
|
+
if (!approved) return "User declined to append to this file.";
|
|
126
|
+
|
|
127
|
+
await fs.mkdir(path.dirname(full), { recursive: true });
|
|
128
|
+
await fs.appendFile(full, args.content, "utf-8");
|
|
129
|
+
|
|
130
|
+
console.log(`${c.green}${c.bold} ✓ Appended to ${args.path}${c.reset}\n`);
|
|
131
|
+
return `Appended ${bytes} bytes to ${args.path}`;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async function renameFileTool(cwd: string, args: { old_path: string; new_path: string }): Promise<string> {
|
|
135
|
+
const oldFull = resolveInCwd(cwd, args.old_path);
|
|
136
|
+
const newFull = resolveInCwd(cwd, args.new_path);
|
|
137
|
+
|
|
138
|
+
header("rename_file", `${args.old_path} → ${args.new_path}`);
|
|
139
|
+
|
|
140
|
+
const approved = await confirm(`Rename "${args.old_path}" to "${args.new_path}"?`);
|
|
141
|
+
if (!approved) return "User declined to rename.";
|
|
142
|
+
|
|
143
|
+
await fs.mkdir(path.dirname(newFull), { recursive: true });
|
|
144
|
+
await fs.rename(oldFull, newFull);
|
|
145
|
+
|
|
146
|
+
console.log(`${c.green}${c.bold} ✓ Renamed${c.reset}\n`);
|
|
147
|
+
return `Renamed ${args.old_path} to ${args.new_path}`;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async function deleteFileTool(cwd: string, args: { path: string }): Promise<string> {
|
|
151
|
+
const full = resolveInCwd(cwd, args.path);
|
|
152
|
+
|
|
153
|
+
header("delete_file", args.path);
|
|
154
|
+
|
|
155
|
+
const approved = await confirm(`Permanently delete "${args.path}"?`);
|
|
156
|
+
if (!approved) return "User declined to delete.";
|
|
157
|
+
|
|
158
|
+
await fs.unlink(full);
|
|
159
|
+
|
|
160
|
+
console.log(`${c.green}${c.bold} ✓ Deleted ${args.path}${c.reset}\n`);
|
|
161
|
+
return `Deleted ${args.path}`;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
async function createDirectoryTool(cwd: string, args: { path: string }): Promise<string> {
|
|
165
|
+
const full = resolveInCwd(cwd, args.path);
|
|
166
|
+
|
|
167
|
+
header("create_directory", args.path);
|
|
168
|
+
|
|
169
|
+
await fs.mkdir(full, { recursive: true });
|
|
170
|
+
|
|
171
|
+
console.log(`${c.green}${c.bold} ✓ Created directory ${args.path}${c.reset}\n`);
|
|
172
|
+
return `Created directory ${args.path}`;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// ── Shell tools ───────────────────────────────────────────────────────────────
|
|
176
|
+
|
|
60
177
|
async function runCommandTool(cwd: string, args: { command: string }): Promise<string> {
|
|
61
178
|
console.log(`\n${c.bold}${c.yellow} ◆ run_command${c.reset} ${c.dim}${args.command}${c.reset}`);
|
|
62
179
|
const approved = await confirm(`Run: ${c.bold}${args.command}${c.reset}?`);
|
|
63
180
|
if (!approved) return "User declined to run this command.";
|
|
64
181
|
|
|
65
182
|
return new Promise((resolve) => {
|
|
66
|
-
const child = exec(args.command, { cwd, timeout:
|
|
183
|
+
const child = exec(args.command, { cwd, timeout: 120_000, maxBuffer: 4 * 1024 * 1024 });
|
|
67
184
|
|
|
68
185
|
let stdout = "";
|
|
69
186
|
let stderr = "";
|
|
@@ -92,7 +209,13 @@ async function runCommandTool(cwd: string, args: { command: string }): Promise<s
|
|
|
92
209
|
|
|
93
210
|
async function gitTool(cwd: string, args: { args: string[] }): Promise<string> {
|
|
94
211
|
const cmd = `git ${args.args.join(" ")}`;
|
|
212
|
+
const isMutating = !["status", "diff", "log", "show", "branch"].includes(args.args[0]);
|
|
213
|
+
|
|
95
214
|
header("git", cmd);
|
|
215
|
+
if (isMutating) {
|
|
216
|
+
const approved = await confirm(`Run: ${c.bold}${cmd}${c.reset}?`);
|
|
217
|
+
if (!approved) return "User declined to run this git command.";
|
|
218
|
+
}
|
|
96
219
|
|
|
97
220
|
return new Promise((resolve) => {
|
|
98
221
|
exec(cmd, { cwd, timeout: 30_000, maxBuffer: 1024 * 1024 }, (err, stdout, stderr) => {
|
|
@@ -138,7 +261,7 @@ async function searchFilesTool(
|
|
|
138
261
|
const grepArgs: string[] = [
|
|
139
262
|
"-rn",
|
|
140
263
|
"--color=never",
|
|
141
|
-
"-m", "
|
|
264
|
+
"-m", "10",
|
|
142
265
|
];
|
|
143
266
|
if (args.glob) {
|
|
144
267
|
grepArgs.push(`--include=${args.glob}`);
|
|
@@ -162,6 +285,91 @@ async function searchFilesTool(
|
|
|
162
285
|
});
|
|
163
286
|
}
|
|
164
287
|
|
|
288
|
+
// ── Web tools ─────────────────────────────────────────────────────────────────
|
|
289
|
+
|
|
290
|
+
async function webSearchTool(_cwd: string, args: { query: string }): Promise<string> {
|
|
291
|
+
header("web_search", args.query);
|
|
292
|
+
|
|
293
|
+
try {
|
|
294
|
+
// Use DuckDuckGo instant answer API
|
|
295
|
+
const url = `https://api.duckduckgo.com/?q=${encodeURIComponent(args.query)}&format=json&no_html=1&skip_disambig=1`;
|
|
296
|
+
const res = await fetch(url, {
|
|
297
|
+
headers: { "User-Agent": "Kundex/2.0.6" },
|
|
298
|
+
signal: AbortSignal.timeout(10_000),
|
|
299
|
+
});
|
|
300
|
+
const data = await res.json() as any;
|
|
301
|
+
|
|
302
|
+
const results: string[] = [];
|
|
303
|
+
|
|
304
|
+
if (data.AbstractText) {
|
|
305
|
+
results.push(`**${data.Heading || args.query}**\n${data.AbstractText}`);
|
|
306
|
+
if (data.AbstractURL) results.push(`Source: ${data.AbstractURL}`);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
if (data.Answer) {
|
|
310
|
+
results.push(`Answer: ${data.Answer}`);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
if (data.RelatedTopics?.length) {
|
|
314
|
+
const topics = data.RelatedTopics
|
|
315
|
+
.filter((t: any) => t.Text)
|
|
316
|
+
.slice(0, 5)
|
|
317
|
+
.map((t: any) => `- ${t.Text}${t.FirstURL ? ` (${t.FirstURL})` : ""}`);
|
|
318
|
+
if (topics.length) {
|
|
319
|
+
results.push("Related:\n" + topics.join("\n"));
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
if (results.length === 0) {
|
|
324
|
+
// Fallback: return a helpful message
|
|
325
|
+
return `No instant answer found for "${args.query}". Try using fetch_url to read a specific documentation page, or rephrase your search.`;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
return results.join("\n\n");
|
|
329
|
+
} catch (err) {
|
|
330
|
+
return `Web search failed: ${(err as Error).message}`;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
async function fetchUrlTool(_cwd: string, args: { url: string }): Promise<string> {
|
|
335
|
+
header("fetch_url", args.url);
|
|
336
|
+
|
|
337
|
+
try {
|
|
338
|
+
const res = await fetch(args.url, {
|
|
339
|
+
headers: {
|
|
340
|
+
"User-Agent": "Kundex/2.0.6",
|
|
341
|
+
"Accept": "text/html,application/xhtml+xml,text/plain",
|
|
342
|
+
},
|
|
343
|
+
signal: AbortSignal.timeout(15_000),
|
|
344
|
+
redirect: "follow",
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
if (!res.ok) {
|
|
348
|
+
return `HTTP ${res.status} ${res.statusText}`;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
const contentType = res.headers.get("content-type") ?? "";
|
|
352
|
+
const text = await res.text();
|
|
353
|
+
|
|
354
|
+
if (contentType.includes("text/html")) {
|
|
355
|
+
// Strip HTML tags for readability
|
|
356
|
+
const stripped = text
|
|
357
|
+
.replace(/<script[\s\S]*?<\/script>/gi, "")
|
|
358
|
+
.replace(/<style[\s\S]*?<\/style>/gi, "")
|
|
359
|
+
.replace(/<[^>]+>/g, " ")
|
|
360
|
+
.replace(/\s+/g, " ")
|
|
361
|
+
.trim();
|
|
362
|
+
return stripped.slice(0, 8000);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
return text.slice(0, 8000);
|
|
366
|
+
} catch (err) {
|
|
367
|
+
return `Fetch failed: ${(err as Error).message}`;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// ── Main dispatcher ───────────────────────────────────────────────────────────
|
|
372
|
+
|
|
165
373
|
export async function executeToolCall(cwd: string, call: ToolCall): Promise<ToolResult> {
|
|
166
374
|
let args: any;
|
|
167
375
|
try {
|
|
@@ -174,6 +382,16 @@ export async function executeToolCall(cwd: string, call: ToolCall): Promise<Tool
|
|
|
174
382
|
switch (call.name) {
|
|
175
383
|
case "read_file":
|
|
176
384
|
return { result: await readFileTool(cwd, args), isError: false };
|
|
385
|
+
case "write_file":
|
|
386
|
+
return { result: await writeFileTool(cwd, args), isError: false };
|
|
387
|
+
case "append_to_file":
|
|
388
|
+
return { result: await appendToFileTool(cwd, args), isError: false };
|
|
389
|
+
case "rename_file":
|
|
390
|
+
return { result: await renameFileTool(cwd, args), isError: false };
|
|
391
|
+
case "delete_file":
|
|
392
|
+
return { result: await deleteFileTool(cwd, args), isError: false };
|
|
393
|
+
case "create_directory":
|
|
394
|
+
return { result: await createDirectoryTool(cwd, args), isError: false };
|
|
177
395
|
case "run_command":
|
|
178
396
|
return { result: await runCommandTool(cwd, args), isError: false };
|
|
179
397
|
case "git":
|
|
@@ -182,6 +400,10 @@ export async function executeToolCall(cwd: string, call: ToolCall): Promise<Tool
|
|
|
182
400
|
return { result: await listDirectoryTool(cwd, args), isError: false };
|
|
183
401
|
case "search_files":
|
|
184
402
|
return { result: await searchFilesTool(cwd, args), isError: false };
|
|
403
|
+
case "web_search":
|
|
404
|
+
return { result: await webSearchTool(cwd, args), isError: false };
|
|
405
|
+
case "fetch_url":
|
|
406
|
+
return { result: await fetchUrlTool(cwd, args), isError: false };
|
|
185
407
|
default:
|
|
186
408
|
return { result: `Unknown tool: ${call.name}`, isError: true };
|
|
187
409
|
}
|