@schmitech/orbit-cli 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +65 -0
- package/bin/orbit-chat.js +2 -0
- package/dist/cli.js +1577 -0
- package/dist/cli.js.map +1 -0
- package/package.json +52 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,1577 @@
|
|
|
1
|
+
// src/cli.ts
|
|
2
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
3
|
+
|
|
4
|
+
// src/args.ts
|
|
5
|
+
var ArgsError = class extends Error {
|
|
6
|
+
};
|
|
7
|
+
var FLAGS = /* @__PURE__ */ new Set(["--url", "--key", "--health", "--agent", "--model"]);
|
|
8
|
+
function isFlagLike(value) {
|
|
9
|
+
return value === void 0 || FLAGS.has(value);
|
|
10
|
+
}
|
|
11
|
+
function parseArgs(argv) {
|
|
12
|
+
const result = { health: false, positional: [] };
|
|
13
|
+
for (let i = 0; i < argv.length; i++) {
|
|
14
|
+
const arg = argv[i];
|
|
15
|
+
switch (arg) {
|
|
16
|
+
case "--url": {
|
|
17
|
+
const value = argv[i + 1];
|
|
18
|
+
if (isFlagLike(value)) {
|
|
19
|
+
throw new ArgsError(`--url requires a value`);
|
|
20
|
+
}
|
|
21
|
+
result.url = value;
|
|
22
|
+
i++;
|
|
23
|
+
break;
|
|
24
|
+
}
|
|
25
|
+
case "--key": {
|
|
26
|
+
const value = argv[i + 1];
|
|
27
|
+
if (isFlagLike(value)) {
|
|
28
|
+
throw new ArgsError(`--key requires a value`);
|
|
29
|
+
}
|
|
30
|
+
result.key = value;
|
|
31
|
+
i++;
|
|
32
|
+
break;
|
|
33
|
+
}
|
|
34
|
+
case "--health":
|
|
35
|
+
result.health = true;
|
|
36
|
+
break;
|
|
37
|
+
case "--agent": {
|
|
38
|
+
const value = argv[i + 1];
|
|
39
|
+
if (isFlagLike(value)) {
|
|
40
|
+
throw new ArgsError(`--agent requires a value`);
|
|
41
|
+
}
|
|
42
|
+
result.agent = value;
|
|
43
|
+
i++;
|
|
44
|
+
break;
|
|
45
|
+
}
|
|
46
|
+
case "--model": {
|
|
47
|
+
const value = argv[i + 1];
|
|
48
|
+
if (isFlagLike(value)) {
|
|
49
|
+
throw new ArgsError(`--model requires a value`);
|
|
50
|
+
}
|
|
51
|
+
result.model = value;
|
|
52
|
+
i++;
|
|
53
|
+
break;
|
|
54
|
+
}
|
|
55
|
+
default:
|
|
56
|
+
result.positional.push(arg);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return result;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// src/prompt.ts
|
|
63
|
+
import * as readline from "readline";
|
|
64
|
+
var ETX = "";
|
|
65
|
+
var DEL = "\x7F";
|
|
66
|
+
var BS = "\b";
|
|
67
|
+
function ask(question) {
|
|
68
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stderr });
|
|
69
|
+
return new Promise((resolve) => {
|
|
70
|
+
rl.question(question, (answer) => {
|
|
71
|
+
rl.close();
|
|
72
|
+
resolve(answer.trim());
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
function askSecret(question) {
|
|
77
|
+
return new Promise((resolve) => {
|
|
78
|
+
const stdin = process.stdin;
|
|
79
|
+
process.stderr.write(question);
|
|
80
|
+
const wasRaw = stdin.isTTY ? stdin.isRaw : false;
|
|
81
|
+
if (stdin.isTTY) stdin.setRawMode(true);
|
|
82
|
+
stdin.resume();
|
|
83
|
+
stdin.setEncoding("utf8");
|
|
84
|
+
let value = "";
|
|
85
|
+
const cleanup = () => {
|
|
86
|
+
stdin.removeListener("data", onData);
|
|
87
|
+
if (stdin.isTTY) stdin.setRawMode(wasRaw);
|
|
88
|
+
stdin.pause();
|
|
89
|
+
};
|
|
90
|
+
const onData = (chunk) => {
|
|
91
|
+
for (const char of chunk) {
|
|
92
|
+
if (char === "\n" || char === "\r") {
|
|
93
|
+
cleanup();
|
|
94
|
+
process.stderr.write("\n");
|
|
95
|
+
resolve(value.trim());
|
|
96
|
+
return;
|
|
97
|
+
} else if (char === ETX) {
|
|
98
|
+
cleanup();
|
|
99
|
+
process.stderr.write("\n");
|
|
100
|
+
process.exit(130);
|
|
101
|
+
} else if (char === DEL || char === BS) {
|
|
102
|
+
value = value.slice(0, -1);
|
|
103
|
+
} else {
|
|
104
|
+
value += char;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
stdin.on("data", onData);
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// src/connection.ts
|
|
113
|
+
var ConfigError = class extends Error {
|
|
114
|
+
};
|
|
115
|
+
var DEFAULT_URL = "http://localhost:3000";
|
|
116
|
+
async function resolveConnection(args) {
|
|
117
|
+
let url = args.url ?? process.env.ORBIT_URL;
|
|
118
|
+
let apiKey = args.key ?? process.env.ORBIT_API_KEY;
|
|
119
|
+
if (!url) {
|
|
120
|
+
if (!process.stdin.isTTY) {
|
|
121
|
+
url = DEFAULT_URL;
|
|
122
|
+
} else {
|
|
123
|
+
const answer = await ask(`ORBIT server URL [${DEFAULT_URL}]: `);
|
|
124
|
+
url = answer || DEFAULT_URL;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
if (!apiKey) {
|
|
128
|
+
if (!process.stdin.isTTY) {
|
|
129
|
+
throw new ConfigError(
|
|
130
|
+
"Missing API key. Pass --key <key> or set ORBIT_API_KEY (preferred over --key, which lands in shell history)."
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
apiKey = await askSecret("ORBIT API key: ");
|
|
134
|
+
}
|
|
135
|
+
url = url.trim().replace(/\/+$/, "");
|
|
136
|
+
if (!url) {
|
|
137
|
+
throw new ConfigError("Server URL cannot be empty.");
|
|
138
|
+
}
|
|
139
|
+
if (!apiKey.trim()) {
|
|
140
|
+
throw new ConfigError("API key cannot be empty.");
|
|
141
|
+
}
|
|
142
|
+
return { url, apiKey: apiKey.trim() };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// src/client.ts
|
|
146
|
+
import { ApiClient } from "@schmitech/chatbot-api";
|
|
147
|
+
function createClient(connection, sessionId) {
|
|
148
|
+
return new ApiClient({
|
|
149
|
+
apiUrl: connection.url,
|
|
150
|
+
apiKey: connection.apiKey,
|
|
151
|
+
sessionId: sessionId ?? null
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
function streamChat(client, message, options = {}) {
|
|
155
|
+
return client.streamChat(
|
|
156
|
+
message,
|
|
157
|
+
options.stream ?? true,
|
|
158
|
+
options.fileIds,
|
|
159
|
+
options.threadId,
|
|
160
|
+
void 0,
|
|
161
|
+
// audioInput
|
|
162
|
+
void 0,
|
|
163
|
+
// audioFormat
|
|
164
|
+
void 0,
|
|
165
|
+
// language
|
|
166
|
+
void 0,
|
|
167
|
+
// returnAudio
|
|
168
|
+
void 0,
|
|
169
|
+
// ttsVoice
|
|
170
|
+
void 0,
|
|
171
|
+
// sourceLanguage
|
|
172
|
+
void 0,
|
|
173
|
+
// targetLanguage
|
|
174
|
+
options.abortSignal,
|
|
175
|
+
options.model,
|
|
176
|
+
options.skill,
|
|
177
|
+
void 0
|
|
178
|
+
// regenerateOfMessageId
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// src/exit-codes.ts
|
|
183
|
+
var EXIT_OK = 0;
|
|
184
|
+
var EXIT_SERVER_ERROR = 1;
|
|
185
|
+
var EXIT_USAGE = 2;
|
|
186
|
+
var EXIT_AUTH = 3;
|
|
187
|
+
var EXIT_NETWORK = 4;
|
|
188
|
+
var EXIT_CANCELLED = 130;
|
|
189
|
+
|
|
190
|
+
// src/errors.ts
|
|
191
|
+
function classifyError(error) {
|
|
192
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
193
|
+
if (/ECONNREFUSED|ENOTFOUND|EAI_AGAIN|fetch failed|Could not connect to the server/i.test(message)) {
|
|
194
|
+
return { code: EXIT_NETWORK, message: "Could not connect to the server. Check the URL and that it is running." };
|
|
195
|
+
}
|
|
196
|
+
if (/:\s*401\b/.test(message) || /invalid|disabled|no associated adapter/i.test(message)) {
|
|
197
|
+
return { code: EXIT_AUTH, message: "API key is invalid, disabled, or lacks an adapter." };
|
|
198
|
+
}
|
|
199
|
+
return { code: EXIT_SERVER_ERROR, message };
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// src/attachments.ts
|
|
203
|
+
import { readFile } from "fs/promises";
|
|
204
|
+
import { basename, extname } from "path";
|
|
205
|
+
var MIME_TYPES = {
|
|
206
|
+
".txt": "text/plain",
|
|
207
|
+
".md": "text/markdown",
|
|
208
|
+
".json": "application/json",
|
|
209
|
+
".csv": "text/csv",
|
|
210
|
+
".pdf": "application/pdf",
|
|
211
|
+
".png": "image/png",
|
|
212
|
+
".jpg": "image/jpeg",
|
|
213
|
+
".jpeg": "image/jpeg",
|
|
214
|
+
".gif": "image/gif",
|
|
215
|
+
".webp": "image/webp"
|
|
216
|
+
};
|
|
217
|
+
function mimeTypeFor(path) {
|
|
218
|
+
return MIME_TYPES[extname(path).toLowerCase()] ?? "application/octet-stream";
|
|
219
|
+
}
|
|
220
|
+
var ATTACHMENT_PATTERN = /@(\S+)/g;
|
|
221
|
+
var AttachmentError = class extends Error {
|
|
222
|
+
};
|
|
223
|
+
async function extractAttachments(client, message) {
|
|
224
|
+
const paths = [...message.matchAll(ATTACHMENT_PATTERN)].map((match) => match[1]);
|
|
225
|
+
if (paths.length === 0) {
|
|
226
|
+
return { text: message, fileIds: [] };
|
|
227
|
+
}
|
|
228
|
+
const fileIds = [];
|
|
229
|
+
for (const path of paths) {
|
|
230
|
+
let bytes;
|
|
231
|
+
try {
|
|
232
|
+
bytes = await readFile(path);
|
|
233
|
+
} catch {
|
|
234
|
+
throw new AttachmentError(`Attachment not found: ${path}`);
|
|
235
|
+
}
|
|
236
|
+
const file = new File([bytes], basename(path), { type: mimeTypeFor(path) });
|
|
237
|
+
const uploaded = await client.uploadFile(file);
|
|
238
|
+
fileIds.push(uploaded.file_id);
|
|
239
|
+
}
|
|
240
|
+
const text = message.replace(ATTACHMENT_PATTERN, "").replace(/\s+/g, " ").trim();
|
|
241
|
+
return { text: text || "Please review the attached file(s).", fileIds };
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// src/artifacts.ts
|
|
245
|
+
import { mkdir, writeFile } from "fs/promises";
|
|
246
|
+
import { randomUUID } from "crypto";
|
|
247
|
+
var OUT_DIR = "./.orbit-out";
|
|
248
|
+
var INLINE_FIELDS = [
|
|
249
|
+
{ data: "image", format: "image_format", kind: "image" },
|
|
250
|
+
{ data: "video", format: "video_format", kind: "video" },
|
|
251
|
+
{ data: "document", format: "document_format", kind: "document" }
|
|
252
|
+
];
|
|
253
|
+
var URL_FIELDS = [
|
|
254
|
+
{ url: "image_url", kind: "image" },
|
|
255
|
+
{ url: "video_url", kind: "video" },
|
|
256
|
+
{ url: "document_url", kind: "document" },
|
|
257
|
+
{ url: "generated_audio_url", kind: "audio" }
|
|
258
|
+
];
|
|
259
|
+
async function writeArtifact(kind, format, bytes) {
|
|
260
|
+
await mkdir(OUT_DIR, { recursive: true });
|
|
261
|
+
const ext = format ? `.${format}` : "";
|
|
262
|
+
const path = `${OUT_DIR}/${kind}-${randomUUID()}${ext}`;
|
|
263
|
+
await writeFile(path, bytes);
|
|
264
|
+
return path;
|
|
265
|
+
}
|
|
266
|
+
function extFromContentType(contentType) {
|
|
267
|
+
if (!contentType) return "";
|
|
268
|
+
const subtype = contentType.split("/")[1]?.split(";")[0];
|
|
269
|
+
return subtype ? `.${subtype}` : "";
|
|
270
|
+
}
|
|
271
|
+
async function saveArtifacts(client, chunk) {
|
|
272
|
+
const paths = [];
|
|
273
|
+
const record = chunk;
|
|
274
|
+
const savedKinds = /* @__PURE__ */ new Set();
|
|
275
|
+
for (const { data, format, kind } of INLINE_FIELDS) {
|
|
276
|
+
const base64 = record[data];
|
|
277
|
+
if (base64) {
|
|
278
|
+
paths.push(await writeArtifact(kind, record[format], Buffer.from(base64, "base64")));
|
|
279
|
+
savedKinds.add(kind);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
for (const { url, kind } of URL_FIELDS) {
|
|
283
|
+
if (savedKinds.has(kind)) continue;
|
|
284
|
+
const value = record[url];
|
|
285
|
+
if (value) {
|
|
286
|
+
const { data, contentType } = await client.downloadArtifact(value);
|
|
287
|
+
paths.push(await writeArtifact(kind, extFromContentType(contentType).replace(/^\./, "") || void 0, Buffer.from(data)));
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
return paths;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// src/one-shot.ts
|
|
294
|
+
async function runOneShot(client, rawMessage, options) {
|
|
295
|
+
let message;
|
|
296
|
+
let fileIds;
|
|
297
|
+
try {
|
|
298
|
+
({ text: message, fileIds } = await extractAttachments(client, rawMessage));
|
|
299
|
+
} catch (error) {
|
|
300
|
+
if (error instanceof AttachmentError) {
|
|
301
|
+
process.stderr.write(`${error.message}
|
|
302
|
+
`);
|
|
303
|
+
return EXIT_USAGE;
|
|
304
|
+
}
|
|
305
|
+
const { code, message: errorMessage } = classifyError(error);
|
|
306
|
+
process.stderr.write(`${errorMessage}
|
|
307
|
+
`);
|
|
308
|
+
return code;
|
|
309
|
+
}
|
|
310
|
+
try {
|
|
311
|
+
for await (const chunk of streamChat(client, message, {
|
|
312
|
+
...options,
|
|
313
|
+
fileIds,
|
|
314
|
+
abortSignal: options.signal
|
|
315
|
+
})) {
|
|
316
|
+
if (chunk.text) {
|
|
317
|
+
process.stdout.write(chunk.text);
|
|
318
|
+
}
|
|
319
|
+
for (const path of await saveArtifacts(client, chunk)) {
|
|
320
|
+
process.stderr.write(`Saved artifact: ${path}
|
|
321
|
+
`);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
process.stdout.write("\n");
|
|
325
|
+
return EXIT_OK;
|
|
326
|
+
} catch (error) {
|
|
327
|
+
if (options.signal?.aborted) {
|
|
328
|
+
return EXIT_CANCELLED;
|
|
329
|
+
}
|
|
330
|
+
const { code, message: errorMessage } = classifyError(error);
|
|
331
|
+
process.stderr.write(`${errorMessage}
|
|
332
|
+
`);
|
|
333
|
+
return code;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// src/repl.ts
|
|
338
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
339
|
+
import * as fs from "fs";
|
|
340
|
+
|
|
341
|
+
// src/agents.ts
|
|
342
|
+
async function listAgents(client, ownAdapter) {
|
|
343
|
+
const choices = [
|
|
344
|
+
{ label: `${ownAdapter.client_name} (default)`, skill: void 0, adapterName: ownAdapter.adapter_name }
|
|
345
|
+
];
|
|
346
|
+
try {
|
|
347
|
+
const { skills } = await client.getAllSkills();
|
|
348
|
+
for (const skill of skills.filter((s) => s.enabled)) {
|
|
349
|
+
choices.push({
|
|
350
|
+
label: `${skill.name} \u2014 ${skill.description}`,
|
|
351
|
+
skill: skill.name,
|
|
352
|
+
adapterName: skill.adapter_name
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
} catch {
|
|
356
|
+
}
|
|
357
|
+
return choices;
|
|
358
|
+
}
|
|
359
|
+
function findAgent(agents, query) {
|
|
360
|
+
const index = Number(query);
|
|
361
|
+
if (Number.isInteger(index) && index >= 1 && index <= agents.length) {
|
|
362
|
+
return agents[index - 1];
|
|
363
|
+
}
|
|
364
|
+
return agents.find((a) => a.skill?.toLowerCase() === query.toLowerCase());
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// src/input.ts
|
|
368
|
+
import * as readline2 from "readline";
|
|
369
|
+
|
|
370
|
+
// src/tty.ts
|
|
371
|
+
function ansiEnabled() {
|
|
372
|
+
return Boolean(process.stdout.isTTY) && !process.env.NO_COLOR;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// src/input.ts
|
|
376
|
+
var RESET = "\x1B[0m";
|
|
377
|
+
var DIM = "\x1B[2m";
|
|
378
|
+
var INVERT = "\x1B[7m";
|
|
379
|
+
var CTRL_C = "";
|
|
380
|
+
var ANSI_PATTERN = /\x1b\[[0-9;]*m/g;
|
|
381
|
+
function isWideCodePoint(cp) {
|
|
382
|
+
return cp >= 4352 && cp <= 4447 || // Hangul Jamo
|
|
383
|
+
cp === 9001 || cp === 9002 || cp >= 11904 && cp <= 12350 || // CJK Radicals..CJK Symbols
|
|
384
|
+
cp >= 12353 && cp <= 13311 || // Hiragana..CJK Compatibility
|
|
385
|
+
cp >= 13312 && cp <= 19903 || // CJK Extension A
|
|
386
|
+
cp >= 19968 && cp <= 40959 || // CJK Unified Ideographs
|
|
387
|
+
cp >= 40960 && cp <= 42191 || // Yi
|
|
388
|
+
cp >= 44032 && cp <= 55203 || // Hangul Syllables
|
|
389
|
+
cp >= 63744 && cp <= 64255 || // CJK Compatibility Ideographs
|
|
390
|
+
cp >= 65072 && cp <= 65103 || // CJK Compatibility Forms
|
|
391
|
+
cp >= 65280 && cp <= 65376 || // Fullwidth Forms
|
|
392
|
+
cp >= 65504 && cp <= 65510 || cp >= 127744 && cp <= 129791 || // emoji blocks (approximate)
|
|
393
|
+
cp >= 131072 && cp <= 262141;
|
|
394
|
+
}
|
|
395
|
+
function isZeroWidthCodePoint(cp) {
|
|
396
|
+
return cp === 8203 || // zero width space
|
|
397
|
+
cp === 8204 || cp === 8205 || // zero width joiner
|
|
398
|
+
cp === 65279 || cp >= 768 && cp <= 879 || // combining diacritics
|
|
399
|
+
cp >= 6832 && cp <= 6911 || cp >= 7616 && cp <= 7679 || cp >= 8400 && cp <= 8447 || cp >= 65024 && cp <= 65039;
|
|
400
|
+
}
|
|
401
|
+
function displayWidth(str) {
|
|
402
|
+
const clean = str.replace(ANSI_PATTERN, "");
|
|
403
|
+
let width = 0;
|
|
404
|
+
for (const ch of clean) {
|
|
405
|
+
const cp = ch.codePointAt(0) ?? 0;
|
|
406
|
+
if (isZeroWidthCodePoint(cp)) continue;
|
|
407
|
+
width += isWideCodePoint(cp) ? 2 : 1;
|
|
408
|
+
}
|
|
409
|
+
return width;
|
|
410
|
+
}
|
|
411
|
+
function rowsFor(text, width) {
|
|
412
|
+
return Math.max(1, Math.ceil(displayWidth(text) / width) || 1);
|
|
413
|
+
}
|
|
414
|
+
var graphemeSegmenter = typeof Intl !== "undefined" && "Segmenter" in Intl ? new Intl.Segmenter(void 0, { granularity: "grapheme" }) : null;
|
|
415
|
+
function graphemes(str) {
|
|
416
|
+
if (graphemeSegmenter) {
|
|
417
|
+
return Array.from(graphemeSegmenter.segment(str), (s) => s.segment);
|
|
418
|
+
}
|
|
419
|
+
return Array.from(str);
|
|
420
|
+
}
|
|
421
|
+
function commonPrefix(strings) {
|
|
422
|
+
if (strings.length === 0) return "";
|
|
423
|
+
let prefix = strings[0];
|
|
424
|
+
for (const s of strings.slice(1)) {
|
|
425
|
+
let i = 0;
|
|
426
|
+
while (i < prefix.length && i < s.length && prefix[i] === s[i]) i++;
|
|
427
|
+
prefix = prefix.slice(0, i);
|
|
428
|
+
}
|
|
429
|
+
return prefix;
|
|
430
|
+
}
|
|
431
|
+
var LineReader = class {
|
|
432
|
+
prompt;
|
|
433
|
+
commands;
|
|
434
|
+
completer;
|
|
435
|
+
onSigint;
|
|
436
|
+
stdin = process.stdin;
|
|
437
|
+
history = [];
|
|
438
|
+
buffer = "";
|
|
439
|
+
cursor = 0;
|
|
440
|
+
historyIndex = 0;
|
|
441
|
+
menuVisible = false;
|
|
442
|
+
menuIndex = 0;
|
|
443
|
+
menuItems = [];
|
|
444
|
+
searchMode = false;
|
|
445
|
+
searchQuery = "";
|
|
446
|
+
searchMatch;
|
|
447
|
+
searchScanIndex = 0;
|
|
448
|
+
secretMode = false;
|
|
449
|
+
secretPrompt = "";
|
|
450
|
+
secretBuffer = "";
|
|
451
|
+
resolveSecret = null;
|
|
452
|
+
listMode = false;
|
|
453
|
+
listPrompt = "";
|
|
454
|
+
listQuery = "";
|
|
455
|
+
listAllItems = [];
|
|
456
|
+
listFiltered = [];
|
|
457
|
+
listIndex = 0;
|
|
458
|
+
resolveList = null;
|
|
459
|
+
resolveLine = null;
|
|
460
|
+
closed = false;
|
|
461
|
+
wasRaw = false;
|
|
462
|
+
/** Row offset (relative to the input's first row) the cursor was left on by the last render, for wrapped-line redraw bookkeeping. */
|
|
463
|
+
lastCursorRow = 0;
|
|
464
|
+
constructor(options) {
|
|
465
|
+
this.prompt = options.prompt;
|
|
466
|
+
this.commands = options.commands;
|
|
467
|
+
this.completer = options.completer;
|
|
468
|
+
this.onSigint = options.onSigint;
|
|
469
|
+
if (this.stdin.isTTY) {
|
|
470
|
+
readline2.emitKeypressEvents(this.stdin);
|
|
471
|
+
this.wasRaw = this.stdin.isRaw;
|
|
472
|
+
this.stdin.setRawMode(true);
|
|
473
|
+
}
|
|
474
|
+
this.stdin.resume();
|
|
475
|
+
this.stdin.setEncoding("utf8");
|
|
476
|
+
this.stdin.on("keypress", this.onKeypress);
|
|
477
|
+
}
|
|
478
|
+
async *[Symbol.asyncIterator]() {
|
|
479
|
+
while (!this.closed) {
|
|
480
|
+
const line = await this.readLine();
|
|
481
|
+
if (line === null) {
|
|
482
|
+
return;
|
|
483
|
+
}
|
|
484
|
+
yield line;
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
close() {
|
|
488
|
+
if (this.closed) return;
|
|
489
|
+
this.closed = true;
|
|
490
|
+
this.stdin.removeListener("keypress", this.onKeypress);
|
|
491
|
+
if (this.stdin.isTTY) {
|
|
492
|
+
this.stdin.setRawMode(this.wasRaw);
|
|
493
|
+
}
|
|
494
|
+
this.stdin.pause();
|
|
495
|
+
}
|
|
496
|
+
/**
|
|
497
|
+
* `cursor` counts grapheme clusters, not UTF-16 code units — a `cursor--`
|
|
498
|
+
* next to an emoji or a surrogate-pair character must skip the whole
|
|
499
|
+
* cluster, not land inside it. This converts that cluster count to the
|
|
500
|
+
* actual string offset needed for `buffer.slice(...)`.
|
|
501
|
+
*/
|
|
502
|
+
cursorStringIndex() {
|
|
503
|
+
return graphemes(this.buffer).slice(0, this.cursor).join("").length;
|
|
504
|
+
}
|
|
505
|
+
/**
|
|
506
|
+
* Insert `text` at string offset `idx` and recompute the cursor by
|
|
507
|
+
* re-segmenting the resulting prefix, rather than adding `graphemes(text)`
|
|
508
|
+
* to the old cursor. The terminal delivers a multi-codepoint emoji (e.g. a
|
|
509
|
+
* ZWJ family sequence) as separate keypress events one codepoint at a
|
|
510
|
+
* time; segmented alone, a joiner counts as its own cluster, but Unicode's
|
|
511
|
+
* grapheme rules attach it to whatever already precedes it in the buffer.
|
|
512
|
+
* Re-segmenting the real prefix after each keystroke keeps the cursor's
|
|
513
|
+
* cluster count correct instead of drifting ahead of the buffer's actual
|
|
514
|
+
* cluster count (which crashes the next backspace/delete on an
|
|
515
|
+
* out-of-range index).
|
|
516
|
+
*/
|
|
517
|
+
insertAt(idx, text) {
|
|
518
|
+
this.buffer = this.buffer.slice(0, idx) + text + this.buffer.slice(idx);
|
|
519
|
+
this.cursor = graphemes(this.buffer.slice(0, idx + text.length)).length;
|
|
520
|
+
}
|
|
521
|
+
/**
|
|
522
|
+
* Read one line of masked input (e.g. an API key) without it ever being
|
|
523
|
+
* echoed, written to terminal scrollback, or added to line history — only
|
|
524
|
+
* a `*` per typed character is shown, same masking `prompt.ts::askSecret()`
|
|
525
|
+
* uses for the initial connection prompt. Ctrl+C cancels and resolves
|
|
526
|
+
* `null` instead of exiting the process, since a REPL command is in
|
|
527
|
+
* progress rather than startup.
|
|
528
|
+
*/
|
|
529
|
+
readSecret(promptText) {
|
|
530
|
+
this.secretMode = true;
|
|
531
|
+
this.secretPrompt = promptText;
|
|
532
|
+
this.secretBuffer = "";
|
|
533
|
+
this.lastCursorRow = 0;
|
|
534
|
+
this.renderSecret();
|
|
535
|
+
return new Promise((resolve) => {
|
|
536
|
+
this.resolveSecret = resolve;
|
|
537
|
+
});
|
|
538
|
+
}
|
|
539
|
+
/**
|
|
540
|
+
* Present `items` as an arrow-key-navigable list (type to filter, like the
|
|
541
|
+
* `/` command menu) and resolve the chosen index, or `null` on Esc/Ctrl+C.
|
|
542
|
+
* Used for `/agents` and `/models` so picking one doesn't require typing
|
|
543
|
+
* its number or name.
|
|
544
|
+
*/
|
|
545
|
+
selectFromList(promptText, items) {
|
|
546
|
+
this.listMode = true;
|
|
547
|
+
this.listPrompt = promptText;
|
|
548
|
+
this.listQuery = "";
|
|
549
|
+
this.listAllItems = items;
|
|
550
|
+
this.listFiltered = items.map((_, i) => i);
|
|
551
|
+
this.listIndex = 0;
|
|
552
|
+
this.lastCursorRow = 0;
|
|
553
|
+
this.renderList();
|
|
554
|
+
return new Promise((resolve) => {
|
|
555
|
+
this.resolveList = resolve;
|
|
556
|
+
});
|
|
557
|
+
}
|
|
558
|
+
readLine() {
|
|
559
|
+
this.buffer = "";
|
|
560
|
+
this.cursor = 0;
|
|
561
|
+
this.historyIndex = this.history.length;
|
|
562
|
+
this.menuVisible = false;
|
|
563
|
+
this.searchMode = false;
|
|
564
|
+
this.secretMode = false;
|
|
565
|
+
this.listMode = false;
|
|
566
|
+
this.lastCursorRow = 0;
|
|
567
|
+
this.render();
|
|
568
|
+
return new Promise((resolve) => {
|
|
569
|
+
this.resolveLine = resolve;
|
|
570
|
+
});
|
|
571
|
+
}
|
|
572
|
+
onKeypress = (str, key) => {
|
|
573
|
+
if (this.closed) return;
|
|
574
|
+
const isCtrlC = key.sequence === CTRL_C || key.ctrl && key.name === "c";
|
|
575
|
+
if (isCtrlC) {
|
|
576
|
+
if (this.secretMode) {
|
|
577
|
+
this.finishSecret(null);
|
|
578
|
+
return;
|
|
579
|
+
}
|
|
580
|
+
if (this.listMode) {
|
|
581
|
+
this.finishList(null);
|
|
582
|
+
return;
|
|
583
|
+
}
|
|
584
|
+
if (this.searchMode) {
|
|
585
|
+
this.exitSearch();
|
|
586
|
+
this.render();
|
|
587
|
+
return;
|
|
588
|
+
}
|
|
589
|
+
this.onSigint();
|
|
590
|
+
return;
|
|
591
|
+
}
|
|
592
|
+
if (this.secretMode) {
|
|
593
|
+
this.handleSecretKey(str, key);
|
|
594
|
+
return;
|
|
595
|
+
}
|
|
596
|
+
if (this.listMode) {
|
|
597
|
+
this.handleListKey(str, key);
|
|
598
|
+
return;
|
|
599
|
+
}
|
|
600
|
+
if (!this.resolveLine) {
|
|
601
|
+
return;
|
|
602
|
+
}
|
|
603
|
+
if (this.searchMode) {
|
|
604
|
+
this.handleSearchKey(str, key);
|
|
605
|
+
return;
|
|
606
|
+
}
|
|
607
|
+
if (key.ctrl && key.name === "r") {
|
|
608
|
+
this.searchMode = true;
|
|
609
|
+
this.searchQuery = "";
|
|
610
|
+
this.searchMatch = void 0;
|
|
611
|
+
this.searchScanIndex = this.history.length;
|
|
612
|
+
this.render();
|
|
613
|
+
return;
|
|
614
|
+
}
|
|
615
|
+
if (key.ctrl && key.name === "d") {
|
|
616
|
+
if (this.buffer.length === 0) {
|
|
617
|
+
this.finish(null);
|
|
618
|
+
}
|
|
619
|
+
return;
|
|
620
|
+
}
|
|
621
|
+
switch (key.name) {
|
|
622
|
+
case "return":
|
|
623
|
+
if (this.menuVisible && this.menuItems.length > 0) {
|
|
624
|
+
this.selectMenuItemOnEnter();
|
|
625
|
+
return;
|
|
626
|
+
}
|
|
627
|
+
this.submit();
|
|
628
|
+
return;
|
|
629
|
+
case "backspace": {
|
|
630
|
+
if (this.cursor > 0) {
|
|
631
|
+
const segs = graphemes(this.buffer);
|
|
632
|
+
const idx = segs.slice(0, this.cursor - 1).join("").length;
|
|
633
|
+
const removed = segs[this.cursor - 1];
|
|
634
|
+
this.buffer = this.buffer.slice(0, idx) + this.buffer.slice(idx + removed.length);
|
|
635
|
+
this.cursor--;
|
|
636
|
+
}
|
|
637
|
+
break;
|
|
638
|
+
}
|
|
639
|
+
case "delete": {
|
|
640
|
+
const segs = graphemes(this.buffer);
|
|
641
|
+
if (this.cursor < segs.length) {
|
|
642
|
+
const idx = segs.slice(0, this.cursor).join("").length;
|
|
643
|
+
const removed = segs[this.cursor];
|
|
644
|
+
this.buffer = this.buffer.slice(0, idx) + this.buffer.slice(idx + removed.length);
|
|
645
|
+
}
|
|
646
|
+
break;
|
|
647
|
+
}
|
|
648
|
+
case "left":
|
|
649
|
+
if (this.cursor > 0) this.cursor--;
|
|
650
|
+
break;
|
|
651
|
+
case "right":
|
|
652
|
+
if (this.cursor < graphemes(this.buffer).length) this.cursor++;
|
|
653
|
+
break;
|
|
654
|
+
case "up":
|
|
655
|
+
if (this.menuVisible && this.menuItems.length > 0) {
|
|
656
|
+
this.menuIndex = (this.menuIndex - 1 + this.menuItems.length) % this.menuItems.length;
|
|
657
|
+
} else {
|
|
658
|
+
this.historyUp();
|
|
659
|
+
}
|
|
660
|
+
break;
|
|
661
|
+
case "down":
|
|
662
|
+
if (this.menuVisible && this.menuItems.length > 0) {
|
|
663
|
+
this.menuIndex = (this.menuIndex + 1) % this.menuItems.length;
|
|
664
|
+
} else {
|
|
665
|
+
this.historyDown();
|
|
666
|
+
}
|
|
667
|
+
break;
|
|
668
|
+
case "tab":
|
|
669
|
+
if (this.menuVisible && this.menuItems.length > 0) {
|
|
670
|
+
this.acceptMenuSelection();
|
|
671
|
+
} else {
|
|
672
|
+
this.applyCompletion();
|
|
673
|
+
}
|
|
674
|
+
break;
|
|
675
|
+
case "escape":
|
|
676
|
+
this.menuVisible = false;
|
|
677
|
+
this.render();
|
|
678
|
+
return;
|
|
679
|
+
default:
|
|
680
|
+
if (str && !key.ctrl && !key.meta && str >= " ") {
|
|
681
|
+
this.insertAt(this.cursorStringIndex(), str);
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
this.updateMenu();
|
|
685
|
+
this.render();
|
|
686
|
+
};
|
|
687
|
+
historyUp() {
|
|
688
|
+
if (this.historyIndex > 0) {
|
|
689
|
+
this.historyIndex--;
|
|
690
|
+
this.buffer = this.history[this.historyIndex];
|
|
691
|
+
this.cursor = graphemes(this.buffer).length;
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
historyDown() {
|
|
695
|
+
if (this.historyIndex < this.history.length - 1) {
|
|
696
|
+
this.historyIndex++;
|
|
697
|
+
this.buffer = this.history[this.historyIndex];
|
|
698
|
+
this.cursor = graphemes(this.buffer).length;
|
|
699
|
+
} else {
|
|
700
|
+
this.historyIndex = this.history.length;
|
|
701
|
+
this.buffer = "";
|
|
702
|
+
this.cursor = 0;
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
updateMenu() {
|
|
706
|
+
const match = /^\/(\S*)$/.exec(this.buffer);
|
|
707
|
+
if (!match) {
|
|
708
|
+
this.menuVisible = false;
|
|
709
|
+
return;
|
|
710
|
+
}
|
|
711
|
+
const query = match[1].toLowerCase();
|
|
712
|
+
const items = this.commands.filter((c) => c.name.slice(1).toLowerCase().startsWith(query));
|
|
713
|
+
if (items.length === 0) {
|
|
714
|
+
this.menuVisible = false;
|
|
715
|
+
return;
|
|
716
|
+
}
|
|
717
|
+
this.menuItems = items;
|
|
718
|
+
if (this.menuIndex >= items.length) {
|
|
719
|
+
this.menuIndex = 0;
|
|
720
|
+
}
|
|
721
|
+
this.menuVisible = true;
|
|
722
|
+
}
|
|
723
|
+
acceptMenuSelection() {
|
|
724
|
+
const item = this.menuItems[this.menuIndex];
|
|
725
|
+
if (!item) return;
|
|
726
|
+
this.buffer = `${item.name} `;
|
|
727
|
+
this.cursor = graphemes(this.buffer).length;
|
|
728
|
+
this.menuVisible = false;
|
|
729
|
+
}
|
|
730
|
+
/**
|
|
731
|
+
* Enter on a visible menu: a command that takes no further argument (empty
|
|
732
|
+
* `usage`, e.g. `/models`) runs immediately, since there's nothing left to
|
|
733
|
+
* type — otherwise Enter would just fill the buffer and force a second
|
|
734
|
+
* Enter to actually submit it. A command that does take an argument (e.g.
|
|
735
|
+
* `/agents <name|number>`) still only fills the buffer, the same as Tab,
|
|
736
|
+
* so the user can type that argument before submitting.
|
|
737
|
+
*/
|
|
738
|
+
selectMenuItemOnEnter() {
|
|
739
|
+
const item = this.menuItems[this.menuIndex];
|
|
740
|
+
if (!item) return;
|
|
741
|
+
if (item.usage) {
|
|
742
|
+
this.acceptMenuSelection();
|
|
743
|
+
this.render();
|
|
744
|
+
return;
|
|
745
|
+
}
|
|
746
|
+
this.menuVisible = false;
|
|
747
|
+
this.buffer = item.name;
|
|
748
|
+
this.cursor = graphemes(this.buffer).length;
|
|
749
|
+
this.submit();
|
|
750
|
+
}
|
|
751
|
+
applyCompletion() {
|
|
752
|
+
const idx = this.cursorStringIndex();
|
|
753
|
+
const [hits, partial] = this.completer(this.buffer.slice(0, idx));
|
|
754
|
+
if (hits.length === 1) {
|
|
755
|
+
this.insertAt(idx, hits[0].slice(partial.length));
|
|
756
|
+
} else if (hits.length > 1) {
|
|
757
|
+
const common = commonPrefix(hits);
|
|
758
|
+
if (common.length > partial.length) {
|
|
759
|
+
this.insertAt(idx, common.slice(partial.length));
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
handleSearchKey(str, key) {
|
|
764
|
+
if (key.name === "return") {
|
|
765
|
+
this.buffer = this.searchMatch ?? this.searchQuery;
|
|
766
|
+
this.cursor = graphemes(this.buffer).length;
|
|
767
|
+
this.exitSearch();
|
|
768
|
+
this.submit();
|
|
769
|
+
return;
|
|
770
|
+
}
|
|
771
|
+
if (key.name === "escape" || key.ctrl && key.name === "g") {
|
|
772
|
+
this.exitSearch();
|
|
773
|
+
this.render();
|
|
774
|
+
return;
|
|
775
|
+
}
|
|
776
|
+
if (key.ctrl && key.name === "r") {
|
|
777
|
+
this.findSearchMatch(true);
|
|
778
|
+
this.render();
|
|
779
|
+
return;
|
|
780
|
+
}
|
|
781
|
+
if (key.name === "backspace") {
|
|
782
|
+
this.searchQuery = graphemes(this.searchQuery).slice(0, -1).join("");
|
|
783
|
+
this.searchScanIndex = this.history.length;
|
|
784
|
+
this.findSearchMatch(false);
|
|
785
|
+
this.render();
|
|
786
|
+
return;
|
|
787
|
+
}
|
|
788
|
+
if (str && !key.ctrl && !key.meta && str >= " ") {
|
|
789
|
+
this.searchQuery += str;
|
|
790
|
+
this.searchScanIndex = this.history.length;
|
|
791
|
+
this.findSearchMatch(false);
|
|
792
|
+
}
|
|
793
|
+
this.render();
|
|
794
|
+
}
|
|
795
|
+
handleSecretKey(str, key) {
|
|
796
|
+
if (key.name === "return") {
|
|
797
|
+
this.finishSecret(this.secretBuffer);
|
|
798
|
+
return;
|
|
799
|
+
}
|
|
800
|
+
if (key.name === "backspace") {
|
|
801
|
+
this.secretBuffer = graphemes(this.secretBuffer).slice(0, -1).join("");
|
|
802
|
+
this.renderSecret();
|
|
803
|
+
return;
|
|
804
|
+
}
|
|
805
|
+
if (key.name === "escape") {
|
|
806
|
+
this.finishSecret(null);
|
|
807
|
+
return;
|
|
808
|
+
}
|
|
809
|
+
if (str && !key.ctrl && !key.meta && str >= " ") {
|
|
810
|
+
this.secretBuffer += str;
|
|
811
|
+
this.renderSecret();
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
renderSecret() {
|
|
815
|
+
if (this.lastCursorRow > 0) {
|
|
816
|
+
readline2.moveCursor(process.stdout, 0, -this.lastCursorRow);
|
|
817
|
+
}
|
|
818
|
+
readline2.cursorTo(process.stdout, 0);
|
|
819
|
+
readline2.clearScreenDown(process.stdout);
|
|
820
|
+
const masked = "*".repeat(graphemes(this.secretBuffer).length);
|
|
821
|
+
const text = this.secretPrompt + masked;
|
|
822
|
+
process.stdout.write(text);
|
|
823
|
+
const width = process.stdout.columns || 80;
|
|
824
|
+
this.lastCursorRow = rowsFor(text, width) - 1;
|
|
825
|
+
}
|
|
826
|
+
finishSecret(value) {
|
|
827
|
+
this.secretMode = false;
|
|
828
|
+
if (this.lastCursorRow > 0) {
|
|
829
|
+
readline2.moveCursor(process.stdout, 0, -this.lastCursorRow);
|
|
830
|
+
}
|
|
831
|
+
readline2.cursorTo(process.stdout, 0);
|
|
832
|
+
readline2.clearScreenDown(process.stdout);
|
|
833
|
+
process.stdout.write("\n");
|
|
834
|
+
this.lastCursorRow = 0;
|
|
835
|
+
const resolve = this.resolveSecret;
|
|
836
|
+
this.resolveSecret = null;
|
|
837
|
+
this.secretBuffer = "";
|
|
838
|
+
resolve?.(value);
|
|
839
|
+
}
|
|
840
|
+
handleListKey(str, key) {
|
|
841
|
+
if (key.name === "return") {
|
|
842
|
+
const idx = this.listFiltered[this.listIndex];
|
|
843
|
+
this.finishList(idx === void 0 ? null : idx);
|
|
844
|
+
return;
|
|
845
|
+
}
|
|
846
|
+
if (key.name === "escape") {
|
|
847
|
+
this.finishList(null);
|
|
848
|
+
return;
|
|
849
|
+
}
|
|
850
|
+
if (key.name === "up") {
|
|
851
|
+
if (this.listFiltered.length > 0) {
|
|
852
|
+
this.listIndex = (this.listIndex - 1 + this.listFiltered.length) % this.listFiltered.length;
|
|
853
|
+
}
|
|
854
|
+
this.renderList();
|
|
855
|
+
return;
|
|
856
|
+
}
|
|
857
|
+
if (key.name === "down") {
|
|
858
|
+
if (this.listFiltered.length > 0) {
|
|
859
|
+
this.listIndex = (this.listIndex + 1) % this.listFiltered.length;
|
|
860
|
+
}
|
|
861
|
+
this.renderList();
|
|
862
|
+
return;
|
|
863
|
+
}
|
|
864
|
+
if (key.name === "backspace") {
|
|
865
|
+
this.listQuery = graphemes(this.listQuery).slice(0, -1).join("");
|
|
866
|
+
this.updateListFilter();
|
|
867
|
+
this.renderList();
|
|
868
|
+
return;
|
|
869
|
+
}
|
|
870
|
+
if (str && !key.ctrl && !key.meta && str >= " ") {
|
|
871
|
+
this.listQuery += str;
|
|
872
|
+
this.updateListFilter();
|
|
873
|
+
}
|
|
874
|
+
this.renderList();
|
|
875
|
+
}
|
|
876
|
+
updateListFilter() {
|
|
877
|
+
const query = this.listQuery.toLowerCase();
|
|
878
|
+
this.listFiltered = this.listAllItems.map((label, i) => ({ label, i })).filter(({ label }) => label.toLowerCase().includes(query)).map(({ i }) => i);
|
|
879
|
+
if (this.listIndex >= this.listFiltered.length) {
|
|
880
|
+
this.listIndex = 0;
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
renderList() {
|
|
884
|
+
const width = process.stdout.columns || 80;
|
|
885
|
+
if (this.lastCursorRow > 0) {
|
|
886
|
+
readline2.moveCursor(process.stdout, 0, -this.lastCursorRow);
|
|
887
|
+
}
|
|
888
|
+
readline2.cursorTo(process.stdout, 0);
|
|
889
|
+
readline2.clearScreenDown(process.stdout);
|
|
890
|
+
const color = ansiEnabled();
|
|
891
|
+
const headerText = `${this.listPrompt}${this.listQuery}`;
|
|
892
|
+
process.stdout.write(headerText);
|
|
893
|
+
let listRows = 0;
|
|
894
|
+
if (this.listFiltered.length === 0) {
|
|
895
|
+
const text = `${color ? DIM : ""} (no matches)${color ? RESET : ""}`;
|
|
896
|
+
process.stdout.write(`
|
|
897
|
+
${text}`);
|
|
898
|
+
listRows += rowsFor(text, width);
|
|
899
|
+
} else {
|
|
900
|
+
for (const [pos, idx] of this.listFiltered.entries()) {
|
|
901
|
+
const selected = pos === this.listIndex;
|
|
902
|
+
const marker = selected ? "\u203A" : " ";
|
|
903
|
+
const text = `${marker} ${this.listAllItems[idx]}`;
|
|
904
|
+
const rendered = `${selected && color ? INVERT : ""}${text}${selected && color ? RESET : ""}`;
|
|
905
|
+
process.stdout.write(`
|
|
906
|
+
${rendered}`);
|
|
907
|
+
listRows += rowsFor(rendered, width);
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
const headerRows = rowsFor(headerText, width);
|
|
911
|
+
const rowsBelowFirst = headerRows - 1 + listRows;
|
|
912
|
+
if (rowsBelowFirst > 0) {
|
|
913
|
+
readline2.moveCursor(process.stdout, 0, -rowsBelowFirst);
|
|
914
|
+
}
|
|
915
|
+
const cursorAbsolute = displayWidth(headerText);
|
|
916
|
+
const cursorRow = Math.floor(cursorAbsolute / width);
|
|
917
|
+
const cursorCol = cursorAbsolute % width;
|
|
918
|
+
if (cursorRow > 0) {
|
|
919
|
+
readline2.moveCursor(process.stdout, 0, cursorRow);
|
|
920
|
+
}
|
|
921
|
+
readline2.cursorTo(process.stdout, cursorCol);
|
|
922
|
+
this.lastCursorRow = cursorRow;
|
|
923
|
+
}
|
|
924
|
+
finishList(index) {
|
|
925
|
+
this.listMode = false;
|
|
926
|
+
if (this.lastCursorRow > 0) {
|
|
927
|
+
readline2.moveCursor(process.stdout, 0, -this.lastCursorRow);
|
|
928
|
+
}
|
|
929
|
+
readline2.cursorTo(process.stdout, 0);
|
|
930
|
+
readline2.clearScreenDown(process.stdout);
|
|
931
|
+
this.lastCursorRow = 0;
|
|
932
|
+
const resolve = this.resolveList;
|
|
933
|
+
this.resolveList = null;
|
|
934
|
+
resolve?.(index);
|
|
935
|
+
}
|
|
936
|
+
findSearchMatch(next) {
|
|
937
|
+
if (!this.searchQuery) {
|
|
938
|
+
this.searchMatch = void 0;
|
|
939
|
+
return;
|
|
940
|
+
}
|
|
941
|
+
const start = next ? this.searchScanIndex - 1 : this.history.length - 1;
|
|
942
|
+
for (let i = start; i >= 0; i--) {
|
|
943
|
+
if (this.history[i].includes(this.searchQuery)) {
|
|
944
|
+
this.searchMatch = this.history[i];
|
|
945
|
+
this.searchScanIndex = i;
|
|
946
|
+
return;
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
this.searchMatch = void 0;
|
|
950
|
+
}
|
|
951
|
+
exitSearch() {
|
|
952
|
+
this.searchMode = false;
|
|
953
|
+
this.searchQuery = "";
|
|
954
|
+
this.searchMatch = void 0;
|
|
955
|
+
}
|
|
956
|
+
submit() {
|
|
957
|
+
const line = this.buffer;
|
|
958
|
+
if (line.trim().length > 0) {
|
|
959
|
+
this.history.push(line);
|
|
960
|
+
}
|
|
961
|
+
this.finish(line);
|
|
962
|
+
}
|
|
963
|
+
finish(line) {
|
|
964
|
+
this.menuVisible = false;
|
|
965
|
+
if (this.lastCursorRow > 0) {
|
|
966
|
+
readline2.moveCursor(process.stdout, 0, -this.lastCursorRow);
|
|
967
|
+
}
|
|
968
|
+
readline2.cursorTo(process.stdout, 0);
|
|
969
|
+
readline2.clearScreenDown(process.stdout);
|
|
970
|
+
process.stdout.write(line !== null ? `${this.prompt}${line}
|
|
971
|
+
` : "\n");
|
|
972
|
+
this.lastCursorRow = 0;
|
|
973
|
+
const resolve = this.resolveLine;
|
|
974
|
+
this.resolveLine = null;
|
|
975
|
+
if (line === null) {
|
|
976
|
+
this.close();
|
|
977
|
+
}
|
|
978
|
+
resolve?.(line);
|
|
979
|
+
}
|
|
980
|
+
/** Force the in-flight read to end as EOF, e.g. on an idle Ctrl+C. */
|
|
981
|
+
requestExit() {
|
|
982
|
+
if (this.searchMode) {
|
|
983
|
+
this.exitSearch();
|
|
984
|
+
}
|
|
985
|
+
this.finish(null);
|
|
986
|
+
}
|
|
987
|
+
render() {
|
|
988
|
+
const width = process.stdout.columns || 80;
|
|
989
|
+
if (this.lastCursorRow > 0) {
|
|
990
|
+
readline2.moveCursor(process.stdout, 0, -this.lastCursorRow);
|
|
991
|
+
}
|
|
992
|
+
readline2.cursorTo(process.stdout, 0);
|
|
993
|
+
readline2.clearScreenDown(process.stdout);
|
|
994
|
+
const color = ansiEnabled();
|
|
995
|
+
if (this.searchMode) {
|
|
996
|
+
const label = `(reverse-i-search)\`${this.searchQuery}': `;
|
|
997
|
+
const text = label + (this.searchMatch ?? "");
|
|
998
|
+
process.stdout.write(text);
|
|
999
|
+
this.lastCursorRow = rowsFor(text, width) - 1;
|
|
1000
|
+
return;
|
|
1001
|
+
}
|
|
1002
|
+
const inputText = this.prompt + this.buffer;
|
|
1003
|
+
process.stdout.write(inputText);
|
|
1004
|
+
let menuRows = 0;
|
|
1005
|
+
if (this.menuVisible) {
|
|
1006
|
+
for (const [i, item] of this.menuItems.entries()) {
|
|
1007
|
+
const selected = i === this.menuIndex;
|
|
1008
|
+
const marker = selected ? "\u203A" : " ";
|
|
1009
|
+
const usage = item.usage ? ` ${item.usage}` : "";
|
|
1010
|
+
const text = `${marker} ${item.name}${usage} ${color ? DIM : ""}\u2014 ${item.description}${color ? RESET : ""}`;
|
|
1011
|
+
const rendered = `${selected && color ? INVERT : ""}${text}${selected && color ? RESET : ""}`;
|
|
1012
|
+
process.stdout.write(`
|
|
1013
|
+
${rendered}`);
|
|
1014
|
+
menuRows += rowsFor(rendered, width);
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
const inputRows = rowsFor(inputText, width);
|
|
1018
|
+
const rowsBelowFirst = inputRows - 1 + menuRows;
|
|
1019
|
+
if (rowsBelowFirst > 0) {
|
|
1020
|
+
readline2.moveCursor(process.stdout, 0, -rowsBelowFirst);
|
|
1021
|
+
}
|
|
1022
|
+
const cursorAbsolute = displayWidth(this.prompt) + displayWidth(this.buffer.slice(0, this.cursorStringIndex()));
|
|
1023
|
+
const cursorRow = Math.floor(cursorAbsolute / width);
|
|
1024
|
+
const cursorCol = cursorAbsolute % width;
|
|
1025
|
+
if (cursorRow > 0) {
|
|
1026
|
+
readline2.moveCursor(process.stdout, 0, cursorRow);
|
|
1027
|
+
}
|
|
1028
|
+
readline2.cursorTo(process.stdout, cursorCol);
|
|
1029
|
+
this.lastCursorRow = cursorRow;
|
|
1030
|
+
}
|
|
1031
|
+
};
|
|
1032
|
+
|
|
1033
|
+
// src/models.ts
|
|
1034
|
+
async function listModels(client, adapterName, skill) {
|
|
1035
|
+
const { models } = await client.getAdapterModels(adapterName, skill);
|
|
1036
|
+
return models;
|
|
1037
|
+
}
|
|
1038
|
+
function findModel(models, query) {
|
|
1039
|
+
const index = Number(query);
|
|
1040
|
+
if (Number.isInteger(index) && index >= 1 && index <= models.length) {
|
|
1041
|
+
return models[index - 1];
|
|
1042
|
+
}
|
|
1043
|
+
return models.find((m) => m.name?.toLowerCase() === query.toLowerCase() || m.id?.toLowerCase() === query.toLowerCase());
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
// src/markdown.ts
|
|
1047
|
+
import { highlight, supportsLanguage } from "cli-highlight";
|
|
1048
|
+
var RESET2 = "\x1B[0m";
|
|
1049
|
+
var BOLD = "\x1B[1m";
|
|
1050
|
+
var DIM2 = "\x1B[2m";
|
|
1051
|
+
var ITALIC = "\x1B[3m";
|
|
1052
|
+
var HEADING = "\x1B[1;36m";
|
|
1053
|
+
function styleLine(line) {
|
|
1054
|
+
const heading = /^(#{1,6})\s+(.*)$/.exec(line);
|
|
1055
|
+
if (heading) {
|
|
1056
|
+
return `${HEADING}${heading[2]}${RESET2}`;
|
|
1057
|
+
}
|
|
1058
|
+
let out = line;
|
|
1059
|
+
out = out.replace(/\*\*(.+?)\*\*/g, `${BOLD}$1${RESET2}`);
|
|
1060
|
+
out = out.replace(/__(.+?)__/g, `${BOLD}$1${RESET2}`);
|
|
1061
|
+
out = out.replace(/(?<!\*)\*([^*]+)\*(?!\*)/g, `${ITALIC}$1${RESET2}`);
|
|
1062
|
+
out = out.replace(/`([^`]+)`/g, `${DIM2}$1${RESET2}`);
|
|
1063
|
+
out = out.replace(/^(\s*)[-*+]\s+/, "$1\u2022 ");
|
|
1064
|
+
out = out.replace(/^(\s*>)\s?/, `${DIM2}\u2502${RESET2} `);
|
|
1065
|
+
return out;
|
|
1066
|
+
}
|
|
1067
|
+
function stripLine(line) {
|
|
1068
|
+
return line.replace(/^#{1,6}\s+/, "").replace(/\*\*(.+?)\*\*/g, "$1").replace(/__(.+?)__/g, "$1").replace(/`([^`]+)`/g, "$1").replace(/^(\s*)[-*+]\s+/, "$1- ");
|
|
1069
|
+
}
|
|
1070
|
+
var MarkdownRenderer = class {
|
|
1071
|
+
buffer = "";
|
|
1072
|
+
inFence = false;
|
|
1073
|
+
fenceLang;
|
|
1074
|
+
push(text) {
|
|
1075
|
+
this.buffer += text;
|
|
1076
|
+
let newlineIndex;
|
|
1077
|
+
while ((newlineIndex = this.buffer.indexOf("\n")) !== -1) {
|
|
1078
|
+
const line = this.buffer.slice(0, newlineIndex);
|
|
1079
|
+
this.buffer = this.buffer.slice(newlineIndex + 1);
|
|
1080
|
+
this.emitLine(line);
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
/** Flush any trailing partial line (the model's reply need not end in \n). */
|
|
1084
|
+
flush() {
|
|
1085
|
+
if (this.buffer.length > 0) {
|
|
1086
|
+
this.emitLine(this.buffer);
|
|
1087
|
+
this.buffer = "";
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
emitLine(line) {
|
|
1091
|
+
const color = ansiEnabled();
|
|
1092
|
+
const fenceMatch = /^```\s*(\S*)/.exec(line.trim());
|
|
1093
|
+
if (fenceMatch) {
|
|
1094
|
+
this.inFence = !this.inFence;
|
|
1095
|
+
this.fenceLang = this.inFence ? fenceMatch[1] || void 0 : void 0;
|
|
1096
|
+
if (color) {
|
|
1097
|
+
process.stdout.write(`${DIM2}${line}${RESET2}
|
|
1098
|
+
`);
|
|
1099
|
+
}
|
|
1100
|
+
return;
|
|
1101
|
+
}
|
|
1102
|
+
if (this.inFence) {
|
|
1103
|
+
if (!color) {
|
|
1104
|
+
process.stdout.write(`${line}
|
|
1105
|
+
`);
|
|
1106
|
+
return;
|
|
1107
|
+
}
|
|
1108
|
+
if (this.fenceLang && supportsLanguage(this.fenceLang)) {
|
|
1109
|
+
try {
|
|
1110
|
+
process.stdout.write(`${highlight(line, { language: this.fenceLang, ignoreIllegals: true })}
|
|
1111
|
+
`);
|
|
1112
|
+
return;
|
|
1113
|
+
} catch {
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
process.stdout.write(`${DIM2}${line}${RESET2}
|
|
1117
|
+
`);
|
|
1118
|
+
return;
|
|
1119
|
+
}
|
|
1120
|
+
process.stdout.write(`${color ? styleLine(line) : stripLine(line)}
|
|
1121
|
+
`);
|
|
1122
|
+
}
|
|
1123
|
+
};
|
|
1124
|
+
|
|
1125
|
+
// src/spinner.ts
|
|
1126
|
+
var FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
1127
|
+
var INTERVAL_MS = 80;
|
|
1128
|
+
var Spinner = class {
|
|
1129
|
+
timer = null;
|
|
1130
|
+
frame = 0;
|
|
1131
|
+
start() {
|
|
1132
|
+
if (!ansiEnabled() || this.timer) {
|
|
1133
|
+
return;
|
|
1134
|
+
}
|
|
1135
|
+
this.timer = setInterval(() => {
|
|
1136
|
+
process.stdout.write(`\r${FRAMES[this.frame]} `);
|
|
1137
|
+
this.frame = (this.frame + 1) % FRAMES.length;
|
|
1138
|
+
}, INTERVAL_MS);
|
|
1139
|
+
}
|
|
1140
|
+
/** Stop and erase the spinner line so following output starts at column 0. */
|
|
1141
|
+
stop() {
|
|
1142
|
+
if (!this.timer) {
|
|
1143
|
+
return;
|
|
1144
|
+
}
|
|
1145
|
+
clearInterval(this.timer);
|
|
1146
|
+
this.timer = null;
|
|
1147
|
+
process.stdout.write("\r\x1B[K");
|
|
1148
|
+
}
|
|
1149
|
+
};
|
|
1150
|
+
|
|
1151
|
+
// src/repl.ts
|
|
1152
|
+
var COMMANDS = [
|
|
1153
|
+
{ name: "/new", usage: "", description: "start a new session (clears server-side context)" },
|
|
1154
|
+
{ name: "/agents", usage: "<name|number>", description: "pick an agent (arrow keys), or /agents <name|number>" },
|
|
1155
|
+
{ name: "/models", usage: "", description: "pick a model for the current agent (arrow keys)" },
|
|
1156
|
+
{ name: "/model", usage: "<id|number>", description: "set the model for later turns" },
|
|
1157
|
+
{ name: "/key", usage: "", description: "switch API key without restarting (masked prompt)" },
|
|
1158
|
+
{ name: "/clear", usage: "", description: "clear the screen and this session's server-side history" },
|
|
1159
|
+
{ name: "/help", usage: "", description: "show this message" },
|
|
1160
|
+
{ name: "/exit", usage: "", description: "exit" }
|
|
1161
|
+
];
|
|
1162
|
+
var HELP = `Commands:
|
|
1163
|
+
${COMMANDS.map((c) => ` ${(c.name + " " + c.usage).padEnd(21)}${c.description}`).join("\n")}
|
|
1164
|
+
Ctrl+C cancel the in-flight turn; again (while idle) to exit
|
|
1165
|
+
Ctrl+R reverse search through this session's input history
|
|
1166
|
+
Ctrl+D exit
|
|
1167
|
+
@path/to/file inline in a message attaches that file to the turn.
|
|
1168
|
+
Type / to see a filtered command menu (arrow keys + Tab/Enter to pick).
|
|
1169
|
+
Anything else is sent as a chat turn.`;
|
|
1170
|
+
function completer(line) {
|
|
1171
|
+
const match = /(?:^|\s)@(\S*)$/.exec(line);
|
|
1172
|
+
if (!match) {
|
|
1173
|
+
return [[], line];
|
|
1174
|
+
}
|
|
1175
|
+
const partial = match[1];
|
|
1176
|
+
const slash = partial.lastIndexOf("/");
|
|
1177
|
+
const dir = slash === -1 ? "." : partial.slice(0, slash) || "/";
|
|
1178
|
+
const prefix = slash === -1 ? partial : partial.slice(slash + 1);
|
|
1179
|
+
const dirPrefix = slash === -1 ? "" : `${partial.slice(0, slash)}/`;
|
|
1180
|
+
let entries;
|
|
1181
|
+
try {
|
|
1182
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
1183
|
+
} catch {
|
|
1184
|
+
return [[], partial];
|
|
1185
|
+
}
|
|
1186
|
+
const hits = entries.filter((entry) => entry.name.startsWith(prefix)).map((entry) => `${dirPrefix}${entry.name}${entry.isDirectory() ? "/" : ""}`);
|
|
1187
|
+
return [hits, partial];
|
|
1188
|
+
}
|
|
1189
|
+
var CLEAR_HISTORY_TIMEOUT_MS = 1500;
|
|
1190
|
+
async function clearHistoryBestEffort(client, sessionId) {
|
|
1191
|
+
let timedOut = false;
|
|
1192
|
+
let error;
|
|
1193
|
+
await new Promise((resolve) => {
|
|
1194
|
+
const timer = setTimeout(() => {
|
|
1195
|
+
timedOut = true;
|
|
1196
|
+
resolve();
|
|
1197
|
+
}, CLEAR_HISTORY_TIMEOUT_MS);
|
|
1198
|
+
client.clearConversationHistory(sessionId).catch((err) => {
|
|
1199
|
+
error = classifyError(err).message;
|
|
1200
|
+
}).then(() => {
|
|
1201
|
+
clearTimeout(timer);
|
|
1202
|
+
resolve();
|
|
1203
|
+
});
|
|
1204
|
+
});
|
|
1205
|
+
return { timedOut, error };
|
|
1206
|
+
}
|
|
1207
|
+
async function runRepl(initialClient, connection, initialSkill, initialModel) {
|
|
1208
|
+
let client = initialClient;
|
|
1209
|
+
let ownAdapter;
|
|
1210
|
+
try {
|
|
1211
|
+
ownAdapter = await client.getAdapterInfo();
|
|
1212
|
+
} catch (error) {
|
|
1213
|
+
const { code, message } = classifyError(error);
|
|
1214
|
+
process.stderr.write(`${message}
|
|
1215
|
+
`);
|
|
1216
|
+
return code;
|
|
1217
|
+
}
|
|
1218
|
+
let agents = await listAgents(client, ownAdapter);
|
|
1219
|
+
let currentAgent = agents[0];
|
|
1220
|
+
if (initialSkill) {
|
|
1221
|
+
const found = findAgent(agents, initialSkill);
|
|
1222
|
+
if (found) {
|
|
1223
|
+
currentAgent = found;
|
|
1224
|
+
} else {
|
|
1225
|
+
process.stderr.write(`Unknown agent "${initialSkill}"; using the default adapter.
|
|
1226
|
+
`);
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1229
|
+
let currentModel = initialModel;
|
|
1230
|
+
let lastModels = [];
|
|
1231
|
+
let controller = null;
|
|
1232
|
+
let currentRequestId;
|
|
1233
|
+
let hasAbandonedCleanup = false;
|
|
1234
|
+
const pendingBackgroundCleanups = /* @__PURE__ */ new Set();
|
|
1235
|
+
const rl = new LineReader({
|
|
1236
|
+
prompt: "> ",
|
|
1237
|
+
commands: COMMANDS,
|
|
1238
|
+
completer,
|
|
1239
|
+
onSigint: () => {
|
|
1240
|
+
if (controller) {
|
|
1241
|
+
controller.abort();
|
|
1242
|
+
const sessionId2 = client.getSessionId();
|
|
1243
|
+
if (sessionId2 && currentRequestId) {
|
|
1244
|
+
client.stopChat(sessionId2, currentRequestId).catch(() => {
|
|
1245
|
+
});
|
|
1246
|
+
}
|
|
1247
|
+
process.stdout.write("\n[cancelled]\n");
|
|
1248
|
+
} else {
|
|
1249
|
+
rl.requestExit();
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
});
|
|
1253
|
+
process.stdout.write("Connected. Type /help for commands, Ctrl+D to exit.\n");
|
|
1254
|
+
for await (const line of rl) {
|
|
1255
|
+
const text = line.trim();
|
|
1256
|
+
if (!text) {
|
|
1257
|
+
continue;
|
|
1258
|
+
}
|
|
1259
|
+
if (text.startsWith("/")) {
|
|
1260
|
+
const [command, ...rest] = text.split(/\s+/);
|
|
1261
|
+
const arg = rest.join(" ");
|
|
1262
|
+
switch (command) {
|
|
1263
|
+
case "/new": {
|
|
1264
|
+
const oldSessionId = client.getSessionId();
|
|
1265
|
+
client.setSessionId(randomUUID2());
|
|
1266
|
+
process.stdout.write("Started a new session.\n");
|
|
1267
|
+
if (oldSessionId) {
|
|
1268
|
+
const cleanup = clearHistoryBestEffort(client, oldSessionId).then(({ timedOut, error }) => {
|
|
1269
|
+
if (timedOut) {
|
|
1270
|
+
hasAbandonedCleanup = true;
|
|
1271
|
+
}
|
|
1272
|
+
if (timedOut || error) {
|
|
1273
|
+
process.stderr.write(`Previous session's history was not cleared: ${error ?? "timed out"}
|
|
1274
|
+
`);
|
|
1275
|
+
}
|
|
1276
|
+
}).catch(() => {
|
|
1277
|
+
});
|
|
1278
|
+
pendingBackgroundCleanups.add(cleanup);
|
|
1279
|
+
cleanup.finally(() => pendingBackgroundCleanups.delete(cleanup));
|
|
1280
|
+
}
|
|
1281
|
+
break;
|
|
1282
|
+
}
|
|
1283
|
+
case "/agents":
|
|
1284
|
+
if (!arg) {
|
|
1285
|
+
const idx = await rl.selectFromList(
|
|
1286
|
+
"Select agent (type to filter, Esc to cancel): ",
|
|
1287
|
+
agents.map((a) => `${a.label}${a === currentAgent ? " (current)" : ""}`)
|
|
1288
|
+
);
|
|
1289
|
+
if (idx !== null) {
|
|
1290
|
+
currentAgent = agents[idx];
|
|
1291
|
+
currentModel = void 0;
|
|
1292
|
+
lastModels = [];
|
|
1293
|
+
process.stdout.write(`Switched to ${currentAgent.label}.
|
|
1294
|
+
`);
|
|
1295
|
+
}
|
|
1296
|
+
} else {
|
|
1297
|
+
const found = findAgent(agents, arg);
|
|
1298
|
+
if (!found) {
|
|
1299
|
+
process.stdout.write(`No agent matches "${arg}". Try /agents to list them.
|
|
1300
|
+
`);
|
|
1301
|
+
} else {
|
|
1302
|
+
currentAgent = found;
|
|
1303
|
+
currentModel = void 0;
|
|
1304
|
+
lastModels = [];
|
|
1305
|
+
process.stdout.write(`Switched to ${found.label}.
|
|
1306
|
+
`);
|
|
1307
|
+
}
|
|
1308
|
+
}
|
|
1309
|
+
break;
|
|
1310
|
+
case "/models":
|
|
1311
|
+
try {
|
|
1312
|
+
lastModels = await listModels(client, currentAgent.adapterName, currentAgent.skill);
|
|
1313
|
+
if (lastModels.length === 0) {
|
|
1314
|
+
process.stdout.write("This agent has no restricted model list (uses its configured default).\n");
|
|
1315
|
+
} else {
|
|
1316
|
+
const idx = await rl.selectFromList(
|
|
1317
|
+
"Select model (type to filter, Esc to cancel): ",
|
|
1318
|
+
lastModels.map((m) => {
|
|
1319
|
+
const id = m.name ?? m.id;
|
|
1320
|
+
return `${id}${id === currentModel ? " (current)" : ""}`;
|
|
1321
|
+
})
|
|
1322
|
+
);
|
|
1323
|
+
if (idx !== null) {
|
|
1324
|
+
const picked = lastModels[idx];
|
|
1325
|
+
currentModel = picked.name ?? picked.id;
|
|
1326
|
+
process.stdout.write(`Model set to ${currentModel}.
|
|
1327
|
+
`);
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
} catch (error) {
|
|
1331
|
+
const { message: message2 } = classifyError(error);
|
|
1332
|
+
process.stderr.write(`${message2}
|
|
1333
|
+
`);
|
|
1334
|
+
}
|
|
1335
|
+
break;
|
|
1336
|
+
case "/model":
|
|
1337
|
+
if (!arg) {
|
|
1338
|
+
process.stdout.write(`Current model: ${currentModel ?? "(adapter default)"}
|
|
1339
|
+
`);
|
|
1340
|
+
} else {
|
|
1341
|
+
const found = findModel(lastModels, arg);
|
|
1342
|
+
currentModel = found ? found.name ?? found.id : arg;
|
|
1343
|
+
process.stdout.write(`Model set to ${currentModel}.
|
|
1344
|
+
`);
|
|
1345
|
+
}
|
|
1346
|
+
break;
|
|
1347
|
+
case "/key": {
|
|
1348
|
+
if (arg) {
|
|
1349
|
+
process.stdout.write(
|
|
1350
|
+
"For your key's safety, run /key with no argument \u2014 it will prompt you separately with input hidden.\n"
|
|
1351
|
+
);
|
|
1352
|
+
break;
|
|
1353
|
+
}
|
|
1354
|
+
const newKey = await rl.readSecret("API key: ");
|
|
1355
|
+
if (newKey === null) {
|
|
1356
|
+
process.stdout.write("Cancelled.\n");
|
|
1357
|
+
break;
|
|
1358
|
+
}
|
|
1359
|
+
if (!newKey.trim()) {
|
|
1360
|
+
process.stderr.write("API key cannot be empty.\n");
|
|
1361
|
+
break;
|
|
1362
|
+
}
|
|
1363
|
+
const candidate = createClient(
|
|
1364
|
+
{ url: connection.url, apiKey: newKey.trim() },
|
|
1365
|
+
client.getSessionId() ?? void 0
|
|
1366
|
+
);
|
|
1367
|
+
let newAdapter;
|
|
1368
|
+
try {
|
|
1369
|
+
newAdapter = await candidate.getAdapterInfo();
|
|
1370
|
+
} catch (error) {
|
|
1371
|
+
const { message: message2 } = classifyError(error);
|
|
1372
|
+
process.stderr.write(`Could not switch key: ${message2}
|
|
1373
|
+
`);
|
|
1374
|
+
break;
|
|
1375
|
+
}
|
|
1376
|
+
client = candidate;
|
|
1377
|
+
ownAdapter = newAdapter;
|
|
1378
|
+
agents = await listAgents(client, ownAdapter);
|
|
1379
|
+
currentAgent = agents[0];
|
|
1380
|
+
currentModel = void 0;
|
|
1381
|
+
lastModels = [];
|
|
1382
|
+
process.stdout.write(`Switched API key. Now using ${currentAgent.label}.
|
|
1383
|
+
`);
|
|
1384
|
+
break;
|
|
1385
|
+
}
|
|
1386
|
+
case "/clear": {
|
|
1387
|
+
const sessionId2 = client.getSessionId();
|
|
1388
|
+
let historyError;
|
|
1389
|
+
if (sessionId2) {
|
|
1390
|
+
const result = await clearHistoryBestEffort(client, sessionId2);
|
|
1391
|
+
if (result.timedOut) {
|
|
1392
|
+
hasAbandonedCleanup = true;
|
|
1393
|
+
}
|
|
1394
|
+
historyError = result.timedOut ? "timed out" : result.error;
|
|
1395
|
+
}
|
|
1396
|
+
process.stdout.write("\x1B[H\x1B[2J\x1B[3J");
|
|
1397
|
+
if (historyError) {
|
|
1398
|
+
process.stderr.write(`Terminal cleared, but server-side history was not: ${historyError}
|
|
1399
|
+
`);
|
|
1400
|
+
}
|
|
1401
|
+
break;
|
|
1402
|
+
}
|
|
1403
|
+
case "/help":
|
|
1404
|
+
process.stdout.write(`${HELP}
|
|
1405
|
+
`);
|
|
1406
|
+
break;
|
|
1407
|
+
case "/exit":
|
|
1408
|
+
rl.requestExit();
|
|
1409
|
+
continue;
|
|
1410
|
+
default:
|
|
1411
|
+
process.stdout.write(`Unknown command: ${command} (try /help)
|
|
1412
|
+
`);
|
|
1413
|
+
}
|
|
1414
|
+
continue;
|
|
1415
|
+
}
|
|
1416
|
+
let message;
|
|
1417
|
+
let fileIds;
|
|
1418
|
+
try {
|
|
1419
|
+
({ text: message, fileIds } = await extractAttachments(client, text));
|
|
1420
|
+
} catch (error) {
|
|
1421
|
+
if (error instanceof AttachmentError) {
|
|
1422
|
+
process.stderr.write(`${error.message}
|
|
1423
|
+
`);
|
|
1424
|
+
} else {
|
|
1425
|
+
const { message: msg } = classifyError(error);
|
|
1426
|
+
process.stderr.write(`${msg}
|
|
1427
|
+
`);
|
|
1428
|
+
}
|
|
1429
|
+
continue;
|
|
1430
|
+
}
|
|
1431
|
+
controller = new AbortController();
|
|
1432
|
+
currentRequestId = void 0;
|
|
1433
|
+
const renderer = new MarkdownRenderer();
|
|
1434
|
+
const spinner = new Spinner();
|
|
1435
|
+
spinner.start();
|
|
1436
|
+
try {
|
|
1437
|
+
for await (const chunk of streamChat(client, message, {
|
|
1438
|
+
skill: currentAgent.skill,
|
|
1439
|
+
model: currentModel,
|
|
1440
|
+
fileIds,
|
|
1441
|
+
abortSignal: controller.signal
|
|
1442
|
+
})) {
|
|
1443
|
+
if (chunk.request_id) {
|
|
1444
|
+
currentRequestId = chunk.request_id;
|
|
1445
|
+
}
|
|
1446
|
+
if (chunk.text) {
|
|
1447
|
+
spinner.stop();
|
|
1448
|
+
renderer.push(chunk.text);
|
|
1449
|
+
}
|
|
1450
|
+
const artifactPaths = await saveArtifacts(client, chunk);
|
|
1451
|
+
if (artifactPaths.length > 0) {
|
|
1452
|
+
spinner.stop();
|
|
1453
|
+
renderer.flush();
|
|
1454
|
+
for (const path of artifactPaths) {
|
|
1455
|
+
process.stdout.write(`Saved artifact: ${path}
|
|
1456
|
+
`);
|
|
1457
|
+
}
|
|
1458
|
+
}
|
|
1459
|
+
}
|
|
1460
|
+
spinner.stop();
|
|
1461
|
+
renderer.flush();
|
|
1462
|
+
process.stdout.write("\n");
|
|
1463
|
+
} catch (error) {
|
|
1464
|
+
spinner.stop();
|
|
1465
|
+
renderer.flush();
|
|
1466
|
+
if (!controller.signal.aborted) {
|
|
1467
|
+
const { message: msg } = classifyError(error);
|
|
1468
|
+
process.stderr.write(`${msg}
|
|
1469
|
+
`);
|
|
1470
|
+
}
|
|
1471
|
+
} finally {
|
|
1472
|
+
spinner.stop();
|
|
1473
|
+
controller = null;
|
|
1474
|
+
currentRequestId = void 0;
|
|
1475
|
+
}
|
|
1476
|
+
}
|
|
1477
|
+
process.stdout.write("\n");
|
|
1478
|
+
const sessionId = client.getSessionId();
|
|
1479
|
+
if (sessionId) {
|
|
1480
|
+
const { timedOut } = await clearHistoryBestEffort(client, sessionId);
|
|
1481
|
+
if (timedOut) {
|
|
1482
|
+
hasAbandonedCleanup = true;
|
|
1483
|
+
}
|
|
1484
|
+
}
|
|
1485
|
+
if (hasAbandonedCleanup || pendingBackgroundCleanups.size > 0) {
|
|
1486
|
+
process.exit(0);
|
|
1487
|
+
}
|
|
1488
|
+
return 0;
|
|
1489
|
+
}
|
|
1490
|
+
|
|
1491
|
+
// src/stdin.ts
|
|
1492
|
+
async function readStdin() {
|
|
1493
|
+
const chunks = [];
|
|
1494
|
+
for await (const chunk of process.stdin) {
|
|
1495
|
+
chunks.push(chunk);
|
|
1496
|
+
}
|
|
1497
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
1498
|
+
}
|
|
1499
|
+
|
|
1500
|
+
// src/cli.ts
|
|
1501
|
+
async function main() {
|
|
1502
|
+
let args;
|
|
1503
|
+
try {
|
|
1504
|
+
args = parseArgs(process.argv.slice(2));
|
|
1505
|
+
} catch (error) {
|
|
1506
|
+
if (error instanceof ArgsError) {
|
|
1507
|
+
process.stderr.write(`${error.message}
|
|
1508
|
+
`);
|
|
1509
|
+
process.exit(EXIT_USAGE);
|
|
1510
|
+
}
|
|
1511
|
+
throw error;
|
|
1512
|
+
}
|
|
1513
|
+
let connection;
|
|
1514
|
+
try {
|
|
1515
|
+
connection = await resolveConnection(args);
|
|
1516
|
+
} catch (error) {
|
|
1517
|
+
if (error instanceof ConfigError) {
|
|
1518
|
+
process.stderr.write(`${error.message}
|
|
1519
|
+
`);
|
|
1520
|
+
process.exit(EXIT_USAGE);
|
|
1521
|
+
}
|
|
1522
|
+
throw error;
|
|
1523
|
+
}
|
|
1524
|
+
const client = createClient(connection, args.health ? void 0 : randomUUID3());
|
|
1525
|
+
if (args.health) {
|
|
1526
|
+
try {
|
|
1527
|
+
const [health, adapterInfo] = await Promise.all([
|
|
1528
|
+
client.getHealth(),
|
|
1529
|
+
client.getAdapterInfo()
|
|
1530
|
+
]);
|
|
1531
|
+
process.stdout.write(`${JSON.stringify({ ...health, adapter: adapterInfo })}
|
|
1532
|
+
`);
|
|
1533
|
+
process.exit(EXIT_OK);
|
|
1534
|
+
} catch (error) {
|
|
1535
|
+
const { code: code2, message: message2 } = classifyError(error);
|
|
1536
|
+
process.stderr.write(`${message2}
|
|
1537
|
+
`);
|
|
1538
|
+
process.exit(code2);
|
|
1539
|
+
}
|
|
1540
|
+
}
|
|
1541
|
+
const rawMessage = args.positional[0];
|
|
1542
|
+
if (rawMessage === void 0) {
|
|
1543
|
+
if (!process.stdin.isTTY) {
|
|
1544
|
+
process.stderr.write(
|
|
1545
|
+
'No message provided and stdin is not a terminal. Pass a message, or "-" to read one from stdin.\n'
|
|
1546
|
+
);
|
|
1547
|
+
process.exit(EXIT_USAGE);
|
|
1548
|
+
}
|
|
1549
|
+
const code2 = await runRepl(client, connection, args.agent, args.model);
|
|
1550
|
+
process.exitCode = code2;
|
|
1551
|
+
return;
|
|
1552
|
+
}
|
|
1553
|
+
const message = rawMessage === "-" ? (await readStdin()).trim() : rawMessage;
|
|
1554
|
+
if (!message) {
|
|
1555
|
+
process.stderr.write("No message provided.\n");
|
|
1556
|
+
process.exit(EXIT_USAGE);
|
|
1557
|
+
}
|
|
1558
|
+
process.stdout.on("error", (error) => {
|
|
1559
|
+
if (error.code === "EPIPE") {
|
|
1560
|
+
process.exit(EXIT_OK);
|
|
1561
|
+
}
|
|
1562
|
+
});
|
|
1563
|
+
const controller = new AbortController();
|
|
1564
|
+
process.once("SIGINT", () => controller.abort());
|
|
1565
|
+
const code = await runOneShot(client, message, {
|
|
1566
|
+
skill: args.agent,
|
|
1567
|
+
model: args.model,
|
|
1568
|
+
signal: controller.signal
|
|
1569
|
+
});
|
|
1570
|
+
process.exitCode = code;
|
|
1571
|
+
}
|
|
1572
|
+
main().catch((error) => {
|
|
1573
|
+
process.stderr.write(`Unexpected error: ${error instanceof Error ? error.stack ?? error.message : String(error)}
|
|
1574
|
+
`);
|
|
1575
|
+
process.exit(1);
|
|
1576
|
+
});
|
|
1577
|
+
//# sourceMappingURL=cli.js.map
|