vite-plugin-opencode-assistant 1.0.13 → 1.0.15
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/es/client/index.js +384 -86
- package/es/core/api.d.ts +1 -0
- package/es/core/api.js +97 -15
- package/es/core/proxy-server.d.ts +5 -1
- package/es/core/proxy-server.js +75 -69
- package/es/core/service.d.ts +8 -1
- package/es/core/service.js +128 -28
- package/es/endpoints/index.js +2 -0
- package/es/endpoints/sse.js +14 -5
- package/es/endpoints/types.d.ts +7 -1
- package/es/endpoints/warmup.d.ts +3 -0
- package/es/endpoints/warmup.js +45 -0
- package/es/index.js +12 -3
- package/es/utils/system.d.ts +2 -1
- package/es/utils/system.js +11 -10
- package/lib/client/index.js +380 -85
- package/lib/client.js +2880 -2619
- package/lib/core/api.d.ts +1 -0
- package/lib/core/api.js +97 -15
- package/lib/core/proxy-server.d.ts +5 -1
- package/lib/core/proxy-server.js +75 -69
- package/lib/core/service.d.ts +8 -1
- package/lib/core/service.js +127 -28
- package/lib/endpoints/index.js +2 -0
- package/lib/endpoints/sse.js +14 -5
- package/lib/endpoints/types.d.ts +7 -1
- package/lib/endpoints/warmup.d.ts +3 -0
- package/lib/endpoints/warmup.js +68 -0
- package/lib/index.js +12 -3
- package/lib/style.css +1 -1
- package/lib/utils/system.d.ts +2 -1
- package/lib/utils/system.js +10 -5
- package/package.json +5 -5
package/lib/core/api.d.ts
CHANGED
package/lib/core/api.js
CHANGED
|
@@ -61,13 +61,36 @@ function sleep(ms) {
|
|
|
61
61
|
function base64Encode(str) {
|
|
62
62
|
return Buffer.from(str).toString("base64");
|
|
63
63
|
}
|
|
64
|
+
function extractTextFromResponse(data) {
|
|
65
|
+
if (!data || typeof data !== "object") return null;
|
|
66
|
+
const obj = data;
|
|
67
|
+
if (obj.parts && Array.isArray(obj.parts)) {
|
|
68
|
+
const textParts = obj.parts.filter(
|
|
69
|
+
(p) => p && typeof p === "object" && p.type === "text"
|
|
70
|
+
).map((p) => p.text).filter(Boolean);
|
|
71
|
+
if (textParts.length > 0) return textParts.join("");
|
|
72
|
+
}
|
|
73
|
+
if (obj.text && typeof obj.text === "string") {
|
|
74
|
+
return obj.text;
|
|
75
|
+
}
|
|
76
|
+
if (obj.content && typeof obj.content === "string") {
|
|
77
|
+
return obj.content;
|
|
78
|
+
}
|
|
79
|
+
if (obj.message && typeof obj.message === "string") {
|
|
80
|
+
return obj.message;
|
|
81
|
+
}
|
|
82
|
+
if (typeof data === "string") {
|
|
83
|
+
return data;
|
|
84
|
+
}
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
64
87
|
class OpenCodeAPI {
|
|
65
88
|
constructor(hostname, getPort, warmupChromeMcpConfig = false) {
|
|
66
89
|
__publicField(this, "hostname", hostname);
|
|
67
90
|
__publicField(this, "getPort", getPort);
|
|
68
91
|
__publicField(this, "warmupChromeMcpConfig", warmupChromeMcpConfig);
|
|
69
92
|
}
|
|
70
|
-
createHttpRequest(options, body) {
|
|
93
|
+
createHttpRequest(options, body, timeout) {
|
|
71
94
|
const timer = new import_shared.PerformanceTimer("HTTP Request", {
|
|
72
95
|
operation: `${options.method || "GET"} ${options.path}`
|
|
73
96
|
});
|
|
@@ -90,6 +113,13 @@ class OpenCodeAPI {
|
|
|
90
113
|
timer.end("\u274C Request failed");
|
|
91
114
|
reject(e);
|
|
92
115
|
});
|
|
116
|
+
if (timeout) {
|
|
117
|
+
req.setTimeout(timeout, () => {
|
|
118
|
+
timer.end("\u274C Request timeout");
|
|
119
|
+
req.destroy();
|
|
120
|
+
reject(new Error(`Request timeout after ${timeout}ms`));
|
|
121
|
+
});
|
|
122
|
+
}
|
|
93
123
|
if (body) req.write(body);
|
|
94
124
|
req.end();
|
|
95
125
|
});
|
|
@@ -243,24 +273,16 @@ class OpenCodeAPI {
|
|
|
243
273
|
try {
|
|
244
274
|
const warmupSession = yield this.createSession(import_shared.DEFAULT_RETRIES, "__chrome_mcp_warmup__");
|
|
245
275
|
warmupSessionId = warmupSession.id;
|
|
246
|
-
let chromeToolIds;
|
|
247
|
-
try {
|
|
248
|
-
const toolIds = yield this.getToolIds();
|
|
249
|
-
chromeToolIds = toolIds.filter((toolId) => /chrome[-_]?devtools/i.test(toolId));
|
|
250
|
-
log.debug("Resolved Chrome MCP tool ids", {
|
|
251
|
-
chromeToolIds
|
|
252
|
-
});
|
|
253
|
-
} catch (e) {
|
|
254
|
-
log.debug("Failed to resolve Chrome MCP tool ids", { error: e });
|
|
255
|
-
}
|
|
256
276
|
const prompt = [
|
|
257
277
|
"Call the browser tool list_pages immediately to establish the Chrome DevTools MCP connection.",
|
|
258
278
|
viteOrigin ? `If there are no pages, call new_page with ${viteOrigin}.` : "If there are no pages, call new_page with about:blank.",
|
|
259
279
|
"Do not read or modify project files.",
|
|
260
280
|
"Do not use any non-browser tools.",
|
|
261
|
-
"After the tool call is complete, reply with exactly: ready"
|
|
281
|
+
"After the tool call is complete, reply with exactly: ready",
|
|
282
|
+
"If the tool call fails, reply with exactly: fail"
|
|
262
283
|
].join(" ");
|
|
263
|
-
|
|
284
|
+
const WARMUP_TIMEOUT = 3e4;
|
|
285
|
+
const data = yield this.createHttpRequest(
|
|
264
286
|
{
|
|
265
287
|
hostname: this.hostname,
|
|
266
288
|
port: this.getPort(),
|
|
@@ -270,14 +292,19 @@ class OpenCodeAPI {
|
|
|
270
292
|
},
|
|
271
293
|
JSON.stringify({
|
|
272
294
|
system: "You are warming up Chrome DevTools MCP during startup. You must use the available browser tools immediately before replying.",
|
|
273
|
-
tools: (chromeToolIds == null ? void 0 : chromeToolIds.length) ? chromeToolIds : void 0,
|
|
274
295
|
parts: [{ type: "text", text: prompt }]
|
|
275
|
-
})
|
|
296
|
+
}),
|
|
297
|
+
WARMUP_TIMEOUT
|
|
276
298
|
);
|
|
299
|
+
const responseText = extractTextFromResponse(data);
|
|
300
|
+
if (!(responseText == null ? void 0 : responseText.toLowerCase().includes("ready"))) {
|
|
301
|
+
throw new Error(`Chrome MCP warmup failed: ${responseText || "No response"}`);
|
|
302
|
+
}
|
|
277
303
|
timer.end("Chrome MCP warmed up");
|
|
278
304
|
} catch (e) {
|
|
279
305
|
log.warn("Failed to warm up Chrome MCP", { error: e });
|
|
280
306
|
timer.end("Chrome MCP warmup skipped");
|
|
307
|
+
throw e;
|
|
281
308
|
} finally {
|
|
282
309
|
if (warmupSessionId) {
|
|
283
310
|
try {
|
|
@@ -314,6 +341,61 @@ class OpenCodeAPI {
|
|
|
314
341
|
return url;
|
|
315
342
|
});
|
|
316
343
|
}
|
|
344
|
+
retryWarmupChromeMcp(viteOrigin) {
|
|
345
|
+
return __async(this, null, function* () {
|
|
346
|
+
const timer = log.timer("retryWarmupChromeMcp", { viteOrigin });
|
|
347
|
+
let warmupSessionId = null;
|
|
348
|
+
try {
|
|
349
|
+
const warmupSession = yield this.createSession(import_shared.DEFAULT_RETRIES, "__chrome_mcp_warmup__");
|
|
350
|
+
warmupSessionId = warmupSession.id;
|
|
351
|
+
const prompt = [
|
|
352
|
+
"Call the browser tool list_pages immediately to establish the Chrome DevTools MCP connection.",
|
|
353
|
+
viteOrigin ? `If there are no pages, call new_page with ${viteOrigin}.` : "If there are no pages, call new_page with about:blank.",
|
|
354
|
+
"Do not read or modify project files.",
|
|
355
|
+
"Do not use any non-browser tools.",
|
|
356
|
+
"After the tool call is complete, reply with exactly: ready",
|
|
357
|
+
"If the tool call fails, reply with exactly: fail"
|
|
358
|
+
].join(" ");
|
|
359
|
+
const WARMUP_TIMEOUT = 6e4;
|
|
360
|
+
const data = yield this.createHttpRequest(
|
|
361
|
+
{
|
|
362
|
+
hostname: this.hostname,
|
|
363
|
+
port: this.getPort(),
|
|
364
|
+
path: `/session/${warmupSessionId}/message`,
|
|
365
|
+
method: "POST",
|
|
366
|
+
headers: { "Content-Type": "application/json" }
|
|
367
|
+
},
|
|
368
|
+
JSON.stringify({
|
|
369
|
+
system: "You are warming up Chrome DevTools MCP during startup. You must use the available browser tools immediately before replying.",
|
|
370
|
+
parts: [{ type: "text", text: prompt }]
|
|
371
|
+
}),
|
|
372
|
+
WARMUP_TIMEOUT
|
|
373
|
+
);
|
|
374
|
+
log.debug("Chrome MCP warmup response:", { data });
|
|
375
|
+
const responseText = extractTextFromResponse(data);
|
|
376
|
+
if (!(responseText == null ? void 0 : responseText.toLowerCase().includes("ready"))) {
|
|
377
|
+
throw new Error(`Chrome MCP warmup failed: ${responseText || "No response"}`);
|
|
378
|
+
}
|
|
379
|
+
timer.end("Chrome MCP warmed up successfully");
|
|
380
|
+
return true;
|
|
381
|
+
} catch (e) {
|
|
382
|
+
log.warn("Failed to retry warm up Chrome MCP", { error: e });
|
|
383
|
+
timer.end("Chrome MCP warmup retry failed");
|
|
384
|
+
return false;
|
|
385
|
+
} finally {
|
|
386
|
+
if (warmupSessionId) {
|
|
387
|
+
try {
|
|
388
|
+
yield this.deleteSession(warmupSessionId, 5);
|
|
389
|
+
} catch (e) {
|
|
390
|
+
log.warn("Failed to delete warmup session after retries", {
|
|
391
|
+
error: e,
|
|
392
|
+
warmupSessionId
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
});
|
|
398
|
+
}
|
|
317
399
|
}
|
|
318
400
|
// Annotate the CommonJS export names for ESM import in node:
|
|
319
401
|
0 && (module.exports = {
|
|
@@ -8,4 +8,8 @@ export interface ProxyServerOptions {
|
|
|
8
8
|
/** OpenCode 内部设置 */
|
|
9
9
|
settings?: OpenCodeSettings;
|
|
10
10
|
}
|
|
11
|
-
export
|
|
11
|
+
export interface ProxyServerResult {
|
|
12
|
+
server: http.Server;
|
|
13
|
+
actualPort: number;
|
|
14
|
+
}
|
|
15
|
+
export declare function startProxyServer(targetUrl: string, port: number, options?: ProxyServerOptions): Promise<ProxyServerResult>;
|
package/lib/core/proxy-server.js
CHANGED
|
@@ -140,80 +140,86 @@ function generateBridgeScript(options) {
|
|
|
140
140
|
`;
|
|
141
141
|
}
|
|
142
142
|
function startProxyServer(targetUrl, port, options = {}) {
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
res.end(body);
|
|
154
|
-
return;
|
|
155
|
-
}
|
|
156
|
-
const options2 = {
|
|
157
|
-
hostname: target.hostname,
|
|
158
|
-
port: target.port,
|
|
159
|
-
path: req.url,
|
|
160
|
-
method: req.method,
|
|
161
|
-
headers: __spreadProps(__spreadValues({}, req.headers), {
|
|
162
|
-
host: target.host,
|
|
163
|
-
// Don't accept compressed responses so we can modify HTML
|
|
164
|
-
"accept-encoding": "identity"
|
|
165
|
-
})
|
|
166
|
-
};
|
|
167
|
-
const proxyReq = import_http.default.request(options2, (proxyRes) => {
|
|
168
|
-
var _a;
|
|
169
|
-
const rawContentType = proxyRes.headers["content-type"];
|
|
170
|
-
const contentType = Array.isArray(rawContentType) ? (_a = rawContentType[0]) != null ? _a : "" : rawContentType != null ? rawContentType : "";
|
|
171
|
-
if (contentType.includes("text/html")) {
|
|
172
|
-
const chunks = [];
|
|
173
|
-
proxyRes.on("data", (chunk) => {
|
|
174
|
-
chunks.push(chunk);
|
|
143
|
+
return new Promise((resolve, reject) => {
|
|
144
|
+
const target = new URL(targetUrl);
|
|
145
|
+
const bridgeScript = generateBridgeScript(options);
|
|
146
|
+
const server = import_http.default.createServer((req, res) => {
|
|
147
|
+
if (req.url === "/__opencode_bridge__.js") {
|
|
148
|
+
const body = bridgeScript;
|
|
149
|
+
res.writeHead(200, {
|
|
150
|
+
"content-type": "application/javascript; charset=utf-8",
|
|
151
|
+
"cache-control": "no-store",
|
|
152
|
+
"content-length": Buffer.byteLength(body)
|
|
175
153
|
});
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
if (body.match(/<\/head>/i)) {
|
|
179
|
-
body = body.replace(
|
|
180
|
-
/<\/head>/i,
|
|
181
|
-
'<script src="/__opencode_bridge__.js"></script></head>'
|
|
182
|
-
);
|
|
183
|
-
} else if (body.match(/<\/body>/i)) {
|
|
184
|
-
body = body.replace(
|
|
185
|
-
/<\/body>/i,
|
|
186
|
-
'<script src="/__opencode_bridge__.js"></script></body>'
|
|
187
|
-
);
|
|
188
|
-
} else {
|
|
189
|
-
body += '<script src="/__opencode_bridge__.js"></script>';
|
|
190
|
-
}
|
|
191
|
-
const headers = {};
|
|
192
|
-
for (const [key, value] of Object.entries(proxyRes.headers)) {
|
|
193
|
-
if (value !== void 0 && key !== "content-encoding" && key !== "transfer-encoding" && key !== "content-length") {
|
|
194
|
-
headers[key] = value;
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
headers["content-length"] = Buffer.byteLength(body);
|
|
198
|
-
res.writeHead(proxyRes.statusCode || 200, headers);
|
|
199
|
-
res.end(body);
|
|
200
|
-
});
|
|
201
|
-
} else {
|
|
202
|
-
res.writeHead(proxyRes.statusCode || 200, proxyRes.headers);
|
|
203
|
-
proxyRes.pipe(res);
|
|
154
|
+
res.end(body);
|
|
155
|
+
return;
|
|
204
156
|
}
|
|
157
|
+
const requestOptions = {
|
|
158
|
+
hostname: target.hostname,
|
|
159
|
+
port: target.port,
|
|
160
|
+
path: req.url,
|
|
161
|
+
method: req.method,
|
|
162
|
+
headers: __spreadProps(__spreadValues({}, req.headers), {
|
|
163
|
+
host: target.host,
|
|
164
|
+
"accept-encoding": "identity"
|
|
165
|
+
})
|
|
166
|
+
};
|
|
167
|
+
const proxyReq = import_http.default.request(requestOptions, (proxyRes) => {
|
|
168
|
+
var _a;
|
|
169
|
+
const rawContentType = proxyRes.headers["content-type"];
|
|
170
|
+
const contentType = Array.isArray(rawContentType) ? (_a = rawContentType[0]) != null ? _a : "" : rawContentType != null ? rawContentType : "";
|
|
171
|
+
if (contentType.includes("text/html")) {
|
|
172
|
+
const chunks = [];
|
|
173
|
+
proxyRes.on("data", (chunk) => {
|
|
174
|
+
chunks.push(chunk);
|
|
175
|
+
});
|
|
176
|
+
proxyRes.on("end", () => {
|
|
177
|
+
let body = Buffer.concat(chunks).toString("utf-8");
|
|
178
|
+
if (body.match(/<\/head>/i)) {
|
|
179
|
+
body = body.replace(
|
|
180
|
+
/<\/head>/i,
|
|
181
|
+
'<script src="/__opencode_bridge__.js"></script></head>'
|
|
182
|
+
);
|
|
183
|
+
} else if (body.match(/<\/body>/i)) {
|
|
184
|
+
body = body.replace(
|
|
185
|
+
/<\/body>/i,
|
|
186
|
+
'<script src="/__opencode_bridge__.js"></script></body>'
|
|
187
|
+
);
|
|
188
|
+
} else {
|
|
189
|
+
body += '<script src="/__opencode_bridge__.js"></script>';
|
|
190
|
+
}
|
|
191
|
+
const headers = {};
|
|
192
|
+
for (const [key, value] of Object.entries(proxyRes.headers)) {
|
|
193
|
+
if (value !== void 0 && key !== "content-encoding" && key !== "transfer-encoding" && key !== "content-length") {
|
|
194
|
+
headers[key] = value;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
headers["content-length"] = Buffer.byteLength(body);
|
|
198
|
+
res.writeHead(proxyRes.statusCode || 200, headers);
|
|
199
|
+
res.end(body);
|
|
200
|
+
});
|
|
201
|
+
} else {
|
|
202
|
+
res.writeHead(proxyRes.statusCode || 200, proxyRes.headers);
|
|
203
|
+
proxyRes.pipe(res);
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
proxyReq.on("error", (err) => {
|
|
207
|
+
log.error("Proxy error", { error: err.message, url: req.url });
|
|
208
|
+
res.writeHead(502);
|
|
209
|
+
res.end("Proxy error");
|
|
210
|
+
});
|
|
211
|
+
req.pipe(proxyReq);
|
|
205
212
|
});
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
213
|
+
server.on("error", (err) => {
|
|
214
|
+
reject(err);
|
|
215
|
+
});
|
|
216
|
+
server.listen(port, () => {
|
|
217
|
+
const address = server.address();
|
|
218
|
+
const actualPort = typeof address === "object" && address ? address.port : port;
|
|
219
|
+
log.info(`Proxy server started on port ${actualPort} -> ${targetUrl}`);
|
|
220
|
+
resolve({ server, actualPort });
|
|
210
221
|
});
|
|
211
|
-
req.pipe(proxyReq);
|
|
212
|
-
});
|
|
213
|
-
server.listen(port, () => {
|
|
214
|
-
log.info(`Proxy server started on port ${port} -> ${targetUrl}`);
|
|
215
222
|
});
|
|
216
|
-
return server;
|
|
217
223
|
}
|
|
218
224
|
// Annotate the CommonJS export names for ESM import in node:
|
|
219
225
|
0 && (module.exports = {
|
package/lib/core/service.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ResultPromise } from "execa";
|
|
2
2
|
import type http from "http";
|
|
3
|
-
import type { OpenCodeOptions } from "@vite-plugin-opencode-assistant/shared";
|
|
3
|
+
import type { OpenCodeOptions, ServiceStartupTask } from "@vite-plugin-opencode-assistant/shared";
|
|
4
4
|
import type { OpenCodeAPI } from "./api.js";
|
|
5
5
|
export declare class OpenCodeService {
|
|
6
6
|
private config;
|
|
@@ -15,7 +15,14 @@ export declare class OpenCodeService {
|
|
|
15
15
|
private startPromise;
|
|
16
16
|
sessionUrl: string | null;
|
|
17
17
|
private proxyServer;
|
|
18
|
+
chromeMcpWarmupFailed: boolean;
|
|
19
|
+
currentTask: {
|
|
20
|
+
task: ServiceStartupTask;
|
|
21
|
+
data?: Record<string, unknown>;
|
|
22
|
+
} | null;
|
|
18
23
|
constructor(config: Required<OpenCodeOptions>, api: OpenCodeAPI, sseClients: Set<http.ServerResponse>, onPortAllocated: (port: number) => void, onProxyPortAllocated: (port: number) => void);
|
|
24
|
+
private sendTaskUpdate;
|
|
19
25
|
start(corsOrigins?: string[], contextApiUrl?: string, viteOrigin?: string): Promise<void>;
|
|
26
|
+
retryWarmupChromeMcp(viteOrigin?: string): Promise<boolean>;
|
|
20
27
|
stop(): Promise<void>;
|
|
21
28
|
}
|
package/lib/core/service.js
CHANGED
|
@@ -1,8 +1,21 @@
|
|
|
1
1
|
var __defProp = Object.defineProperty;
|
|
2
2
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
3
3
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
4
|
+
var __getOwnPropSymbols = Object.getOwnPropertySymbols;
|
|
4
5
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __propIsEnum = Object.prototype.propertyIsEnumerable;
|
|
5
7
|
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
8
|
+
var __spreadValues = (a, b) => {
|
|
9
|
+
for (var prop in b || (b = {}))
|
|
10
|
+
if (__hasOwnProp.call(b, prop))
|
|
11
|
+
__defNormalProp(a, prop, b[prop]);
|
|
12
|
+
if (__getOwnPropSymbols)
|
|
13
|
+
for (var prop of __getOwnPropSymbols(b)) {
|
|
14
|
+
if (__propIsEnum.call(b, prop))
|
|
15
|
+
__defNormalProp(a, prop, b[prop]);
|
|
16
|
+
}
|
|
17
|
+
return a;
|
|
18
|
+
};
|
|
6
19
|
var __export = (target, all) => {
|
|
7
20
|
for (var name in all)
|
|
8
21
|
__defProp(target, name, { get: all[name], enumerable: true });
|
|
@@ -61,10 +74,24 @@ class OpenCodeService {
|
|
|
61
74
|
__publicField(this, "startPromise", null);
|
|
62
75
|
__publicField(this, "sessionUrl", null);
|
|
63
76
|
__publicField(this, "proxyServer", null);
|
|
77
|
+
__publicField(this, "chromeMcpWarmupFailed", false);
|
|
78
|
+
__publicField(this, "currentTask", null);
|
|
64
79
|
var _a;
|
|
65
80
|
this.actualWebPort = config.webPort;
|
|
66
81
|
this.actualProxyPort = (_a = config.proxyPort) != null ? _a : import_shared.DEFAULT_PROXY_PORT;
|
|
67
82
|
}
|
|
83
|
+
sendTaskUpdate(task, data) {
|
|
84
|
+
this.currentTask = __spreadValues({ task }, data);
|
|
85
|
+
this.sseClients.forEach((client) => {
|
|
86
|
+
try {
|
|
87
|
+
client.write(`data: ${JSON.stringify(__spreadValues({ type: "TASK_UPDATE", task }, data))}
|
|
88
|
+
|
|
89
|
+
`);
|
|
90
|
+
} catch (e) {
|
|
91
|
+
log.debug("Failed to send TASK_UPDATE event", { error: e });
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
}
|
|
68
95
|
start(corsOrigins, contextApiUrl, viteOrigin) {
|
|
69
96
|
return __async(this, null, function* () {
|
|
70
97
|
if (this.isStarted && this.webProcess) {
|
|
@@ -76,7 +103,7 @@ class OpenCodeService {
|
|
|
76
103
|
return this.startPromise;
|
|
77
104
|
}
|
|
78
105
|
this.startPromise = (() => __async(this, null, function* () {
|
|
79
|
-
var _a, _b, _c;
|
|
106
|
+
var _a, _b, _c, _d, _e;
|
|
80
107
|
const timer = log.timer("startServices", {
|
|
81
108
|
corsOrigins,
|
|
82
109
|
contextApiUrl,
|
|
@@ -87,6 +114,7 @@ class OpenCodeService {
|
|
|
87
114
|
if (orphanCount > 0) {
|
|
88
115
|
log.debug(`Killed ${orphanCount} orphan OpenCode process(es)`);
|
|
89
116
|
}
|
|
117
|
+
this.sendTaskUpdate("checking_opencode");
|
|
90
118
|
if (!(yield (0, import_system.checkOpenCodeInstalled)())) {
|
|
91
119
|
log.error(`OpenCode is not installed!
|
|
92
120
|
|
|
@@ -106,10 +134,13 @@ Please install OpenCode first:
|
|
|
106
134
|
mise use -g opencode # Any OS
|
|
107
135
|
nix run nixpkgs#opencode # or github:anomalyco/opencode for latest dev branch
|
|
108
136
|
`);
|
|
137
|
+
this.sendTaskUpdate("opencode_not_installed");
|
|
138
|
+
this.startPromise = null;
|
|
109
139
|
timer.end("\u274C OpenCode not installed");
|
|
110
140
|
return;
|
|
111
141
|
}
|
|
112
142
|
timer.checkpoint("OpenCode installation verified");
|
|
143
|
+
this.sendTaskUpdate("allocating_port");
|
|
113
144
|
this.actualWebPort = yield (0, import_system.findAvailablePort)(this.config.webPort, this.config.hostname);
|
|
114
145
|
this.onPortAllocated(this.actualWebPort);
|
|
115
146
|
if (this.actualWebPort !== this.config.webPort) {
|
|
@@ -118,8 +149,10 @@ Please install OpenCode first:
|
|
|
118
149
|
log.debug(`Using port ${this.actualWebPort}`);
|
|
119
150
|
}
|
|
120
151
|
timer.checkpoint("Port allocated");
|
|
152
|
+
this.sendTaskUpdate("preparing_runtime");
|
|
121
153
|
const configDir = (0, import_opencode.prepareOpenCodeRuntime)(process.cwd());
|
|
122
154
|
timer.checkpoint("Plugin setup complete");
|
|
155
|
+
this.sendTaskUpdate("starting_web");
|
|
123
156
|
log.debug("Starting OpenCode Web process...", {
|
|
124
157
|
port: this.actualWebPort,
|
|
125
158
|
hostname: this.config.hostname,
|
|
@@ -137,53 +170,119 @@ Please install OpenCode first:
|
|
|
137
170
|
timer.checkpoint("Web process started");
|
|
138
171
|
const webUrl = `http://${this.config.hostname}:${this.actualWebPort}`;
|
|
139
172
|
log.info(`Waiting for OpenCode Web to become ready at ${webUrl}...`);
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
(_a = this.
|
|
144
|
-
|
|
145
|
-
|
|
173
|
+
this.sendTaskUpdate("waiting_web_ready");
|
|
174
|
+
try {
|
|
175
|
+
yield (0, import_system.waitForServer)(webUrl, import_shared.SERVER_START_TIMEOUT, this.webProcess);
|
|
176
|
+
if (((_a = this.webProcess) == null ? void 0 : _a.exitCode) !== null && ((_b = this.webProcess) == null ? void 0 : _b.exitCode) !== void 0) {
|
|
177
|
+
throw new Error(`OpenCode process exited with code ${this.webProcess.exitCode}`);
|
|
178
|
+
}
|
|
179
|
+
log.info(`OpenCode Web started at ${webUrl}`);
|
|
180
|
+
} catch (e) {
|
|
181
|
+
log.error("OpenCode Web failed to start", { error: e });
|
|
182
|
+
this.sendTaskUpdate("web_start_timeout");
|
|
183
|
+
this.startPromise = null;
|
|
184
|
+
timer.end("\u274C Web start timeout");
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
this.sendTaskUpdate("starting_proxy");
|
|
188
|
+
let proxyStartPort = (_c = this.config.proxyPort) != null ? _c : import_shared.DEFAULT_PROXY_PORT;
|
|
189
|
+
if (proxyStartPort === this.actualWebPort) {
|
|
190
|
+
proxyStartPort = this.actualWebPort + 1;
|
|
191
|
+
log.debug(`Proxy start port conflicts with web port, using ${proxyStartPort} instead`);
|
|
192
|
+
}
|
|
193
|
+
this.actualProxyPort = yield (0, import_system.findAvailablePort)(proxyStartPort, this.config.hostname);
|
|
146
194
|
this.onProxyPortAllocated(this.actualProxyPort);
|
|
147
|
-
if (this.actualProxyPort !== ((
|
|
195
|
+
if (this.actualProxyPort !== ((_d = this.config.proxyPort) != null ? _d : import_shared.DEFAULT_PROXY_PORT)) {
|
|
148
196
|
log.info(
|
|
149
|
-
`Proxy port ${(
|
|
197
|
+
`Proxy port ${(_e = this.config.proxyPort) != null ? _e : import_shared.DEFAULT_PROXY_PORT} is in use, using ${this.actualProxyPort} instead`
|
|
150
198
|
);
|
|
151
199
|
} else {
|
|
152
200
|
log.debug(`Using proxy port ${this.actualProxyPort}`);
|
|
153
201
|
}
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
202
|
+
try {
|
|
203
|
+
const result = yield (0, import_proxy_server.startProxyServer)(webUrl, this.actualProxyPort, {
|
|
204
|
+
theme: this.config.theme,
|
|
205
|
+
language: this.config.language,
|
|
206
|
+
settings: this.config.settings
|
|
207
|
+
});
|
|
208
|
+
this.proxyServer = result.server;
|
|
209
|
+
if (result.actualPort !== this.actualProxyPort) {
|
|
210
|
+
log.info(
|
|
211
|
+
`Proxy port ${this.actualProxyPort} was taken, using ${result.actualPort} instead`
|
|
212
|
+
);
|
|
213
|
+
this.actualProxyPort = result.actualPort;
|
|
214
|
+
this.onProxyPortAllocated(this.actualProxyPort);
|
|
215
|
+
}
|
|
216
|
+
} catch (err) {
|
|
217
|
+
const nodeErr = err;
|
|
218
|
+
if (nodeErr.code === "EADDRINUSE") {
|
|
219
|
+
log.warn(`Proxy port ${this.actualProxyPort} became unavailable, trying next port...`);
|
|
220
|
+
const nextPort = yield (0, import_system.findAvailablePort)(this.actualProxyPort + 1, this.config.hostname);
|
|
221
|
+
const result = yield (0, import_proxy_server.startProxyServer)(webUrl, nextPort, {
|
|
222
|
+
theme: this.config.theme,
|
|
223
|
+
language: this.config.language,
|
|
224
|
+
settings: this.config.settings
|
|
225
|
+
});
|
|
226
|
+
this.proxyServer = result.server;
|
|
227
|
+
this.actualProxyPort = result.actualPort;
|
|
228
|
+
this.onProxyPortAllocated(this.actualProxyPort);
|
|
229
|
+
log.info(`Proxy server started on fallback port ${this.actualProxyPort}`);
|
|
230
|
+
} else {
|
|
231
|
+
throw err;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
159
234
|
timer.checkpoint("Proxy server started");
|
|
160
|
-
|
|
161
|
-
|
|
235
|
+
this.sendTaskUpdate("warming_up_chrome");
|
|
236
|
+
let warmupFailed = false;
|
|
237
|
+
try {
|
|
238
|
+
yield this.api.warmupChromeMcp(viteOrigin);
|
|
239
|
+
timer.checkpoint("Chrome MCP warmup complete");
|
|
240
|
+
} catch (e) {
|
|
241
|
+
log.warn("Chrome MCP warmup failed", { error: e });
|
|
242
|
+
this.chromeMcpWarmupFailed = true;
|
|
243
|
+
warmupFailed = true;
|
|
244
|
+
}
|
|
245
|
+
this.sendTaskUpdate("creating_session");
|
|
246
|
+
let sessionFailed = false;
|
|
162
247
|
try {
|
|
163
248
|
this.sessionUrl = yield this.api.getOrCreateSession();
|
|
164
249
|
timer.checkpoint("Session created");
|
|
165
250
|
log.debug(`Session URL: ${this.sessionUrl}`);
|
|
166
|
-
this.sseClients.forEach((client) => {
|
|
167
|
-
try {
|
|
168
|
-
client.write(
|
|
169
|
-
`data: ${JSON.stringify({ type: "SESSION_READY", sessionUrl: this.sessionUrl })}
|
|
170
|
-
|
|
171
|
-
`
|
|
172
|
-
);
|
|
173
|
-
} catch (e) {
|
|
174
|
-
log.debug("Failed to send SESSION_READY event", { error: e });
|
|
175
|
-
}
|
|
176
|
-
});
|
|
177
251
|
} catch (e) {
|
|
178
252
|
log.warn("Failed to get/create session", { error: e });
|
|
253
|
+
sessionFailed = true;
|
|
254
|
+
}
|
|
255
|
+
if (sessionFailed) {
|
|
256
|
+
this.sendTaskUpdate("session_creation_failed");
|
|
257
|
+
this.isStarted = false;
|
|
258
|
+
this.startPromise = null;
|
|
259
|
+
} else if (warmupFailed) {
|
|
260
|
+
this.sendTaskUpdate("chrome_mcp_failed", { sessionUrl: this.sessionUrl });
|
|
261
|
+
this.isStarted = true;
|
|
262
|
+
} else {
|
|
263
|
+
this.sendTaskUpdate("ready", { sessionUrl: this.sessionUrl });
|
|
264
|
+
}
|
|
265
|
+
if (!sessionFailed) {
|
|
266
|
+
this.isStarted = true;
|
|
267
|
+
} else {
|
|
268
|
+
this.sessionUrl = null;
|
|
179
269
|
}
|
|
180
|
-
this.isStarted = true;
|
|
181
270
|
log.debug(`OpenCode services started successfully: ${this.sessionUrl || webUrl}`);
|
|
182
271
|
timer.end("\u2713 Services started successfully");
|
|
183
272
|
}))();
|
|
184
273
|
return this.startPromise;
|
|
185
274
|
});
|
|
186
275
|
}
|
|
276
|
+
retryWarmupChromeMcp(viteOrigin) {
|
|
277
|
+
return __async(this, null, function* () {
|
|
278
|
+
const success = yield this.api.retryWarmupChromeMcp(viteOrigin);
|
|
279
|
+
if (success) {
|
|
280
|
+
this.chromeMcpWarmupFailed = false;
|
|
281
|
+
this.sendTaskUpdate("ready", { sessionUrl: this.sessionUrl });
|
|
282
|
+
}
|
|
283
|
+
return success;
|
|
284
|
+
});
|
|
285
|
+
}
|
|
187
286
|
stop() {
|
|
188
287
|
return __async(this, null, function* () {
|
|
189
288
|
const timer = log.timer("stopServices");
|
package/lib/endpoints/index.js
CHANGED
|
@@ -26,6 +26,7 @@ var import_context = require("./context.js");
|
|
|
26
26
|
var import_start = require("./start.js");
|
|
27
27
|
var import_sse = require("./sse.js");
|
|
28
28
|
var import_sessions = require("./sessions.js");
|
|
29
|
+
var import_warmup = require("./warmup.js");
|
|
29
30
|
__reExport(endpoints_exports, require("./types.js"), module.exports);
|
|
30
31
|
function setupMiddlewares(server, ctx) {
|
|
31
32
|
(0, import_widget.setupWidgetEndpoints)(server, ctx);
|
|
@@ -33,6 +34,7 @@ function setupMiddlewares(server, ctx) {
|
|
|
33
34
|
(0, import_start.setupStartEndpoint)(server, ctx);
|
|
34
35
|
(0, import_sse.setupSseEndpoint)(server, ctx);
|
|
35
36
|
(0, import_sessions.setupSessionsEndpoint)(server, ctx);
|
|
37
|
+
(0, import_warmup.setupWarmupEndpoint)(server, ctx);
|
|
36
38
|
}
|
|
37
39
|
// Annotate the CommonJS export names for ESM import in node:
|
|
38
40
|
0 && (module.exports = {
|
package/lib/endpoints/sse.js
CHANGED
|
@@ -57,13 +57,22 @@ function setupSseEndpoint(server, ctx) {
|
|
|
57
57
|
res.write(`data: ${JSON.stringify({ type: "CONNECTED" })}
|
|
58
58
|
|
|
59
59
|
`);
|
|
60
|
+
const statusPayload = { type: "STATUS_SYNC" };
|
|
61
|
+
if (ctx.isServiceStarted !== void 0) {
|
|
62
|
+
statusPayload.isStarted = ctx.isServiceStarted;
|
|
63
|
+
}
|
|
64
|
+
if (ctx.currentTask) {
|
|
65
|
+
statusPayload.task = ctx.currentTask.task;
|
|
66
|
+
if (ctx.currentTask.data) {
|
|
67
|
+
Object.assign(statusPayload, ctx.currentTask.data);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
60
70
|
if (ctx.sessionUrl) {
|
|
61
|
-
|
|
62
|
-
`data: ${JSON.stringify({ type: "SESSION_READY", sessionUrl: ctx.sessionUrl })}
|
|
63
|
-
|
|
64
|
-
`
|
|
65
|
-
);
|
|
71
|
+
statusPayload.sessionUrl = ctx.sessionUrl;
|
|
66
72
|
}
|
|
73
|
+
res.write(`data: ${JSON.stringify(statusPayload)}
|
|
74
|
+
|
|
75
|
+
`);
|
|
67
76
|
req.on("close", () => {
|
|
68
77
|
ctx.sseClients.delete(res);
|
|
69
78
|
log.debug("SSE client disconnected", {
|
package/lib/endpoints/types.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { PageContext, SessionInfo } from "@vite-plugin-opencode-assistant/shared";
|
|
1
|
+
import type { PageContext, SessionInfo, ServiceStartupTask } from "@vite-plugin-opencode-assistant/shared";
|
|
2
2
|
import type http from "http";
|
|
3
3
|
export interface EndpointContext {
|
|
4
4
|
get sessionUrl(): string | null;
|
|
@@ -6,9 +6,15 @@ export interface EndpointContext {
|
|
|
6
6
|
get sseClients(): Set<http.ServerResponse>;
|
|
7
7
|
get pageContext(): PageContext;
|
|
8
8
|
set pageContext(ctx: PageContext);
|
|
9
|
+
get isServiceStarted(): boolean;
|
|
10
|
+
get currentTask(): {
|
|
11
|
+
task: ServiceStartupTask;
|
|
12
|
+
data?: Record<string, unknown>;
|
|
13
|
+
} | null;
|
|
9
14
|
getSessions: () => Promise<SessionInfo[]>;
|
|
10
15
|
createSession: () => Promise<SessionInfo>;
|
|
11
16
|
deleteSession: (id: string) => Promise<void>;
|
|
12
17
|
resolveWidgetPath: () => string;
|
|
13
18
|
resolveWidgetStylePath: () => string;
|
|
19
|
+
retryWarmupChromeMcp: () => Promise<boolean>;
|
|
14
20
|
}
|