@redonvn/cli 0.1.9 → 0.1.11
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 +1102 -279
- 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,44 +149,152 @@ async function printAgents(cfg, rl) {
|
|
|
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
|
-
// Pause readline and wait for selection inline
|
|
150
|
-
rl.pause();
|
|
151
|
-
const answer = await new Promise((resolve) => {
|
|
152
|
-
const tmp = readline_1.default.createInterface({ input: process.stdin, output: process.stdout, terminal: false });
|
|
153
|
-
process.stdout.write(chalk_1.default.dim(' › '));
|
|
154
|
-
tmp.once('line', (line) => { tmp.close(); resolve(line.trim()); });
|
|
155
|
-
});
|
|
156
|
-
rl.resume();
|
|
157
|
-
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();
|
|
158
164
|
console.log();
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
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
|
+
}
|
|
164
172
|
console.log();
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
const
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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
|
+
});
|
|
172
296
|
}
|
|
173
|
-
// ─── /permissions
|
|
297
|
+
// ─── /permissions — interactive TUI ──────────────────────────────────────────
|
|
174
298
|
const TOOL_DESC = {
|
|
175
299
|
Read: 'Read files',
|
|
176
300
|
Write: 'Create/overwrite files',
|
|
@@ -186,44 +310,513 @@ const TOOL_DESC = {
|
|
|
186
310
|
McpCall: 'Call MCP tool',
|
|
187
311
|
McpList: 'List MCP tools',
|
|
188
312
|
};
|
|
189
|
-
|
|
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() {
|
|
190
326
|
const perms = (0, permissions_1.loadPermissions)();
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
console.log(
|
|
198
|
-
|
|
199
|
-
|
|
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
|
+
});
|
|
200
411
|
}
|
|
201
|
-
// ─── /mcps
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
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) => {
|
|
209
439
|
console.log();
|
|
210
|
-
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'));
|
|
211
442
|
console.log();
|
|
212
443
|
if (names.length === 0) {
|
|
213
|
-
console.log(chalk_1.default.dim(' No
|
|
214
|
-
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.'));
|
|
215
445
|
}
|
|
216
446
|
else {
|
|
217
|
-
for (
|
|
218
|
-
console.log(
|
|
447
|
+
for (let i = 0; i < names.length; i++) {
|
|
448
|
+
console.log(buildRow(names[i], i === cursor));
|
|
219
449
|
}
|
|
220
450
|
}
|
|
221
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;
|
|
222
691
|
}
|
|
223
692
|
catch {
|
|
224
|
-
|
|
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.'));
|
|
225
701
|
console.log();
|
|
702
|
+
return;
|
|
226
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
|
+
});
|
|
227
820
|
}
|
|
228
821
|
// ─── /thread ──────────────────────────────────────────────────────────────────
|
|
229
822
|
function printThread() {
|
|
@@ -237,127 +830,171 @@ function printThread() {
|
|
|
237
830
|
}
|
|
238
831
|
console.log();
|
|
239
832
|
}
|
|
240
|
-
// ───
|
|
241
|
-
async function
|
|
242
|
-
try {
|
|
243
|
-
const res = await axios_1.default.post(`${cfg.apiBaseUrl}/api/v1/user/chat/threads`, { title }, { headers: buildHeaders(cfg), timeout: 10000 });
|
|
244
|
-
return res.data?.data?.id ?? null;
|
|
245
|
-
}
|
|
246
|
-
catch {
|
|
247
|
-
return null;
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
// ─── Send message + stream response ───────────────────────────────────────────
|
|
251
|
-
async function sendMessage(cfg, prompt) {
|
|
252
|
-
// Ensure thread exists
|
|
833
|
+
// ─── Chat ─────────────────────────────────────────────────────────────────────
|
|
834
|
+
async function chat(cfg, prompt) {
|
|
253
835
|
if (!currentThreadId) {
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
if (!tid) {
|
|
257
|
-
process.stdout.write('\r' + ' '.repeat(30) + '\r');
|
|
258
|
-
console.log(chalk_1.default.red(' ✗ Could not create thread. Check connection.'));
|
|
259
|
-
console.log();
|
|
260
|
-
return;
|
|
261
|
-
}
|
|
262
|
-
currentThreadId = tid;
|
|
263
|
-
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;
|
|
264
838
|
}
|
|
265
|
-
//
|
|
266
|
-
|
|
267
|
-
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;
|
|
268
867
|
try {
|
|
269
|
-
const
|
|
270
|
-
|
|
271
|
-
agentMode: 'FAST',
|
|
272
|
-
};
|
|
273
|
-
if (currentAgentId)
|
|
274
|
-
body.agentId = currentAgentId;
|
|
275
|
-
const res = await axios_1.default.post(`${cfg.apiBaseUrl}/api/v1/user/chat/threads/${currentThreadId}/messages`, body, { headers: buildHeaders(cfg), timeout: 30000 });
|
|
276
|
-
runId = res.data?.data?.runId ?? null;
|
|
277
|
-
if (!runId)
|
|
278
|
-
throw new Error('No runId returned');
|
|
868
|
+
const result = await (0, chat_1.sendMessage)(cfg, currentThreadId, prompt, currentAgentId);
|
|
869
|
+
runId = result.runId;
|
|
279
870
|
}
|
|
280
871
|
catch (err) {
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
? `${err.response.status}: ${JSON.stringify(err.response.data)}`
|
|
284
|
-
: err instanceof Error ? err.message : String(err);
|
|
285
|
-
console.log(chalk_1.default.red(` ✗ ${msg}`));
|
|
872
|
+
stopSpinner();
|
|
873
|
+
console.log(chalk_1.default.red(` ✗ ${err instanceof Error ? err.message : String(err)}`));
|
|
286
874
|
console.log();
|
|
287
875
|
return;
|
|
288
876
|
}
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
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
|
+
};
|
|
293
918
|
try {
|
|
294
|
-
await streamResponse(cfg, currentThreadId, runId);
|
|
919
|
+
await (0, chat_1.streamResponse)(cfg, currentThreadId, runId, streamCbs);
|
|
295
920
|
}
|
|
296
921
|
catch (err) {
|
|
297
|
-
|
|
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();
|
|
298
930
|
}
|
|
299
931
|
console.log('\n');
|
|
300
932
|
}
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
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);
|
|
949
|
+
}
|
|
950
|
+
return ' ' + chalk_1.default.white(cmdPadded) + ' ' + chalk_1.default.dim(c.desc);
|
|
951
|
+
}
|
|
952
|
+
// Write all ANSI output in a single syscall to prevent mid-frame flicker
|
|
953
|
+
function flush(s) {
|
|
954
|
+
process.stdout.write(s);
|
|
955
|
+
}
|
|
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)
|
|
309
967
|
return;
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
try {
|
|
328
|
-
const evt = JSON.parse(raw);
|
|
329
|
-
// Text delta → print inline
|
|
330
|
-
const delta = evt.delta ?? evt.text ?? evt.content ?? '';
|
|
331
|
-
if (delta) {
|
|
332
|
-
// First chunk after non-text event → newline indent
|
|
333
|
-
if (lastType !== 'text')
|
|
334
|
-
process.stdout.write('\n ');
|
|
335
|
-
process.stdout.write(delta);
|
|
336
|
-
lastType = 'text';
|
|
337
|
-
}
|
|
338
|
-
else if (evt.type === 'done' || evt.status === 'completed') {
|
|
339
|
-
break;
|
|
340
|
-
}
|
|
341
|
-
else if (evt.error) {
|
|
342
|
-
console.log(chalk_1.default.red(`\n ✗ ${evt.error}`));
|
|
343
|
-
break;
|
|
344
|
-
}
|
|
345
|
-
}
|
|
346
|
-
catch {
|
|
347
|
-
// Non-JSON SSE line — ignore
|
|
348
|
-
}
|
|
349
|
-
}
|
|
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'}`;
|
|
350
985
|
}
|
|
351
|
-
|
|
986
|
+
out += `\r\x1B[2K` + buildRow(sugg.matched[nextIdx], true);
|
|
987
|
+
out += `\x1B[${backUp}A\r\x1B[${promptCursorCol}C`;
|
|
988
|
+
flush(out);
|
|
352
989
|
}
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
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);
|
|
361
998
|
}
|
|
362
999
|
// ─── Main REPL ────────────────────────────────────────────────────────────────
|
|
363
1000
|
async function askCommand() {
|
|
@@ -370,81 +1007,267 @@ async function askCommand() {
|
|
|
370
1007
|
console.error(chalk_1.default.red('Session expired. Run `redai login` again.'));
|
|
371
1008
|
process.exit(1);
|
|
372
1009
|
}
|
|
373
|
-
|
|
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
|
+
}
|
|
1015
|
+
process.stdout.write('\x1Bc');
|
|
374
1016
|
printBanner(cfg);
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
1017
|
+
if (currentThreadId) {
|
|
1018
|
+
console.log(` ${chalk_1.default.dim('Thread ')} ${chalk_1.default.dim(currentThreadId)}`);
|
|
1019
|
+
console.log();
|
|
1020
|
+
}
|
|
1021
|
+
readline_1.default.emitKeypressEvents(process.stdin);
|
|
1022
|
+
if (process.stdin.isTTY)
|
|
1023
|
+
process.stdin.setRawMode(true);
|
|
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();
|
|
1143
|
+
};
|
|
1144
|
+
process.stdin.on('keypress', async (char, key) => {
|
|
1145
|
+
if (busy)
|
|
386
1146
|
return;
|
|
1147
|
+
// ── Ctrl-C / Ctrl-D ───────────────────────────────────────────────────────
|
|
1148
|
+
if (key?.ctrl && (key.name === 'c' || key.name === 'd')) {
|
|
1149
|
+
if (sugg.renderedLines > 0)
|
|
1150
|
+
eraseSuggestions();
|
|
1151
|
+
console.log(chalk_1.default.dim('\n\n Bye!\n'));
|
|
1152
|
+
process.exit(0);
|
|
387
1153
|
}
|
|
388
|
-
// ──
|
|
389
|
-
if (
|
|
390
|
-
|
|
391
|
-
|
|
1154
|
+
// ── Arrow Up ──────────────────────────────────────────────────────────────
|
|
1155
|
+
if (key?.name === 'up') {
|
|
1156
|
+
if (slashMode && sugg.matched.length > 0)
|
|
1157
|
+
moveSuggestion(-1);
|
|
392
1158
|
return;
|
|
393
1159
|
}
|
|
394
|
-
// ──
|
|
395
|
-
if (
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
switch (cmd) {
|
|
399
|
-
case '/help':
|
|
400
|
-
printHelp();
|
|
401
|
-
break;
|
|
402
|
-
case '/account':
|
|
403
|
-
printAccount(cfg);
|
|
404
|
-
break;
|
|
405
|
-
case '/agents':
|
|
406
|
-
await printAgents(cfg, rl);
|
|
407
|
-
break;
|
|
408
|
-
case '/permissions':
|
|
409
|
-
printPermissions();
|
|
410
|
-
break;
|
|
411
|
-
case '/mcps':
|
|
412
|
-
printMcps();
|
|
413
|
-
break;
|
|
414
|
-
case '/thread':
|
|
415
|
-
printThread();
|
|
416
|
-
break;
|
|
417
|
-
case '/new':
|
|
418
|
-
currentThreadId = null;
|
|
419
|
-
console.log(chalk_1.default.dim('\n Thread reset — next message starts a new conversation.\n'));
|
|
420
|
-
break;
|
|
421
|
-
case '/clear':
|
|
422
|
-
process.stdout.write('\x1Bc');
|
|
423
|
-
printBanner(cfg);
|
|
424
|
-
break;
|
|
425
|
-
case '/exit':
|
|
426
|
-
case '/quit':
|
|
427
|
-
console.log(chalk_1.default.dim('\n Bye!\n'));
|
|
428
|
-
rl.close();
|
|
429
|
-
process.exit(0);
|
|
430
|
-
break;
|
|
431
|
-
default:
|
|
432
|
-
console.log(chalk_1.default.yellow(`\n Unknown command: ${cmd}`));
|
|
433
|
-
console.log(chalk_1.default.dim(' Type / to see available commands.\n'));
|
|
434
|
-
}
|
|
435
|
-
rl.resume();
|
|
436
|
-
rl.prompt();
|
|
1160
|
+
// ── Arrow Down ────────────────────────────────────────────────────────────
|
|
1161
|
+
if (key?.name === 'down') {
|
|
1162
|
+
if (slashMode && sugg.matched.length > 0)
|
|
1163
|
+
moveSuggestion(1);
|
|
437
1164
|
return;
|
|
438
1165
|
}
|
|
439
|
-
// ──
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
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
|
+
}
|
|
1186
|
+
// ── Backspace ────────────────────────────────────────────────────────────
|
|
1187
|
+
if (key?.name === 'backspace') {
|
|
1188
|
+
if (buffer.length === 0)
|
|
1189
|
+
return;
|
|
1190
|
+
buffer = buffer.slice(0, -1);
|
|
1191
|
+
if (slashMode) {
|
|
1192
|
+
if (buffer === '') {
|
|
1193
|
+
slashMode = false;
|
|
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);
|
|
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();
|
|
1217
|
+
return;
|
|
1218
|
+
}
|
|
1219
|
+
// ── Enter — submit ────────────────────────────────────────────────────────
|
|
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
|
+
}
|
|
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';
|
|
1231
|
+
buffer = '';
|
|
1232
|
+
slashMode = false;
|
|
1233
|
+
inputLines = 1;
|
|
1234
|
+
sugg.matched = [];
|
|
1235
|
+
sugg.selectedIdx = 0;
|
|
1236
|
+
sugg.renderedLines = 0;
|
|
1237
|
+
if (!input) {
|
|
1238
|
+
// Empty submit — just reprint prompt in-place, no blank line
|
|
1239
|
+
flush(out + PROMPT_PREFIX);
|
|
1240
|
+
return;
|
|
1241
|
+
}
|
|
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);
|
|
1247
|
+
return;
|
|
1248
|
+
}
|
|
1249
|
+
// ── Regular character ─────────────────────────────────────────────────────
|
|
1250
|
+
if (!char || char.length !== 1)
|
|
1251
|
+
return;
|
|
1252
|
+
buffer += char;
|
|
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
|
+
}
|
|
1261
|
+
if (buffer === '/') {
|
|
1262
|
+
slashMode = true;
|
|
1263
|
+
sugg.matched = [...COMMANDS];
|
|
1264
|
+
sugg.selectedIdx = 0;
|
|
1265
|
+
sugg.buffer = buffer;
|
|
1266
|
+
reprintInputBlock(true);
|
|
1267
|
+
return;
|
|
1268
|
+
}
|
|
1269
|
+
// Plain text — just append char without full reprint (no flicker)
|
|
1270
|
+
process.stdout.write(char);
|
|
448
1271
|
});
|
|
449
1272
|
await new Promise(() => undefined);
|
|
450
1273
|
}
|