@ayoxx/kundex 1.0.0 → 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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@ayoxx/kundex",
3
- "version": "1.0.0",
4
- "description": "Kundex — a terminal-based AI coding agent CLI and SDK, powered by Groq.",
3
+ "version": "2.0.0",
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": {
7
7
  "kundex": "./bin/kundex.mjs"
package/src/index.ts CHANGED
@@ -2,10 +2,8 @@ import { loadConfig, saveConfig, resolveBaseUrl } from "./config";
2
2
  import { runLogin } from "./login";
3
3
  import { runRepl } from "./repl";
4
4
  import { Kundex } from "./sdk";
5
- import fs from "node:fs/promises";
6
- import path from "node:path";
7
5
 
8
- const VERSION = "0.2.0";
6
+ const VERSION = "1.0.0";
9
7
 
10
8
  // ─── Help text ────────────────────────────────────────────────────────────────
11
9
 
@@ -23,18 +21,6 @@ const HELP = `
23
21
  (no command) Start the interactive AI coding agent REPL
24
22
  run <message> Run a single agent turn (non-interactive)
25
23
 
26
- \x1b[1mWorkspaces:\x1b[0m
27
- workspace list List all your agent workspaces
28
- workspace new [cwd] Create a new workspace (default: current dir)
29
- workspace delete <id> Delete a workspace
30
-
31
- \x1b[1mFiles:\x1b[0m
32
- files list <wsId> List files in a workspace
33
- files get <wsId> <path> Print a file's content to stdout
34
- files push <wsId> <localPath> Upload a local file to a workspace
35
- files push <wsId> <localPath> <remotePath>
36
- files pull <wsId> <remotePath> Download a workspace file to stdout (or --out)
37
-
38
24
  \x1b[1mModels:\x1b[0m
39
25
  models List available models
40
26
 
@@ -52,14 +38,7 @@ const HELP = `
52
38
  kundex # start REPL in current directory
53
39
  kundex run "Fix all TypeScript errors"
54
40
  kundex models
55
- kundex workspace list
56
- kundex workspace new /path/to/project
57
- kundex workspace delete 42
58
- kundex files list 17
59
- kundex files get 17 src/index.ts
60
- kundex files push 17 ./README.md
61
- kundex files pull 17 src/index.ts --out ./downloaded.ts
62
- kundex config set model openai/gpt-oss-120b
41
+ kundex config set model codestral-latest
63
42
  kundex config set baseUrl https://api.kundex.com.ng
64
43
  `.trim();
65
44
 
@@ -84,8 +63,6 @@ function header(msg: string) { console.log(`\n${c.bold}${c.cyan} ${msg}${c.rese
84
63
 
85
64
  const PROVIDER_DISPLAY: Record<string, string> = {
86
65
  heavstal: "Heavstal",
87
- gemini: "Gemini",
88
- groq: "Groq",
89
66
  mistral: "Mistral",
90
67
  };
91
68
 
@@ -101,7 +78,6 @@ async function cmdModels(config: ReturnType<typeof loadConfig>, baseUrl: string)
101
78
  console.log(`\n ${c.dim}${"Name".padEnd(nameWidth)} ${"ID".padEnd(idWidth)} ${"Provider".padEnd(providerWidth)} Context${c.reset}`);
102
79
  console.log(` ${c.dim}${"─".repeat(nameWidth + idWidth + providerWidth + 18)}${c.reset}`);
103
80
 
104
- // Group by provider for cleaner output
105
81
  const grouped: Record<string, typeof models> = {};
106
82
  const order: string[] = [];
107
83
  for (const m of models) {
@@ -133,209 +109,6 @@ async function cmdModels(config: ReturnType<typeof loadConfig>, baseUrl: string)
133
109
  }
134
110
  }
135
111
 
