@yvhitxcel/opencode-remote 0.15.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.
- package/README.md +82 -0
- package/bin/opencode-remote.js +70 -0
- package/bin/opencode-weixin.js +10 -0
- package/dist/AGENTS.md +20 -0
- package/dist/MEMORY.md +21 -0
- package/dist/bot-runner.js +180 -0
- package/dist/cli.js +256 -0
- package/dist/core/approval.js +95 -0
- package/dist/core/auth.js +119 -0
- package/dist/core/config.js +61 -0
- package/dist/core/notifications.js +134 -0
- package/dist/core/qiniu.js +267 -0
- package/dist/core/registry.js +86 -0
- package/dist/core/router.js +344 -0
- package/dist/core/session.js +403 -0
- package/dist/core/setup.js +418 -0
- package/dist/core/types.js +16 -0
- package/dist/feishu/adapter.js +72 -0
- package/dist/feishu/bot.js +168 -0
- package/dist/feishu/commands.js +601 -0
- package/dist/feishu/handler.js +380 -0
- package/dist/index.js +60 -0
- package/dist/opencode/client.js +823 -0
- package/dist/package-lock.json +762 -0
- package/dist/patch_spawn.js +28 -0
- package/dist/plugins/agents/acp/acp-adapter.js +42 -0
- package/dist/plugins/agents/claude-code/index.js +69 -0
- package/dist/plugins/agents/codex/index.js +44 -0
- package/dist/plugins/agents/copilot/index.js +44 -0
- package/dist/plugins/agents/opencode/index.js +66 -0
- package/dist/telegram/bot.js +288 -0
- package/dist/utils/message-split.js +38 -0
- package/dist/web/code-viewer.js +266 -0
- package/dist/weixin/adapter.js +135 -0
- package/dist/weixin/api.js +179 -0
- package/dist/weixin/bot.js +183 -0
- package/dist/weixin/commands.js +758 -0
- package/dist/weixin/handler.js +577 -0
- package/dist/weixin/node_modules/encodeurl/LICENSE +22 -0
- package/dist/weixin/node_modules/encodeurl/README.md +109 -0
- package/dist/weixin/node_modules/encodeurl/index.js +60 -0
- package/dist/weixin/node_modules/encodeurl/package.json +40 -0
- package/dist/weixin/node_modules/qiniu/.claude/settings.local.json +7 -0
- package/dist/weixin/node_modules/qiniu/.github/workflows/ci-test.yml +36 -0
- package/dist/weixin/node_modules/qiniu/.github/workflows/npm-publish.yml +20 -0
- package/dist/weixin/node_modules/qiniu/.github/workflows/version-check.yml +19 -0
- package/dist/weixin/node_modules/qiniu/.idea/MarsCodeWorkspaceAppSettings.xml +7 -0
- package/dist/weixin/node_modules/qiniu/.idea/codeStyles/Project.xml +44 -0
- package/dist/weixin/node_modules/qiniu/.idea/codeStyles/codeStyleConfig.xml +5 -0
- package/dist/weixin/node_modules/qiniu/.idea/git_toolbox_blame.xml +6 -0
- package/dist/weixin/node_modules/qiniu/.idea/inspectionProfiles/Project_Default.xml +6 -0
- package/dist/weixin/node_modules/qiniu/.idea/jsLibraryMappings.xml +6 -0
- package/dist/weixin/node_modules/qiniu/.idea/modules.xml +8 -0
- package/dist/weixin/node_modules/qiniu/.idea/nodejs-sdk.iml +12 -0
- package/dist/weixin/node_modules/qiniu/.idea/vcs.xml +6 -0
- package/dist/weixin/node_modules/qiniu/CHANGELOG.md +292 -0
- package/dist/weixin/node_modules/qiniu/README.md +56 -0
- package/dist/weixin/node_modules/qiniu/StorageResponseInterface.d.ts +239 -0
- package/dist/weixin/node_modules/qiniu/codecov.yml +28 -0
- package/dist/weixin/node_modules/qiniu/index.d.ts +1995 -0
- package/dist/weixin/node_modules/qiniu/index.js +32 -0
- package/dist/weixin/node_modules/qiniu/node_modules/encodeurl/HISTORY.md +14 -0
- package/dist/weixin/node_modules/qiniu/node_modules/encodeurl/LICENSE +22 -0
- package/dist/weixin/node_modules/qiniu/node_modules/encodeurl/README.md +128 -0
- package/dist/weixin/node_modules/qiniu/node_modules/encodeurl/index.js +60 -0
- package/dist/weixin/node_modules/qiniu/node_modules/encodeurl/package.json +40 -0
- package/dist/weixin/node_modules/qiniu/package.json +80 -0
- package/dist/weixin/node_modules/qiniu/qiniu/auth/digest.js +13 -0
- package/dist/weixin/node_modules/qiniu/qiniu/cdn.js +149 -0
- package/dist/weixin/node_modules/qiniu/qiniu/conf.js +254 -0
- package/dist/weixin/node_modules/qiniu/qiniu/fop.js +112 -0
- package/dist/weixin/node_modules/qiniu/qiniu/httpc/client.js +253 -0
- package/dist/weixin/node_modules/qiniu/qiniu/httpc/endpoint.js +66 -0
- package/dist/weixin/node_modules/qiniu/qiniu/httpc/endpointsProvider.js +27 -0
- package/dist/weixin/node_modules/qiniu/qiniu/httpc/endpointsRetryPolicy.js +76 -0
- package/dist/weixin/node_modules/qiniu/qiniu/httpc/middleware/base.js +31 -0
- package/dist/weixin/node_modules/qiniu/qiniu/httpc/middleware/index.js +9 -0
- package/dist/weixin/node_modules/qiniu/qiniu/httpc/middleware/qiniuAuth.js +53 -0
- package/dist/weixin/node_modules/qiniu/qiniu/httpc/middleware/retryDomains.js +101 -0
- package/dist/weixin/node_modules/qiniu/qiniu/httpc/middleware/ua.js +36 -0
- package/dist/weixin/node_modules/qiniu/qiniu/httpc/region.js +349 -0
- package/dist/weixin/node_modules/qiniu/qiniu/httpc/regionsProvider.js +788 -0
- package/dist/weixin/node_modules/qiniu/qiniu/httpc/regionsRetryPolicy.js +242 -0
- package/dist/weixin/node_modules/qiniu/qiniu/httpc/responseWrapper.js +40 -0
- package/dist/weixin/node_modules/qiniu/qiniu/retry/index.js +4 -0
- package/dist/weixin/node_modules/qiniu/qiniu/retry/retrier.js +99 -0
- package/dist/weixin/node_modules/qiniu/qiniu/retry/retryPolicy.js +55 -0
- package/dist/weixin/node_modules/qiniu/qiniu/rpc.js +237 -0
- package/dist/weixin/node_modules/qiniu/qiniu/rtc/app.js +123 -0
- package/dist/weixin/node_modules/qiniu/qiniu/rtc/credentials.js +57 -0
- package/dist/weixin/node_modules/qiniu/qiniu/rtc/room.js +118 -0
- package/dist/weixin/node_modules/qiniu/qiniu/rtc/util.js +16 -0
- package/dist/weixin/node_modules/qiniu/qiniu/sms/message.js +58 -0
- package/dist/weixin/node_modules/qiniu/qiniu/storage/form.js +442 -0
- package/dist/weixin/node_modules/qiniu/qiniu/storage/internal.js +214 -0
- package/dist/weixin/node_modules/qiniu/qiniu/storage/resume.js +1272 -0
- package/dist/weixin/node_modules/qiniu/qiniu/storage/rs.js +1764 -0
- package/dist/weixin/node_modules/qiniu/qiniu/util.js +382 -0
- package/dist/weixin/node_modules/qiniu/qiniu/zone.js +230 -0
- package/dist/weixin/node_modules/qiniu/tsconfig.json +112 -0
- package/dist/weixin/types.js +25 -0
- package/package.json +56 -0
|
@@ -0,0 +1,823 @@
|
|
|
1
|
+
// OpenCode SDK client for remote control
|
|
2
|
+
import '../patch_spawn.js';
|
|
3
|
+
import { createRequire } from 'node:module';
|
|
4
|
+
import { platform } from 'node:os';
|
|
5
|
+
import { existsSync, readFileSync } from 'fs';
|
|
6
|
+
import { join } from 'path';
|
|
7
|
+
import { spawn } from 'child_process';
|
|
8
|
+
import { Socket } from 'net';
|
|
9
|
+
import { homedir } from 'os';
|
|
10
|
+
|
|
11
|
+
const CONFIG_DIR = join(homedir(), '.opencode-remote');
|
|
12
|
+
const CONFIG_FILE = join(CONFIG_DIR, '.env');
|
|
13
|
+
|
|
14
|
+
// Find opencode.exe binary
|
|
15
|
+
function findOpenCodeExe() {
|
|
16
|
+
const isWindows = platform() === 'win32';
|
|
17
|
+
if (isWindows) {
|
|
18
|
+
// Try common locations
|
|
19
|
+
const candidates = [
|
|
20
|
+
join(process.env.APPDATA || '', 'npm', 'node_modules', 'opencode-ai', 'node_modules', 'opencode-windows-x64', 'bin', 'opencode.exe'),
|
|
21
|
+
join(process.env.APPDATA || '', 'npm', 'node_modules', 'opencode-ai', 'node_modules', 'opencode-windows-x64-baseline', 'bin', 'opencode.exe'),
|
|
22
|
+
join(process.env.LOCALAPPDATA || '', 'Programs', 'opencode', 'opencode.exe'),
|
|
23
|
+
];
|
|
24
|
+
for (const p of candidates) {
|
|
25
|
+
if (existsSync(p)) return p;
|
|
26
|
+
}
|
|
27
|
+
// Fallback: let shell resolve from PATH
|
|
28
|
+
return 'opencode';
|
|
29
|
+
}
|
|
30
|
+
// Linux/Mac: check common locations
|
|
31
|
+
const candidates = [
|
|
32
|
+
'/opt/homebrew/bin/opencode', // Mac Homebrew (Apple Silicon)
|
|
33
|
+
'/usr/local/bin/opencode', // Mac Homebrew (Intel) / Linux
|
|
34
|
+
join(process.env.HOME || '', '.local', 'bin', 'opencode'), // Linux common
|
|
35
|
+
];
|
|
36
|
+
for (const p of candidates) {
|
|
37
|
+
if (existsSync(p)) return p;
|
|
38
|
+
}
|
|
39
|
+
// Fallback: let shell resolve from PATH
|
|
40
|
+
return 'opencode';
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
let globalProxyUrl = null;
|
|
44
|
+
/**
|
|
45
|
+
* Set the global proxy URL.
|
|
46
|
+
*/
|
|
47
|
+
export function setGlobalProxy(url) {
|
|
48
|
+
globalProxyUrl = url;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Get the current proxy URL.
|
|
52
|
+
* Priority: explicitly set > HTTPS_PROXY > HTTP_PROXY > ALL_PROXY
|
|
53
|
+
*/
|
|
54
|
+
export function getProxyUrl() {
|
|
55
|
+
if (globalProxyUrl)
|
|
56
|
+
return globalProxyUrl;
|
|
57
|
+
// Check environment variables in order of priority
|
|
58
|
+
// For HTTPS requests, HTTPS_PROXY takes precedence
|
|
59
|
+
// For HTTP requests, HTTP_PROXY takes precedence
|
|
60
|
+
// ALL_PROXY is a fallback for both
|
|
61
|
+
return (process.env.HTTPS_PROXY ||
|
|
62
|
+
process.env.https_proxy ||
|
|
63
|
+
process.env.HTTP_PROXY ||
|
|
64
|
+
process.env.http_proxy ||
|
|
65
|
+
process.env.ALL_PROXY ||
|
|
66
|
+
process.env.all_proxy ||
|
|
67
|
+
null);
|
|
68
|
+
}
|
|
69
|
+
// Timeout configuration - can be customized via config file or environment variables
|
|
70
|
+
// Default: 30 minutes for request timeout, 1 minute for keep-alive
|
|
71
|
+
const DEFAULT_REQUEST_TIMEOUT_MINUTES = 30;
|
|
72
|
+
const DEFAULT_KEEP_ALIVE_SECONDS = 60;
|
|
73
|
+
/**
|
|
74
|
+
* Read timeout setting from config file
|
|
75
|
+
*/
|
|
76
|
+
function readTimeoutFromConfig() {
|
|
77
|
+
if (!existsSync(CONFIG_FILE))
|
|
78
|
+
return null;
|
|
79
|
+
try {
|
|
80
|
+
const content = readFileSync(CONFIG_FILE, 'utf-8');
|
|
81
|
+
const match = content.match(/OPENCODE_REQUEST_TIMEOUT_MINUTES=(\d+)/);
|
|
82
|
+
if (match) {
|
|
83
|
+
return parseInt(match[1], 10);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
// Ignore read errors
|
|
88
|
+
}
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Get request timeout in milliseconds.
|
|
93
|
+
* Priority: environment variable > config file > default
|
|
94
|
+
* Default: 30 minutes
|
|
95
|
+
*/
|
|
96
|
+
function getRequestTimeoutMs() {
|
|
97
|
+
// First check environment variable
|
|
98
|
+
if (process.env.OPENCODE_REQUEST_TIMEOUT_MINUTES) {
|
|
99
|
+
const minutes = parseInt(process.env.OPENCODE_REQUEST_TIMEOUT_MINUTES, 10);
|
|
100
|
+
if (!isNaN(minutes) && minutes > 0) {
|
|
101
|
+
return minutes * 60 * 1000;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
// Then check config file
|
|
105
|
+
const configValue = readTimeoutFromConfig();
|
|
106
|
+
if (configValue !== null && configValue > 0) {
|
|
107
|
+
return configValue * 60 * 1000;
|
|
108
|
+
}
|
|
109
|
+
// Fall back to default
|
|
110
|
+
return DEFAULT_REQUEST_TIMEOUT_MINUTES * 60 * 1000;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Get keep-alive timeout in milliseconds.
|
|
114
|
+
* Set via OPENCODE_KEEP_ALIVE_SECONDS environment variable.
|
|
115
|
+
* Default: 60 seconds
|
|
116
|
+
*/
|
|
117
|
+
function getKeepAliveMs() {
|
|
118
|
+
const seconds = parseInt(process.env.OPENCODE_KEEP_ALIVE_SECONDS || String(DEFAULT_KEEP_ALIVE_SECONDS), 10);
|
|
119
|
+
return seconds * 1000;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Configure undici global dispatcher with proper timeouts.
|
|
123
|
+
* This fixes the default 5-minute timeout issue.
|
|
124
|
+
* Must be called before any fetch requests are made.
|
|
125
|
+
*/
|
|
126
|
+
async function configureGlobalDispatcher() {
|
|
127
|
+
const { setGlobalDispatcher, Agent, ProxyAgent } = await import('undici');
|
|
128
|
+
const proxyUrl = getProxyUrl();
|
|
129
|
+
const requestTimeoutMs = getRequestTimeoutMs();
|
|
130
|
+
const keepAliveMs = getKeepAliveMs();
|
|
131
|
+
if (proxyUrl) {
|
|
132
|
+
// Use ProxyAgent for proxy connections
|
|
133
|
+
const proxyAgent = new ProxyAgent({
|
|
134
|
+
uri: proxyUrl,
|
|
135
|
+
requestTls: {
|
|
136
|
+
timeout: requestTimeoutMs,
|
|
137
|
+
},
|
|
138
|
+
});
|
|
139
|
+
setGlobalDispatcher(proxyAgent);
|
|
140
|
+
console.log(`â
Proxy agent initialized (timeout: ${requestTimeoutMs / 60000}min)`);
|
|
141
|
+
}
|
|
142
|
+
else {
|
|
143
|
+
// Use regular Agent with extended timeouts
|
|
144
|
+
const agent = new Agent({
|
|
145
|
+
headersTimeout: requestTimeoutMs,
|
|
146
|
+
bodyTimeout: requestTimeoutMs,
|
|
147
|
+
keepAliveTimeout: keepAliveMs,
|
|
148
|
+
keepAliveMaxTimeout: requestTimeoutMs,
|
|
149
|
+
});
|
|
150
|
+
setGlobalDispatcher(agent);
|
|
151
|
+
console.log(`â
HTTP agent initialized (timeout: ${requestTimeoutMs / 60000}min)`);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
// Track if dispatcher has been configured
|
|
155
|
+
let dispatcherConfigured = false;
|
|
156
|
+
/**
|
|
157
|
+
* Initialize fetch with proper timeouts and proxy configuration.
|
|
158
|
+
* This is now async and must be awaited.
|
|
159
|
+
* Call this before making any fetch requests if you need proxy support.
|
|
160
|
+
*/
|
|
161
|
+
export async function initFetchConfig() {
|
|
162
|
+
if (dispatcherConfigured)
|
|
163
|
+
return;
|
|
164
|
+
try {
|
|
165
|
+
await configureGlobalDispatcher();
|
|
166
|
+
dispatcherConfigured = true;
|
|
167
|
+
}
|
|
168
|
+
catch (err) {
|
|
169
|
+
console.warn('â ī¸ Failed to configure HTTP dispatcher:', err);
|
|
170
|
+
// Continue anyway - default timeouts will be used
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
let opencodeInstance = null;
|
|
174
|
+
let opencodeServer = null;
|
|
175
|
+
const PORTS_TO_TRY = [4096, 4097, 4098];
|
|
176
|
+
|
|
177
|
+
// TCP-level port probe: true = occupied, false = free
|
|
178
|
+
function probeTCP(port, timeoutMs = 2000) {
|
|
179
|
+
return new Promise((resolve) => {
|
|
180
|
+
const socket = new Socket();
|
|
181
|
+
socket.setTimeout(timeoutMs);
|
|
182
|
+
socket.on('connect', () => { socket.destroy(); resolve(true); });
|
|
183
|
+
socket.on('timeout', () => { socket.destroy(); resolve(true); });
|
|
184
|
+
socket.on('error', () => { socket.destroy(); resolve(false); });
|
|
185
|
+
socket.connect(port, '127.0.0.1');
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
async function tryConnectPort(port, timeoutMs = 5000) {
|
|
190
|
+
const { createOpencodeClient } = await import('@opencode-ai/sdk');
|
|
191
|
+
const client = createOpencodeClient({ baseUrl: `http://localhost:${port}` });
|
|
192
|
+
const result = await Promise.race([
|
|
193
|
+
client.session.list(),
|
|
194
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error('timeout')), timeoutMs))
|
|
195
|
+
]);
|
|
196
|
+
if (result.error) return null;
|
|
197
|
+
return { client };
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export async function initOpenCode() {
|
|
201
|
+
await initFetchConfig();
|
|
202
|
+
if (opencodeInstance) {
|
|
203
|
+
return opencodeInstance;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Try to connect to existing OpenCode server (try multiple ports)
|
|
207
|
+
for (const port of PORTS_TO_TRY) {
|
|
208
|
+
// Quick TCP probe first - avoid hanging on dead processes
|
|
209
|
+
const occupied = await probeTCP(port, 1000);
|
|
210
|
+
if (occupied) {
|
|
211
|
+
try {
|
|
212
|
+
const result = await tryConnectPort(port);
|
|
213
|
+
if (result) {
|
|
214
|
+
console.log(`â
Connected to existing OpenCode server (localhost:${port})`);
|
|
215
|
+
opencodeInstance = { client: result.client, server: null };
|
|
216
|
+
return opencodeInstance;
|
|
217
|
+
}
|
|
218
|
+
} catch { /* not opencode server */ }
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Auto-start OpenCode server (try ports in sequence)
|
|
223
|
+
if (!opencodeServer) {
|
|
224
|
+
const exePath = findOpenCodeExe();
|
|
225
|
+
const isWindows = platform() === 'win32';
|
|
226
|
+
const useShell = !isWindows || !existsSync(exePath);
|
|
227
|
+
let started = false;
|
|
228
|
+
|
|
229
|
+
for (const port of PORTS_TO_TRY) {
|
|
230
|
+
const occupied = await probeTCP(port, 500);
|
|
231
|
+
if (occupied) {
|
|
232
|
+
console.log(`â ī¸ Port ${port} occupied, trying next...`);
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
console.log(`đ Starting OpenCode server on port ${port}...`);
|
|
237
|
+
opencodeServer = spawn(exePath, ['serve', `--hostname=127.0.0.1`, `--port=${port}`], {
|
|
238
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
239
|
+
env: { ...process.env },
|
|
240
|
+
shell: useShell,
|
|
241
|
+
windowsHide: isWindows,
|
|
242
|
+
});
|
|
243
|
+
opencodeServer.stdout.on('data', (d) => {
|
|
244
|
+
const msg = d.toString().trim();
|
|
245
|
+
if (msg) console.log(`[opencode] ${msg}`);
|
|
246
|
+
});
|
|
247
|
+
opencodeServer.stderr.on('data', (d) => {
|
|
248
|
+
const msg = d.toString().trim();
|
|
249
|
+
if (msg && !msg.includes('DEP0040') && !msg.includes('DEP0190')) console.error(`[opencode] ${msg}`);
|
|
250
|
+
});
|
|
251
|
+
opencodeServer.on('exit', (code) => console.log(`[opencode] exited with code ${code}`));
|
|
252
|
+
|
|
253
|
+
// Wait for server to be ready
|
|
254
|
+
const { createOpencodeClient } = await import('@opencode-ai/sdk');
|
|
255
|
+
for (let i = 0; i < 15; i++) {
|
|
256
|
+
await new Promise(r => setTimeout(r, 1000));
|
|
257
|
+
try {
|
|
258
|
+
const client = createOpencodeClient({ baseUrl: `http://localhost:${port}` });
|
|
259
|
+
const r = await client.session.list();
|
|
260
|
+
if (!r.error) {
|
|
261
|
+
console.log(`â
OpenCode server ready (localhost:${port})`);
|
|
262
|
+
opencodeInstance = { client, server: opencodeServer };
|
|
263
|
+
started = true;
|
|
264
|
+
return opencodeInstance;
|
|
265
|
+
}
|
|
266
|
+
} catch { /* not ready yet */ }
|
|
267
|
+
}
|
|
268
|
+
// Server didn't start on this port, kill and try next
|
|
269
|
+
try { opencodeServer.kill(); } catch {}
|
|
270
|
+
opencodeServer = null;
|
|
271
|
+
console.log(`â ī¸ OpenCode server failed to start on port ${port}, trying next...`);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
if (!started) {
|
|
275
|
+
console.error('â OpenCode server did not start on any port');
|
|
276
|
+
return null;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
return null;
|
|
280
|
+
}
|
|
281
|
+
export async function verifyOpenCodeInstalled() {
|
|
282
|
+
return new Promise((resolve) => {
|
|
283
|
+
const isWindows = platform() === 'win32';
|
|
284
|
+
const command = isWindows ? 'where' : 'which';
|
|
285
|
+
const proc = spawn(command, ['opencode'], { shell: isWindows });
|
|
286
|
+
let output = '';
|
|
287
|
+
let errorOutput = '';
|
|
288
|
+
proc.stdout?.on('data', (chunk) => {
|
|
289
|
+
output += chunk.toString();
|
|
290
|
+
});
|
|
291
|
+
proc.stderr?.on('data', (chunk) => {
|
|
292
|
+
errorOutput += chunk.toString();
|
|
293
|
+
});
|
|
294
|
+
proc.on('close', (code) => {
|
|
295
|
+
if (code === 0 && output.trim()) {
|
|
296
|
+
resolve({ ok: true });
|
|
297
|
+
}
|
|
298
|
+
else {
|
|
299
|
+
resolve({
|
|
300
|
+
ok: false,
|
|
301
|
+
error: `OpenCode not found in PATH. Please install it first:\n npm install -g @opencode-ai/opencode\n\nThen verify with:\n opencode --version`
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
});
|
|
305
|
+
proc.on('error', (err) => {
|
|
306
|
+
resolve({
|
|
307
|
+
ok: false,
|
|
308
|
+
error: `Failed to check OpenCode installation: ${err.message}\n\nPlease ensure OpenCode is installed:\n npm install -g @opencode-ai/opencode`
|
|
309
|
+
});
|
|
310
|
+
});
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
export async function createSession(_threadId, title = `Remote control session`) {
|
|
314
|
+
const opencode = await initOpenCode();
|
|
315
|
+
try {
|
|
316
|
+
const createResult = await opencode.client.session.create({
|
|
317
|
+
body: { title },
|
|
318
|
+
});
|
|
319
|
+
if (createResult.error) {
|
|
320
|
+
console.error('Failed to create session:', createResult.error);
|
|
321
|
+
return null;
|
|
322
|
+
}
|
|
323
|
+
const sessionId = createResult.data.id;
|
|
324
|
+
console.log(`â
Created OpenCode session: ${sessionId}`);
|
|
325
|
+
let shareUrl;
|
|
326
|
+
if (process.env.SHARE_SESSIONS === 'true') {
|
|
327
|
+
const shareResult = await opencode.client.session.share({
|
|
328
|
+
path: { id: sessionId }
|
|
329
|
+
});
|
|
330
|
+
if (!shareResult.error && shareResult.data?.share?.url) {
|
|
331
|
+
shareUrl = shareResult.data.share.url;
|
|
332
|
+
console.log(`đ Session shared: ${shareUrl}`);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
return {
|
|
336
|
+
sessionId,
|
|
337
|
+
client: opencode.client,
|
|
338
|
+
server: opencode.server,
|
|
339
|
+
shareUrl,
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
catch (error) {
|
|
343
|
+
console.error('Error creating session:', error);
|
|
344
|
+
return null;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
// Send message - use promptAsync then poll for response
|
|
348
|
+
export async function sendMessage(session, message, callbacks) {
|
|
349
|
+
const TIMEOUT_MS = 5 * 60 * 1000; // 5 minute timeout
|
|
350
|
+
const POLL_INTERVAL = 2000; // 2 seconds between polls
|
|
351
|
+
|
|
352
|
+
try {
|
|
353
|
+
// Verify session is valid first
|
|
354
|
+
try {
|
|
355
|
+
const sessionCheck = await session.client.session.get({ path: { id: session.sessionId } });
|
|
356
|
+
if (sessionCheck.error) {
|
|
357
|
+
console.error('[sendMessage] Session error:', sessionCheck.error);
|
|
358
|
+
return 'â äŧ蝿 æīŧ蝎åé /restart éå¯';
|
|
359
|
+
}
|
|
360
|
+
} catch (e) {
|
|
361
|
+
console.error('[sendMessage] Session check failed:', e.message);
|
|
362
|
+
return 'â äŧč¯čŋæĨå¤ąč´Ĩīŧ蝎åé /restart éå¯';
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// Get last message ID and count before sending
|
|
366
|
+
let lastMsgId = null;
|
|
367
|
+
let msgCountBefore = 0;
|
|
368
|
+
try {
|
|
369
|
+
const msgsBefore = await session.client.session.messages({ path: { id: session.sessionId } });
|
|
370
|
+
if (msgsBefore.data?.length > 0) {
|
|
371
|
+
lastMsgId = msgsBefore.data[msgsBefore.data.length - 1].info?.id;
|
|
372
|
+
msgCountBefore = msgsBefore.data.length;
|
|
373
|
+
}
|
|
374
|
+
} catch { /* ignore */ }
|
|
375
|
+
|
|
376
|
+
// Send message using promptAsync (non-blocking)
|
|
377
|
+
const promptBody = {
|
|
378
|
+
parts: [{ type: 'text', text: message }]
|
|
379
|
+
};
|
|
380
|
+
// Per-message model override if set on session
|
|
381
|
+
if (session.model?.providerID && session.model?.modelID) {
|
|
382
|
+
promptBody.model = {
|
|
383
|
+
providerID: session.model.providerID,
|
|
384
|
+
modelID: session.model.modelID,
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
const sendResult = await session.client.session.promptAsync({
|
|
388
|
+
path: { id: session.sessionId },
|
|
389
|
+
body: promptBody,
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
// Poll for new response - multi-turn: continue until truly idle
|
|
393
|
+
const startTime = Date.now();
|
|
394
|
+
let responseText = '';
|
|
395
|
+
let hasToolActivity = false;
|
|
396
|
+
let lastStatus = '';
|
|
397
|
+
let idleCycles = 0;
|
|
398
|
+
const IDLE_THRESHOLD = 3; // Exit after 3 idle polls (no new content, not processing)
|
|
399
|
+
|
|
400
|
+
while (Date.now() - startTime < TIMEOUT_MS) {
|
|
401
|
+
await new Promise(r => setTimeout(r, POLL_INTERVAL));
|
|
402
|
+
|
|
403
|
+
try {
|
|
404
|
+
const msgsResult = await session.client.session.messages({
|
|
405
|
+
path: { id: session.sessionId }
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
if (msgsResult.error) {
|
|
409
|
+
console.error('[sendMessage] Messages error:', msgsResult.error);
|
|
410
|
+
break;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
if (!msgsResult.data?.length) {
|
|
414
|
+
continue;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
const messages = msgsResult.data;
|
|
418
|
+
const newMsgCount = messages.length;
|
|
419
|
+
|
|
420
|
+
// Check session status
|
|
421
|
+
const latestMsg = messages[messages.length - 1];
|
|
422
|
+
const currentStatus = latestMsg?.info?.status;
|
|
423
|
+
if (currentStatus !== lastStatus) {
|
|
424
|
+
lastStatus = currentStatus;
|
|
425
|
+
if (lastStatus) {
|
|
426
|
+
console.log(`[sendMessage] Session status: ${lastStatus}`);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// If actively processing, reset idle counter and wait
|
|
431
|
+
if (lastStatus === 'pending_tool' || lastStatus === 'thinking') {
|
|
432
|
+
idleCycles = 0;
|
|
433
|
+
continue;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// Check if there was tool activity and notify via callback
|
|
437
|
+
if (newMsgCount > msgCountBefore) {
|
|
438
|
+
for (let i = msgCountBefore; i < newMsgCount; i++) {
|
|
439
|
+
const msg = messages[i];
|
|
440
|
+
if (msg.parts) {
|
|
441
|
+
for (const part of msg.parts) {
|
|
442
|
+
if (part.type === 'tool_use' || part.type === 'tool_result') {
|
|
443
|
+
hasToolActivity = true;
|
|
444
|
+
callbacks?.onEvent?.({
|
|
445
|
+
type: 'tool.call',
|
|
446
|
+
properties: {
|
|
447
|
+
name: part.name || part.tool_name || 'unknown',
|
|
448
|
+
input: part.input || {}
|
|
449
|
+
}
|
|
450
|
+
});
|
|
451
|
+
break;
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
if (hasToolActivity) break;
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// Find messages after our last message
|
|
460
|
+
let startIdx = 0;
|
|
461
|
+
if (lastMsgId) {
|
|
462
|
+
const idx = messages.findIndex(m => m.info?.id === lastMsgId);
|
|
463
|
+
if (idx >= 0) startIdx = idx + 1;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// Collect the latest assistant response text
|
|
467
|
+
let newText = '';
|
|
468
|
+
for (let i = messages.length - 1; i >= startIdx; i--) {
|
|
469
|
+
const msg = messages[i];
|
|
470
|
+
if (msg.info?.role === 'assistant') {
|
|
471
|
+
const textParts = msg.parts
|
|
472
|
+
?.filter(p => p.type === 'text' && p.text)
|
|
473
|
+
.map(p => p.text) || [];
|
|
474
|
+
|
|
475
|
+
if (textParts.length > 0) {
|
|
476
|
+
newText = textParts.join('\n');
|
|
477
|
+
break;
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
if (newText && newText !== responseText) {
|
|
483
|
+
const delta = newText.slice(responseText.length);
|
|
484
|
+
responseText = newText;
|
|
485
|
+
callbacks?.onTextDelta?.(delta);
|
|
486
|
+
idleCycles = 0;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// Exit only when truly idle: have response, not processing, no new text for N cycles
|
|
490
|
+
if (responseText && lastStatus !== 'pending_tool' && lastStatus !== 'thinking') {
|
|
491
|
+
idleCycles++;
|
|
492
|
+
if (idleCycles >= IDLE_THRESHOLD) {
|
|
493
|
+
break;
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
} catch (e) {
|
|
497
|
+
console.warn('Poll error:', e.message);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
if (!responseText) {
|
|
502
|
+
console.warn('â° Timeout waiting for response, status:', lastStatus);
|
|
503
|
+
// Try one more time with a fresh message query
|
|
504
|
+
try {
|
|
505
|
+
const finalMsgs = await session.client.session.messages({ path: { id: session.sessionId }, query: { limit: 50 } });
|
|
506
|
+
if (finalMsgs.data?.length) {
|
|
507
|
+
for (let i = finalMsgs.data.length - 1; i >= 0; i--) {
|
|
508
|
+
const msg = finalMsgs.data[i];
|
|
509
|
+
if (msg.info?.role === 'assistant' && msg.parts) {
|
|
510
|
+
const textParts = msg.parts.filter(p => p.type === 'text' && p.text).map(p => p.text);
|
|
511
|
+
if (textParts.length > 0) {
|
|
512
|
+
responseText = textParts.join('\n');
|
|
513
|
+
break;
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
} catch { /* ignore */ }
|
|
519
|
+
|
|
520
|
+
if (!responseText) {
|
|
521
|
+
return 'â° č¯ˇæąčļ
æļīŧ蝎éč¯';
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
callbacks?.onStatusChange?.({ type: 'idle', hasToolActivity });
|
|
526
|
+
console.log(`đŦ Response: ${responseText.slice(0, 100)}...`);
|
|
527
|
+
return responseText;
|
|
528
|
+
}
|
|
529
|
+
catch (error) {
|
|
530
|
+
console.error('Error sending message:', error);
|
|
531
|
+
return `â Error: ${error instanceof Error ? error.message : 'Unknown error'}`;
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
export async function getSession(session) {
|
|
535
|
+
try {
|
|
536
|
+
const result = await session.client.session.get({
|
|
537
|
+
path: { id: session.sessionId }
|
|
538
|
+
});
|
|
539
|
+
if (result.error) {
|
|
540
|
+
return null;
|
|
541
|
+
}
|
|
542
|
+
return result.data;
|
|
543
|
+
}
|
|
544
|
+
catch {
|
|
545
|
+
return null;
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
export async function shareSession(session) {
|
|
549
|
+
try {
|
|
550
|
+
const result = await session.client.session.share({
|
|
551
|
+
path: { id: session.sessionId }
|
|
552
|
+
});
|
|
553
|
+
if (result.error || !result.data?.share?.url) {
|
|
554
|
+
return null;
|
|
555
|
+
}
|
|
556
|
+
return result.data.share.url;
|
|
557
|
+
}
|
|
558
|
+
catch {
|
|
559
|
+
return null;
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
export function getOpenCode() {
|
|
563
|
+
return opencodeInstance;
|
|
564
|
+
}
|
|
565
|
+
export async function checkConnection() {
|
|
566
|
+
try {
|
|
567
|
+
const opencode = await initOpenCode();
|
|
568
|
+
return !!opencode?.client;
|
|
569
|
+
}
|
|
570
|
+
catch {
|
|
571
|
+
return false;
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
export async function abortSession(session) {
|
|
575
|
+
try {
|
|
576
|
+
await session.client.session.abort({
|
|
577
|
+
path: { id: session.sessionId }
|
|
578
|
+
});
|
|
579
|
+
console.log(`đ Aborted session: ${session.sessionId}`);
|
|
580
|
+
return true;
|
|
581
|
+
}
|
|
582
|
+
catch (error) {
|
|
583
|
+
console.error('Failed to abort session:', error.message);
|
|
584
|
+
return false;
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
export async function getSessionMessages(session, limit = 20) {
|
|
588
|
+
try {
|
|
589
|
+
const result = await session.client.session.messages({
|
|
590
|
+
path: { id: session.sessionId }
|
|
591
|
+
});
|
|
592
|
+
if (result.error) {
|
|
593
|
+
return null;
|
|
594
|
+
}
|
|
595
|
+
const messages = result.data || [];
|
|
596
|
+
return messages.slice(-limit);
|
|
597
|
+
}
|
|
598
|
+
catch {
|
|
599
|
+
return null;
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
export async function resumeSession(sessionId, title = 'Resumed session') {
|
|
603
|
+
try {
|
|
604
|
+
const opencode = await initOpenCode();
|
|
605
|
+
if (!opencode) return null;
|
|
606
|
+
const getResult = await opencode.client.session.get({ path: { id: sessionId } });
|
|
607
|
+
if (getResult.error) {
|
|
608
|
+
console.warn(`Session ${sessionId} not found`);
|
|
609
|
+
return null;
|
|
610
|
+
}
|
|
611
|
+
console.log(`â
Resumed OpenCode session: ${sessionId}`);
|
|
612
|
+
return { sessionId, client: opencode.client, server: opencode.server, shareUrl: undefined };
|
|
613
|
+
}
|
|
614
|
+
catch (error) {
|
|
615
|
+
console.error('Error resuming session:', error.message);
|
|
616
|
+
return null;
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
export async function listOpenCodeSessions() {
|
|
620
|
+
try {
|
|
621
|
+
const opencode = await initOpenCode();
|
|
622
|
+
if (!opencode) return [];
|
|
623
|
+
const result = await opencode.client.session.list();
|
|
624
|
+
if (result.error) {
|
|
625
|
+
return [];
|
|
626
|
+
}
|
|
627
|
+
const sessions = result.data || [];
|
|
628
|
+
return sessions.map(s => ({
|
|
629
|
+
id: s.id,
|
|
630
|
+
title: s.title || 'Untitled',
|
|
631
|
+
status: s.status?.type || 'unknown',
|
|
632
|
+
directory: s.directory || '',
|
|
633
|
+
createdAt: s.created_at || 0,
|
|
634
|
+
lastActivity: s.updated_at || 0,
|
|
635
|
+
}));
|
|
636
|
+
}
|
|
637
|
+
catch (error) {
|
|
638
|
+
console.error('Failed to list OpenCode sessions:', error.message);
|
|
639
|
+
return [];
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
export async function listOpenCodeSessionsFromServer(baseUrl) {
|
|
643
|
+
try {
|
|
644
|
+
const { createOpencodeClient } = await import('@opencode-ai/sdk');
|
|
645
|
+
const client = createOpencodeClient({
|
|
646
|
+
baseUrl: baseUrl || 'http://localhost:4096',
|
|
647
|
+
});
|
|
648
|
+
const result = await client.session.list();
|
|
649
|
+
if (result.error) {
|
|
650
|
+
return [];
|
|
651
|
+
}
|
|
652
|
+
const sessions = result.data || [];
|
|
653
|
+
return sessions.map(s => ({
|
|
654
|
+
id: s.id,
|
|
655
|
+
title: s.title || 'Untitled',
|
|
656
|
+
status: s.status?.type || 'unknown',
|
|
657
|
+
directory: s.directory || '',
|
|
658
|
+
createdAt: s.created_at || 0,
|
|
659
|
+
lastActivity: s.updated_at || 0,
|
|
660
|
+
}));
|
|
661
|
+
}
|
|
662
|
+
catch (error) {
|
|
663
|
+
console.error('Failed to list OpenCode sessions from server:', error.message);
|
|
664
|
+
return [];
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
export async function createOpenCodeSession(title = 'New session') {
|
|
668
|
+
try {
|
|
669
|
+
const opencode = await initOpenCode();
|
|
670
|
+
if (!opencode) return null;
|
|
671
|
+
const result = await opencode.client.session.create({
|
|
672
|
+
body: { title }
|
|
673
|
+
});
|
|
674
|
+
if (result.error) {
|
|
675
|
+
return null;
|
|
676
|
+
}
|
|
677
|
+
const session = {
|
|
678
|
+
sessionId: result.data.id,
|
|
679
|
+
client: opencode.client,
|
|
680
|
+
server: opencode.server,
|
|
681
|
+
shareUrl: undefined,
|
|
682
|
+
};
|
|
683
|
+
console.log(`â
Created new OpenCode session: ${session.sessionId}`);
|
|
684
|
+
return session;
|
|
685
|
+
}
|
|
686
|
+
catch (error) {
|
|
687
|
+
console.error('Failed to create OpenCode session:', error.message);
|
|
688
|
+
return null;
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
export async function deleteOpenCodeSession(sessionId) {
|
|
692
|
+
try {
|
|
693
|
+
const opencode = await initOpenCode();
|
|
694
|
+
if (!opencode) return false;
|
|
695
|
+
const result = await opencode.client.session.delete({
|
|
696
|
+
path: { id: sessionId }
|
|
697
|
+
});
|
|
698
|
+
if (result.error) {
|
|
699
|
+
return false;
|
|
700
|
+
}
|
|
701
|
+
console.log(`đī¸ Deleted OpenCode session: ${sessionId}`);
|
|
702
|
+
return true;
|
|
703
|
+
}
|
|
704
|
+
catch (error) {
|
|
705
|
+
console.error('Failed to delete OpenCode session:', error.message);
|
|
706
|
+
return false;
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
export async function renameOpenCodeSession(session, title) {
|
|
710
|
+
try {
|
|
711
|
+
const result = await session.client.session.patch({
|
|
712
|
+
path: { id: session.sessionId },
|
|
713
|
+
body: { title }
|
|
714
|
+
});
|
|
715
|
+
if (result.error) {
|
|
716
|
+
return false;
|
|
717
|
+
}
|
|
718
|
+
console.log(`đˇī¸ Renamed session to: ${title}`);
|
|
719
|
+
return true;
|
|
720
|
+
}
|
|
721
|
+
catch (error) {
|
|
722
|
+
console.error('Failed to rename session:', error.message);
|
|
723
|
+
return false;
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
export async function forkSession(sessionId, messageID, directory) {
|
|
727
|
+
try {
|
|
728
|
+
const opencode = await initOpenCode();
|
|
729
|
+
if (!opencode) return null;
|
|
730
|
+
const result = await opencode.client.session.fork({
|
|
731
|
+
path: { id: sessionId },
|
|
732
|
+
body: { messageID },
|
|
733
|
+
query: directory ? { directory } : {}
|
|
734
|
+
});
|
|
735
|
+
if (result.error) {
|
|
736
|
+
console.warn(`Fork failed: ${result.error}`);
|
|
737
|
+
return null;
|
|
738
|
+
}
|
|
739
|
+
const newSession = result.data;
|
|
740
|
+
console.log(`đ Forked session ${sessionId.slice(0, 8)}... at message ${messageID} â ${newSession.id.slice(0, 8)}...`);
|
|
741
|
+
return {
|
|
742
|
+
sessionId: newSession.id,
|
|
743
|
+
client: opencode.client,
|
|
744
|
+
server: opencode.server,
|
|
745
|
+
shareUrl: undefined,
|
|
746
|
+
};
|
|
747
|
+
}
|
|
748
|
+
catch (error) {
|
|
749
|
+
console.error('Failed to fork session:', error.message);
|
|
750
|
+
return null;
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
export async function revertSessionMessage(sessionId, messageID, partID) {
|
|
754
|
+
try {
|
|
755
|
+
const opencode = await initOpenCode();
|
|
756
|
+
if (!opencode) return false;
|
|
757
|
+
const result = await opencode.client.session.revert({
|
|
758
|
+
path: { id: sessionId },
|
|
759
|
+
body: { messageID, partID }
|
|
760
|
+
});
|
|
761
|
+
if (result.error) {
|
|
762
|
+
console.warn(`Revert failed: ${result.error}`);
|
|
763
|
+
return false;
|
|
764
|
+
}
|
|
765
|
+
console.log(`âŠī¸ Reverted session ${sessionId.slice(0, 8)}... to message ${messageID}`);
|
|
766
|
+
return true;
|
|
767
|
+
}
|
|
768
|
+
catch (error) {
|
|
769
|
+
console.error('Failed to revert session:', error.message);
|
|
770
|
+
return false;
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
export async function unrevertSession(sessionId) {
|
|
774
|
+
try {
|
|
775
|
+
const opencode = await initOpenCode();
|
|
776
|
+
if (!opencode) return false;
|
|
777
|
+
const result = await opencode.client.session.unrevert({
|
|
778
|
+
path: { id: sessionId }
|
|
779
|
+
});
|
|
780
|
+
if (result.error) {
|
|
781
|
+
console.warn(`Unrevert failed: ${result.error}`);
|
|
782
|
+
return false;
|
|
783
|
+
}
|
|
784
|
+
console.log(`âŠī¸ Unreverted session ${sessionId.slice(0, 8)}...`);
|
|
785
|
+
return true;
|
|
786
|
+
}
|
|
787
|
+
catch (error) {
|
|
788
|
+
console.error('Failed to unrevert session:', error.message);
|
|
789
|
+
return false;
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
export async function listProviders() {
|
|
794
|
+
try {
|
|
795
|
+
const opencode = await initOpenCode();
|
|
796
|
+
if (!opencode) return null;
|
|
797
|
+
const result = await opencode.client.provider.list();
|
|
798
|
+
if (result.error || !result.data?.all) return null;
|
|
799
|
+
return result.data.all;
|
|
800
|
+
} catch (error) {
|
|
801
|
+
console.error('Failed to list providers:', error.message);
|
|
802
|
+
return null;
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
export async function updateGlobalModel(modelStr) {
|
|
807
|
+
try {
|
|
808
|
+
const opencode = await initOpenCode();
|
|
809
|
+
if (!opencode) return false;
|
|
810
|
+
const result = await opencode.client.config.update({
|
|
811
|
+
body: { model: modelStr },
|
|
812
|
+
});
|
|
813
|
+
if (result.error) {
|
|
814
|
+
console.error('Failed to update model:', result.error);
|
|
815
|
+
return false;
|
|
816
|
+
}
|
|
817
|
+
console.log(`â
Global model updated to: ${modelStr}`);
|
|
818
|
+
return true;
|
|
819
|
+
} catch (error) {
|
|
820
|
+
console.error('Failed to update model:', error.message);
|
|
821
|
+
return false;
|
|
822
|
+
}
|
|
823
|
+
}
|