blun-king-cli 7.1.0 → 7.2.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/lib/chat.js +480 -243
- package/lib/ui.js +21 -0
- package/package.json +1 -1
package/lib/chat.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* BLUN King CLI v7.
|
|
3
|
-
* Full-screen
|
|
4
|
-
*
|
|
2
|
+
* BLUN King CLI v7.2.0 - Chat Module
|
|
3
|
+
* Full-screen TUI with ANSI Scroll Regions
|
|
4
|
+
* Fixed input panel at bottom, responses scroll above
|
|
5
5
|
* CommonJS
|
|
6
6
|
*/
|
|
7
7
|
|
|
@@ -13,7 +13,17 @@ var client = require("./client");
|
|
|
13
13
|
|
|
14
14
|
var localMode = false;
|
|
15
15
|
|
|
16
|
-
// ──
|
|
16
|
+
// ── ANSI Escape Helpers ──────────────────────────────────
|
|
17
|
+
var ESC = "\x1b[";
|
|
18
|
+
function cursorTo(row, col) { return ESC + row + ";" + col + "H"; }
|
|
19
|
+
function clearLine() { return ESC + "2K"; }
|
|
20
|
+
function setScrollRegion(top, bottom) { return ESC + top + ";" + bottom + "r"; }
|
|
21
|
+
function saveCursor() { return ESC + "s"; }
|
|
22
|
+
function restoreCursor() { return ESC + "u"; }
|
|
23
|
+
function hideCursor() { return ESC + "?25l"; }
|
|
24
|
+
function showCursor() { return ESC + "?25h"; }
|
|
25
|
+
|
|
26
|
+
// ── Slash Commands ───────────────────────────────────────
|
|
17
27
|
var SLASH_COMMANDS = [
|
|
18
28
|
["/help", "Show all commands"],
|
|
19
29
|
["/settings", "Open interactive settings panel"],
|
|
@@ -29,6 +39,12 @@ var SLASH_COMMANDS = [
|
|
|
29
39
|
];
|
|
30
40
|
var slashNames = SLASH_COMMANDS.map(function (c) { return c[0]; });
|
|
31
41
|
|
|
42
|
+
// ── Layout Constants ─────────────────────────────────────
|
|
43
|
+
var INPUT_PANEL_HEIGHT = 5; // top-frame + input + blank + status + bottom-frame
|
|
44
|
+
|
|
45
|
+
function getRows() { return process.stdout.rows || 24; }
|
|
46
|
+
function getCols() { return process.stdout.columns || 80; }
|
|
47
|
+
|
|
32
48
|
function startChat() {
|
|
33
49
|
ui.renderBanner();
|
|
34
50
|
|
|
@@ -53,16 +69,12 @@ function startChat() {
|
|
|
53
69
|
return authPromise.then(function () {
|
|
54
70
|
var creds = auth.getStoredCredentials();
|
|
55
71
|
if (creds) {
|
|
56
|
-
|
|
57
|
-
"Connected as " + (creds.name || creds.email || "user") + " | Plan: " + (creds.plan || "free"),
|
|
58
|
-
"system"
|
|
59
|
-
);
|
|
72
|
+
console.log(chalk.yellow(" [SYS] Connected as " + (creds.name || creds.email || "user") + " | Plan: " + (creds.plan || "free")));
|
|
60
73
|
} else if (localMode) {
|
|
61
|
-
|
|
74
|
+
console.log(chalk.yellow(" [SYS] Local mode - no API calls available"));
|
|
62
75
|
}
|
|
63
76
|
|
|
64
77
|
var settings = auth.getSettings();
|
|
65
|
-
// Ensure all settings fields exist
|
|
66
78
|
if (!settings.theme) settings.theme = "neon";
|
|
67
79
|
if (!settings.streamLevel) settings.streamLevel = "normal";
|
|
68
80
|
if (!settings.permissionMode) settings.permissionMode = "normal";
|
|
@@ -70,173 +82,193 @@ function startChat() {
|
|
|
70
82
|
if (!settings.statuslineMode) settings.statuslineMode = "full";
|
|
71
83
|
|
|
72
84
|
ui.setTheme(settings.theme || "neon");
|
|
73
|
-
|
|
74
|
-
var username = (creds ? creds.name : null) || "local";
|
|
75
85
|
var model = settings.model || "auto";
|
|
76
|
-
|
|
77
|
-
var
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
function showPrompt() {
|
|
92
|
-
ui.renderInputPanel(settings);
|
|
93
|
-
rl.setPrompt(ui.renderPrompt());
|
|
94
|
-
rl.prompt();
|
|
86
|
+
var inputBuffer = "";
|
|
87
|
+
var cursorPos = 0;
|
|
88
|
+
var isInMenu = false;
|
|
89
|
+
|
|
90
|
+
// ── Setup full-screen layout ─────────────────────────
|
|
91
|
+
function setupLayout() {
|
|
92
|
+
var rows = getRows();
|
|
93
|
+
var scrollBottom = rows - INPUT_PANEL_HEIGHT;
|
|
94
|
+
if (scrollBottom < 3) scrollBottom = 3;
|
|
95
|
+
// Set scroll region to top area only
|
|
96
|
+
process.stdout.write(setScrollRegion(1, scrollBottom));
|
|
97
|
+
// Move cursor to scroll area to print initial content
|
|
98
|
+
process.stdout.write(cursorTo(scrollBottom, 1));
|
|
95
99
|
}
|
|
96
100
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
var
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
101
|
+
function drawInputPanel() {
|
|
102
|
+
var rows = getRows();
|
|
103
|
+
var cols = getCols();
|
|
104
|
+
var t = ui.getTheme();
|
|
105
|
+
var panelTop = rows - INPUT_PANEL_HEIGHT + 1;
|
|
106
|
+
var inner = Math.min(cols - 2, 96);
|
|
107
|
+
|
|
108
|
+
// Permission label
|
|
109
|
+
var perm = settings.permissionMode || "normal";
|
|
110
|
+
var permColors = { safe: chalk.green, normal: chalk.yellow, god: chalk.red };
|
|
111
|
+
var permLabel = (permColors[perm] || chalk.white)(perm);
|
|
112
|
+
|
|
113
|
+
var stats = ui.getStats();
|
|
114
|
+
var tokens = ui.formatNumber(stats.tokens);
|
|
115
|
+
var elapsed = ui.formatDuration(stats.uptime);
|
|
116
|
+
var cost = ui.estimateCost(stats.tokens, settings.model);
|
|
117
|
+
|
|
118
|
+
// Build status parts
|
|
119
|
+
var statusParts = [];
|
|
120
|
+
statusParts.push(permLabel);
|
|
121
|
+
statusParts.push(chalk.dim("model:") + chalk.white(settings.model || "auto"));
|
|
122
|
+
statusParts.push(chalk.dim("tokens:") + chalk.white(tokens));
|
|
123
|
+
statusParts.push(chalk.dim(elapsed));
|
|
124
|
+
if (settings.costVisibility !== false) {
|
|
125
|
+
statusParts.push(chalk.dim(cost));
|
|
115
126
|
}
|
|
127
|
+
var statusLine = statusParts.join(chalk.dim(" │ "));
|
|
128
|
+
|
|
129
|
+
// Save cursor position in scroll region
|
|
130
|
+
process.stdout.write(saveCursor());
|
|
131
|
+
|
|
132
|
+
// Row 1: Top frame
|
|
133
|
+
process.stdout.write(cursorTo(panelTop, 1) + clearLine());
|
|
134
|
+
process.stdout.write(t.frame("╭─") + t.frame.bold(" BLUN ") + t.frame("─".repeat(Math.max(2, inner - 10))) + t.frame("╮"));
|
|
135
|
+
|
|
136
|
+
// Row 2: Input line
|
|
137
|
+
var displayInput = inputBuffer || "";
|
|
138
|
+
var maxInputLen = inner - 4;
|
|
139
|
+
var showInput = displayInput.length > maxInputLen ? displayInput.slice(displayInput.length - maxInputLen) : displayInput;
|
|
140
|
+
process.stdout.write(cursorTo(panelTop + 1, 1) + clearLine());
|
|
141
|
+
process.stdout.write(t.frame("│ ") + chalk.bold.green("> ") + chalk.white(showInput) + " ".repeat(Math.max(0, maxInputLen - showInput.length)) + t.frame(" │"));
|
|
142
|
+
|
|
143
|
+
// Row 3: Empty separator
|
|
144
|
+
process.stdout.write(cursorTo(panelTop + 2, 1) + clearLine());
|
|
145
|
+
process.stdout.write(t.frame("│") + " ".repeat(inner) + t.frame("│"));
|
|
146
|
+
|
|
147
|
+
// Row 4: Status line
|
|
148
|
+
process.stdout.write(cursorTo(panelTop + 3, 1) + clearLine());
|
|
149
|
+
process.stdout.write(t.frame("│ ") + statusLine);
|
|
150
|
+
|
|
151
|
+
// Row 5: Bottom frame
|
|
152
|
+
process.stdout.write(cursorTo(panelTop + 4, 1) + clearLine());
|
|
153
|
+
process.stdout.write(t.frame("╰─ ") + chalk.dim("Tab: autocomplete /: commands Esc: exit") + t.frame(" ─") + t.frame("─".repeat(Math.max(1, inner - 48))) + t.frame("╯"));
|
|
154
|
+
|
|
155
|
+
// Restore cursor back to input line
|
|
156
|
+
var cursorCol = 5 + cursorPos; // "│ > " = 4 chars + 1
|
|
157
|
+
if (cursorPos > maxInputLen) cursorCol = 5 + maxInputLen;
|
|
158
|
+
process.stdout.write(cursorTo(panelTop + 1, cursorCol));
|
|
159
|
+
}
|
|
116
160
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
if (field.key === "theme") ui.setTheme(settings.theme);
|
|
129
|
-
auth.saveSettings(settings);
|
|
130
|
-
render();
|
|
131
|
-
} else if (key.name === "left") {
|
|
132
|
-
var field = ui.SETTINGS_FIELDS[selection];
|
|
133
|
-
settings = ui.cycleSetting(settings, field.key, -1);
|
|
134
|
-
if (field.key === "theme") ui.setTheme(settings.theme);
|
|
135
|
-
auth.saveSettings(settings);
|
|
136
|
-
render();
|
|
137
|
-
} else if (key.name === "return" || key.name === "escape") {
|
|
138
|
-
cleanup();
|
|
139
|
-
} else if (key.ctrl && key.name === "c") {
|
|
140
|
-
cleanup();
|
|
141
|
-
}
|
|
161
|
+
function printToScrollArea(text) {
|
|
162
|
+
var rows = getRows();
|
|
163
|
+
var scrollBottom = rows - INPUT_PANEL_HEIGHT;
|
|
164
|
+
// Save cursor, move to scroll area, print, restore
|
|
165
|
+
process.stdout.write(saveCursor());
|
|
166
|
+
process.stdout.write(setScrollRegion(1, scrollBottom));
|
|
167
|
+
process.stdout.write(cursorTo(scrollBottom, 1));
|
|
168
|
+
// Print text - each line scrolls the region up
|
|
169
|
+
var lines = text.split("\n");
|
|
170
|
+
for (var i = 0; i < lines.length; i++) {
|
|
171
|
+
process.stdout.write("\n" + lines[i]);
|
|
142
172
|
}
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
rl.pause();
|
|
146
|
-
readline.emitKeypressEvents(process.stdin);
|
|
147
|
-
if (process.stdin.isTTY && process.stdin.setRawMode) process.stdin.setRawMode(true);
|
|
148
|
-
process.stdin.on("keypress", onKeypress);
|
|
149
|
-
render();
|
|
173
|
+
process.stdout.write(restoreCursor());
|
|
174
|
+
drawInputPanel();
|
|
150
175
|
}
|
|
151
176
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
var input = line.trim();
|
|
158
|
-
|
|
159
|
-
if (!input) {
|
|
160
|
-
showPrompt();
|
|
161
|
-
return;
|
|
162
|
-
}
|
|
177
|
+
// ── Handle resize ────────────────────────────────────
|
|
178
|
+
process.stdout.on("resize", function () {
|
|
179
|
+
setupLayout();
|
|
180
|
+
drawInputPanel();
|
|
181
|
+
});
|
|
163
182
|
|
|
164
|
-
|
|
183
|
+
// ── Process command ──────────────────────────────────
|
|
184
|
+
function processCommand(input) {
|
|
185
|
+
if (!input) return;
|
|
165
186
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
187
|
+
// ── Commands ───────────────────────────────────
|
|
188
|
+
if (input === "/exit" || input === "/quit") {
|
|
189
|
+
cleanup();
|
|
190
|
+
console.log(chalk.yellow("\n Goodbye!\n"));
|
|
191
|
+
process.exit(0);
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
172
194
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
195
|
+
if (input === "/clear") {
|
|
196
|
+
var rows = getRows();
|
|
197
|
+
var scrollBottom = rows - INPUT_PANEL_HEIGHT;
|
|
198
|
+
process.stdout.write(setScrollRegion(1, scrollBottom));
|
|
199
|
+
for (var i = 1; i <= scrollBottom; i++) {
|
|
200
|
+
process.stdout.write(cursorTo(i, 1) + clearLine());
|
|
178
201
|
}
|
|
202
|
+
process.stdout.write(cursorTo(1, 1));
|
|
203
|
+
drawInputPanel();
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
179
206
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
207
|
+
if (input === "/help") {
|
|
208
|
+
var helpLines = [];
|
|
209
|
+
helpLines.push(chalk.cyan.bold(" Commands"));
|
|
210
|
+
helpLines.push(chalk.dim(" " + "─".repeat(40)));
|
|
211
|
+
SLASH_COMMANDS.forEach(function (c) {
|
|
212
|
+
helpLines.push(" " + chalk.green(c[0]) + " ".repeat(Math.max(1, 16 - c[0].length)) + chalk.gray(c[1]));
|
|
213
|
+
});
|
|
214
|
+
helpLines.push("");
|
|
215
|
+
printToScrollArea(helpLines.join("\n"));
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
184
218
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
}
|
|
219
|
+
if (input === "/status") {
|
|
220
|
+
var stats = ui.getStats();
|
|
221
|
+
var statusLines = [];
|
|
222
|
+
statusLines.push(chalk.cyan.bold(" Status"));
|
|
223
|
+
statusLines.push(chalk.dim(" " + "─".repeat(40)));
|
|
224
|
+
statusLines.push(" Model: " + chalk.white(settings.model || "auto"));
|
|
225
|
+
statusLines.push(" Permission: " + ui.formatSettingValue({ key: "permissionMode" }, settings.permissionMode));
|
|
226
|
+
statusLines.push(" Theme: " + chalk.white(settings.theme || "neon"));
|
|
227
|
+
statusLines.push(" Tokens: " + chalk.white(ui.formatNumber(stats.tokens)));
|
|
228
|
+
statusLines.push(" Cost: " + chalk.white(ui.estimateCost(stats.tokens, settings.model)));
|
|
229
|
+
statusLines.push(" Uptime: " + chalk.white(ui.formatDuration(stats.uptime)));
|
|
230
|
+
statusLines.push("");
|
|
231
|
+
printToScrollArea(statusLines.join("\n"));
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
201
234
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
235
|
+
if (input === "/perm" || input === "/permission") {
|
|
236
|
+
var modes = ui.PERMISSION_MODES;
|
|
237
|
+
var idx = modes.indexOf(settings.permissionMode);
|
|
238
|
+
settings.permissionMode = modes[(idx + 1) % modes.length];
|
|
239
|
+
auth.saveSettings(settings);
|
|
240
|
+
printToScrollArea(chalk.yellow(" [SYS] Permission: " + settings.permissionMode));
|
|
241
|
+
drawInputPanel();
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
211
244
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
var modes = Object.keys(ui.THEMES);
|
|
221
|
-
var idx = modes.indexOf(settings.theme || "neon");
|
|
222
|
-
settings.theme = modes[(idx + 1) % modes.length];
|
|
223
|
-
ui.setTheme(settings.theme);
|
|
224
|
-
auth.saveSettings(settings);
|
|
225
|
-
ui.renderResponse("Theme: " + settings.theme, "system");
|
|
226
|
-
}
|
|
227
|
-
showPrompt();
|
|
228
|
-
return;
|
|
245
|
+
if (input.indexOf("/theme") === 0) {
|
|
246
|
+
var parts = input.split(/\s+/);
|
|
247
|
+
if (parts[1] && ui.THEMES[parts[1]]) {
|
|
248
|
+
settings.theme = parts[1];
|
|
249
|
+
} else {
|
|
250
|
+
var themeNames = Object.keys(ui.THEMES);
|
|
251
|
+
var tidx = themeNames.indexOf(settings.theme || "neon");
|
|
252
|
+
settings.theme = themeNames[(tidx + 1) % themeNames.length];
|
|
229
253
|
}
|
|
254
|
+
ui.setTheme(settings.theme);
|
|
255
|
+
auth.saveSettings(settings);
|
|
256
|
+
printToScrollArea(chalk.yellow(" [SYS] Theme: " + settings.theme));
|
|
257
|
+
drawInputPanel();
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
230
260
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
261
|
+
if (input.indexOf("/model") === 0) {
|
|
262
|
+
var parts = input.split(/\s+/);
|
|
263
|
+
if (parts[1]) {
|
|
264
|
+
settings.model = parts[1];
|
|
265
|
+
model = parts[1];
|
|
266
|
+
auth.saveSettings(settings);
|
|
267
|
+
printToScrollArea(chalk.yellow(" [SYS] Model: " + parts[1]));
|
|
268
|
+
drawInputPanel();
|
|
269
|
+
} else {
|
|
270
|
+
// Open inquirer menu
|
|
271
|
+
enterMenuMode(function (done) {
|
|
240
272
|
var inquirer = require("inquirer");
|
|
241
273
|
inquirer.prompt([{
|
|
242
274
|
type: "list",
|
|
@@ -253,57 +285,24 @@ function startChat() {
|
|
|
253
285
|
model = ans.model;
|
|
254
286
|
settings.model = ans.model;
|
|
255
287
|
auth.saveSettings(settings);
|
|
256
|
-
|
|
288
|
+
printToScrollArea(chalk.yellow(" [SYS] Model: " + ans.model));
|
|
257
289
|
}
|
|
258
|
-
|
|
290
|
+
done();
|
|
259
291
|
});
|
|
260
|
-
}
|
|
261
|
-
return;
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
if (input === "/login") {
|
|
265
|
-
auth.login().then(function () {
|
|
266
|
-
localMode = false;
|
|
267
|
-
creds = auth.getStoredCredentials();
|
|
268
|
-
username = (creds ? creds.name : null) || "local";
|
|
269
|
-
ui.renderResponse("Logged in!", "system");
|
|
270
|
-
}).catch(function (e) {
|
|
271
|
-
ui.renderResponse(e.message, "error");
|
|
272
|
-
}).then(function () { showPrompt(); });
|
|
273
|
-
return;
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
if (input === "/whoami") {
|
|
277
|
-
auth.whoami().catch(function (e) {
|
|
278
|
-
ui.renderResponse(e.message, "error");
|
|
279
|
-
}).then(function () { showPrompt(); });
|
|
280
|
-
return;
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
if (input === "/keys") {
|
|
284
|
-
auth.listApiKeys().catch(function (e) {
|
|
285
|
-
ui.renderResponse(e.message, "error");
|
|
286
|
-
}).then(function () { showPrompt(); });
|
|
287
|
-
return;
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
if (input === "/help") {
|
|
291
|
-
var t = ui.getTheme();
|
|
292
|
-
console.log();
|
|
293
|
-
console.log(t.header(" Commands"));
|
|
294
|
-
console.log(t.muted(" " + "\u2500".repeat(40)));
|
|
295
|
-
SLASH_COMMANDS.forEach(function (c) {
|
|
296
|
-
console.log(" " + chalk.green(c[0]) + " ".repeat(Math.max(1, 16 - c[0].length)) + chalk.gray(c[1]));
|
|
297
292
|
});
|
|
298
|
-
console.log();
|
|
299
|
-
console.log(t.muted(" Tip: Type / and press Tab for autocomplete"));
|
|
300
|
-
console.log();
|
|
301
|
-
showPrompt();
|
|
302
|
-
return;
|
|
303
293
|
}
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
if (input === "/settings") {
|
|
298
|
+
enterMenuMode(function (done) {
|
|
299
|
+
openSettingsPanel(done);
|
|
300
|
+
});
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
304
303
|
|
|
305
|
-
|
|
306
|
-
|
|
304
|
+
if (input === "/") {
|
|
305
|
+
enterMenuMode(function (done) {
|
|
307
306
|
var inquirer = require("inquirer");
|
|
308
307
|
inquirer.prompt([{
|
|
309
308
|
type: "list",
|
|
@@ -317,56 +316,294 @@ function startChat() {
|
|
|
317
316
|
]),
|
|
318
317
|
pageSize: 12,
|
|
319
318
|
}]).then(function (ans) {
|
|
319
|
+
done();
|
|
320
320
|
if (ans.cmd) {
|
|
321
|
-
|
|
322
|
-
} else {
|
|
323
|
-
showPrompt();
|
|
321
|
+
processCommand(ans.cmd);
|
|
324
322
|
}
|
|
325
323
|
});
|
|
324
|
+
});
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
if (input === "/login") {
|
|
329
|
+
enterMenuMode(function (done) {
|
|
330
|
+
auth.login().then(function () {
|
|
331
|
+
localMode = false;
|
|
332
|
+
creds = auth.getStoredCredentials();
|
|
333
|
+
printToScrollArea(chalk.yellow(" [SYS] Logged in!"));
|
|
334
|
+
}).catch(function (e) {
|
|
335
|
+
printToScrollArea(chalk.red(" ✘ " + e.message));
|
|
336
|
+
}).then(function () { done(); });
|
|
337
|
+
});
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
if (input === "/whoami") {
|
|
342
|
+
enterMenuMode(function (done) {
|
|
343
|
+
auth.whoami().catch(function (e) {
|
|
344
|
+
printToScrollArea(chalk.red(" ✘ " + e.message));
|
|
345
|
+
}).then(function () { done(); });
|
|
346
|
+
});
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
if (input === "/keys") {
|
|
351
|
+
enterMenuMode(function (done) {
|
|
352
|
+
auth.listApiKeys().catch(function (e) {
|
|
353
|
+
printToScrollArea(chalk.red(" ✘ " + e.message));
|
|
354
|
+
}).then(function () { done(); });
|
|
355
|
+
});
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// ── Send message ──────────────────────────────────
|
|
360
|
+
if (localMode) {
|
|
361
|
+
printToScrollArea(chalk.red(" ✘ Local mode - cannot send messages. Use /login to authenticate."));
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// Show user message in scroll area
|
|
366
|
+
printToScrollArea(chalk.bold.blue(" > " + input));
|
|
367
|
+
|
|
368
|
+
// Show thinking indicator
|
|
369
|
+
printToScrollArea(chalk.dim(" ⠋ Thinking..."));
|
|
370
|
+
|
|
371
|
+
client.sendMessage(input, { model: settings.model || "auto" })
|
|
372
|
+
.then(function (res) {
|
|
373
|
+
if (res.status === 200 && res.data) {
|
|
374
|
+
var text = res.data.answer || res.data.response || res.data.text || res.data.message || JSON.stringify(res.data);
|
|
375
|
+
var tok = res.data.tokens || res.data.usage || {};
|
|
376
|
+
var tokTotal = tok.total || tok.total_tokens || 0;
|
|
377
|
+
if (!tokTotal && text) {
|
|
378
|
+
tokTotal = Math.ceil(text.length / 4) + Math.ceil(input.length / 4);
|
|
379
|
+
}
|
|
380
|
+
if (tokTotal) ui.addTokens(tokTotal);
|
|
381
|
+
|
|
382
|
+
// Render markdown
|
|
383
|
+
var rendered = ui.renderMarkdown(text);
|
|
384
|
+
var lines = rendered.split("\n");
|
|
385
|
+
var output = [];
|
|
386
|
+
for (var i = 0; i < lines.length; i++) {
|
|
387
|
+
output.push(" " + lines[i]);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// Meta line
|
|
391
|
+
var metaParts = [];
|
|
392
|
+
metaParts.push(chalk.bgCyan.black(" " + (settings.model || "auto") + " "));
|
|
393
|
+
if (tokTotal) metaParts.push(chalk.dim("tokens:" + ui.formatNumber(tokTotal)));
|
|
394
|
+
metaParts.push(chalk.dim("total:" + ui.formatNumber(ui.getStats().tokens)));
|
|
395
|
+
if (settings.costVisibility !== false) metaParts.push(chalk.dim(ui.estimateCost(ui.getStats().tokens, settings.model)));
|
|
396
|
+
output.push(" " + metaParts.join(chalk.dim(" │ ")));
|
|
397
|
+
output.push("");
|
|
398
|
+
|
|
399
|
+
printToScrollArea(output.join("\n"));
|
|
400
|
+
} else if (res.status === 401 || res.status === 403) {
|
|
401
|
+
printToScrollArea(chalk.red(" ✘ Auth error. Use /login to re-authenticate."));
|
|
402
|
+
} else {
|
|
403
|
+
printToScrollArea(chalk.red(" ✘ Error: " + JSON.stringify(res.data)));
|
|
404
|
+
}
|
|
405
|
+
drawInputPanel();
|
|
406
|
+
})
|
|
407
|
+
.catch(function (e) {
|
|
408
|
+
printToScrollArea(chalk.red(" ✘ Request failed: " + e.message + "\n Try /login if auth expired."));
|
|
409
|
+
drawInputPanel();
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// ── Settings Panel (inquirer) ─────────────────────────
|
|
414
|
+
function openSettingsPanel(done) {
|
|
415
|
+
var inquirer = require("inquirer");
|
|
416
|
+
var t = ui.getTheme();
|
|
417
|
+
|
|
418
|
+
inquirer.prompt([{
|
|
419
|
+
type: "list",
|
|
420
|
+
name: "field",
|
|
421
|
+
message: "Change setting",
|
|
422
|
+
choices: ui.SETTINGS_FIELDS.map(function (f) {
|
|
423
|
+
return { name: f.label + " — " + ui.formatSettingValue(f, settings[f.key]), value: f.key };
|
|
424
|
+
}).concat([{ name: chalk.dim("Back"), value: "back" }]),
|
|
425
|
+
pageSize: 12,
|
|
426
|
+
}]).then(function (ans) {
|
|
427
|
+
if (ans.field === "back") {
|
|
428
|
+
done();
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
431
|
+
var field = null;
|
|
432
|
+
for (var i = 0; i < ui.SETTINGS_FIELDS.length; i++) {
|
|
433
|
+
if (ui.SETTINGS_FIELDS[i].key === ans.field) { field = ui.SETTINGS_FIELDS[i]; break; }
|
|
434
|
+
}
|
|
435
|
+
if (!field) { done(); return; }
|
|
436
|
+
|
|
437
|
+
return inquirer.prompt([{
|
|
438
|
+
type: "list",
|
|
439
|
+
name: "value",
|
|
440
|
+
message: field.label + " (" + (field.help || "") + ")",
|
|
441
|
+
choices: field.values.map(function (v) {
|
|
442
|
+
var isCurrent = settings[field.key] === v;
|
|
443
|
+
var label = String(v);
|
|
444
|
+
if (typeof v === "boolean") label = v ? "On" : "Off";
|
|
445
|
+
return { name: label + (isCurrent ? chalk.green(" (current)") : ""), value: v };
|
|
446
|
+
}),
|
|
447
|
+
}]).then(function (ans2) {
|
|
448
|
+
settings[field.key] = ans2.value;
|
|
449
|
+
if (field.key === "theme") ui.setTheme(settings.theme);
|
|
450
|
+
auth.saveSettings(settings);
|
|
451
|
+
printToScrollArea(chalk.yellow(" [SYS] " + field.label + ": " + ans2.value));
|
|
452
|
+
done();
|
|
453
|
+
});
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// ── Menu Mode (temporarily disable raw input) ────────
|
|
458
|
+
function enterMenuMode(fn) {
|
|
459
|
+
isInMenu = true;
|
|
460
|
+
// Reset scroll region for inquirer to work properly
|
|
461
|
+
var rows = getRows();
|
|
462
|
+
process.stdout.write(setScrollRegion(1, rows));
|
|
463
|
+
process.stdin.setRawMode(false);
|
|
464
|
+
process.stdin.removeAllListeners("data");
|
|
465
|
+
|
|
466
|
+
fn(function () {
|
|
467
|
+
// Re-enter TUI mode
|
|
468
|
+
isInMenu = false;
|
|
469
|
+
setupLayout();
|
|
470
|
+
drawInputPanel();
|
|
471
|
+
startRawInput();
|
|
472
|
+
});
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// ── Cleanup ──────────────────────────────────────────
|
|
476
|
+
function cleanup() {
|
|
477
|
+
var rows = getRows();
|
|
478
|
+
process.stdout.write(setScrollRegion(1, rows));
|
|
479
|
+
process.stdout.write(cursorTo(rows, 1));
|
|
480
|
+
process.stdout.write(showCursor());
|
|
481
|
+
try { process.stdin.setRawMode(false); } catch (e) {}
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// ── Raw Input Handler ────────────────────────────────
|
|
485
|
+
function startRawInput() {
|
|
486
|
+
if (!process.stdin.isTTY) return;
|
|
487
|
+
process.stdin.setRawMode(true);
|
|
488
|
+
process.stdin.resume();
|
|
489
|
+
process.stdin.setEncoding("utf8");
|
|
490
|
+
|
|
491
|
+
process.stdin.on("data", function onData(key) {
|
|
492
|
+
if (isInMenu) return;
|
|
493
|
+
|
|
494
|
+
// Ctrl+C / Ctrl+D
|
|
495
|
+
if (key === "\u0003" || key === "\u0004") {
|
|
496
|
+
cleanup();
|
|
497
|
+
console.log(chalk.yellow("\n Goodbye!\n"));
|
|
498
|
+
process.exit(0);
|
|
326
499
|
return;
|
|
327
500
|
}
|
|
328
501
|
|
|
329
|
-
//
|
|
330
|
-
if (
|
|
331
|
-
|
|
332
|
-
|
|
502
|
+
// Escape
|
|
503
|
+
if (key === "\u001b" && key.length === 1) {
|
|
504
|
+
cleanup();
|
|
505
|
+
console.log(chalk.yellow("\n Goodbye!\n"));
|
|
506
|
+
process.exit(0);
|
|
333
507
|
return;
|
|
334
508
|
}
|
|
335
509
|
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
.
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
510
|
+
// Enter
|
|
511
|
+
if (key === "\r" || key === "\n") {
|
|
512
|
+
var cmd = inputBuffer.trim();
|
|
513
|
+
inputBuffer = "";
|
|
514
|
+
cursorPos = 0;
|
|
515
|
+
if (cmd) {
|
|
516
|
+
processCommand(cmd);
|
|
517
|
+
}
|
|
518
|
+
drawInputPanel();
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
// Backspace
|
|
523
|
+
if (key === "\u007f" || key === "\b") {
|
|
524
|
+
if (cursorPos > 0) {
|
|
525
|
+
inputBuffer = inputBuffer.slice(0, cursorPos - 1) + inputBuffer.slice(cursorPos);
|
|
526
|
+
cursorPos--;
|
|
527
|
+
}
|
|
528
|
+
drawInputPanel();
|
|
529
|
+
return;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
// Tab - autocomplete
|
|
533
|
+
if (key === "\t") {
|
|
534
|
+
if (inputBuffer.startsWith("/")) {
|
|
535
|
+
var hits = slashNames.filter(function (c) { return c.indexOf(inputBuffer) === 0; });
|
|
536
|
+
if (hits.length === 1) {
|
|
537
|
+
inputBuffer = hits[0];
|
|
538
|
+
cursorPos = inputBuffer.length;
|
|
539
|
+
} else if (hits.length > 1) {
|
|
540
|
+
printToScrollArea(chalk.dim(" " + hits.join(" ")));
|
|
357
541
|
}
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
542
|
+
}
|
|
543
|
+
drawInputPanel();
|
|
544
|
+
return;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
// Arrow keys (escape sequences)
|
|
548
|
+
if (key.length > 1 && key[0] === "\u001b") {
|
|
549
|
+
// Left arrow
|
|
550
|
+
if (key === "\u001b[D") {
|
|
551
|
+
if (cursorPos > 0) cursorPos--;
|
|
552
|
+
drawInputPanel();
|
|
553
|
+
return;
|
|
554
|
+
}
|
|
555
|
+
// Right arrow
|
|
556
|
+
if (key === "\u001b[C") {
|
|
557
|
+
if (cursorPos < inputBuffer.length) cursorPos++;
|
|
558
|
+
drawInputPanel();
|
|
559
|
+
return;
|
|
560
|
+
}
|
|
561
|
+
// Home
|
|
562
|
+
if (key === "\u001b[H") {
|
|
563
|
+
cursorPos = 0;
|
|
564
|
+
drawInputPanel();
|
|
565
|
+
return;
|
|
566
|
+
}
|
|
567
|
+
// End
|
|
568
|
+
if (key === "\u001b[F") {
|
|
569
|
+
cursorPos = inputBuffer.length;
|
|
570
|
+
drawInputPanel();
|
|
571
|
+
return;
|
|
572
|
+
}
|
|
573
|
+
// Delete
|
|
574
|
+
if (key === "\u001b[3~") {
|
|
575
|
+
if (cursorPos < inputBuffer.length) {
|
|
576
|
+
inputBuffer = inputBuffer.slice(0, cursorPos) + inputBuffer.slice(cursorPos + 1);
|
|
577
|
+
}
|
|
578
|
+
drawInputPanel();
|
|
579
|
+
return;
|
|
580
|
+
}
|
|
581
|
+
// Ignore other escape sequences
|
|
582
|
+
return;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
// Normal character
|
|
586
|
+
inputBuffer = inputBuffer.slice(0, cursorPos) + key + inputBuffer.slice(cursorPos);
|
|
587
|
+
cursorPos += key.length;
|
|
588
|
+
drawInputPanel();
|
|
365
589
|
});
|
|
590
|
+
}
|
|
366
591
|
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
592
|
+
// ── Initialize TUI ───────────────────────────────────
|
|
593
|
+
console.log(chalk.dim("\n Type your message or / for commands. Tab for autocomplete.\n"));
|
|
594
|
+
|
|
595
|
+
// Wait a tick for all console.log output to flush, then enter TUI mode
|
|
596
|
+
return new Promise(function (resolveChat) {
|
|
597
|
+
setTimeout(function () {
|
|
598
|
+
setupLayout();
|
|
599
|
+
drawInputPanel();
|
|
600
|
+
startRawInput();
|
|
601
|
+
}, 100);
|
|
602
|
+
|
|
603
|
+
// Handle clean exit
|
|
604
|
+
process.on("SIGINT", function () {
|
|
605
|
+
cleanup();
|
|
606
|
+
process.exit(0);
|
|
370
607
|
});
|
|
371
608
|
});
|
|
372
609
|
});
|
package/lib/ui.js
CHANGED
|
@@ -355,6 +355,25 @@ function getStats() {
|
|
|
355
355
|
return { tokens: totalTokens, cost: totalCost, uptime: Date.now() - startTime };
|
|
356
356
|
}
|
|
357
357
|
|
|
358
|
+
// ── Box & Table (for non-chat CLI commands) ─────────────
|
|
359
|
+
function renderBox(title, content, color) {
|
|
360
|
+
var c = chalk[color] || chalk.cyan;
|
|
361
|
+
console.log();
|
|
362
|
+
console.log(c.bold(" " + title));
|
|
363
|
+
console.log(c(" " + "─".repeat(50)));
|
|
364
|
+
content.split("\n").forEach(function (line) {
|
|
365
|
+
console.log(" " + line);
|
|
366
|
+
});
|
|
367
|
+
console.log();
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function renderTable(headers, rows) {
|
|
371
|
+
var Table = require("cli-table3");
|
|
372
|
+
var t = new Table({ head: headers.map(function (h) { return chalk.cyan(h); }) });
|
|
373
|
+
rows.forEach(function (r) { t.push(r); });
|
|
374
|
+
console.log(t.toString());
|
|
375
|
+
}
|
|
376
|
+
|
|
358
377
|
module.exports = {
|
|
359
378
|
// Core
|
|
360
379
|
renderBanner: renderBanner,
|
|
@@ -385,4 +404,6 @@ module.exports = {
|
|
|
385
404
|
formatDuration: formatDuration,
|
|
386
405
|
PERMISSION_MODES: PERMISSION_MODES,
|
|
387
406
|
THEMES: THEMES,
|
|
407
|
+
renderBox: renderBox,
|
|
408
|
+
renderTable: renderTable,
|
|
388
409
|
};
|