@ryuu-reinzz/ryuu-cli 0.0.0-stage → 0.4.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.
Files changed (63) hide show
  1. package/CHANGELOG.md +592 -0
  2. package/LICENSE +21 -0
  3. package/README.md +1269 -2
  4. package/package.json +45 -4
  5. package/src/agent/agent.js +1194 -0
  6. package/src/agent/approvals.js +76 -0
  7. package/src/agent/context.js +106 -0
  8. package/src/agent/planner.js +237 -0
  9. package/src/agent/worklog.js +318 -0
  10. package/src/cli/app.js +471 -0
  11. package/src/cli/commands.js +1155 -0
  12. package/src/cli/input.js +287 -0
  13. package/src/cli/markdown.js +527 -0
  14. package/src/cli/prompts.js +255 -0
  15. package/src/cli/renderer.js +2155 -0
  16. package/src/cli/settings-editor.js +280 -0
  17. package/src/cli/settings-tui.js +669 -0
  18. package/src/cli/tui.js +1737 -0
  19. package/src/cli/width.js +48 -0
  20. package/src/config/load.js +137 -0
  21. package/src/config/scaffold.js +50 -0
  22. package/src/config/settings-registry.js +495 -0
  23. package/src/config/settings.js +304 -0
  24. package/src/config/validate.js +461 -0
  25. package/src/index.js +449 -0
  26. package/src/providers/catalog.js +407 -0
  27. package/src/providers/ids.js +24 -0
  28. package/src/providers/index.js +103 -0
  29. package/src/providers/nvidia.js +671 -0
  30. package/src/providers/registry.js +104 -0
  31. package/src/providers/river.js +198 -0
  32. package/src/providers/tokenharbor.js +38 -0
  33. package/src/providers/vendor-extras.js +82 -0
  34. package/src/services/clipboard.js +38 -0
  35. package/src/services/diff.js +153 -0
  36. package/src/services/doctor.js +126 -0
  37. package/src/services/logger.js +190 -0
  38. package/src/services/preferences.js +249 -0
  39. package/src/services/processes.js +345 -0
  40. package/src/services/provider-settings.js +226 -0
  41. package/src/services/sessions.js +279 -0
  42. package/src/services/undo.js +133 -0
  43. package/src/services/ushell.js +146 -0
  44. package/src/services/websearch/manager.js +616 -0
  45. package/src/services/websearch/mcp.js +233 -0
  46. package/src/services/websearch/normalize.js +284 -0
  47. package/src/services/websearch/providers.js +178 -0
  48. package/src/services/websearch/serper.js +263 -0
  49. package/src/services/workspace.js +589 -0
  50. package/src/tools/filesystem.js +304 -0
  51. package/src/tools/git.js +114 -0
  52. package/src/tools/index.js +48 -0
  53. package/src/tools/project.js +160 -0
  54. package/src/tools/registry.js +118 -0
  55. package/src/tools/search.js +210 -0
  56. package/src/tools/shell.js +279 -0
  57. package/src/tools/url.js +542 -0
  58. package/src/tools/web.js +67 -0
  59. package/src/utils/errors.js +160 -0
  60. package/src/utils/network.js +129 -0
  61. package/src/utils/redaction.js +73 -0
  62. package/src/utils/validation.js +370 -0
  63. package/src/utils/version.js +24 -0
@@ -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
+ }