@viberlabs/opencode 1.1.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/LICENSE.md +25 -0
- package/README.md +178 -0
- package/dist/index.d.ts +56 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +322 -0
- package/dist/index.js.map +1 -0
- package/dist/integration/index.d.ts +6 -0
- package/dist/integration/index.d.ts.map +1 -0
- package/dist/integration/index.js +7 -0
- package/dist/integration/index.js.map +1 -0
- package/package.json +33 -0
- package/src/__tests__/client.test.d.ts +5 -0
- package/src/__tests__/client.test.d.ts.map +1 -0
- package/src/__tests__/client.test.js +155 -0
- package/src/__tests__/client.test.js.map +1 -0
- package/src/__tests__/client.test.ts +169 -0
- package/src/index.d.ts +56 -0
- package/src/index.d.ts.map +1 -0
- package/src/index.js +322 -0
- package/src/index.js.map +1 -0
- package/src/index.ts +353 -0
- package/src/integration/index.d.ts +6 -0
- package/src/integration/index.d.ts.map +1 -0
- package/src/integration/index.js +7 -0
- package/src/integration/index.js.map +1 -0
- package/src/integration/index.ts +6 -0
- package/tsconfig.json +14 -0
- package/vitest.config.ts +20 -0
package/src/index.js
ADDED
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* @viberlabs/opencode
|
|
4
|
+
* OpenCode HTTP client and SSE stream for real-time agent progress
|
|
5
|
+
*/
|
|
6
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
7
|
+
if (k2 === undefined) k2 = k;
|
|
8
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
9
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
10
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
11
|
+
}
|
|
12
|
+
Object.defineProperty(o, k2, desc);
|
|
13
|
+
}) : (function(o, m, k, k2) {
|
|
14
|
+
if (k2 === undefined) k2 = k;
|
|
15
|
+
o[k2] = m[k];
|
|
16
|
+
}));
|
|
17
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
18
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
19
|
+
}) : function(o, v) {
|
|
20
|
+
o["default"] = v;
|
|
21
|
+
});
|
|
22
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
23
|
+
var ownKeys = function(o) {
|
|
24
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
25
|
+
var ar = [];
|
|
26
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
27
|
+
return ar;
|
|
28
|
+
};
|
|
29
|
+
return ownKeys(o);
|
|
30
|
+
};
|
|
31
|
+
return function (mod) {
|
|
32
|
+
if (mod && mod.__esModule) return mod;
|
|
33
|
+
var result = {};
|
|
34
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
35
|
+
__setModuleDefault(result, mod);
|
|
36
|
+
return result;
|
|
37
|
+
};
|
|
38
|
+
})();
|
|
39
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
40
|
+
exports.OpenCodeLogStream = exports.OpenCodeClient = void 0;
|
|
41
|
+
exports.discoverOpenCodePort = discoverOpenCodePort;
|
|
42
|
+
exports.getOpenCodeBaseUrl = getOpenCodeBaseUrl;
|
|
43
|
+
const fs = __importStar(require("fs/promises"));
|
|
44
|
+
const path = __importStar(require("path"));
|
|
45
|
+
const EventSource = require("eventsource");
|
|
46
|
+
// ============================================================================
|
|
47
|
+
// Port Discovery
|
|
48
|
+
// ============================================================================
|
|
49
|
+
const COMMON_PORTS = [51262, 4096, 3000, 8080];
|
|
50
|
+
async function testPort(port) {
|
|
51
|
+
try {
|
|
52
|
+
const controller = new AbortController();
|
|
53
|
+
const timeoutId = setTimeout(() => controller.abort(), 2000);
|
|
54
|
+
const response = await fetch(`http://127.0.0.1:${port}/global/health`, {
|
|
55
|
+
signal: controller.signal,
|
|
56
|
+
});
|
|
57
|
+
clearTimeout(timeoutId);
|
|
58
|
+
// Some OpenCode servers protect /global/health behind Basic Auth.
|
|
59
|
+
// Treat 401/403 as a positive signal that a server is present.
|
|
60
|
+
if (response.ok)
|
|
61
|
+
return true;
|
|
62
|
+
if (response.status === 401 || response.status === 403)
|
|
63
|
+
return true;
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
async function discoverOpenCodePort() {
|
|
71
|
+
for (const port of COMMON_PORTS) {
|
|
72
|
+
if (await testPort(port)) {
|
|
73
|
+
console.log(`[OpenCode] Found server on port ${port}`);
|
|
74
|
+
return port;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
async function getOpenCodeBaseUrl(explicitPort) {
|
|
80
|
+
if (explicitPort) {
|
|
81
|
+
return `http://127.0.0.1:${explicitPort}`;
|
|
82
|
+
}
|
|
83
|
+
const envPort = process.env["OPENCODE_SERVER_PORT"];
|
|
84
|
+
if (envPort) {
|
|
85
|
+
const port = parseInt(envPort, 10);
|
|
86
|
+
if (!isNaN(port))
|
|
87
|
+
return `http://127.0.0.1:${port}`;
|
|
88
|
+
}
|
|
89
|
+
const discovered = await discoverOpenCodePort();
|
|
90
|
+
if (discovered)
|
|
91
|
+
return `http://127.0.0.1:${discovered}`;
|
|
92
|
+
return "http://127.0.0.1:51262";
|
|
93
|
+
}
|
|
94
|
+
// ============================================================================
|
|
95
|
+
// Credentials Resolution
|
|
96
|
+
// ============================================================================
|
|
97
|
+
function getViberDir() {
|
|
98
|
+
return process.env["VIBER_DIR"] || path.join(process.cwd(), ".viber");
|
|
99
|
+
}
|
|
100
|
+
async function loadCredentialsFromFile() {
|
|
101
|
+
try {
|
|
102
|
+
const viberDir = getViberDir();
|
|
103
|
+
const credsFile = path.join(viberDir, "opencode-auth.json");
|
|
104
|
+
const data = await fs.readFile(credsFile, "utf8");
|
|
105
|
+
const creds = JSON.parse(data);
|
|
106
|
+
if (creds.username && creds.password) {
|
|
107
|
+
console.log(`[OpenCode] Loaded credentials from ${credsFile}`);
|
|
108
|
+
return creds;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
catch (e) {
|
|
112
|
+
console.warn("[OpenCode] Failed to load credentials:", e);
|
|
113
|
+
}
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
async function resolveCredentials(config) {
|
|
117
|
+
const fileCreds = await loadCredentialsFromFile();
|
|
118
|
+
// Precedence: explicit config > env > creds file.
|
|
119
|
+
let username = config.username || process.env["OPENCODE_SERVER_USERNAME"] || fileCreds?.username;
|
|
120
|
+
let password = config.password || process.env["OPENCODE_SERVER_PASSWORD"] || fileCreds?.password;
|
|
121
|
+
// Port hint can come from creds file even when env provides auth.
|
|
122
|
+
const filePort = fileCreds?.port;
|
|
123
|
+
if (!username || !password) {
|
|
124
|
+
throw new Error("OpenCodeClient: No credentials. Set OPENCODE_SERVER_USERNAME/OPENCODE_SERVER_PASSWORD or create .viber/opencode-auth.json");
|
|
125
|
+
}
|
|
126
|
+
let baseUrl;
|
|
127
|
+
if (config.baseUrl) {
|
|
128
|
+
baseUrl = config.baseUrl;
|
|
129
|
+
}
|
|
130
|
+
else if (config.port) {
|
|
131
|
+
baseUrl = `http://127.0.0.1:${config.port}`;
|
|
132
|
+
}
|
|
133
|
+
else if (typeof filePort === "number") {
|
|
134
|
+
baseUrl = `http://127.0.0.1:${filePort}`;
|
|
135
|
+
}
|
|
136
|
+
else {
|
|
137
|
+
baseUrl = await getOpenCodeBaseUrl();
|
|
138
|
+
}
|
|
139
|
+
return { baseUrl, username, password: password || "" };
|
|
140
|
+
}
|
|
141
|
+
// ============================================================================
|
|
142
|
+
// OpenCode Client
|
|
143
|
+
// ============================================================================
|
|
144
|
+
class OpenCodeClient {
|
|
145
|
+
config;
|
|
146
|
+
constructor(config) {
|
|
147
|
+
this.config = config;
|
|
148
|
+
}
|
|
149
|
+
static async create(config = {}) {
|
|
150
|
+
const resolved = await resolveCredentials(config);
|
|
151
|
+
return new OpenCodeClient(resolved);
|
|
152
|
+
}
|
|
153
|
+
getAuthHeader() {
|
|
154
|
+
const creds = `${this.config.username}:${this.config.password}`;
|
|
155
|
+
return `Basic ${Buffer.from(creds).toString("base64")}`;
|
|
156
|
+
}
|
|
157
|
+
async healthCheck() {
|
|
158
|
+
const res = await fetch(`${this.config.baseUrl}/global/health`, {
|
|
159
|
+
headers: { Authorization: this.getAuthHeader() },
|
|
160
|
+
});
|
|
161
|
+
if (!res.ok) {
|
|
162
|
+
if (res.status === 401 || res.status === 403) {
|
|
163
|
+
throw new Error(`Health check unauthorized (HTTP ${res.status}). ` +
|
|
164
|
+
`Check OPENCODE_SERVER_USERNAME/OPENCODE_SERVER_PASSWORD or .viber/opencode-auth.json`);
|
|
165
|
+
}
|
|
166
|
+
throw new Error(`Health check failed: ${res.status}`);
|
|
167
|
+
}
|
|
168
|
+
const data = (await res.json());
|
|
169
|
+
return { status: data.status || "ok", version: data.version };
|
|
170
|
+
}
|
|
171
|
+
async createSession(title) {
|
|
172
|
+
const res = await fetch(`${this.config.baseUrl}/session`, {
|
|
173
|
+
method: "POST",
|
|
174
|
+
headers: {
|
|
175
|
+
"Content-Type": "application/json",
|
|
176
|
+
Authorization: this.getAuthHeader(),
|
|
177
|
+
},
|
|
178
|
+
body: JSON.stringify({ title }),
|
|
179
|
+
});
|
|
180
|
+
if (!res.ok) {
|
|
181
|
+
if (res.status === 401 || res.status === 403) {
|
|
182
|
+
throw new Error(`Create session unauthorized (HTTP ${res.status}). ` +
|
|
183
|
+
`Check OPENCODE_SERVER_USERNAME/OPENCODE_SERVER_PASSWORD or .viber/opencode-auth.json`);
|
|
184
|
+
}
|
|
185
|
+
throw new Error(`Create session failed: ${res.status}`);
|
|
186
|
+
}
|
|
187
|
+
const data = (await res.json());
|
|
188
|
+
return data.id || data.data?.id;
|
|
189
|
+
}
|
|
190
|
+
async getSession(sessionId) {
|
|
191
|
+
const res = await fetch(`${this.config.baseUrl}/session/${sessionId}`, {
|
|
192
|
+
headers: { Authorization: this.getAuthHeader() },
|
|
193
|
+
});
|
|
194
|
+
if (!res.ok)
|
|
195
|
+
throw new Error(`Get session failed: ${res.status}`);
|
|
196
|
+
const data = (await res.json());
|
|
197
|
+
return {
|
|
198
|
+
id: data.id || data.data?.id,
|
|
199
|
+
title: data.title || data.data?.title || "",
|
|
200
|
+
status: data.status || data.data?.status || "unknown",
|
|
201
|
+
createdAt: data.createdAt || data.data?.createdAt || new Date().toISOString(),
|
|
202
|
+
messageCount: data.messageCount || data.data?.messageCount,
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
async sendMessage(sessionId, prompt, model = "qwen/qwen3-coder:480b") {
|
|
206
|
+
const res = await fetch(`${this.config.baseUrl}/session/${sessionId}/message`, {
|
|
207
|
+
method: "POST",
|
|
208
|
+
headers: {
|
|
209
|
+
"Content-Type": "application/json",
|
|
210
|
+
Authorization: this.getAuthHeader(),
|
|
211
|
+
},
|
|
212
|
+
body: JSON.stringify({
|
|
213
|
+
parts: [{ type: "text", text: prompt }],
|
|
214
|
+
model: { providerID: "openrouter", modelID: model },
|
|
215
|
+
agent: "build",
|
|
216
|
+
}),
|
|
217
|
+
});
|
|
218
|
+
if (!res.ok)
|
|
219
|
+
throw new Error(`Message send failed: ${res.status}`);
|
|
220
|
+
const data = (await res.json());
|
|
221
|
+
return {
|
|
222
|
+
messageId: data.id || data.messageId,
|
|
223
|
+
content: data.content || data.text || "",
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
async sendMessageAsync(sessionId, prompt, model = "qwen/qwen3-coder:480b") {
|
|
227
|
+
fetch(`${this.config.baseUrl}/session/${sessionId}/message`, {
|
|
228
|
+
method: "POST",
|
|
229
|
+
headers: {
|
|
230
|
+
"Content-Type": "application/json",
|
|
231
|
+
Authorization: this.getAuthHeader(),
|
|
232
|
+
},
|
|
233
|
+
body: JSON.stringify({
|
|
234
|
+
parts: [{ type: "text", text: prompt }],
|
|
235
|
+
model: { providerID: "openrouter", modelID: model },
|
|
236
|
+
agent: "build",
|
|
237
|
+
}),
|
|
238
|
+
}).catch((e) => console.error("[OpenCode] Async message failed", e));
|
|
239
|
+
}
|
|
240
|
+
async getMessages(sessionId) {
|
|
241
|
+
const res = await fetch(`${this.config.baseUrl}/session/${sessionId}/messages`, {
|
|
242
|
+
headers: { Authorization: this.getAuthHeader() },
|
|
243
|
+
});
|
|
244
|
+
if (!res.ok)
|
|
245
|
+
throw new Error(`Get messages failed: ${res.status}`);
|
|
246
|
+
const data = (await res.json());
|
|
247
|
+
return data.messages || data.data || [];
|
|
248
|
+
}
|
|
249
|
+
async abortSession(sessionId) {
|
|
250
|
+
const res = await fetch(`${this.config.baseUrl}/session/${sessionId}/abort`, {
|
|
251
|
+
method: "POST",
|
|
252
|
+
headers: { Authorization: this.getAuthHeader() },
|
|
253
|
+
});
|
|
254
|
+
if (!res.ok)
|
|
255
|
+
throw new Error(`Abort session failed: ${res.status}`);
|
|
256
|
+
}
|
|
257
|
+
async deleteSession(sessionId) {
|
|
258
|
+
const res = await fetch(`${this.config.baseUrl}/session/${sessionId}`, {
|
|
259
|
+
method: "DELETE",
|
|
260
|
+
headers: { Authorization: this.getAuthHeader() },
|
|
261
|
+
});
|
|
262
|
+
if (!res.ok)
|
|
263
|
+
throw new Error(`Delete session failed: ${res.status}`);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
exports.OpenCodeClient = OpenCodeClient;
|
|
267
|
+
// ============================================================================
|
|
268
|
+
// SSE Stream
|
|
269
|
+
// ============================================================================
|
|
270
|
+
class OpenCodeLogStream {
|
|
271
|
+
url;
|
|
272
|
+
authHeader;
|
|
273
|
+
es;
|
|
274
|
+
constructor(baseUrl, sessionId, authHeader) {
|
|
275
|
+
this.url = `${baseUrl}/session/${sessionId}/stream`;
|
|
276
|
+
this.authHeader = authHeader;
|
|
277
|
+
}
|
|
278
|
+
async connect(onEvent) {
|
|
279
|
+
const es = new EventSource(this.url, {
|
|
280
|
+
headers: { Authorization: this.authHeader },
|
|
281
|
+
});
|
|
282
|
+
es.onmessage = (e) => {
|
|
283
|
+
try {
|
|
284
|
+
const data = JSON.parse(e.data);
|
|
285
|
+
const log = {
|
|
286
|
+
id: data.id || Math.random().toString(36).substring(2, 11),
|
|
287
|
+
type: this.mapType(data),
|
|
288
|
+
content: data.text || data.content || JSON.stringify(data),
|
|
289
|
+
timestamp: new Date().toISOString(),
|
|
290
|
+
};
|
|
291
|
+
onEvent(log);
|
|
292
|
+
}
|
|
293
|
+
catch (err) {
|
|
294
|
+
console.error("[SSE] Parse error:", err);
|
|
295
|
+
}
|
|
296
|
+
};
|
|
297
|
+
es.onerror = () => {
|
|
298
|
+
console.error("[SSE] Stream error");
|
|
299
|
+
this.disconnect();
|
|
300
|
+
};
|
|
301
|
+
this.es = es;
|
|
302
|
+
}
|
|
303
|
+
mapType(data) {
|
|
304
|
+
if (data.type === "thought")
|
|
305
|
+
return "thought";
|
|
306
|
+
if (data.tool_call || data.type === "tool_call")
|
|
307
|
+
return "tool_call";
|
|
308
|
+
if (data.type === "error")
|
|
309
|
+
return "error";
|
|
310
|
+
if (data.type === "status")
|
|
311
|
+
return "status";
|
|
312
|
+
return "text";
|
|
313
|
+
}
|
|
314
|
+
disconnect() {
|
|
315
|
+
if (this.es) {
|
|
316
|
+
this.es.close();
|
|
317
|
+
this.es = undefined;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
exports.OpenCodeLogStream = OpenCodeLogStream;
|
|
322
|
+
//# sourceMappingURL=index.js.map
|
package/src/index.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["index.ts"],"names":[],"mappings":"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"}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @viberlabs/opencode
|
|
3
|
+
* OpenCode HTTP client and SSE stream for real-time agent progress
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import * as fs from "fs/promises";
|
|
7
|
+
import * as path from "path";
|
|
8
|
+
import EventSource = require("eventsource");
|
|
9
|
+
|
|
10
|
+
// ============================================================================
|
|
11
|
+
// Port Discovery
|
|
12
|
+
// ============================================================================
|
|
13
|
+
|
|
14
|
+
const COMMON_PORTS = [51262, 4096, 3000, 8080];
|
|
15
|
+
|
|
16
|
+
async function testPort(port: number): Promise<boolean> {
|
|
17
|
+
try {
|
|
18
|
+
const controller = new AbortController();
|
|
19
|
+
const timeoutId = setTimeout(() => controller.abort(), 2000);
|
|
20
|
+
const response = await fetch(`http://127.0.0.1:${port}/global/health`, {
|
|
21
|
+
signal: controller.signal,
|
|
22
|
+
});
|
|
23
|
+
clearTimeout(timeoutId);
|
|
24
|
+
// Some OpenCode servers protect /global/health behind Basic Auth.
|
|
25
|
+
// Treat 401/403 as a positive signal that a server is present.
|
|
26
|
+
if (response.ok) return true;
|
|
27
|
+
if (response.status === 401 || response.status === 403) return true;
|
|
28
|
+
return false;
|
|
29
|
+
} catch {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export async function discoverOpenCodePort(): Promise<number | null> {
|
|
35
|
+
for (const port of COMMON_PORTS) {
|
|
36
|
+
if (await testPort(port)) {
|
|
37
|
+
console.log(`[OpenCode] Found server on port ${port}`);
|
|
38
|
+
return port;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export async function getOpenCodeBaseUrl(explicitPort?: number): Promise<string> {
|
|
45
|
+
if (explicitPort) {
|
|
46
|
+
return `http://127.0.0.1:${explicitPort}`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const envPort = process.env["OPENCODE_SERVER_PORT"];
|
|
50
|
+
if (envPort) {
|
|
51
|
+
const port = parseInt(envPort, 10);
|
|
52
|
+
if (!isNaN(port)) return `http://127.0.0.1:${port}`;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const discovered = await discoverOpenCodePort();
|
|
56
|
+
if (discovered) return `http://127.0.0.1:${discovered}`;
|
|
57
|
+
|
|
58
|
+
return "http://127.0.0.1:51262";
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// ============================================================================
|
|
62
|
+
// Client Types
|
|
63
|
+
// ============================================================================
|
|
64
|
+
|
|
65
|
+
export interface OpenCodeConfig {
|
|
66
|
+
baseUrl?: string;
|
|
67
|
+
port?: number;
|
|
68
|
+
username: string;
|
|
69
|
+
password?: string;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
interface Credentials {
|
|
73
|
+
username: string;
|
|
74
|
+
password: string;
|
|
75
|
+
port?: number;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export interface OpenCodeMessage {
|
|
79
|
+
messageId: string;
|
|
80
|
+
content: string;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export interface OpenCodeSession {
|
|
84
|
+
id: string;
|
|
85
|
+
title: string;
|
|
86
|
+
status: string;
|
|
87
|
+
createdAt: string;
|
|
88
|
+
messageCount?: number;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export interface LogEvent {
|
|
92
|
+
id: string;
|
|
93
|
+
type: "status" | "thought" | "tool_call" | "text" | "error";
|
|
94
|
+
content: string;
|
|
95
|
+
timestamp: string;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// ============================================================================
|
|
99
|
+
// Credentials Resolution
|
|
100
|
+
// ============================================================================
|
|
101
|
+
|
|
102
|
+
function getViberDir(): string {
|
|
103
|
+
return process.env["VIBER_DIR"] || path.join(process.cwd(), ".viber");
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async function loadCredentialsFromFile(): Promise<Credentials | null> {
|
|
107
|
+
try {
|
|
108
|
+
const viberDir = getViberDir();
|
|
109
|
+
const credsFile = path.join(viberDir, "opencode-auth.json");
|
|
110
|
+
const data = await fs.readFile(credsFile, "utf8");
|
|
111
|
+
const creds: Credentials = JSON.parse(data);
|
|
112
|
+
if (creds.username && creds.password) {
|
|
113
|
+
console.log(`[OpenCode] Loaded credentials from ${credsFile}`);
|
|
114
|
+
return creds;
|
|
115
|
+
}
|
|
116
|
+
} catch (e) {
|
|
117
|
+
console.warn("[OpenCode] Failed to load credentials:", e);
|
|
118
|
+
}
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async function resolveCredentials(config: Partial<OpenCodeConfig>): Promise<OpenCodeConfig> {
|
|
123
|
+
const fileCreds = await loadCredentialsFromFile();
|
|
124
|
+
|
|
125
|
+
// Precedence: explicit config > env > creds file.
|
|
126
|
+
let username = config.username || process.env["OPENCODE_SERVER_USERNAME"] || fileCreds?.username;
|
|
127
|
+
let password = config.password || process.env["OPENCODE_SERVER_PASSWORD"] || fileCreds?.password;
|
|
128
|
+
|
|
129
|
+
// Port hint can come from creds file even when env provides auth.
|
|
130
|
+
const filePort = fileCreds?.port;
|
|
131
|
+
|
|
132
|
+
if (!username || !password) {
|
|
133
|
+
throw new Error(
|
|
134
|
+
"OpenCodeClient: No credentials. Set OPENCODE_SERVER_USERNAME/OPENCODE_SERVER_PASSWORD or create .viber/opencode-auth.json",
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
let baseUrl: string;
|
|
139
|
+
if (config.baseUrl) {
|
|
140
|
+
baseUrl = config.baseUrl;
|
|
141
|
+
} else if (config.port) {
|
|
142
|
+
baseUrl = `http://127.0.0.1:${config.port}`;
|
|
143
|
+
} else if (typeof filePort === "number") {
|
|
144
|
+
baseUrl = `http://127.0.0.1:${filePort}`;
|
|
145
|
+
} else {
|
|
146
|
+
baseUrl = await getOpenCodeBaseUrl();
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return { baseUrl, username, password: password || "" };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// ============================================================================
|
|
153
|
+
// OpenCode Client
|
|
154
|
+
// ============================================================================
|
|
155
|
+
|
|
156
|
+
export class OpenCodeClient {
|
|
157
|
+
private config: OpenCodeConfig;
|
|
158
|
+
|
|
159
|
+
constructor(config: OpenCodeConfig) {
|
|
160
|
+
this.config = config;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
static async create(config: Partial<OpenCodeConfig> = {}): Promise<OpenCodeClient> {
|
|
164
|
+
const resolved = await resolveCredentials(config);
|
|
165
|
+
return new OpenCodeClient(resolved);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
private getAuthHeader(): string {
|
|
169
|
+
const creds = `${this.config.username}:${this.config.password}`;
|
|
170
|
+
return `Basic ${Buffer.from(creds).toString("base64")}`;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
async healthCheck(): Promise<{ status: string; version?: string }> {
|
|
174
|
+
const res = await fetch(`${this.config.baseUrl}/global/health`, {
|
|
175
|
+
headers: { Authorization: this.getAuthHeader() },
|
|
176
|
+
});
|
|
177
|
+
if (!res.ok) {
|
|
178
|
+
if (res.status === 401 || res.status === 403) {
|
|
179
|
+
throw new Error(
|
|
180
|
+
`Health check unauthorized (HTTP ${res.status}). ` +
|
|
181
|
+
`Check OPENCODE_SERVER_USERNAME/OPENCODE_SERVER_PASSWORD or .viber/opencode-auth.json`,
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
throw new Error(`Health check failed: ${res.status}`);
|
|
185
|
+
}
|
|
186
|
+
const data = (await res.json()) as any;
|
|
187
|
+
return { status: data.status || "ok", version: data.version };
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
async createSession(title: string): Promise<string> {
|
|
191
|
+
const res = await fetch(`${this.config.baseUrl}/session`, {
|
|
192
|
+
method: "POST",
|
|
193
|
+
headers: {
|
|
194
|
+
"Content-Type": "application/json",
|
|
195
|
+
Authorization: this.getAuthHeader(),
|
|
196
|
+
},
|
|
197
|
+
body: JSON.stringify({ title }),
|
|
198
|
+
});
|
|
199
|
+
if (!res.ok) {
|
|
200
|
+
if (res.status === 401 || res.status === 403) {
|
|
201
|
+
throw new Error(
|
|
202
|
+
`Create session unauthorized (HTTP ${res.status}). ` +
|
|
203
|
+
`Check OPENCODE_SERVER_USERNAME/OPENCODE_SERVER_PASSWORD or .viber/opencode-auth.json`,
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
throw new Error(`Create session failed: ${res.status}`);
|
|
207
|
+
}
|
|
208
|
+
const data = (await res.json()) as any;
|
|
209
|
+
return data.id || data.data?.id;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
async getSession(sessionId: string): Promise<OpenCodeSession> {
|
|
213
|
+
const res = await fetch(`${this.config.baseUrl}/session/${sessionId}`, {
|
|
214
|
+
headers: { Authorization: this.getAuthHeader() },
|
|
215
|
+
});
|
|
216
|
+
if (!res.ok) throw new Error(`Get session failed: ${res.status}`);
|
|
217
|
+
const data = (await res.json()) as any;
|
|
218
|
+
return {
|
|
219
|
+
id: data.id || data.data?.id,
|
|
220
|
+
title: data.title || data.data?.title || "",
|
|
221
|
+
status: data.status || data.data?.status || "unknown",
|
|
222
|
+
createdAt: data.createdAt || data.data?.createdAt || new Date().toISOString(),
|
|
223
|
+
messageCount: data.messageCount || data.data?.messageCount,
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
async sendMessage(
|
|
228
|
+
sessionId: string,
|
|
229
|
+
prompt: string,
|
|
230
|
+
model: string = "qwen/qwen3-coder:480b",
|
|
231
|
+
): Promise<OpenCodeMessage> {
|
|
232
|
+
const res = await fetch(`${this.config.baseUrl}/session/${sessionId}/message`, {
|
|
233
|
+
method: "POST",
|
|
234
|
+
headers: {
|
|
235
|
+
"Content-Type": "application/json",
|
|
236
|
+
Authorization: this.getAuthHeader(),
|
|
237
|
+
},
|
|
238
|
+
body: JSON.stringify({
|
|
239
|
+
parts: [{ type: "text", text: prompt }],
|
|
240
|
+
model: { providerID: "openrouter", modelID: model },
|
|
241
|
+
agent: "build",
|
|
242
|
+
}),
|
|
243
|
+
});
|
|
244
|
+
if (!res.ok) throw new Error(`Message send failed: ${res.status}`);
|
|
245
|
+
const data = (await res.json()) as any;
|
|
246
|
+
return {
|
|
247
|
+
messageId: data.id || data.messageId,
|
|
248
|
+
content: data.content || data.text || "",
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
async sendMessageAsync(
|
|
253
|
+
sessionId: string,
|
|
254
|
+
prompt: string,
|
|
255
|
+
model: string = "qwen/qwen3-coder:480b",
|
|
256
|
+
): Promise<void> {
|
|
257
|
+
fetch(`${this.config.baseUrl}/session/${sessionId}/message`, {
|
|
258
|
+
method: "POST",
|
|
259
|
+
headers: {
|
|
260
|
+
"Content-Type": "application/json",
|
|
261
|
+
Authorization: this.getAuthHeader(),
|
|
262
|
+
},
|
|
263
|
+
body: JSON.stringify({
|
|
264
|
+
parts: [{ type: "text", text: prompt }],
|
|
265
|
+
model: { providerID: "openrouter", modelID: model },
|
|
266
|
+
agent: "build",
|
|
267
|
+
}),
|
|
268
|
+
}).catch((e) => console.error("[OpenCode] Async message failed", e));
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
async getMessages(sessionId: string): Promise<any[]> {
|
|
272
|
+
const res = await fetch(`${this.config.baseUrl}/session/${sessionId}/messages`, {
|
|
273
|
+
headers: { Authorization: this.getAuthHeader() },
|
|
274
|
+
});
|
|
275
|
+
if (!res.ok) throw new Error(`Get messages failed: ${res.status}`);
|
|
276
|
+
const data = (await res.json()) as any;
|
|
277
|
+
return data.messages || data.data || [];
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
async abortSession(sessionId: string): Promise<void> {
|
|
281
|
+
const res = await fetch(`${this.config.baseUrl}/session/${sessionId}/abort`, {
|
|
282
|
+
method: "POST",
|
|
283
|
+
headers: { Authorization: this.getAuthHeader() },
|
|
284
|
+
});
|
|
285
|
+
if (!res.ok) throw new Error(`Abort session failed: ${res.status}`);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
async deleteSession(sessionId: string): Promise<void> {
|
|
289
|
+
const res = await fetch(`${this.config.baseUrl}/session/${sessionId}`, {
|
|
290
|
+
method: "DELETE",
|
|
291
|
+
headers: { Authorization: this.getAuthHeader() },
|
|
292
|
+
});
|
|
293
|
+
if (!res.ok) throw new Error(`Delete session failed: ${res.status}`);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// ============================================================================
|
|
298
|
+
// SSE Stream
|
|
299
|
+
// ============================================================================
|
|
300
|
+
|
|
301
|
+
export class OpenCodeLogStream {
|
|
302
|
+
private url: string;
|
|
303
|
+
private authHeader: string;
|
|
304
|
+
private es: EventSource | undefined;
|
|
305
|
+
|
|
306
|
+
constructor(baseUrl: string, sessionId: string, authHeader: string) {
|
|
307
|
+
this.url = `${baseUrl}/session/${sessionId}/stream`;
|
|
308
|
+
this.authHeader = authHeader;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
async connect(onEvent: (event: LogEvent) => void): Promise<void> {
|
|
312
|
+
const es = new (EventSource as any)(this.url, {
|
|
313
|
+
headers: { Authorization: this.authHeader },
|
|
314
|
+
} as any);
|
|
315
|
+
|
|
316
|
+
es.onmessage = (e: MessageEvent) => {
|
|
317
|
+
try {
|
|
318
|
+
const data = JSON.parse(e.data);
|
|
319
|
+
const log: LogEvent = {
|
|
320
|
+
id: data.id || Math.random().toString(36).substring(2, 11),
|
|
321
|
+
type: this.mapType(data),
|
|
322
|
+
content: data.text || data.content || JSON.stringify(data),
|
|
323
|
+
timestamp: new Date().toISOString(),
|
|
324
|
+
};
|
|
325
|
+
onEvent(log);
|
|
326
|
+
} catch (err) {
|
|
327
|
+
console.error("[SSE] Parse error:", err);
|
|
328
|
+
}
|
|
329
|
+
};
|
|
330
|
+
|
|
331
|
+
es.onerror = () => {
|
|
332
|
+
console.error("[SSE] Stream error");
|
|
333
|
+
this.disconnect();
|
|
334
|
+
};
|
|
335
|
+
|
|
336
|
+
this.es = es;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
private mapType(data: any): LogEvent["type"] {
|
|
340
|
+
if (data.type === "thought") return "thought";
|
|
341
|
+
if (data.tool_call || data.type === "tool_call") return "tool_call";
|
|
342
|
+
if (data.type === "error") return "error";
|
|
343
|
+
if (data.type === "status") return "status";
|
|
344
|
+
return "text";
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
disconnect(): void {
|
|
348
|
+
if (this.es) {
|
|
349
|
+
this.es.close();
|
|
350
|
+
this.es = undefined;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
}
|