@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.
Files changed (40) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +383 -0
  3. package/package.json +29 -4
  4. package/src/auth/oauth.mjs +46 -6
  5. package/src/config/cli-args.mjs +20 -0
  6. package/src/config/env.mjs +2 -2
  7. package/src/config/settings.mjs +30 -130
  8. package/src/core/agent-loop.mjs +92 -28
  9. package/src/core/providers.mjs +19 -1
  10. package/src/core/session.mjs +34 -149
  11. package/src/core/system-prompt.mjs +43 -47
  12. package/src/hooks/engine.mjs +10 -2
  13. package/src/index.mjs +60 -32
  14. package/src/optimize/cascade.mjs +128 -0
  15. package/src/optimize/commands.mjs +139 -0
  16. package/src/optimize/router.mjs +212 -0
  17. package/src/optimize/store.mjs +170 -0
  18. package/src/permissions/checker.mjs +8 -79
  19. package/src/permissions/injection-check.mjs +3 -2
  20. package/src/permissions/path-check.mjs +8 -0
  21. package/src/permissions/sandbox.mjs +8 -2
  22. package/src/permissions/ssrf-check.mjs +102 -0
  23. package/src/plugins/loader.mjs +14 -6
  24. package/src/skills/loader.mjs +16 -4
  25. package/src/tools/bash.mjs +3 -40
  26. package/src/tools/enter-worktree.mjs +5 -3
  27. package/src/tools/exit-worktree.mjs +4 -3
  28. package/src/tools/grep.mjs +26 -11
  29. package/src/tools/lsp.mjs +23 -9
  30. package/src/tools/read.mjs +20 -9
  31. package/src/tools/send-message.mjs +5 -1
  32. package/src/tools/web-fetch.mjs +15 -1
  33. package/src/tools/web-search.mjs +13 -1
  34. package/src/ui/app.mjs +286 -0
  35. package/src/ui/commands.mjs +121 -71
  36. package/src/ui/components.mjs +219 -0
  37. package/src/ui/ink-app.mjs +13 -52
  38. package/src/ui/markdown.mjs +278 -0
  39. package/src/ui/repl.mjs +10 -25
  40. 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+ slash commands, skill invocation, enhanced UI,
5
- * streaming output with spinner, token/cost display, and permission prompts.
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, renderCostInline, renderMarkdown } from './ink-app.mjs';
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, permissions) {
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 result = executeCommand(trimmed, loop.state);
68
- if (result.exit) { rl.close(); return; }
69
- console.log(result.response);
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 cost display after each turn
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[90m${event.text}\x1b[0m`);
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
- // Apply markdown rendering and code highlighting
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':