remote-opencode 1.0.8 → 1.0.10

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.
@@ -52,17 +52,34 @@ export function removeProject(alias) {
52
52
  saveData(data);
53
53
  return true;
54
54
  }
55
- export function setChannelBinding(channelId, projectAlias) {
55
+ export function setChannelBinding(channelId, projectAlias, model) {
56
56
  const data = loadData();
57
57
  const existing = data.bindings.findIndex(b => b.channelId === channelId);
58
58
  if (existing >= 0) {
59
59
  data.bindings[existing].projectAlias = projectAlias;
60
+ if (model !== undefined) {
61
+ data.bindings[existing].model = model;
62
+ }
60
63
  }
61
64
  else {
62
- data.bindings.push({ channelId, projectAlias });
65
+ data.bindings.push({ channelId, projectAlias, model });
63
66
  }
64
67
  saveData(data);
65
68
  }
69
+ export function setChannelModel(channelId, model) {
70
+ const data = loadData();
71
+ const existing = data.bindings.findIndex(b => b.channelId === channelId);
72
+ if (existing >= 0) {
73
+ data.bindings[existing].model = model;
74
+ saveData(data);
75
+ return true;
76
+ }
77
+ return false;
78
+ }
79
+ export function getChannelModel(channelId) {
80
+ const binding = loadData().bindings.find(b => b.channelId === channelId);
81
+ return binding?.model;
82
+ }
66
83
  export function getChannelBinding(channelId) {
67
84
  const binding = loadData().bindings.find(b => b.channelId === channelId);
68
85
  return binding?.projectAlias;
@@ -206,3 +223,50 @@ export function getProjectAutoWorktree(alias) {
206
223
  const project = getProject(alias);
207
224
  return project?.autoWorktree ?? false;
208
225
  }
226
+ // Queue Management
227
+ export function getQueue(threadId) {
228
+ const data = loadData();
229
+ return data.queues?.[threadId] ?? [];
230
+ }
231
+ export function addToQueue(threadId, message) {
232
+ const data = loadData();
233
+ if (!data.queues)
234
+ data.queues = {};
235
+ if (!data.queues[threadId])
236
+ data.queues[threadId] = [];
237
+ data.queues[threadId].push(message);
238
+ saveData(data);
239
+ }
240
+ export function popFromQueue(threadId) {
241
+ const data = loadData();
242
+ if (!data.queues?.[threadId] || data.queues[threadId].length === 0)
243
+ return undefined;
244
+ const message = data.queues[threadId].shift();
245
+ saveData(data);
246
+ return message;
247
+ }
248
+ export function clearQueue(threadId) {
249
+ const data = loadData();
250
+ if (data.queues?.[threadId]) {
251
+ delete data.queues[threadId];
252
+ saveData(data);
253
+ }
254
+ }
255
+ export function getQueueSettings(threadId) {
256
+ const data = loadData();
257
+ return data.queueSettings?.[threadId] ?? {
258
+ paused: false,
259
+ continueOnFailure: false,
260
+ freshContext: true
261
+ };
262
+ }
263
+ export function updateQueueSettings(threadId, settings) {
264
+ const data = loadData();
265
+ if (!data.queueSettings)
266
+ data.queueSettings = {};
267
+ data.queueSettings[threadId] = {
268
+ ...getQueueSettings(threadId),
269
+ ...settings
270
+ };
271
+ saveData(data);
272
+ }
@@ -0,0 +1,202 @@
1
+ import { ActionRowBuilder, ButtonBuilder, ButtonStyle, EmbedBuilder } from 'discord.js';
2
+ import * as dataStore from './dataStore.js';
3
+ import * as sessionManager from './sessionManager.js';
4
+ import * as serveManager from './serveManager.js';
5
+ import * as worktreeManager from './worktreeManager.js';
6
+ import { SSEClient } from './sseClient.js';
7
+ import { formatOutput } from '../utils/messageFormatter.js';
8
+ import { processNextInQueue } from './queueManager.js';
9
+ export async function runPrompt(channel, threadId, prompt, parentChannelId) {
10
+ const projectPath = dataStore.getChannelProjectPath(parentChannelId);
11
+ if (!projectPath) {
12
+ await channel.send('❌ No project bound to parent channel.');
13
+ return;
14
+ }
15
+ let worktreeMapping = dataStore.getWorktreeMapping(threadId);
16
+ // Auto-create worktree if enabled and no mapping exists for this thread
17
+ if (!worktreeMapping) {
18
+ const projectAlias = dataStore.getChannelBinding(parentChannelId);
19
+ if (projectAlias && dataStore.getProjectAutoWorktree(projectAlias)) {
20
+ try {
21
+ const branchName = worktreeManager.sanitizeBranchName(`auto/${threadId.slice(0, 8)}-${Date.now()}`);
22
+ const worktreePath = await worktreeManager.createWorktree(projectPath, branchName);
23
+ const newMapping = {
24
+ threadId,
25
+ branchName,
26
+ worktreePath,
27
+ projectPath,
28
+ description: prompt.slice(0, 50) + (prompt.length > 50 ? '...' : ''),
29
+ createdAt: Date.now()
30
+ };
31
+ dataStore.setWorktreeMapping(newMapping);
32
+ worktreeMapping = newMapping;
33
+ const embed = new EmbedBuilder()
34
+ .setTitle(`🌳 Auto-Worktree: ${branchName}`)
35
+ .setDescription('Automatically created for this session')
36
+ .addFields({ name: 'Branch', value: branchName, inline: true }, { name: 'Path', value: worktreePath, inline: true })
37
+ .setColor(0x2ecc71);
38
+ const worktreeButtons = new ActionRowBuilder()
39
+ .addComponents(new ButtonBuilder()
40
+ .setCustomId(`delete_${threadId}`)
41
+ .setLabel('Delete')
42
+ .setStyle(ButtonStyle.Danger), new ButtonBuilder()
43
+ .setCustomId(`pr_${threadId}`)
44
+ .setLabel('Create PR')
45
+ .setStyle(ButtonStyle.Primary));
46
+ await channel.send({ embeds: [embed], components: [worktreeButtons] });
47
+ }
48
+ catch (error) {
49
+ console.error('Auto-worktree creation failed:', error);
50
+ }
51
+ }
52
+ }
53
+ const effectivePath = worktreeMapping?.worktreePath ?? projectPath;
54
+ const preferredModel = dataStore.getChannelModel(parentChannelId);
55
+ const modelDisplay = preferredModel ? `\`${preferredModel}\`` : 'default';
56
+ const buttons = new ActionRowBuilder()
57
+ .addComponents(new ButtonBuilder()
58
+ .setCustomId(`interrupt_${threadId}`)
59
+ .setLabel('⏸️ Interrupt')
60
+ .setStyle(ButtonStyle.Secondary));
61
+ let streamMessage;
62
+ try {
63
+ streamMessage = await channel.send({
64
+ content: `📌 **Prompt**: ${prompt}\n\n🚀 Starting OpenCode server... (Model: ${modelDisplay})`,
65
+ components: [buttons]
66
+ });
67
+ }
68
+ catch {
69
+ return;
70
+ }
71
+ let port;
72
+ let sessionId;
73
+ let updateInterval = null;
74
+ let accumulatedText = '';
75
+ let lastContent = '';
76
+ let tick = 0;
77
+ const spinner = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
78
+ const updateStreamMessage = async (content, components) => {
79
+ try {
80
+ await streamMessage.edit({ content, components });
81
+ }
82
+ catch {
83
+ }
84
+ };
85
+ try {
86
+ port = await serveManager.spawnServe(effectivePath, preferredModel);
87
+ await updateStreamMessage(`📌 **Prompt**: ${prompt}\n\n⏳ Waiting for OpenCode server... (Model: ${modelDisplay})`, [buttons]);
88
+ await serveManager.waitForReady(port, 30000, effectivePath, preferredModel);
89
+ const settings = dataStore.getQueueSettings(threadId);
90
+ // If fresh context is enabled, we always clear the session before starting
91
+ if (settings.freshContext) {
92
+ sessionManager.clearSessionForThread(threadId);
93
+ }
94
+ const existingSession = sessionManager.getSessionForThread(threadId);
95
+ if (existingSession && existingSession.projectPath === effectivePath) {
96
+ const isValid = await sessionManager.validateSession(port, existingSession.sessionId);
97
+ if (isValid) {
98
+ sessionId = existingSession.sessionId;
99
+ sessionManager.updateSessionLastUsed(threadId);
100
+ }
101
+ else {
102
+ sessionId = await sessionManager.createSession(port);
103
+ sessionManager.setSessionForThread(threadId, sessionId, effectivePath, port);
104
+ }
105
+ }
106
+ else {
107
+ sessionId = await sessionManager.createSession(port);
108
+ sessionManager.setSessionForThread(threadId, sessionId, effectivePath, port);
109
+ }
110
+ const sseClient = new SSEClient();
111
+ sseClient.connect(`http://127.0.0.1:${port}`);
112
+ sessionManager.setSseClient(threadId, sseClient);
113
+ sseClient.onPartUpdated((part) => {
114
+ accumulatedText = part.text;
115
+ });
116
+ sseClient.onSessionIdle(() => {
117
+ if (updateInterval) {
118
+ clearInterval(updateInterval);
119
+ updateInterval = null;
120
+ }
121
+ (async () => {
122
+ try {
123
+ const formatted = formatOutput(accumulatedText);
124
+ const disabledButtons = new ActionRowBuilder()
125
+ .addComponents(new ButtonBuilder()
126
+ .setCustomId(`interrupt_${threadId}`)
127
+ .setLabel('⏸️ Interrupt')
128
+ .setStyle(ButtonStyle.Secondary)
129
+ .setDisabled(true));
130
+ await updateStreamMessage(`📌 **Prompt**: ${prompt}\n\n\`\`\`\n${formatted}\n\`\`\``, [disabledButtons]);
131
+ await channel.send({ content: '✅ Done' });
132
+ sseClient.disconnect();
133
+ sessionManager.clearSseClient(threadId);
134
+ // Trigger next in queue
135
+ await processNextInQueue(channel, threadId, parentChannelId);
136
+ }
137
+ catch (error) {
138
+ console.error('Error in onSessionIdle:', error);
139
+ }
140
+ })();
141
+ });
142
+ sseClient.onError((error) => {
143
+ if (updateInterval) {
144
+ clearInterval(updateInterval);
145
+ updateInterval = null;
146
+ }
147
+ (async () => {
148
+ try {
149
+ await updateStreamMessage(`📌 **Prompt**: ${prompt}\n\n❌ Connection error: ${error.message}`, []);
150
+ sseClient.disconnect();
151
+ sessionManager.clearSseClient(threadId);
152
+ const settings = dataStore.getQueueSettings(threadId);
153
+ if (settings.continueOnFailure) {
154
+ await processNextInQueue(channel, threadId, parentChannelId);
155
+ }
156
+ else {
157
+ dataStore.clearQueue(threadId);
158
+ await channel.send('❌ Execution failed. Queue cleared. Use `/queue settings` to change this behavior.');
159
+ }
160
+ }
161
+ catch {
162
+ }
163
+ })();
164
+ });
165
+ updateInterval = setInterval(async () => {
166
+ tick++;
167
+ try {
168
+ const formatted = formatOutput(accumulatedText);
169
+ const spinnerChar = spinner[tick % spinner.length];
170
+ const newContent = formatted || 'Processing...';
171
+ if (newContent !== lastContent || tick % 2 === 0) {
172
+ lastContent = newContent;
173
+ await updateStreamMessage(`📌 **Prompt**: ${prompt}\n\n${spinnerChar} **Running...**\n\`\`\`\n${newContent}\n\`\`\``, [buttons]);
174
+ }
175
+ }
176
+ catch {
177
+ }
178
+ }, 1000);
179
+ await updateStreamMessage(`📌 **Prompt**: ${prompt}\n\n📝 Sending prompt...`, [buttons]);
180
+ await sessionManager.sendPrompt(port, sessionId, prompt, preferredModel);
181
+ }
182
+ catch (error) {
183
+ if (updateInterval) {
184
+ clearInterval(updateInterval);
185
+ }
186
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
187
+ await updateStreamMessage(`📌 **Prompt**: ${prompt}\n\n❌ OpenCode execution failed: ${errorMessage}`, []);
188
+ const client = sessionManager.getSseClient(threadId);
189
+ if (client) {
190
+ client.disconnect();
191
+ sessionManager.clearSseClient(threadId);
192
+ }
193
+ const settings = dataStore.getQueueSettings(threadId);
194
+ if (settings.continueOnFailure) {
195
+ await processNextInQueue(channel, threadId, parentChannelId);
196
+ }
197
+ else {
198
+ dataStore.clearQueue(threadId);
199
+ await channel.send('❌ Execution failed. Queue cleared.');
200
+ }
201
+ }
202
+ }
@@ -0,0 +1,20 @@
1
+ import * as dataStore from './dataStore.js';
2
+ import { runPrompt } from './executionService.js';
3
+ import * as sessionManager from './sessionManager.js';
4
+ export async function processNextInQueue(channel, threadId, parentChannelId) {
5
+ const settings = dataStore.getQueueSettings(threadId);
6
+ if (settings.paused)
7
+ return;
8
+ const next = dataStore.popFromQueue(threadId);
9
+ if (!next)
10
+ return;
11
+ // Visual indication that we are starting the next one
12
+ if ('send' in channel) {
13
+ await channel.send(`🔄 **Queue**: Starting next task...\n> ${next.prompt}`);
14
+ }
15
+ await runPrompt(channel, threadId, next.prompt, parentChannelId);
16
+ }
17
+ export function isBusy(threadId) {
18
+ const sseClient = sessionManager.getSseClient(threadId);
19
+ return !!(sseClient && sseClient.isConnected());
20
+ }
@@ -1,7 +1,8 @@
1
1
  import { spawn } from 'node:child_process';
2
2
  import { Server } from 'node:net';
3
- const PORT_MIN = 14097;
4
- const PORT_MAX = 14200;
3
+ import { getPortConfig } from './configStore.js';
4
+ const DEFAULT_PORT_MIN = 14097;
5
+ const DEFAULT_PORT_MAX = 14200;
5
6
  const instances = new Map();
6
7
  function isPortAvailable(port) {
7
8
  return new Promise((resolve) => {
@@ -14,21 +15,45 @@ function isPortAvailable(port) {
14
15
  resolve(true);
15
16
  });
16
17
  });
17
- server.listen(port);
18
+ // Bind to 127.0.0.1 explicitly to match opencode serve's default binding
19
+ server.listen(port, '127.0.0.1');
18
20
  });
