@ruvnet/open-claude-code 2.0.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/package.json +27 -0
  2. package/src/agents/loader.mjs +89 -0
  3. package/src/agents/parser.mjs +98 -0
  4. package/src/agents/teams.mjs +123 -0
  5. package/src/auth/oauth.mjs +220 -0
  6. package/src/config/cli-args.mjs +139 -0
  7. package/src/config/env.mjs +261 -0
  8. package/src/config/settings.mjs +119 -0
  9. package/src/core/agent-loop.mjs +462 -0
  10. package/src/core/cache.mjs +105 -0
  11. package/src/core/checkpoints.mjs +141 -0
  12. package/src/core/context-manager.mjs +198 -0
  13. package/src/core/providers.mjs +219 -0
  14. package/src/core/rate-limiter.mjs +119 -0
  15. package/src/core/scheduler.mjs +173 -0
  16. package/src/core/session.mjs +143 -0
  17. package/src/core/streaming.mjs +182 -0
  18. package/src/core/system-prompt.mjs +135 -0
  19. package/src/hooks/engine.mjs +162 -0
  20. package/src/index.mjs +220 -0
  21. package/src/mcp/client.mjs +253 -0
  22. package/src/mcp/transport-shttp.mjs +130 -0
  23. package/src/mcp/transport-sse.mjs +131 -0
  24. package/src/mcp/transport-ws.mjs +134 -0
  25. package/src/permissions/checker.mjs +57 -0
  26. package/src/permissions/injection-check.mjs +60 -0
  27. package/src/permissions/path-check.mjs +102 -0
  28. package/src/permissions/prompt.mjs +73 -0
  29. package/src/permissions/sandbox.mjs +112 -0
  30. package/src/plugins/loader.mjs +138 -0
  31. package/src/skills/loader.mjs +147 -0
  32. package/src/skills/runner.mjs +55 -0
  33. package/src/telemetry/index.mjs +96 -0
  34. package/src/tools/agent.mjs +127 -0
  35. package/src/tools/ask-user.mjs +61 -0
  36. package/src/tools/bash.mjs +148 -0
  37. package/src/tools/cron-create.mjs +120 -0
  38. package/src/tools/cron-delete.mjs +49 -0
  39. package/src/tools/cron-list.mjs +37 -0
  40. package/src/tools/edit.mjs +82 -0
  41. package/src/tools/enter-worktree.mjs +69 -0
  42. package/src/tools/exit-worktree.mjs +57 -0
  43. package/src/tools/glob.mjs +117 -0
  44. package/src/tools/grep.mjs +127 -0
  45. package/src/tools/ls.mjs +58 -0
  46. package/src/tools/lsp.mjs +115 -0
  47. package/src/tools/multi-edit.mjs +94 -0
  48. package/src/tools/notebook-edit.mjs +96 -0
  49. package/src/tools/read-mcp-resource.mjs +57 -0
  50. package/src/tools/read.mjs +138 -0
  51. package/src/tools/registry.mjs +114 -0
  52. package/src/tools/remote-trigger.mjs +84 -0
  53. package/src/tools/send-message.mjs +64 -0
  54. package/src/tools/skill.mjs +52 -0
  55. package/src/tools/todo-write.mjs +68 -0
  56. package/src/tools/tool-search.mjs +77 -0
  57. package/src/tools/web-fetch.mjs +65 -0
  58. package/src/tools/web-search.mjs +89 -0
  59. package/src/tools/write.mjs +55 -0
  60. package/src/ui/commands.mjs +499 -0
  61. package/src/ui/ink-app.mjs +231 -0
  62. package/src/ui/repl.mjs +144 -0
  63. package/test/test.mjs +1857 -0
