@ayoxx/kundex 2.0.4 → 2.0.6

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.4",
3
+ "version": "2.0.6",
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.4";
6
+ const VERSION = "2.0.6";
7
7
 
8
8
  // ─── Help text ────────────────────────────────────────────────────────────────
9
9
 
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,8 +362,11 @@ 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();
package/src/sdk.ts CHANGED
@@ -133,7 +133,7 @@ export class Kundex {
133
133
 
134
134
  models = {
135
135
  list: async (): Promise<ModelInfo[]> => {
136
- const res = await fetch(`${this.baseUrl}/v1/models`, {
136
+ const res = await fetch(`${this.baseUrl}/api/v1/models`, {
137
137
  headers: { authorization: `Bearer ${this.apiKey}` },
138
138
  });
139
139
  if (!res.ok) throw new Error(`Kundex API error (${res.status})`);
@@ -155,7 +155,7 @@ export class Kundex {
155
155
  message: KundexChatMessage;
156
156
  finishReason: string;
157
157
  usage: ChatUsage;
158
- }>("/v1/chat/completions", { ...params, stream: false }),
158
+ }>("/api/v1/chat/completions", { ...params, stream: false }),
159
159
 
160
160
  /** Streaming chat completion; invokes onDelta as tokens arrive. */
161
161
  stream: async (
@@ -168,7 +168,7 @@ export class Kundex {
168
168
  onDelta: (text: string) => void,
169
169
  signal?: AbortSignal,
170
170
  ): Promise<{ finishReason: string; usage: ChatUsage | null }> => {
171
- const res = await fetch(`${this.baseUrl}/v1/chat/completions`, {
171
+ const res = await fetch(`${this.baseUrl}/api/v1/chat/completions`, {
172
172
  method: "POST",
173
173
  headers: {
174
174
  "content-type": "application/json",
@@ -229,19 +229,19 @@ export class Kundex {
229
229
  description?: string;
230
230
  language?: string;
231
231
  }) =>
232
- this.request<AgentSession>("/v1/agent/session", params),
232
+ this.request<AgentSession>("/api/v1/agent/session", params),
233
233
 
234
234
  listSessions: () =>
235
- this.requestGet<AgentSession[]>("/v1/agent/sessions"),
235
+ this.requestGet<AgentSession[]>("/api/v1/agent/sessions"),
236
236
 
237
237
  getSession: (id: number) =>
238
- this.requestGet<AgentSession>(`/v1/agent/sessions/${id}`),
238
+ this.requestGet<AgentSession>(`/api/v1/agent/sessions/${id}`),
239
239
 
240
240
  deleteSession: (id: number) =>
241
- this.requestDelete(`/v1/agent/sessions/${id}`),
241
+ this.requestDelete(`/api/v1/agent/sessions/${id}`),
242
242
 
243
243
  getMessages: (id: number) =>
244
- this.requestGet<{ sessionId: number; messages: unknown[] }>(`/v1/agent/sessions/${id}/messages`),
244
+ this.requestGet<{ sessionId: number; messages: unknown[] }>(`/api/v1/agent/sessions/${id}/messages`),
245
245
 
246
246
  /**
247
247
  * Send a message to the agent and stream the response.
@@ -266,7 +266,7 @@ export class Kundex {
266
266
  onDelta: (text: string) => void,
267
267
  signal?: AbortSignal,
268
268
  ): Promise<AgentTurn> => {
269
- const endpoint = "/v1/agent/message";
269
+ const endpoint = "/api/v1/agent/message";
270
270
  const res = await fetch(`${this.baseUrl}${endpoint}`, {
271
271
  method: "POST",
272
272
  headers: {
@@ -365,7 +365,7 @@ export class Kundex {
365
365
  onDelta: (text: string) => void,
366
366
  signal?: AbortSignal,
367
367
  ): Promise<AgentTurn> => {
368
- const res = await fetch(`${this.baseUrl}/v1/agent/tool-result`, {
368
+ const res = await fetch(`${this.baseUrl}/api/v1/agent/tool-result`, {
369
369
  method: "POST",
370
370
  headers: {
371
371
  "content-type": "application/json",
@@ -447,17 +447,17 @@ export class Kundex {
447
447
 
448
448
  files = {
449
449
  list: (wsId: number) =>
450
- this.requestGet<{ files: WorkspaceFile[] }>(`/v1/workspace/${wsId}/files`),
450
+ this.requestGet<{ files: WorkspaceFile[] }>(`/api/v1/workspace/${wsId}/files`),
451
451
 
452
452
  get: (wsId: number, path: string) =>
453
453
  this.requestGet<WorkspaceFile>(
454
- `/v1/workspace/${wsId}/files/content?path=${encodeURIComponent(path)}`,
454
+ `/api/v1/workspace/${wsId}/files/content?path=${encodeURIComponent(path)}`,
455
455
  ),
456
456
 
457
457
  upsert: (wsId: number, params: { path: string; name: string; content: string; mimeType?: string }) =>
458
- this.request<{ file: WorkspaceFile }>(`/v1/workspace/${wsId}/files`, params),
458
+ this.request<{ file: WorkspaceFile }>(`/api/v1/workspace/${wsId}/files`, params),
459
459
 
460
460
  delete: (wsId: number, path: string) =>
461
- this.requestDelete(`/v1/workspace/${wsId}/files?path=${encodeURIComponent(path)}`),
461
+ this.requestDelete(`/api/v1/workspace/${wsId}/files?path=${encodeURIComponent(path)}`),
462
462
  };
463
463
  }
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
  }