Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

102 changes: 73 additions & 29 deletions src/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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
// ============================================================================
Expand Down Expand Up @@ -652,11 +678,11 @@ async function ensureWindsurfProxyServer(): Promise<string> {
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,
})),
}
: {}),
};
}),
Expand Down Expand Up @@ -758,6 +784,15 @@ async function ensureWindsurfProxyServer(): Promise<string> {
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;
Expand All @@ -781,6 +816,15 @@ async function ensureWindsurfProxyServer(): Promise<string> {
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;
}
}
Expand All @@ -797,36 +841,36 @@ async function ensureWindsurfProxyServer(): Promise<string> {
*/
export const createWindsurfPlugin =
(providerId: string = PLUGIN_ID) =>
async (_context: PluginInput): Promise<Hooks> => {
// Start proxy server on plugin load
const proxyBaseURL = await ensureWindsurfProxyServer();
async (_context: PluginInput): Promise<Hooks> => {
// Start proxy server on plugin load
const proxyBaseURL = await ensureWindsurfProxyServer();

return {
auth: {
provider: providerId,
return {
auth: {
provider: providerId,

async loader(_getAuth: () => Promise<unknown>) {
// Return empty - we handle auth via the proxy server
return {};
},
async loader(_getAuth: () => Promise<unknown>) {
// 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();
Expand Down
132 changes: 71 additions & 61 deletions src/plugin/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
// ============================================================================
Expand All @@ -84,25 +91,34 @@ 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
const output = execSync(
`wmic process where "name like '%${pattern}%'" get CommandLine /format:list`,
{ encoding: 'utf8', timeout: 5000 }
);
return output;
// 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
// 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}`,
`ps aux | grep ${pattern} | grep -iE "/windsurf/|--ide_name windsurf" | grep -v grep`,
{ encoding: 'utf8', timeout: 5000 }
);
return output;
return output.trim() || null;
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
} catch {
return null;
Expand All @@ -114,83 +130,77 @@ 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
);
}

/**
* 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
}

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 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();

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);
}
// Otherwise just return the first listening port
return ports[0];
}
} 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
);
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Expand All @@ -206,22 +216,22 @@ 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 {
const result = execSync(
`sqlite3 "${statePath}" "SELECT value FROM ItemTable WHERE key = 'windsurfAuthStatus';"`,
{ encoding: 'utf8', timeout: 5000 }
).trim();

if (result) {
const parsed = JSON.parse(result);
if (parsed.apiKey) {
Expand All @@ -232,7 +242,7 @@ export function getApiKey(): string {
// Fall through to legacy config
}
}

// Try legacy config file
if (fs.existsSync(LEGACY_CONFIG_PATH)) {
try {
Expand All @@ -245,7 +255,7 @@ export function getApiKey(): string {
// Fall through
}
}

throw new WindsurfError(
'API key not found. Please login to Windsurf first.',
WindsurfErrorCode.API_KEY_MISSING
Expand All @@ -257,7 +267,7 @@ export function getApiKey(): string {
*/
export function getWindsurfVersion(): string {
const processInfo = getLanguageServerProcess();

if (processInfo) {
const match = processInfo.match(/--windsurf_version\s+([^\s]+)/);
if (match) {
Expand All @@ -266,7 +276,7 @@ export function getWindsurfVersion(): string {
return version;
}
}

// Default fallback version
return '1.13.104';
}
Expand Down