/**
* Lotus VS Code Extension
*
* This extension:
* - Registers the bundled MCP server with Cursor
* - Watches a temp file for browser URLs
* - When a URL appears, opens it in Simple Browser
*
* The MCP server (bundled in mcp-server/) is registered with Cursor on activation.
* This creates a seamless experience where users just need to install the extension.
*/
const vscode = require('vscode');
const fs = require('fs');
const path = require('path');
const os = require('os');
// Same file paths as the MCP server uses
const BROWSER_URL_FILE = path.join(os.tmpdir(), 'lotus-browser-url.txt');
const RECORDING_STATE_FILE = path.join(os.tmpdir(), 'lotus-recording-state.json');
// Status bar item for recording indicator
let statusBarItem;
/**
* Called when extension is activated
*/
function activate(context) {
console.log('🪷 Lotus extension activated');
// Get API key from settings
const config = vscode.workspace.getConfiguration('lotus');
const apiKey = config.get('apiKey');
// Register MCP server with Cursor
if (vscode.cursor?.mcp?.registerServer) {
vscode.cursor.mcp.registerServer({
name: 'lotus',
server: {
command: 'node',
args: [path.join(__dirname, 'mcp-server', 'index.js')],
env: {
LOTUS_API_KEY: apiKey || '',
LOTUS_URL: 'https://www.uselotus.ai',
LOTUS_PLATFORM: 'cursor'
}
}
});
console.log('🪷 Registered MCP server with Cursor');
} else {
console.log('🪷 Cursor MCP API not available - running in standalone mode');
}
// Create status bar item
statusBarItem = vscode.window.createStatusBarItem(
vscode.StatusBarAlignment.Right,
100
);
statusBarItem.command = 'lotus.openBrowser';
statusBarItem.text = '🪷 Lotus';
statusBarItem.tooltip = 'Lotus - Click to open browser';
context.subscriptions.push(statusBarItem);
statusBarItem.show();
// Register manual command (backup if file watching fails)
const openCommand = vscode.commands.registerCommand('lotus.openBrowser', async () => {
try {
if (fs.existsSync(BROWSER_URL_FILE)) {
const url = fs.readFileSync(BROWSER_URL_FILE, 'utf8').trim();
if (url.startsWith('http')) {
await openBrowser(url);
} else {
vscode.window.showWarningMessage('No valid browser URL found. Start a recording first.');
}
} else {
vscode.window.showWarningMessage('No Lotus browser session active. Start a recording first.');
}
} catch (error) {
vscode.window.showErrorMessage(`Failed to open browser: ${error.message}`);
}
});
context.subscriptions.push(openCommand);
// Ensure temp file exists
ensureTempFile();
// Watch for URL file changes
startFileWatcher(context);
// Watch for recording state changes
watchRecordingState(context);
// Watch for API key changes
context.subscriptions.push(
vscode.workspace.onDidChangeConfiguration(e => {
if (e.affectsConfiguration('lotus.apiKey')) {
vscode.window.showInformationMessage(
'Lotus API key changed. Reload window to apply.',
'Reload'
).then(selection => {
if (selection === 'Reload') {
vscode.commands.executeCommand('workbench.action.reloadWindow');
}
});
}
})
);
}
/**
* Ensure the temp file exists (create empty if not)
*/
function ensureTempFile() {
try {
if (!fs.existsSync(BROWSER_URL_FILE)) {
fs.writeFileSync(BROWSER_URL_FILE, '', 'utf8');
}
} catch (error) {
console.error('Failed to create temp file:', error);
}
}
/**
* Watch the temp file for URL changes
*/
function startFileWatcher(context) {
try {
// Track last URL to avoid duplicates
let lastUrl = '';
let lastTimestamp = 0;
// Use polling for reliability across platforms
const interval = setInterval(() => {
try {
if (!fs.existsSync(BROWSER_URL_FILE)) return;
const stat = fs.statSync(BROWSER_URL_FILE);
const mtime = stat.mtimeMs;
// Only process if file was modified
if (mtime <= lastTimestamp) return;
lastTimestamp = mtime;
const url = fs.readFileSync(BROWSER_URL_FILE, 'utf8').trim();
// Open if URL is valid (mtime check above ensures file was modified)
if (url && url.startsWith('http')) {
lastUrl = url;
console.log('🪷 Browser URL detected:', url);
openBrowser(url);
}
} catch (error) {
// File might not exist yet, that's ok
}
}, 500); // Check every 500ms
// Clean up on deactivate
context.subscriptions.push({
dispose: () => clearInterval(interval)
});
console.log('🪷 Watching for browser URLs at:', BROWSER_URL_FILE);
} catch (error) {
console.error('Failed to start file watcher:', error);
}
}
/**
* Watch recording state for status bar updates
*/
function watchRecordingState(context) {
try {
let lastTimestamp = 0;
const interval = setInterval(() => {
try {
if (!fs.existsSync(RECORDING_STATE_FILE)) return;
const stat = fs.statSync(RECORDING_STATE_FILE);
const mtime = stat.mtimeMs;
if (mtime <= lastTimestamp) return;
lastTimestamp = mtime;
const state = JSON.parse(fs.readFileSync(RECORDING_STATE_FILE, 'utf8'));
updateStatusBar(state.status);
} catch (error) {
// File might not exist or be invalid
}
}, 1000); // Check every second
context.subscriptions.push({
dispose: () => clearInterval(interval)
});
} catch (error) {
console.error('Failed to watch recording state:', error);
}
}
/**
* Open URL in Simple Browser panel
*/
async function openBrowser(url) {
try {
await vscode.commands.executeCommand('simpleBrowser.show', url);
console.log('🪷 Opened Simple Browser with:', url);
} catch (error) {
console.error('Failed to open Simple Browser:', error);
// Fallback: show URL to user
const action = await vscode.window.showErrorMessage(
`Failed to open browser automatically.`,
'Copy URL',
'Open Externally'
);
if (action === 'Copy URL') {
await vscode.env.clipboard.writeText(url);
vscode.window.showInformationMessage('URL copied to clipboard!');
} else if (action === 'Open Externally') {
vscode.env.openExternal(vscode.Uri.parse(url));
}
}
}
/**
* Update status bar based on recording state
*/
function updateStatusBar(status) {
if (!statusBarItem) return;
switch (status) {
case 'recording':
statusBarItem.text = '$(record) Lotus Recording';
statusBarItem.backgroundColor = new vscode.ThemeColor('statusBarItem.warningBackground');
statusBarItem.tooltip = 'Lotus - Recording workflow... Click to open browser.';
break;
case 'refining':
statusBarItem.text = '$(sync~spin) Lotus Refining';
statusBarItem.backgroundColor = new vscode.ThemeColor('statusBarItem.warningBackground');
statusBarItem.tooltip = 'Lotus - AI is refining your skill... Click to open browser.';
break;
case 'stopped':
statusBarItem.text = '$(check) Lotus Done';
statusBarItem.backgroundColor = undefined;
statusBarItem.tooltip = 'Lotus - Recording complete';
// Reset after 5 seconds
setTimeout(() => {
if (statusBarItem) {
statusBarItem.text = '🪷 Lotus';
statusBarItem.tooltip = 'Lotus - Click to open browser';
statusBarItem.backgroundColor = undefined;
}
}, 5000);
break;
default:
statusBarItem.text = '🪷 Lotus';
statusBarItem.tooltip = 'Lotus - Click to open browser';
statusBarItem.backgroundColor = undefined;
}
}
/**
* Called when extension is deactivated
*/
function deactivate() {
console.log('🪷 Lotus extension deactivated');
}
module.exports = {
activate,
deactivate
};