@rind-ai/cli 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,5 @@
1
+ import { runFrontendCliApp } from "./frontend-cli-implementation.js";
2
+
3
+ export function runFrontendCli(cliArgs = process.argv.slice(2)) {
4
+ return runFrontendCliApp(cliArgs);
5
+ }
@@ -0,0 +1,94 @@
1
+ export function createInputController({
2
+ terminalUi = null,
3
+ state = {},
4
+ askInput,
5
+ onSubmit = () => {},
6
+ onCommand = async () => false,
7
+ onSigint = () => {},
8
+ onPaste = () => {},
9
+ onInput = () => {},
10
+ cancelInput = () => {},
11
+ renderPrompt = () => {},
12
+ prompt = () => "",
13
+ placeholder = () => "",
14
+ }) {
15
+ const promptResumeWaiters = [];
16
+
17
+ function start() {
18
+ terminalUi?.start({
19
+ onInput,
20
+ onPaste,
21
+ });
22
+ }
23
+
24
+ async function promptLoop() {
25
+ while (!state.runtimeClosing) {
26
+ await waitForResume();
27
+ if (state.runtimeClosing) {
28
+ return;
29
+ }
30
+ // Keep the prompt callback intact so TTY redraws reflect live runtime state.
31
+ const text = (await askInput(prompt, placeholder())).trim();
32
+ if (state.runtimeClosing) {
33
+ return;
34
+ }
35
+ if (!text) {
36
+ continue;
37
+ }
38
+ if (await onCommand(text)) {
39
+ continue;
40
+ }
41
+ onSubmit(text);
42
+ }
43
+ }
44
+
45
+ function ask(promptText, placeholderText) {
46
+ return askInput(promptText, placeholderText);
47
+ }
48
+
49
+ function pause() {
50
+ state.promptPaused = true;
51
+ cancelInput();
52
+ }
53
+
54
+ function resume() {
55
+ state.promptPaused = false;
56
+ while (promptResumeWaiters.length) {
57
+ promptResumeWaiters.shift()();
58
+ }
59
+ }
60
+
61
+ function waitForResume() {
62
+ if (!state.promptPaused) {
63
+ return Promise.resolve();
64
+ }
65
+ return new Promise((resolve) => promptResumeWaiters.push(resolve));
66
+ }
67
+
68
+ function cancel() {
69
+ cancelInput();
70
+ }
71
+
72
+ function redraw(force = false) {
73
+ renderPrompt(force);
74
+ }
75
+
76
+ function close() {
77
+ cancel();
78
+ terminalUi?.stop();
79
+ }
80
+
81
+ return {
82
+ start,
83
+ promptLoop,
84
+ ask,
85
+ pause,
86
+ resume,
87
+ cancel,
88
+ redraw,
89
+ close,
90
+ handleInput: onInput,
91
+ handlePaste: onPaste,
92
+ handleSigint: onSigint,
93
+ };
94
+ }
@@ -0,0 +1,3 @@
1
+ export function isInputClosed(error) {
2
+ return error instanceof Error && ["Input closed", "readline was closed"].includes(error.message);
3
+ }
@@ -0,0 +1,9 @@
1
+ export function sigintAction({ activeTurn, interruptRequested, runtimeClosing = false }) {
2
+ if (runtimeClosing) {
3
+ return "force-shutdown";
4
+ }
5
+ if (!activeTurn) {
6
+ return "shutdown";
7
+ }
8
+ return interruptRequested ? "force-shutdown" : "interrupt";
9
+ }
@@ -0,0 +1,541 @@
1
+ import { graphemes, textWidth, wrapTextCells } from "./text-width.js";
2
+
3
+ const HISTORY_LIMIT = 100;
4
+ const UNDO_LIMIT = 100;
5
+ const INPUT_PREFIX_WIDTH = 4;
6
+
7
+ export function createLineEditor(initialValue = "") {
8
+ let lines = splitLines(initialValue);
9
+ let cursorLine = lines.length - 1;
10
+ let cursorColumn = graphemes(lines[cursorLine]).length;
11
+ const history = [];
12
+ let historyIndex = -1;
13
+ let historyDraft = null;
14
+ const undoStack = [];
15
+ let killedText = "";
16
+ let viewportWidth = 80;
17
+ let preferredVisualColumn = null;
18
+
19
+ const editor = {
20
+ input() {
21
+ return lines.join("\n");
22
+ },
23
+ cursorPosition() {
24
+ return { line: cursorLine, column: cursorColumn };
25
+ },
26
+ setInput(value) {
27
+ setText(value);
28
+ },
29
+ setViewportWidth(value) {
30
+ const width = Math.max(1, Math.floor(Number(value) || 0));
31
+ if (width !== viewportWidth) {
32
+ viewportWidth = width;
33
+ preferredVisualColumn = null;
34
+ }
35
+ },
36
+ handleInput(event = {}) {
37
+ if (event.kind === "paste") {
38
+ insertText(cleanPaste(event.text));
39
+ return "edit";
40
+ }
41
+ if (event.kind === "text") {
42
+ insertText(event.text);
43
+ return "edit";
44
+ }
45
+ return editor.handleKey(event.text || "", event);
46
+ },
47
+ handleKey(chunk, key = {}) {
48
+ if (key.name !== "up" && key.name !== "down") {
49
+ preferredVisualColumn = null;
50
+ }
51
+ if (key.name === "enter" || key.name === "return") {
52
+ if (key.shift || key.ctrl) {
53
+ insertNewline();
54
+ return "edit";
55
+ }
56
+ return "submit";
57
+ }
58
+ if (key.name === "left") {
59
+ return key.ctrl || key.alt ? moveWord(-1) : moveHorizontal(-1);
60
+ }
61
+ if (key.name === "right") {
62
+ return key.ctrl || key.alt ? moveWord(1) : moveHorizontal(1);
63
+ }
64
+ if (key.ctrl && key.name === "b") {
65
+ return moveHorizontal(-1);
66
+ }
67
+ if (key.ctrl && key.name === "f") {
68
+ return moveHorizontal(1);
69
+ }
70
+ if (key.alt && key.name === "b") {
71
+ return moveWord(-1);
72
+ }
73
+ if (key.alt && key.name === "f") {
74
+ return moveWord(1);
75
+ }
76
+ if (key.name === "up") {
77
+ if (key.ctrl || key.alt || key.shift) {
78
+ return "";
79
+ }
80
+ return moveUp();
81
+ }
82
+ if (key.name === "down") {
83
+ if (key.ctrl || key.alt || key.shift) {
84
+ return "";
85
+ }
86
+ return moveDown();
87
+ }
88
+ if (key.name === "home") {
89
+ cursorColumn = 0;
90
+ return "move";
91
+ }
92
+ if (key.name === "end") {
93
+ cursorColumn = lineLength();
94
+ return "move";
95
+ }
96
+ if (key.name === "backspace") {
97
+ return key.ctrl || key.alt ? deleteWordBackward() : deleteBackward();
98
+ }
99
+ if (key.name === "delete") {
100
+ return key.ctrl || key.alt ? deleteWordForward() : deleteForward();
101
+ }
102
+ if (key.ctrl && key.name === "a") {
103
+ cursorColumn = 0;
104
+ return "move";
105
+ }
106
+ if (key.ctrl && key.name === "e") {
107
+ cursorColumn = lineLength();
108
+ return "move";
109
+ }
110
+ if (key.ctrl && key.name === "w") {
111
+ return deleteWordBackward();
112
+ }
113
+ if (key.ctrl && key.name === "d") {
114
+ return deleteForward();
115
+ }
116
+ if (key.alt && key.name === "d") {
117
+ return deleteWordForward();
118
+ }
119
+ if (key.ctrl && key.name === "u") {
120
+ return deleteToStart();
121
+ }
122
+ if (key.ctrl && key.name === "k") {
123
+ return deleteToEnd();
124
+ }
125
+ if (key.ctrl && key.name === "y") {
126
+ return yank();
127
+ }
128
+ if (key.ctrl && (key.name === "-" || key.name === "_")) {
129
+ return undo();
130
+ }
131
+ if (key.name === "j" && key.ctrl) {
132
+ insertNewline();
133
+ return "edit";
134
+ }
135
+ if (isPrintable(chunk, key)) {
136
+ insertText(chunk);
137
+ return "edit";
138
+ }
139
+ return "";
140
+ },
141
+ addToHistory(value = "") {
142
+ const text = String(value || "").trim();
143
+ if (!text || history[0] === text) {
144
+ resetHistory();
145
+ return;
146
+ }
147
+ history.unshift(text);
148
+ if (history.length > HISTORY_LIMIT) {
149
+ history.length = HISTORY_LIMIT;
150
+ }
151
+ resetHistory();
152
+ },
153
+ };
154
+ return editor;
155
+
156
+ function setText(value) {
157
+ lines = splitLines(value);
158
+ cursorLine = lines.length - 1;
159
+ cursorColumn = lineLength();
160
+ preferredVisualColumn = null;
161
+ resetHistory();
162
+ undoStack.length = 0;
163
+ }
164
+
165
+ function splitLines(value) {
166
+ const result = normalizeText(value).split("\n");
167
+ return result.length ? result : [""];
168
+ }
169
+
170
+ function normalizeText(value) {
171
+ return String(value || "").replace(/\r\n?/g, "\n").replace(/\t/g, " ");
172
+ }
173
+
174
+ function cleanPaste(value) {
175
+ return normalizeText(value).replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g, "");
176
+ }
177
+
178
+ function lineLength(line = cursorLine) {
179
+ return graphemes(lines[line] || "").length;
180
+ }
181
+
182
+ function isEditorEmpty() {
183
+ return lines.length === 1 && lines[0] === "";
184
+ }
185
+
186
+ function moveHorizontal(delta) {
187
+ if (delta < 0 && cursorColumn === 0 && cursorLine > 0) {
188
+ cursorLine -= 1;
189
+ cursorColumn = lineLength();
190
+ return "move";
191
+ }
192
+ if (delta > 0 && cursorColumn === lineLength() && cursorLine < lines.length - 1) {
193
+ cursorLine += 1;
194
+ cursorColumn = 0;
195
+ return "move";
196
+ }
197
+ cursorColumn = Math.max(0, Math.min(lineLength(), cursorColumn + delta));
198
+ return "move";
199
+ }
200
+
201
+ function moveUp() {
202
+ const visual = visualLineState();
203
+ if (visual.index > 0) {
204
+ return moveVertical(-1, visual);
205
+ }
206
+ if (isEditorEmpty() || historyIndex > -1 || cursorColumn === 0) {
207
+ return navigateHistory(-1);
208
+ }
209
+ return moveToLineStart();
210
+ }
211
+
212
+ function moveDown() {
213
+ const visual = visualLineState();
214
+ if (visual.index < visual.lines.length - 1) {
215
+ return moveVertical(1, visual);
216
+ }
217
+ if (historyIndex > -1) {
218
+ return navigateHistory(1);
219
+ }
220
+ return moveToLineEnd();
221
+ }
222
+
223
+ function moveVertical(delta, visual) {
224
+ const current = visual.lines[visual.index];
225
+ const targetIndex = visual.index + delta;
226
+ const target = visual.lines[targetIndex];
227
+ const currentColumn = visualColumn(current, cursorColumn - current.startColumn);
228
+ const desiredColumn = preferredVisualColumn ?? currentColumn;
229
+ const targetOffset = visualOffset(target, desiredColumn);
230
+ const targetColumn = visualColumn(target, targetOffset);
231
+ cursorLine = target.line;
232
+ cursorColumn = target.startColumn + targetOffset;
233
+ preferredVisualColumn = targetColumn === desiredColumn ? null : desiredColumn;
234
+ return "move";
235
+ }
236
+
237
+ function visualLineState() {
238
+ const visualLines = [];
239
+ const contentWidth = Math.max(1, viewportWidth - INPUT_PREFIX_WIDTH);
240
+ for (const [lineIndex, text] of lines.entries()) {
241
+ const chunks = wrapTextCells(text, contentWidth, contentWidth);
242
+ for (const chunk of chunks) {
243
+ visualLines.push({
244
+ line: lineIndex,
245
+ startColumn: chunk.startColumn,
246
+ length: chunk.length,
247
+ allowsEnd: chunk.allowsEnd,
248
+ text: chunk.text,
249
+ });
250
+ }
251
+ const lastChunk = chunks.at(-1);
252
+ if (lineIndex === lines.length - 1 && !lastChunk.allowsEnd) {
253
+ visualLines.push({
254
+ line: lineIndex,
255
+ startColumn: lastChunk.startColumn + lastChunk.length,
256
+ length: 0,
257
+ allowsEnd: true,
258
+ text: "",
259
+ });
260
+ }
261
+ }
262
+
263
+ let index = visualLines.length - 1;
264
+ for (let candidate = 0; candidate < visualLines.length; candidate += 1) {
265
+ const visualLine = visualLines[candidate];
266
+ if (visualLine.line !== cursorLine) {
267
+ continue;
268
+ }
269
+ const offset = cursorColumn - visualLine.startColumn;
270
+ if (offset >= 0 && (offset < visualLine.length || (visualLine.allowsEnd && offset === visualLine.length))) {
271
+ index = candidate;
272
+ break;
273
+ }
274
+ }
275
+ return { lines: visualLines, index };
276
+ }
277
+
278
+ function visualColumn(visualLine, offset) {
279
+ return textWidth(graphemes(visualLine.text).slice(0, offset).join(""));
280
+ }
281
+
282
+ function visualOffset(visualLine, targetColumn) {
283
+ const chars = graphemes(visualLine.text);
284
+ const maxOffset = visualLine.allowsEnd ? chars.length : Math.max(0, chars.length - 1);
285
+ let offset = 0;
286
+ let column = 0;
287
+ while (offset < maxOffset) {
288
+ const nextWidth = textWidth(chars[offset]);
289
+ if (column + nextWidth > targetColumn) {
290
+ break;
291
+ }
292
+ column += nextWidth;
293
+ offset += 1;
294
+ }
295
+ return offset;
296
+ }
297
+
298
+ function moveToLineStart() {
299
+ preferredVisualColumn = null;
300
+ cursorColumn = 0;
301
+ return "move";
302
+ }
303
+
304
+ function moveToLineEnd() {
305
+ preferredVisualColumn = null;
306
+ cursorColumn = lineLength();
307
+ return "move";
308
+ }
309
+
310
+ function moveWord(delta) {
311
+ const chars = graphemes(lines[cursorLine]);
312
+ if (delta < 0) {
313
+ if (cursorColumn === 0) {
314
+ return moveHorizontal(-1);
315
+ }
316
+ let next = cursorColumn;
317
+ while (next > 0 && /\s/.test(chars[next - 1])) {
318
+ next -= 1;
319
+ }
320
+ while (next > 0 && !/\s/.test(chars[next - 1])) {
321
+ next -= 1;
322
+ }
323
+ cursorColumn = next;
324
+ return "move";
325
+ }
326
+ if (cursorColumn >= chars.length) {
327
+ return moveHorizontal(1);
328
+ }
329
+ let next = cursorColumn;
330
+ while (next < chars.length && /\s/.test(chars[next])) {
331
+ next += 1;
332
+ }
333
+ while (next < chars.length && !/\s/.test(chars[next])) {
334
+ next += 1;
335
+ }
336
+ cursorColumn = next;
337
+ return "move";
338
+ }
339
+
340
+ function insertText(value) {
341
+ const text = normalizeText(value);
342
+ if (!text) {
343
+ return;
344
+ }
345
+ preferredVisualColumn = null;
346
+ pushUndoSnapshot();
347
+ const inserted = text.split("\n");
348
+ const current = graphemes(lines[cursorLine]);
349
+ const before = current.slice(0, cursorColumn).join("");
350
+ const after = current.slice(cursorColumn).join("");
351
+ if (inserted.length === 1) {
352
+ lines[cursorLine] = before + inserted[0] + after;
353
+ cursorColumn = graphemes(before + inserted[0]).length;
354
+ } else {
355
+ lines.splice(cursorLine, 1, `${before}${inserted[0]}`, ...inserted.slice(1, -1), `${inserted.at(-1)}${after}`);
356
+ cursorLine += inserted.length - 1;
357
+ cursorColumn = graphemes(inserted.at(-1)).length;
358
+ }
359
+ resetHistory();
360
+ }
361
+
362
+ function insertNewline() {
363
+ insertText("\n");
364
+ }
365
+
366
+ function deleteBackward() {
367
+ if (cursorColumn === 0 && cursorLine === 0) {
368
+ return "edit";
369
+ }
370
+ pushUndoSnapshot();
371
+ if (cursorColumn === 0) {
372
+ const previousLength = lineLength(cursorLine - 1);
373
+ lines[cursorLine - 1] += lines[cursorLine];
374
+ lines.splice(cursorLine, 1);
375
+ cursorLine -= 1;
376
+ cursorColumn = previousLength;
377
+ } else {
378
+ const chars = graphemes(lines[cursorLine]);
379
+ chars.splice(cursorColumn - 1, 1);
380
+ lines[cursorLine] = chars.join("");
381
+ cursorColumn -= 1;
382
+ }
383
+ resetHistory();
384
+ return "edit";
385
+ }
386
+
387
+ function deleteForward() {
388
+ if (cursorColumn === lineLength() && cursorLine === lines.length - 1) {
389
+ return "edit";
390
+ }
391
+ pushUndoSnapshot();
392
+ if (cursorColumn === lineLength()) {
393
+ lines[cursorLine] += lines[cursorLine + 1];
394
+ lines.splice(cursorLine + 1, 1);
395
+ } else {
396
+ const chars = graphemes(lines[cursorLine]);
397
+ chars.splice(cursorColumn, 1);
398
+ lines[cursorLine] = chars.join("");
399
+ }
400
+ resetHistory();
401
+ return "edit";
402
+ }
403
+
404
+ function deleteWordBackward() {
405
+ const chars = graphemes(lines[cursorLine]);
406
+ if (cursorColumn === 0) {
407
+ return deleteBackward();
408
+ }
409
+ let next = cursorColumn;
410
+ while (next > 0 && /\s/.test(chars[next - 1])) {
411
+ next -= 1;
412
+ }
413
+ while (next > 0 && !/\s/.test(chars[next - 1])) {
414
+ next -= 1;
415
+ }
416
+ pushUndoSnapshot();
417
+ killedText = chars.slice(next, cursorColumn).join("");
418
+ lines[cursorLine] = chars.slice(0, next).concat(chars.slice(cursorColumn)).join("");
419
+ cursorColumn = next;
420
+ resetHistory();
421
+ return "edit";
422
+ }
423
+
424
+ function deleteWordForward() {
425
+ const chars = graphemes(lines[cursorLine]);
426
+ if (cursorColumn >= chars.length) {
427
+ return deleteForward();
428
+ }
429
+ let next = cursorColumn;
430
+ while (next < chars.length && /\s/.test(chars[next])) {
431
+ next += 1;
432
+ }
433
+ while (next < chars.length && !/\s/.test(chars[next])) {
434
+ next += 1;
435
+ }
436
+ pushUndoSnapshot();
437
+ killedText = chars.slice(cursorColumn, next).join("");
438
+ lines[cursorLine] = chars.slice(0, cursorColumn).concat(chars.slice(next)).join("");
439
+ resetHistory();
440
+ return "edit";
441
+ }
442
+
443
+ function deleteToStart() {
444
+ if (cursorColumn === 0) {
445
+ return "edit";
446
+ }
447
+ pushUndoSnapshot();
448
+ killedText = graphemes(lines[cursorLine]).slice(0, cursorColumn).join("");
449
+ lines[cursorLine] = graphemes(lines[cursorLine]).slice(cursorColumn).join("");
450
+ cursorColumn = 0;
451
+ resetHistory();
452
+ return "edit";
453
+ }
454
+
455
+ function deleteToEnd() {
456
+ if (cursorColumn === lineLength()) {
457
+ return "edit";
458
+ }
459
+ pushUndoSnapshot();
460
+ killedText = graphemes(lines[cursorLine]).slice(cursorColumn).join("");
461
+ lines[cursorLine] = graphemes(lines[cursorLine]).slice(0, cursorColumn).join("");
462
+ resetHistory();
463
+ return "edit";
464
+ }
465
+
466
+ function yank() {
467
+ if (!killedText) {
468
+ return "move";
469
+ }
470
+ insertText(killedText);
471
+ return "edit";
472
+ }
473
+
474
+ function undo() {
475
+ const snapshot = undoStack.pop();
476
+ if (!snapshot) {
477
+ return "move";
478
+ }
479
+ lines = snapshot.lines;
480
+ cursorLine = snapshot.cursorLine;
481
+ cursorColumn = snapshot.cursorColumn;
482
+ resetHistory();
483
+ return "edit";
484
+ }
485
+
486
+ function pushUndoSnapshot() {
487
+ undoStack.push({
488
+ lines: [...lines],
489
+ cursorLine,
490
+ cursorColumn,
491
+ });
492
+ if (undoStack.length > UNDO_LIMIT) {
493
+ undoStack.shift();
494
+ }
495
+ }
496
+
497
+ function resetHistory() {
498
+ historyIndex = -1;
499
+ historyDraft = null;
500
+ }
501
+
502
+ function navigateHistory(direction) {
503
+ if (!history.length) {
504
+ return "move";
505
+ }
506
+ const nextIndex = historyIndex - direction;
507
+ if (nextIndex < -1 || nextIndex >= history.length) {
508
+ return "move";
509
+ }
510
+ if (historyIndex === -1 && nextIndex >= 0) {
511
+ pushUndoSnapshot();
512
+ historyDraft = snapshot();
513
+ }
514
+ preferredVisualColumn = null;
515
+ historyIndex = nextIndex;
516
+ if (historyIndex === -1) {
517
+ restore(historyDraft || { lines: [""], cursorLine: 0, cursorColumn: 0 });
518
+ historyDraft = null;
519
+ } else {
520
+ const value = history[historyIndex];
521
+ lines = splitLines(value);
522
+ cursorLine = direction < 0 ? 0 : lines.length - 1;
523
+ cursorColumn = direction < 0 ? 0 : lineLength();
524
+ }
525
+ return "edit";
526
+ }
527
+
528
+ function snapshot() {
529
+ return { lines: [...lines], cursorLine, cursorColumn };
530
+ }
531
+
532
+ function restore(value) {
533
+ lines = [...value.lines];
534
+ cursorLine = value.cursorLine;
535
+ cursorColumn = value.cursorColumn;
536
+ }
537
+ }
538
+
539
+ function isPrintable(chunk, key) {
540
+ return Boolean(chunk && !key.ctrl && !key.alt && String(chunk) >= " ");
541
+ }
@@ -0,0 +1,50 @@
1
+ export function createModelMenuState(models, currentModel = "") {
2
+ const items = normalizeModels(models, currentModel);
3
+ let selected = Math.max(0, items.findIndex((item) => item.current));
4
+ return {
5
+ items() {
6
+ return items;
7
+ },
8
+ selectedIndex() {
9
+ return selected;
10
+ },
11
+ selectedModel() {
12
+ return items[selected] || null;
13
+ },
14
+ handleKey(key = {}) {
15
+ if (!items.length) {
16
+ return false;
17
+ }
18
+ if (key.name === "up") {
19
+ selected = selected <= 0 ? items.length - 1 : selected - 1;
20
+ return true;
21
+ }
22
+ if (key.name === "down") {
23
+ selected = selected >= items.length - 1 ? 0 : selected + 1;
24
+ return true;
25
+ }
26
+ return false;
27
+ },
28
+ };
29
+ }
30
+
31
+ function normalizeModels(models, currentModel) {
32
+ const current = String(currentModel || "").trim();
33
+ const seen = new Set();
34
+ const items = [];
35
+ let currentFound = false;
36
+ for (const model of Array.isArray(models) ? models : []) {
37
+ const name = String(model || "").trim();
38
+ if (!name || seen.has(name)) {
39
+ continue;
40
+ }
41
+ seen.add(name);
42
+ const isCurrent = name === current;
43
+ currentFound ||= isCurrent;
44
+ items.push({ name, current: isCurrent });
45
+ }
46
+ if (current && !currentFound) {
47
+ items.unshift({ name: current, current: true });
48
+ }
49
+ return items;
50
+ }