@ai-ide-bridge/cli 1.0.3

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.
@@ -0,0 +1,4 @@
1
+
2
+ > @ai-ide-bridge/cli@1.0.3 build /home/runner/work/llm-bridge/llm-bridge/cli
3
+ > tsc
4
+
package/README.md ADDED
@@ -0,0 +1,63 @@
1
+ # @ai-ide-bridge/cli
2
+
3
+ The standalone command-line interface for **AI IDE Bridge**.
4
+
5
+ AI IDE Bridge is a local HTTP server that translates OpenAI-compatible API requests into provider-specific calls (Cursor SDK, GitHub Copilot, Windsurf), enabling any OpenAI-format client to use any AI IDE's model catalog.
6
+
7
+ ## Installation
8
+
9
+ ```bash
10
+ npm install -g @ai-ide-bridge/cli
11
+ ```
12
+
13
+ ## Quick Start
14
+
15
+ ### 1. Initialize
16
+
17
+ ```bash
18
+ llm-bridge init
19
+ ```
20
+
21
+ This interactive wizard will:
22
+
23
+ - Ask which providers you want to enable (cursor, copilot, windsurf)
24
+ - Collect your API tokens
25
+ - Generate a config file at `~/.config/llm-bridge/config.json`
26
+
27
+ ### 2. Start the Bridge
28
+
29
+ ```bash
30
+ llm-bridge start
31
+ ```
32
+
33
+ The server starts on `http://127.0.0.1:3849` by default.
34
+
35
+ ### 3. Configure your Client
36
+
37
+ ```bash
38
+ llm-bridge configure
39
+ ```
40
+
41
+ This injects the llm-bridge provider into your `opencode.json`.
42
+
43
+ ## Commands
44
+
45
+ | Command | Description |
46
+ | ----------------------------- | ------------------------------------ |
47
+ | `llm-bridge init` | Interactive setup wizard |
48
+ | `llm-bridge start` | Launch bridge server |
49
+ | `llm-bridge configure` | Inject provider config into OpenCode |
50
+ | `llm-bridge doctor` | Run diagnostics |
51
+ | `llm-bridge install-daemon` | Install macOS LaunchAgent |
52
+ | `llm-bridge uninstall-daemon` | Remove macOS LaunchAgent |
53
+ | `llm-bridge daemon status` | Check Windsurf daemon status |
54
+ | `llm-bridge daemon download` | Download Windsurf daemon |
55
+ | `llm-bridge daemon locate` | Find Windsurf daemon path |
56
+
57
+ ## Documentation
58
+
59
+ For full documentation, architecture diagrams, and advanced configuration, please visit the main repository: [https://github.com/aeswibon/llm-bridge](https://github.com/aeswibon/llm-bridge).
60
+
61
+ ## License
62
+
63
+ MIT
@@ -0,0 +1 @@
1
+ export declare function configureOpencodeCommand(): Promise<void>;
@@ -0,0 +1,21 @@
1
+ import { findOpencodeConfig, injectProvider } from '../utils/opencode.js';
2
+ import { readConfig } from '../utils/config.js';
3
+ export async function configureOpencodeCommand() {
4
+ const configPath = findOpencodeConfig();
5
+ if (!configPath) {
6
+ console.error('No opencode.json found. Create one at ~/.config/opencode/opencode.json');
7
+ process.exit(1);
8
+ }
9
+ const bridgeConfig = readConfig();
10
+ const providerId = 'llm-bridge';
11
+ const plugin = bridgeConfig.defaultPlugin ?? 'cursor';
12
+ const modelId = plugin === 'copilot'
13
+ ? 'copilot/gpt-4o-copilot'
14
+ : plugin === 'windsurf'
15
+ ? 'windsurf/claude-4.5-sonnet'
16
+ : 'cursor/composer-2';
17
+ injectProvider(configPath, providerId, modelId, bridgeConfig.port);
18
+ console.log(`Injected provider into ${configPath}`);
19
+ console.log(`Provider: ${providerId}, Model: ${modelId}, Port: ${bridgeConfig.port}`);
20
+ console.log(`Plugin: ${plugin}`);
21
+ }
@@ -0,0 +1,5 @@
1
+ export declare function installDaemonCommand(): Promise<void>;
2
+ export declare function uninstallDaemonCommand(): Promise<void>;
3
+ export declare function daemonStatusCommand(): Promise<void>;
4
+ export declare function daemonDownloadCommand(): Promise<void>;
5
+ export declare function daemonLocateCommand(): Promise<void>;
@@ -0,0 +1,110 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import os from 'node:os';
4
+ import { execSync } from 'node:child_process';
5
+ import { fileURLToPath } from 'node:url';
6
+ const __filename = fileURLToPath(import.meta.url);
7
+ const __dirname = path.dirname(__filename);
8
+ const LABEL = 'com.llm-bridge.daemon';
9
+ export async function installDaemonCommand() {
10
+ if (process.platform !== 'darwin') {
11
+ console.error('Daemon installation is only supported on macOS.');
12
+ process.exit(1);
13
+ }
14
+ const plistPath = path.join(os.homedir(), 'Library', 'LaunchAgents', `${LABEL}.plist`);
15
+ const wrapperPath = path.join(__dirname, '..', '..', 'scripts', 'llm-bridge-daemon.sh');
16
+ const plist = `<?xml version="1.0" encoding="UTF-8"?>
17
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
18
+ <plist version="1.0">
19
+ <dict>
20
+ <key>Label</key>
21
+ <string>${LABEL}</string>
22
+ <key>ProgramArguments</key>
23
+ <array>
24
+ <string>${wrapperPath}</string>
25
+ </array>
26
+ <key>RunAtLoad</key>
27
+ <true/>
28
+ <key>KeepAlive</key>
29
+ <true/>
30
+ <key>ThrottleInterval</key>
31
+ <integer>30</integer>
32
+ <key>StandardOutPath</key>
33
+ <string>${os.homedir()}/Library/Logs/llm-bridge.log</string>
34
+ <key>StandardErrorPath</key>
35
+ <string>${os.homedir()}/Library/Logs/llm-bridge.err.log</string>
36
+ </dict>
37
+ </plist>`;
38
+ fs.mkdirSync(path.dirname(plistPath), { recursive: true });
39
+ fs.writeFileSync(plistPath, plist);
40
+ try {
41
+ execSync(`launchctl bootstrap "gui/$(id -u)" "${plistPath}"`, { stdio: 'inherit' });
42
+ console.log(`Installed LaunchAgent: ${plistPath}`);
43
+ console.log(`Logs: ~/Library/Logs/llm-bridge.{log,err.log}`);
44
+ }
45
+ catch (e) {
46
+ console.error('Failed to bootstrap daemon:', e);
47
+ process.exit(1);
48
+ }
49
+ }
50
+ export async function uninstallDaemonCommand() {
51
+ if (process.platform !== 'darwin') {
52
+ console.error('Daemon uninstallation is only supported on macOS.');
53
+ process.exit(1);
54
+ }
55
+ const plistPath = path.join(os.homedir(), 'Library', 'LaunchAgents', `${LABEL}.plist`);
56
+ try {
57
+ execSync(`launchctl bootout "gui/$(id -u)" "${plistPath}" 2>/dev/null || true`, {
58
+ stdio: 'inherit',
59
+ });
60
+ }
61
+ catch {
62
+ // Ignore errors during unbootstrap
63
+ }
64
+ if (fs.existsSync(plistPath)) {
65
+ fs.unlinkSync(plistPath);
66
+ console.log(`Removed LaunchAgent: ${plistPath}`);
67
+ }
68
+ else {
69
+ console.log('No LaunchAgent found.');
70
+ }
71
+ }
72
+ export async function daemonStatusCommand() {
73
+ const { createWindsurfDaemon } = await import('@ai-ide-bridge/windsurf/daemon.js');
74
+ const daemon = createWindsurfDaemon();
75
+ const path = await daemon.locate();
76
+ if (path) {
77
+ console.log(`Windsurf language server found at: ${path}`);
78
+ const healthy = await daemon.healthCheck();
79
+ console.log(`Health: ${healthy ? 'OK' : 'Unhealthy'}`);
80
+ }
81
+ else {
82
+ console.log('Windsurf language server not found.');
83
+ console.log('Run: llm-bridge daemon download');
84
+ }
85
+ }
86
+ export async function daemonDownloadCommand() {
87
+ const { createWindsurfDaemon } = await import('@ai-ide-bridge/windsurf/daemon.js');
88
+ const daemon = createWindsurfDaemon();
89
+ console.log('Downloading Windsurf language server...');
90
+ try {
91
+ const path = await daemon.download();
92
+ console.log(`Downloaded to: ${path}`);
93
+ }
94
+ catch (err) {
95
+ console.error(`Download failed: ${err.message}`);
96
+ process.exit(1);
97
+ }
98
+ }
99
+ export async function daemonLocateCommand() {
100
+ const { createWindsurfDaemon } = await import('@ai-ide-bridge/windsurf/daemon.js');
101
+ const daemon = createWindsurfDaemon();
102
+ const path = await daemon.locate();
103
+ if (path) {
104
+ console.log(path);
105
+ }
106
+ else {
107
+ console.log('Not found');
108
+ process.exit(1);
109
+ }
110
+ }
@@ -0,0 +1 @@
1
+ export declare function doctorCommand(): Promise<void>;
@@ -0,0 +1,66 @@
1
+ import http from 'node:http';
2
+ import { readConfig } from '../utils/config.js';
3
+ import fs from 'node:fs';
4
+ import { configPath } from '@ai-ide-bridge/core';
5
+ export async function doctorCommand() {
6
+ const config = readConfig();
7
+ console.log('llm-bridge diagnostics\n');
8
+ console.log(`Config: ${configPath()}`);
9
+ console.log(`Active plugin: ${config.activePlugin ?? config.defaultPlugin}`);
10
+ console.log(`Port: ${config.port}`);
11
+ console.log(`Host: ${config.host}`);
12
+ console.log(`Tool mode: ${config.toolMode}`);
13
+ if (fs.existsSync(configPath())) {
14
+ console.log('✓ Config file exists');
15
+ }
16
+ else {
17
+ console.log('✗ Config file not found (using defaults)');
18
+ }
19
+ const activePlugin = config.activePlugin ?? config.defaultPlugin;
20
+ const pluginConfig = config.plugins[activePlugin];
21
+ if (pluginConfig && Object.keys(pluginConfig).length > 0) {
22
+ console.log(`✓ Plugin "${activePlugin}" has configuration`);
23
+ }
24
+ else {
25
+ console.log(`✗ Plugin "${activePlugin}" has no configuration`);
26
+ }
27
+ try {
28
+ const res = await new Promise((resolve, reject) => {
29
+ http
30
+ .get(`http://${config.host}:${config.port}/health`, (res) => {
31
+ resolve(res.statusCode ?? 0);
32
+ })
33
+ .on('error', reject);
34
+ });
35
+ if (res === 200) {
36
+ console.log('✓ Bridge server is running');
37
+ }
38
+ else {
39
+ console.log(`✗ Bridge server returned status ${res}`);
40
+ }
41
+ }
42
+ catch {
43
+ console.log('✗ Cannot reach bridge server (is it running?)');
44
+ }
45
+ try {
46
+ await new Promise((resolve, reject) => {
47
+ const server = http.createServer();
48
+ server.listen(config.port, config.host, () => {
49
+ server.close();
50
+ resolve();
51
+ });
52
+ server.on('error', (err) => {
53
+ if (err.code === 'EADDRINUSE') {
54
+ reject(new Error('Port in use'));
55
+ }
56
+ else {
57
+ resolve();
58
+ }
59
+ });
60
+ });
61
+ console.log(`✓ Port ${config.port} is available`);
62
+ }
63
+ catch {
64
+ console.log(`✗ Port ${config.port} is already in use`);
65
+ }
66
+ }
@@ -0,0 +1 @@
1
+ export declare function initCommand(): Promise<void>;
@@ -0,0 +1,87 @@
1
+ import { setPluginConfig, writeConfig, readConfig } from '../utils/config.js';
2
+ import { CursorBridgePlugin } from '@ai-ide-bridge/cursor';
3
+ import { CopilotBridgePlugin } from '@ai-ide-bridge/copilot';
4
+ import { WindsurfBridgePlugin } from '@ai-ide-bridge/windsurf';
5
+ import { createInterface } from 'node:readline';
6
+ const PROVIDERS = ['cursor', 'copilot', 'windsurf'];
7
+ async function getProviderPlugin(provider) {
8
+ switch (provider) {
9
+ case 'cursor':
10
+ return new CursorBridgePlugin();
11
+ case 'copilot':
12
+ return new CopilotBridgePlugin();
13
+ case 'windsurf':
14
+ return new WindsurfBridgePlugin();
15
+ }
16
+ }
17
+ function getCredentialPrompt(provider) {
18
+ switch (provider) {
19
+ case 'cursor':
20
+ return 'Enter your CURSOR_API_KEY: ';
21
+ case 'copilot':
22
+ return 'Enter your GITHUB_TOKEN: ';
23
+ case 'windsurf':
24
+ return 'Enter your WINDSURF_TOKEN: ';
25
+ }
26
+ }
27
+ function getEnvVar(provider) {
28
+ switch (provider) {
29
+ case 'cursor':
30
+ return 'CURSOR_API_KEY';
31
+ case 'copilot':
32
+ return 'GITHUB_TOKEN';
33
+ case 'windsurf':
34
+ return 'WINDSURF_TOKEN';
35
+ }
36
+ }
37
+ export async function initCommand() {
38
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
39
+ const ask = (q) => new Promise((resolve) => rl.question(q, resolve));
40
+ console.log('llm-bridge setup wizard\n');
41
+ const existingConfig = readConfig();
42
+ const configuredProviders = Object.keys(existingConfig.plugins || {});
43
+ if (configuredProviders.length > 0) {
44
+ console.log(`Already configured: ${configuredProviders.join(', ')}\n`);
45
+ }
46
+ let firstProvider = null;
47
+ while (true) {
48
+ const available = PROVIDERS.filter((p) => !configuredProviders.includes(p));
49
+ if (available.length === 0) {
50
+ console.log('All providers already configured.');
51
+ break;
52
+ }
53
+ const input = await ask(`Provider to configure (${available.join(', ')}, or 'skip'): `);
54
+ if (input === 'skip' || !PROVIDERS.includes(input)) {
55
+ break;
56
+ }
57
+ const provider = input;
58
+ const envVar = getEnvVar(provider);
59
+ const credential = await ask(getCredentialPrompt(provider));
60
+ if (!credential) {
61
+ console.error('Credential is required.');
62
+ continue;
63
+ }
64
+ const plugin = await getProviderPlugin(provider);
65
+ const valid = await plugin.authenticate({ [envVar]: credential });
66
+ if (!valid) {
67
+ console.error('Authentication failed. Please check your credential and try again.');
68
+ continue;
69
+ }
70
+ console.log(`Authentication successful for ${provider}.`);
71
+ setPluginConfig(provider, { [envVar]: credential });
72
+ configuredProviders.push(provider);
73
+ if (!firstProvider) {
74
+ firstProvider = provider;
75
+ }
76
+ const more = await ask('Configure another provider? (y/n): ');
77
+ if (more.toLowerCase() !== 'y')
78
+ break;
79
+ }
80
+ const config = readConfig();
81
+ if (!config.defaultPlugin && firstProvider) {
82
+ config.defaultPlugin = firstProvider;
83
+ writeConfig(config);
84
+ }
85
+ console.log(`\nConfig saved. Run 'llm-bridge start' to launch.`);
86
+ rl.close();
87
+ }
@@ -0,0 +1 @@
1
+ export declare function startCommand(): Promise<void>;
@@ -0,0 +1,60 @@
1
+ import { BridgeServer, loadConfig } from '@ai-ide-bridge/core';
2
+ import { CursorBridgePlugin } from '@ai-ide-bridge/cursor';
3
+ import { CopilotBridgePlugin } from '@ai-ide-bridge/copilot';
4
+ import { WindsurfBridgePlugin } from '@ai-ide-bridge/windsurf';
5
+ async function getPlugin(name) {
6
+ switch (name) {
7
+ case 'cursor':
8
+ return new CursorBridgePlugin();
9
+ case 'copilot':
10
+ return new CopilotBridgePlugin();
11
+ case 'windsurf':
12
+ return new WindsurfBridgePlugin();
13
+ default:
14
+ return null;
15
+ }
16
+ }
17
+ export async function startCommand() {
18
+ const config = loadConfig();
19
+ const server = new BridgeServer(config);
20
+ // Register all configured plugins
21
+ const pluginNames = Object.keys(config.plugins);
22
+ if (pluginNames.length === 0) {
23
+ console.error('[llm-bridge] error: no plugins configured. Run: llm-bridge init');
24
+ process.exit(1);
25
+ }
26
+ for (const name of pluginNames) {
27
+ const plugin = await getPlugin(name);
28
+ if (plugin) {
29
+ server.registerPlugin(plugin);
30
+ console.error(`[llm-bridge] registered plugin: ${name}`);
31
+ }
32
+ else {
33
+ console.error(`[llm-bridge] warning: unknown plugin "${name}"`);
34
+ }
35
+ }
36
+ // Set default plugin for fallback routing
37
+ if (config.defaultPlugin) {
38
+ server.setDefaultPlugin(config.defaultPlugin);
39
+ console.error(`[llm-bridge] default plugin: ${config.defaultPlugin}`);
40
+ }
41
+ try {
42
+ await server.start();
43
+ }
44
+ catch (err) {
45
+ if (err.code === 'EADDRINUSE') {
46
+ console.error(`[llm-bridge] error: port ${config.port} is already in use`);
47
+ }
48
+ else {
49
+ console.error(`[llm-bridge] error: ${err.message}`);
50
+ }
51
+ process.exit(1);
52
+ }
53
+ const shutdown = async () => {
54
+ console.error('\n[llm-bridge] shutting down...');
55
+ await server.stop();
56
+ process.exit(0);
57
+ };
58
+ process.on('SIGINT', shutdown);
59
+ process.on('SIGTERM', shutdown);
60
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,64 @@
1
+ #!/usr/bin/env node
2
+ import { initCommand } from './commands/init.js';
3
+ import { startCommand } from './commands/start.js';
4
+ import { configureOpencodeCommand } from './commands/configure.js';
5
+ import { doctorCommand } from './commands/doctor.js';
6
+ import { daemonStatusCommand, daemonDownloadCommand, daemonLocateCommand, } from './commands/daemon.js';
7
+ import { installDaemonCommand, uninstallDaemonCommand } from './commands/daemon.js';
8
+ const command = process.argv[2] ?? 'help';
9
+ async function main() {
10
+ switch (command) {
11
+ case 'init':
12
+ await initCommand();
13
+ break;
14
+ case 'start':
15
+ await startCommand();
16
+ break;
17
+ case 'configure':
18
+ await configureOpencodeCommand();
19
+ break;
20
+ case 'doctor':
21
+ await doctorCommand();
22
+ break;
23
+ case 'install-daemon':
24
+ await installDaemonCommand();
25
+ break;
26
+ case 'uninstall-daemon':
27
+ await uninstallDaemonCommand();
28
+ break;
29
+ case 'daemon': {
30
+ const subcommand = process.argv[3] ?? 'status';
31
+ switch (subcommand) {
32
+ case 'status':
33
+ await daemonStatusCommand();
34
+ break;
35
+ case 'download':
36
+ await daemonDownloadCommand();
37
+ break;
38
+ case 'locate':
39
+ await daemonLocateCommand();
40
+ break;
41
+ default:
42
+ console.log('Usage: llm-bridge daemon [status|download|locate]');
43
+ }
44
+ break;
45
+ }
46
+ case 'help':
47
+ default:
48
+ console.log(`llm-bridge v1.0.0
49
+
50
+ Usage:
51
+ llm-bridge init Interactive setup wizard (configure one or more providers)
52
+ llm-bridge start Launch bridge server (all configured plugins registered)
53
+ llm-bridge configure Inject OpenCode config for the default provider
54
+ llm-bridge doctor Run diagnostics
55
+ llm-bridge install-daemon Install macOS LaunchAgent
56
+ llm-bridge uninstall-daemon Remove macOS LaunchAgent
57
+ llm-bridge daemon [status|download|locate] Manage Windsurf daemon binary
58
+ llm-bridge help Show this help`);
59
+ }
60
+ }
61
+ main().catch((err) => {
62
+ console.error(err);
63
+ process.exit(1);
64
+ });
@@ -0,0 +1,4 @@
1
+ import { BridgeConfig } from '@ai-ide-bridge/core';
2
+ export declare function readConfig(): BridgeConfig;
3
+ export declare function writeConfig(config: BridgeConfig): void;
4
+ export declare function setPluginConfig(pluginName: string, envVars: Record<string, string>): void;
@@ -0,0 +1,15 @@
1
+ import { loadConfig, saveConfig } from '@ai-ide-bridge/core';
2
+ export function readConfig() {
3
+ return loadConfig();
4
+ }
5
+ export function writeConfig(config) {
6
+ saveConfig(config);
7
+ }
8
+ export function setPluginConfig(pluginName, envVars) {
9
+ const config = readConfig();
10
+ config.plugins[pluginName] = { ...config.plugins[pluginName], ...envVars };
11
+ if (!config.defaultPlugin) {
12
+ config.defaultPlugin = pluginName;
13
+ }
14
+ writeConfig(config);
15
+ }
@@ -0,0 +1,2 @@
1
+ export declare function findOpencodeConfig(): string | null;
2
+ export declare function injectProvider(configPath: string, providerId: string, modelId: string, port: number): void;
@@ -0,0 +1,39 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import os from 'node:os';
4
+ export function findOpencodeConfig() {
5
+ const candidates = [
6
+ path.join(os.homedir(), '.config', 'opencode', 'opencode.json'),
7
+ path.join(os.homedir(), '.config', 'opencode', 'opencode.jsonc'),
8
+ path.join(process.cwd(), 'opencode.json'),
9
+ path.join(process.cwd(), 'opencode.jsonc'),
10
+ ];
11
+ for (const p of candidates) {
12
+ if (fs.existsSync(p))
13
+ return p;
14
+ }
15
+ return null;
16
+ }
17
+ export function injectProvider(configPath, providerId, modelId, port) {
18
+ const raw = fs.readFileSync(configPath, 'utf8');
19
+ let config;
20
+ try {
21
+ config = JSON.parse(raw);
22
+ }
23
+ catch (e) {
24
+ throw new Error(`Failed to parse ${configPath}: ${e.message}`);
25
+ }
26
+ if (!config.provider)
27
+ config.provider = {};
28
+ config.provider[providerId] = {
29
+ npm: '@ai-sdk/openai-compatible',
30
+ name: 'LLM Bridge',
31
+ options: {
32
+ apiKey: 'bridge-local',
33
+ baseURL: `http://127.0.0.1:${port}/v1`,
34
+ },
35
+ models: { [modelId]: { name: modelId } },
36
+ };
37
+ config.model = `${providerId}/${modelId}`;
38
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
39
+ }
package/package.json ADDED
@@ -0,0 +1,26 @@
1
+ {
2
+ "name": "@ai-ide-bridge/cli",
3
+ "version": "1.0.3",
4
+ "type": "module",
5
+ "bin": {
6
+ "llm-bridge": "./dist/index.js"
7
+ },
8
+ "scripts": {
9
+ "build": "tsc",
10
+ "test": "vitest run",
11
+ "lint": "tsc --noEmit",
12
+ "typecheck": "tsc --noEmit",
13
+ "dev": "tsc --watch",
14
+ "start": "node dist/index.js"
15
+ },
16
+ "dependencies": {
17
+ "@ai-ide-bridge/copilot": "workspace:*",
18
+ "@ai-ide-bridge/core": "workspace:*",
19
+ "@ai-ide-bridge/cursor": "workspace:*",
20
+ "@ai-ide-bridge/windsurf": "workspace:*"
21
+ },
22
+ "devDependencies": {
23
+ "@types/node": "^22.15.0",
24
+ "vitest": "^2.0.0"
25
+ }
26
+ }
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ BRIDGE_BIN="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)/dist/index.js"
5
+
6
+ if ! command -v node &>/dev/null; then
7
+ echo "llm-bridge: node is required." >&2
8
+ exit 1
9
+ fi
10
+
11
+ exec node "$BRIDGE_BIN" start
@@ -0,0 +1,25 @@
1
+ import { findOpencodeConfig, injectProvider } from '../utils/opencode.js';
2
+ import { readConfig } from '../utils/config.js';
3
+
4
+ export async function configureOpencodeCommand(): Promise<void> {
5
+ const configPath = findOpencodeConfig();
6
+ if (!configPath) {
7
+ console.error('No opencode.json found. Create one at ~/.config/opencode/opencode.json');
8
+ process.exit(1);
9
+ }
10
+
11
+ const bridgeConfig = readConfig();
12
+ const providerId = 'llm-bridge';
13
+ const plugin = bridgeConfig.defaultPlugin ?? 'cursor';
14
+ const modelId =
15
+ plugin === 'copilot'
16
+ ? 'copilot/gpt-4o-copilot'
17
+ : plugin === 'windsurf'
18
+ ? 'windsurf/claude-4.5-sonnet'
19
+ : 'cursor/composer-2';
20
+
21
+ injectProvider(configPath, providerId, modelId, bridgeConfig.port);
22
+ console.log(`Injected provider into ${configPath}`);
23
+ console.log(`Provider: ${providerId}, Model: ${modelId}, Port: ${bridgeConfig.port}`);
24
+ console.log(`Plugin: ${plugin}`);
25
+ }
@@ -0,0 +1,120 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import os from 'node:os';
4
+ import { execSync } from 'node:child_process';
5
+ import { fileURLToPath } from 'node:url';
6
+
7
+ const __filename = fileURLToPath(import.meta.url);
8
+ const __dirname = path.dirname(__filename);
9
+ const LABEL = 'com.llm-bridge.daemon';
10
+
11
+ export async function installDaemonCommand(): Promise<void> {
12
+ if (process.platform !== 'darwin') {
13
+ console.error('Daemon installation is only supported on macOS.');
14
+ process.exit(1);
15
+ }
16
+
17
+ const plistPath = path.join(os.homedir(), 'Library', 'LaunchAgents', `${LABEL}.plist`);
18
+ const wrapperPath = path.join(__dirname, '..', '..', 'scripts', 'llm-bridge-daemon.sh');
19
+
20
+ const plist = `<?xml version="1.0" encoding="UTF-8"?>
21
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
22
+ <plist version="1.0">
23
+ <dict>
24
+ <key>Label</key>
25
+ <string>${LABEL}</string>
26
+ <key>ProgramArguments</key>
27
+ <array>
28
+ <string>${wrapperPath}</string>
29
+ </array>
30
+ <key>RunAtLoad</key>
31
+ <true/>
32
+ <key>KeepAlive</key>
33
+ <true/>
34
+ <key>ThrottleInterval</key>
35
+ <integer>30</integer>
36
+ <key>StandardOutPath</key>
37
+ <string>${os.homedir()}/Library/Logs/llm-bridge.log</string>
38
+ <key>StandardErrorPath</key>
39
+ <string>${os.homedir()}/Library/Logs/llm-bridge.err.log</string>
40
+ </dict>
41
+ </plist>`;
42
+
43
+ fs.mkdirSync(path.dirname(plistPath), { recursive: true });
44
+ fs.writeFileSync(plistPath, plist);
45
+
46
+ try {
47
+ execSync(`launchctl bootstrap "gui/$(id -u)" "${plistPath}"`, { stdio: 'inherit' });
48
+ console.log(`Installed LaunchAgent: ${plistPath}`);
49
+ console.log(`Logs: ~/Library/Logs/llm-bridge.{log,err.log}`);
50
+ } catch (e) {
51
+ console.error('Failed to bootstrap daemon:', e);
52
+ process.exit(1);
53
+ }
54
+ }
55
+
56
+ export async function uninstallDaemonCommand(): Promise<void> {
57
+ if (process.platform !== 'darwin') {
58
+ console.error('Daemon uninstallation is only supported on macOS.');
59
+ process.exit(1);
60
+ }
61
+
62
+ const plistPath = path.join(os.homedir(), 'Library', 'LaunchAgents', `${LABEL}.plist`);
63
+
64
+ try {
65
+ execSync(`launchctl bootout "gui/$(id -u)" "${plistPath}" 2>/dev/null || true`, {
66
+ stdio: 'inherit',
67
+ });
68
+ } catch {
69
+ // Ignore errors during unbootstrap
70
+ }
71
+
72
+ if (fs.existsSync(plistPath)) {
73
+ fs.unlinkSync(plistPath);
74
+ console.log(`Removed LaunchAgent: ${plistPath}`);
75
+ } else {
76
+ console.log('No LaunchAgent found.');
77
+ }
78
+ }
79
+
80
+ export async function daemonStatusCommand(): Promise<void> {
81
+ const { createWindsurfDaemon } = await import('@ai-ide-bridge/windsurf/daemon.js');
82
+ const daemon = createWindsurfDaemon();
83
+
84
+ const path = await daemon.locate();
85
+ if (path) {
86
+ console.log(`Windsurf language server found at: ${path}`);
87
+ const healthy = await daemon.healthCheck();
88
+ console.log(`Health: ${healthy ? 'OK' : 'Unhealthy'}`);
89
+ } else {
90
+ console.log('Windsurf language server not found.');
91
+ console.log('Run: llm-bridge daemon download');
92
+ }
93
+ }
94
+
95
+ export async function daemonDownloadCommand(): Promise<void> {
96
+ const { createWindsurfDaemon } = await import('@ai-ide-bridge/windsurf/daemon.js');
97
+ const daemon = createWindsurfDaemon();
98
+
99
+ console.log('Downloading Windsurf language server...');
100
+ try {
101
+ const path = await daemon.download();
102
+ console.log(`Downloaded to: ${path}`);
103
+ } catch (err: any) {
104
+ console.error(`Download failed: ${err.message}`);
105
+ process.exit(1);
106
+ }
107
+ }
108
+
109
+ export async function daemonLocateCommand(): Promise<void> {
110
+ const { createWindsurfDaemon } = await import('@ai-ide-bridge/windsurf/daemon.js');
111
+ const daemon = createWindsurfDaemon();
112
+
113
+ const path = await daemon.locate();
114
+ if (path) {
115
+ console.log(path);
116
+ } else {
117
+ console.log('Not found');
118
+ process.exit(1);
119
+ }
120
+ }
@@ -0,0 +1,65 @@
1
+ import http from 'node:http';
2
+ import { readConfig } from '../utils/config.js';
3
+ import fs from 'node:fs';
4
+ import { configPath } from '@ai-ide-bridge/core';
5
+
6
+ export async function doctorCommand(): Promise<void> {
7
+ const config = readConfig();
8
+ console.log('llm-bridge diagnostics\n');
9
+ console.log(`Config: ${configPath()}`);
10
+ console.log(`Active plugin: ${config.activePlugin ?? config.defaultPlugin}`);
11
+ console.log(`Port: ${config.port}`);
12
+ console.log(`Host: ${config.host}`);
13
+ console.log(`Tool mode: ${config.toolMode}`);
14
+
15
+ if (fs.existsSync(configPath())) {
16
+ console.log('✓ Config file exists');
17
+ } else {
18
+ console.log('✗ Config file not found (using defaults)');
19
+ }
20
+
21
+ const activePlugin = config.activePlugin ?? config.defaultPlugin;
22
+ const pluginConfig = config.plugins[activePlugin];
23
+ if (pluginConfig && Object.keys(pluginConfig).length > 0) {
24
+ console.log(`✓ Plugin "${activePlugin}" has configuration`);
25
+ } else {
26
+ console.log(`✗ Plugin "${activePlugin}" has no configuration`);
27
+ }
28
+
29
+ try {
30
+ const res = await new Promise<number>((resolve, reject) => {
31
+ http
32
+ .get(`http://${config.host}:${config.port}/health`, (res) => {
33
+ resolve(res.statusCode ?? 0);
34
+ })
35
+ .on('error', reject);
36
+ });
37
+ if (res === 200) {
38
+ console.log('✓ Bridge server is running');
39
+ } else {
40
+ console.log(`✗ Bridge server returned status ${res}`);
41
+ }
42
+ } catch {
43
+ console.log('✗ Cannot reach bridge server (is it running?)');
44
+ }
45
+
46
+ try {
47
+ await new Promise<void>((resolve, reject) => {
48
+ const server = http.createServer();
49
+ server.listen(config.port, config.host, () => {
50
+ server.close();
51
+ resolve();
52
+ });
53
+ server.on('error', (err: any) => {
54
+ if (err.code === 'EADDRINUSE') {
55
+ reject(new Error('Port in use'));
56
+ } else {
57
+ resolve();
58
+ }
59
+ });
60
+ });
61
+ console.log(`✓ Port ${config.port} is available`);
62
+ } catch {
63
+ console.log(`✗ Port ${config.port} is already in use`);
64
+ }
65
+ }
@@ -0,0 +1,106 @@
1
+ import { setPluginConfig, writeConfig, readConfig } from '../utils/config.js';
2
+ import { CursorBridgePlugin } from '@ai-ide-bridge/cursor';
3
+ import { CopilotBridgePlugin } from '@ai-ide-bridge/copilot';
4
+ import { WindsurfBridgePlugin } from '@ai-ide-bridge/windsurf';
5
+ import { createInterface } from 'node:readline';
6
+
7
+ const PROVIDERS = ['cursor', 'copilot', 'windsurf'] as const;
8
+ type Provider = (typeof PROVIDERS)[number];
9
+
10
+ async function getProviderPlugin(provider: Provider) {
11
+ switch (provider) {
12
+ case 'cursor':
13
+ return new CursorBridgePlugin();
14
+ case 'copilot':
15
+ return new CopilotBridgePlugin();
16
+ case 'windsurf':
17
+ return new WindsurfBridgePlugin();
18
+ }
19
+ }
20
+
21
+ function getCredentialPrompt(provider: Provider): string {
22
+ switch (provider) {
23
+ case 'cursor':
24
+ return 'Enter your CURSOR_API_KEY: ';
25
+ case 'copilot':
26
+ return 'Enter your GITHUB_TOKEN: ';
27
+ case 'windsurf':
28
+ return 'Enter your WINDSURF_TOKEN: ';
29
+ }
30
+ }
31
+
32
+ function getEnvVar(provider: Provider): string {
33
+ switch (provider) {
34
+ case 'cursor':
35
+ return 'CURSOR_API_KEY';
36
+ case 'copilot':
37
+ return 'GITHUB_TOKEN';
38
+ case 'windsurf':
39
+ return 'WINDSURF_TOKEN';
40
+ }
41
+ }
42
+
43
+ export async function initCommand(): Promise<void> {
44
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
45
+ const ask = (q: string) => new Promise<string>((resolve) => rl.question(q, resolve));
46
+
47
+ console.log('llm-bridge setup wizard\n');
48
+
49
+ const existingConfig = readConfig();
50
+ const configuredProviders = Object.keys(existingConfig.plugins || {});
51
+ if (configuredProviders.length > 0) {
52
+ console.log(`Already configured: ${configuredProviders.join(', ')}\n`);
53
+ }
54
+
55
+ let firstProvider: Provider | null = null;
56
+
57
+ while (true) {
58
+ const available = PROVIDERS.filter((p) => !configuredProviders.includes(p));
59
+ if (available.length === 0) {
60
+ console.log('All providers already configured.');
61
+ break;
62
+ }
63
+
64
+ const input = await ask(`Provider to configure (${available.join(', ')}, or 'skip'): `);
65
+
66
+ if (input === 'skip' || !PROVIDERS.includes(input as Provider)) {
67
+ break;
68
+ }
69
+
70
+ const provider = input as Provider;
71
+
72
+ const envVar = getEnvVar(provider);
73
+ const credential = await ask(getCredentialPrompt(provider));
74
+ if (!credential) {
75
+ console.error('Credential is required.');
76
+ continue;
77
+ }
78
+
79
+ const plugin = await getProviderPlugin(provider);
80
+ const valid = await plugin.authenticate({ [envVar]: credential });
81
+ if (!valid) {
82
+ console.error('Authentication failed. Please check your credential and try again.');
83
+ continue;
84
+ }
85
+ console.log(`Authentication successful for ${provider}.`);
86
+
87
+ setPluginConfig(provider, { [envVar]: credential });
88
+ configuredProviders.push(provider);
89
+
90
+ if (!firstProvider) {
91
+ firstProvider = provider;
92
+ }
93
+
94
+ const more = await ask('Configure another provider? (y/n): ');
95
+ if (more.toLowerCase() !== 'y') break;
96
+ }
97
+
98
+ const config = readConfig();
99
+ if (!config.defaultPlugin && firstProvider) {
100
+ config.defaultPlugin = firstProvider;
101
+ writeConfig(config);
102
+ }
103
+
104
+ console.log(`\nConfig saved. Run 'llm-bridge start' to launch.`);
105
+ rl.close();
106
+ }
@@ -0,0 +1,65 @@
1
+ import { BridgeServer, loadConfig } from '@ai-ide-bridge/core';
2
+ import { CursorBridgePlugin } from '@ai-ide-bridge/cursor';
3
+ import { CopilotBridgePlugin } from '@ai-ide-bridge/copilot';
4
+ import { WindsurfBridgePlugin } from '@ai-ide-bridge/windsurf';
5
+
6
+ async function getPlugin(name: string) {
7
+ switch (name) {
8
+ case 'cursor':
9
+ return new CursorBridgePlugin();
10
+ case 'copilot':
11
+ return new CopilotBridgePlugin();
12
+ case 'windsurf':
13
+ return new WindsurfBridgePlugin();
14
+ default:
15
+ return null;
16
+ }
17
+ }
18
+
19
+ export async function startCommand(): Promise<void> {
20
+ const config = loadConfig();
21
+ const server = new BridgeServer(config);
22
+
23
+ // Register all configured plugins
24
+ const pluginNames = Object.keys(config.plugins);
25
+ if (pluginNames.length === 0) {
26
+ console.error('[llm-bridge] error: no plugins configured. Run: llm-bridge init');
27
+ process.exit(1);
28
+ }
29
+
30
+ for (const name of pluginNames) {
31
+ const plugin = await getPlugin(name);
32
+ if (plugin) {
33
+ server.registerPlugin(plugin);
34
+ console.error(`[llm-bridge] registered plugin: ${name}`);
35
+ } else {
36
+ console.error(`[llm-bridge] warning: unknown plugin "${name}"`);
37
+ }
38
+ }
39
+
40
+ // Set default plugin for fallback routing
41
+ if (config.defaultPlugin) {
42
+ server.setDefaultPlugin(config.defaultPlugin);
43
+ console.error(`[llm-bridge] default plugin: ${config.defaultPlugin}`);
44
+ }
45
+
46
+ try {
47
+ await server.start();
48
+ } catch (err: any) {
49
+ if (err.code === 'EADDRINUSE') {
50
+ console.error(`[llm-bridge] error: port ${config.port} is already in use`);
51
+ } else {
52
+ console.error(`[llm-bridge] error: ${err.message}`);
53
+ }
54
+ process.exit(1);
55
+ }
56
+
57
+ const shutdown = async () => {
58
+ console.error('\n[llm-bridge] shutting down...');
59
+ await server.stop();
60
+ process.exit(0);
61
+ };
62
+
63
+ process.on('SIGINT', shutdown);
64
+ process.on('SIGTERM', shutdown);
65
+ }
package/src/index.ts ADDED
@@ -0,0 +1,71 @@
1
+ #!/usr/bin/env node
2
+ import { initCommand } from './commands/init.js';
3
+ import { startCommand } from './commands/start.js';
4
+ import { configureOpencodeCommand } from './commands/configure.js';
5
+ import { doctorCommand } from './commands/doctor.js';
6
+ import {
7
+ daemonStatusCommand,
8
+ daemonDownloadCommand,
9
+ daemonLocateCommand,
10
+ } from './commands/daemon.js';
11
+ import { installDaemonCommand, uninstallDaemonCommand } from './commands/daemon.js';
12
+
13
+ const command = process.argv[2] ?? 'help';
14
+
15
+ async function main(): Promise<void> {
16
+ switch (command) {
17
+ case 'init':
18
+ await initCommand();
19
+ break;
20
+ case 'start':
21
+ await startCommand();
22
+ break;
23
+ case 'configure':
24
+ await configureOpencodeCommand();
25
+ break;
26
+ case 'doctor':
27
+ await doctorCommand();
28
+ break;
29
+ case 'install-daemon':
30
+ await installDaemonCommand();
31
+ break;
32
+ case 'uninstall-daemon':
33
+ await uninstallDaemonCommand();
34
+ break;
35
+ case 'daemon': {
36
+ const subcommand = process.argv[3] ?? 'status';
37
+ switch (subcommand) {
38
+ case 'status':
39
+ await daemonStatusCommand();
40
+ break;
41
+ case 'download':
42
+ await daemonDownloadCommand();
43
+ break;
44
+ case 'locate':
45
+ await daemonLocateCommand();
46
+ break;
47
+ default:
48
+ console.log('Usage: llm-bridge daemon [status|download|locate]');
49
+ }
50
+ break;
51
+ }
52
+ case 'help':
53
+ default:
54
+ console.log(`llm-bridge v1.0.0
55
+
56
+ Usage:
57
+ llm-bridge init Interactive setup wizard (configure one or more providers)
58
+ llm-bridge start Launch bridge server (all configured plugins registered)
59
+ llm-bridge configure Inject OpenCode config for the default provider
60
+ llm-bridge doctor Run diagnostics
61
+ llm-bridge install-daemon Install macOS LaunchAgent
62
+ llm-bridge uninstall-daemon Remove macOS LaunchAgent
63
+ llm-bridge daemon [status|download|locate] Manage Windsurf daemon binary
64
+ llm-bridge help Show this help`);
65
+ }
66
+ }
67
+
68
+ main().catch((err) => {
69
+ console.error(err);
70
+ process.exit(1);
71
+ });
@@ -0,0 +1,18 @@
1
+ import { loadConfig, saveConfig, BridgeConfig } from '@ai-ide-bridge/core';
2
+
3
+ export function readConfig(): BridgeConfig {
4
+ return loadConfig();
5
+ }
6
+
7
+ export function writeConfig(config: BridgeConfig): void {
8
+ saveConfig(config);
9
+ }
10
+
11
+ export function setPluginConfig(pluginName: string, envVars: Record<string, string>): void {
12
+ const config = readConfig();
13
+ config.plugins[pluginName] = { ...config.plugins[pluginName], ...envVars };
14
+ if (!config.defaultPlugin) {
15
+ config.defaultPlugin = pluginName;
16
+ }
17
+ writeConfig(config);
18
+ }
@@ -0,0 +1,43 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import os from 'node:os';
4
+
5
+ export function findOpencodeConfig(): string | null {
6
+ const candidates = [
7
+ path.join(os.homedir(), '.config', 'opencode', 'opencode.json'),
8
+ path.join(os.homedir(), '.config', 'opencode', 'opencode.jsonc'),
9
+ path.join(process.cwd(), 'opencode.json'),
10
+ path.join(process.cwd(), 'opencode.jsonc'),
11
+ ];
12
+ for (const p of candidates) {
13
+ if (fs.existsSync(p)) return p;
14
+ }
15
+ return null;
16
+ }
17
+
18
+ export function injectProvider(
19
+ configPath: string,
20
+ providerId: string,
21
+ modelId: string,
22
+ port: number,
23
+ ): void {
24
+ const raw = fs.readFileSync(configPath, 'utf8');
25
+ let config: any;
26
+ try {
27
+ config = JSON.parse(raw);
28
+ } catch (e) {
29
+ throw new Error(`Failed to parse ${configPath}: ${(e as Error).message}`);
30
+ }
31
+ if (!config.provider) config.provider = {};
32
+ config.provider[providerId] = {
33
+ npm: '@ai-sdk/openai-compatible',
34
+ name: 'LLM Bridge',
35
+ options: {
36
+ apiKey: 'bridge-local',
37
+ baseURL: `http://127.0.0.1:${port}/v1`,
38
+ },
39
+ models: { [modelId]: { name: modelId } },
40
+ };
41
+ config.model = `${providerId}/${modelId}`;
42
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
43
+ }
@@ -0,0 +1,30 @@
1
+ import { describe, it, expect, beforeEach } from 'vitest';
2
+ import { injectProvider } from '../src/utils/opencode.js';
3
+ import fs from 'node:fs';
4
+ import os from 'node:os';
5
+ import path from 'node:path';
6
+
7
+ describe('opencode utils', () => {
8
+ let tmpFile: string;
9
+
10
+ beforeEach(() => {
11
+ tmpFile = path.join(os.tmpdir(), `opencode-test-${Date.now()}.json`);
12
+ fs.writeFileSync(tmpFile, '{}');
13
+ });
14
+
15
+ it('injects provider into empty config', () => {
16
+ injectProvider(tmpFile, 'test-provider', 'test-model', 3849);
17
+ const config = JSON.parse(fs.readFileSync(tmpFile, 'utf8'));
18
+ expect(config.provider['test-provider']).toBeDefined();
19
+ expect(config.provider['test-provider'].options.baseURL).toBe('http://127.0.0.1:3849/v1');
20
+ expect(config.model).toBe('test-provider/test-model');
21
+ });
22
+
23
+ it('preserves existing config fields', () => {
24
+ fs.writeFileSync(tmpFile, JSON.stringify({ existing: 'value' }));
25
+ injectProvider(tmpFile, 'test-provider', 'test-model', 3849);
26
+ const config = JSON.parse(fs.readFileSync(tmpFile, 'utf8'));
27
+ expect(config.existing).toBe('value');
28
+ expect(config.provider['test-provider']).toBeDefined();
29
+ });
30
+ });
@@ -0,0 +1,61 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
+ import fs from 'node:fs';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+
6
+ describe('initCommand', () => {
7
+ let tmpDir: string;
8
+ let configDir: string;
9
+
10
+ beforeEach(() => {
11
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'llm-bridge-init-test-'));
12
+ configDir = path.join(tmpDir, '.config', 'llm-bridge');
13
+ fs.mkdirSync(configDir, { recursive: true });
14
+ vi.spyOn(os, 'homedir').mockReturnValue(tmpDir);
15
+ });
16
+
17
+ afterEach(() => {
18
+ fs.rmSync(tmpDir, { recursive: true, force: true });
19
+ vi.restoreAllMocks();
20
+ });
21
+
22
+ it('writes config with single provider', async () => {
23
+ const config = {
24
+ defaultPlugin: 'cursor',
25
+ port: 3849,
26
+ host: '127.0.0.1',
27
+ plugins: { cursor: { CURSOR_API_KEY: 'test-api-key' } },
28
+ sessionTTL: 1800,
29
+ toolMode: 'lenient',
30
+ };
31
+
32
+ const configPath = path.join(configDir, 'config.json');
33
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
34
+ const loaded = JSON.parse(fs.readFileSync(configPath, 'utf8'));
35
+
36
+ expect(loaded.defaultPlugin).toBe('cursor');
37
+ expect(loaded.plugins.cursor.CURSOR_API_KEY).toBe('test-api-key');
38
+ });
39
+
40
+ it('writes config with multiple providers', async () => {
41
+ const config = {
42
+ defaultPlugin: 'cursor',
43
+ port: 3849,
44
+ host: '127.0.0.1',
45
+ plugins: {
46
+ cursor: { CURSOR_API_KEY: 'test-api-key' },
47
+ windsurf: { WINDSURF_TOKEN: 'test-windsurf-token' },
48
+ },
49
+ sessionTTL: 1800,
50
+ toolMode: 'lenient',
51
+ };
52
+
53
+ const configPath = path.join(configDir, 'config.json');
54
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
55
+ const loaded = JSON.parse(fs.readFileSync(configPath, 'utf8'));
56
+
57
+ expect(loaded.defaultPlugin).toBe('cursor');
58
+ expect(loaded.plugins.cursor.CURSOR_API_KEY).toBe('test-api-key');
59
+ expect(loaded.plugins.windsurf.WINDSURF_TOKEN).toBe('test-windsurf-token');
60
+ });
61
+ });
@@ -0,0 +1,13 @@
1
+ import { describe, it, expect } from 'vitest';
2
+
3
+ describe('startCommand', () => {
4
+ it('registers all configured plugins', async () => {
5
+ // This is an integration test — verify config loading works
6
+ const { readConfig } = await import('../src/utils/config.js');
7
+ const config = readConfig();
8
+
9
+ // Config should have defaultPlugin field
10
+ expect(config.defaultPlugin).toBeDefined();
11
+ expect(typeof config.defaultPlugin).toBe('string');
12
+ });
13
+ });
package/tsconfig.json ADDED
@@ -0,0 +1,8 @@
1
+ {
2
+ "extends": "../tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "outDir": "dist",
5
+ "rootDir": "src"
6
+ },
7
+ "include": ["src/**/*.ts"]
8
+ }