19
21
  }
22
+ async function isOrphanedServerRunning(port) {
23
+ try {
24
+ const response = await fetch(`http://127.0.0.1:${port}/session`, {
25
+ signal: AbortSignal.timeout(1000),
26
+ });
27
+ // If we get any response, there's already a server running
28
+ return true;
29
+ }
30
+ catch {
31
+ return false;
32
+ }
33
+ }
20
34
  async function findAvailablePort() {
21
- for (let port = PORT_MIN; port <= PORT_MAX; port++) {
22
- const usedPorts = new Set(Array.from(instances.values()).map(i => i.port));
23
- if (!usedPorts.has(port) && await isPortAvailable(port)) {
35
+ const config = getPortConfig();
36
+ const min = config?.min ?? DEFAULT_PORT_MIN;
37
+ const max = config?.max ?? DEFAULT_PORT_MAX;
38
+ for (let port = min; port <= max; port++) {
39
+ const usedPorts = new Set(Array.from(instances.values()).filter(i => !i.exited).map(i => i.port));
40
+ if (usedPorts.has(port)) {
41
+ continue;
42
+ }
43
+ // Check if there's an orphaned opencode server on this port
44
+ if (await isOrphanedServerRunning(port)) {
45
+ continue;
46
+ }
47
+ // Check if we can bind to this port
48
+ if (await isPortAvailable(port)) {
24
49
  return port;
25
50
  }
26
51
  }
27
- throw new Error(`No available ports in range ${PORT_MIN}-${PORT_MAX}`);
52
+ throw new Error(`No available ports in range ${min}-${max}`);
28
53
  }
29
54
  async function isServerResponding(port) {
30
55
  try {
31
- const response = await fetch(`http://localhost:${port}/session`, {
56
+ const response = await fetch(`http://127.0.0.1:${port}/session`, {
32
57
  signal: AbortSignal.timeout(2000),
33
58
  });
34
59
  return response.ok;
@@ -37,58 +62,110 @@ async function isServerResponding(port) {
37
62
  return false;
38
63
  }
39
64
  }
40
- function cleanupInstance(projectPath) {
41
- instances.delete(projectPath);
65
+ function cleanupInstance(key) {
66
+ instances.delete(key);
42
67
  }
43
- export async function spawnServe(projectPath) {
44
- const existing = instances.get(projectPath);
45
- if (existing) {
68
+ export async function spawnServe(projectPath, model) {
69
+ const key = model ? `${projectPath}:${model}` : projectPath;
70
+ const existing = instances.get(key);
71
+ if (existing && !existing.exited) {
46
72
  return existing.port;
47
73
  }
74
+ // Clean up any exited instance before spawning a new one
75
+ if (existing?.exited) {
76
+ cleanupInstance(key);
77
+ }
48
78
  const port = await findAvailablePort();
49
- const child = spawn('opencode', ['serve', '--port', port.toString()], {
79
+ // Note: opencode serve doesn't support --model flag
80
+ // Model selection must happen at session/prompt level, not server startup
81
+ const args = ['serve', '--port', port.toString()];
82
+ console.log(`[opencode] Spawning: opencode ${args.join(' ')}`);
83
+ console.log(`[opencode] Working directory: ${projectPath}`);
84
+ const child = spawn('opencode', args, {
50
85
  cwd: projectPath,
51
86
  env: { ...process.env },
52
87
  stdio: ['inherit', 'pipe', 'pipe'],
88
+ shell: true,
53
89
  });
54
- child.stdout?.on('data', () => { });
55
- child.stderr?.on('data', () => { });
56
90
  const instance = {
57
91
  port,
58
92
  process: child,
59
93
  startTime: Date.now(),
94
+ exited: false,
60
95
  };
61
- instances.set(projectPath, instance);
62
- child.on('exit', async () => {
63
- const inst = instances.get(projectPath);
96
+ instances.set(key, instance);
97
+ let stderrBuffer = '';
98
+ let stdoutBuffer = '';
99
+ child.stdout?.on('data', (data) => {
100
+ const text = data.toString();
101
+ stdoutBuffer += text;
102
+ if (stdoutBuffer.length > 2000) {
103
+ stdoutBuffer = stdoutBuffer.slice(-2000);
104
+ }
105
+ console.log(`[opencode stdout] ${text.trim()}`);
106
+ });
107
+ child.stderr?.on('data', (data) => {
108
+ const text = data.toString();
109
+ stderrBuffer += text;
110
+ if (stderrBuffer.length > 2000) {
111
+ stderrBuffer = stderrBuffer.slice(-2000);
112
+ }
113
+ console.error(`[opencode stderr] ${text.trim()}`);
114
+ });
115
+ child.on('exit', (code) => {
116
+ const inst = instances.get(key);
64
117
  if (inst) {
65
- const stillRunning = await isServerResponding(inst.port);
66
- if (!stillRunning) {
67
- cleanupInstance(projectPath);
118
+ inst.exited = true;
119
+ inst.exitCode = code;
120
+ if (code !== 0 && code !== null) {
121
+ // Combine stdout and stderr for error message
122
+ const combinedOutput = (stderrBuffer.trim() || stdoutBuffer.trim());
123
+ inst.exitError = combinedOutput || `Process exited with code ${code}`;
124
+ console.error(`[opencode] Process exited with code ${code}`);
125
+ if (combinedOutput) {
126
+ console.error(`[opencode] Output: ${combinedOutput}`);
127
+ }
68
128
  }
69
129
  }
70
130
  });
71
- child.on('error', () => {
72
- cleanupInstance(projectPath);
131
+ child.on('error', (error) => {
132
+ console.error(`[opencode] Spawn error: ${error.message}`);
133
+ const inst = instances.get(key);
134
+ if (inst) {
135
+ inst.exited = true;
136
+ inst.exitError = error.message || 'Failed to spawn opencode process';
137
+ }
73
138
  });
74
139
  return port;
75
140
  }
76
- export function getPort(projectPath) {
77
- return instances.get(projectPath)?.port;
141
+ export function getPort(projectPath, model) {
142
+ const key = model ? `${projectPath}:${model}` : projectPath;
143
+ return instances.get(key)?.port;
78
144
  }
79
- export function stopServe(projectPath) {
80
- const instance = instances.get(projectPath);
145
+ export function stopServe(projectPath, model) {
146
+ const key = model ? `${projectPath}:${model}` : projectPath;
147
+ const instance = instances.get(key);
81
148
  if (!instance) {
82
149
  return false;
83
150
  }
84
151
  instance.process.kill();
85
- cleanupInstance(projectPath);
152
+ cleanupInstance(key);
86
153
  return true;
87
154
  }
88
- export async function waitForReady(port, timeout = 10000) {
155
+ export async function waitForReady(port, timeout = 30000, projectPath, model) {
89
156
  const start = Date.now();
90
- const url = `http://localhost:${port}/session`;
157
+ const url = `http://127.0.0.1:${port}/session`;
158
+ const key = projectPath ? (model ? `${projectPath}:${model}` : projectPath) : null;
91
159
  while (Date.now() - start < timeout) {
160
+ // Check if the process has exited early
161
+ if (key) {
162
+ const instance = instances.get(key);
163
+ if (instance?.exited) {
164
+ const errorMsg = instance.exitError || `opencode serve exited with code ${instance.exitCode}`;
165
+ cleanupInstance(key);
166
+ throw new Error(`opencode serve failed to start: ${errorMsg}`);
167
+ }
168
+ }
92
169
  try {
93
170
  const response = await fetch(url);
94
171
  if (response.ok) {
@@ -97,19 +174,39 @@ export async function waitForReady(port, timeout = 10000) {
97
174
  }
98
175
  catch {
99
176
  }
100
- await new Promise((resolve) => setTimeout(resolve, 500));
177
+ await new Promise((resolve) => setTimeout(resolve, 1000));
178
+ }
179
+ // Final check - did the process exit?
180
+ if (key) {
181
+ const instance = instances.get(key);
182
+ if (instance?.exited) {
183
+ const errorMsg = instance.exitError || `opencode serve exited with code ${instance.exitCode}`;
184
+ cleanupInstance(key);
185
+ throw new Error(`opencode serve failed to start: ${errorMsg}`);
186
+ }
101
187
  }
102
- throw new Error(`Service at port ${port} failed to become ready within ${timeout}ms`);
188
+ throw new Error(`Service at port ${port} failed to become ready within ${timeout}ms. Check if 'opencode serve' is working correctly.`);
103
189
  }
104
190
  export function stopAll() {
105
- for (const [projectPath, instance] of instances) {
191
+ for (const [key, instance] of instances) {
106
192
  instance.process.kill();
107
- cleanupInstance(projectPath);
193
+ cleanupInstance(key);
108
194
  }
109
195
  }
110
196
  export function getAllInstances() {
111
- return Array.from(instances.entries()).map(([projectPath, instance]) => ({
112
- projectPath,
197
+ return Array.from(instances.entries()).map(([key, instance]) => ({
198
+ key,
113
199
  port: instance.port,
114
200
  }));
115
201
  }
202
+ export function getInstanceState(projectPath, model) {
203
+ const key = model ? `${projectPath}:${model}` : projectPath;
204
+ const instance = instances.get(key);
205
+ if (!instance)
206
+ return undefined;
207
+ return {
208
+ exited: instance.exited ?? false,
209
+ exitCode: instance.exitCode,
210
+ exitError: instance.exitError,
211
+ };
212
+ }
@@ -1,7 +1,7 @@
1
1
  import * as dataStore from './dataStore.js';
2
2
  const threadSseClients = new Map();
3
3
  export async function createSession(port) {
4
- const url = `http://localhost:${port}/session`;
4
+ const url = `http://127.0.0.1:${port}/session`;
5
5
  const response = await fetch(url, {
6
6
  method: 'POST',
7
7
  headers: { 'Content-Type': 'application/json' },
@@ -16,14 +16,31 @@ export async function createSession(port) {
16
16
  }
17
17
  return data.id;
18
18
  }
19
- export async function sendPrompt(port, sessionId, text) {
20
- const url = `http://localhost:${port}/session/${sessionId}/prompt_async`;
19
+ function parseModelString(model) {
20
+ const slashIndex = model.indexOf('/');
21
+ if (slashIndex === -1) {
22
+ return null;
23
+ }
24
+ return {
25
+ providerID: model.slice(0, slashIndex),
26
+ modelID: model.slice(slashIndex + 1),
27
+ };
28
+ }
29
+ export async function sendPrompt(port, sessionId, text, model) {
30
+ const url = `http://127.0.0.1:${port}/session/${sessionId}/prompt_async`;
31
+ const body = {
32
+ parts: [{ type: 'text', text }],
33
+ };
34
+ if (model) {
35
+ const parsedModel = parseModelString(model);
36
+ if (parsedModel) {
37
+ body.model = parsedModel;
38
+ }
39
+ }
21
40
  const response = await fetch(url, {
22
41
  method: 'POST',
23
42
  headers: { 'Content-Type': 'application/json' },
24
- body: JSON.stringify({
25
- parts: [{ type: 'text', text }],
26
- }),
43
+ body: JSON.stringify(body),
27
44
  });
28
45
  if (!response.ok) {
29
46
  throw new Error(`Failed to send prompt: ${response.status} ${response.statusText}`);
@@ -31,7 +48,7 @@ export async function sendPrompt(port, sessionId, text) {
31
48
  }
32
49
  export async function validateSession(port, sessionId) {
33
50
  try {
34
- const url = `http://localhost:${port}/session/${sessionId}`;
51
+ const url = `http://127.0.0.1:${port}/session/${sessionId}`;
35
52
  const response = await fetch(url, {
36
53
  method: 'GET',
37
54
  headers: { 'Content-Type': 'application/json' },
@@ -44,7 +61,7 @@ export async function validateSession(port, sessionId) {
44
61
  }
45
62
  export async function listSessions(port) {
46
63
  try {
47
- const url = `http://localhost:${port}/session`;
64
+ const url = `http://127.0.0.1:${port}/session`;
48
65
  const response = await fetch(url, {
49
66
  method: 'GET',
50
67
  headers: { 'Content-Type': 'application/json' },
@@ -64,7 +81,7 @@ export async function listSessions(port) {
64
81
  }
65
82
  export async function abortSession(port, sessionId) {
66
83
  try {
67
- const url = `http://localhost:${port}/session/${sessionId}/abort`;
84
+ const url = `http://127.0.0.1:${port}/session/${sessionId}/abort`;
68
85
  const response = await fetch(url, {
69
86
  method: 'POST',
70
87
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "remote-opencode",
3
- "version": "1.0.8",
3
+ "version": "1.0.10",
4
4
  "description": "Discord bot for remote OpenCode CLI access",
5
5
  "main": "dist/src/index.js",
6
6
  "bin": {
@@ -15,7 +15,7 @@
15
15
  },
16
16
  "scripts": {
17
17
  "build": "tsc",
18
- "start": "node dist/src/cli.js",
18
+ "start": "node --no-deprecation dist/src/cli.js start",
19
19
  "dev": "node --loader ts-node/esm src/cli.ts",
20
20
  "deploy-commands": "npm run build && node dist/src/cli.js deploy",
21
21
  "test": "vitest",