@ryuu-reinzz/ryuu-cli 0.0.0-stage → 0.3.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.
- package/CHANGELOG.md +560 -0
- package/LICENSE +21 -0
- package/README.md +1213 -2
- package/package.json +45 -4
- package/src/agent/agent.js +1194 -0
- package/src/agent/approvals.js +76 -0
- package/src/agent/context.js +106 -0
- package/src/agent/planner.js +237 -0
- package/src/agent/worklog.js +318 -0
- package/src/cli/app.js +463 -0
- package/src/cli/commands.js +1150 -0
- package/src/cli/input.js +287 -0
- package/src/cli/markdown.js +527 -0
- package/src/cli/prompts.js +255 -0
- package/src/cli/renderer.js +2155 -0
- package/src/cli/settings-editor.js +280 -0
- package/src/cli/settings-tui.js +669 -0
- package/src/cli/tui.js +1737 -0
- package/src/cli/width.js +48 -0
- package/src/config/load.js +132 -0
- package/src/config/scaffold.js +50 -0
- package/src/config/settings-registry.js +495 -0
- package/src/config/settings.js +304 -0
- package/src/config/validate.js +451 -0
- package/src/index.js +449 -0
- package/src/providers/catalog.js +282 -0
- package/src/providers/index.js +84 -0
- package/src/providers/nvidia.js +645 -0
- package/src/providers/registry.js +104 -0
- package/src/services/clipboard.js +38 -0
- package/src/services/diff.js +153 -0
- package/src/services/doctor.js +126 -0
- package/src/services/logger.js +190 -0
- package/src/services/preferences.js +249 -0
- package/src/services/processes.js +345 -0
- package/src/services/provider-settings.js +226 -0
- package/src/services/sessions.js +279 -0
- package/src/services/undo.js +133 -0
- package/src/services/ushell.js +146 -0
- package/src/services/websearch/manager.js +616 -0
- package/src/services/websearch/mcp.js +233 -0
- package/src/services/websearch/normalize.js +284 -0
- package/src/services/websearch/providers.js +178 -0
- package/src/services/websearch/serper.js +263 -0
- package/src/services/workspace.js +589 -0
- package/src/tools/filesystem.js +304 -0
- package/src/tools/git.js +114 -0
- package/src/tools/index.js +48 -0
- package/src/tools/project.js +160 -0
- package/src/tools/registry.js +118 -0
- package/src/tools/search.js +210 -0
- package/src/tools/shell.js +279 -0
- package/src/tools/url.js +542 -0
- package/src/tools/web.js +67 -0
- package/src/utils/errors.js +160 -0
- package/src/utils/network.js +129 -0
- package/src/utils/redaction.js +73 -0
- package/src/utils/validation.js +370 -0
- package/src/utils/version.js +24 -0
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
import readline from 'node:readline/promises';
|
|
2
|
+
import { stdin, stdout } from 'node:process';
|
|
3
|
+
import { ComposerInput, PasteRegistry, composerWidth } from './input.js';
|
|
4
|
+
|
|
5
|
+
const PASTE_MODE_ON = '\x1b[?2004h';
|
|
6
|
+
const PASTE_MODE_OFF = '\x1b[?2004l';
|
|
7
|
+
const MOUSE_MODE_ON = '\x1b[?1000h\x1b[?1006h';
|
|
8
|
+
const MOUSE_MODE_OFF = '\x1b[?1000l\x1b[?1006l';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Thin wrapper around readline/promises.
|
|
12
|
+
*
|
|
13
|
+
* In a terminal it filters stdin first: long pastes become composer
|
|
14
|
+
* placeholders, mouse clicks are routed to the renderer, and keys that belong
|
|
15
|
+
* to an open block panel never reach the line buffer. It can be reset after
|
|
16
|
+
* Ctrl+C so a pending approval question does not swallow the next command.
|
|
17
|
+
*
|
|
18
|
+
* One read is armed for the whole session instead of one per question. That is
|
|
19
|
+
* what lets the composer stay on screen while a task streams: the line typed
|
|
20
|
+
* during a run is delivered to the REPL (or to a pending approval), never lost
|
|
21
|
+
* and never turned into a second prompt for the model.
|
|
22
|
+
*/
|
|
23
|
+
export class PromptSession {
|
|
24
|
+
constructor({ input = stdin, output = stdout, onSigint = null, mouse = true } = {}) {
|
|
25
|
+
this.input = input;
|
|
26
|
+
this.output = output;
|
|
27
|
+
this.onSigint = onSigint;
|
|
28
|
+
this.terminal = Boolean(input.isTTY && output.isTTY);
|
|
29
|
+
// Mouse reporting is only useful (and only harmless) when the renderer can
|
|
30
|
+
// draw the clickable blocks. It swallows the wheel on the main screen, so
|
|
31
|
+
// plain/no-colour runs keep the terminal's native scrolling.
|
|
32
|
+
this.mouse = Boolean(mouse && this.terminal);
|
|
33
|
+
this.registry = new PasteRegistry();
|
|
34
|
+
this.questionText = '';
|
|
35
|
+
// True from the first question until close: the composer is on screen, so
|
|
36
|
+
// the renderer can park and redraw it around streaming output.
|
|
37
|
+
this.pending = false;
|
|
38
|
+
// One rl.question at a time, always aimed at the current prompt text.
|
|
39
|
+
this.reading = false;
|
|
40
|
+
this.drawnQuestion = null;
|
|
41
|
+
// Lines for the REPL, modal questions (approvals) and lines typed while
|
|
42
|
+
// nobody was waiting yet.
|
|
43
|
+
this.consumers = [];
|
|
44
|
+
this.claimers = [];
|
|
45
|
+
this.lineQueue = [];
|
|
46
|
+
this.closed = false;
|
|
47
|
+
this.composer = null;
|
|
48
|
+
this.rl = null;
|
|
49
|
+
|
|
50
|
+
// Hooks wired by the app: mouse/panel control and panel state.
|
|
51
|
+
this.onMouse = null;
|
|
52
|
+
this.onOverlayKey = null;
|
|
53
|
+
this.isOverlayOpen = () => false;
|
|
54
|
+
this.onBeforeSubmit = null;
|
|
55
|
+
|
|
56
|
+
this.exitHandler = () => this.#writeModes(false);
|
|
57
|
+
if (this.terminal) {
|
|
58
|
+
process.once('exit', this.exitHandler);
|
|
59
|
+
this.#writeModes(true);
|
|
60
|
+
}
|
|
61
|
+
this.#create();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
#writeModes(enabled) {
|
|
65
|
+
if (!this.terminal || !this.output) return;
|
|
66
|
+
const mouse = this.mouse ? (enabled ? MOUSE_MODE_ON : MOUSE_MODE_OFF) : '';
|
|
67
|
+
this.output.write(enabled ? `${PASTE_MODE_ON}${mouse}` : `${mouse}${PASTE_MODE_OFF}`);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
#create() {
|
|
71
|
+
this.composer?.dispose();
|
|
72
|
+
this.composer = null;
|
|
73
|
+
this.reading = false;
|
|
74
|
+
this.drawnQuestion = null;
|
|
75
|
+
|
|
76
|
+
if (this.terminal) {
|
|
77
|
+
this.composer = new ComposerInput({ input: this.input, registry: this.registry });
|
|
78
|
+
this.composer.overlayOpen = () => Boolean(this.isOverlayOpen?.());
|
|
79
|
+
this.composer.composerEmpty = () => !this.rl?.line;
|
|
80
|
+
this.composer.mouseHandler = (event) => this.onMouse?.(event);
|
|
81
|
+
this.composer.overlayKeyHandler = (key) => this.onOverlayKey?.(key);
|
|
82
|
+
this.composer.beforeSubmit = () => this.onBeforeSubmit?.();
|
|
83
|
+
this.composer.composing = this.pending;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const input = this.composer?.proxy || this.input;
|
|
87
|
+
this.rl = readline.createInterface({
|
|
88
|
+
input,
|
|
89
|
+
output: this.output,
|
|
90
|
+
terminal: this.terminal,
|
|
91
|
+
});
|
|
92
|
+
if (this.onSigint) {
|
|
93
|
+
this.rl.on('SIGINT', () => this.onSigint());
|
|
94
|
+
}
|
|
95
|
+
this.#ensureReading();
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Keep exactly one read in flight for as long as the composer is wanted. */
|
|
99
|
+
#ensureReading() {
|
|
100
|
+
if (!this.rl || this.closed || this.reading || !this.pending) return;
|
|
101
|
+
const rl = this.rl;
|
|
102
|
+
this.reading = true;
|
|
103
|
+
this.drawnQuestion = this.questionText;
|
|
104
|
+
rl.question(this.questionText).then(
|
|
105
|
+
(line) => {
|
|
106
|
+
if (rl !== this.rl) return; // the interface was reset meanwhile
|
|
107
|
+
this.reading = false;
|
|
108
|
+
this.#dispatch(line);
|
|
109
|
+
this.#ensureReading();
|
|
110
|
+
},
|
|
111
|
+
() => {
|
|
112
|
+
if (rl !== this.rl) return;
|
|
113
|
+
this.reading = false;
|
|
114
|
+
this.#ensureReading();
|
|
115
|
+
},
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Route a submitted line. An approval (or any modal question) owns the next
|
|
121
|
+
* line; otherwise the REPL gets it, or it waits in the queue when the REPL is
|
|
122
|
+
* busy running a command.
|
|
123
|
+
*/
|
|
124
|
+
#dispatch(line) {
|
|
125
|
+
const waiter = this.claimers.shift() || this.consumers.shift();
|
|
126
|
+
if (waiter) waiter(line);
|
|
127
|
+
else this.lineQueue.push(line);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
#drainQueue() {
|
|
131
|
+
while (this.consumers.length > 0 && this.lineQueue.length > 0) {
|
|
132
|
+
this.consumers.shift()(this.lineQueue.shift());
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
#claimLine() {
|
|
137
|
+
return new Promise((resolve) => {
|
|
138
|
+
this.claimers.push(resolve);
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
#settleClaims(answer) {
|
|
143
|
+
for (const resolve of this.claimers.splice(0)) resolve(answer);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** Show `questionText`, redrawing the prompt when the text changed. */
|
|
147
|
+
#syncPrompt() {
|
|
148
|
+
if (!this.rl || this.drawnQuestion === this.questionText) return;
|
|
149
|
+
this.drawnQuestion = this.questionText;
|
|
150
|
+
this.rl.setPrompt(this.questionText);
|
|
151
|
+
if (this.reading) this.rl.prompt(true);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** Ask a question; returns null when the interface is closed/cancelled. */
|
|
155
|
+
async ask(question) {
|
|
156
|
+
this.questionText = String(question ?? '');
|
|
157
|
+
this.pending = true;
|
|
158
|
+
if (this.composer) this.composer.composing = true;
|
|
159
|
+
this.#ensureReading();
|
|
160
|
+
this.#syncPrompt();
|
|
161
|
+
const answer = await new Promise((resolve) => {
|
|
162
|
+
this.consumers.push(resolve);
|
|
163
|
+
this.#drainQueue();
|
|
164
|
+
});
|
|
165
|
+
if (answer === null || answer === undefined) return null;
|
|
166
|
+
return this.registry.resolve(answer);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* A modal question (approval). It takes the next submitted line, even while
|
|
171
|
+
* the REPL is between asks, and never starts a second readline read.
|
|
172
|
+
*/
|
|
173
|
+
async confirm(question) {
|
|
174
|
+
const previous = this.questionText;
|
|
175
|
+
this.questionText = String(question ?? '');
|
|
176
|
+
this.pending = true;
|
|
177
|
+
this.#ensureReading();
|
|
178
|
+
this.#syncPrompt();
|
|
179
|
+
const answer = await this.#claimLine();
|
|
180
|
+
this.questionText = previous;
|
|
181
|
+
this.drawnQuestion = null;
|
|
182
|
+
this.#syncPrompt();
|
|
183
|
+
if (answer === null || answer === undefined) return { answer: 'cancel', value: false };
|
|
184
|
+
const text = String(answer).trim().toLowerCase();
|
|
185
|
+
if (text === 'y' || text === 'yes') return { answer: text, value: true };
|
|
186
|
+
if (text === 'a') return { answer: text, value: true, all: true };
|
|
187
|
+
return { answer: text, value: false };
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Rows and cursor row of the prompt currently on screen. The block panel is
|
|
192
|
+
* drawn above it and transcript output is written in its place, so both need
|
|
193
|
+
* to know exactly how far the prompt reaches.
|
|
194
|
+
*/
|
|
195
|
+
metrics() {
|
|
196
|
+
if (!this.terminal || !this.pending || !this.rl || !this.reading) {
|
|
197
|
+
return { active: false, rows: 0, cursorRow: 0 };
|
|
198
|
+
}
|
|
199
|
+
const width = Math.max(20, Number(this.output.columns) || 80);
|
|
200
|
+
const line = this.rl.line || '';
|
|
201
|
+
const { total, cursor } = composerWidth(this.questionText, line, this.rl.cursor);
|
|
202
|
+
const rows = Math.max(1, Math.ceil(total / width));
|
|
203
|
+
const cursorRow = Math.min(rows - 1, Math.floor(cursor / width));
|
|
204
|
+
return { active: true, rows, cursorRow };
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/** Redraw the prompt in place; readline clears everything below it first. */
|
|
208
|
+
repaint() {
|
|
209
|
+
if (!this.terminal || !this.pending || !this.rl || !this.reading) return;
|
|
210
|
+
this.rl.prompt(true);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* The renderer moved the cursor to the prompt's first row itself, so readline
|
|
215
|
+
* must not move up again from its old cursor position.
|
|
216
|
+
*/
|
|
217
|
+
resetCursor() {
|
|
218
|
+
if (this.rl) this.rl.prevRows = 0;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
reset() {
|
|
222
|
+
this.#settleClaims('');
|
|
223
|
+
this.reading = false;
|
|
224
|
+
this.drawnQuestion = null;
|
|
225
|
+
try {
|
|
226
|
+
this.rl?.close();
|
|
227
|
+
} catch {
|
|
228
|
+
// Ignore: the interface may already be closed.
|
|
229
|
+
}
|
|
230
|
+
this.#create();
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
close() {
|
|
234
|
+
if (this.closed) return;
|
|
235
|
+
this.closed = true;
|
|
236
|
+
this.#writeModes(false);
|
|
237
|
+
if (this.exitHandler) process.off('exit', this.exitHandler);
|
|
238
|
+
// Nothing may stay blocked on a line once the session is over.
|
|
239
|
+
this.#settleClaims(null);
|
|
240
|
+
for (const resolve of this.consumers.splice(0)) resolve(null);
|
|
241
|
+
this.lineQueue.length = 0;
|
|
242
|
+
try {
|
|
243
|
+
this.rl?.close();
|
|
244
|
+
} catch {
|
|
245
|
+
// Ignore double close.
|
|
246
|
+
}
|
|
247
|
+
this.rl = null;
|
|
248
|
+
this.reading = false;
|
|
249
|
+
this.composer?.dispose();
|
|
250
|
+
this.composer = null;
|
|
251
|
+
// Release stdin so the event loop can drain: `/exit` must return to the
|
|
252
|
+
// shell instead of leaving the process waiting for one more key.
|
|
253
|
+
this.input?.pause?.();
|
|
254
|
+
}
|
|
255
|
+
}
|