neagen 0.1.3 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/eve-product-tui.mjs +36 -0
- package/package.json +1 -1
- package/lib/eve-chat.mjs +0 -224
- package/lib/picker.mjs +0 -280
- package/lib/slash.mjs +0 -49
- package/lib/tui.mjs +0 -311
package/lib/eve-product-tui.mjs
CHANGED
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
} from "./api.mjs";
|
|
16
16
|
import { runDeviceGrantLogin } from "./device-login.mjs";
|
|
17
17
|
import { parseSseChunk } from "./turn-stream.mjs";
|
|
18
|
+
import { subscribeInboxWakeAuto } from "./wake.mjs";
|
|
18
19
|
|
|
19
20
|
const require = createRequire(import.meta.url);
|
|
20
21
|
|
|
@@ -28,6 +29,11 @@ async function loadEveTuiRunner() {
|
|
|
28
29
|
return mod.EveTUIRunner;
|
|
29
30
|
}
|
|
30
31
|
|
|
32
|
+
function wakeSyncPrompt(ev) {
|
|
33
|
+
const noun = ev.kind === "message" ? "a text message" : "a file";
|
|
34
|
+
return `[mail] You just received new mail (${noun}) from a connection. Call sync_inbox once to fetch it, then tell me in one short line who it is from and what it is.`;
|
|
35
|
+
}
|
|
36
|
+
|
|
31
37
|
function normalizeHost(host) {
|
|
32
38
|
return host.replace(/\/$/, "");
|
|
33
39
|
}
|
|
@@ -359,6 +365,34 @@ export async function runProductTui({ profile } = {}) {
|
|
|
359
365
|
const selected = await client.selectedModel().catch(() => undefined);
|
|
360
366
|
process.stdout.write(`Agent online. Model: ${selected?.id ?? "unknown"}.\n`);
|
|
361
367
|
|
|
368
|
+
let wakeChain = Promise.resolve();
|
|
369
|
+
let wake = null;
|
|
370
|
+
try {
|
|
371
|
+
wake = await subscribeInboxWakeAuto(identity.ownerId, (ev) => {
|
|
372
|
+
const noun = ev.kind === "message" ? "a message" : `a file (${ev.filename ?? "unknown"})`;
|
|
373
|
+
process.stdout.write(`\nNew mail: ${noun} arrived. Fetching it now...\n`);
|
|
374
|
+
wakeChain = wakeChain
|
|
375
|
+
.then(async () => {
|
|
376
|
+
const syncSession = client.session();
|
|
377
|
+
const response = await syncSession.send({ message: wakeSyncPrompt(ev) });
|
|
378
|
+
for await (const _event of response) {
|
|
379
|
+
// drain the sync turn stream silently in the background
|
|
380
|
+
}
|
|
381
|
+
})
|
|
382
|
+
.catch((e) => {
|
|
383
|
+
process.stderr.write(`! wake sync failed: ${e instanceof Error ? e.message : String(e)}\n`);
|
|
384
|
+
});
|
|
385
|
+
}, { host, bearerToken: token });
|
|
386
|
+
} catch {
|
|
387
|
+
// wake is best-effort
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
const stopWake = async () => {
|
|
391
|
+
try { await wake?.close(); } catch { /* best-effort */ }
|
|
392
|
+
};
|
|
393
|
+
process.on("SIGINT", async () => { await stopWake(); process.exit(0); });
|
|
394
|
+
process.on("exit", () => void wake?.close());
|
|
395
|
+
|
|
362
396
|
await new EveTUIRunner({
|
|
363
397
|
session,
|
|
364
398
|
client,
|
|
@@ -388,6 +422,8 @@ export async function runProductTui({ profile } = {}) {
|
|
|
388
422
|
{ name: "exit", aliases: ["quit"], description: "Quit", takesArgument: false },
|
|
389
423
|
],
|
|
390
424
|
}).run();
|
|
425
|
+
|
|
426
|
+
await stopWake();
|
|
391
427
|
}
|
|
392
428
|
|
|
393
429
|
export {
|
package/package.json
CHANGED
package/lib/eve-chat.mjs
DELETED
|
@@ -1,224 +0,0 @@
|
|
|
1
|
-
import { Client } from "eve/client";
|
|
2
|
-
import {
|
|
3
|
-
createTurnStatusSpinner,
|
|
4
|
-
createTurnStreamRenderer,
|
|
5
|
-
readTurnSseStream,
|
|
6
|
-
} from "./turn-stream.mjs";
|
|
7
|
-
|
|
8
|
-
export function createEveClient(host, token) {
|
|
9
|
-
return new Client({
|
|
10
|
-
host,
|
|
11
|
-
auth: { bearer: token },
|
|
12
|
-
preserveCompletedSessions: true,
|
|
13
|
-
redirect: "manual",
|
|
14
|
-
});
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export function summarizeResult(result) {
|
|
18
|
-
const neagenBody = cleanReply(result?.neagenMessage?.body);
|
|
19
|
-
if (neagenBody) return neagenBody;
|
|
20
|
-
const message = cleanReply(result?.message);
|
|
21
|
-
if (message) return message;
|
|
22
|
-
if (result?.runtimeStatus === "waiting") return "(waiting)";
|
|
23
|
-
if (result?.status) return `(${result.status})`;
|
|
24
|
-
return "(no reply)";
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
function cleanReply(value) {
|
|
28
|
-
if (typeof value !== "string") return undefined;
|
|
29
|
-
const text = value.trim();
|
|
30
|
-
if (!text || text === "undefined" || text === "null") return undefined;
|
|
31
|
-
return text;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export function requestToolName(request) {
|
|
35
|
-
return request?.action?.toolName ?? "input";
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
function preview(value) {
|
|
39
|
-
if (typeof value !== "string") return undefined;
|
|
40
|
-
return value.length > 120 ? `${value.slice(0, 117)}...` : value;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
export function formatRequestDetails(request) {
|
|
44
|
-
const input = request?.action?.input;
|
|
45
|
-
if (!input || typeof input !== "object") return "";
|
|
46
|
-
const parts = [];
|
|
47
|
-
if (typeof input.to === "string") parts.push(`to: ${input.to}`);
|
|
48
|
-
if (typeof input.path === "string") parts.push(`path: ${input.path}`);
|
|
49
|
-
if (typeof input.draftId === "string") parts.push(`draft: ${input.draftId}`);
|
|
50
|
-
const body = preview(input.body);
|
|
51
|
-
if (body !== undefined) parts.push(`body: ${JSON.stringify(body)}`);
|
|
52
|
-
return parts.join(", ");
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
export function pickOption(request, approved) {
|
|
56
|
-
const options = request?.options ?? [];
|
|
57
|
-
const pattern = approved ? /approve|allow|yes|confirm/i : /deny|reject|no|cancel/i;
|
|
58
|
-
return (
|
|
59
|
-
options.find((option) => pattern.test(`${option.id} ${option.label}`)) ??
|
|
60
|
-
options[approved ? 0 : options.length - 1]
|
|
61
|
-
);
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
export function approvalResponse(request, approved) {
|
|
65
|
-
const option = pickOption(request, approved);
|
|
66
|
-
return { requestId: request.requestId, optionId: option?.id ?? (approved ? "approve" : "deny") };
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
export function questionResponse(request, answer) {
|
|
70
|
-
const value = answer.trim();
|
|
71
|
-
const numeric = Number(value);
|
|
72
|
-
const option = Number.isInteger(numeric) ? request.options?.[numeric - 1] : undefined;
|
|
73
|
-
if (option) return { requestId: request.requestId, optionId: option.id };
|
|
74
|
-
|
|
75
|
-
const byText = request.options?.find((entry) => {
|
|
76
|
-
const haystack = `${entry.id} ${entry.label}`.toLowerCase();
|
|
77
|
-
return haystack === value.toLowerCase() || entry.label.toLowerCase() === value.toLowerCase();
|
|
78
|
-
});
|
|
79
|
-
if (byText) return { requestId: request.requestId, optionId: byText.id };
|
|
80
|
-
|
|
81
|
-
return { requestId: request.requestId, text: answer };
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
export function wakeSyncPrompt(event) {
|
|
85
|
-
return (
|
|
86
|
-
`[mail] You just received new mail (${event.kind === "message" ? "a text message" : "a file"}) from a connection. ` +
|
|
87
|
-
"Call sync_inbox once to fetch it, then tell me in one short line who it is from and what it is."
|
|
88
|
-
);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
export async function settleInputRequests(result, session, handlers) {
|
|
92
|
-
let settled = result;
|
|
93
|
-
while ((settled.inputRequests?.length ?? 0) > 0) {
|
|
94
|
-
const request = settled.inputRequests[0];
|
|
95
|
-
const response =
|
|
96
|
-
requestToolName(request) === "ask_question"
|
|
97
|
-
? questionResponse(request, await handlers.askQuestion(request))
|
|
98
|
-
: approvalResponse(request, await handlers.askApproval(request));
|
|
99
|
-
|
|
100
|
-
const resumed = await session.send({ inputResponses: [response] });
|
|
101
|
-
settled = await resumed.result();
|
|
102
|
-
}
|
|
103
|
-
return settled;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
export async function sendEveTurn(session, message, options = {}) {
|
|
107
|
-
const response = await session.send({
|
|
108
|
-
message,
|
|
109
|
-
...(options.clientContext ? { clientContext: options.clientContext } : {}),
|
|
110
|
-
});
|
|
111
|
-
const result = await response.result();
|
|
112
|
-
return options.settle ? options.settle(result, session) : result;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
function resolveTurnPayload(events) {
|
|
116
|
-
const finalized = events.find((entry) => entry.event === "neagen.finalized")?.data;
|
|
117
|
-
const waiting = events.find((entry) => entry.event === "neagen.waiting_saved")?.data;
|
|
118
|
-
const declined = events.find((entry) => entry.event === "neagen.gate_declined")?.data;
|
|
119
|
-
const legacyTurn = events.find((entry) => entry.event === "turn")?.data;
|
|
120
|
-
|
|
121
|
-
if (waiting) {
|
|
122
|
-
return {
|
|
123
|
-
conversationId: waiting.conversationId,
|
|
124
|
-
runtimeStatus: waiting.runtimeStatus,
|
|
125
|
-
inputRequests: waiting.inputRequests,
|
|
126
|
-
};
|
|
127
|
-
}
|
|
128
|
-
if (finalized) return finalized;
|
|
129
|
-
if (declined) return declined;
|
|
130
|
-
if (legacyTurn) return legacyTurn;
|
|
131
|
-
throw new Error("Turn stream ended without a Neagen result.");
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
async function postTurnStream(host, token, body, { render } = {}) {
|
|
135
|
-
const res = await fetch(`${host.replace(/\/$/, "")}/api/turns`, {
|
|
136
|
-
method: "POST",
|
|
137
|
-
headers: {
|
|
138
|
-
authorization: `Bearer ${token}`,
|
|
139
|
-
"content-type": "application/json",
|
|
140
|
-
accept: "text/event-stream",
|
|
141
|
-
},
|
|
142
|
-
body: JSON.stringify(body),
|
|
143
|
-
signal: AbortSignal.timeout(120_000),
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
const renderer = render ?? createTurnStreamRenderer();
|
|
147
|
-
const spinner = createTurnStatusSpinner("thinking");
|
|
148
|
-
let streamError;
|
|
149
|
-
|
|
150
|
-
try {
|
|
151
|
-
const events = await readTurnSseStream(res, (event, data) => {
|
|
152
|
-
spinner.stop();
|
|
153
|
-
if (event === "neagen.error") {
|
|
154
|
-
streamError = new Error(data?.message ?? "Turn failed.");
|
|
155
|
-
}
|
|
156
|
-
renderer.handle(event, data);
|
|
157
|
-
});
|
|
158
|
-
renderer.finish();
|
|
159
|
-
|
|
160
|
-
if (streamError) throw streamError;
|
|
161
|
-
|
|
162
|
-
return {
|
|
163
|
-
result: resolveTurnPayload(events),
|
|
164
|
-
assistantTextStreamed: renderer.assistantTextStreamed,
|
|
165
|
-
};
|
|
166
|
-
} finally {
|
|
167
|
-
spinner.stop();
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
export async function sendWakeSyncTurn({ host, token, event, handlers, render }) {
|
|
172
|
-
return sendTransportTurn({
|
|
173
|
-
host,
|
|
174
|
-
token,
|
|
175
|
-
conversationId: undefined,
|
|
176
|
-
message: wakeSyncPrompt(event),
|
|
177
|
-
handlers,
|
|
178
|
-
render,
|
|
179
|
-
});
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
export async function sendTransportTurn({
|
|
183
|
-
host,
|
|
184
|
-
token,
|
|
185
|
-
conversationId,
|
|
186
|
-
message,
|
|
187
|
-
handlers,
|
|
188
|
-
render,
|
|
189
|
-
}) {
|
|
190
|
-
const renderer = render ?? createTurnStreamRenderer();
|
|
191
|
-
renderer.reset();
|
|
192
|
-
let assistantTextStreamed = false;
|
|
193
|
-
|
|
194
|
-
let leg = await postTurnStream(
|
|
195
|
-
host,
|
|
196
|
-
token,
|
|
197
|
-
{ conversationId, body: message },
|
|
198
|
-
{ render: renderer },
|
|
199
|
-
);
|
|
200
|
-
assistantTextStreamed ||= leg.assistantTextStreamed;
|
|
201
|
-
let result = leg.result;
|
|
202
|
-
|
|
203
|
-
while ((result?.inputRequests?.length ?? 0) > 0) {
|
|
204
|
-
const request = result.inputRequests[0];
|
|
205
|
-
const response =
|
|
206
|
-
requestToolName(request) === "ask_question"
|
|
207
|
-
? questionResponse(request, await handlers.askQuestion(request))
|
|
208
|
-
: approvalResponse(request, await handlers.askApproval(request));
|
|
209
|
-
renderer.reset();
|
|
210
|
-
leg = await postTurnStream(
|
|
211
|
-
host,
|
|
212
|
-
token,
|
|
213
|
-
{
|
|
214
|
-
conversationId: result.conversationId,
|
|
215
|
-
inputResponses: [response],
|
|
216
|
-
},
|
|
217
|
-
{ render: renderer },
|
|
218
|
-
);
|
|
219
|
-
assistantTextStreamed ||= leg.assistantTextStreamed;
|
|
220
|
-
result = leg.result;
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
return { ...result, assistantTextStreamed };
|
|
224
|
-
}
|
package/lib/picker.mjs
DELETED
|
@@ -1,280 +0,0 @@
|
|
|
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
|
-
}
|
package/lib/slash.mjs
DELETED
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
// Slash commands for the neagen TUI — single source for /help and command selection.
|
|
2
|
-
|
|
3
|
-
export const SLASH_COMMANDS = [
|
|
4
|
-
{ cmd: "/model", help: "pick a model — type to filter, arrows to move, enter to select" },
|
|
5
|
-
{ cmd: "/new", help: "start a fresh conversation" },
|
|
6
|
-
{ cmd: "/help", help: "show commands" },
|
|
7
|
-
{ cmd: "/exit", help: "quit (also /quit, Ctrl-D, Ctrl-C)" },
|
|
8
|
-
];
|
|
9
|
-
|
|
10
|
-
/** Hidden alias — completable, not listed in /help. */
|
|
11
|
-
export const SLASH_ALIASES = ["/quit"];
|
|
12
|
-
|
|
13
|
-
export const SLASH_NAMES = [...SLASH_COMMANDS.map((entry) => entry.cmd), ...SLASH_ALIASES];
|
|
14
|
-
|
|
15
|
-
export function slashQuery(line) {
|
|
16
|
-
if (!line.startsWith("/") || line.includes(" ")) return null;
|
|
17
|
-
return line.toLowerCase();
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export function slashCommandMatches(line, commands = SLASH_COMMANDS) {
|
|
21
|
-
const query = slashQuery(line);
|
|
22
|
-
if (query === null) return [];
|
|
23
|
-
return commands.filter((entry) => entry.cmd.toLowerCase().startsWith(query));
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export function slashCompletionSuffix(line) {
|
|
27
|
-
const query = slashQuery(line);
|
|
28
|
-
if (query === null) return "";
|
|
29
|
-
const hits = SLASH_NAMES.filter((name) => name.toLowerCase().startsWith(query));
|
|
30
|
-
if (hits.length !== 1) return "";
|
|
31
|
-
return hits[0].slice(line.length);
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* readline completer: Tab-completes slash commands on the command word only
|
|
36
|
-
* (leading "/" with no space yet) so Tab never hijacks normal chat input.
|
|
37
|
-
*/
|
|
38
|
-
export function completeSlash(line) {
|
|
39
|
-
if (!line.startsWith("/") || line.includes(" ")) return [[], line];
|
|
40
|
-
const query = line.toLowerCase();
|
|
41
|
-
const hits = SLASH_NAMES.filter((name) => name.startsWith(query));
|
|
42
|
-
return [hits, line];
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
export function slashHelpLines() {
|
|
46
|
-
const rows = SLASH_COMMANDS.map(({ cmd, help }) => ` ${cmd.padEnd(11)} ${help}`);
|
|
47
|
-
rows.push(" (type / to open the command selector; Tab accepts the prefill)");
|
|
48
|
-
return rows;
|
|
49
|
-
}
|
package/lib/tui.mjs
DELETED
|
@@ -1,311 +0,0 @@
|
|
|
1
|
-
// neagen CLI — chat through Neagen's server-mediated turn transport.
|
|
2
|
-
// Login happens here (device grant) when no valid session exists.
|
|
3
|
-
|
|
4
|
-
import readline from "node:readline/promises";
|
|
5
|
-
import { stdin } from "node:process";
|
|
6
|
-
import { dim, bold } from "./ansi.mjs";
|
|
7
|
-
import { assertTransportSafe, clearCreds, loadToken, resolveHost } from "./creds.mjs";
|
|
8
|
-
import {
|
|
9
|
-
getSelectedModel,
|
|
10
|
-
listModels,
|
|
11
|
-
probeHost,
|
|
12
|
-
resolveIdentity,
|
|
13
|
-
selectModel,
|
|
14
|
-
} from "./api.mjs";
|
|
15
|
-
import { runDeviceGrantLogin } from "./device-login.mjs";
|
|
16
|
-
import {
|
|
17
|
-
formatRequestDetails,
|
|
18
|
-
requestToolName,
|
|
19
|
-
sendTransportTurn,
|
|
20
|
-
sendWakeSyncTurn,
|
|
21
|
-
summarizeResult,
|
|
22
|
-
} from "./eve-chat.mjs";
|
|
23
|
-
import { createTurnStreamRenderer } from "./turn-stream.mjs";
|
|
24
|
-
import { pickModel, readPromptLine } from "./picker.mjs";
|
|
25
|
-
import { SLASH_COMMANDS, slashHelpLines } from "./slash.mjs";
|
|
26
|
-
import { subscribeInboxWakeAbly } from "./wake.mjs";
|
|
27
|
-
|
|
28
|
-
const profile = process.env.NEAGEN_PROFILE?.trim() || undefined;
|
|
29
|
-
const host = resolveHost();
|
|
30
|
-
|
|
31
|
-
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
32
|
-
console.error("neagen requires an interactive terminal.");
|
|
33
|
-
process.exit(1);
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
try {
|
|
37
|
-
assertTransportSafe(host);
|
|
38
|
-
} catch (e) {
|
|
39
|
-
console.error(`\n ${e.message}\n`);
|
|
40
|
-
process.exit(1);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function fail(message) {
|
|
44
|
-
console.error(`\n ${message}\n`);
|
|
45
|
-
process.exit(1);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
async function ensureReachable() {
|
|
49
|
-
process.stdout.write(`Connecting to ${host}...\n`);
|
|
50
|
-
try {
|
|
51
|
-
await probeHost(host);
|
|
52
|
-
process.stdout.write("Agent online.\n");
|
|
53
|
-
} catch (e) {
|
|
54
|
-
process.stderr.write(`Can't reach ${host} (${e.message}).\n`);
|
|
55
|
-
process.stderr.write("Check your connection, or set NEAGEN_HOST to your server.\n");
|
|
56
|
-
process.exit(1);
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
async function ensureAuthenticated() {
|
|
61
|
-
let token = loadToken(profile);
|
|
62
|
-
if (token) {
|
|
63
|
-
const identity = await resolveIdentity(host, token);
|
|
64
|
-
if (identity) return { token, me: identity };
|
|
65
|
-
clearCreds(profile);
|
|
66
|
-
token = null;
|
|
67
|
-
}
|
|
68
|
-
token = await runDeviceGrantLogin({ host, profile });
|
|
69
|
-
const me = await resolveIdentity(host, token);
|
|
70
|
-
if (!me) {
|
|
71
|
-
clearCreds(profile);
|
|
72
|
-
fail("Session could not be verified after login.");
|
|
73
|
-
}
|
|
74
|
-
return { token, me };
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
function formatModel(model) {
|
|
78
|
-
if (!model) return "unknown";
|
|
79
|
-
const label = model.providerLabel && model.name ? `${model.providerLabel} ${model.name}` : model.id;
|
|
80
|
-
return `${label} (${model.id})`;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
function modelAliases(input) {
|
|
84
|
-
const value = input.trim();
|
|
85
|
-
const lower = value.toLowerCase();
|
|
86
|
-
if (lower === "deepseek" || lower === "flash") return "deepseek/deepseek-v4-flash";
|
|
87
|
-
if (lower === "sonnet" || lower === "claude" || lower === "claude-sonnet") {
|
|
88
|
-
return "anthropic/claude-sonnet-4.6";
|
|
89
|
-
}
|
|
90
|
-
return value;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
async function runModelPicker(rl, token) {
|
|
94
|
-
let selection;
|
|
95
|
-
let models;
|
|
96
|
-
try {
|
|
97
|
-
[selection, models] = await Promise.all([
|
|
98
|
-
getSelectedModel(host, token),
|
|
99
|
-
listModels(host, token),
|
|
100
|
-
]);
|
|
101
|
-
} catch (e) {
|
|
102
|
-
process.stderr.write(`! ${e?.message ?? e}\n`);
|
|
103
|
-
return;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
const activeId = selection?.activeModel?.id;
|
|
107
|
-
const chosen = await pickModel(rl, models, activeId);
|
|
108
|
-
if (!chosen) {
|
|
109
|
-
process.stdout.write(`${dim("(no change)")}\n`);
|
|
110
|
-
return;
|
|
111
|
-
}
|
|
112
|
-
if (chosen.id === activeId) {
|
|
113
|
-
process.stdout.write(`${dim(`already using ${formatModel(chosen)}`)}\n`);
|
|
114
|
-
return;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
try {
|
|
118
|
-
const updated = await selectModel(host, token, chosen.id);
|
|
119
|
-
process.stdout.write(`Switched model to ${bold(formatModel(updated.activeModel))}.\n`);
|
|
120
|
-
} catch (e) {
|
|
121
|
-
process.stderr.write(`! ${e?.message ?? e}\n`);
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
async function switchModelByName(token, raw) {
|
|
126
|
-
const modelId = modelAliases(raw);
|
|
127
|
-
const selection = await selectModel(host, token, modelId);
|
|
128
|
-
process.stdout.write(`Switched model to ${bold(formatModel(selection.activeModel))}.\n`);
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
async function handleModelCommand(rl, token, raw) {
|
|
132
|
-
const arg = raw.trim();
|
|
133
|
-
if (!arg) {
|
|
134
|
-
await runModelPicker(rl, token);
|
|
135
|
-
return;
|
|
136
|
-
}
|
|
137
|
-
await switchModelByName(token, arg);
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
function printHelp() {
|
|
141
|
-
process.stdout.write([...slashHelpLines(), ""].join("\n"));
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
function normalizeSlashInput(input) {
|
|
145
|
-
if (input === "/quit") return "/exit";
|
|
146
|
-
return input;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
async function askApproval(rl, request) {
|
|
150
|
-
const tool = requestToolName(request);
|
|
151
|
-
const details = formatRequestDetails(request);
|
|
152
|
-
const answer = (await rl.question(
|
|
153
|
-
`\nApprove ${tool}${details ? ` (${details})` : ""}?\n(y/n) > `,
|
|
154
|
-
)).trim().toLowerCase();
|
|
155
|
-
return answer === "y" || answer === "yes";
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
async function askQuestion(rl, request) {
|
|
159
|
-
const options = (request.options ?? []).map((option, index) => `${index + 1}. ${option.label}`).join(" ");
|
|
160
|
-
return rl.question(`\n${request.prompt}\n${options ? `${options}\n` : ""}> `);
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
const turnRenderer = createTurnStreamRenderer();
|
|
164
|
-
|
|
165
|
-
function printTurnOutcome(result) {
|
|
166
|
-
if (result?.assistantTextStreamed) {
|
|
167
|
-
process.stdout.write("\n");
|
|
168
|
-
return;
|
|
169
|
-
}
|
|
170
|
-
const summary = summarizeResult(result);
|
|
171
|
-
if (summary && summary !== "(no reply)") {
|
|
172
|
-
process.stdout.write(`\n${summary}\n\n`);
|
|
173
|
-
} else {
|
|
174
|
-
process.stdout.write("\n");
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
await ensureReachable();
|
|
179
|
-
|
|
180
|
-
let session;
|
|
181
|
-
try {
|
|
182
|
-
session = await ensureAuthenticated();
|
|
183
|
-
} catch (e) {
|
|
184
|
-
fail(e.message ?? "Login failed.");
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
const { me } = session;
|
|
188
|
-
const label = me.displayName ?? "you";
|
|
189
|
-
const ownerId = me.ownerId;
|
|
190
|
-
const token = session.token;
|
|
191
|
-
|
|
192
|
-
let conversationId;
|
|
193
|
-
const rl = readline.createInterface({
|
|
194
|
-
input: stdin,
|
|
195
|
-
output: process.stdout,
|
|
196
|
-
historySize: 100,
|
|
197
|
-
});
|
|
198
|
-
|
|
199
|
-
let chain = Promise.resolve();
|
|
200
|
-
function runExclusive(fn) {
|
|
201
|
-
const next = chain.then(fn, fn);
|
|
202
|
-
chain = next.catch(() => {});
|
|
203
|
-
return next;
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
async function sendTurn(message) {
|
|
207
|
-
const result = await sendTransportTurn({
|
|
208
|
-
host,
|
|
209
|
-
token,
|
|
210
|
-
conversationId,
|
|
211
|
-
message,
|
|
212
|
-
render: turnRenderer,
|
|
213
|
-
handlers: {
|
|
214
|
-
askApproval: (request) => askApproval(rl, request),
|
|
215
|
-
askQuestion: (request) => askQuestion(rl, request),
|
|
216
|
-
},
|
|
217
|
-
});
|
|
218
|
-
conversationId = result?.conversationId ?? conversationId;
|
|
219
|
-
return result;
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
let wake;
|
|
223
|
-
try {
|
|
224
|
-
wake = await subscribeInboxWakeAbly(ownerId, (ev) => {
|
|
225
|
-
const what = ev.kind === "message" ? "a message" : `a file (${ev.filename})`;
|
|
226
|
-
process.stdout.write(`\nNew mail: ${what} arrived. Fetching it now...\n`);
|
|
227
|
-
void runExclusive(async () => {
|
|
228
|
-
const result = await sendWakeSyncTurn({
|
|
229
|
-
host,
|
|
230
|
-
token,
|
|
231
|
-
event: ev,
|
|
232
|
-
render: turnRenderer,
|
|
233
|
-
handlers: {
|
|
234
|
-
askApproval: (request) => askApproval(rl, request),
|
|
235
|
-
askQuestion: (request) => askQuestion(rl, request),
|
|
236
|
-
},
|
|
237
|
-
});
|
|
238
|
-
printTurnOutcome(result);
|
|
239
|
-
}).catch((e) => {
|
|
240
|
-
process.stderr.write(`! wake sync failed: ${e?.message ?? e}\n`);
|
|
241
|
-
});
|
|
242
|
-
}, { host, bearerToken: token });
|
|
243
|
-
} catch {
|
|
244
|
-
/* wake is best-effort */
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
const stopWake = async () => {
|
|
248
|
-
try {
|
|
249
|
-
await wake?.close();
|
|
250
|
-
} catch {
|
|
251
|
-
/* best-effort */
|
|
252
|
-
}
|
|
253
|
-
};
|
|
254
|
-
process.on("SIGINT", stopWake);
|
|
255
|
-
process.on("exit", () => void wake?.close());
|
|
256
|
-
|
|
257
|
-
process.stdout.write(`neagen (${label})\n`);
|
|
258
|
-
process.stdout.write("Type a message, / for commands, /model to switch models, /help for the list.\n\n");
|
|
259
|
-
|
|
260
|
-
async function handleSlash(rl, input) {
|
|
261
|
-
const normalized = normalizeSlashInput(input);
|
|
262
|
-
|
|
263
|
-
if (normalized === "/exit") return true;
|
|
264
|
-
if (normalized === "/help") {
|
|
265
|
-
printHelp();
|
|
266
|
-
return false;
|
|
267
|
-
}
|
|
268
|
-
if (normalized === "/new") {
|
|
269
|
-
conversationId = undefined;
|
|
270
|
-
process.stdout.write("Started a new conversation.\n");
|
|
271
|
-
return false;
|
|
272
|
-
}
|
|
273
|
-
if (normalized === "/model" || normalized.startsWith("/model ")) {
|
|
274
|
-
await handleModelCommand(rl, token, normalized.slice("/model".length));
|
|
275
|
-
return false;
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
process.stdout.write(`${dim(`unknown command: ${input} (try /help)`)}\n`);
|
|
279
|
-
return false;
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
try {
|
|
283
|
-
for (;;) {
|
|
284
|
-
let line;
|
|
285
|
-
try {
|
|
286
|
-
line = await readPromptLine(rl, { prompt: "neagen> ", slashCommands: SLASH_COMMANDS });
|
|
287
|
-
} catch {
|
|
288
|
-
break;
|
|
289
|
-
}
|
|
290
|
-
if (line === null) break;
|
|
291
|
-
|
|
292
|
-
const input = line.trim();
|
|
293
|
-
if (!input) continue;
|
|
294
|
-
|
|
295
|
-
try {
|
|
296
|
-
if (input.startsWith("/")) {
|
|
297
|
-
if (await handleSlash(rl, input)) break;
|
|
298
|
-
continue;
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
const result = await runExclusive(() => sendTurn(line));
|
|
302
|
-
printTurnOutcome(result);
|
|
303
|
-
} catch (e) {
|
|
304
|
-
process.stderr.write(`! ${e?.message ?? e}\n`);
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
} finally {
|
|
308
|
-
rl.close();
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
await stopWake();
|