136
- async function cmdWorkspaceList(config: ReturnType<typeof loadConfig>, baseUrl: string) {
137
- const kundex = new Kundex({ apiKey: config.apiKey!, baseUrl });
138
- header("Workspaces");
139
- try {
140
- const sessions = await kundex.agent.listSessions();
141
-
142
- if (!sessions.length) {
143
- dim("No workspaces yet. Create one with: kundex workspace new <directory>");
144
- return;
145
- }
146
-
147
- console.log(`\n ${c.dim}${"ID".padEnd(6)} ${"Name".padEnd(28)} ${"Directory".padEnd(34)} Model${c.reset}`);
148
- console.log(` ${c.dim}${"─".repeat(90)}${c.reset}`);
149
- for (const s of sessions) {
150
- const cwd = (s.cwd ?? "").length > 32 ? `…${s.cwd.slice(-31)}` : (s.cwd ?? "");
151
- const name = (s.name ?? `Workspace #${s.id}`).slice(0, 26);
152
- console.log(` ${c.bold}${String(s.id).padEnd(6)}${c.reset} ${name.padEnd(28)} ${c.dim}${cwd.padEnd(34)} ${s.model}${c.reset}`);
153
- }
154
- console.log();
155
- } catch (e) {
156
- errMsg(`Failed to list workspaces: ${(e as Error).message}`);
157
- process.exitCode = 1;
158
- }
159
- }
160
-
161
- async function cmdWorkspaceNew(
162
- config: ReturnType<typeof loadConfig>,
163
- baseUrl: string,
164
- args: string[],
165
- ) {
166
- const cwd = args[0] ?? process.cwd();
167
-
168
- // Parse optional flags: --model <model>, --name <name>
169
- let model = config.defaultModel ?? "openai/gpt-oss-120b";
170
- let name: string | undefined;
171
- for (let i = 0; i < args.length; i++) {
172
- if (args[i] === "--model" && args[i + 1]) { model = args[++i]; }
173
- else if (args[i] === "--name" && args[i + 1]) { name = args[++i]; }
174
- }
175
-
176
- const kundex = new Kundex({ apiKey: config.apiKey!, baseUrl });
177
-
178
- try {
179
- const session = await kundex.agent.createSession({ cwd, model, name });
180
- ok(`Created workspace #${session.id}`);
181
- dim(`Name: ${session.name}`);
182
- dim(`Directory: ${cwd}`);
183
- dim(`Model: ${session.model}`);
184
- console.log();
185
- } catch (e) {
186
- errMsg(`Failed to create workspace: ${(e as Error).message}`);
187
- process.exitCode = 1;
188
- }
189
- }
190
-
191
- async function cmdWorkspaceDelete(
192
- config: ReturnType<typeof loadConfig>,
193
- baseUrl: string,
194
- idStr: string,
195
- ) {
196
- const id = Number(idStr);
197
- if (!Number.isFinite(id) || id <= 0) {
198
- errMsg("Usage: kundex workspace delete <id>");
199
- process.exitCode = 1;
200
- return;
201
- }
202
-
203
- const kundex = new Kundex({ apiKey: config.apiKey!, baseUrl });
204
-
205
- try {
206
- await kundex.agent.deleteSession(id);
207
- ok(`Deleted workspace #${id}`);
208
- } catch (e) {
209
- errMsg(`Failed to delete workspace: ${(e as Error).message}`);
210
- process.exitCode = 1;
211
- }
212
- }
213
-
214
- // ── Files ─────────────────────────────────────────────────────────────────────
215
-
216
- async function cmdFilesList(
217
- config: ReturnType<typeof loadConfig>,
218
- baseUrl: string,
219
- wsIdStr: string,
220
- ) {
221
- const wsId = Number(wsIdStr);
222
- if (!Number.isFinite(wsId) || wsId <= 0) {
223
- errMsg("Usage: kundex files list <wsId>");
224
- process.exitCode = 1;
225
- return;
226
- }
227
-
228
- const kundex = new Kundex({ apiKey: config.apiKey!, baseUrl });
229
- header(`Files in workspace #${wsId}`);
230
-
231
- try {
232
- const { files } = await kundex.files.list(wsId);
233
- if (!files.length) {
234
- dim("No files yet.");
235
- return;
236
- }
237
-
238
- console.log(`\n ${c.dim}${"Path".padEnd(44)} ${"Size".padEnd(10)} MIME${c.reset}`);
239
- console.log(` ${c.dim}${"─".repeat(80)}${c.reset}`);
240
- for (const f of files) {
241
- const size = f.size < 1024 ? `${f.size} B`
242
- : f.size < 1024 * 1024 ? `${(f.size / 1024).toFixed(1)} KB`
243
- : `${(f.size / 1024 / 1024).toFixed(1)} MB`;
244
- const displayPath = f.path.length > 42 ? `…${f.path.slice(-41)}` : f.path;
245
- console.log(` ${displayPath.padEnd(44)} ${size.padEnd(10)} ${c.dim}${f.mimeType}${c.reset}`);
246
- }
247
- console.log();
248
- } catch (e) {
249
- errMsg(`Failed to list files: ${(e as Error).message}`);
250
- process.exitCode = 1;
251
- }
252
- }
253
-
254
- async function cmdFilesGet(
255
- config: ReturnType<typeof loadConfig>,
256
- baseUrl: string,
257
- wsIdStr: string,
258
- remotePath: string,
259
- ) {
260
- const wsId = Number(wsIdStr);
261
- if (!Number.isFinite(wsId) || wsId <= 0 || !remotePath) {
262
- errMsg("Usage: kundex files get <wsId> <path>");
263
- process.exitCode = 1;
264
- return;
265
- }
266
-
267
- const kundex = new Kundex({ apiKey: config.apiKey!, baseUrl });
268
-
269
- try {
270
- const file = await kundex.files.get(wsId, remotePath);
271
- process.stdout.write(file.content ?? "");
272
- } catch (e) {
273
- errMsg(`Failed to get file: ${(e as Error).message}`);
274
- process.exitCode = 1;
275
- }
276
- }
277
-
278
- async function cmdFilesPush(
279
- config: ReturnType<typeof loadConfig>,
280
- baseUrl: string,
281
- wsIdStr: string,
282
- localPath: string,
283
- remotePath?: string,
284
- ) {
285
- const wsId = Number(wsIdStr);
286
- if (!Number.isFinite(wsId) || wsId <= 0 || !localPath) {
287
- errMsg("Usage: kundex files push <wsId> <localPath> [remotePath]");
288
- process.exitCode = 1;
289
- return;
290
- }
291
-
292
- const kundex = new Kundex({ apiKey: config.apiKey!, baseUrl });
293
- const destPath = remotePath ?? path.basename(localPath);
294
-
295
- try {
296
- const content = await fs.readFile(localPath, "utf-8");
297
- const name = path.basename(destPath);
298
- await kundex.files.upsert(wsId, { path: destPath, name, content });
299
- ok(`Pushed ${localPath} → workspace #${wsId}:${destPath}`);
300
- } catch (e) {
301
- errMsg(`Failed to push file: ${(e as Error).message}`);
302
- process.exitCode = 1;
303
- }
304
- }
305
-
306
- async function cmdFilesPull(
307
- config: ReturnType<typeof loadConfig>,
308
- baseUrl: string,
309
- wsIdStr: string,
310
- remotePath: string,
311
- outPath?: string,
312
- ) {
313
- const wsId = Number(wsIdStr);
314
- if (!Number.isFinite(wsId) || wsId <= 0 || !remotePath) {
315
- errMsg("Usage: kundex files pull <wsId> <remotePath> [--out <localPath>]");
316
- process.exitCode = 1;
317
- return;
318
- }
319
-
320
- const kundex = new Kundex({ apiKey: config.apiKey!, baseUrl });
321
-
322
- try {
323
- const file = await kundex.files.get(wsId, remotePath);
324
- const content = file.content ?? "";
325
-
326
- if (outPath) {
327
- await fs.mkdir(path.dirname(outPath), { recursive: true });
328
- await fs.writeFile(outPath, content, "utf-8");
329
- ok(`Saved ${remotePath} → ${outPath} (${content.length} bytes)`);
330
- } else {
331
- process.stdout.write(content);
332
- }
333
- } catch (e) {
334
- errMsg(`Failed to pull file: ${(e as Error).message}`);
335
- process.exitCode = 1;
336
- }
337
- }
338
-
339
112
  // ── Run (single-turn non-interactive) ─────────────────────────────────────────
