@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 +2 -2
- package/src/index.ts +57 -292
- package/src/repl.ts +11 -104
- package/src/sdk.ts +1 -1
- package/src/tools.ts +0 -72
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ayoxx/kundex",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "Kundex — a terminal-based AI coding agent CLI and SDK, powered by
|
|
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.
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
368
|
-
|
|
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 —
|
|
243
|
+
// No command — start REPL, prompting for API key if needed
|
|
457
244
|
if (!command) {
|
|
458
|
-
|
|
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
|
-
|
|
276
|
+
let config = loadConfig();
|
|
481
277
|
const baseUrl = resolveBaseUrl(config);
|
|
482
278
|
|
|
483
279
|
if (!config.apiKey || !baseUrl) {
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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,
|
|
177
|
-
• You will be asked to confirm any
|
|
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");
|
|
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 ?? "
|
|
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", "
|
|
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":
|