@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,48 @@
1
+ /** ANSI control sequences (colours, cursor moves) that take no columns. */
2
+ export const ANSI_SEQUENCE = /\x1b\[[0-?]*[ -/]*[@-~]/g;
3
+
4
+ const ZERO_WIDTH_RE = /[\u0300-\u036f\u0483-\u0489\u200b-\u200f\u202a-\u202e\u2060-\u2064\ufeff]/;
5
+
6
+ /** Code point ranges that occupy two terminal columns (CJK, kana, emoji…). */
7
+ const WIDE_RANGES = [
8
+ [0x1100, 0x115f],
9
+ [0x2e80, 0x303e],
10
+ [0x3041, 0x33ff],
11
+ [0x3400, 0x4dbf],
12
+ [0x4e00, 0x9fff],
13
+ [0xa000, 0xa4cf],
14
+ [0xa960, 0xa97f],
15
+ [0xac00, 0xd7a3],
16
+ [0xf900, 0xfaff],
17
+ [0xfe10, 0xfe19],
18
+ [0xfe30, 0xfe6f],
19
+ [0xff00, 0xff60],
20
+ [0xffe0, 0xffe6],
21
+ [0x1f300, 0x1f64f],
22
+ [0x1f680, 0x1f6ff],
23
+ [0x1f900, 0x1f9ff],
24
+ [0x1fa70, 0x1faff],
25
+ [0x20000, 0x3fffd],
26
+ ];
27
+
28
+ /** Terminal columns for one code point (0 for marks, 2 for wide glyphs). */
29
+ export function charWidth(code) {
30
+ if (!code) return 0;
31
+ if (ZERO_WIDTH_RE.test(String.fromCodePoint(code))) return 0;
32
+ for (const [start, end] of WIDE_RANGES) {
33
+ if (code >= start && code <= end) return 2;
34
+ }
35
+ return 1;
36
+ }
37
+
38
+ /**
39
+ * Printable columns of a string. The box border math depends on this: counting
40
+ * characters instead made CJK reasoning lines wrap, so the cursor-up erase
41
+ * walked the wrong number of rows and the screen scrolled itself apart.
42
+ */
43
+ export function displayWidth(text, { ansi = false } = {}) {
44
+ const value = ansi ? String(text ?? '').replace(ANSI_SEQUENCE, '') : String(text ?? '');
45
+ let width = 0;
46
+ for (const char of value) width += charWidth(char.codePointAt(0));
47
+ return width;
48
+ }
@@ -0,0 +1,137 @@
1
+ import { ConfigError } from '../utils/errors.js';
2
+ import { getSetting } from './settings.js';
3
+
4
+ // The provider/model catalog lives with the providers; re-exported here so the
5
+ // existing import sites (and the documented defaults) keep one location.
6
+ export { DEFAULT_BASE_URL, DEFAULT_MODEL, MODEL_CATALOG } from '../providers/catalog.js';
7
+
8
+ /**
9
+ * config.json `agent` keys mapped to their raw-config names. The names are the
10
+ * same on purpose: one vocabulary for the file, the settings editor and the
11
+ * validated config.
12
+ */
13
+ export const AGENT_SETTING_KEYS = [
14
+ 'maxSteps',
15
+ 'maxOutputTokens',
16
+ 'requestTimeoutMs',
17
+ 'streamIdleTimeoutMs',
18
+ 'maxRetries',
19
+ 'maxRecoveryAttempts',
20
+ 'recoveryBackoffMs',
21
+ 'maxFileBytes',
22
+ 'maxSearchResults',
23
+ 'maxToolOutputBytes',
24
+ 'maxTaskDurationMs',
25
+ 'maxContextChars',
26
+ 'approvalPolicy',
27
+ 'logLevel',
28
+ 'allowShell',
29
+ 'debug',
30
+ 'historyDir',
31
+ 'logDir',
32
+ ];
33
+
34
+ /**
35
+ * config.json `webSearch` keys mapped to the raw-config key they fill.
36
+ *
37
+ * `duckduckgoEnabled`, `duckduckgoCommand` and `duckduckgoArgs` are stored
38
+ * flat under `webSearch` for a simpler editor row each.
39
+ */
40
+ export const WEB_SETTING_KEYS = {
41
+ serperApiKey: 'webSearchApiKey',
42
+ youApiKey: 'youApiKey',
43
+ firecrawlApiKey: 'firecrawlApiKey',
44
+ tavilyApiKey: 'tavilyApiKey',
45
+ results: 'webSearchResults',
46
+ timeoutMs: 'webSearchTimeoutMs',
47
+ providers: 'webSearchProviders',
48
+ enabled: 'webSearchEnabled',
49
+ fallback: 'webSearchFallback',
50
+ disabled: 'webSearchDisabled',
51
+ endpoint: 'webSearchEndpoint',
52
+ authHeader: 'webSearchAuthHeader',
53
+ mcpTimeoutMs: 'webMcpTimeoutMs',
54
+ mcpMaxRetries: 'webMcpMaxRetries',
55
+ debug: 'webMcpDebug',
56
+ duckduckgoEnabled: 'duckduckgoMcpEnabled',
57
+ duckduckgoCommand: 'duckduckgoMcpCommand',
58
+ duckduckgoArgs: 'duckduckgoMcpArgs',
59
+ };
60
+
61
+ /** A list setting is stored as an array but validated as a comma list. */
62
+ function rawListValue(value) {
63
+ if (!Array.isArray(value)) return value;
64
+ return value.length > 0 ? value.join(',') : 'none';
65
+ }
66
+
67
+ /**
68
+ * Build a raw, unvalidated config object from the user's config.json contents.
69
+ *
70
+ * This is the only configuration source: no `.env`, no environment variables.
71
+ * CLI flags are applied on top by the entry point, and the workspace's
72
+ * `.ryuu/preferences.json` supplies the last `/provider` and `/model` choices.
73
+ */
74
+ export function buildRawConfig(settings = {}, { cwd = process.cwd() } = {}) {
75
+ const data = settings && typeof settings === 'object' && !Array.isArray(settings) ? settings : {};
76
+
77
+ // One credentials entry per provider: the key, the base URL override and the
78
+ // model chosen in `providerModel`. Providers without stored values still get
79
+ // their catalog defaults from `buildProviders`.
80
+ const credentials = {};
81
+ const storedProviders = data.providers && typeof data.providers === 'object' ? data.providers : {};
82
+ for (const [id, stored] of Object.entries(storedProviders)) {
83
+ if (!stored || typeof stored !== 'object' || Array.isArray(stored)) continue;
84
+ credentials[id] = {
85
+ apiKey: typeof stored.apiKey === 'string' ? stored.apiKey : '',
86
+ baseURL: typeof stored.baseURL === 'string' ? stored.baseURL : '',
87
+ model: '',
88
+ };
89
+ // Per-provider request overrides (River's `reasoningEffort`, a
90
+ // pass-through `extraBody`, ...) are stored next to the key.
91
+ for (const field of ['reasoningEffort', 'thinking', 'extraBody', 'requestParams']) {
92
+ if (stored[field] !== undefined) credentials[id][field] = stored[field];
93
+ }
94
+ }
95
+ const storedModels = data.providerModel && typeof data.providerModel === 'object' ? data.providerModel : {};
96
+ for (const [id, model] of Object.entries(storedModels)) {
97
+ if (typeof model !== 'string') continue;
98
+ if (!credentials[id]) credentials[id] = { apiKey: '', baseURL: '', model: '' };
99
+ credentials[id].model = model;
100
+ }
101
+
102
+ const raw = {
103
+ provider: typeof data.provider === 'string' ? data.provider : null,
104
+ model: null,
105
+ providerCredentials: credentials,
106
+ extraProviders: data.extraProviders ?? null,
107
+ workspace: '.',
108
+ cwd,
109
+ };
110
+ for (const key of AGENT_SETTING_KEYS) {
111
+ raw[key] = getSetting(data, `agent.${key}`);
112
+ }
113
+ for (const [settingsKey, rawKey] of Object.entries(WEB_SETTING_KEYS)) {
114
+ const value = getSetting(data, `webSearch.${settingsKey}`);
115
+ raw[rawKey] = rawListValue(value);
116
+ }
117
+ // The selected model comes from the same place the editor writes it.
118
+ const activeProvider = typeof raw.provider === 'string' ? raw.provider.trim().toLowerCase() : '';
119
+ if (activeProvider) raw.model = getSetting(data, `providerModel.${activeProvider}`) ?? null;
120
+ return raw;
121
+ }
122
+
123
+ /** Verify the Node.js runtime is new enough for the APIs used here. */
124
+ export function checkRuntime({ nodeVersion = process.versions.node } = {}) {
125
+ const major = Number.parseInt(nodeVersion.split('.')[0], 10);
126
+ const warnings = [];
127
+ if (!Number.isFinite(major) || major < 22) {
128
+ throw new ConfigError(
129
+ `Node.js ${nodeVersion} is too old. Ryuu Agent CLI requires Node.js 22+ (target: Node.js 25.x).`,
130
+ { nodeVersion },
131
+ );
132
+ }
133
+ if (major < 25) {
134
+ warnings.push(`Node.js ${nodeVersion} detected. Ryuu Agent CLI targets Node.js 25.x; older majors are untested.`);
135
+ }
136
+ return { major, warnings };
137
+ }
@@ -0,0 +1,50 @@
1
+ import { BUILTIN_PROVIDERS } from '../providers/catalog.js';
2
+ import { AGENT_SETTING_KEYS, WEB_SETTING_KEYS } from './load.js';
3
+ import { SETTINGS_VERSION, readSettings, settingsFile, writeSettings } from './settings.js';
4
+
5
+ /**
6
+ * First-run configuration scaffolding.
7
+ *
8
+ * `config.json` is the only configuration source, so a missing file used to
9
+ * leave a new install with nothing to look at. `buildSettingsSkeleton` returns
10
+ * the documented shape with every editable key present and empty: an empty
11
+ * value means "use the built-in default" during validation, which lets the file
12
+ * double as a checklist without changing behaviour.
13
+ */
14
+ export function buildSettingsSkeleton({ providerIds = BUILTIN_PROVIDERS.map((provider) => provider.id) } = {}) {
15
+ const providers = {};
16
+ const providerModel = {};
17
+ for (const id of providerIds) {
18
+ providers[id] = { apiKey: '', baseURL: '' };
19
+ providerModel[id] = '';
20
+ }
21
+ const skeleton = {
22
+ version: SETTINGS_VERSION,
23
+ provider: '',
24
+ providers,
25
+ providerModel,
26
+ extraProviders: [],
27
+ agent: {},
28
+ webSearch: {},
29
+ };
30
+ for (const key of AGENT_SETTING_KEYS) skeleton.agent[key] = '';
31
+ for (const key of Object.keys(WEB_SETTING_KEYS)) skeleton.webSearch[key] = '';
32
+ return skeleton;
33
+ }
34
+
35
+ /**
36
+ * Create the settings file when it is missing. Existing files are returned
37
+ * untouched (including a malformed one, which readSettings reports), so a
38
+ * startup never overwrites something the user wrote.
39
+ *
40
+ * Returns the readSettings result plus `created`; the file is written with the
41
+ * same atomic, owner-only path as every other settings save.
42
+ */
43
+ export async function ensureSettingsFile({ file = null, ...options } = {}) {
44
+ const target = file || settingsFile(options);
45
+ const current = await readSettings({ file: target });
46
+ if (current.exists) return { ...current, created: false };
47
+ const data = buildSettingsSkeleton();
48
+ await writeSettings(data, { file: target });
49
+ return { file: target, exists: true, created: true, data };
50
+ }