@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ayoxx/kundex",
3
- "version": "2.0.5",
3
+ "version": "2.0.7",
4
4
  "description": "Kundex — a terminal-based AI coding agent CLI and SDK, powered by Mistral and Heavstal AI.",
5
5
  "type": "module",
6
6
  "bin": {
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.5";
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
- console.error(`${c.red} Fatal: ${e instanceof Error ? e.message : e}${c.reset}`);
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
- The agent can read files, search code, run commands, and use git.
166
- You will be asked to confirm any shell commands before they run.
167
- • Responses stream in real-time — text appears as the model generates it.
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: 60_000, maxBuffer: 4 * 1024 * 1024 });
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", "5",
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
  }