@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,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
+ }