@gricha/perry 0.0.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 (47) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +153 -0
  3. package/dist/agent/index.js +6 -0
  4. package/dist/agent/router.js +1017 -0
  5. package/dist/agent/run.js +182 -0
  6. package/dist/agent/static.js +58 -0
  7. package/dist/agent/systemd.js +229 -0
  8. package/dist/agent/web/assets/index-9t2sFIJM.js +101 -0
  9. package/dist/agent/web/assets/index-CCFpTruF.css +1 -0
  10. package/dist/agent/web/index.html +14 -0
  11. package/dist/agent/web/vite.svg +1 -0
  12. package/dist/chat/handler.js +174 -0
  13. package/dist/chat/host-handler.js +170 -0
  14. package/dist/chat/host-opencode-handler.js +169 -0
  15. package/dist/chat/index.js +2 -0
  16. package/dist/chat/opencode-handler.js +177 -0
  17. package/dist/chat/opencode-websocket.js +95 -0
  18. package/dist/chat/websocket.js +100 -0
  19. package/dist/client/api.js +138 -0
  20. package/dist/client/config.js +34 -0
  21. package/dist/client/docker-proxy.js +103 -0
  22. package/dist/client/index.js +4 -0
  23. package/dist/client/proxy.js +96 -0
  24. package/dist/client/shell.js +71 -0
  25. package/dist/client/ws-shell.js +120 -0
  26. package/dist/config/loader.js +59 -0
  27. package/dist/docker/index.js +372 -0
  28. package/dist/docker/types.js +1 -0
  29. package/dist/index.js +475 -0
  30. package/dist/sessions/index.js +2 -0
  31. package/dist/sessions/metadata.js +55 -0
  32. package/dist/sessions/parser.js +553 -0
  33. package/dist/sessions/types.js +1 -0
  34. package/dist/shared/base-websocket.js +51 -0
  35. package/dist/shared/client-types.js +1 -0
  36. package/dist/shared/constants.js +11 -0
  37. package/dist/shared/types.js +5 -0
  38. package/dist/terminal/handler.js +86 -0
  39. package/dist/terminal/host-handler.js +76 -0
  40. package/dist/terminal/index.js +3 -0
  41. package/dist/terminal/types.js +8 -0
  42. package/dist/terminal/websocket.js +115 -0
  43. package/dist/workspace/index.js +3 -0
  44. package/dist/workspace/manager.js +475 -0
  45. package/dist/workspace/state.js +66 -0
  46. package/dist/workspace/types.js +1 -0
  47. package/package.json +68 -0