340
113
 
341
114
  async function cmdRun(
@@ -350,26 +123,18 @@ async function cmdRun(
350
123
  return;
351
124
  }
352
125
 
353
- // Parse optional flags: --model, --workspace, --cwd
354
- let model = config.defaultModel ?? "openai/gpt-oss-120b";
355
- let wsId: number | null = null;
126
+ let model = config.defaultModel ?? "codestral-latest";
356
127
  let cwd = process.cwd();
357
128
  for (let i = 0; i < args.length; i++) {
358
129
  if (args[i] === "--model" && args[i + 1]) model = args[++i];
359
- else if ((args[i] === "--workspace" || args[i] === "-w") && args[i + 1]) wsId = Number(args[++i]);
360
130
  else if (args[i] === "--cwd" && args[i + 1]) cwd = args[++i];
361
131
  }
362
132
 
363
133
  const kundex = new Kundex({ apiKey: config.apiKey!, baseUrl });
364
- let sessionId: number;
365
134
 
366
135
  try {
367
- if (wsId !== null && Number.isFinite(wsId)) {
368
- sessionId = wsId;
369
- } else {
370
- const session = await kundex.agent.createSession({ cwd, model });
371
- sessionId = session.id;
372
- }
136
+ const session = await kundex.agent.createSession({ cwd, model });
137
+ const sessionId = session.id;
373
138
 
374
139
  process.stdout.write(`\n`);
375
140
  const { executeToolCall } = await import("./tools");
@@ -379,7 +144,6 @@ async function cmdRun(
379
144
  (delta) => process.stdout.write(delta),
380
145
  );
381
146
 
382
- // Handle tool calls in a loop until the turn is done
383
147
  while (!turn.done && turn.toolCalls && turn.toolCalls.length > 0) {
384
148
  for (const call of turn.toolCalls) {
385
149
  const { result, isError } = await executeToolCall(cwd, call);
@@ -435,6 +199,29 @@ function cmdLogout() {
435
199
  ok("Logged out. Run `kundex login` to authenticate again.");
436
200
  }
437
201
 
202
+ // ─── API key prompt ──────────────────────────────────────────────────────────
203
+
204
+ async function promptForApiKey(): Promise<string | null> {
205
+ const readline = await import("node:readline/promises");
206
+ const rl = readline.default.createInterface({ input: process.stdin, output: process.stdout });
207
+ try {
208
+ console.log(`\n${c.bold}${c.cyan} Welcome to Kundex!${c.reset}`);
209
+ console.log(`${c.dim} You need an API key to use the agent.${c.reset}`);
210
+ console.log(`${c.dim} Get one at: https://kundex.com.ng/dashboard/api-keys${c.reset}\n`);
211
+ const answer = await rl.question(`${c.bold} Enter your API key: ${c.reset}`);
212
+ const apiKey = answer.trim();
213
+ if (!apiKey) {
214
+ console.log(`${c.dim} No key entered. Run \`kundex login\` later to set one.${c.reset}\n`);
215
+ return null;
216
+ }
217
+ saveConfig({ apiKey });
218
+ ok("API key saved.");
219
+ return apiKey;
220
+ } finally {
221
+ rl.close();
222
+ }
223
+ }
224
+
438
225
  // ─── Main ─────────────────────────────────────────────────────────────────────
439
226
 
440
227
  async function main(): Promise<void> {
@@ -453,16 +240,25 @@ async function main(): Promise<void> {
453
240
  return;
454
241
  }
455
242
 
456
- // No command — drop into REPL if logged in, else show help
243
+ // No command — start REPL, prompting for API key if needed
457
244
  if (!command) {
458
- const config = loadConfig();
245
+ let config = loadConfig();
459
246
  const baseUrl = resolveBaseUrl(config);
247
+
248
+ if (!config.apiKey) {
249
+ const key = await promptForApiKey();
250
+ if (!key) {
251
+ console.log(HELP);
252
+ return;
253
+ }
254
+ config = loadConfig();
255
+ }
256
+
460
257
  if (config.apiKey && baseUrl) {
461
258
  await runRepl(config, config.apiKey, baseUrl);
462
259
  return;
463
260
  }
464
261
  console.log(HELP);
465
- console.log(`\n${c.yellow} Run \`kundex login\` to get started.${c.reset}\n`);
466
262
  return;
467
263
  }
468
264
 
@@ -477,25 +273,35 @@ async function main(): Promise<void> {
477
273
  }
478
274
 
479
275
  // Load config — required for all remaining commands
480
- const config = loadConfig();
276
+ let config = loadConfig();
481
277
  const baseUrl = resolveBaseUrl(config);
482
278
 
483
279
  if (!config.apiKey || !baseUrl) {
484
- errMsg('Not logged in. Run "kundex login" first.');
485
- process.exitCode = 1;
486
- return;
280
+ if (!config.apiKey) {
281
+ const key = await promptForApiKey();
282
+ if (!key) {
283
+ process.exitCode = 1;
284
+ return;
285
+ }
286
+ config = loadConfig();
287
+ }
288
+ if (!config.apiKey || !resolveBaseUrl(config)) {
289
+ errMsg('Not logged in. Run "kundex login" first.');
290
+ process.exitCode = 1;
291
+ return;
292
+ }
487
293
  }
488
294
 
489
295
  // Models
490
296
  if (command === "models") {
491
- await cmdModels(config, baseUrl);
297
+ await cmdModels(config, resolveBaseUrl(config)!);
492
298
  return;
493
299
  }
494
300
 
495
301
  // Config
496
302
  if (command === "config") {
497
303
  if (sub === "show" || !sub) {
498
- cmdConfigShow(config, baseUrl);
304
+ cmdConfigShow(config, resolveBaseUrl(config)!);
499
305
  } else if (sub === "set") {
500
306
  const [key, value] = rest;
501
307
  if (!key || !value) {
@@ -511,56 +317,15 @@ async function main(): Promise<void> {
511
317
  return;
512
318
  }
513
319
 
514
- // Workspace
515
- if (command === "workspace") {
516
- if (sub === "list" || !sub) {
517
- await cmdWorkspaceList(config, baseUrl);
518
- } else if (sub === "new") {
519
- await cmdWorkspaceNew(config, baseUrl, rest);
520
- } else if (sub === "delete") {
521
- await cmdWorkspaceDelete(config, baseUrl, rest[0]);
522
- } else {
523
- errMsg(`Unknown workspace subcommand "${sub}". Use: list, new, delete`);
524
- process.exitCode = 1;
525
- }
526
- return;
527
- }
528
-
529
- // Files
530
- if (command === "files") {
531
- if (sub === "list") {
532
- await cmdFilesList(config, baseUrl, rest[0]);
533
- } else if (sub === "get") {
534
- await cmdFilesGet(config, baseUrl, rest[0], rest[1]);
535
- } else if (sub === "push") {
536
- await cmdFilesPush(config, baseUrl, rest[0], rest[1], rest[2]);
537
- } else if (sub === "pull") {
538
- // Support --out flag
539
- let outPath: string | undefined;
540
- const remaining: string[] = [];
541
- for (let i = 0; i < rest.length; i++) {
542
- if (rest[i] === "--out" && rest[i + 1]) { outPath = rest[++i]; }
543
- else remaining.push(rest[i]);
544
- }
545
- await cmdFilesPull(config, baseUrl, remaining[0], remaining[1], outPath);
546
- } else {
547
- errMsg(`Unknown files subcommand "${sub}". Use: list, get, push, pull`);
548
- process.exitCode = 1;
549
- }
550
- return;
551
- }
552
-
553
320
  // Run (single-turn agent)
554
321
  if (command === "run") {
555
- // "kundex run <message>" — join remaining args as the message
556
322
  const allArgs = [sub, ...rest].filter(Boolean);
557
- // Find message (everything before the first -- flag or flag-like arg)
558
323
  const flagStart = allArgs.findIndex((a) => a.startsWith("--"));
559
324
  const messageParts = flagStart >= 0 ? allArgs.slice(0, flagStart) : allArgs;
560
325
  const flagArgs = flagStart >= 0 ? allArgs.slice(flagStart) : [];
561
326
  const message = messageParts.join(" ").trim();
562
327
 
563
- await cmdRun(config, baseUrl, message, flagArgs);
328
+ await cmdRun(config, resolveBaseUrl(config)!, message, flagArgs);
564
329
  return;
565
330
  }
566
331
 
package/src/repl.ts CHANGED
@@ -18,14 +18,13 @@ const c = {
18
18
  blue: "\x1b[34m",
19
19
  magenta: "\x1b[35m",
20
20
  white: "\x1b[37m",
21
- // Syntax-highlight colours (256-colour palette)
22
- kw: "\x1b[38;5;204m", // keywords – salmon/red
23
- str: "\x1b[38;5;150m", // strings – light green
24
- num: "\x1b[38;5;141m", // numbers – light purple
25
- comment: "\x1b[38;5;244m", // comments – grey
26
- fn: "\x1b[38;5;117m", // functions – light blue
27
- type_: "\x1b[38;5;215m", // types/decorators – orange
28
- punct: "\x1b[38;5;250m", // punctuation – light grey
21
+ kw: "\x1b[38;5;204m",
22
+ str: "\x1b[38;5;150m",
23
+ num: "\x1b[38;5;141m",
24
+ comment: "\x1b[38;5;244m",
25
+ fn: "\x1b[38;5;117m",
26
+ type_: "\x1b[38;5;215m",
27
+ punct: "\x1b[38;5;250m",
29
28
  };
30
29
 
31
30
  // ─── Per-language syntax highlighter (ANSI, no deps) ─────────────────────────
@@ -56,22 +55,15 @@ const KEYWORDS: Record<string, string[]> = {
56
55
  function buildRules(lang: string): LangRule[] {
57
56
  const kws = KEYWORDS[lang] || KEYWORDS["javascript"];
58
57
  return [
59
- // Comments
60
58
  { pattern: /(#.*$|\/\/.*$|\/\*[\s\S]*?\*\/|"""[\s\S]*?"""|'''[\s\S]*?''')/gm, color: c.comment },
61
- // Strings
62
59
  { pattern: /("(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|`(?:[^`\\]|\\.)*`)/g, color: c.str },
63
- // Numbers
64
60
  { pattern: /\b(\d+\.?\d*(?:e[+-]?\d+)?)\b/g, color: c.num },
65
- // Keywords (built from per-language list)
66
61
  { pattern: new RegExp(`\\b(${kws.join("|")})\\b`, "g"), color: c.kw },
67
- // Function calls
68
62
  { pattern: /\b([a-zA-Z_]\w*)\s*(?=\()/g, color: c.fn },
69
- // Type annotations (CamelCase identifiers)
70
63
  { pattern: /\b([A-Z][a-zA-Z0-9_]*)\b/g, color: c.type_ },
71
64
  ];
72
65
  }
73
66
 
74
- /** Apply regex-based ANSI syntax highlighting to a code string. */
75
67
  function syntaxHighlight(code: string, lang: string): string {
76
68
  const l = lang.toLowerCase();
77
69
  const normalised = ["tsx","ts"].includes(l) ? "typescript"
@@ -167,14 +159,11 @@ ${c.bold}Commands:${c.reset}
167
159
  /help Show this help
168
160
  /model Show the model for this session
169
161
  /clear Start a fresh agent session
170
- /workspace list List all your workspaces
171
- /workspace use <id> Switch to an existing workspace session
172
- /files List files in the current workspace
173
162
  /exit Quit kundex
174
163
 
175
164
  ${c.bold}Tips:${c.reset}
176
- • The agent can read, edit and search files, run commands, and use git.
177
- • You will be asked to confirm any writes or shell commands before they run.
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.
178
167
  • Responses stream in real-time — text appears as the model generates it.
179
168
  • Code blocks are syntax-highlighted in the terminal.
180
169
  • Press Ctrl+C to abort a running request.
@@ -199,10 +188,6 @@ function safeFileTree(cwd: string): string | undefined {
199
188
  }
200
189
  }
201
190
 
202
- /**
203
- * Spinner that writes to stdout with carriage-return to overwrite itself.
204
- * Call clear() to erase it before writing other output.
205
- */
206
191
  function createSpinner(label: string) {
207
192
  const frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
208
193
  let frame = 0;
@@ -213,15 +198,11 @@ function createSpinner(label: string) {
213
198
  return {
214
199
  clear() {
215
200
  clearInterval(timer);
216
- process.stdout.write("\r\x1b[2K"); // CR + erase line
201
+ process.stdout.write("\r\x1b[2K");
217
202
  },
218
203
  };
219
204
  }
220
205
 
221
- /**
222
- * Handle a completed AgentTurn: process any tool calls, stream tool-result turns.
223
- * All tool results are submitted using the streaming API so progress is visible.
224
- */
225
206
  async function handleTurn(
226
207
  kundex: Kundex,
227
208
  cwd: string,
@@ -238,7 +219,6 @@ async function handleTurn(
238
219
  process.stdout.write(`${c.red} ✗ ${call.name}: ${result}${c.reset}\n`);
239
220
  }
240
221
 
241
- // Show a spinner while waiting for the follow-up AI turn
242
222
  const sp = createSpinner("processing tool result…");
243
223
  let followUpText = "";
244
224
 
@@ -247,7 +227,6 @@ async function handleTurn(
247
227
  { sessionId, toolCallId: call.id, result, isError },
248
228
  (delta) => {
249
229
  if (!followUpText) {
250
- // First token arrived — clear the spinner
251
230
  sp.clear();
252
231
  process.stdout.write("\n");
253
232
  }
@@ -267,7 +246,6 @@ async function handleTurn(
267
246
  }
268
247
 
269
248
  if (current.reply && !turn.toolCalls?.length) {
270
- // Reply was already streamed inline; just print the closing rule
271
249
  process.stdout.write(`\n${c.dim}${"─".repeat(60)}${c.reset}\n\n`);
272
250
  } else if (current.reply) {
273
251
  printReply(current.reply);
@@ -296,15 +274,13 @@ ${c.bold}${c.cyan} ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═
296
274
  export async function runRepl(config: KundexConfig, apiKey: string, baseUrl: string): Promise<void> {
297
275
  const cwd = process.cwd();
298
276
  const kundex = new Kundex({ apiKey, baseUrl });
299
- const model = config.defaultModel ?? "openai/gpt-oss-120b";
277
+ const model = config.defaultModel ?? "codestral-latest";
300
278
 
301
279
  printBanner(cwd, model);
302
280
 
303
281
  let session = await kundex.agent.createSession({ cwd, model });
304
282
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
305
283
 
306
- // AbortController for the current in-progress request.
307
- // Ctrl+C signals the controller and sends a SIGINT to abort the fetch.
308
284
  let currentAbort: AbortController | null = null;
309
285
 
310
286
  const sigintHandler = () => {
@@ -313,7 +289,6 @@ export async function runRepl(config: KundexConfig, apiKey: string, baseUrl: str
313
289
  process.stdout.write(`\n${c.yellow} ⚡ Stopped.${c.reset}\n\n`);
314
290
  currentAbort = null;
315
291
  } else {
316
- // Second Ctrl+C with no request in flight → exit
317
292
  console.log(`\n${c.dim}Goodbye!${c.reset}\n`);
318
293
  rl.close();
319
294
  process.exit(0);
@@ -328,7 +303,6 @@ export async function runRepl(config: KundexConfig, apiKey: string, baseUrl: str
328
303
  const input = raw.trim();
329
304
  if (!input) continue;
330
305
 
331
- // ── REPL commands ───────────────────────────────────────────────────────
332
306
  if (input === "/exit" || input === "/quit") {
333
307
  console.log(`\n${c.dim}Goodbye!${c.reset}\n`);
334
308
  break;
@@ -344,69 +318,6 @@ export async function runRepl(config: KundexConfig, apiKey: string, baseUrl: str
344
318
  continue;
345
319
  }
346
320
 
347
- // /workspace list
348
- if (input === "/workspace list") {
349
- try {
350
- const sessions = await kundex.agent.listSessions();
351
- if (!sessions.length) {
352
- console.log(`${c.dim} No workspaces yet.${c.reset}`);
353
- } else {
354
- console.log(`\n ${c.dim}${"ID".padEnd(6)} ${"Name".padEnd(26)} CWD${c.reset}`);
355
- console.log(` ${c.dim}${"─".repeat(70)}${c.reset}`);
356
- for (const s of sessions) {
357
- const isActive = s.id === session.id ? ` ${c.cyan}← active${c.reset}` : "";
358
- const name = (s.name ?? `Workspace #${s.id}`).slice(0, 24);
359
- const cwd2 = (s.cwd ?? "").length > 30 ? `…${s.cwd.slice(-29)}` : s.cwd;
360
- console.log(` ${c.bold}${String(s.id).padEnd(6)}${c.reset} ${name.padEnd(26)} ${c.dim}${cwd2}${c.reset}${isActive}`);
361
- }
362
- console.log();
363
- }
364
- } catch (err) {
365
- console.error(`${c.red} error: ${(err as Error).message}${c.reset}`);
366
- }
367
- continue;
368
- }
369
-
370
- // /workspace use <id>
371
- if (input.startsWith("/workspace use ")) {
372
- const id = Number(input.slice("/workspace use ".length).trim());
373
- if (!Number.isFinite(id) || id <= 0) {
374
- console.error(`${c.red} Invalid workspace ID.${c.reset}`);
375
- continue;
376
- }
377
- try {
378
- const s = await kundex.agent.getSession(id) as AgentSession;
379
- session = s as any;
380
- console.log(`\n${c.green} ✓ Switched to workspace #${id} — ${s.name}${c.reset}\n`);
381
- } catch (err) {
382
- console.error(`${c.red} error: ${(err as Error).message}${c.reset}`);
383
- }
384
- continue;
385
- }
386
-
387
- // /files
388
- if (input === "/files") {
389
- try {
390
- const { files } = await kundex.files.list(session.id);
391
- if (!files.length) {
392
- console.log(`${c.dim} No files in this workspace yet.${c.reset}`);
393
- } else {
394
- console.log(`\n ${c.dim}${"Path".padEnd(40)} Size${c.reset}`);
395
- console.log(` ${c.dim}${"─".repeat(52)}${c.reset}`);
396
- for (const f of files) {
397
- const size = f.size < 1024 ? `${f.size} B`
398
- : f.size < 1024 * 1024 ? `${(f.size / 1024).toFixed(1)} KB`
399
- : `${(f.size / 1024 / 1024).toFixed(1)} MB`;
400
- console.log(` ${f.path.padEnd(40)} ${c.dim}${size}${c.reset}`);
401
- }
402
- console.log();
403
- }
404
- } catch (err) {
405
- console.error(`${c.red} error: ${(err as Error).message}${c.reset}`);
406
- }
407
- continue;
408
- }
409
-
410
321
  // ── Agent message ────────────────────────────────────────────────────────
411
322
  currentAbort = new AbortController();
412
323
  const { signal } = currentAbort;
@@ -421,7 +332,6 @@ export async function runRepl(config: KundexConfig, apiKey: string, baseUrl: str
421
332
  context: {
422
333
  gitStatus: safeGitStatus(cwd),
423
334
  fileTree: safeFileTree(cwd),
424
- openFiles: [],
425
335
  },
426
336
  },
427
337
  (delta) => {
@@ -437,12 +347,9 @@ export async function runRepl(config: KundexConfig, apiKey: string, baseUrl: str
437
347
 
438
348
  spinner.clear();
439
349
 
440
- // If the response contained only tool calls (no streamed text), let
441
- // handleTurn print the tool output and follow-up replies.
442
350
  if (turn.toolCalls?.length) {
443
351
  await handleTurn(kundex, cwd, session.id, turn, signal);
444
352
  } else if (streamedAnyText) {
445
- // Text was streamed inline; just print the closing separator.
446
353
  process.stdout.write(`\n${c.dim}${"─".repeat(60)}${c.reset}\n\n`);
447
354
  } else if (turn.reply) {
448
355
  printReply(turn.reply);
package/src/sdk.ts CHANGED
@@ -21,7 +21,7 @@ export interface ChatUsage {
21
21
  export interface ModelInfo {
22
22
  id: string;
23
23
  name: string;
24
- /** Which inference provider serves this model (e.g. "heavstal", "groq", "gemini", "mistral"). */
24
+ /** Which inference provider serves this model (e.g. "heavstal", "mistral"). */
25
25
  provider?: string;
26
26
  contextWindow: number;
27
27
  inputPricePerMTok: number;
package/src/tools.ts CHANGED
@@ -25,7 +25,6 @@ const c = {
25
25
  blue: "\x1b[34m",
26
26
  magenta: "\x1b[35m",
27
27
  white: "\x1b[37m",
28
- bgDark: "\x1b[48;5;235m",
29
28
  };
30
29
 
31
30
  function header(label: string, value: string) {
@@ -34,34 +33,6 @@ function header(label: string, value: string) {
34
33
  );
35
34
  }
36
35
 
37
- function printDiff(oldContent: string | null, newContent: string, filePath: string) {
38
- const oldLines = oldContent ? oldContent.split("\n") : [];
39
- const newLines = newContent.split("\n");
40
- const maxLines = Math.max(oldLines.length, newLines.length);
41
- const LIMIT = 60;
42
- const shown = Math.min(maxLines, LIMIT);
43
-
44
- console.log(`\n${c.bold}${c.white} ┌─ ${filePath} ─────────────────────────${c.reset}`);
45
- for (let i = 0; i < shown; i++) {
46
- const o = oldLines[i];
47
- const n = newLines[i];
48
- if (o === undefined) {
49
- process.stdout.write(`${c.green} + ${(i + 1).toString().padStart(4)} ${n ?? ""}${c.reset}\n`);
50
- } else if (n === undefined) {
51
- process.stdout.write(`${c.red} - ${(i + 1).toString().padStart(4)} ${o ?? ""}${c.reset}\n`);
52
- } else if (o !== n) {
53
- process.stdout.write(`${c.red} - ${(i + 1).toString().padStart(4)} ${o}${c.reset}\n`);
54
- process.stdout.write(`${c.green} + ${(i + 1).toString().padStart(4)} ${n}${c.reset}\n`);
55
- } else {
56
- process.stdout.write(`${c.dim} ${(i + 1).toString().padStart(4)} ${o}${c.reset}\n`);
57
- }
58
- }
59
- if (maxLines > LIMIT) {
60
- console.log(`${c.dim} ... ${maxLines - LIMIT} more lines omitted${c.reset}`);
61
- }
62
- console.log(`${c.bold}${c.white} └──────────────────────────────────────${c.reset}\n`);
63
- }
64
-
65
36
  async function confirm(prompt: string): Promise<boolean> {
66
37
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
67
38
  try {
@@ -86,35 +57,6 @@ async function readFileTool(cwd: string, args: { path: string }): Promise<string
86
57
  return fs.readFile(full, "utf-8");
87
58
  }
88
59
 
89
- async function writeFileTool(cwd: string, args: { path: string; content: string }): Promise<string> {
90
- const full = resolveInCwd(cwd, args.path);
91
- const lines = args.content.split("\n").length;
92
- const bytes = Buffer.byteLength(args.content, "utf-8");
93
-
94
- console.log(`\n${c.bold}${c.magenta} ◆ write_file${c.reset} ${c.dim}${args.path}${c.reset}`);
95
- console.log(`${c.dim} ${lines} lines · ${bytes} bytes${c.reset}`);
96
-
97
- // Show diff vs existing file
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
60
  async function runCommandTool(cwd: string, args: { command: string }): Promise<string> {
119
61
  console.log(`\n${c.bold}${c.yellow} ◆ run_command${c.reset} ${c.dim}${args.command}${c.reset}`);
120
62
  const approved = await confirm(`Run: ${c.bold}${args.command}${c.reset}?`);
@@ -150,13 +92,7 @@ async function runCommandTool(cwd: string, args: { command: string }): Promise<s
150
92
 
151
93
  async function gitTool(cwd: string, args: { args: string[] }): Promise<string> {
152
94
  const cmd = `git ${args.args.join(" ")}`;
153
- const isMutating = !["status", "diff", "log", "show", "branch"].includes(args.args[0]);
154
-
155
95
  header("git", cmd);
156
- if (isMutating) {
157
- const approved = await confirm(`Run: ${c.bold}${cmd}${c.reset}?`);
158
- if (!approved) return "User declined to run this git command.";
159
- }
160
96
 
161
97
  return new Promise((resolve) => {
162
98
  exec(cmd, { cwd, timeout: 30_000, maxBuffer: 1024 * 1024 }, (err, stdout, stderr) => {
@@ -178,7 +114,6 @@ async function listDirectoryTool(cwd: string, args: { path: string }): Promise<s
178
114
  if (e.isSymbolicLink()) return ` ${c.cyan}${e.name}@${c.reset}`;
179
115
  return ` ${e.name}`;
180
116
  });
181
- // Print inline preview
182
117
  lines.forEach(l => process.stdout.write(l + "\n"));
183
118
  return entries
184
119
  .map(e => (e.isDirectory() ? `${e.name}/` : e.name))
@@ -189,8 +124,6 @@ async function searchFilesTool(
189
124
  cwd: string,
190
125
  args: { pattern: string; glob?: string }
191
126
  ): Promise<string> {
192
- // Validate inputs: pattern must be a non-empty string; glob (if provided)
193
- // must only contain safe filename characters to prevent shell expansion.
194
127
  if (!args.pattern || typeof args.pattern !== "string") {
195
128
  return "Search failed: pattern must be a non-empty string.";
196
129
  }
@@ -202,7 +135,6 @@ async function searchFilesTool(
202
135
 
203
136
  header("search_files", `"${args.pattern}"${args.glob ? ` in ${args.glob}` : ""}`);
204
137
 
205
- // Build the argument array passed directly to execFile — no shell involved.
206
138
  const grepArgs: string[] = [
207
139
  "-rn",
208
140
  "--color=never",
@@ -211,13 +143,11 @@ async function searchFilesTool(
211
143
  if (args.glob) {
212
144
  grepArgs.push(`--include=${args.glob}`);
213
145
  }
214
- // Pattern and path as positional args (not shell-interpolated)
215
146
  grepArgs.push(args.pattern, ".");
216
147
 
217
148
  return new Promise((resolve) => {
218
149
  execFile("grep", grepArgs, { cwd, timeout: 15_000, maxBuffer: 1024 * 1024 }, (err, stdout, stderr) => {
219
150
  if (err && (err as NodeJS.ErrnoException & { code: number }).code === 1) {
220
- // grep exits 1 when no matches are found — not an error
221
151
  resolve("No matches found.");
222
152
  return;
223
153
  }
@@ -244,8 +174,6 @@ export async function executeToolCall(cwd: string, call: ToolCall): Promise<Tool
244
174
  switch (call.name) {
245
175
  case "read_file":
246
176
  return { result: await readFileTool(cwd, args), isError: false };
247
- case "write_file":
248
- return { result: await writeFileTool(cwd, args), isError: false };
249
177
  case "run_command":
250
178
  return { result: await runCommandTool(cwd, args), isError: false };
251
179
  case "git":