@redonvn/cli 0.1.10 → 0.1.12
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/dist/api/agent.d.ts +7 -0
- package/dist/api/agent.js +18 -0
- package/dist/api/agent.js.map +1 -0
- package/dist/api/auth.d.ts +5 -0
- package/dist/api/auth.js +14 -0
- package/dist/api/auth.js.map +1 -0
- package/dist/api/chat.d.ts +22 -0
- package/dist/api/chat.js +231 -0
- package/dist/api/chat.js.map +1 -0
- package/dist/auth/store.d.ts +7 -0
- package/dist/auth/store.js +17 -0
- package/dist/auth/store.js.map +1 -1
- package/dist/build-info.d.ts +2 -2
- package/dist/build-info.js +3 -3
- package/dist/build-info.js.map +1 -1
- package/dist/cli/commands/ask.js +1066 -324
- package/dist/cli/commands/ask.js.map +1 -1
- package/dist/cli/commands/login.d.ts +0 -1
- package/dist/cli/commands/login.js +7 -18
- package/dist/cli/commands/login.js.map +1 -1
- package/dist/cli/commands/logs.d.ts +4 -0
- package/dist/cli/commands/logs.js +66 -0
- package/dist/cli/commands/logs.js.map +1 -0
- package/dist/cli/commands/mcp.d.ts +3 -16
- package/dist/cli/commands/mcp.js +191 -74
- package/dist/cli/commands/mcp.js.map +1 -1
- package/dist/cli/commands/start.d.ts +7 -1
- package/dist/cli/commands/start.js +61 -2
- package/dist/cli/commands/start.js.map +1 -1
- package/dist/cli/commands/status.js +3 -5
- package/dist/cli/commands/status.js.map +1 -1
- package/dist/cli/commands/stop.d.ts +1 -0
- package/dist/cli/commands/stop.js +53 -0
- package/dist/cli/commands/stop.js.map +1 -0
- package/dist/cli/index.js +42 -3
- package/dist/cli/index.js.map +1 -1
- package/dist/cli-router/detect.d.ts +2 -1
- package/dist/cli-router/detect.js +10 -3
- package/dist/cli-router/detect.js.map +1 -1
- package/dist/config.d.ts +1 -1
- package/dist/daemon/tunnel.js +4 -3
- package/dist/daemon/tunnel.js.map +1 -1
- package/dist/mcp/chrome-launcher.d.ts +14 -0
- package/dist/mcp/chrome-launcher.js +114 -0
- package/dist/mcp/chrome-launcher.js.map +1 -0
- package/dist/mcp/client.d.ts +1 -0
- package/dist/mcp/client.js +9 -2
- package/dist/mcp/client.js.map +1 -1
- package/dist/mcp/config.d.ts +17 -9
- package/dist/mcp/config.js +134 -23
- package/dist/mcp/config.js.map +1 -1
- package/package.json +4 -2
package/dist/cli/commands/ask.js
CHANGED
|
@@ -1,37 +1,4 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
-
if (k2 === undefined) k2 = k;
|
|
4
|
-
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
-
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
-
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
-
}
|
|
8
|
-
Object.defineProperty(o, k2, desc);
|
|
9
|
-
}) : (function(o, m, k, k2) {
|
|
10
|
-
if (k2 === undefined) k2 = k;
|
|
11
|
-
o[k2] = m[k];
|
|
12
|
-
}));
|
|
13
|
-
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
-
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
-
}) : function(o, v) {
|
|
16
|
-
o["default"] = v;
|
|
17
|
-
});
|
|
18
|
-
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
-
var ownKeys = function(o) {
|
|
20
|
-
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
-
var ar = [];
|
|
22
|
-
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
-
return ar;
|
|
24
|
-
};
|
|
25
|
-
return ownKeys(o);
|
|
26
|
-
};
|
|
27
|
-
return function (mod) {
|
|
28
|
-
if (mod && mod.__esModule) return mod;
|
|
29
|
-
var result = {};
|
|
30
|
-
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
-
__setModuleDefault(result, mod);
|
|
32
|
-
return result;
|
|
33
|
-
};
|
|
34
|
-
})();
|
|
35
2
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
4
|
};
|
|
@@ -45,11 +12,15 @@ exports.askCommand = askCommand;
|
|
|
45
12
|
*/
|
|
46
13
|
const chalk_1 = __importDefault(require("chalk"));
|
|
47
14
|
const readline_1 = __importDefault(require("readline"));
|
|
48
|
-
const axios_1 = __importDefault(require("axios"));
|
|
49
15
|
const store_1 = require("../../auth/store");
|
|
50
16
|
const permissions_1 = require("../../auth/permissions");
|
|
51
17
|
const permissions_2 = require("../../auth/permissions");
|
|
52
18
|
const config_1 = require("../../config");
|
|
19
|
+
const agent_1 = require("../../api/agent");
|
|
20
|
+
const chat_1 = require("../../api/chat");
|
|
21
|
+
const config_2 = require("../../mcp/config");
|
|
22
|
+
const client_1 = require("../../mcp/client");
|
|
23
|
+
const chrome_launcher_1 = require("../../mcp/chrome-launcher");
|
|
53
24
|
// ─── Session state ─────────────────────────────────────────────────────────────
|
|
54
25
|
let currentThreadId = null;
|
|
55
26
|
let currentAgentId = null;
|
|
@@ -63,16 +34,26 @@ const LOGO = [
|
|
|
63
34
|
' ██║ ██║███████╗██████╔╝██║ ██║██║',
|
|
64
35
|
' ╚═╝ ╚═╝╚══════╝╚═════╝ ╚═╝ ╚═╝╚═╝',
|
|
65
36
|
];
|
|
37
|
+
// Gradient #FF3333 → #FFCC99 top→bottom (logo) / #FFCC99 → #FF3333 for highlights
|
|
38
|
+
function gradientColor(step, total) {
|
|
39
|
+
const t = total <= 1 ? 0 : step / (total - 1);
|
|
40
|
+
const r = 0xFF;
|
|
41
|
+
const g = Math.round(0xCC - t * (0xCC - 0x33));
|
|
42
|
+
const b = Math.round(0x99 - t * (0x99 - 0x33));
|
|
43
|
+
return chalk_1.default.rgb(r, g, b);
|
|
44
|
+
}
|
|
45
|
+
// Single highlight color: bright orange-red, same family as logo
|
|
46
|
+
const HIGHLIGHT = chalk_1.default.rgb(0xFF, 0x66, 0x33);
|
|
66
47
|
function printBanner(cfg) {
|
|
67
48
|
console.log();
|
|
68
|
-
|
|
69
|
-
console.log(
|
|
70
|
-
}
|
|
49
|
+
LOGO.forEach((line, i) => {
|
|
50
|
+
console.log(gradientColor(i, LOGO.length)(line));
|
|
51
|
+
});
|
|
71
52
|
console.log();
|
|
72
53
|
console.log(chalk_1.default.dim(' v' + config_1.CLI_VERSION) +
|
|
73
54
|
' ' +
|
|
74
55
|
chalk_1.default.white(cfg.userName ?? cfg.userEmail ?? String(cfg.userId)));
|
|
75
|
-
console.log(chalk_1.default.dim(' Type /
|
|
56
|
+
console.log(chalk_1.default.dim(' Type / for commands · ↑↓ navigate · Ctrl+Enter new line · Ctrl-C exit'));
|
|
76
57
|
console.log();
|
|
77
58
|
}
|
|
78
59
|
// ─── Slash command list ────────────────────────────────────────────────────────
|
|
@@ -81,7 +62,8 @@ const COMMANDS = [
|
|
|
81
62
|
{ cmd: '/account', desc: 'Account info' },
|
|
82
63
|
{ cmd: '/agents', desc: 'List AI agents — select default' },
|
|
83
64
|
{ cmd: '/permissions', desc: 'View / toggle tool permissions on this machine' },
|
|
84
|
-
{ cmd: '/mcps', desc: 'List
|
|
65
|
+
{ cmd: '/mcps', desc: 'List MCP servers and their tools' },
|
|
66
|
+
{ cmd: '/threads', desc: 'Browse & switch conversation threads' },
|
|
85
67
|
{ cmd: '/thread', desc: 'Show current conversation thread ID' },
|
|
86
68
|
{ cmd: '/new', desc: 'Start a new conversation thread' },
|
|
87
69
|
{ cmd: '/clear', desc: 'Clear screen' },
|
|
@@ -92,7 +74,7 @@ function printHelp() {
|
|
|
92
74
|
console.log(chalk_1.default.bold(' Commands'));
|
|
93
75
|
console.log();
|
|
94
76
|
for (const { cmd, desc } of COMMANDS) {
|
|
95
|
-
console.log(` ${
|
|
77
|
+
console.log(` ${HIGHLIGHT(cmd.padEnd(16))} ${chalk_1.default.dim(desc)}`);
|
|
96
78
|
}
|
|
97
79
|
console.log();
|
|
98
80
|
console.log(chalk_1.default.dim(' Type anything else to chat with the current agent.'));
|
|
@@ -110,16 +92,50 @@ function printAccount(cfg) {
|
|
|
110
92
|
(cfg.expiresAt < Date.now() ? chalk_1.default.red(' (EXPIRED)') : ''));
|
|
111
93
|
console.log();
|
|
112
94
|
}
|
|
113
|
-
// ─── /agents
|
|
114
|
-
|
|
115
|
-
|
|
95
|
+
// ─── /agents — viewport TUI with lazy-load pagination ────────────────────────
|
|
96
|
+
//
|
|
97
|
+
// Layout (VIEWPORT_SIZE rows visible at a time):
|
|
98
|
+
//
|
|
99
|
+
// AI Agents (42) ↑↓ move · Enter select · 0 reset · Esc cancel
|
|
100
|
+
// Default (no agent): RedAI
|
|
101
|
+
//
|
|
102
|
+
// ❯ ● CLI Proxy ← focused row
|
|
103
|
+
// ○ Trung Hiếu
|
|
104
|
+
// ○ ...
|
|
105
|
+
// ── loading more... ── ← shown when fetching next page
|
|
106
|
+
//
|
|
107
|
+
// viewTop = first visible index in `agents[]`.
|
|
108
|
+
// cursor = absolute index in `agents[]`.
|
|
109
|
+
// When cursor reaches viewTop + VIEWPORT_SIZE - LOAD_AHEAD and more pages exist,
|
|
110
|
+
// fetch next page silently and append to `agents[]`.
|
|
111
|
+
const VIEWPORT_SIZE = 12;
|
|
112
|
+
const LOAD_AHEAD = 3; // trigger fetch when this many rows remain below cursor
|
|
113
|
+
const PAGE_SIZE = 20;
|
|
114
|
+
const AGENT_COL = 28;
|
|
115
|
+
function buildAgentRow(a, focused) {
|
|
116
|
+
const bullet = a.id === currentAgentId ? chalk_1.default.green('●') : chalk_1.default.dim('○');
|
|
117
|
+
const inactive = a.active ? '' : chalk_1.default.red(' (off)');
|
|
118
|
+
const pad = a.name.slice(0, AGENT_COL).padEnd(AGENT_COL);
|
|
119
|
+
const nameStr = focused
|
|
120
|
+
? HIGHLIGHT.bold(pad)
|
|
121
|
+
: a.id === currentAgentId
|
|
122
|
+
? chalk_1.default.bold.green(pad)
|
|
123
|
+
: chalk_1.default.white(pad);
|
|
124
|
+
return (focused ? HIGHLIGHT(' ❯ ') : ' ') + bullet + ' ' + nameStr + inactive;
|
|
125
|
+
}
|
|
126
|
+
async function interactiveAgents(cfg) {
|
|
127
|
+
process.stdout.write(chalk_1.default.dim('\n Loading agents...\r'));
|
|
116
128
|
let agents = [];
|
|
129
|
+
let totalItems = 0;
|
|
130
|
+
let currentPage = 1;
|
|
131
|
+
let totalPages = 1;
|
|
132
|
+
let loadingMore = false;
|
|
117
133
|
try {
|
|
118
|
-
const
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
134
|
+
const first = await (0, agent_1.fetchAgentsPage)(cfg, 1, PAGE_SIZE);
|
|
135
|
+
agents = first.items;
|
|
136
|
+
totalItems = first.meta.totalItems;
|
|
137
|
+
totalPages = first.meta.totalPages;
|
|
138
|
+
currentPage = 1;
|
|
123
139
|
}
|
|
124
140
|
catch {
|
|
125
141
|
process.stdout.write(' \r');
|
|
@@ -133,41 +149,152 @@ async function printAgentsInteractive(cfg) {
|
|
|
133
149
|
console.log();
|
|
134
150
|
return;
|
|
135
151
|
}
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
console.log(chalk_1.default.dim(' Enter number to select agent, or press Enter to keep current.'));
|
|
149
|
-
const answer = await new Promise((resolve) => {
|
|
150
|
-
const tmp = readline_1.default.createInterface({ input: process.stdin, output: process.stdout, terminal: false });
|
|
151
|
-
process.stdout.write(chalk_1.default.dim(' › '));
|
|
152
|
-
tmp.once('line', (line) => { tmp.close(); resolve(line.trim()); });
|
|
153
|
-
});
|
|
154
|
-
if (!answer) {
|
|
152
|
+
// cursor starts on currently selected agent, or 0
|
|
153
|
+
let cursor = Math.max(0, agents.findIndex(a => a.id === currentAgentId));
|
|
154
|
+
let viewTop = Math.max(0, Math.min(cursor, agents.length - VIEWPORT_SIZE));
|
|
155
|
+
// ── render helpers ─────────────────────────────────────────────────────────
|
|
156
|
+
// Number of rows currently shown (min of VIEWPORT_SIZE and available)
|
|
157
|
+
const viewLen = () => Math.min(VIEWPORT_SIZE, agents.length - viewTop);
|
|
158
|
+
// Header line (2 fixed lines) + viewLen data rows + footer (1 line) = full block
|
|
159
|
+
const HEADER_LINES = 3; // blank + title + hint
|
|
160
|
+
const FOOTER_LINES = 1; // blank line after list
|
|
161
|
+
const blockLines = () => HEADER_LINES + viewLen() + FOOTER_LINES;
|
|
162
|
+
const printBlock = () => {
|
|
163
|
+
const shown = viewLen();
|
|
155
164
|
console.log();
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
165
|
+
console.log(chalk_1.default.bold(` AI Agents (${totalItems})`) +
|
|
166
|
+
chalk_1.default.dim(' ↑↓ move · Enter select · 0 reset · Esc cancel'));
|
|
167
|
+
console.log(chalk_1.default.dim(' Default (no agent): RedAI'));
|
|
168
|
+
for (let i = 0; i < shown; i++) {
|
|
169
|
+
const absIdx = viewTop + i;
|
|
170
|
+
console.log(buildAgentRow(agents[absIdx], absIdx === cursor));
|
|
171
|
+
}
|
|
161
172
|
console.log();
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
const
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
173
|
+
};
|
|
174
|
+
// Full redraw: erase existing block, reprint from top
|
|
175
|
+
const redraw = () => {
|
|
176
|
+
const bl = blockLines();
|
|
177
|
+
// Move up past entire block + erase to end of screen
|
|
178
|
+
process.stdout.write(`\x1B[${bl}A\r\x1B[J`);
|
|
179
|
+
printBlock();
|
|
180
|
+
};
|
|
181
|
+
// Repaint a single row in the viewport (absIdx = absolute index in agents[])
|
|
182
|
+
const repaintRow = (absIdx) => {
|
|
183
|
+
const relIdx = absIdx - viewTop;
|
|
184
|
+
if (relIdx < 0 || relIdx >= viewLen())
|
|
185
|
+
return;
|
|
186
|
+
// From bottom of block (after trailing blank line), move up to that row
|
|
187
|
+
const stepsUp = FOOTER_LINES + (viewLen() - 1 - relIdx) + 1; // +1 for blank
|
|
188
|
+
process.stdout.write(`\x1B[${stepsUp}A\r\x1B[2K`);
|
|
189
|
+
process.stdout.write(buildAgentRow(agents[absIdx], absIdx === cursor));
|
|
190
|
+
process.stdout.write(`\x1B[${stepsUp}B\r`);
|
|
191
|
+
};
|
|
192
|
+
printBlock();
|
|
193
|
+
// ── lazy-load next page ────────────────────────────────────────────────────
|
|
194
|
+
const maybeLoadMore = (resolve, handler) => {
|
|
195
|
+
if (loadingMore)
|
|
196
|
+
return;
|
|
197
|
+
if (currentPage >= totalPages)
|
|
198
|
+
return;
|
|
199
|
+
// trigger when cursor is within LOAD_AHEAD rows of the end of loaded list
|
|
200
|
+
if (cursor < agents.length - LOAD_AHEAD)
|
|
201
|
+
return;
|
|
202
|
+
loadingMore = true;
|
|
203
|
+
const nextPage = currentPage + 1;
|
|
204
|
+
// Show "loading" indicator on the last visible row
|
|
205
|
+
const lastVisAbs = viewTop + viewLen() - 1;
|
|
206
|
+
const stepsUp = FOOTER_LINES + 1; // blank + last row
|
|
207
|
+
process.stdout.write(`\x1B[${stepsUp}A\r\x1B[2K`);
|
|
208
|
+
process.stdout.write(chalk_1.default.dim(' ── loading more... ──'));
|
|
209
|
+
process.stdout.write(`\x1B[${stepsUp}B\r`);
|
|
210
|
+
(0, agent_1.fetchAgentsPage)(cfg, nextPage, PAGE_SIZE).then((page) => {
|
|
211
|
+
agents.push(...page.items);
|
|
212
|
+
currentPage = nextPage;
|
|
213
|
+
totalPages = page.meta.totalPages;
|
|
214
|
+
totalItems = page.meta.totalItems;
|
|
215
|
+
loadingMore = false;
|
|
216
|
+
void lastVisAbs;
|
|
217
|
+
redraw();
|
|
218
|
+
}).catch(() => {
|
|
219
|
+
loadingMore = false;
|
|
220
|
+
// restore overwritten row
|
|
221
|
+
repaintRow(lastVisAbs);
|
|
222
|
+
});
|
|
223
|
+
void resolve;
|
|
224
|
+
void handler;
|
|
225
|
+
};
|
|
226
|
+
// ── keypress loop ──────────────────────────────────────────────────────────
|
|
227
|
+
await new Promise((resolve) => {
|
|
228
|
+
const handler = (_char, key) => {
|
|
229
|
+
if (!key || loadingMore)
|
|
230
|
+
return;
|
|
231
|
+
// ↑ / k
|
|
232
|
+
if (key.name === 'up' || (key.name === 'k' && !key.ctrl)) {
|
|
233
|
+
if (cursor === 0)
|
|
234
|
+
return;
|
|
235
|
+
const prev = cursor--;
|
|
236
|
+
if (cursor < viewTop) {
|
|
237
|
+
viewTop--;
|
|
238
|
+
redraw();
|
|
239
|
+
}
|
|
240
|
+
else {
|
|
241
|
+
repaintRow(prev);
|
|
242
|
+
repaintRow(cursor);
|
|
243
|
+
}
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
// ↓ / j
|
|
247
|
+
if (key.name === 'down' || (key.name === 'j' && !key.ctrl)) {
|
|
248
|
+
if (cursor === agents.length - 1 && currentPage >= totalPages)
|
|
249
|
+
return;
|
|
250
|
+
if (cursor < agents.length - 1) {
|
|
251
|
+
const prev = cursor++;
|
|
252
|
+
if (cursor >= viewTop + VIEWPORT_SIZE) {
|
|
253
|
+
viewTop++;
|
|
254
|
+
redraw();
|
|
255
|
+
}
|
|
256
|
+
else {
|
|
257
|
+
repaintRow(prev);
|
|
258
|
+
repaintRow(cursor);
|
|
259
|
+
}
|
|
260
|
+
maybeLoadMore(resolve, handler);
|
|
261
|
+
}
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
// Enter — select
|
|
265
|
+
if (key.name === 'return') {
|
|
266
|
+
process.stdin.removeListener('keypress', handler);
|
|
267
|
+
const chosen = agents[cursor];
|
|
268
|
+
currentAgentId = chosen.id;
|
|
269
|
+
currentAgentName = chosen.name;
|
|
270
|
+
console.log(chalk_1.default.green(` ✓ Agent set to: ${chalk_1.default.bold(chosen.name)}`));
|
|
271
|
+
console.log();
|
|
272
|
+
resolve();
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
// 0 — reset to default RedAI
|
|
276
|
+
if (key.name === '0') {
|
|
277
|
+
process.stdin.removeListener('keypress', handler);
|
|
278
|
+
currentAgentId = null;
|
|
279
|
+
currentAgentName = 'RedAI';
|
|
280
|
+
console.log(chalk_1.default.green(' ✓ Reset to default: RedAI'));
|
|
281
|
+
console.log();
|
|
282
|
+
resolve();
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
// Esc / q — cancel
|
|
286
|
+
if (key.name === 'escape' || key.name === 'q' || (key.ctrl && key.name === 'c')) {
|
|
287
|
+
process.stdin.removeListener('keypress', handler);
|
|
288
|
+
console.log(chalk_1.default.dim(' Cancelled. Agent unchanged.'));
|
|
289
|
+
console.log();
|
|
290
|
+
resolve();
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
};
|
|
294
|
+
process.stdin.on('keypress', handler);
|
|
295
|
+
});
|
|
169
296
|
}
|
|
170
|
-
// ─── /permissions
|
|
297
|
+
// ─── /permissions — interactive TUI ──────────────────────────────────────────
|
|
171
298
|
const TOOL_DESC = {
|
|
172
299
|
Read: 'Read files',
|
|
173
300
|
Write: 'Create/overwrite files',
|
|
@@ -183,44 +310,513 @@ const TOOL_DESC = {
|
|
|
183
310
|
McpCall: 'Call MCP tool',
|
|
184
311
|
McpList: 'List MCP tools',
|
|
185
312
|
};
|
|
186
|
-
|
|
313
|
+
// Render one permission row. cursorLine=true → this row is focused.
|
|
314
|
+
function permRow(name, enabled, focused) {
|
|
315
|
+
const checkbox = enabled ? chalk_1.default.green('[✓]') : chalk_1.default.dim('[ ]');
|
|
316
|
+
const nameStr = focused
|
|
317
|
+
? HIGHLIGHT.bold(name.padEnd(13))
|
|
318
|
+
: chalk_1.default.white(name.padEnd(13));
|
|
319
|
+
const desc = focused
|
|
320
|
+
? HIGHLIGHT.dim(TOOL_DESC[name])
|
|
321
|
+
: chalk_1.default.dim(TOOL_DESC[name]);
|
|
322
|
+
const cursor = focused ? HIGHLIGHT(' ❯ ') : ' ';
|
|
323
|
+
return cursor + checkbox + ' ' + nameStr + ' ' + desc;
|
|
324
|
+
}
|
|
325
|
+
async function interactivePermissions() {
|
|
187
326
|
const perms = (0, permissions_1.loadPermissions)();
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
console.log(
|
|
195
|
-
|
|
196
|
-
|
|
327
|
+
// Build mutable state: enabled[i] mirrors perms for each tool
|
|
328
|
+
const enabled = permissions_2.ALL_TOOL_NAMES.map((n) => perms[n] !== false);
|
|
329
|
+
let cursor = 0;
|
|
330
|
+
const total = permissions_2.ALL_TOOL_NAMES.length;
|
|
331
|
+
// Print header + all rows
|
|
332
|
+
const printAll = () => {
|
|
333
|
+
console.log();
|
|
334
|
+
console.log(chalk_1.default.bold(' Tool Permissions') +
|
|
335
|
+
chalk_1.default.dim(' ↑↓ move · Space toggle · Enter save · Esc cancel'));
|
|
336
|
+
console.log();
|
|
337
|
+
for (let i = 0; i < total; i++) {
|
|
338
|
+
console.log(permRow(permissions_2.ALL_TOOL_NAMES[i], enabled[i], i === cursor));
|
|
339
|
+
}
|
|
340
|
+
console.log();
|
|
341
|
+
};
|
|
342
|
+
// Move cursor up N rows from current bottom of list, rewrite 1 row, return
|
|
343
|
+
const repaintRow = (idx) => {
|
|
344
|
+
// cursor is currently at bottom of list (after the trailing \n of printAll)
|
|
345
|
+
// We need absolute positioning: go up (total - idx) lines from bottom-of-list
|
|
346
|
+
const stepsUp = total - idx + 1; // +1 for the blank line after list
|
|
347
|
+
process.stdout.write(`\x1B[${stepsUp}A`); // move up
|
|
348
|
+
process.stdout.write('\r\x1B[2K'); // clear line
|
|
349
|
+
process.stdout.write(permRow(permissions_2.ALL_TOOL_NAMES[idx], enabled[idx], idx === cursor));
|
|
350
|
+
process.stdout.write(`\x1B[${stepsUp}B`); // move back down
|
|
351
|
+
process.stdout.write('\r');
|
|
352
|
+
};
|
|
353
|
+
printAll();
|
|
354
|
+
return new Promise((resolve) => {
|
|
355
|
+
const handler = (_char, key) => {
|
|
356
|
+
if (!key)
|
|
357
|
+
return;
|
|
358
|
+
// ↑ / k
|
|
359
|
+
if (key.name === 'up' || (key.name === 'k' && !key.ctrl)) {
|
|
360
|
+
if (cursor === 0)
|
|
361
|
+
return;
|
|
362
|
+
const prev = cursor;
|
|
363
|
+
cursor--;
|
|
364
|
+
repaintRow(prev);
|
|
365
|
+
repaintRow(cursor);
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
// ↓ / j
|
|
369
|
+
if (key.name === 'down' || (key.name === 'j' && !key.ctrl)) {
|
|
370
|
+
if (cursor === total - 1)
|
|
371
|
+
return;
|
|
372
|
+
const prev = cursor;
|
|
373
|
+
cursor++;
|
|
374
|
+
repaintRow(prev);
|
|
375
|
+
repaintRow(cursor);
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
// Space — toggle
|
|
379
|
+
if (key.name === 'space') {
|
|
380
|
+
enabled[cursor] = !enabled[cursor];
|
|
381
|
+
repaintRow(cursor);
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
// Enter — save & exit
|
|
385
|
+
if (key.name === 'return') {
|
|
386
|
+
process.stdin.removeListener('keypress', handler);
|
|
387
|
+
const newPerms = {};
|
|
388
|
+
for (let i = 0; i < total; i++) {
|
|
389
|
+
newPerms[permissions_2.ALL_TOOL_NAMES[i]] = enabled[i];
|
|
390
|
+
}
|
|
391
|
+
(0, permissions_1.savePermissions)(newPerms);
|
|
392
|
+
const onCount = enabled.filter(Boolean).length;
|
|
393
|
+
const offCount = total - onCount;
|
|
394
|
+
console.log(chalk_1.default.green(' ✓ Saved.') +
|
|
395
|
+
chalk_1.default.dim(` ${onCount} enabled, ${offCount} disabled.`));
|
|
396
|
+
console.log();
|
|
397
|
+
resolve();
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
// Esc / q / Ctrl-C — cancel
|
|
401
|
+
if (key.name === 'escape' || key.name === 'q' || (key.ctrl && key.name === 'c')) {
|
|
402
|
+
process.stdin.removeListener('keypress', handler);
|
|
403
|
+
console.log(chalk_1.default.dim(' Cancelled. No changes saved.'));
|
|
404
|
+
console.log();
|
|
405
|
+
resolve();
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
};
|
|
409
|
+
process.stdin.on('keypress', handler);
|
|
410
|
+
});
|
|
197
411
|
}
|
|
198
|
-
// ─── /mcps
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
412
|
+
// ─── /mcps — interactive TUI ──────────────────────────────────────────────────
|
|
413
|
+
//
|
|
414
|
+
// Keys:
|
|
415
|
+
// ↑↓ / j k navigate
|
|
416
|
+
// Space toggle enable/disable
|
|
417
|
+
// d remove server from config
|
|
418
|
+
// a open catalog to add servers
|
|
419
|
+
// Enter/t test selected server (spawn + list tools)
|
|
420
|
+
// Esc/q exit
|
|
421
|
+
async function interactiveMcps() {
|
|
422
|
+
let servers = (0, config_2.loadMcpConfig)();
|
|
423
|
+
let cursor = 0;
|
|
424
|
+
const getNames = () => Object.keys(servers);
|
|
425
|
+
const COL_NAME = 16;
|
|
426
|
+
const buildRow = (name, focused) => {
|
|
427
|
+
const cfg = servers[name];
|
|
428
|
+
const on = (0, config_2.isMcpEnabled)(cfg);
|
|
429
|
+
const statusIcon = on ? chalk_1.default.green('●') : chalk_1.default.dim('○');
|
|
430
|
+
const checkbox = on ? chalk_1.default.green('[on] ') : chalk_1.default.dim('[off]');
|
|
431
|
+
const nameStr = focused
|
|
432
|
+
? HIGHLIGHT.bold(name.padEnd(COL_NAME))
|
|
433
|
+
: chalk_1.default.white(name.padEnd(COL_NAME));
|
|
434
|
+
const cmd = chalk_1.default.dim(`${cfg.command} ${(cfg.args ?? []).slice(0, 2).join(' ')}`);
|
|
435
|
+
const cur = focused ? HIGHLIGHT(' ❯ ') : ' ';
|
|
436
|
+
return cur + statusIcon + ' ' + checkbox + ' ' + nameStr + ' ' + cmd;
|
|
437
|
+
};
|
|
438
|
+
const printAll = (names) => {
|
|
206
439
|
console.log();
|
|
207
|
-
console.log(chalk_1.default.bold(
|
|
440
|
+
console.log(chalk_1.default.bold(` MCP Servers (${names.length})`) +
|
|
441
|
+
chalk_1.default.dim(' ↑↓ move · Space on/off · d remove · a add · c chrome · Enter test · Esc exit'));
|
|
208
442
|
console.log();
|
|
209
443
|
if (names.length === 0) {
|
|
210
|
-
console.log(chalk_1.default.dim(' No
|
|
211
|
-
console.log(chalk_1.default.dim(' Run `redai mcp add` to configure one.'));
|
|
444
|
+
console.log(chalk_1.default.dim(' No servers configured. Press a to add from catalog.'));
|
|
212
445
|
}
|
|
213
446
|
else {
|
|
214
|
-
for (
|
|
215
|
-
console.log(
|
|
447
|
+
for (let i = 0; i < names.length; i++) {
|
|
448
|
+
console.log(buildRow(names[i], i === cursor));
|
|
216
449
|
}
|
|
217
450
|
}
|
|
218
451
|
console.log();
|
|
452
|
+
};
|
|
453
|
+
const repaintRow = (idx, names) => {
|
|
454
|
+
const total = names.length;
|
|
455
|
+
if (total === 0)
|
|
456
|
+
return;
|
|
457
|
+
const stepsUp = total - idx + 1;
|
|
458
|
+
process.stdout.write(`\x1B[${stepsUp}A\r\x1B[2K`);
|
|
459
|
+
process.stdout.write(buildRow(names[idx], idx === cursor));
|
|
460
|
+
process.stdout.write(`\x1B[${stepsUp}B\r`);
|
|
461
|
+
};
|
|
462
|
+
const printStatus = (msg) => {
|
|
463
|
+
process.stdout.write('\r\x1B[2K' + msg + '\n');
|
|
464
|
+
};
|
|
465
|
+
printAll(getNames());
|
|
466
|
+
await new Promise((resolve) => {
|
|
467
|
+
const handler = async (_char, key) => {
|
|
468
|
+
if (!key)
|
|
469
|
+
return;
|
|
470
|
+
const names = getNames();
|
|
471
|
+
const total = names.length;
|
|
472
|
+
if (key.name === 'up' || (key.name === 'k' && !key.ctrl)) {
|
|
473
|
+
if (total === 0 || cursor === 0)
|
|
474
|
+
return;
|
|
475
|
+
const prev = cursor--;
|
|
476
|
+
repaintRow(prev, names);
|
|
477
|
+
repaintRow(cursor, names);
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
480
|
+
if (key.name === 'down' || (key.name === 'j' && !key.ctrl)) {
|
|
481
|
+
if (total === 0 || cursor === total - 1)
|
|
482
|
+
return;
|
|
483
|
+
const prev = cursor++;
|
|
484
|
+
repaintRow(prev, names);
|
|
485
|
+
repaintRow(cursor, names);
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
488
|
+
// Space — toggle enable/disable
|
|
489
|
+
if (key.name === 'space' && total > 0) {
|
|
490
|
+
const name = names[cursor];
|
|
491
|
+
const cfg = servers[name];
|
|
492
|
+
const nowEnabled = !(0, config_2.isMcpEnabled)(cfg);
|
|
493
|
+
servers[name] = { ...cfg, enabled: nowEnabled };
|
|
494
|
+
(0, config_2.saveMcpConfig)(servers);
|
|
495
|
+
repaintRow(cursor, names);
|
|
496
|
+
return;
|
|
497
|
+
}
|
|
498
|
+
// d — remove focused server
|
|
499
|
+
if (key.name === 'd' && total > 0) {
|
|
500
|
+
const name = names[cursor];
|
|
501
|
+
delete servers[name];
|
|
502
|
+
(0, config_2.saveMcpConfig)(servers);
|
|
503
|
+
if (cursor >= Object.keys(servers).length)
|
|
504
|
+
cursor = Math.max(0, Object.keys(servers).length - 1);
|
|
505
|
+
// Full redraw since row count changed
|
|
506
|
+
process.stdout.write(`\x1B[${total + 3}A`); // jump up past entire list + header + footer
|
|
507
|
+
process.stdout.write('\x1B[0J'); // erase from cursor to end
|
|
508
|
+
printAll(getNames());
|
|
509
|
+
return;
|
|
510
|
+
}
|
|
511
|
+
// a — add from catalog (pause this handler, run catalog TUI, resume)
|
|
512
|
+
if (key.name === 'a') {
|
|
513
|
+
process.stdin.removeListener('keypress', handler);
|
|
514
|
+
await addFromCatalogInline(servers);
|
|
515
|
+
// reload and redraw
|
|
516
|
+
servers = (0, config_2.loadMcpConfig)();
|
|
517
|
+
cursor = 0;
|
|
518
|
+
printAll(getNames());
|
|
519
|
+
process.stdin.on('keypress', handler);
|
|
520
|
+
return;
|
|
521
|
+
}
|
|
522
|
+
// Enter / t — test focused server
|
|
523
|
+
if ((key.name === 'return' || key.name === 't') && total > 0) {
|
|
524
|
+
const name = names[cursor];
|
|
525
|
+
const cfg = servers[name];
|
|
526
|
+
if (!(0, config_2.isMcpEnabled)(cfg)) {
|
|
527
|
+
printStatus(chalk_1.default.yellow(` "${name}" is disabled.`));
|
|
528
|
+
return;
|
|
529
|
+
}
|
|
530
|
+
printStatus(chalk_1.default.dim(` Connecting to ${name}...`));
|
|
531
|
+
try {
|
|
532
|
+
const client = await client_1.mcpClientPool.getClient(name);
|
|
533
|
+
const { tools } = await client.listTools();
|
|
534
|
+
printStatus(chalk_1.default.green(` ✓ ${name}: ${tools.length} tool(s) — ${tools.map(t => t.name).join(', ')}`));
|
|
535
|
+
await client_1.mcpClientPool.shutdown();
|
|
536
|
+
}
|
|
537
|
+
catch (err) {
|
|
538
|
+
printStatus(chalk_1.default.red(` ✗ ${name}: ${err instanceof Error ? err.message : String(err)}`));
|
|
539
|
+
}
|
|
540
|
+
return;
|
|
541
|
+
}
|
|
542
|
+
// c — launch Chrome with debug profile for chrome-devtools MCP
|
|
543
|
+
if (key.name === 'c') {
|
|
544
|
+
const already = await (0, chrome_launcher_1.isCdpAlive)();
|
|
545
|
+
if (already) {
|
|
546
|
+
printStatus(chalk_1.default.green(` ✓ Chrome CDP already active on port ${chrome_launcher_1.CDP_PORT}.`));
|
|
547
|
+
return;
|
|
548
|
+
}
|
|
549
|
+
printStatus(chalk_1.default.dim(' Launching Chrome with debug profile...'));
|
|
550
|
+
const result = await (0, chrome_launcher_1.launchChromeForDebug)();
|
|
551
|
+
if (result.ok) {
|
|
552
|
+
printStatus(chalk_1.default.green(` ✓ Chrome launched. CDP ready on port ${chrome_launcher_1.CDP_PORT}.`));
|
|
553
|
+
}
|
|
554
|
+
else {
|
|
555
|
+
printStatus(chalk_1.default.red(` ✗ ${result.error}`));
|
|
556
|
+
}
|
|
557
|
+
return;
|
|
558
|
+
}
|
|
559
|
+
// Esc / q — exit
|
|
560
|
+
if (key.name === 'escape' || key.name === 'q' || (key.ctrl && key.name === 'c')) {
|
|
561
|
+
process.stdin.removeListener('keypress', handler);
|
|
562
|
+
resolve();
|
|
563
|
+
return;
|
|
564
|
+
}
|
|
565
|
+
};
|
|
566
|
+
process.stdin.on('keypress', handler);
|
|
567
|
+
});
|
|
568
|
+
}
|
|
569
|
+
// Inline catalog picker called from within /mcps TUI
|
|
570
|
+
async function addFromCatalogInline(existing) {
|
|
571
|
+
const items = config_2.MCP_CATALOG;
|
|
572
|
+
let cursor = 0;
|
|
573
|
+
const selected = new Array(items.length).fill(false);
|
|
574
|
+
const total = items.length;
|
|
575
|
+
const COL_NAME = 20;
|
|
576
|
+
const buildRow = (i, focused) => {
|
|
577
|
+
const entry = items[i];
|
|
578
|
+
const alreadyAdded = !!existing[entry.name];
|
|
579
|
+
const checked = selected[i];
|
|
580
|
+
const checkbox = alreadyAdded ? chalk_1.default.dim('[~]') : checked ? chalk_1.default.green('[✓]') : chalk_1.default.dim('[ ]');
|
|
581
|
+
const nameStr = alreadyAdded
|
|
582
|
+
? chalk_1.default.dim(entry.name.padEnd(COL_NAME))
|
|
583
|
+
: focused ? HIGHLIGHT.bold(entry.name.padEnd(COL_NAME)) : chalk_1.default.white(entry.name.padEnd(COL_NAME));
|
|
584
|
+
const desc = focused && !alreadyAdded ? HIGHLIGHT.dim(entry.description) : chalk_1.default.dim(entry.description);
|
|
585
|
+
return (focused ? HIGHLIGHT(' ❯ ') : ' ') + checkbox + ' ' + nameStr + ' ' + desc;
|
|
586
|
+
};
|
|
587
|
+
const repaintRow = (idx) => {
|
|
588
|
+
const stepsUp = total - idx + 1;
|
|
589
|
+
process.stdout.write(`\x1B[${stepsUp}A\r\x1B[2K`);
|
|
590
|
+
process.stdout.write(buildRow(idx, idx === cursor));
|
|
591
|
+
process.stdout.write(`\x1B[${stepsUp}B\r`);
|
|
592
|
+
};
|
|
593
|
+
console.log();
|
|
594
|
+
console.log(chalk_1.default.bold(' Add from Catalog') + chalk_1.default.dim(' ↑↓ move · Space select · Enter install · Esc back'));
|
|
595
|
+
console.log(chalk_1.default.dim(' [~] = already installed'));
|
|
596
|
+
console.log();
|
|
597
|
+
for (let i = 0; i < total; i++)
|
|
598
|
+
console.log(buildRow(i, i === cursor));
|
|
599
|
+
console.log();
|
|
600
|
+
await new Promise((resolve) => {
|
|
601
|
+
const handler = (_char, key) => {
|
|
602
|
+
if (!key)
|
|
603
|
+
return;
|
|
604
|
+
if (key.name === 'up' || (key.name === 'k' && !key.ctrl)) {
|
|
605
|
+
if (cursor === 0)
|
|
606
|
+
return;
|
|
607
|
+
const prev = cursor--;
|
|
608
|
+
repaintRow(prev);
|
|
609
|
+
repaintRow(cursor);
|
|
610
|
+
return;
|
|
611
|
+
}
|
|
612
|
+
if (key.name === 'down' || (key.name === 'j' && !key.ctrl)) {
|
|
613
|
+
if (cursor === total - 1)
|
|
614
|
+
return;
|
|
615
|
+
const prev = cursor++;
|
|
616
|
+
repaintRow(prev);
|
|
617
|
+
repaintRow(cursor);
|
|
618
|
+
return;
|
|
619
|
+
}
|
|
620
|
+
if (key.name === 'space') {
|
|
621
|
+
if (!existing[items[cursor].name]) {
|
|
622
|
+
selected[cursor] = !selected[cursor];
|
|
623
|
+
repaintRow(cursor);
|
|
624
|
+
}
|
|
625
|
+
return;
|
|
626
|
+
}
|
|
627
|
+
if (key.name === 'return') {
|
|
628
|
+
process.stdin.removeListener('keypress', handler);
|
|
629
|
+
const toAdd = items.filter((_, i) => selected[i]);
|
|
630
|
+
for (const entry of toAdd) {
|
|
631
|
+
existing[entry.name] = entry.config;
|
|
632
|
+
console.log(chalk_1.default.green(` ✓ Added "${entry.name}"`));
|
|
633
|
+
if (entry.config.env)
|
|
634
|
+
console.log(chalk_1.default.yellow(` Needs: ${Object.keys(entry.config.env).join(', ')}`));
|
|
635
|
+
}
|
|
636
|
+
if (toAdd.length > 0)
|
|
637
|
+
(0, config_2.saveMcpConfig)(existing);
|
|
638
|
+
else
|
|
639
|
+
console.log(chalk_1.default.dim(' Nothing selected.'));
|
|
640
|
+
console.log();
|
|
641
|
+
resolve();
|
|
642
|
+
return;
|
|
643
|
+
}
|
|
644
|
+
if (key.name === 'escape' || key.name === 'q' || (key.ctrl && key.name === 'c')) {
|
|
645
|
+
process.stdin.removeListener('keypress', handler);
|
|
646
|
+
resolve();
|
|
647
|
+
return;
|
|
648
|
+
}
|
|
649
|
+
};
|
|
650
|
+
process.stdin.on('keypress', handler);
|
|
651
|
+
});
|
|
652
|
+
}
|
|
653
|
+
// ─── /threads — viewport TUI with lazy-load pagination ───────────────────────
|
|
654
|
+
const THREAD_VIEWPORT = 12;
|
|
655
|
+
const THREAD_PAGE_SIZE = 20;
|
|
656
|
+
const THREAD_LOAD_AHEAD = 3;
|
|
657
|
+
const THREAD_COL_TITLE = 36;
|
|
658
|
+
function fmtThreadTime(ts) {
|
|
659
|
+
const d = new Date(ts);
|
|
660
|
+
const now = new Date();
|
|
661
|
+
const sameDay = d.toDateString() === now.toDateString();
|
|
662
|
+
if (sameDay)
|
|
663
|
+
return d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
|
664
|
+
return d.toLocaleDateString([], { month: 'short', day: 'numeric' });
|
|
665
|
+
}
|
|
666
|
+
function buildThreadRow(t, focused) {
|
|
667
|
+
const isCurrent = t.id === currentThreadId;
|
|
668
|
+
const bullet = isCurrent ? chalk_1.default.cyan('▶') : chalk_1.default.dim('○');
|
|
669
|
+
const title = (t.title ?? 'Untitled').slice(0, THREAD_COL_TITLE).padEnd(THREAD_COL_TITLE);
|
|
670
|
+
const time = chalk_1.default.dim(fmtThreadTime(t.updatedAt));
|
|
671
|
+
const cur = focused ? HIGHLIGHT(' ❯ ') : ' ';
|
|
672
|
+
const nameStr = focused
|
|
673
|
+
? HIGHLIGHT.bold(title)
|
|
674
|
+
: isCurrent
|
|
675
|
+
? chalk_1.default.bold.cyan(title)
|
|
676
|
+
: chalk_1.default.white(title);
|
|
677
|
+
return cur + bullet + ' ' + nameStr + ' ' + time;
|
|
678
|
+
}
|
|
679
|
+
async function interactiveThreads(cfg) {
|
|
680
|
+
process.stdout.write(chalk_1.default.dim('\n Loading threads...\r'));
|
|
681
|
+
let threads = [];
|
|
682
|
+
let totalItems = 0;
|
|
683
|
+
let currentPage = 1;
|
|
684
|
+
let totalPages = 1;
|
|
685
|
+
let loadingMore = false;
|
|
686
|
+
try {
|
|
687
|
+
const first = await (0, chat_1.fetchThreadsPage)(cfg, 1, THREAD_PAGE_SIZE);
|
|
688
|
+
threads = first.items;
|
|
689
|
+
totalItems = first.meta.totalItems;
|
|
690
|
+
totalPages = first.meta.totalPages;
|
|
219
691
|
}
|
|
220
692
|
catch {
|
|
221
|
-
|
|
693
|
+
process.stdout.write(' \r');
|
|
694
|
+
console.log(chalk_1.default.red(' ✗ Could not fetch threads.'));
|
|
695
|
+
console.log();
|
|
696
|
+
return;
|
|
697
|
+
}
|
|
698
|
+
process.stdout.write(' \r');
|
|
699
|
+
if (threads.length === 0) {
|
|
700
|
+
console.log(chalk_1.default.dim(' No threads found. Use /new to create one.'));
|
|
222
701
|
console.log();
|
|
702
|
+
return;
|
|
223
703
|
}
|
|
704
|
+
let cursor = Math.max(0, threads.findIndex(t => t.id === currentThreadId));
|
|
705
|
+
let viewTop = Math.max(0, Math.min(cursor, threads.length - THREAD_VIEWPORT));
|
|
706
|
+
const HEADER_LINES = 3;
|
|
707
|
+
const FOOTER_LINES = 1;
|
|
708
|
+
const viewLen = () => Math.min(THREAD_VIEWPORT, threads.length - viewTop);
|
|
709
|
+
const blockLines = () => HEADER_LINES + viewLen() + FOOTER_LINES;
|
|
710
|
+
const printBlock = () => {
|
|
711
|
+
const shown = viewLen();
|
|
712
|
+
console.log();
|
|
713
|
+
console.log(chalk_1.default.bold(` Threads (${totalItems})`) +
|
|
714
|
+
chalk_1.default.dim(' ↑↓ move · Enter switch · Esc cancel'));
|
|
715
|
+
console.log(chalk_1.default.dim(` Current: ${currentThreadId ? currentThreadId.slice(0, 8) + '…' : 'none'}`));
|
|
716
|
+
for (let i = 0; i < shown; i++) {
|
|
717
|
+
console.log(buildThreadRow(threads[viewTop + i], viewTop + i === cursor));
|
|
718
|
+
}
|
|
719
|
+
console.log();
|
|
720
|
+
};
|
|
721
|
+
const redraw = () => {
|
|
722
|
+
process.stdout.write(`\x1B[${blockLines()}A\r\x1B[J`);
|
|
723
|
+
printBlock();
|
|
724
|
+
};
|
|
725
|
+
const repaintRow = (absIdx) => {
|
|
726
|
+
const relIdx = absIdx - viewTop;
|
|
727
|
+
if (relIdx < 0 || relIdx >= viewLen())
|
|
728
|
+
return;
|
|
729
|
+
const stepsUp = FOOTER_LINES + (viewLen() - 1 - relIdx) + 1;
|
|
730
|
+
process.stdout.write(`\x1B[${stepsUp}A\r\x1B[2K`);
|
|
731
|
+
process.stdout.write(buildThreadRow(threads[absIdx], absIdx === cursor));
|
|
732
|
+
process.stdout.write(`\x1B[${stepsUp}B\r`);
|
|
733
|
+
};
|
|
734
|
+
printBlock();
|
|
735
|
+
const maybeLoadMore = () => {
|
|
736
|
+
if (loadingMore || currentPage >= totalPages)
|
|
737
|
+
return;
|
|
738
|
+
if (cursor < threads.length - THREAD_LOAD_AHEAD)
|
|
739
|
+
return;
|
|
740
|
+
loadingMore = true;
|
|
741
|
+
const nextPage = currentPage + 1;
|
|
742
|
+
const lastVisAbs = viewTop + viewLen() - 1;
|
|
743
|
+
const stepsUp = FOOTER_LINES + 1;
|
|
744
|
+
process.stdout.write(`\x1B[${stepsUp}A\r\x1B[2K`);
|
|
745
|
+
process.stdout.write(chalk_1.default.dim(' ── loading more... ──'));
|
|
746
|
+
process.stdout.write(`\x1B[${stepsUp}B\r`);
|
|
747
|
+
(0, chat_1.fetchThreadsPage)(cfg, nextPage, THREAD_PAGE_SIZE).then((page) => {
|
|
748
|
+
threads.push(...page.items);
|
|
749
|
+
currentPage = nextPage;
|
|
750
|
+
totalPages = page.meta.totalPages;
|
|
751
|
+
totalItems = page.meta.totalItems;
|
|
752
|
+
loadingMore = false;
|
|
753
|
+
void lastVisAbs;
|
|
754
|
+
redraw();
|
|
755
|
+
}).catch(() => {
|
|
756
|
+
loadingMore = false;
|
|
757
|
+
repaintRow(lastVisAbs);
|
|
758
|
+
});
|
|
759
|
+
};
|
|
760
|
+
await new Promise((resolve) => {
|
|
761
|
+
const handler = (_char, key) => {
|
|
762
|
+
if (!key || loadingMore)
|
|
763
|
+
return;
|
|
764
|
+
if (key.name === 'up' || (key.name === 'k' && !key.ctrl)) {
|
|
765
|
+
if (cursor === 0)
|
|
766
|
+
return;
|
|
767
|
+
const prev = cursor--;
|
|
768
|
+
if (cursor < viewTop) {
|
|
769
|
+
viewTop--;
|
|
770
|
+
redraw();
|
|
771
|
+
}
|
|
772
|
+
else {
|
|
773
|
+
repaintRow(prev);
|
|
774
|
+
repaintRow(cursor);
|
|
775
|
+
}
|
|
776
|
+
return;
|
|
777
|
+
}
|
|
778
|
+
if (key.name === 'down' || (key.name === 'j' && !key.ctrl)) {
|
|
779
|
+
if (cursor === threads.length - 1 && currentPage >= totalPages)
|
|
780
|
+
return;
|
|
781
|
+
if (cursor < threads.length - 1) {
|
|
782
|
+
const prev = cursor++;
|
|
783
|
+
if (cursor >= viewTop + THREAD_VIEWPORT) {
|
|
784
|
+
viewTop++;
|
|
785
|
+
redraw();
|
|
786
|
+
}
|
|
787
|
+
else {
|
|
788
|
+
repaintRow(prev);
|
|
789
|
+
repaintRow(cursor);
|
|
790
|
+
}
|
|
791
|
+
maybeLoadMore();
|
|
792
|
+
}
|
|
793
|
+
return;
|
|
794
|
+
}
|
|
795
|
+
if (key.name === 'return') {
|
|
796
|
+
process.stdin.removeListener('keypress', handler);
|
|
797
|
+
const chosen = threads[cursor];
|
|
798
|
+
currentThreadId = chosen.id;
|
|
799
|
+
// persist to session
|
|
800
|
+
const s = (0, store_1.loadSession)();
|
|
801
|
+
s.threads[cfg.apiBaseUrl] = chosen.id;
|
|
802
|
+
(0, store_1.saveSession)(s);
|
|
803
|
+
const title = chosen.title ?? 'Untitled';
|
|
804
|
+
console.log(chalk_1.default.cyan(` ✓ Switched to: ${chalk_1.default.bold(title.slice(0, 50))}`));
|
|
805
|
+
console.log(` ${chalk_1.default.dim('Thread ')} ${chosen.id}`);
|
|
806
|
+
console.log();
|
|
807
|
+
resolve();
|
|
808
|
+
return;
|
|
809
|
+
}
|
|
810
|
+
if (key.name === 'escape' || key.name === 'q' || (key.ctrl && key.name === 'c')) {
|
|
811
|
+
process.stdin.removeListener('keypress', handler);
|
|
812
|
+
console.log(chalk_1.default.dim(' Cancelled. Thread unchanged.'));
|
|
813
|
+
console.log();
|
|
814
|
+
resolve();
|
|
815
|
+
return;
|
|
816
|
+
}
|
|
817
|
+
};
|
|
818
|
+
process.stdin.on('keypress', handler);
|
|
819
|
+
});
|
|
224
820
|
}
|
|
225
821
|
// ─── /thread ──────────────────────────────────────────────────────────────────
|
|
226
822
|
function printThread() {
|
|
@@ -234,153 +830,171 @@ function printThread() {
|
|
|
234
830
|
}
|
|
235
831
|
console.log();
|
|
236
832
|
}
|
|
237
|
-
// ───
|
|
238
|
-
async function
|
|
239
|
-
try {
|
|
240
|
-
const res = await axios_1.default.post(`${cfg.apiBaseUrl}/api/v1/user/chat/threads`, { title }, { headers: buildHeaders(cfg), timeout: 10000 });
|
|
241
|
-
return res.data?.data?.id ?? null;
|
|
242
|
-
}
|
|
243
|
-
catch {
|
|
244
|
-
return null;
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
// ─── Send message + stream response ───────────────────────────────────────────
|
|
248
|
-
async function sendMessage(cfg, prompt) {
|
|
249
|
-
// Ensure thread exists
|
|
833
|
+
// ─── Chat ─────────────────────────────────────────────────────────────────────
|
|
834
|
+
async function chat(cfg, prompt) {
|
|
250
835
|
if (!currentThreadId) {
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
if (!tid) {
|
|
254
|
-
process.stdout.write('\r' + ' '.repeat(30) + '\r');
|
|
255
|
-
console.log(chalk_1.default.red(' ✗ Could not create thread. Check connection.'));
|
|
256
|
-
console.log();
|
|
257
|
-
return;
|
|
258
|
-
}
|
|
259
|
-
currentThreadId = tid;
|
|
260
|
-
process.stdout.write('\r' + ' '.repeat(30) + '\r');
|
|
836
|
+
console.log(chalk_1.default.yellow('\n No active thread. Run /new to create one first.\n'));
|
|
837
|
+
return;
|
|
261
838
|
}
|
|
262
|
-
//
|
|
263
|
-
|
|
264
|
-
let
|
|
839
|
+
// Animated spinner: cycles until first delta or error
|
|
840
|
+
const FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
841
|
+
let frameIdx = 0;
|
|
842
|
+
let spinnerActive = false;
|
|
843
|
+
let spinnerTimer = null;
|
|
844
|
+
const startSpinner = (label) => {
|
|
845
|
+
spinnerActive = true;
|
|
846
|
+
frameIdx = 0;
|
|
847
|
+
process.stdout.write(chalk_1.default.dim(` ${FRAMES[0]} ${label}...`));
|
|
848
|
+
spinnerTimer = setInterval(() => {
|
|
849
|
+
if (!spinnerActive)
|
|
850
|
+
return;
|
|
851
|
+
frameIdx = (frameIdx + 1) % FRAMES.length;
|
|
852
|
+
process.stdout.write(`\r ${chalk_1.default.dim(FRAMES[frameIdx] + ' ' + label + '...')}`);
|
|
853
|
+
}, 80);
|
|
854
|
+
};
|
|
855
|
+
const stopSpinner = () => {
|
|
856
|
+
if (!spinnerActive)
|
|
857
|
+
return; // idempotent — don't erase content after spinner stopped
|
|
858
|
+
if (spinnerTimer) {
|
|
859
|
+
clearInterval(spinnerTimer);
|
|
860
|
+
spinnerTimer = null;
|
|
861
|
+
}
|
|
862
|
+
spinnerActive = false;
|
|
863
|
+
process.stdout.write('\r' + ' '.repeat(40) + '\r');
|
|
864
|
+
};
|
|
865
|
+
startSpinner(currentAgentName);
|
|
866
|
+
let runId;
|
|
265
867
|
try {
|
|
266
|
-
const
|
|
267
|
-
|
|
268
|
-
agentMode: 'FAST',
|
|
269
|
-
};
|
|
270
|
-
if (currentAgentId)
|
|
271
|
-
body.agentId = currentAgentId;
|
|
272
|
-
const res = await axios_1.default.post(`${cfg.apiBaseUrl}/api/v1/user/chat/threads/${currentThreadId}/messages`, body, { headers: buildHeaders(cfg), timeout: 30000 });
|
|
273
|
-
runId = res.data?.data?.runId ?? null;
|
|
274
|
-
if (!runId)
|
|
275
|
-
throw new Error('No runId returned');
|
|
868
|
+
const result = await (0, chat_1.sendMessage)(cfg, currentThreadId, prompt, currentAgentId);
|
|
869
|
+
runId = result.runId;
|
|
276
870
|
}
|
|
277
871
|
catch (err) {
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
? `${err.response.status}: ${JSON.stringify(err.response.data)}`
|
|
281
|
-
: err instanceof Error ? err.message : String(err);
|
|
282
|
-
console.log(chalk_1.default.red(` ✗ ${msg}`));
|
|
872
|
+
stopSpinner();
|
|
873
|
+
console.log(chalk_1.default.red(` ✗ ${err instanceof Error ? err.message : String(err)}`));
|
|
283
874
|
console.log();
|
|
284
875
|
return;
|
|
285
876
|
}
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
877
|
+
// Spinner keeps running while waiting for first SSE event
|
|
878
|
+
let firstEvent = true;
|
|
879
|
+
let streamErrored = false;
|
|
880
|
+
const stopSpinnerAndBegin = () => {
|
|
881
|
+
if (!firstEvent)
|
|
882
|
+
return;
|
|
883
|
+
firstEvent = false;
|
|
884
|
+
stopSpinner();
|
|
885
|
+
console.log();
|
|
886
|
+
process.stdout.write(chalk_1.default.dim(' '));
|
|
887
|
+
};
|
|
888
|
+
const streamCbs = {
|
|
889
|
+
onDelta: (text) => {
|
|
890
|
+
stopSpinnerAndBegin();
|
|
891
|
+
process.stdout.write(text);
|
|
892
|
+
},
|
|
893
|
+
onToolCall: (toolName, toolArgs) => {
|
|
894
|
+
stopSpinnerAndBegin();
|
|
895
|
+
const argsStr = JSON.stringify(toolArgs, null, 2)
|
|
896
|
+
.split('\n')
|
|
897
|
+
.map((l, i) => (i === 0 ? l : ' ' + l))
|
|
898
|
+
.join('\n');
|
|
899
|
+
process.stdout.write(`\n\n ${chalk_1.default.dim('⚙')} ${chalk_1.default.cyan(toolName)} ${chalk_1.default.dim(argsStr)}\n `);
|
|
900
|
+
},
|
|
901
|
+
onToolResult: (_toolName, result) => {
|
|
902
|
+
const preview = typeof result === 'string'
|
|
903
|
+
? result.slice(0, 120)
|
|
904
|
+
: JSON.stringify(result ?? '').slice(0, 120);
|
|
905
|
+
process.stdout.write(`\n ${chalk_1.default.dim('→')} ${chalk_1.default.dim(preview)}${(preview.length >= 120 ? chalk_1.default.dim('…') : '')}\n `);
|
|
906
|
+
},
|
|
907
|
+
onToolInterrupt: (toolName, toolArgs) => {
|
|
908
|
+
stopSpinnerAndBegin();
|
|
909
|
+
const argsStr = JSON.stringify(toolArgs);
|
|
910
|
+
process.stdout.write(`\n\n ${chalk_1.default.yellow('⏸')} ${chalk_1.default.yellow(`Tool call requires approval: ${toolName}`)} ${chalk_1.default.dim(argsStr)}\n `);
|
|
911
|
+
},
|
|
912
|
+
onError: (msg) => {
|
|
913
|
+
stopSpinnerAndBegin();
|
|
914
|
+
streamErrored = true;
|
|
915
|
+
process.stdout.write(`\n\n ${chalk_1.default.red('✗')} ${chalk_1.default.red(msg)}\n`);
|
|
916
|
+
},
|
|
917
|
+
};
|
|
290
918
|
try {
|
|
291
|
-
await streamResponse(cfg, currentThreadId, runId);
|
|
919
|
+
await (0, chat_1.streamResponse)(cfg, currentThreadId, runId, streamCbs);
|
|
292
920
|
}
|
|
293
921
|
catch (err) {
|
|
294
|
-
|
|
922
|
+
stopSpinnerAndBegin();
|
|
923
|
+
if (!streamErrored) {
|
|
924
|
+
console.log(chalk_1.default.red(`\n ✗ Stream error: ${err instanceof Error ? err.message : String(err)}`));
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
finally {
|
|
928
|
+
// Ensure spinner cleared even if stream ended without any events
|
|
929
|
+
stopSpinner();
|
|
295
930
|
}
|
|
296
931
|
console.log('\n');
|
|
297
932
|
}
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
break;
|
|
315
|
-
buf += decoder.decode(value, { stream: true });
|
|
316
|
-
const lines = buf.split('\n');
|
|
317
|
-
buf = lines.pop() ?? '';
|
|
318
|
-
for (const line of lines) {
|
|
319
|
-
if (!line.startsWith('data: '))
|
|
320
|
-
continue;
|
|
321
|
-
const raw = line.slice(6).trim();
|
|
322
|
-
if (!raw || raw === '[DONE]')
|
|
323
|
-
continue;
|
|
324
|
-
try {
|
|
325
|
-
const evt = JSON.parse(raw);
|
|
326
|
-
// Text delta → print inline
|
|
327
|
-
const delta = evt.delta ?? evt.text ?? evt.content ?? '';
|
|
328
|
-
if (delta) {
|
|
329
|
-
// First chunk after non-text event → newline indent
|
|
330
|
-
if (lastType !== 'text')
|
|
331
|
-
process.stdout.write('\n ');
|
|
332
|
-
process.stdout.write(delta);
|
|
333
|
-
lastType = 'text';
|
|
334
|
-
}
|
|
335
|
-
else if (evt.type === 'done' || evt.status === 'completed') {
|
|
336
|
-
break;
|
|
337
|
-
}
|
|
338
|
-
else if (evt.error) {
|
|
339
|
-
console.log(chalk_1.default.red(`\n ✗ ${evt.error}`));
|
|
340
|
-
break;
|
|
341
|
-
}
|
|
342
|
-
}
|
|
343
|
-
catch {
|
|
344
|
-
// Non-JSON SSE line — ignore
|
|
345
|
-
}
|
|
346
|
-
}
|
|
933
|
+
const sugg = {
|
|
934
|
+
matched: [],
|
|
935
|
+
selectedIdx: 0,
|
|
936
|
+
renderedLines: 0,
|
|
937
|
+
buffer: '',
|
|
938
|
+
};
|
|
939
|
+
const COL_CMD = 18;
|
|
940
|
+
// Strip ANSI codes to get visual width
|
|
941
|
+
function visibleLen(s) {
|
|
942
|
+
return s.replace(/\x1B\[[0-9;]*m/g, '').length;
|
|
943
|
+
}
|
|
944
|
+
// Build a single suggestion row string
|
|
945
|
+
function buildRow(c, selected) {
|
|
946
|
+
const cmdPadded = c.cmd.padEnd(COL_CMD);
|
|
947
|
+
if (selected) {
|
|
948
|
+
return ' ' + HIGHLIGHT.bold(cmdPadded) + ' ' + HIGHLIGHT(c.desc);
|
|
347
949
|
}
|
|
348
|
-
|
|
950
|
+
return ' ' + chalk_1.default.white(cmdPadded) + ' ' + chalk_1.default.dim(c.desc);
|
|
349
951
|
}
|
|
350
|
-
//
|
|
351
|
-
function
|
|
352
|
-
|
|
353
|
-
Authorization: `Bearer ${cfg.cliToken}`,
|
|
354
|
-
'Content-Type': 'application/json',
|
|
355
|
-
// workspace scope — required by guards
|
|
356
|
-
...(cfg.workspaceId ? { 'x-workspace-id': cfg.workspaceId } : {}),
|
|
357
|
-
};
|
|
952
|
+
// Write all ANSI output in a single syscall to prevent mid-frame flicker
|
|
953
|
+
function flush(s) {
|
|
954
|
+
process.stdout.write(s);
|
|
358
955
|
}
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
956
|
+
function buildEraseBlock() {
|
|
957
|
+
if (sugg.renderedLines === 0)
|
|
958
|
+
return '';
|
|
959
|
+
let s = '';
|
|
960
|
+
for (let i = 0; i < sugg.renderedLines; i++)
|
|
961
|
+
s += '\x1B[1B\x1B[2K';
|
|
962
|
+
s += `\x1B[${sugg.renderedLines}A`;
|
|
963
|
+
return s;
|
|
964
|
+
}
|
|
965
|
+
function eraseSuggestions() {
|
|
966
|
+
if (sugg.renderedLines === 0)
|
|
967
|
+
return;
|
|
968
|
+
flush(buildEraseBlock());
|
|
969
|
+
sugg.renderedLines = 0;
|
|
970
|
+
}
|
|
971
|
+
// Partial update — only rewrite the 2 rows that changed (prev + next selected).
|
|
972
|
+
// Batched into ONE flush call to eliminate flicker.
|
|
973
|
+
function patchSelection(prevIdx, nextIdx) {
|
|
974
|
+
if (sugg.matched.length === 0)
|
|
975
|
+
return;
|
|
976
|
+
const buf = sugg.buffer;
|
|
977
|
+
const promptCursorCol = visibleLen('redai › ' + buf);
|
|
978
|
+
// From prompt line, go to prevIdx row, rewrite it
|
|
979
|
+
const toPrev = prevIdx + 1;
|
|
980
|
+
const toNext = nextIdx - prevIdx; // relative move after rewriting prev
|
|
981
|
+
const backUp = nextIdx + 1; // lines to go back up to prompt
|
|
982
|
+
let out = `\x1B[${toPrev}B\r\x1B[2K` + buildRow(sugg.matched[prevIdx], false);
|
|
983
|
+
if (toNext !== 0) {
|
|
984
|
+
out += `\x1B[${Math.abs(toNext)}${toNext > 0 ? 'B' : 'A'}`;
|
|
380
985
|
}
|
|
986
|
+
out += `\r\x1B[2K` + buildRow(sugg.matched[nextIdx], true);
|
|
987
|
+
out += `\x1B[${backUp}A\r\x1B[${promptCursorCol}C`;
|
|
988
|
+
flush(out);
|
|
381
989
|
}
|
|
382
|
-
function
|
|
383
|
-
|
|
990
|
+
function moveSuggestion(dir) {
|
|
991
|
+
if (sugg.matched.length === 0)
|
|
992
|
+
return;
|
|
993
|
+
const prev = sugg.selectedIdx;
|
|
994
|
+
const next = (prev + dir + sugg.matched.length) % sugg.matched.length;
|
|
995
|
+
sugg.selectedIdx = next;
|
|
996
|
+
// Only patch the 2 changed rows — no full redraw, no flicker
|
|
997
|
+
patchSelection(prev, next);
|
|
384
998
|
}
|
|
385
999
|
// ─── Main REPL ────────────────────────────────────────────────────────────────
|
|
386
1000
|
async function askCommand() {
|
|
@@ -393,139 +1007,267 @@ async function askCommand() {
|
|
|
393
1007
|
console.error(chalk_1.default.red('Session expired. Run `redai login` again.'));
|
|
394
1008
|
process.exit(1);
|
|
395
1009
|
}
|
|
1010
|
+
// Restore last thread for this server from session cache
|
|
1011
|
+
const session = (0, store_1.loadSession)();
|
|
1012
|
+
if (session.threads[cfg.apiBaseUrl]) {
|
|
1013
|
+
currentThreadId = session.threads[cfg.apiBaseUrl];
|
|
1014
|
+
}
|
|
396
1015
|
process.stdout.write('\x1Bc');
|
|
397
1016
|
printBanner(cfg);
|
|
398
|
-
|
|
1017
|
+
if (currentThreadId) {
|
|
1018
|
+
console.log(` ${chalk_1.default.dim('Thread ')} ${chalk_1.default.dim(currentThreadId)}`);
|
|
1019
|
+
console.log();
|
|
1020
|
+
}
|
|
399
1021
|
readline_1.default.emitKeypressEvents(process.stdin);
|
|
400
1022
|
if (process.stdin.isTTY)
|
|
401
1023
|
process.stdin.setRawMode(true);
|
|
402
|
-
|
|
403
|
-
let
|
|
404
|
-
let
|
|
405
|
-
let
|
|
406
|
-
|
|
407
|
-
const
|
|
408
|
-
|
|
409
|
-
|
|
1024
|
+
let buffer = '';
|
|
1025
|
+
let slashMode = false;
|
|
1026
|
+
let busy = false;
|
|
1027
|
+
let inputLines = 1; // terminal rows the current input block occupies
|
|
1028
|
+
const PROMPT_PREFIX = chalk_1.default.cyan('redai') + chalk_1.default.dim(' › ');
|
|
1029
|
+
const CONT_PREFIX = chalk_1.default.dim(' ... '); // continuation prefix for Shift+Enter lines
|
|
1030
|
+
const PROMPT_VIS = 'redai › ';
|
|
1031
|
+
const CONT_VIS = ' ... ';
|
|
1032
|
+
// Build one display string per logical line in buffer
|
|
1033
|
+
const buildInputLines = () => buffer.split('\n').map((part, i) => i === 0 ? PROMPT_PREFIX + part : CONT_PREFIX + part);
|
|
1034
|
+
// Erase the current input block + any suggestion rows, then reprint everything
|
|
1035
|
+
// in a single flush() call to avoid flicker.
|
|
1036
|
+
// After the call, cursor sits at end of the last input line.
|
|
1037
|
+
const reprintInputBlock = (withSuggestions = false) => {
|
|
1038
|
+
const lines = buildInputLines();
|
|
1039
|
+
const newCount = lines.length;
|
|
1040
|
+
const lastLine = lines[newCount - 1];
|
|
1041
|
+
const lastVis = newCount === 1
|
|
1042
|
+
? visibleLen(PROMPT_VIS + buffer.split('\n')[0])
|
|
1043
|
+
: visibleLen(CONT_VIS + buffer.split('\n')[newCount - 1]);
|
|
1044
|
+
let out = '';
|
|
1045
|
+
// Move up to top of old input block (cursor is on last input line)
|
|
1046
|
+
if (inputLines > 1)
|
|
1047
|
+
out += `\x1B[${inputLines - 1}A`;
|
|
1048
|
+
// Erase from here to bottom of screen (wipes input + old suggestions)
|
|
1049
|
+
out += '\r\x1B[J';
|
|
1050
|
+
// Write all input lines
|
|
1051
|
+
out += lines.join('\n');
|
|
1052
|
+
inputLines = newCount;
|
|
1053
|
+
if (withSuggestions && sugg.matched.length > 0) {
|
|
1054
|
+
const rows = sugg.matched.map((c, i) => buildRow(c, i === sugg.selectedIdx));
|
|
1055
|
+
out += '\n' + rows.join('\n');
|
|
1056
|
+
// Jump back up past suggestion rows to last input line
|
|
1057
|
+
out += `\x1B[${rows.length}A`;
|
|
1058
|
+
sugg.renderedLines = rows.length;
|
|
1059
|
+
}
|
|
1060
|
+
else {
|
|
1061
|
+
sugg.renderedLines = 0;
|
|
1062
|
+
}
|
|
1063
|
+
// Position cursor at end of last input line
|
|
1064
|
+
out += `\r\x1B[${lastVis}C`;
|
|
1065
|
+
// lastLine is used only for its length — already captured above
|
|
1066
|
+
void lastLine;
|
|
1067
|
+
flush(out);
|
|
1068
|
+
};
|
|
1069
|
+
const printPrompt = () => {
|
|
1070
|
+
buffer = '';
|
|
1071
|
+
inputLines = 1;
|
|
1072
|
+
process.stdout.write('\n' + PROMPT_PREFIX);
|
|
1073
|
+
};
|
|
1074
|
+
printPrompt();
|
|
1075
|
+
// Execute whatever command is currently in buffer (or selected suggestion)
|
|
1076
|
+
const executeInput = async (input) => {
|
|
1077
|
+
if (!input.startsWith('/')) {
|
|
1078
|
+
busy = true;
|
|
1079
|
+
await chat(cfg, input);
|
|
1080
|
+
busy = false;
|
|
1081
|
+
printPrompt();
|
|
1082
|
+
return;
|
|
1083
|
+
}
|
|
1084
|
+
const cmd = input.split(/\s+/)[0].toLowerCase();
|
|
1085
|
+
busy = true;
|
|
1086
|
+
switch (cmd) {
|
|
1087
|
+
case '/':
|
|
1088
|
+
case '/help':
|
|
1089
|
+
printHelp();
|
|
1090
|
+
break;
|
|
1091
|
+
case '/account':
|
|
1092
|
+
printAccount(cfg);
|
|
1093
|
+
break;
|
|
1094
|
+
case '/agents':
|
|
1095
|
+
await interactiveAgents(cfg);
|
|
1096
|
+
break;
|
|
1097
|
+
case '/permissions':
|
|
1098
|
+
await interactivePermissions();
|
|
1099
|
+
break;
|
|
1100
|
+
case '/mcps':
|
|
1101
|
+
await interactiveMcps();
|
|
1102
|
+
break;
|
|
1103
|
+
case '/threads':
|
|
1104
|
+
await interactiveThreads(cfg);
|
|
1105
|
+
break;
|
|
1106
|
+
case '/thread':
|
|
1107
|
+
printThread();
|
|
1108
|
+
break;
|
|
1109
|
+
case '/new': {
|
|
1110
|
+
process.stdout.write(chalk_1.default.dim('\n Creating thread...'));
|
|
1111
|
+
try {
|
|
1112
|
+
currentThreadId = await (0, chat_1.createThread)(cfg, 'New conversation');
|
|
1113
|
+
const s = (0, store_1.loadSession)();
|
|
1114
|
+
s.threads[cfg.apiBaseUrl] = currentThreadId;
|
|
1115
|
+
(0, store_1.saveSession)(s);
|
|
1116
|
+
process.stdout.write('\r' + ' '.repeat(30) + '\r');
|
|
1117
|
+
console.log(chalk_1.default.green(' ✓ New thread created'));
|
|
1118
|
+
console.log(` ${chalk_1.default.dim('Thread ')} ${currentThreadId}`);
|
|
1119
|
+
console.log();
|
|
1120
|
+
}
|
|
1121
|
+
catch (err) {
|
|
1122
|
+
process.stdout.write('\r' + ' '.repeat(30) + '\r');
|
|
1123
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1124
|
+
console.log(chalk_1.default.red(' ✗ Could not create thread:'), chalk_1.default.dim(msg));
|
|
1125
|
+
console.log();
|
|
1126
|
+
}
|
|
1127
|
+
break;
|
|
1128
|
+
}
|
|
1129
|
+
case '/clear':
|
|
1130
|
+
process.stdout.write('\x1Bc');
|
|
1131
|
+
printBanner(cfg);
|
|
1132
|
+
break;
|
|
1133
|
+
case '/exit':
|
|
1134
|
+
case '/quit':
|
|
1135
|
+
console.log(chalk_1.default.dim('\n Bye!\n'));
|
|
1136
|
+
process.exit(0);
|
|
1137
|
+
default:
|
|
1138
|
+
console.log(chalk_1.default.yellow(`\n Unknown command: ${cmd}`));
|
|
1139
|
+
console.log(chalk_1.default.dim(' Type /help to see available commands.\n'));
|
|
1140
|
+
}
|
|
1141
|
+
busy = false;
|
|
1142
|
+
printPrompt();
|
|
410
1143
|
};
|
|
411
|
-
printPromptWithSuggestionSlot();
|
|
412
1144
|
process.stdin.on('keypress', async (char, key) => {
|
|
413
1145
|
if (busy)
|
|
414
1146
|
return;
|
|
415
|
-
// ── Ctrl-C / Ctrl-D
|
|
1147
|
+
// ── Ctrl-C / Ctrl-D ───────────────────────────────────────────────────────
|
|
416
1148
|
if (key?.ctrl && (key.name === 'c' || key.name === 'd')) {
|
|
1149
|
+
if (sugg.renderedLines > 0)
|
|
1150
|
+
eraseSuggestions();
|
|
417
1151
|
console.log(chalk_1.default.dim('\n\n Bye!\n'));
|
|
418
1152
|
process.exit(0);
|
|
419
1153
|
}
|
|
1154
|
+
// ── Arrow Up ──────────────────────────────────────────────────────────────
|
|
1155
|
+
if (key?.name === 'up') {
|
|
1156
|
+
if (slashMode && sugg.matched.length > 0)
|
|
1157
|
+
moveSuggestion(-1);
|
|
1158
|
+
return;
|
|
1159
|
+
}
|
|
1160
|
+
// ── Arrow Down ────────────────────────────────────────────────────────────
|
|
1161
|
+
if (key?.name === 'down') {
|
|
1162
|
+
if (slashMode && sugg.matched.length > 0)
|
|
1163
|
+
moveSuggestion(1);
|
|
1164
|
+
return;
|
|
1165
|
+
}
|
|
1166
|
+
// ── Escape — close suggestions, keep buffer ───────────────────────────────
|
|
1167
|
+
if (key?.name === 'escape') {
|
|
1168
|
+
if (slashMode) {
|
|
1169
|
+
slashMode = false;
|
|
1170
|
+
sugg.matched = [];
|
|
1171
|
+
sugg.selectedIdx = 0;
|
|
1172
|
+
sugg.buffer = '';
|
|
1173
|
+
reprintInputBlock();
|
|
1174
|
+
}
|
|
1175
|
+
return;
|
|
1176
|
+
}
|
|
1177
|
+
// ── Ctrl+Enter (0x0A, name='enter') — insert newline ─────────────────────
|
|
1178
|
+
// Windows Terminal sends 0x0A for Ctrl+Enter, distinct from Enter (0x0D)
|
|
1179
|
+
if (key?.name === 'enter') {
|
|
1180
|
+
if (slashMode)
|
|
1181
|
+
return;
|
|
1182
|
+
buffer += '\n';
|
|
1183
|
+
reprintInputBlock();
|
|
1184
|
+
return;
|
|
1185
|
+
}
|
|
420
1186
|
// ── Backspace ────────────────────────────────────────────────────────────
|
|
421
1187
|
if (key?.name === 'backspace') {
|
|
422
1188
|
if (buffer.length === 0)
|
|
423
1189
|
return;
|
|
424
1190
|
buffer = buffer.slice(0, -1);
|
|
425
|
-
// Xóa 1 ký tự trên stdout
|
|
426
|
-
process.stdout.write('\b \b');
|
|
427
1191
|
if (slashMode) {
|
|
428
1192
|
if (buffer === '') {
|
|
429
1193
|
slashMode = false;
|
|
430
|
-
clearSuggestions();
|
|
431
|
-
}
|
|
432
|
-
else {
|
|
433
|
-
renderSuggestions(buffer);
|
|
434
|
-
process.stdout.write(buffer); // reprint buffer sau khi render lại
|
|
435
1194
|
}
|
|
1195
|
+
// reprint input + updated suggestions in one shot
|
|
1196
|
+
sugg.matched = buffer === ''
|
|
1197
|
+
? []
|
|
1198
|
+
: COMMANDS.filter(c => c.cmd.startsWith(buffer.toLowerCase()));
|
|
1199
|
+
sugg.selectedIdx = 0;
|
|
1200
|
+
sugg.buffer = buffer;
|
|
1201
|
+
reprintInputBlock(buffer.length > 0);
|
|
436
1202
|
}
|
|
1203
|
+
else {
|
|
1204
|
+
reprintInputBlock();
|
|
1205
|
+
}
|
|
1206
|
+
return;
|
|
1207
|
+
}
|
|
1208
|
+
// ── Tab — fill selected suggestion ───────────────────────────────────────
|
|
1209
|
+
if (key?.name === 'tab' && slashMode && sugg.matched.length > 0) {
|
|
1210
|
+
buffer = sugg.matched[sugg.selectedIdx].cmd;
|
|
1211
|
+
slashMode = false;
|
|
1212
|
+
sugg.matched = [];
|
|
1213
|
+
sugg.selectedIdx = 0;
|
|
1214
|
+
sugg.buffer = '';
|
|
1215
|
+
sugg.renderedLines = 0;
|
|
1216
|
+
reprintInputBlock();
|
|
437
1217
|
return;
|
|
438
1218
|
}
|
|
439
|
-
// ── Enter
|
|
1219
|
+
// ── Enter — submit ────────────────────────────────────────────────────────
|
|
440
1220
|
if (key?.name === 'return') {
|
|
1221
|
+
// Pick selected suggestion if list is open
|
|
1222
|
+
if (slashMode && sugg.matched.length > 0) {
|
|
1223
|
+
buffer = sugg.matched[sugg.selectedIdx].cmd;
|
|
1224
|
+
}
|
|
441
1225
|
const input = buffer.trim();
|
|
1226
|
+
// Erase input block + suggestions
|
|
1227
|
+
let out = '';
|
|
1228
|
+
if (inputLines > 1)
|
|
1229
|
+
out += `\x1B[${inputLines - 1}A`;
|
|
1230
|
+
out += '\r\x1B[J';
|
|
442
1231
|
buffer = '';
|
|
443
1232
|
slashMode = false;
|
|
444
|
-
|
|
1233
|
+
inputLines = 1;
|
|
1234
|
+
sugg.matched = [];
|
|
1235
|
+
sugg.selectedIdx = 0;
|
|
1236
|
+
sugg.renderedLines = 0;
|
|
445
1237
|
if (!input) {
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
}
|
|
449
|
-
if (input.startsWith('/')) {
|
|
450
|
-
const cmd = input.split(' ')[0].toLowerCase();
|
|
451
|
-
busy = true;
|
|
452
|
-
switch (cmd) {
|
|
453
|
-
case '/':
|
|
454
|
-
case '/help':
|
|
455
|
-
printHelp();
|
|
456
|
-
break;
|
|
457
|
-
case '/account':
|
|
458
|
-
printAccount(cfg);
|
|
459
|
-
break;
|
|
460
|
-
case '/agents':
|
|
461
|
-
await printAgentsInteractive(cfg);
|
|
462
|
-
break;
|
|
463
|
-
case '/permissions':
|
|
464
|
-
printPermissions();
|
|
465
|
-
break;
|
|
466
|
-
case '/mcps':
|
|
467
|
-
printMcps();
|
|
468
|
-
break;
|
|
469
|
-
case '/thread':
|
|
470
|
-
printThread();
|
|
471
|
-
break;
|
|
472
|
-
case '/new':
|
|
473
|
-
currentThreadId = null;
|
|
474
|
-
console.log(chalk_1.default.dim('\n Thread reset — next message starts a new conversation.\n'));
|
|
475
|
-
break;
|
|
476
|
-
case '/clear':
|
|
477
|
-
process.stdout.write('\x1Bc');
|
|
478
|
-
printBanner(cfg);
|
|
479
|
-
break;
|
|
480
|
-
case '/exit':
|
|
481
|
-
case '/quit':
|
|
482
|
-
console.log(chalk_1.default.dim('\n Bye!\n'));
|
|
483
|
-
process.exit(0);
|
|
484
|
-
default:
|
|
485
|
-
console.log(chalk_1.default.yellow(`\n Unknown command: ${cmd}`));
|
|
486
|
-
console.log(chalk_1.default.dim(' Type / to see available commands.\n'));
|
|
487
|
-
}
|
|
488
|
-
busy = false;
|
|
489
|
-
printPromptWithSuggestionSlot();
|
|
1238
|
+
// Empty submit — just reprint prompt in-place, no blank line
|
|
1239
|
+
flush(out + PROMPT_PREFIX);
|
|
490
1240
|
return;
|
|
491
1241
|
}
|
|
492
|
-
//
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
return;
|
|
498
|
-
}
|
|
499
|
-
// ── Tab → autocomplete lệnh đầu tiên match ───────────────────────────────
|
|
500
|
-
if (key?.name === 'tab' && slashMode) {
|
|
501
|
-
const query = buffer.toLowerCase();
|
|
502
|
-
const match = CMD_NAMES.find((c) => c.startsWith(query));
|
|
503
|
-
if (match) {
|
|
504
|
-
// Xóa buffer hiện tại trên stdout
|
|
505
|
-
process.stdout.write('\b \b'.repeat(buffer.length));
|
|
506
|
-
buffer = match;
|
|
507
|
-
clearSuggestions();
|
|
508
|
-
slashMode = false;
|
|
509
|
-
process.stdout.write(buffer);
|
|
510
|
-
}
|
|
1242
|
+
// Echo submitted text collapsed to single line, then newline
|
|
1243
|
+
const displayText = input.replace(/\n/g, ' ↵ ');
|
|
1244
|
+
out += PROMPT_PREFIX + displayText + '\n';
|
|
1245
|
+
flush(out);
|
|
1246
|
+
await executeInput(input);
|
|
511
1247
|
return;
|
|
512
1248
|
}
|
|
513
|
-
// ──
|
|
1249
|
+
// ── Regular character ─────────────────────────────────────────────────────
|
|
514
1250
|
if (!char || char.length !== 1)
|
|
515
1251
|
return;
|
|
516
1252
|
buffer += char;
|
|
517
|
-
|
|
518
|
-
|
|
1253
|
+
if (slashMode) {
|
|
1254
|
+
// update matched list then reprint input + suggestions together
|
|
1255
|
+
sugg.matched = COMMANDS.filter(c => c.cmd.startsWith(buffer.toLowerCase()));
|
|
1256
|
+
sugg.selectedIdx = 0;
|
|
1257
|
+
sugg.buffer = buffer;
|
|
1258
|
+
reprintInputBlock(true);
|
|
1259
|
+
return;
|
|
1260
|
+
}
|
|
519
1261
|
if (buffer === '/') {
|
|
520
1262
|
slashMode = true;
|
|
521
|
-
|
|
522
|
-
|
|
1263
|
+
sugg.matched = [...COMMANDS];
|
|
1264
|
+
sugg.selectedIdx = 0;
|
|
1265
|
+
sugg.buffer = buffer;
|
|
1266
|
+
reprintInputBlock(true);
|
|
523
1267
|
return;
|
|
524
1268
|
}
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
process.stdout.write(buffer); // reprint toàn bộ buffer sau suggestion
|
|
528
|
-
}
|
|
1269
|
+
// Plain text — just append char without full reprint (no flicker)
|
|
1270
|
+
process.stdout.write(char);
|
|
529
1271
|
});
|
|
530
1272
|
await new Promise(() => undefined);
|
|
531
1273
|
}
|