@ruvnet/open-claude-code 2.0.1 → 2.1.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/LICENSE +21 -0
- package/README.md +383 -0
- package/package.json +29 -4
- package/src/auth/oauth.mjs +46 -6
- package/src/config/cli-args.mjs +20 -0
- package/src/config/env.mjs +2 -2
- package/src/config/settings.mjs +30 -130
- package/src/core/agent-loop.mjs +92 -28
- package/src/core/providers.mjs +19 -1
- package/src/core/session.mjs +34 -149
- package/src/core/system-prompt.mjs +43 -47
- package/src/hooks/engine.mjs +10 -2
- package/src/index.mjs +60 -32
- package/src/optimize/cascade.mjs +128 -0
- package/src/optimize/commands.mjs +139 -0
- package/src/optimize/router.mjs +212 -0
- package/src/optimize/store.mjs +170 -0
- package/src/permissions/checker.mjs +8 -79
- package/src/permissions/injection-check.mjs +3 -2
- package/src/permissions/path-check.mjs +8 -0
- package/src/permissions/sandbox.mjs +8 -2
- package/src/permissions/ssrf-check.mjs +102 -0
- package/src/plugins/loader.mjs +14 -6
- package/src/skills/loader.mjs +16 -4
- package/src/tools/bash.mjs +3 -40
- package/src/tools/enter-worktree.mjs +5 -3
- package/src/tools/exit-worktree.mjs +4 -3
- package/src/tools/grep.mjs +26 -11
- package/src/tools/lsp.mjs +23 -9
- package/src/tools/read.mjs +20 -9
- package/src/tools/send-message.mjs +5 -1
- package/src/tools/web-fetch.mjs +15 -1
- package/src/tools/web-search.mjs +13 -1
- package/src/ui/app.mjs +286 -0
- package/src/ui/commands.mjs +121 -71
- package/src/ui/components.mjs +219 -0
- package/src/ui/ink-app.mjs +13 -52
- package/src/ui/markdown.mjs +278 -0
- package/src/ui/repl.mjs +10 -25
- package/test/test.mjs +0 -2657
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Terminal Markdown Renderer — parse markdown to ANSI-styled text.
|
|
3
|
+
*
|
|
4
|
+
* Supports:
|
|
5
|
+
* - **bold** -> bold text
|
|
6
|
+
* - *italic* -> italic text
|
|
7
|
+
* - `code` -> inverse/cyan
|
|
8
|
+
* - ```code block``` -> bordered box
|
|
9
|
+
* - - list item -> bullet
|
|
10
|
+
* - # heading -> bold + underline
|
|
11
|
+
* - [link](url) -> blue underline
|
|
12
|
+
* - | table | -> formatted table
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const ANSI = {
|
|
16
|
+
reset: '\x1b[0m',
|
|
17
|
+
bold: '\x1b[1m',
|
|
18
|
+
dim: '\x1b[2m',
|
|
19
|
+
italic: '\x1b[3m',
|
|
20
|
+
underline: '\x1b[4m',
|
|
21
|
+
inverse: '\x1b[7m',
|
|
22
|
+
red: '\x1b[31m',
|
|
23
|
+
green: '\x1b[32m',
|
|
24
|
+
yellow: '\x1b[33m',
|
|
25
|
+
blue: '\x1b[34m',
|
|
26
|
+
magenta: '\x1b[35m',
|
|
27
|
+
cyan: '\x1b[36m',
|
|
28
|
+
white: '\x1b[37m',
|
|
29
|
+
gray: '\x1b[90m',
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const noColor = process.env.NO_COLOR === '1';
|
|
33
|
+
|
|
34
|
+
function a(codes, text) {
|
|
35
|
+
if (noColor) return text;
|
|
36
|
+
const prefix = Array.isArray(codes) ? codes.join('') : codes;
|
|
37
|
+
return `${prefix}${text}${ANSI.reset}`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Render inline markdown formatting within a single line.
|
|
42
|
+
* @param {string} line
|
|
43
|
+
* @returns {string}
|
|
44
|
+
*/
|
|
45
|
+
export function renderInline(line) {
|
|
46
|
+
if (noColor) return line;
|
|
47
|
+
let result = line;
|
|
48
|
+
|
|
49
|
+
// Bold: **text**
|
|
50
|
+
result = result.replace(/\*\*(.+?)\*\*/g, (_, t) => a(ANSI.bold, t));
|
|
51
|
+
|
|
52
|
+
// Italic: *text* (not preceded/followed by *)
|
|
53
|
+
result = result.replace(/(?<!\*)\*([^*]+)\*(?!\*)/g, (_, t) => a(ANSI.italic, t));
|
|
54
|
+
|
|
55
|
+
// Inline code: `text`
|
|
56
|
+
result = result.replace(/`([^`]+)`/g, (_, t) => a(ANSI.cyan, t));
|
|
57
|
+
|
|
58
|
+
// Links: [text](url)
|
|
59
|
+
result = result.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_, text, url) => {
|
|
60
|
+
return `${a([ANSI.blue, ANSI.underline], text)} ${a(ANSI.dim, `(${url})`)}`;
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
return result;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Highlight syntax within a code line (basic keyword/string/number/comment coloring).
|
|
68
|
+
* @param {string} line
|
|
69
|
+
* @param {string} lang
|
|
70
|
+
* @returns {string}
|
|
71
|
+
*/
|
|
72
|
+
export function highlightSyntax(line, lang) {
|
|
73
|
+
if (noColor) return line;
|
|
74
|
+
let result = line;
|
|
75
|
+
|
|
76
|
+
// Strings
|
|
77
|
+
result = result.replace(/(["'`])(.*?)\1/g, (m, q, s) => a(ANSI.green, `${q}${s}${q}`));
|
|
78
|
+
|
|
79
|
+
// Keywords
|
|
80
|
+
const kw = /\b(const|let|var|function|return|if|else|for|while|class|import|export|from|async|await|try|catch|throw|new|this|def|fn|pub|use|mod|struct|enum|impl|match|type|interface)\b/g;
|
|
81
|
+
result = result.replace(kw, (m) => a(ANSI.magenta, m));
|
|
82
|
+
|
|
83
|
+
// Numbers
|
|
84
|
+
result = result.replace(/\b(\d+\.?\d*)\b/g, (m) => a(ANSI.yellow, m));
|
|
85
|
+
|
|
86
|
+
// Comments
|
|
87
|
+
result = result.replace(/(\/\/.*|#.*)$/, (m) => a(ANSI.gray, m));
|
|
88
|
+
|
|
89
|
+
return result;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Render a full markdown string to ANSI terminal output.
|
|
94
|
+
* Handles block-level elements (headings, code blocks, lists, tables)
|
|
95
|
+
* and inline formatting.
|
|
96
|
+
*
|
|
97
|
+
* @param {string} text - raw markdown
|
|
98
|
+
* @returns {string} - ANSI-formatted string
|
|
99
|
+
*/
|
|
100
|
+
export function renderMarkdown(text) {
|
|
101
|
+
if (!text) return '';
|
|
102
|
+
const lines = text.split('\n');
|
|
103
|
+
const output = [];
|
|
104
|
+
let inCodeBlock = false;
|
|
105
|
+
let codeLang = '';
|
|
106
|
+
let codeLines = [];
|
|
107
|
+
let inTable = false;
|
|
108
|
+
let tableRows = [];
|
|
109
|
+
|
|
110
|
+
for (let i = 0; i < lines.length; i++) {
|
|
111
|
+
const line = lines[i];
|
|
112
|
+
|
|
113
|
+
// Code block start/end
|
|
114
|
+
if (line.trimStart().startsWith('```')) {
|
|
115
|
+
if (inCodeBlock) {
|
|
116
|
+
// End code block - render it
|
|
117
|
+
output.push(formatCodeBlock(codeLines, codeLang));
|
|
118
|
+
inCodeBlock = false;
|
|
119
|
+
codeLines = [];
|
|
120
|
+
codeLang = '';
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
// Start code block
|
|
124
|
+
inCodeBlock = true;
|
|
125
|
+
codeLang = line.trimStart().slice(3).trim();
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (inCodeBlock) {
|
|
130
|
+
codeLines.push(line);
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Table detection
|
|
135
|
+
if (line.trim().startsWith('|') && line.trim().endsWith('|')) {
|
|
136
|
+
// Check if separator row
|
|
137
|
+
if (/^\|[\s\-:|]+\|$/.test(line.trim())) {
|
|
138
|
+
// Table separator, skip
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
tableRows.push(line);
|
|
142
|
+
// Check if next line is NOT a table row
|
|
143
|
+
const nextLine = lines[i + 1];
|
|
144
|
+
if (!nextLine || (!nextLine.trim().startsWith('|') || !nextLine.trim().endsWith('|'))) {
|
|
145
|
+
// Flush table
|
|
146
|
+
if (tableRows.length > 0) {
|
|
147
|
+
output.push(formatTable(tableRows));
|
|
148
|
+
tableRows = [];
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Flush any pending table rows
|
|
155
|
+
if (tableRows.length > 0) {
|
|
156
|
+
output.push(formatTable(tableRows));
|
|
157
|
+
tableRows = [];
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Headings
|
|
161
|
+
const headingMatch = line.match(/^(#{1,6})\s+(.+)/);
|
|
162
|
+
if (headingMatch) {
|
|
163
|
+
const level = headingMatch[1].length;
|
|
164
|
+
const text = headingMatch[2];
|
|
165
|
+
if (level === 1) {
|
|
166
|
+
output.push(a([ANSI.bold, ANSI.underline], text));
|
|
167
|
+
} else if (level === 2) {
|
|
168
|
+
output.push(a(ANSI.bold, text));
|
|
169
|
+
} else {
|
|
170
|
+
output.push(a(ANSI.bold, text));
|
|
171
|
+
}
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Unordered list
|
|
176
|
+
const listMatch = line.match(/^(\s*)([-*+])\s+(.*)/);
|
|
177
|
+
if (listMatch) {
|
|
178
|
+
const indent = listMatch[1];
|
|
179
|
+
const content = renderInline(listMatch[3]);
|
|
180
|
+
output.push(`${indent} * ${content}`);
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Ordered list
|
|
185
|
+
const olMatch = line.match(/^(\s*)(\d+)\.\s+(.*)/);
|
|
186
|
+
if (olMatch) {
|
|
187
|
+
const indent = olMatch[1];
|
|
188
|
+
const num = olMatch[2];
|
|
189
|
+
const content = renderInline(olMatch[3]);
|
|
190
|
+
output.push(`${indent} ${num}. ${content}`);
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Horizontal rule
|
|
195
|
+
if (/^[-*_]{3,}\s*$/.test(line)) {
|
|
196
|
+
const cols = process.stdout.columns || 80;
|
|
197
|
+
output.push(a(ANSI.dim, '\u2500'.repeat(Math.min(cols, 60))));
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Blockquote
|
|
202
|
+
const bqMatch = line.match(/^>\s?(.*)/);
|
|
203
|
+
if (bqMatch) {
|
|
204
|
+
output.push(a(ANSI.dim, ` | ${renderInline(bqMatch[1])}`));
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Normal line with inline formatting
|
|
209
|
+
output.push(renderInline(line));
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Flush remaining
|
|
213
|
+
if (inCodeBlock && codeLines.length > 0) {
|
|
214
|
+
output.push(formatCodeBlock(codeLines, codeLang));
|
|
215
|
+
}
|
|
216
|
+
if (tableRows.length > 0) {
|
|
217
|
+
output.push(formatTable(tableRows));
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return output.join('\n');
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Format a code block with a border.
|
|
225
|
+
* @param {string[]} lines
|
|
226
|
+
* @param {string} lang
|
|
227
|
+
* @returns {string}
|
|
228
|
+
*/
|
|
229
|
+
function formatCodeBlock(lines, lang) {
|
|
230
|
+
if (noColor) {
|
|
231
|
+
return lines.map(l => ` ${l}`).join('\n');
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const maxLen = Math.max(...lines.map(l => l.length), 20);
|
|
235
|
+
const width = Math.min(maxLen + 4, (process.stdout.columns || 80) - 4);
|
|
236
|
+
const top = a(ANSI.gray, ` \u250C${'─'.repeat(width)}\u2510${lang ? ` ${lang}` : ''}`);
|
|
237
|
+
const bot = a(ANSI.gray, ` \u2514${'─'.repeat(width)}\u2518`);
|
|
238
|
+
const body = lines.map(l => {
|
|
239
|
+
const highlighted = highlightSyntax(l, lang);
|
|
240
|
+
return ` ${a(ANSI.gray, '\u2502')} ${highlighted}`;
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
return [top, ...body, bot].join('\n');
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Format a markdown table.
|
|
248
|
+
* @param {string[]} rows
|
|
249
|
+
* @returns {string}
|
|
250
|
+
*/
|
|
251
|
+
function formatTable(rows) {
|
|
252
|
+
const parsed = rows.map(r =>
|
|
253
|
+
r.split('|').slice(1, -1).map(c => c.trim())
|
|
254
|
+
);
|
|
255
|
+
|
|
256
|
+
if (parsed.length === 0) return '';
|
|
257
|
+
|
|
258
|
+
// Calculate column widths
|
|
259
|
+
const colCount = parsed[0].length;
|
|
260
|
+
const widths = [];
|
|
261
|
+
for (let c = 0; c < colCount; c++) {
|
|
262
|
+
widths.push(Math.max(...parsed.map(r => (r[c] || '').length)));
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const formatted = parsed.map((row, ri) => {
|
|
266
|
+
const cells = row.map((cell, ci) => cell.padEnd(widths[ci] || 0));
|
|
267
|
+
const line = ` ${cells.join(' ')}`;
|
|
268
|
+
return ri === 0 ? a(ANSI.bold, line) : line;
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
// Add separator after header
|
|
272
|
+
if (formatted.length > 1) {
|
|
273
|
+
const sep = widths.map(w => '─'.repeat(w)).join('──');
|
|
274
|
+
formatted.splice(1, 0, a(ANSI.dim, ` ${sep}`));
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
return formatted.join('\n');
|
|
278
|
+
}
|
package/src/ui/repl.mjs
CHANGED
|
@@ -1,21 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* REPL — interactive read-eval-print loop for open-claude-code.
|
|
3
3
|
*
|
|
4
|
-
* Supports all 39
|
|
5
|
-
* streaming output with spinner
|
|
4
|
+
* Supports all 39 slash commands, skill invocation, enhanced UI,
|
|
5
|
+
* and streaming output with spinner and token display.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import readline from 'readline';
|
|
9
9
|
import { executeCommand, getCompletions, COMMANDS } from './commands.mjs';
|
|
10
|
-
import { Spinner, highlightCode, renderStatusBar, renderToolProgress
|
|
10
|
+
import { Spinner, highlightCode, renderStatusBar, renderToolProgress } from './ink-app.mjs';
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
13
|
* Start the interactive REPL.
|
|
14
14
|
* @param {object} loop - agent loop instance (from createAgentLoop)
|
|
15
15
|
* @param {object} settings - loaded settings
|
|
16
|
-
* @param {object} [permissions] - permission checker to wire readline into
|
|
17
16
|
*/
|
|
18
|
-
export async function startRepl(loop, settings
|
|
17
|
+
export async function startRepl(loop, settings) {
|
|
19
18
|
const rl = readline.createInterface({
|
|
20
19
|
input: process.stdin,
|
|
21
20
|
output: process.stdout,
|
|
@@ -29,11 +28,6 @@ export async function startRepl(loop, settings, permissions) {
|
|
|
29
28
|
},
|
|
30
29
|
});
|
|
31
30
|
|
|
32
|
-
// Wire readline into permission checker for interactive prompts
|
|
33
|
-
if (permissions && typeof permissions === 'object') {
|
|
34
|
-
permissions.rl = rl;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
31
|
console.log('\x1b[1mopen-claude-code v2\x1b[0m — type your prompt or /help');
|
|
38
32
|
console.log('\x1b[2mModel: %s | Tools: %d\x1b[0m', loop.state.model || 'default', loop.state.tools?.list?.()?.length || 0);
|
|
39
33
|
console.log('');
|
|
@@ -64,9 +58,9 @@ export async function startRepl(loop, settings, permissions) {
|
|
|
64
58
|
}
|
|
65
59
|
}
|
|
66
60
|
|
|
67
|
-
const
|
|
68
|
-
if (
|
|
69
|
-
console.log(
|
|
61
|
+
const { response, exit } = executeCommand(trimmed, loop.state);
|
|
62
|
+
if (exit) { rl.close(); return; }
|
|
63
|
+
console.log(response);
|
|
70
64
|
askPrompt();
|
|
71
65
|
return;
|
|
72
66
|
}
|
|
@@ -86,7 +80,7 @@ export async function startRepl(loop, settings, permissions) {
|
|
|
86
80
|
}
|
|
87
81
|
console.log('');
|
|
88
82
|
|
|
89
|
-
// Show
|
|
83
|
+
// Show status bar if enabled
|
|
90
84
|
if (settings.showTokenUsage !== false) {
|
|
91
85
|
process.stderr.write(renderStatusBar(loop.state) + '\n');
|
|
92
86
|
}
|
|
@@ -116,12 +110,7 @@ function renderEvent(event) {
|
|
|
116
110
|
break;
|
|
117
111
|
case 'thinking':
|
|
118
112
|
if (process.env.SHOW_THINKING) {
|
|
119
|
-
process.stdout.write(`\x1b[
|
|
120
|
-
}
|
|
121
|
-
break;
|
|
122
|
-
case 'thinking_complete':
|
|
123
|
-
if (process.env.SHOW_THINKING) {
|
|
124
|
-
process.stderr.write(`\x1b[90m[thinking complete]\x1b[0m\n`);
|
|
113
|
+
process.stdout.write(`\x1b[2m${event.text}\x1b[0m`);
|
|
125
114
|
}
|
|
126
115
|
break;
|
|
127
116
|
case 'tool_progress':
|
|
@@ -135,11 +124,7 @@ function renderEvent(event) {
|
|
|
135
124
|
break;
|
|
136
125
|
case 'assistant':
|
|
137
126
|
if (event.content) {
|
|
138
|
-
|
|
139
|
-
let text = event.content;
|
|
140
|
-
text = highlightCode(text);
|
|
141
|
-
text = renderMarkdown(text);
|
|
142
|
-
process.stdout.write(text);
|
|
127
|
+
process.stdout.write(highlightCode(event.content));
|
|
143
128
|
}
|
|
144
129
|
break;
|
|
145
130
|
case 'compaction':
|