@hemansubedi/aether-ai 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/.gitattributes +3 -0
- package/.github/workflows/live-stats.yml +42 -0
- package/.github/workflows/publish.yml +34 -0
- package/.github/workflows/update-preview.yml +41 -0
- package/INSTALL.md +59 -0
- package/LICENSE +21 -0
- package/README.md +397 -0
- package/assets/aether-arena.svg +72 -0
- package/assets/aether-banner.svg +62 -0
- package/assets/aether-router.svg +129 -0
- package/dist/agent.js +125 -0
- package/dist/arena.js +486 -0
- package/dist/checkpoint.js +105 -0
- package/dist/client.js +95 -0
- package/dist/combos.js +176 -0
- package/dist/commands.js +483 -0
- package/dist/config.js +104 -0
- package/dist/cost.js +176 -0
- package/dist/git.js +52 -0
- package/dist/health.js +81 -0
- package/dist/index.js +272 -0
- package/dist/keys.js +128 -0
- package/dist/memory.js +98 -0
- package/dist/modes.js +68 -0
- package/dist/providers/index.js +32 -0
- package/dist/providers/ollama.js +206 -0
- package/dist/providers/openai-compat.js +181 -0
- package/dist/providers/openrouter.js +189 -0
- package/dist/providers/registry.js +211 -0
- package/dist/router-engine.js +200 -0
- package/dist/router.js +171 -0
- package/dist/server.js +210 -0
- package/dist/session.js +97 -0
- package/dist/settings.js +97 -0
- package/dist/skills.js +100 -0
- package/dist/tokensaver.js +50 -0
- package/dist/tools/filesystem.js +243 -0
- package/dist/tools/git.js +53 -0
- package/dist/tools/glob.js +175 -0
- package/dist/tools/grep.js +193 -0
- package/dist/tools/registry.js +39 -0
- package/dist/tools/vision.js +140 -0
- package/dist/tools/websearch.js +118 -0
- package/dist/tui.js +562 -0
- package/dist/types.js +8 -0
- package/docs/preview.txt +51 -0
- package/docs/screenshots.md +110 -0
- package/docs/stats.md +5 -0
- package/install.ps1 +170 -0
- package/install.sh +196 -0
- package/package.json +34 -0
- package/scripts/generate-stats-card.ts +62 -0
- package/scripts/patch_index.ps1 +17 -0
- package/scripts/release.sh +7 -0
- package/src/agent.ts +146 -0
- package/src/arena.ts +584 -0
- package/src/checkpoint.ts +111 -0
- package/src/client.ts +172 -0
- package/src/combos.ts +199 -0
- package/src/commands.ts +973 -0
- package/src/config.ts +122 -0
- package/src/cost.ts +206 -0
- package/src/git.ts +68 -0
- package/src/health.ts +90 -0
- package/src/index.ts +281 -0
- package/src/keys.ts +135 -0
- package/src/memory.ts +101 -0
- package/src/modes.ts +84 -0
- package/src/providers/index.ts +59 -0
- package/src/providers/ollama.ts +222 -0
- package/src/providers/openai-compat.ts +188 -0
- package/src/providers/openrouter.ts +198 -0
- package/src/providers/registry.ts +223 -0
- package/src/router-engine.ts +214 -0
- package/src/router.ts +195 -0
- package/src/server.ts +242 -0
- package/src/session.ts +111 -0
- package/src/settings.ts +125 -0
- package/src/skills.ts +106 -0
- package/src/tokensaver.ts +57 -0
- package/src/tools/filesystem.ts +258 -0
- package/src/tools/git.ts +53 -0
- package/src/tools/glob.ts +180 -0
- package/src/tools/grep.ts +192 -0
- package/src/tools/registry.ts +54 -0
- package/src/tools/vision.ts +152 -0
- package/src/tools/websearch.ts +130 -0
- package/src/tui.ts +664 -0
- package/src/types.ts +77 -0
- package/tsconfig.json +16 -0
package/dist/tui.js
ADDED
|
@@ -0,0 +1,562 @@
|
|
|
1
|
+
import * as readline from "node:readline";
|
|
2
|
+
import { Agent } from "./agent.js";
|
|
3
|
+
import { Session } from "./session.js";
|
|
4
|
+
import { Router } from "./router.js";
|
|
5
|
+
import { Arena } from "./arena.js";
|
|
6
|
+
import { CommandHandler, parseCommand } from "./commands.js";
|
|
7
|
+
import { getConfig } from "./config.js";
|
|
8
|
+
import { messageText } from "./types.js";
|
|
9
|
+
import { HealthTracker } from "./health.js";
|
|
10
|
+
import { ToolRegistry } from "./tools/registry.js";
|
|
11
|
+
import { makeReadFileTool, makeWriteFileTool, makeEditFileTool, makeListDirTool, makeBashTool, } from "./tools/filesystem.js";
|
|
12
|
+
const ESC = "\u001b";
|
|
13
|
+
const CURSOR_HIDE = `${ESC}[?25l`;
|
|
14
|
+
const CURSOR_SHOW = `${ESC}[?25h`;
|
|
15
|
+
const ALT_SCREEN = `${ESC}[?1049h`;
|
|
16
|
+
const ALT_SCREEN_OFF = `${ESC}[?1049l`;
|
|
17
|
+
const CLEAR = `${ESC}[2J${ESC}[H`;
|
|
18
|
+
const RESET = `${ESC}[0m`;
|
|
19
|
+
const BOLD = `${ESC}[1m`;
|
|
20
|
+
const DIM = `${ESC}[2m`;
|
|
21
|
+
const ITALIC = `${ESC}[3m`;
|
|
22
|
+
const UNDERLINE = `${ESC}[4m`;
|
|
23
|
+
const FG = {
|
|
24
|
+
red: `${ESC}[31m`,
|
|
25
|
+
green: `${ESC}[32m`,
|
|
26
|
+
yellow: `${ESC}[33m`,
|
|
27
|
+
blue: `${ESC}[34m`,
|
|
28
|
+
magenta: `${ESC}[35m`,
|
|
29
|
+
cyan: `${ESC}[36m`,
|
|
30
|
+
white: `${ESC}[37m`,
|
|
31
|
+
gray: `${ESC}[90m`,
|
|
32
|
+
brightRed: `${ESC}[91m`,
|
|
33
|
+
brightGreen: `${ESC}[92m`,
|
|
34
|
+
brightYellow: `${ESC}[93m`,
|
|
35
|
+
brightCyan: `${ESC}[96m`,
|
|
36
|
+
};
|
|
37
|
+
const BG = {
|
|
38
|
+
blue: `\u001b[44m`,
|
|
39
|
+
darkGray: `\u001b[100m`,
|
|
40
|
+
};
|
|
41
|
+
const SPINNER = ['�', '?', '?', '?', '?', '?'];
|
|
42
|
+
export class TUI {
|
|
43
|
+
agent;
|
|
44
|
+
router;
|
|
45
|
+
session;
|
|
46
|
+
arena;
|
|
47
|
+
costTracker;
|
|
48
|
+
skills;
|
|
49
|
+
checkpoint;
|
|
50
|
+
settings;
|
|
51
|
+
rl;
|
|
52
|
+
isTty;
|
|
53
|
+
running = true;
|
|
54
|
+
messages = [];
|
|
55
|
+
inputLines = [""];
|
|
56
|
+
inputCursor = 0;
|
|
57
|
+
scrollOffset = 0;
|
|
58
|
+
spinnerIdx = 0;
|
|
59
|
+
spinnerTimer = null;
|
|
60
|
+
thinking = false;
|
|
61
|
+
arenaMode = false;
|
|
62
|
+
lastRenderHeight = 0;
|
|
63
|
+
resizeTimer = null;
|
|
64
|
+
constructor(opts) {
|
|
65
|
+
this.agent = opts.agent;
|
|
66
|
+
this.router = opts.router;
|
|
67
|
+
this.session = opts.session;
|
|
68
|
+
this.arena = opts.arena;
|
|
69
|
+
this.costTracker = opts.costTracker;
|
|
70
|
+
this.skills = opts.skills;
|
|
71
|
+
this.checkpoint = opts.checkpoint;
|
|
72
|
+
this.settings = opts.settings;
|
|
73
|
+
this.isTty = Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
74
|
+
this.rl = readline.createInterface({
|
|
75
|
+
input: process.stdin,
|
|
76
|
+
output: process.stdout,
|
|
77
|
+
terminal: false,
|
|
78
|
+
crlfDelay: Infinity,
|
|
79
|
+
});
|
|
80
|
+
for (const m of this.session.messages) {
|
|
81
|
+
if (m.role === "system")
|
|
82
|
+
continue;
|
|
83
|
+
this.messages.push({ role: m.role, content: messageText(m) });
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
showSystem(text) {
|
|
87
|
+
this.messages.push({ role: "system", content: text });
|
|
88
|
+
this.render();
|
|
89
|
+
if (!this.isTty)
|
|
90
|
+
process.stdout.write(text + "\n");
|
|
91
|
+
}
|
|
92
|
+
exit() {
|
|
93
|
+
this.running = false;
|
|
94
|
+
this.cleanup();
|
|
95
|
+
process.exit(0);
|
|
96
|
+
}
|
|
97
|
+
enterArena() {
|
|
98
|
+
this.arenaMode = true;
|
|
99
|
+
this.messages.push({
|
|
100
|
+
role: "system",
|
|
101
|
+
content: "Arena mode: type a prompt to compare models side-by-side. /arena exit to return to chat.",
|
|
102
|
+
});
|
|
103
|
+
this.render();
|
|
104
|
+
}
|
|
105
|
+
exitArena() {
|
|
106
|
+
this.arenaMode = false;
|
|
107
|
+
this.messages.push({ role: "system", content: "Returned to chat mode." });
|
|
108
|
+
this.render();
|
|
109
|
+
}
|
|
110
|
+
start() {
|
|
111
|
+
if (!this.isTty) {
|
|
112
|
+
this.runNonTty();
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
this.initTty();
|
|
116
|
+
this.render();
|
|
117
|
+
this.bindKeys();
|
|
118
|
+
this.prompt();
|
|
119
|
+
}
|
|
120
|
+
initTty() {
|
|
121
|
+
process.stdin.setEncoding("utf8");
|
|
122
|
+
process.stdin.resume();
|
|
123
|
+
process.stdout.write(CURSOR_HIDE);
|
|
124
|
+
process.stdout.write(ALT_SCREEN);
|
|
125
|
+
process.stdin.on("resize", () => this.scheduleResize());
|
|
126
|
+
}
|
|
127
|
+
scheduleResize() {
|
|
128
|
+
if (this.resizeTimer)
|
|
129
|
+
clearTimeout(this.resizeTimer);
|
|
130
|
+
this.resizeTimer = setTimeout(() => {
|
|
131
|
+
this.scrollOffset = 0;
|
|
132
|
+
this.render();
|
|
133
|
+
}, 100);
|
|
134
|
+
}
|
|
135
|
+
cleanup() {
|
|
136
|
+
if (this.spinnerTimer) {
|
|
137
|
+
clearInterval(this.spinnerTimer);
|
|
138
|
+
this.spinnerTimer = null;
|
|
139
|
+
}
|
|
140
|
+
if (this.resizeTimer) {
|
|
141
|
+
clearTimeout(this.resizeTimer);
|
|
142
|
+
this.resizeTimer = null;
|
|
143
|
+
}
|
|
144
|
+
process.stdout.write(CURSOR_SHOW);
|
|
145
|
+
process.stdout.write(ALT_SCREEN_OFF);
|
|
146
|
+
process.stdout.write(RESET);
|
|
147
|
+
this.rl.close();
|
|
148
|
+
}
|
|
149
|
+
runNonTty() {
|
|
150
|
+
this.thinking = false;
|
|
151
|
+
this.rl.on("line", async (line) => {
|
|
152
|
+
await this.handleLine(line);
|
|
153
|
+
});
|
|
154
|
+
this.rl.on("close", () => {
|
|
155
|
+
this.running = false;
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
prompt() {
|
|
159
|
+
if (!this.isTty)
|
|
160
|
+
return;
|
|
161
|
+
this.render();
|
|
162
|
+
}
|
|
163
|
+
bindKeys() {
|
|
164
|
+
process.stdin.on("data", (data) => {
|
|
165
|
+
this.handleRawInput(data.toString("utf8"));
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
termWidth() {
|
|
169
|
+
return process.stdout.columns || 80;
|
|
170
|
+
}
|
|
171
|
+
termHeight() {
|
|
172
|
+
return process.stdout.rows || 24;
|
|
173
|
+
}
|
|
174
|
+
wrap(text, width) {
|
|
175
|
+
const lines = [];
|
|
176
|
+
for (const para of text.split("\n")) {
|
|
177
|
+
if (para.length === 0) {
|
|
178
|
+
lines.push("");
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
let start = 0;
|
|
182
|
+
while (start < para.length) {
|
|
183
|
+
const chunk = para.slice(start, start + width);
|
|
184
|
+
lines.push(chunk);
|
|
185
|
+
start += width;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return lines;
|
|
189
|
+
}
|
|
190
|
+
renderMessage(m, width) {
|
|
191
|
+
const color = m.role === "user"
|
|
192
|
+
? FG.brightCyan
|
|
193
|
+
: m.role === "assistant"
|
|
194
|
+
? FG.brightGreen
|
|
195
|
+
: m.role === "tool"
|
|
196
|
+
? FG.gray
|
|
197
|
+
: FG.brightYellow;
|
|
198
|
+
const label = m.role === "user"
|
|
199
|
+
? "You"
|
|
200
|
+
: m.role === "assistant"
|
|
201
|
+
? "Assistant"
|
|
202
|
+
: m.role === "tool"
|
|
203
|
+
? "Tool"
|
|
204
|
+
: "System";
|
|
205
|
+
const out = [];
|
|
206
|
+
out.push(`${color}${label}${RESET}`);
|
|
207
|
+
const body = this.wrap(m.content, width - 2);
|
|
208
|
+
for (const line of body) {
|
|
209
|
+
out.push(`${DIM}${line}${RESET}`);
|
|
210
|
+
}
|
|
211
|
+
return out;
|
|
212
|
+
}
|
|
213
|
+
banner() {
|
|
214
|
+
const provider = this.router.getActiveProvider() ?? "auto";
|
|
215
|
+
const model = this.router.getActiveModel() ?? "(default)";
|
|
216
|
+
const mode = this.arenaMode ? "ARENA" : "CHAT";
|
|
217
|
+
const line = `aether | ${mode} | ${provider} | ${model}`;
|
|
218
|
+
const pad = Math.max(0, this.termWidth() - line.length);
|
|
219
|
+
return `${BG.blue}${FG.white}${BOLD} ${line}${" ".repeat(pad)} ${RESET}`;
|
|
220
|
+
}
|
|
221
|
+
render() {
|
|
222
|
+
if (!this.isTty)
|
|
223
|
+
return;
|
|
224
|
+
const width = this.termWidth();
|
|
225
|
+
const height = this.termHeight();
|
|
226
|
+
const rendered = [];
|
|
227
|
+
rendered.push(this.banner());
|
|
228
|
+
rendered.push(`${FG.gray}${"-".repeat(width)}${RESET}`);
|
|
229
|
+
const bodyWidth = width - 2;
|
|
230
|
+
const allLines = [];
|
|
231
|
+
const start = Math.max(0, this.messages.length - 60);
|
|
232
|
+
for (let i = start; i < this.messages.length; i++) {
|
|
233
|
+
allLines.push(...this.renderMessage(this.messages[i], bodyWidth));
|
|
234
|
+
}
|
|
235
|
+
// Apply scroll offset (scroll down to newest by default).
|
|
236
|
+
const maxScroll = Math.max(0, allLines.length - (height - 10));
|
|
237
|
+
if (this.scrollOffset > maxScroll)
|
|
238
|
+
this.scrollOffset = maxScroll;
|
|
239
|
+
const visible = allLines.slice(this.scrollOffset, this.scrollOffset + height - 10);
|
|
240
|
+
for (const line of visible) {
|
|
241
|
+
rendered.push(` ${line}`);
|
|
242
|
+
}
|
|
243
|
+
while (rendered.length < height - 4) {
|
|
244
|
+
rendered.push("");
|
|
245
|
+
}
|
|
246
|
+
if (this.thinking) {
|
|
247
|
+
const sp = SPINNER[this.spinnerIdx % SPINNER.length];
|
|
248
|
+
rendered.push(`${FG.brightYellow}${sp} thinking...${RESET}`);
|
|
249
|
+
}
|
|
250
|
+
// Input box.
|
|
251
|
+
rendered.push(`${FG.gray}${"-".repeat(width)}${RESET}`);
|
|
252
|
+
const inputText = this.inputLines.join("\n");
|
|
253
|
+
const inputDisplay = this.wrap(inputText || "", width - 4);
|
|
254
|
+
if (inputDisplay.length === 0)
|
|
255
|
+
inputDisplay.push("");
|
|
256
|
+
for (const line of inputDisplay) {
|
|
257
|
+
rendered.push(` ${FG.brightCyan}${line}${RESET}`);
|
|
258
|
+
}
|
|
259
|
+
rendered.push(`${FG.brightCyan}> ${RESET}`);
|
|
260
|
+
process.stdout.write(CLEAR);
|
|
261
|
+
process.stdout.write(rendered.join("\n"));
|
|
262
|
+
this.lastRenderHeight = rendered.length;
|
|
263
|
+
}
|
|
264
|
+
scrollUp(n = 1) {
|
|
265
|
+
this.scrollOffset = Math.max(0, this.scrollOffset - n);
|
|
266
|
+
this.render();
|
|
267
|
+
}
|
|
268
|
+
scrollDown(n = 1) {
|
|
269
|
+
this.scrollOffset += n;
|
|
270
|
+
this.render();
|
|
271
|
+
}
|
|
272
|
+
startSpinner() {
|
|
273
|
+
if (this.spinnerTimer)
|
|
274
|
+
return;
|
|
275
|
+
this.thinking = true;
|
|
276
|
+
this.spinnerIdx = 0;
|
|
277
|
+
this.spinnerTimer = setInterval(() => {
|
|
278
|
+
this.spinnerIdx++;
|
|
279
|
+
this.render();
|
|
280
|
+
}, 120);
|
|
281
|
+
}
|
|
282
|
+
stopSpinner() {
|
|
283
|
+
if (this.spinnerTimer) {
|
|
284
|
+
clearInterval(this.spinnerTimer);
|
|
285
|
+
this.spinnerTimer = null;
|
|
286
|
+
}
|
|
287
|
+
this.thinking = false;
|
|
288
|
+
}
|
|
289
|
+
handleRawInput(s) {
|
|
290
|
+
if (s === "\u0003") {
|
|
291
|
+
// Ctrl+C: clear input
|
|
292
|
+
this.inputLines = [""];
|
|
293
|
+
this.inputCursor = 0;
|
|
294
|
+
this.render();
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
if (s === "\u001b") {
|
|
298
|
+
// Esc: clear input line
|
|
299
|
+
this.inputLines = [""];
|
|
300
|
+
this.inputCursor = 0;
|
|
301
|
+
this.render();
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
if (s === "\u001b[A") {
|
|
305
|
+
this.scrollUp(2);
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
if (s === "\u001b[B") {
|
|
309
|
+
this.scrollDown(2);
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
if (s === "\u001b[C") {
|
|
313
|
+
// right arrow: noop for now
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
if (s === "\u001b[D") {
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
if (s === "\u000c") {
|
|
320
|
+
// Ctrl+L: redraw
|
|
321
|
+
this.render();
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
if (s === "\u0004") {
|
|
325
|
+
// Ctrl+D: exit
|
|
326
|
+
this.exit();
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
if (s === "\r" || s === "\n") {
|
|
330
|
+
this.submitLine();
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
if (s === "\u0008" || s === "\u007f") {
|
|
334
|
+
// backspace
|
|
335
|
+
this.backspace();
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
if (s === "\t") {
|
|
339
|
+
this.cycleProvider();
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
if (s === "\u001b\r" || s === "\u001b\n") {
|
|
343
|
+
// Alt+Enter: newline
|
|
344
|
+
this.insertText("\n");
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
if (s.length === 1 && s >= " ") {
|
|
348
|
+
this.insertText(s);
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
this.render();
|
|
352
|
+
}
|
|
353
|
+
insertText(s) {
|
|
354
|
+
const line = this.inputLines[this.inputLines.length - 1] ?? "";
|
|
355
|
+
this.inputLines[this.inputLines.length - 1] = line + s;
|
|
356
|
+
this.inputCursor++;
|
|
357
|
+
}
|
|
358
|
+
backspace() {
|
|
359
|
+
const last = this.inputLines.length - 1;
|
|
360
|
+
const line = this.inputLines[last] ?? "";
|
|
361
|
+
if (line.length > 0) {
|
|
362
|
+
this.inputLines[last] = line.slice(0, -1);
|
|
363
|
+
}
|
|
364
|
+
else if (this.inputLines.length > 1) {
|
|
365
|
+
this.inputLines.pop();
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
submitLine() {
|
|
369
|
+
const text = this.inputLines.join("\n").trim();
|
|
370
|
+
this.inputLines = [""];
|
|
371
|
+
this.inputCursor = 0;
|
|
372
|
+
if (!text)
|
|
373
|
+
return;
|
|
374
|
+
void this.handleInput(text);
|
|
375
|
+
}
|
|
376
|
+
cycleProvider() {
|
|
377
|
+
const names = this.router.getProviderNames();
|
|
378
|
+
if (names.length === 0) {
|
|
379
|
+
this.showSystem("No providers available.");
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
const current = this.router.getActiveProvider();
|
|
383
|
+
const idx = current ? names.indexOf(current) : -1;
|
|
384
|
+
const next = names[(idx + 1) % names.length];
|
|
385
|
+
this.router.setActiveProvider(next);
|
|
386
|
+
this.showSystem(`Provider: ${next}`);
|
|
387
|
+
}
|
|
388
|
+
async handleInput(text) {
|
|
389
|
+
if (text.startsWith("/")) {
|
|
390
|
+
await this.handleCommand(text);
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
if (this.arenaMode) {
|
|
394
|
+
await this.runArena(text);
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
this.messages.push({ role: "user", content: text });
|
|
398
|
+
this.session.add({ role: "user", content: text });
|
|
399
|
+
this.scrollOffset = 0;
|
|
400
|
+
this.render();
|
|
401
|
+
this.startSpinner();
|
|
402
|
+
const history = this.session.messages.filter((m) => m.role !== "system");
|
|
403
|
+
let assistantText = "";
|
|
404
|
+
let toolCalls = [];
|
|
405
|
+
try {
|
|
406
|
+
for await (const chunk of this.agent.run(text, history)) {
|
|
407
|
+
this.handleStreamChunk(chunk, { assistantText, toolCalls });
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
catch (err) {
|
|
411
|
+
const msg = err.message;
|
|
412
|
+
this.messages.push({ role: "system", content: `[error] ${msg}` });
|
|
413
|
+
}
|
|
414
|
+
finally {
|
|
415
|
+
this.stopSpinner();
|
|
416
|
+
}
|
|
417
|
+
const finalText = assistantText;
|
|
418
|
+
if (finalText.trim()) {
|
|
419
|
+
this.messages.push({ role: "assistant", content: finalText });
|
|
420
|
+
this.session.add({ role: "assistant", content: finalText, tool_calls: toolCalls.length ? toolCalls : undefined });
|
|
421
|
+
}
|
|
422
|
+
this.scrollOffset = 0;
|
|
423
|
+
this.render();
|
|
424
|
+
}
|
|
425
|
+
handleStreamChunk(chunk, state) {
|
|
426
|
+
if (chunk.type === "text" && chunk.text) {
|
|
427
|
+
state.assistantText += chunk.text;
|
|
428
|
+
this.messages.push({ role: "assistant", content: chunk.text });
|
|
429
|
+
}
|
|
430
|
+
else if (chunk.type === "tool_call" && chunk.tool_call) {
|
|
431
|
+
state.toolCalls.push(chunk.tool_call);
|
|
432
|
+
const name = chunk.tool_call.function.name;
|
|
433
|
+
this.messages.push({
|
|
434
|
+
role: "tool",
|
|
435
|
+
content: `[Tool: ${name}]`,
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
else if (chunk.type === "error" && chunk.error) {
|
|
439
|
+
this.messages.push({ role: "system", content: `[error] ${chunk.error}` });
|
|
440
|
+
}
|
|
441
|
+
this.render();
|
|
442
|
+
}
|
|
443
|
+
async runArena(text) {
|
|
444
|
+
this.messages.push({ role: "user", content: text });
|
|
445
|
+
this.session.add({ role: "user", content: text });
|
|
446
|
+
this.scrollOffset = 0;
|
|
447
|
+
this.render();
|
|
448
|
+
this.startSpinner();
|
|
449
|
+
const history = this.session.messages.filter((m) => m.role !== "system");
|
|
450
|
+
const modelIds = this.allModelIds();
|
|
451
|
+
if (modelIds.length < 2) {
|
|
452
|
+
this.stopSpinner();
|
|
453
|
+
this.showSystem(`Arena needs at least 2 models. Found: ${modelIds.join(", ") || "(none)"}`);
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
456
|
+
const results = await this.arena.compare(text, history, modelIds, (modelId, chunk) => {
|
|
457
|
+
if (chunk.type === "text" && chunk.text) {
|
|
458
|
+
this.messages.push({
|
|
459
|
+
role: "system",
|
|
460
|
+
content: `${FG.brightCyan}[${modelId}]${RESET} ${chunk.text}`,
|
|
461
|
+
});
|
|
462
|
+
this.render();
|
|
463
|
+
}
|
|
464
|
+
});
|
|
465
|
+
this.stopSpinner();
|
|
466
|
+
this.messages.push({ role: "system", content: this.arena.render(results) });
|
|
467
|
+
this.render();
|
|
468
|
+
}
|
|
469
|
+
allModelIds() {
|
|
470
|
+
const ids = new Set();
|
|
471
|
+
for (const cfg of this.router.configs) {
|
|
472
|
+
if (!cfg.enabled)
|
|
473
|
+
continue;
|
|
474
|
+
for (const m of cfg.models)
|
|
475
|
+
ids.add(m);
|
|
476
|
+
}
|
|
477
|
+
return Array.from(ids);
|
|
478
|
+
}
|
|
479
|
+
async handleCommand(text) {
|
|
480
|
+
const { command, args } = parseCommand(text);
|
|
481
|
+
if (!command)
|
|
482
|
+
return;
|
|
483
|
+
const ctx = {
|
|
484
|
+
tui: this,
|
|
485
|
+
agent: this.agent,
|
|
486
|
+
router: this.router,
|
|
487
|
+
session: this.session,
|
|
488
|
+
arena: this.arena,
|
|
489
|
+
settings: this.settings,
|
|
490
|
+
};
|
|
491
|
+
const handler = CommandHandler[command];
|
|
492
|
+
if (!handler) {
|
|
493
|
+
this.showSystem(`Unknown command: /${command}. Type /help for available commands.`);
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
496
|
+
try {
|
|
497
|
+
const result = await handler.run(args, ctx);
|
|
498
|
+
if (typeof result === "string") {
|
|
499
|
+
this.showSystem(result);
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
catch (err) {
|
|
503
|
+
this.showSystem(`Command error: ${err.message}`);
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
async handleLine(line) {
|
|
507
|
+
const text = line.trim();
|
|
508
|
+
if (!text)
|
|
509
|
+
return;
|
|
510
|
+
if (text.startsWith("/")) {
|
|
511
|
+
await this.handleCommand(text);
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
514
|
+
if (this.arenaMode) {
|
|
515
|
+
await this.runArena(text);
|
|
516
|
+
return;
|
|
517
|
+
}
|
|
518
|
+
this.messages.push({ role: "user", content: text });
|
|
519
|
+
this.session.add({ role: "user", content: text });
|
|
520
|
+
let assistantText = "";
|
|
521
|
+
const history = this.session.messages.filter((m) => m.role !== "system");
|
|
522
|
+
try {
|
|
523
|
+
for await (const chunk of this.agent.run(text, history)) {
|
|
524
|
+
if (chunk.type === "text" && chunk.text) {
|
|
525
|
+
assistantText += chunk.text;
|
|
526
|
+
process.stdout.write(chunk.text);
|
|
527
|
+
}
|
|
528
|
+
else if (chunk.type === "tool_call" && chunk.tool_call) {
|
|
529
|
+
process.stdout.write(`\n[Tool: ${chunk.tool_call.function.name}]\n`);
|
|
530
|
+
}
|
|
531
|
+
else if (chunk.type === "error" && chunk.error) {
|
|
532
|
+
process.stderr.write(`[error] ${chunk.error}\n`);
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
catch (err) {
|
|
537
|
+
process.stderr.write(`error: ${err.message}\n`);
|
|
538
|
+
}
|
|
539
|
+
if (assistantText.trim()) {
|
|
540
|
+
this.messages.push({ role: "assistant", content: assistantText });
|
|
541
|
+
this.session.add({ role: "assistant", content: assistantText });
|
|
542
|
+
}
|
|
543
|
+
process.stdout.write("\n");
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
export function createTUI(opts) {
|
|
547
|
+
return new TUI(opts);
|
|
548
|
+
}
|
|
549
|
+
export async function runTUI() {
|
|
550
|
+
const cfg = getConfig();
|
|
551
|
+
const router = new Router(cfg.providers, new HealthTracker());
|
|
552
|
+
const registry = new ToolRegistry();
|
|
553
|
+
for (const make of [makeReadFileTool, makeWriteFileTool, makeEditFileTool, makeListDirTool, makeBashTool]) {
|
|
554
|
+
const tool = make(process.cwd());
|
|
555
|
+
registry.register(tool.def, tool.execute);
|
|
556
|
+
}
|
|
557
|
+
const agent = new Agent(router, registry);
|
|
558
|
+
const session = new Session();
|
|
559
|
+
const arena = new Arena(router);
|
|
560
|
+
const tui = new TUI({ agent, router, session, arena });
|
|
561
|
+
tui.start();
|
|
562
|
+
}
|
package/dist/types.js
ADDED
package/docs/preview.txt
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
Commands:
|
|
2
|
+
/help - Show all available commands
|
|
3
|
+
/model <name> - Switch the active model
|
|
4
|
+
/provider <name> - Switch the active provider
|
|
5
|
+
/models - List all available models across providers
|
|
6
|
+
/providers - Show provider health status
|
|
7
|
+
/session save [name] | load <name> | list | clear - Manage sessions
|
|
8
|
+
/arena - Enter arena mode (compare models)
|
|
9
|
+
/exit - Exit the TUI
|
|
10
|
+
/cost - Show token usage and estimated cost summary
|
|
11
|
+
/reset-cost - Reset cost and token counters
|
|
12
|
+
/settings [set <key> <value> | reset] - View or modify settings
|
|
13
|
+
/stats - Show session stats, cost summary, and provider health
|
|
14
|
+
/combo list | create <name> [provider...] | select <name> | delete <name> - Manage provider/model combos
|
|
15
|
+
/quit - Exit the TUI
|
|
16
|
+
/reset-health - Reset health state for all providers
|
|
17
|
+
/skills - List available custom skills
|
|
18
|
+
/skill <name> [args] - Run a custom skill
|
|
19
|
+
/connect <provider> <api_key> - Connect an API key for a provider
|
|
20
|
+
/keys - List which providers have API keys configured
|
|
21
|
+
/disconnect <provider> - Remove an API key
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
=== /stats ===
|
|
25
|
+
Session:
|
|
26
|
+
messages: 0 (user: 0, assistant: 0, tool: 0)
|
|
27
|
+
|
|
28
|
+
Cost:
|
|
29
|
+
Provider Model Requests Input Tok Output Tok Cost (USD)
|
|
30
|
+
-------- ----- -------- --------- ---------- ----------
|
|
31
|
+
- - - - - -
|
|
32
|
+
Total 0.000000
|
|
33
|
+
|
|
34
|
+
Provider health:
|
|
35
|
+
ollama-local: unhealthy (fetch failed)
|
|
36
|
+
openrouter-free: healthy
|
|
37
|
+
groq: unhealthy (HTTP 401)
|
|
38
|
+
mistral: unhealthy (fetch failed)
|
|
39
|
+
cohere: unhealthy (HTTP 401)
|
|
40
|
+
huggingface: unhealthy (fetch failed)
|
|
41
|
+
fireworks: unhealthy (HTTP 401)
|
|
42
|
+
together: unhealthy (HTTP 401)
|
|
43
|
+
deepseek: unhealthy (HTTP 401)
|
|
44
|
+
gemini: unhealthy (HTTP 404)
|
|
45
|
+
xai: unhealthy (HTTP 401)
|
|
46
|
+
perplexity: unhealthy (HTTP 404)
|
|
47
|
+
cerebras: unhealthy (HTTP 403)
|
|
48
|
+
nvidia: healthy
|
|
49
|
+
jina: healthy
|
|
50
|
+
parasail: unhealthy (fetch failed)
|
|
51
|
+
featherless: unhealthy (HTTP 404)
|