@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.
Files changed (102) hide show
  1. package/README.md +82 -0
  2. package/bin/opencode-remote.js +70 -0
  3. package/bin/opencode-weixin.js +10 -0
  4. package/dist/AGENTS.md +20 -0
  5. package/dist/MEMORY.md +21 -0
  6. package/dist/bot-runner.js +180 -0
  7. package/dist/cli.js +256 -0
  8. package/dist/core/approval.js +95 -0
  9. package/dist/core/auth.js +119 -0
  10. package/dist/core/config.js +61 -0
  11. package/dist/core/notifications.js +134 -0
  12. package/dist/core/qiniu.js +267 -0
  13. package/dist/core/registry.js +86 -0
  14. package/dist/core/router.js +344 -0
  15. package/dist/core/session.js +403 -0
  16. package/dist/core/setup.js +418 -0
  17. package/dist/core/types.js +16 -0
  18. package/dist/feishu/adapter.js +72 -0
  19. package/dist/feishu/bot.js +168 -0
  20. package/dist/feishu/commands.js +601 -0
  21. package/dist/feishu/handler.js +380 -0
  22. package/dist/index.js +60 -0
  23. package/dist/opencode/client.js +823 -0
  24. package/dist/package-lock.json +762 -0
  25. package/dist/patch_spawn.js +28 -0
  26. package/dist/plugins/agents/acp/acp-adapter.js +42 -0
  27. package/dist/plugins/agents/claude-code/index.js +69 -0
  28. package/dist/plugins/agents/codex/index.js +44 -0
  29. package/dist/plugins/agents/copilot/index.js +44 -0
  30. package/dist/plugins/agents/opencode/index.js +66 -0
  31. package/dist/telegram/bot.js +288 -0
  32. package/dist/utils/message-split.js +38 -0
  33. package/dist/web/code-viewer.js +266 -0
  34. package/dist/weixin/adapter.js +135 -0
  35. package/dist/weixin/api.js +179 -0
  36. package/dist/weixin/bot.js +183 -0
  37. package/dist/weixin/commands.js +758 -0
  38. package/dist/weixin/handler.js +577 -0
  39. package/dist/weixin/node_modules/encodeurl/LICENSE +22 -0
  40. package/dist/weixin/node_modules/encodeurl/README.md +109 -0
  41. package/dist/weixin/node_modules/encodeurl/index.js +60 -0
  42. package/dist/weixin/node_modules/encodeurl/package.json +40 -0
  43. package/dist/weixin/node_modules/qiniu/.claude/settings.local.json +7 -0
  44. package/dist/weixin/node_modules/qiniu/.github/workflows/ci-test.yml +36 -0
  45. package/dist/weixin/node_modules/qiniu/.github/workflows/npm-publish.yml +20 -0
  46. package/dist/weixin/node_modules/qiniu/.github/workflows/version-check.yml +19 -0
  47. package/dist/weixin/node_modules/qiniu/.idea/MarsCodeWorkspaceAppSettings.xml +7 -0
  48. package/dist/weixin/node_modules/qiniu/.idea/codeStyles/Project.xml +44 -0
  49. package/dist/weixin/node_modules/qiniu/.idea/codeStyles/codeStyleConfig.xml +5 -0
  50. package/dist/weixin/node_modules/qiniu/.idea/git_toolbox_blame.xml +6 -0
  51. package/dist/weixin/node_modules/qiniu/.idea/inspectionProfiles/Project_Default.xml +6 -0
  52. package/dist/weixin/node_modules/qiniu/.idea/jsLibraryMappings.xml +6 -0
  53. package/dist/weixin/node_modules/qiniu/.idea/modules.xml +8 -0
  54. package/dist/weixin/node_modules/qiniu/.idea/nodejs-sdk.iml +12 -0
  55. package/dist/weixin/node_modules/qiniu/.idea/vcs.xml +6 -0
  56. package/dist/weixin/node_modules/qiniu/CHANGELOG.md +292 -0
  57. package/dist/weixin/node_modules/qiniu/README.md +56 -0
  58. package/dist/weixin/node_modules/qiniu/StorageResponseInterface.d.ts +239 -0
  59. package/dist/weixin/node_modules/qiniu/codecov.yml +28 -0
  60. package/dist/weixin/node_modules/qiniu/index.d.ts +1995 -0
  61. package/dist/weixin/node_modules/qiniu/index.js +32 -0
  62. package/dist/weixin/node_modules/qiniu/node_modules/encodeurl/HISTORY.md +14 -0
  63. package/dist/weixin/node_modules/qiniu/node_modules/encodeurl/LICENSE +22 -0
  64. package/dist/weixin/node_modules/qiniu/node_modules/encodeurl/README.md +128 -0
  65. package/dist/weixin/node_modules/qiniu/node_modules/encodeurl/index.js +60 -0
  66. package/dist/weixin/node_modules/qiniu/node_modules/encodeurl/package.json +40 -0
  67. package/dist/weixin/node_modules/qiniu/package.json +80 -0
  68. package/dist/weixin/node_modules/qiniu/qiniu/auth/digest.js +13 -0
  69. package/dist/weixin/node_modules/qiniu/qiniu/cdn.js +149 -0
  70. package/dist/weixin/node_modules/qiniu/qiniu/conf.js +254 -0
  71. package/dist/weixin/node_modules/qiniu/qiniu/fop.js +112 -0
  72. package/dist/weixin/node_modules/qiniu/qiniu/httpc/client.js +253 -0
  73. package/dist/weixin/node_modules/qiniu/qiniu/httpc/endpoint.js +66 -0
  74. package/dist/weixin/node_modules/qiniu/qiniu/httpc/endpointsProvider.js +27 -0
  75. package/dist/weixin/node_modules/qiniu/qiniu/httpc/endpointsRetryPolicy.js +76 -0
  76. package/dist/weixin/node_modules/qiniu/qiniu/httpc/middleware/base.js +31 -0
  77. package/dist/weixin/node_modules/qiniu/qiniu/httpc/middleware/index.js +9 -0
  78. package/dist/weixin/node_modules/qiniu/qiniu/httpc/middleware/qiniuAuth.js +53 -0
  79. package/dist/weixin/node_modules/qiniu/qiniu/httpc/middleware/retryDomains.js +101 -0
  80. package/dist/weixin/node_modules/qiniu/qiniu/httpc/middleware/ua.js +36 -0
  81. package/dist/weixin/node_modules/qiniu/qiniu/httpc/region.js +349 -0
  82. package/dist/weixin/node_modules/qiniu/qiniu/httpc/regionsProvider.js +788 -0
  83. package/dist/weixin/node_modules/qiniu/qiniu/httpc/regionsRetryPolicy.js +242 -0
  84. package/dist/weixin/node_modules/qiniu/qiniu/httpc/responseWrapper.js +40 -0
  85. package/dist/weixin/node_modules/qiniu/qiniu/retry/index.js +4 -0
  86. package/dist/weixin/node_modules/qiniu/qiniu/retry/retrier.js +99 -0
  87. package/dist/weixin/node_modules/qiniu/qiniu/retry/retryPolicy.js +55 -0
  88. package/dist/weixin/node_modules/qiniu/qiniu/rpc.js +237 -0
  89. package/dist/weixin/node_modules/qiniu/qiniu/rtc/app.js +123 -0
  90. package/dist/weixin/node_modules/qiniu/qiniu/rtc/credentials.js +57 -0
  91. package/dist/weixin/node_modules/qiniu/qiniu/rtc/room.js +118 -0
  92. package/dist/weixin/node_modules/qiniu/qiniu/rtc/util.js +16 -0
  93. package/dist/weixin/node_modules/qiniu/qiniu/sms/message.js +58 -0
  94. package/dist/weixin/node_modules/qiniu/qiniu/storage/form.js +442 -0
  95. package/dist/weixin/node_modules/qiniu/qiniu/storage/internal.js +214 -0
  96. package/dist/weixin/node_modules/qiniu/qiniu/storage/resume.js +1272 -0
  97. package/dist/weixin/node_modules/qiniu/qiniu/storage/rs.js +1764 -0
  98. package/dist/weixin/node_modules/qiniu/qiniu/util.js +382 -0
  99. package/dist/weixin/node_modules/qiniu/qiniu/zone.js +230 -0
  100. package/dist/weixin/node_modules/qiniu/tsconfig.json +112 -0
  101. package/dist/weixin/types.js +25 -0
  102. 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
+ }