From 2706befa67b92c37a3ab0116d676023024e6093e Mon Sep 17 00:00:00 2001 From: Igor Barcik Date: Tue, 27 Jan 2026 21:38:18 +0100 Subject: [PATCH 1/4] fix(auth): improve Windsurf process identifying and port detection --- src/plugin/auth.ts | 121 ++++++++++++++++++++++----------------------- 1 file changed, 59 insertions(+), 62 deletions(-) diff --git a/src/plugin/auth.ts b/src/plugin/auth.ts index 04efc8c..b33edef 100644 --- a/src/plugin/auth.ts +++ b/src/plugin/auth.ts @@ -70,6 +70,13 @@ const LANGUAGE_SERVER_PATTERNS = { win32: 'language_server_windows', } as const; +// Windsurf log directories for port discovery +const WINDSURF_LOG_PATHS = { + darwin: path.join(os.homedir(), 'Library/Application Support/Windsurf/logs'), + linux: path.join(os.homedir(), '.config/Windsurf/logs'), + win32: path.join(os.homedir(), 'AppData/Roaming/Windsurf/logs'), +} as const; + // ============================================================================ // Process Discovery // ============================================================================ @@ -84,10 +91,11 @@ function getLanguageServerPattern(): string { /** * Get process listing for language server + * Filters specifically for Windsurf's language server (not Antigravity's) */ function getLanguageServerProcess(): string | null { const pattern = getLanguageServerPattern(); - + try { if (process.platform === 'win32') { // Windows: use WMIC @@ -95,14 +103,19 @@ function getLanguageServerProcess(): string | null { `wmic process where "name like '%${pattern}%'" get CommandLine /format:list`, { encoding: 'utf8', timeout: 5000 } ); - return output; + // Filter for Windsurf-specific lines (path contains /windsurf/ or \windsurf\ or has --ide_name windsurf) + const lines = output.split('\n').filter(line => + line.includes('/windsurf/') || line.includes('\\windsurf\\') || line.includes('--ide_name windsurf') + ); + return lines.length > 0 ? lines.join('\n') : null; } else { - // Unix-like: use ps + // Unix-like: use ps and filter for Windsurf-specific process + // Use /windsurf/ in path or --ide_name windsurf to avoid matching other language servers const output = execSync( - `ps aux | grep ${pattern}`, + `ps aux | grep ${pattern} | grep -E "/windsurf/|--ide_name windsurf" | grep -v grep`, { encoding: 'utf8', timeout: 5000 } ); - return output; + return output.trim() || null; } } catch { return null; @@ -114,19 +127,19 @@ function getLanguageServerProcess(): string | null { */ export function getCSRFToken(): string { const processInfo = getLanguageServerProcess(); - + if (!processInfo) { throw new WindsurfError( 'Windsurf language server not found. Is Windsurf running?', WindsurfErrorCode.NOT_RUNNING ); } - + const match = processInfo.match(/--csrf_token\s+([a-f0-9-]+)/); if (match?.[1]) { return match[1]; } - + throw new WindsurfError( 'CSRF token not found in Windsurf process. Is Windsurf running?', WindsurfErrorCode.CSRF_MISSING @@ -134,63 +147,47 @@ export function getCSRFToken(): string { } /** - * Get the language server gRPC port dynamically using lsof - * The port offset from extension_server_port varies (--random_port flag), so we use lsof + * Get the language server gRPC port from Windsurf log files + * Parses the most recent "Language server listening on random port at XXXXX" log entry */ export function getPort(): number { - const processInfo = getLanguageServerProcess(); - - if (!processInfo) { + const platform = process.platform as keyof typeof WINDSURF_LOG_PATHS; + const logsDir = WINDSURF_LOG_PATHS[platform]; + + if (!logsDir || !fs.existsSync(logsDir)) { throw new WindsurfError( - 'Windsurf language server not found. Is Windsurf running?', + `Windsurf logs directory not found at ${logsDir}. Is Windsurf installed?`, WindsurfErrorCode.NOT_RUNNING ); } - - // Extract PID from ps output (second column) - const pidMatch = processInfo.match(/^\s*\S+\s+(\d+)/); - const pid = pidMatch ? pidMatch[1] : null; - - // Get extension_server_port as a reference point - const portMatch = processInfo.match(/--extension_server_port\s+(\d+)/); - const extPort = portMatch ? parseInt(portMatch[1], 10) : null; - - // Use lsof to find actual listening ports for this specific PID - if (process.platform !== 'win32' && pid) { - try { - const lsof = execSync( - `lsof -p ${pid} -i -P -n 2>/dev/null | grep LISTEN`, - { encoding: 'utf8', timeout: 15000 } - ); - - // Extract all listening ports - const portMatches = lsof.matchAll(/:(\d+)\s+\(LISTEN\)/g); - const ports = Array.from(portMatches).map(m => parseInt(m[1], 10)); - - if (ports.length > 0) { - // If we have extension_server_port, prefer the port closest to it (usually +3) - if (extPort) { - // Sort by distance from extPort and pick the closest one > extPort - const candidatePorts = ports.filter(p => p > extPort).sort((a, b) => a - b); - if (candidatePorts.length > 0) { - return candidatePorts[0]; // Return the first port after extPort - } - } - // Otherwise just return the first listening port - return ports[0]; + + try { + // Search for port in log files and get the most recent entry + // Log line format: "2026-01-27 11:46:40.251 [info] ... Language server listening on random port at 41085" + let grepCmd: string; + if (process.platform === 'win32') { + // Windows: use findstr + grepCmd = `findstr /s /r "Language server listening on random port at" "${logsDir}\\*Windsurf.log"`; + } else { + // Unix-like: use grep with recursive search + grepCmd = `grep -rh "Language server listening on random port at" "${logsDir}" 2>/dev/null | sort | tail -1`; + } + + const output = execSync(grepCmd, { encoding: 'utf8', timeout: 10000 }).trim(); + + if (output) { + // Extract port from the log line + const portMatch = output.match(/Language server listening on random port at (\d+)/); + if (portMatch?.[1]) { + return parseInt(portMatch[1], 10); } - } catch { - // Fall through to offset-based approach } + } catch { + // Fall through to error } - - // Fallback: try common offsets (+3, +2, +4) - if (extPort) { - return extPort + 3; - } - + throw new WindsurfError( - 'Windsurf language server port not found. Is Windsurf running?', + 'Windsurf language server port not found in logs. Is Windsurf running?', WindsurfErrorCode.NOT_RUNNING ); } @@ -206,14 +203,14 @@ export function getPort(): number { export function getApiKey(): string { const platform = process.platform as keyof typeof VSCODE_STATE_PATHS; const statePath = VSCODE_STATE_PATHS[platform]; - + if (!statePath) { throw new WindsurfError( `Unsupported platform: ${process.platform}`, WindsurfErrorCode.API_KEY_MISSING ); } - + // Try to get API key from VSCode state database if (fs.existsSync(statePath)) { try { @@ -221,7 +218,7 @@ export function getApiKey(): string { `sqlite3 "${statePath}" "SELECT value FROM ItemTable WHERE key = 'windsurfAuthStatus';"`, { encoding: 'utf8', timeout: 5000 } ).trim(); - + if (result) { const parsed = JSON.parse(result); if (parsed.apiKey) { @@ -232,7 +229,7 @@ export function getApiKey(): string { // Fall through to legacy config } } - + // Try legacy config file if (fs.existsSync(LEGACY_CONFIG_PATH)) { try { @@ -245,7 +242,7 @@ export function getApiKey(): string { // Fall through } } - + throw new WindsurfError( 'API key not found. Please login to Windsurf first.', WindsurfErrorCode.API_KEY_MISSING @@ -257,7 +254,7 @@ export function getApiKey(): string { */ export function getWindsurfVersion(): string { const processInfo = getLanguageServerProcess(); - + if (processInfo) { const match = processInfo.match(/--windsurf_version\s+([^\s]+)/); if (match) { @@ -266,7 +263,7 @@ export function getWindsurfVersion(): string { return version; } } - + // Default fallback version return '1.13.104'; } From a899eabf16fc94658ba5d3fe04adacf0fd9ddb22 Mon Sep 17 00:00:00 2001 From: Igor Barcik Date: Tue, 27 Jan 2026 21:38:23 +0100 Subject: [PATCH 2/4] feat(plugin): add file-based logging for debugging --- src/plugin.ts | 102 ++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 73 insertions(+), 29 deletions(-) diff --git a/src/plugin.ts b/src/plugin.ts index 8af1d3b..bf01068 100644 --- a/src/plugin.ts +++ b/src/plugin.ts @@ -15,6 +15,9 @@ */ import * as crypto from 'crypto'; +import * as fs from 'fs'; +import * as path from 'path'; +import * as os from 'os'; import type { PluginInput, Hooks } from '@opencode-ai/plugin'; import { getCredentials, isWindsurfRunning, WindsurfCredentials } from './plugin/auth.js'; import { streamChatGenerator, ChatMessage } from './plugin/grpc-client.js'; @@ -26,6 +29,29 @@ import { } from './plugin/models.js'; import { PLUGIN_ID } from './constants.js'; +// ============================================================================ +// Logging Helper +// ============================================================================ + +const LOG_FILE = path.join(os.homedir(), '.local', 'share', 'opencode', 'windsurf-plugin.log'); + +function pluginLog(message: string): void { + const timestamp = new Date().toISOString(); + const line = `[${timestamp}] ${message}\n`; + try { + // Ensure directory exists + const dir = path.dirname(LOG_FILE); + if (!fs.existsSync(dir)) { + fs.mkdirSync(dir, { recursive: true }); + } + fs.appendFileSync(LOG_FILE, line); + } catch { + // Silently ignore logging errors + } + // Also log to console for cases where stdout is visible + console.log(`[windsurf-auth] ${message}`); +} + // ============================================================================ // Types // ============================================================================ @@ -652,11 +678,11 @@ async function ensureWindsurfProxyServer(): Promise { owned_by: 'windsurf', ...(variants ? { - variants: Object.entries(variants).map(([name, meta]) => ({ - id: name, - description: meta.description, - })), - } + variants: Object.entries(variants).map(([name, meta]) => ({ + id: name, + description: meta.description, + })), + } : {}), }; }), @@ -758,6 +784,15 @@ async function ensureWindsurfProxyServer(): Promise { const server = startServer(WINDSURF_PROXY_DEFAULT_PORT); const baseURL = `http://${WINDSURF_PROXY_HOST}:${server.port}/v1`; g[key].baseURL = baseURL; + pluginLog(`Proxy server started on port ${server.port}`); + if (isWindsurfRunning()) { + try { + const credentials = getCredentials(); + pluginLog(`Connected to Windsurf language server on port ${credentials.port}`); + } catch (e) { + pluginLog(`Windsurf running but credentials not available: ${e instanceof Error ? e.message : e}`); + } + } return baseURL; } catch (error) { const code = (error as any)?.code; @@ -781,6 +816,15 @@ async function ensureWindsurfProxyServer(): Promise { const server = startServer(0); const baseURL = `http://${WINDSURF_PROXY_HOST}:${server.port}/v1`; g[key].baseURL = baseURL; + pluginLog(`Proxy server started on fallback port ${server.port}`); + if (isWindsurfRunning()) { + try { + const credentials = getCredentials(); + pluginLog(`Connected to Windsurf language server on port ${credentials.port}`); + } catch (e) { + pluginLog(`Windsurf running but credentials not available: ${e instanceof Error ? e.message : e}`); + } + } return baseURL; } } @@ -797,36 +841,36 @@ async function ensureWindsurfProxyServer(): Promise { */ export const createWindsurfPlugin = (providerId: string = PLUGIN_ID) => - async (_context: PluginInput): Promise => { - // Start proxy server on plugin load - const proxyBaseURL = await ensureWindsurfProxyServer(); + async (_context: PluginInput): Promise => { + // Start proxy server on plugin load + const proxyBaseURL = await ensureWindsurfProxyServer(); - return { - auth: { - provider: providerId, + return { + auth: { + provider: providerId, - async loader(_getAuth: () => Promise) { - // Return empty - we handle auth via the proxy server - return {}; - }, + async loader(_getAuth: () => Promise) { + // Return empty - we handle auth via the proxy server + return {}; + }, - // No auth methods needed - we use Windsurf's existing auth - methods: [], - }, + // No auth methods needed - we use Windsurf's existing auth + methods: [], + }, - // Dynamic baseURL injection (key pattern from cursor-auth) - async 'chat.params'(input: any, output: any) { - if (input.model?.providerID !== providerId) { - return; - } + // Dynamic baseURL injection (key pattern from cursor-auth) + async 'chat.params'(input: any, output: any) { + if (input.model?.providerID !== providerId) { + return; + } - // Inject the proxy server URL dynamically - output.options = output.options || {}; - output.options.baseURL = proxyBaseURL; - output.options.apiKey = output.options.apiKey || 'windsurf-local'; - }, + // Inject the proxy server URL dynamically + output.options = output.options || {}; + output.options.baseURL = proxyBaseURL; + output.options.apiKey = output.options.apiKey || 'windsurf-local'; + }, + }; }; - }; /** Default Windsurf plugin export */ export const WindsurfPlugin = createWindsurfPlugin(); From 26cfd103d50cc67a89ebff206e8bf1f6798af2ed Mon Sep 17 00:00:00 2001 From: Igor Barcik Date: Tue, 27 Jan 2026 21:38:28 +0100 Subject: [PATCH 3/4] chore: update lockfile --- bun.lock | 1 + 1 file changed, 1 insertion(+) diff --git a/bun.lock b/bun.lock index 7f726e9..dbb3526 100644 --- a/bun.lock +++ b/bun.lock @@ -1,5 +1,6 @@ { "lockfileVersion": 1, + "configVersion": 0, "workspaces": { "": { "name": "opencode-windsurf-auth", From 7ff537b8560044e300df2d42da6c580678dd7b07 Mon Sep 17 00:00:00 2001 From: Igor Barcik Date: Thu, 29 Jan 2026 22:50:02 +0100 Subject: [PATCH 4/4] fix: case-insensitive matching and Windows port detection. Fixes #5 --- src/plugin/auth.ts | 43 ++++++++++++++++++++++++++++--------------- 1 file changed, 28 insertions(+), 15 deletions(-) diff --git a/src/plugin/auth.ts b/src/plugin/auth.ts index b33edef..572d2c8 100644 --- a/src/plugin/auth.ts +++ b/src/plugin/auth.ts @@ -103,16 +103,19 @@ function getLanguageServerProcess(): string | null { `wmic process where "name like '%${pattern}%'" get CommandLine /format:list`, { encoding: 'utf8', timeout: 5000 } ); - // Filter for Windsurf-specific lines (path contains /windsurf/ or \windsurf\ or has --ide_name windsurf) - const lines = output.split('\n').filter(line => - line.includes('/windsurf/') || line.includes('\\windsurf\\') || line.includes('--ide_name windsurf') - ); + // Filter for Windsurf-specific lines (case-insensitive) + // Path contains /windsurf/ or \windsurf\ or has --ide_name windsurf + const lowerOutput = output.toLowerCase(); + const lines = output.split('\n').filter((line, idx) => { + const lowerLine = line.toLowerCase(); + return lowerLine.includes('/windsurf/') || lowerLine.includes('\\windsurf\\') || lowerLine.includes('--ide_name windsurf'); + }); return lines.length > 0 ? lines.join('\n') : null; } else { - // Unix-like: use ps and filter for Windsurf-specific process + // Unix-like: use ps and filter for Windsurf-specific process (case-insensitive) // Use /windsurf/ in path or --ide_name windsurf to avoid matching other language servers const output = execSync( - `ps aux | grep ${pattern} | grep -E "/windsurf/|--ide_name windsurf" | grep -v grep`, + `ps aux | grep ${pattern} | grep -iE "/windsurf/|--ide_name windsurf" | grep -v grep`, { encoding: 'utf8', timeout: 5000 } ); return output.trim() || null; @@ -166,20 +169,30 @@ export function getPort(): number { // Log line format: "2026-01-27 11:46:40.251 [info] ... Language server listening on random port at 41085" let grepCmd: string; if (process.platform === 'win32') { - // Windows: use findstr + // Windows: use findstr to get all matches, then sort and pick the last (most recent) grepCmd = `findstr /s /r "Language server listening on random port at" "${logsDir}\\*Windsurf.log"`; + const output = execSync(grepCmd, { encoding: 'utf8', timeout: 10000 }); + // Split into lines, filter empty, sort lexicographically (ISO timestamps make this valid), pick last + const lines = output.split('\n').filter(line => line.trim().length > 0); + if (lines.length > 0) { + lines.sort(); + const lastLine = lines[lines.length - 1]; + const portMatch = lastLine.match(/Language server listening on random port at (\d+)/); + if (portMatch?.[1]) { + return parseInt(portMatch[1], 10); + } + } } else { // Unix-like: use grep with recursive search grepCmd = `grep -rh "Language server listening on random port at" "${logsDir}" 2>/dev/null | sort | tail -1`; - } - - const output = execSync(grepCmd, { encoding: 'utf8', timeout: 10000 }).trim(); + const output = execSync(grepCmd, { encoding: 'utf8', timeout: 10000 }).trim(); - if (output) { - // Extract port from the log line - const portMatch = output.match(/Language server listening on random port at (\d+)/); - if (portMatch?.[1]) { - return parseInt(portMatch[1], 10); + if (output) { + // Extract port from the log line + const portMatch = output.match(/Language server listening on random port at (\d+)/); + if (portMatch?.[1]) { + return parseInt(portMatch[1], 10); + } } } } catch {