vite-plugin-opencode-assistant 1.0.3 → 1.0.4

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,20 +1,20 @@
1
- import path from 'path';
2
- import fs from 'fs';
3
- import http from 'http';
4
- import { fileURLToPath } from 'url';
5
- import Inspector from 'unplugin-vue-inspector/vite';
1
+ import path from "path";
2
+ import fs from "fs";
3
+ import http from "http";
4
+ import { fileURLToPath } from "url";
5
+ import Inspector from "unplugin-vue-inspector/vite";
6
6
  const __filename = fileURLToPath(import.meta.url);
7
7
  const __dirname = path.dirname(__filename);
8
- import { startOpenCodeWeb } from '../opencode/web.js';
9
- import { injectWidget } from './injector.js';
10
- import { checkOpenCodeInstalled, findAvailablePort, waitForServer, killOrphanOpenCodeProcesses } from './utils.js';
11
- import { DEFAULT_CONFIG, DEFAULT_RETRIES, RETRY_DELAY, WIDGET_SCRIPT_PATH, CONTEXT_API_PATH, START_API_PATH, SESSIONS_API_PATH, SSE_EVENTS_PATH, SERVER_START_TIMEOUT, } from '../constants.js';
12
- import { setVerbose, PerformanceTimer, RequestContext, createLogger, } from '../logger.js';
8
+ import { startOpenCodeWeb } from "../opencode/web.js";
9
+ import { injectWidget } from "./injector.js";
10
+ import { checkOpenCodeInstalled, findAvailablePort, waitForServer, killOrphanOpenCodeProcesses, } from "./utils.js";
11
+ import { DEFAULT_CONFIG, DEFAULT_RETRIES, RETRY_DELAY, WIDGET_SCRIPT_PATH, CONTEXT_API_PATH, START_API_PATH, SESSIONS_API_PATH, SSE_EVENTS_PATH, SERVER_START_TIMEOUT, } from "../constants.js";
12
+ import { setVerbose, PerformanceTimer, RequestContext, createLogger, } from "../logger.js";
13
13
  export default function opencodePlugin(options = {}) {
14
14
  const plugins = [];
15
15
  plugins.push(...Inspector({
16
16
  enabled: false,
17
- toggleButtonVisibility: 'never',
17
+ toggleButtonVisibility: "never",
18
18
  toggleComboKey: false,
19
19
  }));
20
20
  plugins.push(createOpenCodePlugin(options));
@@ -26,39 +26,39 @@ function createOpenCodePlugin(options = {}) {
26
26
  let actualWebPort = DEFAULT_CONFIG.webPort;
27
27
  let isStarted = false;
28
28
  let startPromise = null;
29
- let pageContext = { url: '', title: '' };
29
+ let pageContext = { url: "", title: "" };
30
30
  const sseClients = new Set();
31
31
  const config = { ...DEFAULT_CONFIG, ...options };
32
32
  setVerbose(config.verbose);
33
- const log = createLogger('Plugin');
33
+ const log = createLogger("Plugin");
34
34
  function base64Encode(str) {
35
- return Buffer.from(str).toString('base64');
35
+ return Buffer.from(str).toString("base64");
36
36
  }
37
37
  function sleep(ms) {
38
- return new Promise(resolve => setTimeout(resolve, ms));
38
+ return new Promise((resolve) => setTimeout(resolve, ms));
39
39
  }
40
40
  function createHttpRequest(options, body) {
41
- const timer = new PerformanceTimer('HTTP Request', {
42
- operation: `${options.method || 'GET'} ${options.path}`
41
+ const timer = new PerformanceTimer("HTTP Request", {
42
+ operation: `${options.method || "GET"} ${options.path}`,
43
43
  });
44
44
  return new Promise((resolve, reject) => {
45
45
  const req = http.request(options, (res) => {
46
- let data = '';
47
- res.on('data', chunk => data += chunk);
48
- res.on('end', () => {
46
+ let data = "";
47
+ res.on("data", (chunk) => (data += chunk));
48
+ res.on("end", () => {
49
49
  try {
50
50
  const result = JSON.parse(data);
51
51
  timer.end(`✓ Status: ${res.statusCode}`);
52
52
  resolve(result);
53
53
  }
54
54
  catch (e) {
55
- timer.end('❌ JSON parse error');
55
+ timer.end("❌ JSON parse error");
56
56
  reject(new Error(`JSON parse error: ${data.substring(0, 100)}`));
57
57
  }
58
58
  });
59
59
  });
60
- req.on('error', (e) => {
61
- timer.end('❌ Request failed');
60
+ req.on("error", (e) => {
61
+ timer.end("❌ Request failed");
62
62
  reject(e);
63
63
  });
64
64
  if (body)
@@ -67,158 +67,301 @@ function createOpenCodePlugin(options = {}) {
67
67
  });
68
68
  }
69
69
  async function getSessions(retries = DEFAULT_RETRIES) {
70
- const timer = log.timer('getSessions', { retries });
70
+ const timer = log.timer("getSessions", { retries });
71
71
  let lastError = null;
72
72
  for (let i = 0; i < retries; i++) {
73
73
  try {
74
- log.debug(`Attempt ${i + 1}/${retries}`, { operation: 'getSessions' });
74
+ log.debug(`Attempt ${i + 1}/${retries}`, { operation: "getSessions" });
75
75
  const sessions = await createHttpRequest({
76
76
  hostname: config.hostname,
77
77
  port: actualWebPort,
78
- path: '/session',
78
+ path: "/session",
79
79
  });
80
80
  timer.end(`Found ${sessions.length} sessions`);
81
81
  return sessions;
82
82
  }
83
83
  catch (e) {
84
84
  lastError = e instanceof Error ? e : new Error(String(e));
85
- log.debug(`Attempt ${i + 1} failed: ${lastError.message}`, { operation: 'getSessions' });
85
+ log.debug(`Attempt ${i + 1} failed: ${lastError.message}`, {
86
+ operation: "getSessions",
87
+ });
86
88
  if (i < retries - 1) {
87
- log.debug(`Retrying in ${RETRY_DELAY}ms...`, { operation: 'getSessions' });
89
+ log.debug(`Retrying in ${RETRY_DELAY}ms...`, {
90
+ operation: "getSessions",
91
+ });
88
92
  await sleep(RETRY_DELAY);
89
93
  }
90
94
  }
91
95
  }
92
- timer.end('❌ All retries exhausted');
96
+ timer.end("❌ All retries exhausted");
93
97
  throw lastError;
94
98
  }
95
- async function createSession(retries = DEFAULT_RETRIES) {
96
- const timer = log.timer('createSession', { retries });
99
+ async function createSession(retries = DEFAULT_RETRIES, title) {
100
+ const timer = log.timer("createSession", { retries, title });
97
101
  let lastError = null;
98
102
  for (let i = 0; i < retries; i++) {
99
103
  try {
100
- log.debug(`Attempt ${i + 1}/${retries}`, { operation: 'createSession' });
104
+ log.debug(`Attempt ${i + 1}/${retries}`, {
105
+ operation: "createSession",
106
+ title,
107
+ });
108
+ const requestBody = title ? JSON.stringify({ title }) : undefined;
101
109
  const session = await createHttpRequest({
102
110
  hostname: config.hostname,
103
111
  port: actualWebPort,
104
- path: '/session',
105
- method: 'POST',
106
- });
112
+ path: "/session",
113
+ method: "POST",
114
+ headers: requestBody
115
+ ? { "Content-Type": "application/json" }
116
+ : undefined,
117
+ }, requestBody);
107
118
  timer.end(`Created session: ${session.id}`);
108
119
  return session;
109
120
  }
110
121
  catch (e) {
111
122
  lastError = e instanceof Error ? e : new Error(String(e));
112
- log.debug(`Attempt ${i + 1} failed: ${lastError.message}`, { operation: 'createSession' });
123
+ log.debug(`Attempt ${i + 1} failed: ${lastError.message}`, {
124
+ operation: "createSession",
125
+ });
113
126
  if (i < retries - 1) {
114
- log.debug(`Retrying in ${RETRY_DELAY}ms...`, { operation: 'createSession' });
127
+ log.debug(`Retrying in ${RETRY_DELAY}ms...`, {
128
+ operation: "createSession",
129
+ });
115
130
  await sleep(RETRY_DELAY);
116
131
  }
117
132
  }
118
133
  }
119
- timer.end('❌ All retries exhausted');
134
+ timer.end("❌ All retries exhausted");
120
135
  throw lastError;
121
136
  }
122
137
  async function deleteSession(sessionId, retries = DEFAULT_RETRIES) {
123
- const timer = log.timer('deleteSession', { sessionId, retries });
138
+ const timer = log.timer("deleteSession", { sessionId, retries });
124
139
  let lastError = null;
125
140
  for (let i = 0; i < retries; i++) {
126
141
  try {
127
- log.debug(`Attempt ${i + 1}/${retries}`, { operation: 'deleteSession', sessionId });
142
+ log.debug(`Attempt ${i + 1}/${retries}`, {
143
+ operation: "deleteSession",
144
+ sessionId,
145
+ });
128
146
  await createHttpRequest({
129
147
  hostname: config.hostname,
130
148
  port: actualWebPort,
131
149
  path: `/session/${sessionId}`,
132
- method: 'DELETE',
150
+ method: "DELETE",
133
151
  });
134
152
  timer.end(`Deleted session: ${sessionId}`);
135
153
  return;
136
154
  }
137
155
  catch (e) {
138
156
  lastError = e instanceof Error ? e : new Error(String(e));
139
- log.debug(`Attempt ${i + 1} failed: ${lastError.message}`, { operation: 'deleteSession', sessionId });
157
+ log.debug(`Attempt ${i + 1} failed: ${lastError.message}`, {
158
+ operation: "deleteSession",
159
+ sessionId,
160
+ });
140
161
  if (i < retries - 1) {
141
- log.debug(`Retrying in ${RETRY_DELAY}ms...`, { operation: 'deleteSession', sessionId });
162
+ log.debug(`Retrying in ${RETRY_DELAY}ms...`, {
163
+ operation: "deleteSession",
164
+ sessionId,
165
+ });
142
166
  await sleep(RETRY_DELAY);
143
167
  }
144
168
  }
145
169
  }
146
- timer.end('❌ All retries exhausted');
170
+ timer.end("❌ All retries exhausted");
147
171
  throw lastError;
148
172
  }
173
+ async function getToolIds(retries = DEFAULT_RETRIES) {
174
+ const timer = log.timer("getToolIds", { retries });
175
+ let lastError = null;
176
+ for (let i = 0; i < retries; i++) {
177
+ try {
178
+ log.debug(`Attempt ${i + 1}/${retries}`, {
179
+ operation: "getToolIds",
180
+ });
181
+ const toolIds = await createHttpRequest({
182
+ hostname: config.hostname,
183
+ port: actualWebPort,
184
+ path: "/experimental/tool/ids",
185
+ });
186
+ timer.end(`Found ${toolIds.length} tools`);
187
+ return toolIds;
188
+ }
189
+ catch (e) {
190
+ lastError = e instanceof Error ? e : new Error(String(e));
191
+ log.debug(`Attempt ${i + 1} failed: ${lastError.message}`, {
192
+ operation: "getToolIds",
193
+ });
194
+ if (i < retries - 1) {
195
+ log.debug(`Retrying in ${RETRY_DELAY}ms...`, {
196
+ operation: "getToolIds",
197
+ });
198
+ await sleep(RETRY_DELAY);
199
+ }
200
+ }
201
+ }
202
+ timer.end("❌ All retries exhausted");
203
+ throw lastError;
204
+ }
205
+ async function warmupChromeMcp(viteOrigin) {
206
+ if (!config.warmupChromeMcp)
207
+ return;
208
+ const timer = log.timer("warmupChromeMcp", { viteOrigin });
209
+ let warmupSessionId = null;
210
+ try {
211
+ const warmupSession = await createSession(DEFAULT_RETRIES, "__chrome_mcp_warmup__");
212
+ warmupSessionId = warmupSession.id;
213
+ let chromeToolIds;
214
+ try {
215
+ const toolIds = await getToolIds();
216
+ chromeToolIds = toolIds.filter((toolId) => /chrome[-_]?devtools/i.test(toolId));
217
+ log.debug("Resolved Chrome MCP tool ids", {
218
+ chromeToolIds,
219
+ });
220
+ }
221
+ catch (e) {
222
+ log.debug("Failed to resolve Chrome MCP tool ids", { error: e });
223
+ }
224
+ const prompt = [
225
+ "Call the browser tool list_pages immediately to establish the Chrome DevTools MCP connection.",
226
+ viteOrigin
227
+ ? `If there are no pages, call new_page with ${viteOrigin}.`
228
+ : "If there are no pages, call new_page with about:blank.",
229
+ "Do not read or modify project files.",
230
+ "Do not use any non-browser tools.",
231
+ "After the tool call is complete, reply with exactly: ready",
232
+ ].join(" ");
233
+ await createHttpRequest({
234
+ hostname: config.hostname,
235
+ port: actualWebPort,
236
+ path: `/session/${warmupSessionId}/message`,
237
+ method: "POST",
238
+ headers: { "Content-Type": "application/json" },
239
+ }, JSON.stringify({
240
+ system: "You are warming up Chrome DevTools MCP during startup. You must use the available browser tools immediately before replying.",
241
+ tools: chromeToolIds?.length ? chromeToolIds : undefined,
242
+ parts: [{ type: "text", text: prompt }],
243
+ }));
244
+ timer.end("Chrome MCP warmed up");
245
+ }
246
+ catch (e) {
247
+ log.warn("Failed to warm up Chrome MCP", { error: e });
248
+ timer.end("Chrome MCP warmup skipped");
249
+ }
250
+ finally {
251
+ if (warmupSessionId) {
252
+ try {
253
+ await deleteSession(warmupSessionId);
254
+ }
255
+ catch (e) {
256
+ log.debug("Failed to delete warmup session", {
257
+ error: e,
258
+ warmupSessionId,
259
+ });
260
+ }
261
+ }
262
+ }
263
+ }
149
264
  async function getOrCreateSession() {
150
- const timer = log.timer('getOrCreateSession');
265
+ const timer = log.timer("getOrCreateSession");
151
266
  const projectDir = process.cwd();
152
- log.debug('Getting sessions...', { projectDir });
267
+ log.debug("Getting sessions...", { projectDir });
153
268
  const sessions = await getSessions();
154
269
  log.debug(`Found ${sessions.length} sessions`, {
155
- sessions: sessions.map(s => ({ id: s.id, directory: s.directory }))
270
+ sessions: sessions.map((s) => ({ id: s.id, directory: s.directory })),
156
271
  });
157
- const matchingSession = sessions.find(s => s.directory === projectDir);
272
+ const matchingSession = sessions.find((s) => s.directory === projectDir);
158
273
  if (matchingSession) {
159
274
  const url = `http://${config.hostname}:${actualWebPort}/${base64Encode(projectDir)}/session/${matchingSession.id}`;
160
275
  timer.end(`Using existing session: ${matchingSession.id}`);
161
276
  return url;
162
277
  }
163
- log.debug('Creating new session...', { projectDir });
278
+ log.debug("Creating new session...", { projectDir });
164
279
  const newSession = await createSession();
165
280
  const url = `http://${config.hostname}:${actualWebPort}/${base64Encode(projectDir)}/session/${newSession.id}`;
166
281
  timer.end(`Created new session: ${newSession.id}`);
167
282
  return url;
168
283
  }
169
284
  function setupOpenCodePlugin() {
170
- const timer = log.timer('setupOpenCodePlugin');
285
+ const timer = log.timer("setupOpenCodePlugin");
171
286
  const projectDir = process.cwd();
172
- const cacheDir = path.join(projectDir, 'node_modules', '.cache', 'opencode');
173
- const pluginsDir = path.join(cacheDir, 'plugins');
174
- log.debug('Setting up plugin directory', { cacheDir, pluginsDir });
287
+ const cacheDir = path.join(projectDir, "node_modules", ".cache", "opencode");
288
+ const pluginsDir = path.join(cacheDir, "plugins");
289
+ log.debug("Setting up plugin directory", { cacheDir, pluginsDir });
175
290
  if (!fs.existsSync(pluginsDir)) {
176
291
  fs.mkdirSync(pluginsDir, { recursive: true });
177
- log.debug('Created plugins directory', { pluginsDir });
292
+ log.debug("Created plugins directory", { pluginsDir });
178
293
  }
179
- const pluginSourcePath = path.join(__dirname, '..', 'opencode', 'plugins', 'page-context.js');
180
- const pluginTargetPath = path.join(pluginsDir, 'page-context.js');
294
+ const pluginSourcePath = path.join(__dirname, "..", "opencode", "plugins", "page-context.js");
295
+ const pluginTargetPath = path.join(pluginsDir, "page-context.js");
181
296
  if (fs.existsSync(pluginSourcePath)) {
182
297
  fs.copyFileSync(pluginSourcePath, pluginTargetPath);
183
- log.debug('Plugin installed', { source: pluginSourcePath, target: pluginTargetPath });
298
+ log.debug("Plugin installed", {
299
+ source: pluginSourcePath,
300
+ target: pluginTargetPath,
301
+ });
184
302
  }
185
303
  else {
186
- log.warn('Plugin source not found', { path: pluginSourcePath });
304
+ log.warn("Plugin source not found", { path: pluginSourcePath });
187
305
  }
306
+ // 生成内置的 Chrome DevTools MCP 配置
307
+ const mcpConfig = {
308
+ mcp: {
309
+ "chrome-devtools": {
310
+ type: "local",
311
+ command: ["npx", "-y", "chrome-devtools-mcp@latest", "--autoConnect"],
312
+ enabled: true,
313
+ },
314
+ },
315
+ };
316
+ const mcpConfigPath = path.join(cacheDir, "opencode.json");
317
+ fs.writeFileSync(mcpConfigPath, JSON.stringify(mcpConfig, null, 2));
318
+ log.debug("Created OpenCode MCP config", { mcpConfigPath });
188
319
  timer.end();
189
320
  return cacheDir;
190
321
  }
191
- async function startServices(corsOrigins, contextApiUrl) {
322
+ async function startServices(corsOrigins, contextApiUrl, viteOrigin) {
192
323
  if (isStarted && webProcess) {
193
- log.debug('Services already started, skipping');
324
+ log.debug("Services already started, skipping");
194
325
  return;
195
326
  }
196
327
  if (startPromise) {
197
- log.debug('Waiting for existing start promise');
328
+ log.debug("Waiting for existing start promise");
198
329
  return startPromise;
199
330
  }
200
331
  startPromise = (async () => {
201
- const timer = log.timer('startServices', { corsOrigins, contextApiUrl });
202
- log.info('Starting OpenCode services...');
332
+ const timer = log.timer("startServices", {
333
+ corsOrigins,
334
+ contextApiUrl,
335
+ viteOrigin,
336
+ });
337
+ log.info("Starting OpenCode services...");
203
338
  const orphanCount = await killOrphanOpenCodeProcesses();
204
339
  if (orphanCount > 0) {
205
340
  log.debug(`Killed ${orphanCount} orphan OpenCode process(es)`);
206
341
  }
207
- if (!await checkOpenCodeInstalled()) {
342
+ if (!(await checkOpenCodeInstalled())) {
208
343
  log.error(`OpenCode is not installed!
209
344
 
210
345
  Please install OpenCode first:
211
346
 
212
- # Using Homebrew (macOS)
213
- brew install opencode-ai/tap/opencode
214
-
215
- # Or using the install script
347
+ # YOLO
216
348
  curl -fsSL https://opencode.ai/install | bash
349
+
350
+ # Package managers
351
+ npm i -g opencode-ai@latest # or bun/pnpm/yarn
352
+ scoop install opencode # Windows
353
+ choco install opencode # Windows
354
+ brew install anomalyco/tap/opencode # macOS and Linux (recommended, always up to date)
355
+ brew install opencode # macOS and Linux (official brew formula, updated less)
356
+ sudo pacman -S opencode # Arch Linux (Stable)
357
+ paru -S opencode-bin # Arch Linux (Latest from AUR)
358
+ mise use -g opencode # Any OS
359
+ nix run nixpkgs#opencode # or github:anomalyco/opencode for latest dev branch
217
360
  `);
218
- timer.end('❌ OpenCode not installed');
361
+ timer.end("❌ OpenCode not installed");
219
362
  return;
220
363
  }
221
- timer.checkpoint('OpenCode installation verified');
364
+ timer.checkpoint("OpenCode installation verified");
222
365
  actualWebPort = await findAvailablePort(config.webPort, config.hostname);
223
366
  if (actualWebPort !== config.webPort) {
224
367
  log.info(`Port ${config.webPort} is in use, using ${actualWebPort} instead`);
@@ -226,111 +369,128 @@ Please install OpenCode first:
226
369
  else {
227
370
  log.debug(`Using port ${actualWebPort}`);
228
371
  }
229
- timer.checkpoint('Port allocated');
372
+ timer.checkpoint("Port allocated");
230
373
  const configDir = setupOpenCodePlugin();
231
- timer.checkpoint('Plugin setup complete');
232
- log.debug('Starting OpenCode Web process...', {
374
+ timer.checkpoint("Plugin setup complete");
375
+ log.debug("Starting OpenCode Web process...", {
233
376
  port: actualWebPort,
234
377
  hostname: config.hostname,
235
- configDir
378
+ configDir,
236
379
  });
237
380
  webProcess = startOpenCodeWeb({
238
381
  port: actualWebPort,
239
382
  hostname: config.hostname,
240
- serverUrl: '',
383
+ serverUrl: "",
241
384
  cwd: process.cwd(),
242
385
  configDir,
243
386
  corsOrigins,
244
387
  contextApiUrl,
245
388
  });
246
- timer.checkpoint('Web process started');
247
- await waitForServer(`http://${config.hostname}:${actualWebPort}`, SERVER_START_TIMEOUT);
248
- log.info(`OpenCode Web started at http://${config.hostname}:${actualWebPort}`);
389
+ timer.checkpoint("Web process started");
390
+ const webUrl = `http://${config.hostname}:${actualWebPort}`;
391
+ log.info(`Waiting for OpenCode Web to become ready at ${webUrl}...`);
392
+ await waitForServer(webUrl, SERVER_START_TIMEOUT);
393
+ log.info(`OpenCode Web started at ${webUrl}`);
394
+ await warmupChromeMcp(viteOrigin);
395
+ timer.checkpoint("Chrome MCP warmup complete");
249
396
  try {
250
397
  sessionUrl = await getOrCreateSession();
251
- timer.checkpoint('Session created');
398
+ timer.checkpoint("Session created");
252
399
  log.debug(`Session URL: ${sessionUrl}`);
400
+ sseClients.forEach((client) => {
401
+ try {
402
+ client.write(`data: ${JSON.stringify({ type: "SESSION_READY", sessionUrl })}\n\n`);
403
+ }
404
+ catch (e) {
405
+ log.debug("Failed to send SESSION_READY event", { error: e });
406
+ }
407
+ });
253
408
  }
254
409
  catch (e) {
255
- log.warn('Failed to get/create session', { error: e });
410
+ log.warn("Failed to get/create session", { error: e });
256
411
  }
257
412
  isStarted = true;
258
- timer.end('✓ Services started successfully');
413
+ log.debug(`OpenCode services started successfully: ${sessionUrl || webUrl}`);
414
+ timer.end("✓ Services started successfully");
259
415
  })();
260
416
  return startPromise;
261
417
  }
262
418
  async function stopServices() {
263
- const timer = log.timer('stopServices');
264
- log.info('Stopping OpenCode services...');
419
+ const timer = log.timer("stopServices");
420
+ log.info("Stopping OpenCode services...");
265
421
  if (webProcess) {
266
- log.debug('Killing web process', { pid: webProcess.pid });
267
- webProcess.kill('SIGTERM');
422
+ log.debug("Killing web process", { pid: webProcess.pid });
423
+ webProcess.kill("SIGTERM");
268
424
  webProcess = null;
269
425
  }
270
426
  isStarted = false;
271
427
  startPromise = null;
272
- timer.end('✓ Services stopped');
428
+ timer.end("✓ Services stopped");
273
429
  }
274
430
  function handleContextRequest(req, res) {
275
- const ctx = new RequestContext(req.method || 'GET', CONTEXT_API_PATH);
276
- res.setHeader('Content-Type', 'application/json');
277
- res.setHeader('Access-Control-Allow-Origin', '*');
278
- res.setHeader('Access-Control-Allow-Methods', 'GET, POST, DELETE, OPTIONS');
279
- res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
280
- if (req.method === 'OPTIONS') {
431
+ const ctx = new RequestContext(req.method || "GET", CONTEXT_API_PATH);
432
+ res.setHeader("Content-Type", "application/json");
433
+ res.setHeader("Access-Control-Allow-Origin", "*");
434
+ res.setHeader("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS");
435
+ res.setHeader("Access-Control-Allow-Headers", "Content-Type");
436
+ if (req.method === "OPTIONS") {
281
437
  res.writeHead(200);
282
438
  res.end();
283
439
  ctx.end(200);
284
440
  return;
285
441
  }
286
- if (req.method === 'GET') {
442
+ if (req.method === "GET") {
287
443
  res.writeHead(200);
288
444
  res.end(JSON.stringify(pageContext));
289
445
  ctx.end(200);
290
446
  return;
291
447
  }
292
- if (req.method === 'DELETE') {
448
+ if (req.method === "DELETE") {
293
449
  pageContext.selectedElements = [];
294
- log.debug('Selected elements cleared', { sseClients: sseClients.size });
450
+ log.debug("Selected elements cleared", { sseClients: sseClients.size });
295
451
  let sentCount = 0;
296
- sseClients.forEach(client => {
452
+ sseClients.forEach((client) => {
297
453
  try {
298
- client.write(`data: ${JSON.stringify({ type: 'CLEAR_ELEMENTS' })}\n\n`);
454
+ client.write(`data: ${JSON.stringify({ type: "CLEAR_ELEMENTS" })}\n\n`);
299
455
  sentCount++;
300
456
  }
301
457
  catch (e) {
302
- log.debug('Failed to send SSE message', { error: e });
458
+ log.debug("Failed to send SSE message", { error: e });
303
459
  }
304
460
  });
305
- log.debug('SSE messages sent', { count: sentCount, totalClients: sseClients.size });
461
+ log.debug("SSE messages sent", {
462
+ count: sentCount,
463
+ totalClients: sseClients.size,
464
+ });
306
465
  res.writeHead(200);
307
466
  res.end(JSON.stringify({ success: true }));
308
467
  ctx.end(200);
309
468
  return;
310
469
  }
311
- if (req.method === 'POST') {
312
- let body = '';
313
- req.on('data', chunk => body += chunk);
314
- req.on('end', () => {
470
+ if (req.method === "POST") {
471
+ let body = "";
472
+ req.on("data", (chunk) => (body += chunk));
473
+ req.on("end", () => {
315
474
  try {
316
475
  const data = JSON.parse(body);
317
476
  pageContext = {
318
- url: data.url || '',
319
- title: data.title || '',
477
+ url: data.url || "",
478
+ title: data.title || "",
320
479
  selectedElements: data.selectedElements || [],
321
480
  };
322
- log.debug('Context updated', {
481
+ log.debug("Context updated", {
323
482
  url: pageContext.url,
324
483
  title: pageContext.title,
325
- selectedElementsCount: pageContext.selectedElements?.length || 0
484
+ selectedElementsCount: pageContext.selectedElements?.length || 0,
326
485
  });
327
- if (pageContext.selectedElements && pageContext.selectedElements.length > 0) {
328
- log.debug('Selected elements details', {
329
- elements: pageContext.selectedElements.map(el => ({
486
+ if (pageContext.selectedElements &&
487
+ pageContext.selectedElements.length > 0) {
488
+ log.debug("Selected elements details", {
489
+ elements: pageContext.selectedElements.map((el) => ({
330
490
  filePath: el.filePath,
331
491
  line: el.line,
332
- text: el.innerText?.substring(0, 50)
333
- }))
492
+ text: el.innerText?.substring(0, 50),
493
+ })),
334
494
  });
335
495
  }
336
496
  res.writeHead(200);
@@ -338,38 +498,39 @@ Please install OpenCode first:
338
498
  ctx.end(200);
339
499
  }
340
500
  catch (e) {
341
- log.debug('Invalid JSON in request body', { error: e });
501
+ log.debug("Invalid JSON in request body", { error: e });
342
502
  res.writeHead(400);
343
- res.end(JSON.stringify({ error: 'Invalid JSON' }));
503
+ res.end(JSON.stringify({ error: "Invalid JSON" }));
344
504
  ctx.end(400);
345
505
  }
346
506
  });
347
507
  return;
348
508
  }
349
509
  res.writeHead(405);
350
- res.end(JSON.stringify({ error: 'Method not allowed' }));
510
+ res.end(JSON.stringify({ error: "Method not allowed" }));
351
511
  ctx.end(405);
352
512
  }
353
513
  return {
354
- name: 'vite-plugin-opencode',
514
+ name: "vite-plugin-opencode",
515
+ apply(_viteConfig, env) {
516
+ if (!config.enabled)
517
+ return false;
518
+ return env.command === "serve" && process.env.NODE_ENV !== "test";
519
+ },
355
520
  async configureServer(server) {
356
- if (!config.enabled) {
357
- log.debug('Plugin disabled, skipping configuration');
358
- return;
359
- }
360
- const timer = log.timer('configureServer');
521
+ const timer = log.timer("configureServer");
361
522
  server.middlewares.use(WIDGET_SCRIPT_PATH, async (_req, res) => {
362
- const ctx = new RequestContext('GET', WIDGET_SCRIPT_PATH);
363
- const widgetPath = path.join(__dirname, 'client.js');
523
+ const ctx = new RequestContext("GET", WIDGET_SCRIPT_PATH);
524
+ const widgetPath = path.join(__dirname, "client.js");
364
525
  if (fs.existsSync(widgetPath)) {
365
- res.setHeader('Content-Type', 'application/javascript');
366
- res.setHeader('Access-Control-Allow-Origin', '*');
526
+ res.setHeader("Content-Type", "application/javascript");
527
+ res.setHeader("Access-Control-Allow-Origin", "*");
367
528
  fs.createReadStream(widgetPath).pipe(res);
368
529
  ctx.end(200);
369
530
  }
370
531
  else {
371
532
  res.writeHead(404);
372
- res.end('Widget script not found');
533
+ res.end("Widget script not found");
373
534
  ctx.end(404);
374
535
  }
375
536
  });
@@ -377,63 +538,68 @@ Please install OpenCode first:
377
538
  handleContextRequest(req, res);
378
539
  });
379
540
  server.middlewares.use(START_API_PATH, async (_req, res) => {
380
- const ctx = new RequestContext('GET', START_API_PATH);
381
- res.setHeader('Content-Type', 'application/json');
382
- res.setHeader('Access-Control-Allow-Origin', '*');
541
+ const ctx = new RequestContext("GET", START_API_PATH);
542
+ res.setHeader("Content-Type", "application/json");
543
+ res.setHeader("Access-Control-Allow-Origin", "*");
383
544
  res.writeHead(200);
384
545
  res.end(JSON.stringify({ success: true, sessionUrl }));
385
546
  ctx.end(200);
386
547
  });
387
548
  server.middlewares.use(SSE_EVENTS_PATH, async (req, res) => {
388
- const ctx = new RequestContext('GET', SSE_EVENTS_PATH);
549
+ const ctx = new RequestContext("GET", SSE_EVENTS_PATH);
389
550
  res.writeHead(200, {
390
- 'Content-Type': 'text/event-stream',
391
- 'Cache-Control': 'no-cache',
392
- 'Connection': 'keep-alive',
393
- 'Access-Control-Allow-Origin': '*',
551
+ "Content-Type": "text/event-stream",
552
+ "Cache-Control": "no-cache",
553
+ Connection: "keep-alive",
554
+ "Access-Control-Allow-Origin": "*",
394
555
  });
395
556
  sseClients.add(res);
396
- log.debug('SSE client connected', { totalClients: sseClients.size });
397
- res.write(`data: ${JSON.stringify({ type: 'CONNECTED' })}\n\n`);
398
- req.on('close', () => {
557
+ log.debug("SSE client connected", { totalClients: sseClients.size });
558
+ res.write(`data: ${JSON.stringify({ type: "CONNECTED" })}\n\n`);
559
+ if (sessionUrl) {
560
+ res.write(`data: ${JSON.stringify({ type: "SESSION_READY", sessionUrl })}\n\n`);
561
+ }
562
+ req.on("close", () => {
399
563
  sseClients.delete(res);
400
- log.debug('SSE client disconnected', { totalClients: sseClients.size });
564
+ log.debug("SSE client disconnected", {
565
+ totalClients: sseClients.size,
566
+ });
401
567
  });
402
568
  ctx.end(200);
403
569
  });
404
570
  server.middlewares.use(SESSIONS_API_PATH, async (req, res) => {
405
- const ctx = new RequestContext(req.method || 'GET', SESSIONS_API_PATH);
406
- res.setHeader('Content-Type', 'application/json');
407
- res.setHeader('Access-Control-Allow-Origin', '*');
408
- res.setHeader('Access-Control-Allow-Methods', 'GET, POST, DELETE, OPTIONS');
409
- res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
410
- if (req.method === 'OPTIONS') {
571
+ const ctx = new RequestContext(req.method || "GET", SESSIONS_API_PATH);
572
+ res.setHeader("Content-Type", "application/json");
573
+ res.setHeader("Access-Control-Allow-Origin", "*");
574
+ res.setHeader("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS");
575
+ res.setHeader("Access-Control-Allow-Headers", "Content-Type");
576
+ if (req.method === "OPTIONS") {
411
577
  res.writeHead(200);
412
578
  res.end();
413
579
  ctx.end(200);
414
580
  return;
415
581
  }
416
582
  try {
417
- if (req.method === 'GET') {
418
- ctx.checkpoint('Fetching sessions');
583
+ if (req.method === "GET") {
584
+ ctx.checkpoint("Fetching sessions");
419
585
  const sessions = await getSessions();
420
586
  res.writeHead(200);
421
587
  res.end(JSON.stringify(sessions));
422
588
  ctx.end(200);
423
589
  }
424
- else if (req.method === 'POST') {
425
- ctx.checkpoint('Creating session');
590
+ else if (req.method === "POST") {
591
+ ctx.checkpoint("Creating session");
426
592
  const newSession = await createSession();
427
593
  res.writeHead(200);
428
594
  res.end(JSON.stringify(newSession));
429
595
  ctx.end(200);
430
596
  }
431
- else if (req.method === 'DELETE') {
432
- const url = new URL(req.url || '', `http://${req.headers.host}`);
433
- const sessionId = url.searchParams.get('id');
597
+ else if (req.method === "DELETE") {
598
+ const url = new URL(req.url || "", `http://${req.headers.host}`);
599
+ const sessionId = url.searchParams.get("id");
434
600
  if (!sessionId) {
435
601
  res.writeHead(400);
436
- res.end(JSON.stringify({ error: 'Session ID is required' }));
602
+ res.end(JSON.stringify({ error: "Session ID is required" }));
437
603
  ctx.end(400);
438
604
  return;
439
605
  }
@@ -445,27 +611,27 @@ Please install OpenCode first:
445
611
  }
446
612
  else {
447
613
  res.writeHead(405);
448
- res.end(JSON.stringify({ error: 'Method not allowed' }));
614
+ res.end(JSON.stringify({ error: "Method not allowed" }));
449
615
  ctx.end(405);
450
616
  }
451
617
  }
452
618
  catch (e) {
453
- log.error('Session API error', { error: e, method: req.method });
619
+ log.error("Session API error", { error: e, method: req.method });
454
620
  res.writeHead(500);
455
621
  res.end(JSON.stringify({ error: String(e) }));
456
622
  ctx.error(e);
457
623
  }
458
624
  });
459
- server.httpServer?.on('listening', async () => {
460
- log.debug('Vite server listening event fired');
625
+ server.httpServer?.on("listening", async () => {
626
+ log.debug("Vite server listening event fired");
461
627
  const address = server.httpServer?.address();
462
628
  let vitePort;
463
629
  let viteHost;
464
- if (address && typeof address === 'object') {
630
+ if (address && typeof address === "object") {
465
631
  vitePort = address.port;
466
632
  const addr = address.address;
467
- if (addr === '::' || addr === '::1' || addr === '0.0.0.0' || !addr) {
468
- viteHost = 'localhost';
633
+ if (addr === "::" || addr === "::1" || addr === "0.0.0.0" || !addr) {
634
+ viteHost = "localhost";
469
635
  }
470
636
  else {
471
637
  viteHost = addr;
@@ -474,33 +640,44 @@ Please install OpenCode first:
474
640
  else {
475
641
  const host = server.config.server.host;
476
642
  vitePort = server.config.server.port || 5173;
477
- viteHost = typeof host === 'string' && host !== '0.0.0.0' && host !== '::' && host !== '::1' ? host : 'localhost';
643
+ viteHost =
644
+ typeof host === "string" &&
645
+ host !== "0.0.0.0" &&
646
+ host !== "::" &&
647
+ host !== "::1"
648
+ ? host
649
+ : "localhost";
478
650
  }
479
651
  const viteOrigin = `http://${viteHost}:${vitePort}`;
480
652
  const contextApiUrl = `http://${viteHost}:${vitePort}${CONTEXT_API_PATH}`;
481
- log.debug('Vite server ready', { vitePort, viteHost, viteOrigin, contextApiUrl });
653
+ log.debug("Vite server ready", {
654
+ vitePort,
655
+ viteHost,
656
+ viteOrigin,
657
+ contextApiUrl,
658
+ });
482
659
  try {
483
- await startServices([viteOrigin], contextApiUrl);
660
+ await startServices([viteOrigin], contextApiUrl, viteOrigin);
484
661
  }
485
662
  catch (e) {
486
- log.error('Failed to start services', { error: e });
663
+ log.error("Failed to start services", { error: e });
487
664
  }
488
665
  });
489
- server.httpServer?.on('close', () => {
490
- log.debug('HTTP server closing');
666
+ server.httpServer?.on("close", () => {
667
+ log.debug("HTTP server closing");
491
668
  stopServices();
492
669
  });
493
670
  const cleanup = async () => {
494
- log.debug('Process cleanup triggered');
671
+ log.debug("Process cleanup triggered");
495
672
  await stopServices();
496
673
  process.exit(0);
497
674
  };
498
- process.on('SIGINT', cleanup);
499
- process.on('SIGTERM', cleanup);
500
- timer.end('✓ Server configured');
675
+ process.on("SIGINT", cleanup);
676
+ process.on("SIGTERM", cleanup);
677
+ timer.end("✓ Server configured");
501
678
  },
502
679
  transformIndexHtml(html) {
503
- const timer = log.timer('transformIndexHtml');
680
+ const timer = log.timer("transformIndexHtml");
504
681
  const widget = injectWidget({
505
682
  webUrl: `http://${config.hostname}:${actualWebPort}`,
506
683
  serverUrl: `http://${config.hostname}:${actualWebPort}`,
@@ -513,7 +690,7 @@ Please install OpenCode first:
513
690
  hotkey: config.hotkey,
514
691
  });
515
692
  timer.end();
516
- return html.replace('</body>', `${widget}</body>`);
693
+ return html.replace("</body>", `${widget}</body>`);
517
694
  },
518
695
  };
519
696
  }