@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
package/src/cli/input.js
ADDED
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
import { StringDecoder } from 'node:string_decoder';
|
|
2
|
+
import { PassThrough } from 'node:stream';
|
|
3
|
+
import { displayWidth } from './width.js';
|
|
4
|
+
|
|
5
|
+
/** Pastes at least this long are collapsed to a placeholder in the composer. */
|
|
6
|
+
export const PASTE_THRESHOLD = 1000;
|
|
7
|
+
export const PASTE_START = '\x1b[200~';
|
|
8
|
+
export const PASTE_END = '\x1b[201~';
|
|
9
|
+
|
|
10
|
+
const MOUSE_SEQUENCE = /^\x1b\[<(\d+);(\d+);(\d+)([Mm])/;
|
|
11
|
+
const PLACEHOLDER_RE = /> \[Pasted text #(\d+) [^\]]*\]/g;
|
|
12
|
+
|
|
13
|
+
/** The visible stand-in for a long paste, e.g. `> [Pasted text #1 3196 chars]`. */
|
|
14
|
+
export function formatPastePlaceholder(id, chars) {
|
|
15
|
+
return `> [Pasted text #${id} ${chars} chars]`;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Session-wide store for pasted text. The composer only shows a placeholder;
|
|
20
|
+
* the original text is kept here and restored when the line is submitted
|
|
21
|
+
* (including lines recalled from readline history and sent again).
|
|
22
|
+
*
|
|
23
|
+
* Ids are monotonic for the whole session, so editing or deleting one
|
|
24
|
+
* placeholder can never renumber another one.
|
|
25
|
+
*/
|
|
26
|
+
export class PasteRegistry {
|
|
27
|
+
constructor() {
|
|
28
|
+
this.entries = new Map();
|
|
29
|
+
this.nextId = 1;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
add(text) {
|
|
33
|
+
const value = String(text ?? '');
|
|
34
|
+
const id = this.nextId++;
|
|
35
|
+
const chars = [...value].length;
|
|
36
|
+
const entry = { id, text: value, chars };
|
|
37
|
+
this.entries.set(id, entry);
|
|
38
|
+
return { ...entry, placeholder: formatPastePlaceholder(id, chars) };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
get(id) {
|
|
42
|
+
return this.entries.get(Number(id)) || null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Restore every placeholder still present in the line; unknown ids stay as typed. */
|
|
46
|
+
resolve(line) {
|
|
47
|
+
return String(line ?? '').replace(PLACEHOLDER_RE, (match, rawId) => {
|
|
48
|
+
const entry = this.entries.get(Number(rawId));
|
|
49
|
+
return entry ? entry.text : match;
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const KEY_SEQUENCES = new Map([
|
|
55
|
+
['\x1b[A', 'up'],
|
|
56
|
+
['\x1b[B', 'down'],
|
|
57
|
+
['\x1b[C', 'right'],
|
|
58
|
+
['\x1b[D', 'left'],
|
|
59
|
+
['\x1b[H', 'home'],
|
|
60
|
+
['\x1b[F', 'end'],
|
|
61
|
+
['\x1b[1~', 'home'],
|
|
62
|
+
['\x1b[4~', 'end'],
|
|
63
|
+
['\x1b[5~', 'pageup'],
|
|
64
|
+
['\x1b[6~', 'pagedown'],
|
|
65
|
+
['\x1b', 'escape'],
|
|
66
|
+
]);
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Sits between the real stdin and readline. It is the only place that can
|
|
70
|
+
* swallow input before readline turns it into text, so it owns:
|
|
71
|
+
*
|
|
72
|
+
* - bracketed paste detection (a 3 000-character paste never echoes into
|
|
73
|
+
* the composer; it becomes a placeholder),
|
|
74
|
+
* - SGR mouse reporting (clicks are routed to the renderer, never typed),
|
|
75
|
+
* - panel keys (arrows/wheel scroll the open block panel instead of moving
|
|
76
|
+
* the readline cursor).
|
|
77
|
+
*/
|
|
78
|
+
export class ComposerInput {
|
|
79
|
+
constructor({ input, registry = new PasteRegistry(), threshold = PASTE_THRESHOLD } = {}) {
|
|
80
|
+
this.input = input;
|
|
81
|
+
this.registry = registry;
|
|
82
|
+
this.threshold = threshold;
|
|
83
|
+
this.buffer = '';
|
|
84
|
+
this.paste = null;
|
|
85
|
+
this.composing = false;
|
|
86
|
+
this.decoder = new StringDecoder('utf8');
|
|
87
|
+
this.escTimer = null;
|
|
88
|
+
this.mouseHandler = null;
|
|
89
|
+
this.overlayKeyHandler = null;
|
|
90
|
+
this.overlayOpen = () => false;
|
|
91
|
+
this.composerEmpty = () => true;
|
|
92
|
+
this.beforeSubmit = null;
|
|
93
|
+
|
|
94
|
+
// readline reads from this proxy; stdin is filtered below before it gets here.
|
|
95
|
+
this.proxy = new PassThrough();
|
|
96
|
+
this.proxy.isTTY = Boolean(input?.isTTY);
|
|
97
|
+
this.proxy.setRawMode = (mode) => input?.setRawMode?.(mode);
|
|
98
|
+
this.proxy.columns = input?.columns;
|
|
99
|
+
this.proxy.rows = input?.rows;
|
|
100
|
+
|
|
101
|
+
this.onData = (chunk) => this.push(chunk);
|
|
102
|
+
this.onEnd = () => {
|
|
103
|
+
const rest = this.decoder.end();
|
|
104
|
+
if (rest) this.push(rest);
|
|
105
|
+
this.proxy.end();
|
|
106
|
+
};
|
|
107
|
+
input?.on('data', this.onData);
|
|
108
|
+
input?.on('end', this.onEnd);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
dispose() {
|
|
112
|
+
this.input?.off('data', this.onData);
|
|
113
|
+
this.input?.off('end', this.onEnd);
|
|
114
|
+
this.reset();
|
|
115
|
+
this.proxy.end();
|
|
116
|
+
// Attaching a 'data' listener put stdin into flowing mode, and removing it
|
|
117
|
+
// does not undo that. Without pausing here the stream keeps the event loop
|
|
118
|
+
// alive, so `/exit` looked like a hang on a blank screen until Ctrl+C.
|
|
119
|
+
// The next composer re-attaches its listener and resumes it again.
|
|
120
|
+
this.input?.pause?.();
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
reset() {
|
|
124
|
+
this.buffer = '';
|
|
125
|
+
this.paste = null;
|
|
126
|
+
if (this.escTimer) clearTimeout(this.escTimer);
|
|
127
|
+
this.escTimer = null;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
push(chunk) {
|
|
131
|
+
if (typeof chunk === 'string') this.buffer += chunk;
|
|
132
|
+
else this.buffer += this.decoder.write(chunk);
|
|
133
|
+
this.#drain();
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
#forward(text) {
|
|
137
|
+
if (!text) return;
|
|
138
|
+
// Typing while a block panel is open dismisses it: the panel is a modal
|
|
139
|
+
// inspector, and leaving it up while readline redraws a wrapped prompt
|
|
140
|
+
// could make the cursor walk into the panel rows.
|
|
141
|
+
if (this.overlayOpen() && /[^\x00-\x1f]/.test(text)) this.beforeSubmit?.();
|
|
142
|
+
// Terminals without bracketed-paste support hand a paste over as one
|
|
143
|
+
// plain chunk. Treat a very large pure-text chunk as a paste as well, so
|
|
144
|
+
// the composer never has to echo thousands of characters.
|
|
145
|
+
if (this.composing && !text.includes('\x1b') && [...text].length >= this.threshold) {
|
|
146
|
+
const entry = this.registry.add(text);
|
|
147
|
+
this.proxy.write(entry.placeholder);
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
if (this.overlayOpen() && /[\r\n\x03]/.test(text)) this.beforeSubmit?.();
|
|
151
|
+
this.proxy.write(text);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
#drain() {
|
|
155
|
+
while (this.buffer.length > 0) {
|
|
156
|
+
if (this.paste !== null) {
|
|
157
|
+
// Accumulate the payload so an end marker split across stdin chunks
|
|
158
|
+
// (or a paste larger than the chunk size) is still found.
|
|
159
|
+
this.paste += this.buffer;
|
|
160
|
+
this.buffer = '';
|
|
161
|
+
const end = this.paste.indexOf(PASTE_END);
|
|
162
|
+
if (end === -1) return;
|
|
163
|
+
const text = this.paste.slice(0, end);
|
|
164
|
+
this.buffer = this.paste.slice(end + PASTE_END.length);
|
|
165
|
+
this.paste = null;
|
|
166
|
+
this.#handlePaste(text);
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const escAt = this.buffer.indexOf('\x1b');
|
|
171
|
+
if (escAt === -1) {
|
|
172
|
+
const text = this.buffer;
|
|
173
|
+
this.buffer = '';
|
|
174
|
+
this.#forward(text);
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
if (escAt > 0) {
|
|
178
|
+
const text = this.buffer.slice(0, escAt);
|
|
179
|
+
this.buffer = this.buffer.slice(escAt);
|
|
180
|
+
this.#forward(text);
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (this.buffer.startsWith(PASTE_START)) {
|
|
185
|
+
this.buffer = this.buffer.slice(PASTE_START.length);
|
|
186
|
+
this.paste = '';
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const mouse = MOUSE_SEQUENCE.exec(this.buffer);
|
|
191
|
+
if (mouse) {
|
|
192
|
+
this.buffer = this.buffer.slice(mouse[0].length);
|
|
193
|
+
this.#handleMouse(mouse);
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const sequence = this.#readEscape();
|
|
198
|
+
if (!sequence) {
|
|
199
|
+
this.#armEscapeTimeout();
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
this.buffer = this.buffer.slice(sequence.length);
|
|
203
|
+
this.#handleEscape(sequence);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/** A complete CSI/SS3/Alt sequence, or null while it is still incomplete. */
|
|
208
|
+
#readEscape() {
|
|
209
|
+
if (this.buffer.length === 1) return null; // could be a lone Esc
|
|
210
|
+
if (this.buffer[1] === '[') {
|
|
211
|
+
for (let index = 2; index < this.buffer.length; index += 1) {
|
|
212
|
+
const code = this.buffer.charCodeAt(index);
|
|
213
|
+
if (code >= 0x40 && code <= 0x7e) return this.buffer.slice(0, index + 1);
|
|
214
|
+
}
|
|
215
|
+
return null;
|
|
216
|
+
}
|
|
217
|
+
if (this.buffer[1] === 'O') {
|
|
218
|
+
return this.buffer.length < 3 ? null : this.buffer.slice(0, 3);
|
|
219
|
+
}
|
|
220
|
+
return this.buffer.slice(0, 2); // Alt+key
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
#armEscapeTimeout() {
|
|
224
|
+
if (this.escTimer) return;
|
|
225
|
+
this.escTimer = setTimeout(() => {
|
|
226
|
+
this.escTimer = null;
|
|
227
|
+
if (!this.buffer.startsWith('\x1b')) return;
|
|
228
|
+
if (this.buffer.length === 1) {
|
|
229
|
+
this.buffer = '';
|
|
230
|
+
this.#handleEscape('\x1b');
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
// A chunk ended mid-sequence; hand it to readline unchanged rather than
|
|
234
|
+
// swallowing the user's input.
|
|
235
|
+
const rest = this.buffer;
|
|
236
|
+
this.buffer = '';
|
|
237
|
+
this.#forward(rest);
|
|
238
|
+
}, 40);
|
|
239
|
+
this.escTimer.unref?.();
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
#handleEscape(sequence) {
|
|
243
|
+
const key = KEY_SEQUENCES.get(sequence);
|
|
244
|
+
if (this.overlayOpen() && key) {
|
|
245
|
+
if (key === 'escape') {
|
|
246
|
+
this.overlayKeyHandler?.('close');
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
const navigation = key === 'up' || key === 'down' || key === 'home' || key === 'end';
|
|
250
|
+
const paging = key === 'pageup' || key === 'pagedown';
|
|
251
|
+
if (paging || (navigation && this.composerEmpty())) {
|
|
252
|
+
this.overlayKeyHandler?.(key);
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
this.#forward(sequence);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
#handleMouse(match) {
|
|
260
|
+
const rawButton = Number(match[1]);
|
|
261
|
+
const x = Number(match[2]);
|
|
262
|
+
const y = Number(match[3]);
|
|
263
|
+
const release = match[4] === 'm';
|
|
264
|
+
const wheel = (rawButton & 64) === 64 ? ((rawButton & 1) === 1 ? 'down' : 'up') : null;
|
|
265
|
+
this.mouseHandler?.({ button: rawButton & 3, wheel, x, y, release });
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
#handlePaste(text) {
|
|
269
|
+
if (!this.composing) return; // no composer on screen: never replay it later
|
|
270
|
+
if ([...text].length >= this.threshold) {
|
|
271
|
+
const entry = this.registry.add(text);
|
|
272
|
+
this.proxy.write(entry.placeholder);
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
this.#forward(text);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/** Columns of the visible composer line (ANSI stripped). */
|
|
280
|
+
export function composerWidth(prefix, line, cursor = null) {
|
|
281
|
+
const prefixWidth = displayWidth(prefix, { ansi: true });
|
|
282
|
+
const lineWidth = displayWidth(line);
|
|
283
|
+
return {
|
|
284
|
+
total: prefixWidth + lineWidth,
|
|
285
|
+
cursor: prefixWidth + (cursor === null ? lineWidth : displayWidth(String(line).slice(0, cursor))),
|
|
286
|
+
};
|
|
287
|
+
}
|