remote-opencode 1.0.8 โ†’ 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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
  });
@@ -67,6 +67,15 @@ export async function removeWorktree(worktreePath, deleteBranch) {
67
67
  throw new Error(`Failed to remove worktree: ${error.message}`);
68
68
  }
69
69
  }
70
+ export async function getCurrentBranch(cwd) {
71
+ try {
72
+ const { stdout } = await execAsync('git rev-parse --abbrev-ref HEAD', { cwd });
73
+ return stdout.trim() || null;
74
+ }
75
+ catch {
76
+ return null;
77
+ }
78
+ }
70
79
  export function worktreeExists(worktreePath) {
71
80
  return existsSync(worktreePath);
72
81
  }
@@ -54,6 +54,9 @@ export function parseOpenCodeOutput(buffer) {
54
54
  }
55
55
  return result;
56
56
  }
57
+ export function buildContextHeader(branchName, modelName) {
58
+ return `๐ŸŒฟ \`${branchName}\` ยท ๐Ÿค– \`${modelName}\``;
59
+ }
57
60
  export function formatOutput(buffer, maxLength = 1900) {
58
61
  const parsed = parseOpenCodeOutput(buffer);
59
62
  if (!parsed.trim()) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "remote-opencode",
3
- "version": "1.0.8",
3
+ "version": "1.1.1",
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",
@@ -42,6 +42,7 @@
42
42
  "eventsource": "^4.1.0",
43
43
  "node-pty": "^1.1.0",
44
44
  "open": "^10.1.0",
45
+ "opencode-antigravity-auth": "^1.4.6",
45
46
  "picocolors": "^1.1.1",
46
47
  "update-notifier": "^7.3.1"
47
48
  },