@@ -0,0 +1,169 @@
1
+ import { homedir } from 'os';
2
+ export class HostOpencodeSession {
3
+ process = null;
4
+ workDir;
5
+ sessionId;
6
+ onMessage;
7
+ buffer = '';
8
+ constructor(options, onMessage) {
9
+ this.workDir = options.workDir || homedir();
10
+ this.sessionId = options.sessionId;
11
+ this.onMessage = onMessage;
12
+ }
13
+ async sendMessage(userMessage) {
14
+ const args = ['run', '--format', 'json'];
15
+ if (this.sessionId) {
16
+ args.push('--session', this.sessionId);
17
+ }
18
+ args.push(userMessage);
19
+ console.log('[host-opencode] Running: opencode', args.join(' '));
20
+ this.onMessage({
21
+ type: 'system',
22
+ content: 'Processing your message...',
23
+ timestamp: new Date().toISOString(),
24
+ });
25
+ try {
26
+ const proc = Bun.spawn(['opencode', ...args], {
27
+ cwd: this.workDir,
28
+ stdin: 'ignore',
29
+ stdout: 'pipe',
30
+ stderr: 'pipe',
31
+ env: {
32
+ ...process.env,
33
+ },
34
+ });
35
+ this.process = proc;
36
+ if (!proc.stdout || !proc.stderr) {
37
+ throw new Error('Failed to get process streams');
38
+ }
39
+ console.log('[host-opencode] Process spawned, waiting for output...');
40
+ const stderrPromise = new Response(proc.stderr).text();
41
+ const decoder = new TextDecoder();
42
+ let receivedAnyOutput = false;
43
+ for await (const chunk of proc.stdout) {
44
+ const text = decoder.decode(chunk);
45
+ console.log('[host-opencode] Received chunk:', text.length, 'bytes');
46
+ receivedAnyOutput = true;
47
+ this.buffer += text;
48
+ this.processBuffer();
49
+ }
50
+ const exitCode = await proc.exited;
51
+ console.log('[host-opencode] Process exited with code:', exitCode, 'receivedOutput:', receivedAnyOutput);
52
+ const stderrText = await stderrPromise;
53
+ if (stderrText) {
54
+ console.error('[host-opencode] stderr:', stderrText);
55
+ }
56
+ if (exitCode !== 0) {
57
+ this.onMessage({
58
+ type: 'error',
59
+ content: stderrText || `OpenCode exited with code ${exitCode}`,
60
+ timestamp: new Date().toISOString(),
61
+ });
62
+ return;
63
+ }
64
+ if (!receivedAnyOutput) {
65
+ this.onMessage({
66
+ type: 'error',
67
+ content: 'No response from OpenCode. Check if OpenCode is installed and configured.',
68
+ timestamp: new Date().toISOString(),
69
+ });
70
+ return;
71
+ }
72
+ this.onMessage({
73
+ type: 'done',
74
+ content: 'Response complete',
75
+ timestamp: new Date().toISOString(),
76
+ });
77
+ }
78
+ catch (err) {
79
+ console.error('[host-opencode] Error:', err);
80
+ this.onMessage({
81
+ type: 'error',
82
+ content: err.message,
83
+ timestamp: new Date().toISOString(),
84
+ });
85
+ }
86
+ finally {
87
+ this.process = null;
88
+ }
89
+ }
90
+ processBuffer() {
91
+ const lines = this.buffer.split('\n');
92
+ this.buffer = lines.pop() || '';
93
+ for (const line of lines) {
94
+ if (!line.trim())
95
+ continue;
96
+ try {
97
+ const event = JSON.parse(line);
98
+ this.handleStreamEvent(event);
99
+ }
100
+ catch {
101
+ console.error('[host-opencode] Failed to parse:', line);
102
+ }
103
+ }
104
+ }
105
+ handleStreamEvent(event) {
106
+ const timestamp = new Date().toISOString();
107
+ if (event.type === 'step_start' && event.sessionID) {
108
+ if (!this.sessionId) {
109
+ this.sessionId = event.sessionID;
110
+ this.onMessage({
111
+ type: 'system',
112
+ content: `Session started ${this.sessionId}`,
113
+ timestamp,
114
+ });
115
+ }
116
+ return;
117
+ }
118
+ if (event.type === 'text' && event.part?.text) {
119
+ this.onMessage({
120
+ type: 'assistant',
121
+ content: event.part.text,
122
+ timestamp,
123
+ });
124
+ return;
125
+ }
126
+ if (event.type === 'tool_use' && event.part) {
127
+ const toolName = event.part.tool || 'unknown';
128
+ const toolId = event.part.callID || event.part.id;
129
+ const input = event.part.state?.input;
130
+ const output = event.part.state?.output;
131
+ const title = event.part.state?.title || input?.description || toolName;
132
+ console.log('[host-opencode] Tool use:', toolName, title);
133
+ this.onMessage({
134
+ type: 'tool_use',
135
+ content: JSON.stringify(input, null, 2),
136
+ toolName: title || toolName,
137
+ toolId,
138
+ timestamp,
139
+ });
140
+ if (output) {
141
+ this.onMessage({
142
+ type: 'tool_result',
143
+ content: output,
144
+ toolName,
145
+ toolId,
146
+ timestamp,
147
+ });
148
+ }
149
+ return;
150
+ }
151
+ }
152
+ async interrupt() {
153
+ if (this.process) {
154
+ this.process.kill();
155
+ this.process = null;
156
+ this.onMessage({
157
+ type: 'system',
158
+ content: 'Chat interrupted',
159
+ timestamp: new Date().toISOString(),
160
+ });
161
+ }
162
+ }
163
+ getSessionId() {
164
+ return this.sessionId;
165
+ }
166
+ }
167
+ export function createHostOpencodeSession(options, onMessage) {
168
+ return new HostOpencodeSession(options, onMessage);
169
+ }
@@ -0,0 +1,2 @@
1
+ export * from './handler';
2
+ export * from './websocket';
@@ -0,0 +1,177 @@
1
+ export class OpencodeSession {
2
+ process = null;
3
+ containerName;
4
+ workDir;
5
+ sessionId;
6
+ onMessage;
7
+ buffer = '';
8
+ constructor(options, onMessage) {
9
+ this.containerName = options.containerName;
10
+ this.workDir = options.workDir || '/home/workspace';
11
+ this.sessionId = options.sessionId;
12
+ this.onMessage = onMessage;
13
+ }
14
+ async sendMessage(userMessage) {
15
+ const args = [
16
+ 'exec',
17
+ '-u',
18
+ 'workspace',
19
+ '-w',
20
+ this.workDir,
21
+ this.containerName,
22
+ 'opencode',
23
+ 'run',
24
+ '--format',
25
+ 'json',
26
+ ];
27
+ if (this.sessionId) {
28
+ args.push('--session', this.sessionId);
29
+ }
30
+ args.push(userMessage);
31
+ console.log('[opencode] Running:', 'docker', args.join(' '));
32
+ this.onMessage({
33
+ type: 'system',
34
+ content: 'Processing your message...',
35
+ timestamp: new Date().toISOString(),
36
+ });
37
+ try {
38
+ const proc = Bun.spawn(['docker', ...args], {
39
+ stdin: 'ignore',
40
+ stdout: 'pipe',
41
+ stderr: 'pipe',
42
+ });
43
+ this.process = proc;
44
+ if (!proc.stdout || !proc.stderr) {
45
+ throw new Error('Failed to get process streams');
46
+ }
47
+ console.log('[opencode] Process spawned, waiting for output...');
48
+ const stderrPromise = new Response(proc.stderr).text();
49
+ const decoder = new TextDecoder();
50
+ let receivedAnyOutput = false;
51
+ for await (const chunk of proc.stdout) {
52
+ const text = decoder.decode(chunk);
53
+ console.log('[opencode] Received chunk:', text.length, 'bytes');
54
+ receivedAnyOutput = true;
55
+ this.buffer += text;
56
+ this.processBuffer();
57
+ }
58
+ const exitCode = await proc.exited;
59
+ console.log('[opencode] Process exited with code:', exitCode, 'receivedOutput:', receivedAnyOutput);
60
+ const stderrText = await stderrPromise;
61
+ if (stderrText) {
62
+ console.error('[opencode] stderr:', stderrText);
63
+ }
64
+ if (exitCode !== 0) {
65
+ this.onMessage({
66
+ type: 'error',
67
+ content: stderrText || `OpenCode exited with code ${exitCode}`,
68
+ timestamp: new Date().toISOString(),
69
+ });
70
+ return;
71
+ }
72
+ if (!receivedAnyOutput) {
73
+ this.onMessage({
74
+ type: 'error',
75
+ content: 'No response from OpenCode. Check if OpenCode is configured in the workspace.',
76
+ timestamp: new Date().toISOString(),
77
+ });
78
+ return;
79
+ }
80
+ this.onMessage({
81
+ type: 'done',
82
+ content: 'Response complete',
83
+ timestamp: new Date().toISOString(),
84
+ });
85
+ }
86
+ catch (err) {
87
+ console.error('[opencode] Error:', err);
88
+ this.onMessage({
89
+ type: 'error',
90
+ content: err.message,
91
+ timestamp: new Date().toISOString(),
92
+ });
93
+ }
94
+ finally {
95
+ this.process = null;
96
+ }
97
+ }
98
+ processBuffer() {
99
+ const lines = this.buffer.split('\n');
100
+ this.buffer = lines.pop() || '';
101
+ for (const line of lines) {
102
+ if (!line.trim())
103
+ continue;
104
+ try {
105
+ const event = JSON.parse(line);
106
+ this.handleStreamEvent(event);
107
+ }
108
+ catch {
109
+ console.error('[opencode] Failed to parse:', line);
110
+ }
111
+ }
112
+ }
113
+ handleStreamEvent(event) {
114
+ const timestamp = new Date().toISOString();
115
+ if (event.type === 'step_start' && event.sessionID) {
116
+ if (!this.sessionId) {
117
+ this.sessionId = event.sessionID;
118
+ this.onMessage({
119
+ type: 'system',
120
+ content: `Session started ${this.sessionId}`,
121
+ timestamp,
122
+ });
123
+ }
124
+ return;
125
+ }
126
+ if (event.type === 'text' && event.part?.text) {
127
+ this.onMessage({
128
+ type: 'assistant',
129
+ content: event.part.text,
130
+ timestamp,
131
+ });
132
+ return;
133
+ }
134
+ if (event.type === 'tool_use' && event.part) {
135
+ const toolName = event.part.tool || 'unknown';
136
+ const toolId = event.part.callID || event.part.id;
137
+ const input = event.part.state?.input;
138
+ const output = event.part.state?.output;
139
+ const title = event.part.state?.title || input?.description || toolName;
140
+ console.log('[opencode] Tool use:', toolName, title);
141
+ this.onMessage({
142
+ type: 'tool_use',
143
+ content: JSON.stringify(input, null, 2),
144
+ toolName: title || toolName,
145
+ toolId,
146
+ timestamp,
147
+ });
148
+ if (output) {
149
+ this.onMessage({
150
+ type: 'tool_result',
151
+ content: output,
152
+ toolName,
153
+ toolId,
154
+ timestamp,
155
+ });
156
+ }
157
+ return;
158
+ }
159
+ }
160
+ async interrupt() {
161
+ if (this.process) {
162
+ this.process.kill();
163
+ this.process = null;
164
+ this.onMessage({
165
+ type: 'system',
166
+ content: 'Chat interrupted',
167
+ timestamp: new Date().toISOString(),
168
+ });
169
+ }
170
+ }
171
+ getSessionId() {
172
+ return this.sessionId;
173
+ }
174
+ }
175
+ export function createOpencodeSession(options, onMessage) {
176
+ return new OpencodeSession(options, onMessage);
177
+ }
@@ -0,0 +1,95 @@
1
+ import { WebSocket } from 'ws';
2
+ import { BaseWebSocketServer } from '../shared/base-websocket';
3
+ import { createOpencodeSession } from './opencode-handler';
4
+ import { createHostOpencodeSession } from './host-opencode-handler';
5
+ import { getContainerName } from '../docker';
6
+ import { HOST_WORKSPACE_NAME } from '../shared/types';
7
+ export class OpencodeWebSocketServer extends BaseWebSocketServer {
8
+ isHostAccessAllowed;
9
+ constructor(options) {
10
+ super(options);
11
+ this.isHostAccessAllowed = options.isHostAccessAllowed || (() => false);
12
+ }
13
+ handleConnection(ws, workspaceName) {
14
+ const isHostMode = workspaceName === HOST_WORKSPACE_NAME;
15
+ if (isHostMode && !this.isHostAccessAllowed()) {
16
+ ws.close(4003, 'Host access is disabled');
17
+ return;
18
+ }
19
+ const connection = {
20
+ ws,
21
+ session: null,
22
+ workspaceName,
23
+ };
24
+ this.connections.set(ws, connection);
25
+ ws.send(JSON.stringify({
26
+ type: 'connected',
27
+ workspaceName,
28
+ agentType: 'opencode',
29
+ timestamp: new Date().toISOString(),
30
+ }));
31
+ ws.on('message', async (data) => {
32
+ const str = typeof data === 'string' ? data : data.toString();
33
+ try {
34
+ const message = JSON.parse(str);
35
+ if (message.type === 'interrupt') {
36
+ if (connection.session) {
37
+ await connection.session.interrupt();
38
+ connection.session = null;
39
+ }
40
+ return;
41
+ }
42
+ if (message.type === 'message' && message.content) {
43
+ const onMessage = (chatMessage) => {
44
+ if (ws.readyState === WebSocket.OPEN) {
45
+ ws.send(JSON.stringify(chatMessage));
46
+ }
47
+ };
48
+ if (!connection.session) {
49
+ if (isHostMode) {
50
+ connection.session = createHostOpencodeSession({
51
+ sessionId: message.sessionId,
52
+ }, onMessage);
53
+ }
54
+ else {
55
+ const containerName = getContainerName(workspaceName);
56
+ connection.session = createOpencodeSession({
57
+ containerName,
58
+ workDir: '/home/workspace',
59
+ sessionId: message.sessionId,
60
+ }, onMessage);
61
+ }
62
+ }
63
+ await connection.session.sendMessage(message.content);
64
+ }
65
+ }
66
+ catch (err) {
67
+ ws.send(JSON.stringify({
68
+ type: 'error',
69
+ content: err.message,
70
+ timestamp: new Date().toISOString(),
71
+ }));
72
+ }
73
+ });
74
+ ws.on('close', () => {
75
+ const conn = this.connections.get(ws);
76
+ if (conn?.session) {
77
+ conn.session.interrupt().catch(() => { });
78
+ }
79
+ this.connections.delete(ws);
80
+ });
81
+ ws.on('error', (err) => {
82
+ console.error('OpenCode WebSocket error:', err);
83
+ const conn = this.connections.get(ws);
84
+ if (conn?.session) {
85
+ conn.session.interrupt().catch(() => { });
86
+ }
87
+ this.connections.delete(ws);
88
+ });
89
+ }
90
+ cleanupConnection(connection) {
91
+ if (connection.session) {
92
+ connection.session.interrupt().catch(() => { });
93
+ }
94
+ }
95
+ }
@@ -0,0 +1,100 @@
1
+ import { WebSocket } from 'ws';
2
+ import { BaseWebSocketServer } from '../shared/base-websocket';
3
+ import { createChatSession } from './handler';
4
+ import { createHostChatSession } from './host-handler';
5
+ import { getContainerName } from '../docker';
6
+ import { HOST_WORKSPACE_NAME } from '../shared/types';
7
+ export class ChatWebSocketServer extends BaseWebSocketServer {
8
+ getConfig;
9
+ isHostAccessAllowed;
10
+ constructor(options) {
11
+ super(options);
12
+ this.getConfig = options.getConfig;
13
+ this.isHostAccessAllowed = options.isHostAccessAllowed || (() => false);
14
+ }
15
+ handleConnection(ws, workspaceName) {
16
+ const isHostMode = workspaceName === HOST_WORKSPACE_NAME;
17
+ if (isHostMode && !this.isHostAccessAllowed()) {
18
+ ws.close(4003, 'Host access is disabled');
19
+ return;
20
+ }
21
+ const connection = {
22
+ ws,
23
+ session: null,
24
+ workspaceName,
25
+ };
26
+ this.connections.set(ws, connection);
27
+ ws.send(JSON.stringify({
28
+ type: 'connected',
29
+ workspaceName,
30
+ timestamp: new Date().toISOString(),
31
+ }));
32
+ ws.on('message', async (data) => {
33
+ const str = typeof data === 'string' ? data : data.toString();
34
+ try {
35
+ const message = JSON.parse(str);
36
+ if (message.type === 'interrupt') {
37
+ if (connection.session) {
38
+ await connection.session.interrupt();
39
+ connection.session = null;
40
+ }
41
+ return;
42
+ }
43
+ if (message.type === 'message' && message.content) {
44
+ const onMessage = (chatMessage) => {
45
+ if (ws.readyState === WebSocket.OPEN) {
46
+ ws.send(JSON.stringify(chatMessage));
47
+ }
48
+ };
49
+ if (!connection.session) {
50
+ const config = this.getConfig();
51
+ const model = config.agents?.claude_code?.model;
52
+ if (isHostMode) {
53
+ connection.session = createHostChatSession({
54
+ sessionId: message.sessionId,
55
+ model,
56
+ }, onMessage);
57
+ }
58
+ else {
59
+ const containerName = getContainerName(workspaceName);
60
+ connection.session = createChatSession({
61
+ containerName,
62
+ workDir: '/home/workspace',
63
+ sessionId: message.sessionId,
64
+ model,
65
+ }, onMessage);
66
+ }
67
+ }
68
+ await connection.session.sendMessage(message.content);
69
+ }
70
+ }
71
+ catch (err) {
72
+ ws.send(JSON.stringify({
73
+ type: 'error',
74
+ content: err.message,
75
+ timestamp: new Date().toISOString(),
76
+ }));
77
+ }
78
+ });
79
+ ws.on('close', () => {
80
+ const conn = this.connections.get(ws);
81
+ if (conn?.session) {
82
+ conn.session.interrupt().catch(() => { });
83
+ }
84
+ this.connections.delete(ws);
85
+ });
86
+ ws.on('error', (err) => {
87
+ console.error('Chat WebSocket error:', err);
88
+ const conn = this.connections.get(ws);
89
+ if (conn?.session) {
90
+ conn.session.interrupt().catch(() => { });
91
+ }
92
+ this.connections.delete(ws);
93
+ });
94
+ }
95
+ cleanupConnection(connection) {
96
+ if (connection.session) {
97
+ connection.session.interrupt().catch(() => { });
98
+ }
99
+ }
100
+ }
@@ -0,0 +1,138 @@
1
+ import { createORPCClient } from '@orpc/client';
2
+ import { RPCLink } from '@orpc/client/fetch';
3
+ import { DEFAULT_AGENT_PORT } from '../shared/constants';
4
+ export class ApiClientError extends Error {
5
+ status;
6
+ code;
7
+ constructor(message, status, code) {
8
+ super(message);
9
+ this.status = status;
10
+ this.code = code;
11
+ this.name = 'ApiClientError';
12
+ }
13
+ }
14
+ export class ApiClient {
15
+ baseUrl;
16
+ client;
17
+ constructor(options) {
18
+ this.baseUrl = options.baseUrl.replace(/\/$/, '');
19
+ const link = new RPCLink({
20
+ url: `${this.baseUrl}/rpc`,
21
+ fetch: (url, init) => fetch(url, { ...init, signal: AbortSignal.timeout(options.timeout || 30000) }),
22
+ });
23
+ this.client = createORPCClient(link);
24
+ }
25
+ async health() {
26
+ const response = await fetch(`${this.baseUrl}/health`);
27
+ return response.json();
28
+ }
29
+ async info() {
30
+ try {
31
+ return await this.client.info();
32
+ }
33
+ catch (err) {
34
+ throw this.wrapError(err);
35
+ }
36
+ }
37
+ async listWorkspaces() {
38
+ try {
39
+ return await this.client.workspaces.list();
40
+ }
41
+ catch (err) {
42
+ throw this.wrapError(err);
43
+ }
44
+ }
45
+ async getWorkspace(name) {
46
+ try {
47
+ return await this.client.workspaces.get({ name });
48
+ }
49
+ catch (err) {
50
+ throw this.wrapError(err);
51
+ }
52
+ }
53
+ async createWorkspace(request) {
54
+ try {
55
+ return await this.client.workspaces.create(request);
56
+ }
57
+ catch (err) {
58
+ throw this.wrapError(err);
59
+ }
60
+ }
61
+ async deleteWorkspace(name) {
62
+ try {
63
+ await this.client.workspaces.delete({ name });
64
+ }
65
+ catch (err) {
66
+ throw this.wrapError(err);
67
+ }
68
+ }
69
+ async startWorkspace(name) {
70
+ try {
71
+ return await this.client.workspaces.start({ name });
72
+ }
73
+ catch (err) {
74
+ throw this.wrapError(err);
75
+ }
76
+ }
77
+ async stopWorkspace(name) {
78
+ try {
79
+ return await this.client.workspaces.stop({ name });
80
+ }
81
+ catch (err) {
82
+ throw this.wrapError(err);
83
+ }
84
+ }
85
+ async getLogs(name, tail) {
86
+ try {
87
+ return await this.client.workspaces.logs({ name, tail });
88
+ }
89
+ catch (err) {
90
+ throw this.wrapError(err);
91
+ }
92
+ }
93
+ async syncWorkspace(name) {
94
+ try {
95
+ await this.client.workspaces.sync({ name });
96
+ }
97
+ catch (err) {
98
+ throw this.wrapError(err);
99
+ }
100
+ }
101
+ getTerminalUrl(name) {
102
+ const wsUrl = this.baseUrl.replace(/^http/, 'ws');
103
+ return `${wsUrl}/rpc/terminal/${encodeURIComponent(name)}`;
104
+ }
105
+ wrapError(err) {
106
+ if (err instanceof ApiClientError) {
107
+ return err;
108
+ }
109
+ if (err instanceof Error) {
110
+ if (err.message.includes('fetch failed') || err.message.includes('ECONNREFUSED')) {
111
+ return new ApiClientError(`Cannot connect to agent at ${this.baseUrl}`, 0, 'CONNECTION_FAILED');
112
+ }
113
+ if (err.name === 'TimeoutError' || err.message.includes('timeout')) {
114
+ return new ApiClientError('Request timed out', 0, 'TIMEOUT');
115
+ }
116
+ const orpcError = err;
117
+ if (orpcError.code) {
118
+ return new ApiClientError(err.message, orpcError.status || 500, orpcError.code);
119
+ }
120
+ return new ApiClientError(err.message, 0, 'UNKNOWN');
121
+ }
122
+ return new ApiClientError('Unknown error', 0, 'UNKNOWN');
123
+ }
124
+ }
125
+ export function createApiClient(worker, port) {
126
+ let baseUrl;
127
+ if (worker.includes('://')) {
128
+ baseUrl = worker;
129
+ }
130
+ else if (worker.includes(':')) {
131
+ baseUrl = `http://${worker}`;
132
+ }
133
+ else {
134
+ const effectivePort = port || DEFAULT_AGENT_PORT;
135
+ baseUrl = `http://${worker}:${effectivePort}`;
136
+ }
137
+ return new ApiClient({ baseUrl });
138
+ }