package/src/index.mjs ADDED
@@ -0,0 +1,220 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * open-claude-code v2
4
+ *
5
+ * Open source implementation of Claude Code CLI architecture.
6
+ * Based on ruDevolution decompilation of Claude Code v2.1.91.
7
+ *
8
+ * Architecture mirrors the actual Claude Code internals:
9
+ * - Async generator agent loop (13 event types)
10
+ * - 25+ tools with validateInput/call interface
11
+ * - MCP client (stdio/SSE/WS/sHTTP transports)
12
+ * - 6 permission modes + sandbox
13
+ * - Context compaction + auto-compaction
14
+ * - Hooks system (7 events)
15
+ * - Settings chain (5 layers, 76 properties)
16
+ * - Multi-provider support (Anthropic, OpenAI, Google)
17
+ * - Custom agents and skills
18
+ * - Session management and checkpoints
19
+ * - Prompt caching
20
+ * - 39 slash commands
21
+ * - Telemetry stub
22
+ */
23
+
24
+ import { createAgentLoop } from './core/agent-loop.mjs';
25
+ import { createToolRegistry } from './tools/registry.mjs';
26
+ import { createPermissionChecker } from './permissions/checker.mjs';
27
+ import { loadSettings } from './config/settings.mjs';
28
+ import { parseArgs, getUsageText } from './config/cli-args.mjs';
29
+ import { HookEngine } from './hooks/engine.mjs';
30
+ import { McpClient } from './mcp/client.mjs';
31
+ import { AgentLoader } from './agents/loader.mjs';
32
+ import { SkillsLoader } from './skills/loader.mjs';
33
+ import { SessionManager } from './core/session.mjs';
34
+ import { CheckpointManager } from './core/checkpoints.mjs';
35
+ import { PromptCache } from './core/cache.mjs';
36
+ import { readEnv } from './config/env.mjs';
37
+ import * as telemetry from './telemetry/index.mjs';
38
+
39
+ async function main() {
40
+ const args = parseArgs(process.argv.slice(2));
41
+
42
+ // Handle --version
43
+ if (args.showVersion) {
44
+ console.log('open-claude-code v2.0.0-alpha.1');
45
+ process.exit(0);
46
+ }
47
+
48
+ // Handle --help
49
+ if (args.showHelp) {
50
+ console.log(getUsageText());
51
+ process.exit(0);
52
+ }
53
+
54
+ const settings = await loadSettings();
55
+ const env = readEnv();
56
+
57
+ // Apply CLI overrides to settings
58
+ if (args.permissionMode) settings.permissions = { ...settings.permissions, defaultMode: args.permissionMode };
59
+ if (args.systemPrompt) settings.systemPromptOverride = args.systemPrompt;
60
+ if (args.addDirs?.length) settings.addDirs = args.addDirs;
61
+ if (args.maxTurns) settings.maxTurns = args.maxTurns;
62
+ if (args.verbose) settings.verbose = true;
63
+ if (args.debug) settings.debug = true;
64
+
65
+ const tools = createToolRegistry();
66
+ const permissions = createPermissionChecker(settings.permissions);
67
+ const hooks = new HookEngine(settings.hooks);
68
+
69
+ // Apply tool allow/deny lists
70
+ if (args.allowedTools) settings.allowedTools = args.allowedTools;
71
+ if (args.disallowedTools) settings.disallowedTools = args.disallowedTools;
72
+
73
+ // Load custom agents
74
+ const agentLoader = new AgentLoader();
75
+ agentLoader.load();
76
+
77
+ // Load skills
78
+ const skillsLoader = new SkillsLoader();
79
+ skillsLoader.load();
80
+
81
+ // Wire skill tool
82
+ const skillTool = tools.get('Skill');
83
+ if (skillTool) skillTool._skillsLoader = skillsLoader;
84
+
85
+ // Session management
86
+ const sessionManager = new SessionManager();
87
+ const checkpointManager = new CheckpointManager();
88
+ const promptCache = new PromptCache();
89
+
90
+ // Connect MCP servers if configured
91
+ const mcpClients = [];
92
+ if (settings.mcpServers) {
93
+ for (const [name, config] of Object.entries(settings.mcpServers)) {
94
+ try {
95
+ const client = new McpClient(config);
96
+ await client.connect();
97
+ const mcpTools = await client.listTools();
98
+ tools.registerMcpTools(mcpTools, (toolName, toolArgs) => client.callTool(toolName, toolArgs));
99
+ mcpClients.push(client);
100
+ } catch (err) {
101
+ console.error(`MCP server "${name}" failed to connect: ${err.message}`);
102
+ }
103
+ }
104
+ }
105
+
106
+ // Wire MCP resource tool
107
+ const mcpResourceTool = tools.get('ReadMcpResource');
108
+ if (mcpResourceTool) mcpResourceTool._mcpClients = mcpClients;
109
+
110
+ const loop = createAgentLoop({
111
+ model: args.model || settings.model || 'claude-sonnet-4-6',
112
+ tools,
113
+ permissions,
114
+ settings,
115
+ hooks,
116
+ });
117
+
118
+ // Attach extra state for commands to access
119
+ loop.state._agentLoader = agentLoader;
120
+ loop.state._skillsLoader = skillsLoader;
121
+ loop.state._mcpClients = mcpClients;
122
+ loop.state._hooks = settings.hooks;
123
+ loop.state._permissionMode = settings.permissions?.defaultMode || 'default';
124
+ loop.state._sessionManager = sessionManager;
125
+ loop.state._checkpointManager = checkpointManager;
126
+ loop.state._promptCache = promptCache;
127
+
128
+ telemetry.track('session.start', { model: loop.state.model });
129
+
130
+ // Graceful shutdown
131
+ const cleanup = async () => {
132
+ telemetry.track('session.end', {
133
+ turns: loop.state.turnCount,
134
+ tokens: loop.state.tokenUsage,
135
+ });
136
+ for (const client of mcpClients) {
137
+ await client.disconnect().catch(() => {});
138
+ }
139
+ };
140
+ process.on('SIGINT', async () => { await cleanup(); process.exit(0); });
141
+ process.on('SIGTERM', async () => { await cleanup(); process.exit(0); });
142
+
143
+ if (args.prompt) {
144
+ // Non-interactive: run prompt and exit
145
+ const outputFormat = args.outputFormat || 'text';
146
+ const results = [];
147
+
148
+ for await (const event of loop.run(args.prompt)) {
149
+ if (outputFormat === 'json') {
150
+ results.push(event);
151
+ } else if (outputFormat === 'stream-json') {
152
+ console.log(JSON.stringify(event));
153
+ } else {
154
+ handleEvent(event, settings);
155
+ }
156
+ }
157
+
158
+ if (outputFormat === 'json') {
159
+ // Extract final text
160
+ const texts = results
161
+ .filter(e => e.type === 'assistant')
162
+ .map(e => e.content)
163
+ .filter(Boolean);
164
+ console.log(JSON.stringify({
165
+ result: texts.join('\n'),
166
+ usage: loop.state.tokenUsage,
167
+ model: loop.state.model,
168
+ }));
169
+ } else {
170
+ console.log('');
171
+ }
172
+
173
+ await cleanup();
174
+ } else {
175
+ // Interactive REPL
176
+ const { startRepl } = await import('./ui/repl.mjs');
177
+ await startRepl(loop, settings);
178
+ await cleanup();
179
+ }
180
+ }
181
+
182
+ function handleEvent(event, settings = {}) {
183
+ switch (event.type) {
184
+ case 'stream_request_start':
185
+ break;
186
+ case 'stream_event':
187
+ process.stdout.write(event.text || '');
188
+ break;
189
+ case 'thinking':
190
+ if (process.env.SHOW_THINKING || settings.verbose) {
191
+ process.stdout.write(`\x1b[2m${event.text}\x1b[0m`);
192
+ }
193
+ break;
194
+ case 'assistant':
195
+ if (!event._streamed && event.content) console.log(event.content);
196
+ break;
197
+ case 'tool_progress':
198
+ process.stderr.write(`\x1b[33m[${event.tool}]\x1b[0m running...\n`);
199
+ break;
200
+ case 'result':
201
+ break;
202
+ case 'compaction':
203
+ process.stderr.write(`\x1b[2m[compaction #${event.count}]\x1b[0m\n`);
204
+ break;
205
+ case 'hookPermissionResult':
206
+ if (!event.allowed) {
207
+ process.stderr.write(`\x1b[31m[blocked: ${event.tool}]\x1b[0m\n`);
208
+ }
209
+ break;
210
+ case 'error':
211
+ console.error(`\x1b[31mError: ${event.message}\x1b[0m`);
212
+ break;
213
+ case 'stop':
214
+ break;
215
+ default:
216
+ break;
217
+ }
218
+ }
219
+
220
+ main().catch(e => { console.error(e); process.exit(1); });
@@ -0,0 +1,253 @@
1
+ /**
2
+ * MCP Client — multi-transport Model Context Protocol client.
3
+ *
4
+ * Supports four transports:
5
+ * - stdio: spawn child process, communicate via stdin/stdout
6
+ * - sse: Server-Sent Events over HTTP
7
+ * - websocket: bidirectional WebSocket
8
+ * - streamable-http: POST with SSE response (new MCP transport)
9
+ *
10
+ * Auto-detects transport from server config.
11
+ */
12
+
13
+ import { spawn } from 'child_process';
14
+
15
+ const MCP_PROTOCOL_VERSION = '2024-11-05';
16
+
17
+ export class McpClient {
18
+ /**
19
+ * @param {object} serverConfig - { command, args, env, url, transport }
20
+ */
21
+ constructor(serverConfig) {
22
+ this.config = serverConfig;
23
+ this.process = null;
24
+ this.transport = null;
25
+ this.requestId = 0;
26
+ this.pending = new Map();
27
+ this.buffer = '';
28
+ this.tools = [];
29
+ this.resources = [];
30
+ this.serverInfo = null;
31
+ this.connected = false;
32
+ }
33
+
34
+ _detectTransport() {
35
+ if (this.config.transport) return this.config.transport;
36
+ if (this.config.command) return 'stdio';
37
+ if (this.config.url) {
38
+ if (this.config.url.startsWith('ws://') || this.config.url.startsWith('wss://')) return 'websocket';
39
+ if (this.config.url.includes('/sse')) return 'sse';
40
+ return 'streamable-http';
41
+ }
42
+ return 'stdio';
43
+ }
44
+
45
+ async connect() {
46
+ const transportType = this._detectTransport();
47
+
48
+ switch (transportType) {
49
+ case 'stdio': return this._connectStdio();
50
+ case 'sse': return this._connectSSE();
51
+ case 'websocket': return this._connectWebSocket();
52
+ case 'streamable-http': return this._connectStreamableHttp();
53
+ default: throw new Error(`Unknown MCP transport: ${transportType}`);
54
+ }
55
+ }
56
+
57
+ async _connectStdio() {
58
+ this.process = spawn(this.config.command, this.config.args || [], {
59
+ stdio: ['pipe', 'pipe', 'pipe'],
60
+ env: { ...process.env, ...this.config.env },
61
+ });
62
+
63
+ this.process.stdout.on('data', (data) => this._onData(data));
64
+ this.process.stderr.on('data', (data) => {
65
+ if (process.env.MCP_DEBUG) {
66
+ process.stderr.write(`[mcp:${this.config.command}] ${data}`);
67
+ }
68
+ });
69
+
70
+ this.process.on('exit', (code) => {
71
+ this.connected = false;
72
+ for (const [, { reject }] of this.pending) {
73
+ reject(new Error(`MCP server exited with code ${code}`));
74
+ }
75
+ this.pending.clear();
76
+ });
77
+
78
+ const initResult = await this._request('initialize', {
79
+ protocolVersion: MCP_PROTOCOL_VERSION,
80
+ capabilities: {},
81
+ clientInfo: { name: 'open-claude-code', version: '2.0.0' },
82
+ });
83
+
84
+ this.serverInfo = initResult;
85
+ this.connected = true;
86
+ this._notify('notifications/initialized', {});
87
+ return this.serverInfo;
88
+ }
89
+
90
+ async _connectSSE() {
91
+ const { SseTransport } = await import('./transport-sse.mjs');
92
+ this.transport = new SseTransport(this.config.url, {
93
+ headers: this.config.headers || {},
94
+ });
95
+ await this.transport.connect();
96
+ this.transport.onMessage((msg) => {
97
+ if (msg.data?.id && this.pending.has(msg.data.id)) {
98
+ const { resolve, reject } = this.pending.get(msg.data.id);
99
+ this.pending.delete(msg.data.id);
100
+ if (msg.data.error) reject(new Error(msg.data.error.message));
101
+ else resolve(msg.data.result);
102
+ }
103
+ });
104
+ this.connected = true;
105
+ return this._initRemote();
106
+ }
107
+
108
+ async _connectWebSocket() {
109
+ const { WebSocketTransport } = await import('./transport-ws.mjs');
110
+ this.transport = new WebSocketTransport(this.config.url, {
111
+ headers: this.config.headers || {},
112
+ });
113
+ await this.transport.connect();
114
+ this.connected = true;
115
+ this.serverInfo = await this.transport.request('initialize', {
116
+ protocolVersion: MCP_PROTOCOL_VERSION,
117
+ capabilities: {},
118
+ clientInfo: { name: 'open-claude-code', version: '2.0.0' },
119
+ });
120
+ return this.serverInfo;
121
+ }
122
+
123
+ async _connectStreamableHttp() {
124
+ const { StreamableHttpTransport } = await import('./transport-shttp.mjs');
125
+ this.transport = new StreamableHttpTransport(this.config.url, {
126
+ headers: this.config.headers || {},
127
+ });
128
+ await this.transport.connect();
129
+ this.connected = true;
130
+ this.serverInfo = await this.transport.request('initialize', {
131
+ protocolVersion: MCP_PROTOCOL_VERSION,
132
+ capabilities: {},
133
+ clientInfo: { name: 'open-claude-code', version: '2.0.0' },
134
+ });
135
+ return this.serverInfo;
136
+ }
137
+
138
+ async _initRemote() {
139
+ const result = await this._transportRequest('initialize', {
140
+ protocolVersion: MCP_PROTOCOL_VERSION,
141
+ capabilities: {},
142
+ clientInfo: { name: 'open-claude-code', version: '2.0.0' },
143
+ });
144
+ this.serverInfo = result;
145
+ return result;
146
+ }
147
+
148
+ async _transportRequest(method, params) {
149
+ return new Promise((resolve, reject) => {
150
+ const id = ++this.requestId;
151
+ this.pending.set(id, { resolve, reject });
152
+ this.transport.send({ jsonrpc: '2.0', id, method, params });
153
+ });
154
+ }
155
+
156
+ async listTools() {
157
+ let result;
158
+ if (this.transport?.request) {
159
+ result = await this.transport.request('tools/list', {});
160
+ } else if (this.transport) {
161
+ result = await this._transportRequest('tools/list', {});
162
+ } else {
163
+ result = await this._request('tools/list', {});
164
+ }
165
+ this.tools = result?.tools || [];
166
+ return this.tools;
167
+ }
168
+
169
+ async callTool(name, args) {
170
+ const params = { name, arguments: args };
171
+ let result;
172
+ if (this.transport?.request) {
173
+ result = await this.transport.request('tools/call', params);
174
+ } else if (this.transport) {
175
+ result = await this._transportRequest('tools/call', params);
176
+ } else {
177
+ result = await this._request('tools/call', params);
178
+ }
179
+ if (result?.content && Array.isArray(result.content)) {
180
+ return result.content.filter(c => c.type === 'text').map(c => c.text).join('\n');
181
+ }
182
+ return result;
183
+ }
184
+
185
+ async readResource(uri) {
186
+ const params = { uri };
187
+ if (this.transport?.request) return this.transport.request('resources/read', params);
188
+ if (this.transport) return this._transportRequest('resources/read', params);
189
+ return this._request('resources/read', params);
190
+ }
191
+
192
+ async listResources() {
193
+ let result;
194
+ if (this.transport?.request) result = await this.transport.request('resources/list', {});
195
+ else if (this.transport) result = await this._transportRequest('resources/list', {});
196
+ else result = await this._request('resources/list', {});
197
+ this.resources = result?.resources || [];
198
+ return this.resources;
199
+ }
200
+
201
+ async disconnect() {
202
+ this.connected = false;
203
+ if (this.transport) {
204
+ await this.transport.disconnect();
205
+ this.transport = null;
206
+ return;
207
+ }
208
+ if (!this.process) return;
209
+ try {
210
+ await this._request('shutdown', {});
211
+ this._notify('exit', {});
212
+ } catch { /* best effort */ }
213
+
214
+ await new Promise(resolve => {
215
+ const timeout = setTimeout(() => { this.process?.kill('SIGKILL'); resolve(); }, 2000);
216
+ this.process.on('exit', () => { clearTimeout(timeout); resolve(); });
217
+ this.process.kill('SIGTERM');
218
+ });
219
+ this.process = null;
220
+ }
221
+
222
+ _request(method, params) {
223
+ return new Promise((resolve, reject) => {
224
+ const id = ++this.requestId;
225
+ this.pending.set(id, { resolve, reject });
226
+ const msg = JSON.stringify({ jsonrpc: '2.0', id, method, params });
227
+ this.process.stdin.write(msg + '\n');
228
+ });
229
+ }
230
+
231
+ _notify(method, params) {
232
+ const msg = JSON.stringify({ jsonrpc: '2.0', method, params });
233
+ this.process?.stdin.write(msg + '\n');
234
+ }
235
+
236
+ _onData(data) {
237
+ this.buffer += data.toString();
238
+ const lines = this.buffer.split('\n');
239
+ this.buffer = lines.pop();
240
+ for (const line of lines) {
241
+ if (!line.trim()) continue;
242
+ try {
243
+ const msg = JSON.parse(line);
244
+ if (msg.id && this.pending.has(msg.id)) {
245
+ const { resolve, reject } = this.pending.get(msg.id);
246
+ this.pending.delete(msg.id);
247
+ if (msg.error) reject(new Error(`MCP error: ${msg.error.message || JSON.stringify(msg.error)}`));
248
+ else resolve(msg.result);
249
+ }
250
+ } catch { /* malformed */ }
251
+ }
252
+ }
253
+ }
@@ -0,0 +1,130 @@
1
+ /**
2
+ * Streamable HTTP Transport — new MCP transport.
3
+ *
4
+ * Uses POST with SSE response body for bidirectional communication.
5
+ * Each request is a POST that returns a streaming SSE response.
6
+ */
7
+
8
+ export class StreamableHttpTransport {
9
+ /**
10
+ * @param {string} url - HTTP endpoint URL
11
+ * @param {object} [options] - { headers, timeout, sessionId }
12
+ */
13
+ constructor(url, options = {}) {
14
+ this.url = url;
15
+ this.headers = options.headers || {};
16
+ this.timeout = options.timeout || 30000;
17
+ this.sessionId = options.sessionId || null;
18
+ this.connected = false;
19
+ }
20
+
21
+ async connect() {
22
+ // Test connectivity with an empty request
23
+ const res = await fetch(this.url, {
24
+ method: 'POST',
25
+ headers: {
26
+ 'Content-Type': 'application/json',
27
+ Accept: 'text/event-stream',
28
+ ...this.headers,
29
+ },
30
+ body: JSON.stringify({ jsonrpc: '2.0', method: 'ping', params: {} }),
31
+ });
32
+
33
+ if (res.ok) {
34
+ this.sessionId = res.headers.get('x-session-id') || this.sessionId;
35
+ this.connected = true;
36
+ }
37
+ return this;
38
+ }
39
+
40
+ /**
41
+ * Send a JSON-RPC request and receive a streaming SSE response.
42
+ * Collects all events and returns the final result.
43
+ */
44
+ async request(method, params) {
45
+ const id = Date.now();
46
+ const body = { jsonrpc: '2.0', id, method, params };
47
+
48
+ const headers = {
49
+ 'Content-Type': 'application/json',
50
+ Accept: 'text/event-stream',
51
+ ...this.headers,
52
+ };
53
+
54
+ if (this.sessionId) {
55
+ headers['x-session-id'] = this.sessionId;
56
+ }
57
+
58
+ const res = await fetch(this.url, {
59
+ method: 'POST',
60
+ headers,
61
+ body: JSON.stringify(body),
62
+ });
63
+
64
+ if (!res.ok) {
65
+ throw new Error(`sHTTP error: ${res.status} ${res.statusText}`);
66
+ }
67
+
68
+ const contentType = res.headers.get('content-type') || '';
69
+
70
+ // If SSE response, stream it
71
+ if (contentType.includes('text/event-stream')) {
72
+ return this._readSSEResponse(res);
73
+ }
74
+
75
+ // Otherwise, plain JSON
76
+ return res.json();
77
+ }
78
+
79
+ async _readSSEResponse(res) {
80
+ const reader = res.body.getReader();
81
+ const decoder = new TextDecoder();
82
+ let buffer = '';
83
+ let result = null;
84
+
85
+ try {
86
+ while (true) {
87
+ const { done, value } = await reader.read();
88
+ if (done) break;
89
+
90
+ buffer += decoder.decode(value, { stream: true });
91
+ const events = buffer.split('\n\n');
92
+ buffer = events.pop() || '';
93
+
94
+ for (const raw of events) {
95
+ const parsed = this._parseEvent(raw);
96
+ if (parsed?.data?.result !== undefined) {
97
+ result = parsed.data.result;
98
+ } else if (parsed?.data?.error) {
99
+ throw new Error(parsed.data.error.message || 'Unknown error');
100
+ }
101
+ }
102
+ }
103
+ } finally {
104
+ reader.releaseLock();
105
+ }
106
+
107
+ return result;
108
+ }
109
+
110
+ _parseEvent(raw) {
111
+ const dataLines = [];
112
+ for (const line of raw.split('\n')) {
113
+ if (line.startsWith('data: ')) dataLines.push(line.slice(6));
114
+ }
115
+ if (dataLines.length === 0) return null;
116
+ try {
117
+ return { data: JSON.parse(dataLines.join('\n')) };
118
+ } catch {
119
+ return null;
120
+ }
121
+ }
122
+
123
+ async send(message) {
124
+ return this.request(message.method, message.params);
125
+ }
126
+
127
+ async disconnect() {
128
+ this.connected = false;
129
+ }
130
+ }