@ayoxx/kundex 0.1.7 → 1.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/README.md +5 -5
- package/package.json +1 -1
- package/src/config.ts +19 -3
- package/src/index.ts +551 -17
- package/src/repl.ts +403 -34
- package/src/sdk.ts +320 -35
- package/src/tools.ts +171 -13
package/src/repl.ts
CHANGED
|
@@ -1,16 +1,184 @@
|
|
|
1
1
|
import readline from "node:readline/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { execSync } from "node:child_process";
|
|
4
|
-
import { Kundex, type AgentTurn } from "./sdk";
|
|
4
|
+
import { Kundex, type AgentTurn, type AgentSession } from "./sdk";
|
|
5
5
|
import { executeToolCall } from "./tools";
|
|
6
6
|
import type { KundexConfig } from "./config";
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
8
|
+
// ─── ANSI colour helpers ──────────────────────────────────────────────────────
|
|
9
|
+
const c = {
|
|
10
|
+
reset: "\x1b[0m",
|
|
11
|
+
bold: "\x1b[1m",
|
|
12
|
+
dim: "\x1b[2m",
|
|
13
|
+
italic: "\x1b[3m",
|
|
14
|
+
cyan: "\x1b[36m",
|
|
15
|
+
green: "\x1b[32m",
|
|
16
|
+
yellow: "\x1b[33m",
|
|
17
|
+
red: "\x1b[31m",
|
|
18
|
+
blue: "\x1b[34m",
|
|
19
|
+
magenta: "\x1b[35m",
|
|
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
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
// ─── Per-language syntax highlighter (ANSI, no deps) ─────────────────────────
|
|
32
|
+
interface LangRule {
|
|
33
|
+
pattern: RegExp;
|
|
34
|
+
color: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const KEYWORDS: Record<string, string[]> = {
|
|
38
|
+
python: ["def","class","return","import","from","if","elif","else","for","while",
|
|
39
|
+
"try","except","with","as","in","not","and","or","is","None","True","False","pass","raise","yield","async","await","lambda"],
|
|
40
|
+
javascript: ["const","let","var","function","return","if","else","for","while",
|
|
41
|
+
"class","extends","import","export","from","default","new","this",
|
|
42
|
+
"try","catch","finally","throw","async","await","typeof","instanceof","of","in","null","undefined","true","false"],
|
|
43
|
+
typescript: ["const","let","var","function","return","if","else","for","while",
|
|
44
|
+
"class","extends","implements","interface","type","import","export",
|
|
45
|
+
"from","default","new","this","try","catch","finally","throw","async",
|
|
46
|
+
"await","typeof","instanceof","of","in","null","undefined","true","false",
|
|
47
|
+
"string","number","boolean","any","void","never","unknown","enum","as","namespace"],
|
|
48
|
+
bash: ["if","then","else","elif","fi","for","while","do","done","case","esac",
|
|
49
|
+
"function","return","echo","exit","local","export","source","cd","ls","grep","awk","sed"],
|
|
50
|
+
go: ["func","package","import","var","const","type","struct","interface","return",
|
|
51
|
+
"if","else","for","range","switch","case","default","go","chan","defer","select","nil","true","false","map","make","new"],
|
|
52
|
+
rust: ["fn","let","mut","pub","use","mod","struct","enum","impl","trait","return",
|
|
53
|
+
"if","else","for","while","loop","match","Some","None","Ok","Err","true","false","self","Self","super","as","in","where","async","await"],
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
function buildRules(lang: string): LangRule[] {
|
|
57
|
+
const kws = KEYWORDS[lang] || KEYWORDS["javascript"];
|
|
58
|
+
return [
|
|
59
|
+
// Comments
|
|
60
|
+
{ pattern: /(#.*$|\/\/.*$|\/\*[\s\S]*?\*\/|"""[\s\S]*?"""|'''[\s\S]*?''')/gm, color: c.comment },
|
|
61
|
+
// Strings
|
|
62
|
+
{ pattern: /("(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|`(?:[^`\\]|\\.)*`)/g, color: c.str },
|
|
63
|
+
// Numbers
|
|
64
|
+
{ pattern: /\b(\d+\.?\d*(?:e[+-]?\d+)?)\b/g, color: c.num },
|
|
65
|
+
// Keywords (built from per-language list)
|
|
66
|
+
{ pattern: new RegExp(`\\b(${kws.join("|")})\\b`, "g"), color: c.kw },
|
|
67
|
+
// Function calls
|
|
68
|
+
{ pattern: /\b([a-zA-Z_]\w*)\s*(?=\()/g, color: c.fn },
|
|
69
|
+
// Type annotations (CamelCase identifiers)
|
|
70
|
+
{ pattern: /\b([A-Z][a-zA-Z0-9_]*)\b/g, color: c.type_ },
|
|
71
|
+
];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Apply regex-based ANSI syntax highlighting to a code string. */
|
|
75
|
+
function syntaxHighlight(code: string, lang: string): string {
|
|
76
|
+
const l = lang.toLowerCase();
|
|
77
|
+
const normalised = ["tsx","ts"].includes(l) ? "typescript"
|
|
78
|
+
: ["jsx","js","mjs"].includes(l) ? "javascript"
|
|
79
|
+
: ["python","py"].includes(l) ? "python"
|
|
80
|
+
: ["sh","shell","zsh","bash"].includes(l) ? "bash"
|
|
81
|
+
: ["rs"].includes(l) ? "rust"
|
|
82
|
+
: l;
|
|
83
|
+
|
|
84
|
+
const rules = buildRules(normalised in KEYWORDS ? normalised : "javascript");
|
|
85
|
+
|
|
86
|
+
type Span = { start: number; end: number; color: string };
|
|
87
|
+
const spans: Span[] = [];
|
|
88
|
+
|
|
89
|
+
for (const rule of rules) {
|
|
90
|
+
rule.pattern.lastIndex = 0;
|
|
91
|
+
let m: RegExpExecArray | null;
|
|
92
|
+
while ((m = rule.pattern.exec(code)) !== null) {
|
|
93
|
+
spans.push({ start: m.index, end: m.index + m[0].length, color: rule.color });
|
|
94
|
+
if (rule.pattern.lastIndex === m.index) rule.pattern.lastIndex++;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (spans.length === 0) return code;
|
|
99
|
+
|
|
100
|
+
spans.sort((a, b) => a.start - b.start);
|
|
101
|
+
const merged: Span[] = [];
|
|
102
|
+
let cursor = 0;
|
|
103
|
+
for (const span of spans) {
|
|
104
|
+
if (span.start < cursor) continue;
|
|
105
|
+
merged.push(span);
|
|
106
|
+
cursor = span.end;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
let out = "";
|
|
110
|
+
cursor = 0;
|
|
111
|
+
for (const span of merged) {
|
|
112
|
+
out += code.slice(cursor, span.start);
|
|
113
|
+
out += span.color + code.slice(span.start, span.end) + c.reset;
|
|
114
|
+
cursor = span.end;
|
|
115
|
+
}
|
|
116
|
+
out += code.slice(cursor);
|
|
117
|
+
return out;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// ─── Code-block printer ───────────────────────────────────────────────────────
|
|
121
|
+
function printCodeBlock(lang: string, code: string) {
|
|
122
|
+
const border = "─".repeat(52);
|
|
123
|
+
const label = lang || "code";
|
|
124
|
+
const labelPad = "─".repeat(Math.max(0, 52 - label.length - 3));
|
|
125
|
+
|
|
126
|
+
process.stdout.write(`${c.dim}┌─ ${c.reset}${c.bold}${label}${c.reset}${c.dim} ${labelPad}${c.reset}\n`);
|
|
127
|
+
|
|
128
|
+
const highlighted = syntaxHighlight(code, lang);
|
|
129
|
+
highlighted.split("\n").forEach(line => {
|
|
130
|
+
process.stdout.write(`${c.dim}│${c.reset} ${line}\n`);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
process.stdout.write(`${c.dim}└${border}${c.reset}\n`);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// ─── Reply printer (handles mixed text + code blocks) ────────────────────────
|
|
137
|
+
function printReply(text: string) {
|
|
138
|
+
process.stdout.write("\n");
|
|
139
|
+
const codeBlockRe = /```(\w*)\n?([\s\S]*?)```/g;
|
|
140
|
+
let last = 0;
|
|
141
|
+
let match: RegExpExecArray | null;
|
|
142
|
+
|
|
143
|
+
while ((match = codeBlockRe.exec(text)) !== null) {
|
|
144
|
+
const before = text.slice(last, match.index);
|
|
145
|
+
if (before.trim()) {
|
|
146
|
+
before.split("\n").forEach(line => {
|
|
147
|
+
process.stdout.write(` ${line}\n`);
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
printCodeBlock(match[1], match[2].trimEnd());
|
|
151
|
+
last = match.index + match[0].length;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const tail = text.slice(last);
|
|
155
|
+
if (tail.trim()) {
|
|
156
|
+
tail.split("\n").forEach(line => {
|
|
157
|
+
process.stdout.write(` ${line}\n`);
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
process.stdout.write(`\n${c.dim}${"─".repeat(60)}${c.reset}\n\n`);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
|
165
|
+
const HELP = `
|
|
166
|
+
${c.bold}Commands:${c.reset}
|
|
167
|
+
/help Show this help
|
|
168
|
+
/model Show the model for this session
|
|
169
|
+
/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
|
+
/exit Quit kundex
|
|
174
|
+
|
|
175
|
+
${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.
|
|
178
|
+
• Responses stream in real-time — text appears as the model generates it.
|
|
179
|
+
• Code blocks are syntax-highlighted in the terminal.
|
|
180
|
+
• Press Ctrl+C to abort a running request.
|
|
181
|
+
`;
|
|
14
182
|
|
|
15
183
|
function safeGitStatus(cwd: string): string | undefined {
|
|
16
184
|
try {
|
|
@@ -20,78 +188,279 @@ function safeGitStatus(cwd: string): string | undefined {
|
|
|
20
188
|
}
|
|
21
189
|
}
|
|
22
190
|
|
|
191
|
+
function safeFileTree(cwd: string): string | undefined {
|
|
192
|
+
try {
|
|
193
|
+
return execSync(
|
|
194
|
+
`find . -maxdepth 3 ! -path '*/node_modules/*' ! -path '*/.git/*' ! -path '*/dist/*' ! -path '*/build/*' -print`,
|
|
195
|
+
{ cwd, timeout: 5000 }
|
|
196
|
+
).toString().trim().split("\n").slice(0, 200).join("\n");
|
|
197
|
+
} catch {
|
|
198
|
+
return undefined;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
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
|
+
function createSpinner(label: string) {
|
|
207
|
+
const frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
208
|
+
let frame = 0;
|
|
209
|
+
const timer = setInterval(() => {
|
|
210
|
+
process.stdout.write(`\r${c.dim} ${frames[frame++ % frames.length]} ${label}${c.reset} `);
|
|
211
|
+
}, 80);
|
|
212
|
+
|
|
213
|
+
return {
|
|
214
|
+
clear() {
|
|
215
|
+
clearInterval(timer);
|
|
216
|
+
process.stdout.write("\r\x1b[2K"); // CR + erase line
|
|
217
|
+
},
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
|
|
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
|
+
*/
|
|
23
225
|
async function handleTurn(
|
|
24
226
|
kundex: Kundex,
|
|
25
227
|
cwd: string,
|
|
26
228
|
sessionId: number,
|
|
27
229
|
turn: AgentTurn,
|
|
230
|
+
signal?: AbortSignal,
|
|
28
231
|
): Promise<void> {
|
|
29
232
|
let current = turn;
|
|
30
233
|
|
|
31
234
|
while (!current.done && current.toolCalls && current.toolCalls.length > 0) {
|
|
32
235
|
for (const call of current.toolCalls) {
|
|
33
|
-
console.log(`\n\u2192 ${call.name}(${call.arguments})`);
|
|
34
236
|
const { result, isError } = await executeToolCall(cwd, call);
|
|
35
|
-
|
|
237
|
+
if (isError) {
|
|
238
|
+
process.stdout.write(`${c.red} ✗ ${call.name}: ${result}${c.reset}\n`);
|
|
239
|
+
}
|
|
36
240
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
241
|
+
// Show a spinner while waiting for the follow-up AI turn
|
|
242
|
+
const sp = createSpinner("processing tool result…");
|
|
243
|
+
let followUpText = "";
|
|
244
|
+
|
|
245
|
+
try {
|
|
246
|
+
current = await kundex.agent.submitToolResult(
|
|
247
|
+
{ sessionId, toolCallId: call.id, result, isError },
|
|
248
|
+
(delta) => {
|
|
249
|
+
if (!followUpText) {
|
|
250
|
+
// First token arrived — clear the spinner
|
|
251
|
+
sp.clear();
|
|
252
|
+
process.stdout.write("\n");
|
|
253
|
+
}
|
|
254
|
+
followUpText += delta;
|
|
255
|
+
process.stdout.write(delta);
|
|
256
|
+
},
|
|
257
|
+
signal,
|
|
258
|
+
);
|
|
259
|
+
} finally {
|
|
260
|
+
sp.clear();
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
if (followUpText && !followUpText.endsWith("\n")) {
|
|
264
|
+
process.stdout.write("\n");
|
|
265
|
+
}
|
|
43
266
|
}
|
|
44
267
|
}
|
|
45
268
|
|
|
46
|
-
if (current.reply) {
|
|
47
|
-
|
|
269
|
+
if (current.reply && !turn.toolCalls?.length) {
|
|
270
|
+
// Reply was already streamed inline; just print the closing rule
|
|
271
|
+
process.stdout.write(`\n${c.dim}${"─".repeat(60)}${c.reset}\n\n`);
|
|
272
|
+
} else if (current.reply) {
|
|
273
|
+
printReply(current.reply);
|
|
48
274
|
}
|
|
49
275
|
}
|
|
50
276
|
|
|
277
|
+
function printBanner(cwd: string, model: string) {
|
|
278
|
+
const shortCwd = cwd.replace(process.env.HOME ?? "", "~");
|
|
279
|
+
console.log(`
|
|
280
|
+
${c.bold}${c.cyan} ██╗ ██╗██╗ ██╗███╗ ██╗██████╗ ███████╗██╗ ██╗${c.reset}
|
|
281
|
+
${c.bold}${c.cyan} ██║ ██╔╝██║ ██║████╗ ██║██╔══██╗██╔════╝╚██╗██╔╝${c.reset}
|
|
282
|
+
${c.bold}${c.cyan} █████╔╝ ██║ ██║██╔██╗ ██║██║ ██║█████╗ ╚███╔╝ ${c.reset}
|
|
283
|
+
${c.bold}${c.cyan} ██╔═██╗ ██║ ██║██║╚██╗██║██║ ██║██╔══╝ ██╔██╗ ${c.reset}
|
|
284
|
+
${c.bold}${c.cyan} ██║ ██╗╚██████╔╝██║ ╚████║██████╔╝███████╗██╔╝ ██╗${c.reset}
|
|
285
|
+
${c.bold}${c.cyan} ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═══╝╚═════╝ ╚══════╝╚═╝ ╚═╝${c.reset}
|
|
286
|
+
|
|
287
|
+
${c.dim}AI coding agent — terminal edition${c.reset}
|
|
288
|
+
${c.dim}cwd:${c.reset} ${c.bold}${shortCwd}${c.reset}
|
|
289
|
+
${c.dim}model:${c.reset} ${c.bold}${model}${c.reset}
|
|
290
|
+
|
|
291
|
+
Type ${c.cyan}/help${c.reset} for commands, ${c.cyan}Ctrl+C${c.reset} to stop a running request.
|
|
292
|
+
`);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// ─── REPL ─────────────────────────────────────────────────────────────────────
|
|
51
296
|
export async function runRepl(config: KundexConfig, apiKey: string, baseUrl: string): Promise<void> {
|
|
52
297
|
const cwd = process.cwd();
|
|
53
298
|
const kundex = new Kundex({ apiKey, baseUrl });
|
|
54
299
|
const model = config.defaultModel ?? "openai/gpt-oss-120b";
|
|
55
300
|
|
|
56
|
-
|
|
57
|
-
console.log("Type /help for commands.\n");
|
|
301
|
+
printBanner(cwd, model);
|
|
58
302
|
|
|
59
303
|
let session = await kundex.agent.createSession({ cwd, model });
|
|
60
|
-
|
|
61
304
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
62
305
|
|
|
306
|
+
// AbortController for the current in-progress request.
|
|
307
|
+
// Ctrl+C signals the controller and sends a SIGINT to abort the fetch.
|
|
308
|
+
let currentAbort: AbortController | null = null;
|
|
309
|
+
|
|
310
|
+
const sigintHandler = () => {
|
|
311
|
+
if (currentAbort && !currentAbort.signal.aborted) {
|
|
312
|
+
currentAbort.abort();
|
|
313
|
+
process.stdout.write(`\n${c.yellow} ⚡ Stopped.${c.reset}\n\n`);
|
|
314
|
+
currentAbort = null;
|
|
315
|
+
} else {
|
|
316
|
+
// Second Ctrl+C with no request in flight → exit
|
|
317
|
+
console.log(`\n${c.dim}Goodbye!${c.reset}\n`);
|
|
318
|
+
rl.close();
|
|
319
|
+
process.exit(0);
|
|
320
|
+
}
|
|
321
|
+
};
|
|
322
|
+
|
|
323
|
+
process.on("SIGINT", sigintHandler);
|
|
324
|
+
|
|
63
325
|
try {
|
|
64
326
|
for (;;) {
|
|
65
|
-
const
|
|
327
|
+
const raw = await rl.question(`${c.bold}${c.cyan}kundex${c.reset} ${c.dim}▸${c.reset} `);
|
|
328
|
+
const input = raw.trim();
|
|
66
329
|
if (!input) continue;
|
|
67
330
|
|
|
68
|
-
|
|
69
|
-
if (input === "/
|
|
70
|
-
console.log(
|
|
71
|
-
|
|
331
|
+
// ── REPL commands ───────────────────────────────────────────────────────
|
|
332
|
+
if (input === "/exit" || input === "/quit") {
|
|
333
|
+
console.log(`\n${c.dim}Goodbye!${c.reset}\n`);
|
|
334
|
+
break;
|
|
72
335
|
}
|
|
336
|
+
if (input === "/help") { console.log(HELP); continue; }
|
|
73
337
|
if (input === "/model") {
|
|
74
|
-
console.log(`
|
|
338
|
+
console.log(` ${c.dim}model:${c.reset} ${c.bold}${session.model}${c.reset}`);
|
|
75
339
|
continue;
|
|
76
340
|
}
|
|
77
341
|
if (input === "/clear") {
|
|
78
342
|
session = await kundex.agent.createSession({ cwd, model });
|
|
79
|
-
console.log(
|
|
343
|
+
console.log(`\n${c.green} ✓ Started a new session.${c.reset}\n`);
|
|
80
344
|
continue;
|
|
81
345
|
}
|
|
82
346
|
|
|
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
|
+
// ── Agent message ────────────────────────────────────────────────────────
|
|
411
|
+
currentAbort = new AbortController();
|
|
412
|
+
const { signal } = currentAbort;
|
|
413
|
+
const spinner = createSpinner("thinking…");
|
|
414
|
+
let streamedAnyText = false;
|
|
415
|
+
|
|
83
416
|
try {
|
|
84
|
-
const turn = await kundex.agent.
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
417
|
+
const turn = await kundex.agent.streamMessage(
|
|
418
|
+
{
|
|
419
|
+
sessionId: session.id,
|
|
420
|
+
message: input,
|
|
421
|
+
context: {
|
|
422
|
+
gitStatus: safeGitStatus(cwd),
|
|
423
|
+
fileTree: safeFileTree(cwd),
|
|
424
|
+
openFiles: [],
|
|
425
|
+
},
|
|
426
|
+
},
|
|
427
|
+
(delta) => {
|
|
428
|
+
if (!streamedAnyText) {
|
|
429
|
+
spinner.clear();
|
|
430
|
+
process.stdout.write("\n");
|
|
431
|
+
streamedAnyText = true;
|
|
432
|
+
}
|
|
433
|
+
process.stdout.write(delta);
|
|
434
|
+
},
|
|
435
|
+
signal,
|
|
436
|
+
);
|
|
437
|
+
|
|
438
|
+
spinner.clear();
|
|
439
|
+
|
|
440
|
+
// If the response contained only tool calls (no streamed text), let
|
|
441
|
+
// handleTurn print the tool output and follow-up replies.
|
|
442
|
+
if (turn.toolCalls?.length) {
|
|
443
|
+
await handleTurn(kundex, cwd, session.id, turn, signal);
|
|
444
|
+
} else if (streamedAnyText) {
|
|
445
|
+
// Text was streamed inline; just print the closing separator.
|
|
446
|
+
process.stdout.write(`\n${c.dim}${"─".repeat(60)}${c.reset}\n\n`);
|
|
447
|
+
} else if (turn.reply) {
|
|
448
|
+
printReply(turn.reply);
|
|
449
|
+
}
|
|
90
450
|
} catch (err) {
|
|
91
|
-
|
|
451
|
+
spinner.clear();
|
|
452
|
+
const e = err as Error;
|
|
453
|
+
if (e.name === "AbortError") {
|
|
454
|
+
// Already printed "⚡ Stopped" via sigintHandler
|
|
455
|
+
} else {
|
|
456
|
+
console.error(`\n${c.red} error: ${e.message}${c.reset}\n`);
|
|
457
|
+
}
|
|
458
|
+
} finally {
|
|
459
|
+
currentAbort = null;
|
|
92
460
|
}
|
|
93
461
|
}
|
|
94
462
|
} finally {
|
|
463
|
+
process.off("SIGINT", sigintHandler);
|
|
95
464
|
rl.close();
|
|
96
465
|
}
|
|
97
466
|
}
|