@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,280 @@
1
+ import process from 'node:process';
2
+ import readline from 'node:readline/promises';
3
+ import {
4
+ describeSetting,
5
+ findSetting,
6
+ formatSettingValue,
7
+ validateSetting,
8
+ } from '../config/settings-registry.js';
9
+ import { setSetting } from '../config/settings.js';
10
+ import { redact } from '../utils/redaction.js';
11
+ import { charWidth, displayWidth } from './width.js';
12
+
13
+ /**
14
+ * `ryuu --settings`: the line-oriented settings editor.
15
+ *
16
+ * Every successfully validated change is written immediately (read-modify-write
17
+ * through the store), so an interrupted terminal never loses a saved value. A
18
+ * failed write leaves the in-memory copy untouched and says so, which means the
19
+ * editor's view never pretends a change was persisted.
20
+ */
21
+
22
+ const HELP_LINES = [
23
+ 'Settings commands:',
24
+ ' <number> <value> save a new value for that setting',
25
+ ' /list redraw the numbered settings list',
26
+ ' /show <number> show one setting, its description and constraints',
27
+ ' /reset <number> reset one setting to its default (asks first)',
28
+ ' /help show this help',
29
+ ' /exit leave the editor (changes are already saved)',
30
+ 'A value may contain spaces; everything after the number is kept.',
31
+ ];
32
+
33
+ function truncate(text, width) {
34
+ const value = String(text ?? '');
35
+ if (displayWidth(value) <= width) return value;
36
+ let output = '';
37
+ let used = 0;
38
+ for (const char of value) {
39
+ const size = charWidth(char.codePointAt(0));
40
+ if (used + size > width - 1) break;
41
+ output += char;
42
+ used += size;
43
+ }
44
+ return `${output}…`;
45
+ }
46
+
47
+ export class SettingsSession {
48
+ constructor({ store, makeRegistry, data, file = store?.file || '', title = '◇ Ryuu Settings' }) {
49
+ if (!store) throw new Error('SettingsSession needs a store.');
50
+ if (typeof makeRegistry !== 'function') throw new Error('SettingsSession needs a makeRegistry function.');
51
+ this.store = store;
52
+ this.makeRegistry = makeRegistry;
53
+ this.data = data || { version: 1 };
54
+ this.file = file;
55
+ this.title = title;
56
+ this.entries = makeRegistry(this.data);
57
+ }
58
+
59
+ #refresh() {
60
+ this.entries = this.makeRegistry(this.data);
61
+ }
62
+
63
+ listLines() {
64
+ const width = Math.min(
65
+ 34,
66
+ Math.max(12, ...this.entries.map((entry) => displayWidth(entry.label))),
67
+ );
68
+ const lines = [
69
+ this.title,
70
+ `file: ${this.file}`,
71
+ '',
72
+ ` ${'#'.padEnd(3)} ${'SETTING'.padEnd(width)} VALUE`,
73
+ ];
74
+ this.entries.forEach((entry, index) => {
75
+ const label = entry.label.padEnd(width);
76
+ lines.push(` ${String(index + 1).padEnd(3)} ${label} ${truncate(formatSettingValue(entry), 56)}`);
77
+ });
78
+ lines.push('');
79
+ lines.push('Enter a number and a new value. Format: <number> <value>');
80
+ lines.push('Type /help for commands, /exit to leave (each change is saved immediately).');
81
+ return lines;
82
+ }
83
+
84
+ describeLines(entry) {
85
+ const lines = [
86
+ `${entry.label} · ${entry.key}`,
87
+ ` value: ${formatSettingValue(entry)}`,
88
+ ` type: ${entry.type}${entry.secret ? ' (secret, always masked)' : ''}${
89
+ entry.required ? ' · required' : ''
90
+ }`,
91
+ ` ${entry.description}`,
92
+ ];
93
+ const constraints = describeSetting(entry);
94
+ if (constraints) lines.push(` ${constraints}`);
95
+ lines.push(' use: /reset ' + this.#indexOf(entry) + ' to restore the default');
96
+ return lines;
97
+ }
98
+
99
+ #indexOf(entry) {
100
+ return this.entries.indexOf(entry) + 1;
101
+ }
102
+
103
+ #lookup(reference, { allowKey = true } = {}) {
104
+ return findSetting(this.entries, reference, { allowKey });
105
+ }
106
+
107
+ /** Validate and persist one value; the in-memory copy only moves on success. */
108
+ async assign(reference, rawValue) {
109
+ const entry = this.#lookup(reference);
110
+ if (!entry) {
111
+ return { ok: false, message: `Unknown setting: ${reference}. Use /list to see the numbers.` };
112
+ }
113
+ const result = validateSetting(entry, rawValue);
114
+ if (!result.ok) return { ok: false, message: `${entry.label}: ${result.error}` };
115
+ if (!entry.editable) return { ok: false, message: `${entry.label} is not editable here.` };
116
+
117
+ const key = entry.key;
118
+ let saved;
119
+ try {
120
+ saved = await this.store.update((draft) => {
121
+ setSetting(draft, key, result.value);
122
+ return draft;
123
+ });
124
+ } catch (error) {
125
+ return {
126
+ ok: false,
127
+ message: `${entry.label} was NOT saved: ${redact(error.message)}`,
128
+ };
129
+ }
130
+ this.data = saved;
131
+ this.#refresh();
132
+ const refreshed = findSetting(this.entries, key, { allowKey: true });
133
+ const warning = result.warning ? ` — ${result.warning}` : '';
134
+ return { ok: true, message: `Saved ${entry.label} = ${formatSettingValue(refreshed)}${warning}` };
135
+ }
136
+
137
+ /** Reset one setting to its registry default after an explicit confirmation. */
138
+ async reset(reference, { confirm } = {}) {
139
+ const entry = this.#lookup(reference);
140
+ if (!entry) {
141
+ return { ok: false, message: `Unknown setting: ${reference}. Use /list to see the numbers.` };
142
+ }
143
+ const shown = entry.secret
144
+ ? '(empty)'
145
+ : Array.isArray(entry.defaultValue)
146
+ ? entry.defaultValue.join(', ')
147
+ : entry.defaultValue === null || entry.defaultValue === undefined || entry.defaultValue === ''
148
+ ? '(empty)'
149
+ : String(entry.defaultValue);
150
+ const approved = typeof confirm === 'function' ? await confirm(`Reset ${entry.label} to ${shown}? [y/N] `) : false;
151
+ if (!approved) return { ok: false, message: 'Reset cancelled; nothing changed.' };
152
+ const raw = Array.isArray(entry.defaultValue)
153
+ ? entry.defaultValue.join(', ')
154
+ : entry.defaultValue === null || entry.defaultValue === undefined
155
+ ? ''
156
+ : String(entry.defaultValue);
157
+ return this.assign(entry.key, raw);
158
+ }
159
+
160
+ /**
161
+ * Handle one editor line. Returns the lines to print and whether to exit.
162
+ * Invalid input is reported, never fatal.
163
+ */
164
+ async handleLine(line, { confirm = null } = {}) {
165
+ const text = String(line ?? '');
166
+ const trimmed = text.trim();
167
+ if (!trimmed) return { lines: ['Type a number and a value, or /help.'], exit: false };
168
+
169
+ if (trimmed.startsWith('/')) {
170
+ const [command, ...rest] = trimmed.split(/\s+/);
171
+ const argument = rest.join(' ');
172
+ switch (command.toLowerCase()) {
173
+ case '/help':
174
+ return { lines: HELP_LINES, exit: false };
175
+ case '/list':
176
+ return { lines: this.listLines(), exit: false };
177
+ case '/show': {
178
+ const entry = this.#lookup(argument);
179
+ if (!argument) return { lines: ['Usage: /show <number>'], exit: false };
180
+ if (!entry) return { lines: [`Unknown setting: ${argument}. Use /list to see the numbers.`], exit: false };
181
+ return { lines: this.describeLines(entry), exit: false };
182
+ }
183
+ case '/reset': {
184
+ if (!argument) return { lines: ['Usage: /reset <number>'], exit: false };
185
+ const result = await this.reset(argument, { confirm });
186
+ return { lines: [result.message], exit: false };
187
+ }
188
+ case '/exit':
189
+ case '/quit':
190
+ return {
191
+ lines: [`Settings saved to ${this.file}. Goodbye.`],
192
+ exit: true,
193
+ };
194
+ default:
195
+ return {
196
+ lines: [`Unknown command ${command}. Try /help, /list, /show <n>, /reset <n> or /exit.`],
197
+ exit: false,
198
+ };
199
+ }
200
+ }
201
+
202
+ const match = trimmed.match(/^(\d+)(?:\s+([\s\S]*))?$/);
203
+ if (!match) {
204
+ return {
205
+ lines: ['Format: <number> <value>. Type /help for commands, /list to see the numbers.'],
206
+ exit: false,
207
+ };
208
+ }
209
+ if (match[2] === undefined) {
210
+ return {
211
+ lines: ['Enter a value after the number; use /reset <number> to restore the default.'],
212
+ exit: false,
213
+ };
214
+ }
215
+ const result = await this.assign(match[1], match[2].trim());
216
+ return { lines: [result.message], exit: false };
217
+ }
218
+ }
219
+
220
+ /**
221
+ * Run the numbered editor on a real terminal. Changes are saved as they are
222
+ * entered, so Ctrl+C or EOF never discards a saved value; the terminal is
223
+ * released on every exit path.
224
+ */
225
+ export async function runSettingsEditor({
226
+ store,
227
+ makeRegistry,
228
+ input = process.stdin,
229
+ output = process.stdout,
230
+ data = null,
231
+ }) {
232
+ const loaded = data ? { data } : await store.read();
233
+ const session = new SettingsSession({
234
+ store,
235
+ makeRegistry,
236
+ data: loaded.data,
237
+ file: store.file,
238
+ });
239
+
240
+ const rl = readline.createInterface({ input, output, terminal: Boolean(input.isTTY && output.isTTY) });
241
+ let interrupted = false;
242
+ rl.on('SIGINT', () => {
243
+ interrupted = true;
244
+ rl.close();
245
+ });
246
+
247
+ const ask = async (question) => {
248
+ try {
249
+ return await rl.question(question);
250
+ } catch {
251
+ return null;
252
+ }
253
+ };
254
+
255
+ output.write(`${session.listLines().join('\n')}\n\n`);
256
+ try {
257
+ for (;;) {
258
+ const line = await ask('settings > ');
259
+ if (line === null) break;
260
+ const result = await session.handleLine(line, {
261
+ confirm: async (question) => {
262
+ const answer = await ask(question);
263
+ return ['y', 'yes'].includes(String(answer ?? '').trim().toLowerCase());
264
+ },
265
+ });
266
+ for (const row of result.lines) output.write(`${row}\n`);
267
+ output.write('\n');
268
+ if (result.exit) break;
269
+ }
270
+ } finally {
271
+ try {
272
+ rl.close();
273
+ } catch {
274
+ // Already closed by SIGINT.
275
+ }
276
+ input.pause?.();
277
+ }
278
+ if (interrupted) output.write('\nSettings closed; every saved change is already on disk.\n');
279
+ return session;
280
+ }