neagen 0.1.0 → 0.1.2

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.
@@ -0,0 +1,399 @@
1
+ // Product Neagen TUI: Eve's terminal runner, Neagen's authenticated /api/turns
2
+ // transport. This keeps Eve's TUI/approval/tool rendering while preserving the
3
+ // app's wallet, memory, billing, conversation, and model-selection lifecycle.
4
+
5
+ import { createRequire } from "node:module";
6
+ import { dirname, join } from "node:path";
7
+ import { pathToFileURL } from "node:url";
8
+ import { assertTransportSafe, clearCreds, loadToken, resolveHost } from "./creds.mjs";
9
+ import {
10
+ getSelectedModel,
11
+ listModels,
12
+ probeHost,
13
+ resolveIdentity,
14
+ selectModel,
15
+ } from "./api.mjs";
16
+ import { runDeviceGrantLogin } from "./device-login.mjs";
17
+ import { parseSseChunk } from "./turn-stream.mjs";
18
+
19
+ const require = createRequire(import.meta.url);
20
+
21
+ function eveInternalUrl(relativePath) {
22
+ const root = dirname(require.resolve("eve/package.json"));
23
+ return pathToFileURL(join(root, relativePath)).href;
24
+ }
25
+
26
+ async function loadEveTuiRunner() {
27
+ const mod = await import(eveInternalUrl("dist/src/cli/dev/tui/runner.js"));
28
+ return mod.EveTUIRunner;
29
+ }
30
+
31
+ function normalizeHost(host) {
32
+ return host.replace(/\/$/, "");
33
+ }
34
+
35
+ function modelAliases(input) {
36
+ const value = input.trim();
37
+ const lower = value.toLowerCase();
38
+ if (lower === "deepseek" || lower === "flash") return "deepseek/deepseek-v4-flash";
39
+ if (lower === "sonnet" || lower === "claude" || lower === "claude-sonnet") {
40
+ return "anthropic/claude-sonnet-4.6";
41
+ }
42
+ return value;
43
+ }
44
+
45
+ function formatModel(model) {
46
+ if (!model) return "unknown";
47
+ const label = model.providerLabel && model.name ? `${model.providerLabel} ${model.name}` : model.id;
48
+ return `${label} (${model.id})`;
49
+ }
50
+
51
+ function agentInfoForModel(model, identity) {
52
+ return {
53
+ agent: {
54
+ name: identity?.neagenName ?? "Neagen",
55
+ model: {
56
+ id: model?.id ?? "unknown",
57
+ provider: model?.provider ?? model?.providerLabel,
58
+ },
59
+ },
60
+ diagnostics: {
61
+ discoveryErrors: 0,
62
+ discoveryWarnings: 0,
63
+ },
64
+ };
65
+ }
66
+
67
+ function inputRequestToolName(request) {
68
+ return request?.action?.toolName ?? "input";
69
+ }
70
+
71
+ function isNeagenCommitEvent(eventName) {
72
+ return eventName === "neagen.finalized" ||
73
+ eventName === "neagen.waiting_saved" ||
74
+ eventName === "neagen.gate_declined";
75
+ }
76
+
77
+ function neagenErrorToSessionFailed(data) {
78
+ return {
79
+ type: "session.failed",
80
+ data: {
81
+ error: {
82
+ code: data?.code ?? "turn_failed",
83
+ message: data?.message ?? "Turn failed.",
84
+ },
85
+ },
86
+ };
87
+ }
88
+
89
+ function isBoundaryEvent(event) {
90
+ return event?.type === "session.waiting" ||
91
+ event?.type === "session.completed" ||
92
+ event?.type === "session.failed";
93
+ }
94
+
95
+ async function* readProductTurnEvents(response, onProductEvent) {
96
+ if (!response.ok) {
97
+ let message = `HTTP ${response.status}`;
98
+ const text = await response.text();
99
+ try {
100
+ message = JSON.parse(text)?.error?.message ?? message;
101
+ } catch {
102
+ if (text.trim()) message = text.trim();
103
+ }
104
+ throw new Error(message);
105
+ }
106
+
107
+ const reader = response.body?.getReader();
108
+ if (!reader) throw new Error("Turn stream is not readable.");
109
+
110
+ const decoder = new TextDecoder();
111
+ let buffer = "";
112
+ let heldBoundary;
113
+
114
+ async function* handleEntry(entry) {
115
+ const { event, data } = entry;
116
+
117
+ if (event === "done") {
118
+ if (heldBoundary) {
119
+ yield heldBoundary;
120
+ heldBoundary = undefined;
121
+ }
122
+ return;
123
+ }
124
+
125
+ if (event === "neagen.error") {
126
+ yield neagenErrorToSessionFailed(data);
127
+ heldBoundary = undefined;
128
+ return;
129
+ }
130
+
131
+ if (isNeagenCommitEvent(event)) {
132
+ onProductEvent(event, data);
133
+ if (heldBoundary) {
134
+ yield heldBoundary;
135
+ heldBoundary = undefined;
136
+ }
137
+ return;
138
+ }
139
+
140
+ const eveEvent = { type: event, data };
141
+ if (isBoundaryEvent(eveEvent)) {
142
+ heldBoundary = eveEvent;
143
+ return;
144
+ }
145
+ yield eveEvent;
146
+ }
147
+
148
+ while (true) {
149
+ const { done, value } = await reader.read();
150
+ if (done) break;
151
+ buffer += decoder.decode(value, { stream: true });
152
+ const parsed = parseSseChunk(buffer);
153
+ buffer = parsed.trailing;
154
+ for (const entry of parsed.events) {
155
+ yield* handleEntry(entry);
156
+ }
157
+ }
158
+
159
+ if (buffer.trim()) {
160
+ const parsed = parseSseChunk(`${buffer}\n\n`);
161
+ for (const entry of parsed.events) {
162
+ yield* handleEntry(entry);
163
+ }
164
+ }
165
+
166
+ if (heldBoundary) yield heldBoundary;
167
+ }
168
+
169
+ class ProductMessageResponse {
170
+ constructor(events) {
171
+ this.events = events;
172
+ }
173
+
174
+ [Symbol.asyncIterator]() {
175
+ return this.events[Symbol.asyncIterator]();
176
+ }
177
+ }
178
+
179
+ class ProductSession {
180
+ constructor(client, state = {}) {
181
+ this.client = client;
182
+ this.conversationId = state.conversationId;
183
+ }
184
+
185
+ get state() {
186
+ return {
187
+ sessionId: this.conversationId,
188
+ streamIndex: 0,
189
+ conversationId: this.conversationId,
190
+ };
191
+ }
192
+
193
+ async send(input) {
194
+ const payload = typeof input === "string" ? { message: input } : (input ?? {});
195
+ const body = payload.inputResponses?.length
196
+ ? { conversationId: this.conversationId, inputResponses: payload.inputResponses }
197
+ : { conversationId: this.conversationId, body: payload.message };
198
+
199
+ if (!body.body && !body.inputResponses) {
200
+ throw new Error("Nothing to send.");
201
+ }
202
+ if (body.inputResponses && !body.conversationId) {
203
+ throw new Error("No Neagen conversation is waiting for approval.");
204
+ }
205
+
206
+ const response = await fetch(`${this.client.host}/api/turns`, {
207
+ method: "POST",
208
+ headers: {
209
+ authorization: `Bearer ${this.client.token}`,
210
+ "content-type": "application/json",
211
+ accept: "text/event-stream",
212
+ },
213
+ body: JSON.stringify(body),
214
+ signal: payload.signal,
215
+ });
216
+
217
+ const events = readProductTurnEvents(response, (event, data) => {
218
+ if (data?.conversationId) this.conversationId = data.conversationId;
219
+ if (event === "neagen.gate_declined" && data?.conversationId) {
220
+ this.conversationId = data.conversationId;
221
+ }
222
+ });
223
+ return new ProductMessageResponse(events);
224
+ }
225
+ }
226
+
227
+ class ProductClient {
228
+ constructor({ host, token, identity }) {
229
+ this.host = normalizeHost(host);
230
+ this.token = token;
231
+ this.identity = identity;
232
+ this.cachedSelection = undefined;
233
+ }
234
+
235
+ session(state) {
236
+ return new ProductSession(this, state);
237
+ }
238
+
239
+ async selectedModel() {
240
+ const selection = await getSelectedModel(this.host, this.token);
241
+ this.cachedSelection = selection;
242
+ return selection.activeModel;
243
+ }
244
+
245
+ async info() {
246
+ let model;
247
+ try {
248
+ model = await this.selectedModel();
249
+ } catch {
250
+ model = this.cachedSelection?.activeModel;
251
+ }
252
+ return agentInfoForModel(model, this.identity);
253
+ }
254
+ }
255
+
256
+ function modelOptions(models, activeId) {
257
+ return models.map((model) => ({
258
+ value: model.id,
259
+ label: `${model.id === activeId ? "● " : ""}${model.name ?? model.id}`,
260
+ description: `${model.providerLabel ?? model.provider ?? ""} · ${model.id}`,
261
+ }));
262
+ }
263
+
264
+ function createModelCommandHandler({ host, token, productClient, title }) {
265
+ return {
266
+ async handle(command, context) {
267
+ if (command.name !== "model") {
268
+ return { message: `/${command.name} is not supported in the Neagen TUI.` };
269
+ }
270
+
271
+ const raw = command.argument.trim();
272
+ if (raw) {
273
+ const updated = await selectModel(host, token, modelAliases(raw));
274
+ productClient.cachedSelection = updated;
275
+ context.renderer.renderAgentHeader?.({
276
+ name: title,
277
+ serverUrl: host,
278
+ info: agentInfoForModel(updated.activeModel, productClient.identity),
279
+ });
280
+ return { message: `Switched model to ${formatModel(updated.activeModel)}.` };
281
+ }
282
+
283
+ const [selection, models] = await Promise.all([
284
+ getSelectedModel(host, token),
285
+ listModels(host, token),
286
+ ]);
287
+ productClient.cachedSelection = selection;
288
+ const activeId = selection.activeModel?.id;
289
+ const flow = context.renderer.setupFlow;
290
+ if (!flow) {
291
+ return {
292
+ message: `Current model: ${formatModel(selection.activeModel)}. Use /model provider/model to switch.`,
293
+ };
294
+ }
295
+
296
+ flow.begin("Choose Neagen model", "pulse");
297
+ try {
298
+ const chosen = await flow.readSelect({
299
+ kind: "search",
300
+ message: "Pick the model Neagen should use",
301
+ placeholder: "Filter models",
302
+ initialValue: activeId,
303
+ options: modelOptions(models, activeId),
304
+ });
305
+ const modelId = chosen?.[0];
306
+ if (!modelId) return { message: "/model cancelled." };
307
+ if (modelId === activeId) return { message: `Already using ${formatModel(selection.activeModel)}.` };
308
+ const updated = await selectModel(host, token, modelId);
309
+ productClient.cachedSelection = updated;
310
+ context.renderer.renderAgentHeader?.({
311
+ name: title,
312
+ serverUrl: host,
313
+ info: agentInfoForModel(updated.activeModel, productClient.identity),
314
+ });
315
+ return { message: `Switched model to ${formatModel(updated.activeModel)}.` };
316
+ } finally {
317
+ flow.end({ preserveDiagnostics: false });
318
+ }
319
+ },
320
+ };
321
+ }
322
+
323
+ async function ensureReachable(host) {
324
+ process.stdout.write(`Connecting to ${host}...\n`);
325
+ await probeHost(host);
326
+ }
327
+
328
+ async function ensureAuthenticated({ host, profile }) {
329
+ let token = loadToken(profile, host);
330
+ if (token) {
331
+ const identity = await resolveIdentity(host, token);
332
+ if (identity) return { token, identity };
333
+ clearCreds(profile);
334
+ token = null;
335
+ }
336
+ token = await runDeviceGrantLogin({ host, profile });
337
+ const identity = await resolveIdentity(host, token);
338
+ if (!identity) {
339
+ clearCreds(profile);
340
+ throw new Error("Session could not be verified after login.");
341
+ }
342
+ return { token, identity };
343
+ }
344
+
345
+ export async function runProductTui({ profile } = {}) {
346
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
347
+ throw new Error("neagen requires an interactive terminal.");
348
+ }
349
+
350
+ const host = normalizeHost(resolveHost());
351
+ assertTransportSafe(host);
352
+ await ensureReachable(host);
353
+ const { token, identity } = await ensureAuthenticated({ host, profile });
354
+ const EveTUIRunner = await loadEveTuiRunner();
355
+ const client = new ProductClient({ host, token, identity });
356
+ const title = identity.neagenName ?? "neagen";
357
+ const session = client.session();
358
+
359
+ const selected = await client.selectedModel().catch(() => undefined);
360
+ process.stdout.write(`Agent online. Model: ${selected?.id ?? "unknown"}.\n`);
361
+
362
+ await new EveTUIRunner({
363
+ session,
364
+ client,
365
+ serverUrl: host,
366
+ name: title,
367
+ tools: "full",
368
+ reasoning: "full",
369
+ assistantResponseStats: "tokensPerSecond",
370
+ promptCommandHandler: createModelCommandHandler({ host, token, productClient: client, title }),
371
+ availablePromptCommands: [
372
+ { name: "help", aliases: [], description: "Show available commands", takesArgument: false },
373
+ { name: "new", aliases: [], description: "Start a fresh session", takesArgument: false },
374
+ {
375
+ name: "model",
376
+ aliases: [],
377
+ description: "Pick the Neagen model",
378
+ argumentHint: "[provider/model]",
379
+ takesArgument: true,
380
+ },
381
+ {
382
+ name: "loglevel",
383
+ aliases: [],
384
+ description: "Show or hide logs",
385
+ argumentHint: "[all|stderr|sandbox|none]",
386
+ takesArgument: true,
387
+ },
388
+ { name: "exit", aliases: ["quit"], description: "Quit", takesArgument: false },
389
+ ],
390
+ }).run();
391
+ }
392
+
393
+ export {
394
+ createModelCommandHandler,
395
+ ProductClient,
396
+ ProductSession,
397
+ readProductTurnEvents,
398
+ inputRequestToolName,
399
+ };
package/lib/picker.mjs ADDED
@@ -0,0 +1,280 @@
1
+ // Interactive terminal picker — type to filter, arrows to move, enter to select.
2
+
3
+ import { emitKeypressEvents } from "node:readline";
4
+ import { stdin, stdout } from "node:process";
5
+ import { dim, bold } from "./ansi.mjs";
6
+ import { slashCommandMatches, slashCompletionSuffix } from "./slash.mjs";
7
+
8
+ const DEFAULT_WINDOW = 10;
9
+
10
+ /**
11
+ * @param {import("node:readline/promises").Interface} rl
12
+ * @param {object} options
13
+ * @param {readonly unknown[]} options.items
14
+ * @param {(item: unknown, filter: string) => boolean} options.matchItem
15
+ * @param {(item: unknown, selected: boolean, active: boolean) => string} options.formatRow
16
+ * @param {string[]} options.headerLines
17
+ * @param {number} [options.windowSize]
18
+ * @param {number} [options.initialIndex]
19
+ */
20
+ export function runPicker(rl, options) {
21
+ const {
22
+ items,
23
+ matchItem,
24
+ formatRow,
25
+ headerLines,
26
+ windowSize = DEFAULT_WINDOW,
27
+ initialIndex = 0,
28
+ } = options;
29
+
30
+ return new Promise((resolve) => {
31
+ let filter = "";
32
+ let view = [...items];
33
+ let index = Math.min(Math.max(0, initialIndex), Math.max(0, view.length - 1));
34
+ let offset = Math.max(0, Math.min(index - 2, Math.max(0, view.length - windowSize)));
35
+
36
+ const refilter = () => {
37
+ view = items.filter((item) => matchItem(item, filter));
38
+ index = 0;
39
+ offset = 0;
40
+ };
41
+
42
+ rl.pause();
43
+ const prevRaw = stdin.isRaw;
44
+ const savedKeypress = stdin.listeners("keypress");
45
+ stdin.removeAllListeners("keypress");
46
+ emitKeypressEvents(stdin);
47
+ if (stdin.isTTY) stdin.setRawMode(true);
48
+ stdin.resume();
49
+
50
+ let lastLines = 0;
51
+ const clearRender = () => {
52
+ if (lastLines > 0) stdout.write(`\x1b[${lastLines}A\x1b[J`);
53
+ };
54
+ const render = () => {
55
+ clearRender();
56
+ const lines = [...headerLines, dim(` filter: ${filter || "(all)"} · ${view.length} match`)];
57
+ if (view.length === 0) {
58
+ lines.push(dim(" no matches"));
59
+ } else {
60
+ for (let i = offset; i < Math.min(offset + windowSize, view.length); i++) {
61
+ lines.push(formatRow(view[i], i === index, false));
62
+ }
63
+ }
64
+ stdout.write(`${lines.join("\n")}\n`);
65
+ lastLines = lines.length;
66
+ };
67
+
68
+ const finish = (result) => {
69
+ stdin.removeListener("keypress", onKey);
70
+ clearRender();
71
+ if (stdin.isTTY) stdin.setRawMode(prevRaw);
72
+ stdin.removeAllListeners("keypress");
73
+ for (const listener of savedKeypress) stdin.on("keypress", listener);
74
+ rl.resume();
75
+ resolve(result);
76
+ };
77
+
78
+ function onKey(str, key) {
79
+ if (!key) return;
80
+ if (key.ctrl && key.name === "c") return finish(null);
81
+ if (key.name === "escape") return finish(null);
82
+ if (key.name === "return" || key.name === "enter") return finish(view[index] ?? null);
83
+ if (key.name === "up") {
84
+ if (index > 0) index--;
85
+ } else if (key.name === "down") {
86
+ if (index < view.length - 1) index++;
87
+ } else if (key.name === "backspace") {
88
+ filter = filter.slice(0, -1);
89
+ refilter();
90
+ } else if (str && str.length === 1 && !key.ctrl && !key.meta && str >= " ") {
91
+ filter += str;
92
+ refilter();
93
+ } else {
94
+ return;
95
+ }
96
+ if (index < offset) offset = index;
97
+ if (index >= offset + windowSize) offset = index - windowSize + 1;
98
+ render();
99
+ }
100
+
101
+ stdin.on("keypress", onKey);
102
+ render();
103
+ });
104
+ }
105
+
106
+ export function pickModel(rl, models, activeId) {
107
+ const initialIndex = Math.max(0, models.findIndex((model) => model.id === activeId));
108
+ return runPicker(rl, {
109
+ items: models,
110
+ initialIndex,
111
+ headerLines: [dim(" pick a model — type to filter · ↑/↓ move · enter select · esc cancel")],
112
+ matchItem: (model, filter) => {
113
+ const haystack = `${model.name ?? ""} ${model.providerLabel ?? ""} ${model.id}`.toLowerCase();
114
+ return haystack.includes(filter.toLowerCase());
115
+ },
116
+ formatRow: (model, selected, _active) => {
117
+ const arrow = selected ? bold("›") : " ";
118
+ const mark = model.id === activeId ? "●" : " ";
119
+ const name = selected ? bold(model.name ?? model.id) : (model.name ?? model.id);
120
+ const meta = dim(
121
+ `${model.providerLabel ?? ""} · $${model.inputUsdPerMillionTokens ?? "?"}/$${model.outputUsdPerMillionTokens ?? "?"} per M`,
122
+ );
123
+ return ` ${arrow} ${mark} ${name} ${meta}`;
124
+ },
125
+ });
126
+ }
127
+
128
+ export function pickSlashCommand(rl, commands) {
129
+ return runPicker(rl, {
130
+ items: commands,
131
+ headerLines: [dim(" pick a command — type to filter · ↑/↓ move · enter select · esc cancel")],
132
+ matchItem: (entry, filter) => {
133
+ const haystack = `${entry.cmd} ${entry.help}`.toLowerCase();
134
+ return haystack.includes(filter.toLowerCase());
135
+ },
136
+ formatRow: (entry, selected) => {
137
+ const arrow = selected ? bold("›") : " ";
138
+ const cmd = selected ? bold(entry.cmd) : entry.cmd;
139
+ return ` ${arrow} ${cmd.padEnd(11)} ${dim(entry.help)}`;
140
+ },
141
+ });
142
+ }
143
+
144
+ export function readPromptLine(rl, options) {
145
+ const {
146
+ prompt,
147
+ slashCommands = [],
148
+ } = options;
149
+
150
+ return new Promise((resolve, reject) => {
151
+ let buffer = "";
152
+ let selectedIndex = 0;
153
+ let matches = [];
154
+ let lastLines = 0;
155
+
156
+ function visibleSlashMatches() {
157
+ if (!buffer.startsWith("/") || buffer.includes(" ")) return [];
158
+ return slashCommandMatches(buffer, slashCommands);
159
+ }
160
+
161
+ function clampSelection() {
162
+ if (matches.length === 0) {
163
+ selectedIndex = 0;
164
+ return;
165
+ }
166
+ selectedIndex = Math.max(0, Math.min(selectedIndex, matches.length - 1));
167
+ }
168
+
169
+ function clearRender() {
170
+ if (lastLines > 0) stdout.write(`\x1b[${lastLines}A\x1b[J`);
171
+ }
172
+
173
+ function render() {
174
+ matches = visibleSlashMatches();
175
+ clampSelection();
176
+
177
+ clearRender();
178
+ const suffix = slashCompletionSuffix(buffer);
179
+ const lines = [`${prompt}${buffer}${suffix ? dim(suffix) : ""}`];
180
+
181
+ if (buffer.startsWith("/") && !buffer.includes(" ")) {
182
+ lines.push(dim(" pick a command - type to filter · ↑/↓ move · tab complete · enter select"));
183
+ if (matches.length === 0) {
184
+ lines.push(dim(" no matches"));
185
+ } else {
186
+ for (let i = 0; i < matches.length; i++) {
187
+ const entry = matches[i];
188
+ const arrow = i === selectedIndex ? bold("›") : " ";
189
+ const cmd = i === selectedIndex ? bold(entry.cmd) : entry.cmd;
190
+ lines.push(` ${arrow} ${cmd.padEnd(11)} ${dim(entry.help)}`);
191
+ }
192
+ }
193
+ }
194
+
195
+ stdout.write(`${lines.join("\n")}\n`);
196
+ lastLines = lines.length;
197
+ }
198
+
199
+ function finish(value) {
200
+ stdin.removeListener("keypress", onKey);
201
+ clearRender();
202
+ if (value !== null) stdout.write(`${prompt}${value}\n`);
203
+ if (stdin.isTTY) stdin.setRawMode(prevRaw);
204
+ stdin.removeAllListeners("keypress");
205
+ for (const listener of savedKeypress) stdin.on("keypress", listener);
206
+ rl.resume();
207
+ resolve(value);
208
+ }
209
+
210
+ function acceptSlashSelection() {
211
+ if (!buffer.startsWith("/") || buffer.includes(" ")) return false;
212
+ const suffix = slashCompletionSuffix(buffer);
213
+ if (suffix) {
214
+ buffer += suffix;
215
+ return true;
216
+ }
217
+ if (matches.length === 0) return false;
218
+ buffer = matches[selectedIndex]?.cmd ?? buffer;
219
+ return true;
220
+ }
221
+
222
+ function onKey(str, key) {
223
+ if (!key) return;
224
+ if (key.ctrl && key.name === "c") return finish(null);
225
+ if (key.ctrl && key.name === "d" && buffer.length === 0) return finish(null);
226
+ if (key.name === "return" || key.name === "enter") {
227
+ if (acceptSlashSelection()) return finish(buffer);
228
+ return finish(buffer);
229
+ }
230
+ if (key.name === "tab") {
231
+ const suffix = slashCompletionSuffix(buffer);
232
+ if (suffix) {
233
+ buffer += suffix;
234
+ selectedIndex = 0;
235
+ return render();
236
+ }
237
+ if (acceptSlashSelection()) return render();
238
+ return;
239
+ }
240
+ if (key.name === "escape") {
241
+ buffer = "";
242
+ selectedIndex = 0;
243
+ return render();
244
+ }
245
+ if (key.name === "backspace") {
246
+ buffer = buffer.slice(0, -1);
247
+ selectedIndex = 0;
248
+ return render();
249
+ }
250
+ if (key.name === "up" && matches.length > 0) {
251
+ selectedIndex = Math.max(0, selectedIndex - 1);
252
+ return render();
253
+ }
254
+ if (key.name === "down" && matches.length > 0) {
255
+ selectedIndex = Math.min(matches.length - 1, selectedIndex + 1);
256
+ return render();
257
+ }
258
+ if (str && str.length === 1 && !key.ctrl && !key.meta && str >= " ") {
259
+ buffer += str;
260
+ selectedIndex = 0;
261
+ return render();
262
+ }
263
+ }
264
+
265
+ rl.pause();
266
+ const prevRaw = stdin.isRaw;
267
+ const savedKeypress = stdin.listeners("keypress");
268
+ stdin.removeAllListeners("keypress");
269
+ emitKeypressEvents(stdin);
270
+ if (stdin.isTTY) stdin.setRawMode(true);
271
+ stdin.resume();
272
+
273
+ try {
274
+ stdin.on("keypress", onKey);
275
+ render();
276
+ } catch (error) {
277
+ reject(error);
278
+ }
279
+ });
280
+ }