@ai-ide-bridge/cli 1.0.5 → 1.1.1
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.
- package/.turbo/turbo-build.log +1 -1
- package/dist/commands/configure.js +78 -10
- package/dist/commands/daemon.d.ts +1 -0
- package/dist/commands/daemon.js +107 -13
- package/dist/commands/doctor.js +1 -1
- package/dist/commands/init.js +70 -5
- package/dist/commands/login.d.ts +1 -0
- package/dist/commands/login.js +62 -0
- package/dist/commands/logout.d.ts +1 -0
- package/dist/commands/logout.js +12 -0
- package/dist/commands/start.js +4 -4
- package/dist/core/config.d.ts +4 -0
- package/dist/core/config.js +43 -0
- package/dist/core/daemon-session.d.ts +14 -0
- package/dist/core/daemon-session.js +179 -0
- package/dist/core/daemon.d.ts +16 -0
- package/dist/core/daemon.js +168 -0
- package/dist/core/formatter.d.ts +3 -0
- package/dist/core/formatter.js +44 -0
- package/dist/core/index.d.ts +9 -0
- package/dist/core/index.js +9 -0
- package/dist/core/parser.d.ts +164 -0
- package/dist/core/parser.js +37 -0
- package/dist/core/registry.d.ts +16 -0
- package/dist/core/registry.js +53 -0
- package/dist/core/server.d.ts +19 -0
- package/dist/core/server.js +185 -0
- package/dist/core/session.d.ts +11 -0
- package/dist/core/session.js +39 -0
- package/dist/core/types.d.ts +166 -0
- package/dist/core/types.js +44 -0
- package/dist/index.js +22 -5
- package/dist/oauth/device-flow.d.ts +12 -0
- package/dist/oauth/device-flow.js +93 -0
- package/dist/oauth/flow.d.ts +11 -0
- package/dist/oauth/flow.js +75 -0
- package/dist/oauth/index.d.ts +6 -0
- package/dist/oauth/index.js +5 -0
- package/dist/oauth/lifecycle.d.ts +13 -0
- package/dist/oauth/lifecycle.js +56 -0
- package/dist/oauth/providers.d.ts +2 -0
- package/dist/oauth/providers.js +19 -0
- package/dist/oauth/storage-file.d.ts +2 -0
- package/dist/oauth/storage-file.js +68 -0
- package/dist/oauth/storage.d.ts +2 -0
- package/dist/oauth/storage.js +4 -0
- package/dist/oauth/types.d.ts +44 -0
- package/dist/oauth/types.js +1 -0
- package/dist/plugins/copilot/auth.d.ts +7 -0
- package/dist/plugins/copilot/auth.js +30 -0
- package/dist/plugins/copilot/index.d.ts +5 -0
- package/dist/plugins/copilot/index.js +4 -0
- package/dist/plugins/copilot/plugin.d.ts +8 -0
- package/dist/plugins/copilot/plugin.js +29 -0
- package/dist/plugins/copilot/session.d.ts +8 -0
- package/dist/plugins/copilot/session.js +115 -0
- package/dist/plugins/copilot/tools.d.ts +10 -0
- package/dist/plugins/copilot/tools.js +10 -0
- package/dist/plugins/copilot/types.d.ts +15 -0
- package/dist/plugins/copilot/types.js +27 -0
- package/dist/plugins/cursor/index.d.ts +2 -0
- package/dist/plugins/cursor/index.js +2 -0
- package/dist/plugins/cursor/plugin.d.ts +8 -0
- package/dist/plugins/cursor/plugin.js +36 -0
- package/dist/plugins/cursor/session.d.ts +11 -0
- package/dist/plugins/cursor/session.js +69 -0
- package/dist/plugins/cursor/tools.d.ts +11 -0
- package/dist/plugins/cursor/tools.js +13 -0
- package/dist/plugins/windsurf/auth.d.ts +3 -0
- package/dist/plugins/windsurf/auth.js +20 -0
- package/dist/plugins/windsurf/daemon.d.ts +6 -0
- package/dist/plugins/windsurf/daemon.js +16 -0
- package/dist/plugins/windsurf/index.d.ts +5 -0
- package/dist/plugins/windsurf/index.js +4 -0
- package/dist/plugins/windsurf/models.d.ts +2 -0
- package/dist/plugins/windsurf/models.js +42 -0
- package/dist/plugins/windsurf/plugin.d.ts +8 -0
- package/dist/plugins/windsurf/plugin.js +31 -0
- package/dist/plugins/windsurf/session.d.ts +5 -0
- package/dist/plugins/windsurf/session.js +6 -0
- package/dist/plugins/windsurf/tools.d.ts +3 -0
- package/dist/plugins/windsurf/tools.js +10 -0
- package/dist/plugins/windsurf/types.d.ts +22 -0
- package/dist/plugins/windsurf/types.js +1 -0
- package/dist/utils/config.d.ts +1 -1
- package/dist/utils/config.js +1 -1
- package/dist/utils/opencode.d.ts +3 -1
- package/dist/utils/opencode.js +3 -3
- package/dist/utils/platform.d.ts +1 -0
- package/dist/utils/platform.js +3 -0
- package/package.json +3 -5
- package/src/commands/configure.ts +107 -12
- package/src/commands/daemon.ts +112 -13
- package/src/commands/doctor.ts +1 -1
- package/src/commands/init.ts +72 -5
- package/src/commands/login.ts +98 -0
- package/src/commands/logout.ts +15 -0
- package/src/commands/start.ts +4 -4
- package/src/core/config.ts +45 -0
- package/src/core/daemon-session.ts +199 -0
- package/src/core/daemon.ts +206 -0
- package/src/core/formatter.ts +56 -0
- package/src/core/index.ts +9 -0
- package/src/core/parser.ts +47 -0
- package/src/core/registry.ts +62 -0
- package/src/core/server.ts +211 -0
- package/src/core/session.ts +54 -0
- package/src/core/types.ts +100 -0
- package/src/index.ts +22 -4
- package/src/oauth/device-flow.ts +111 -0
- package/src/oauth/flow.ts +94 -0
- package/src/oauth/index.ts +6 -0
- package/src/oauth/lifecycle.ts +77 -0
- package/src/oauth/providers.ts +21 -0
- package/src/oauth/storage-file.ts +77 -0
- package/src/oauth/storage.ts +6 -0
- package/src/oauth/types.ts +50 -0
- package/src/plugins/copilot/auth.ts +39 -0
- package/src/plugins/copilot/index.ts +5 -0
- package/src/plugins/copilot/plugin.ts +31 -0
- package/src/plugins/copilot/session.ts +130 -0
- package/src/plugins/copilot/tools.ts +21 -0
- package/src/plugins/copilot/types.ts +43 -0
- package/src/plugins/cursor/index.ts +2 -0
- package/src/plugins/cursor/plugin.ts +37 -0
- package/src/plugins/cursor/session.ts +78 -0
- package/src/plugins/cursor/tools.ts +25 -0
- package/src/plugins/windsurf/auth.ts +23 -0
- package/src/plugins/windsurf/daemon.ts +24 -0
- package/src/plugins/windsurf/index.ts +5 -0
- package/src/plugins/windsurf/models.ts +44 -0
- package/src/plugins/windsurf/plugin.ts +34 -0
- package/src/plugins/windsurf/session.ts +8 -0
- package/src/plugins/windsurf/tools.ts +13 -0
- package/src/plugins/windsurf/types.ts +24 -0
- package/src/utils/config.ts +1 -1
- package/src/utils/opencode.ts +4 -3
- package/src/utils/platform.ts +3 -0
- package/test/configure.test.ts +19 -4
- package/test/daemon.test.ts +224 -0
package/src/commands/daemon.ts
CHANGED
|
@@ -2,20 +2,31 @@ import fs from 'node:fs';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import os from 'node:os';
|
|
4
4
|
import { execSync } from 'node:child_process';
|
|
5
|
-
import {
|
|
5
|
+
import { createWindsurfDaemon } from '../plugins/windsurf/daemon.js';
|
|
6
|
+
import { getPlatform } from '../utils/platform.js';
|
|
6
7
|
|
|
7
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
8
|
-
const __dirname = path.dirname(__filename);
|
|
9
8
|
const LABEL = 'com.llm-bridge.daemon';
|
|
10
9
|
|
|
11
10
|
export async function installDaemonCommand(): Promise<void> {
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
const platform = getPlatform();
|
|
12
|
+
if (platform === 'darwin') {
|
|
13
|
+
await installMacOSDaemon();
|
|
14
|
+
} else if (platform === 'linux') {
|
|
15
|
+
await installLinuxDaemon();
|
|
16
|
+
} else {
|
|
17
|
+
console.error('Daemon installation is only supported on macOS and Linux.');
|
|
14
18
|
process.exit(1);
|
|
15
19
|
}
|
|
20
|
+
}
|
|
16
21
|
|
|
22
|
+
async function installMacOSDaemon(): Promise<void> {
|
|
17
23
|
const plistPath = path.join(os.homedir(), 'Library', 'LaunchAgents', `${LABEL}.plist`);
|
|
18
|
-
const wrapperPath = path.join(
|
|
24
|
+
const wrapperPath = path.join(
|
|
25
|
+
path.dirname(process.execPath),
|
|
26
|
+
'..',
|
|
27
|
+
'scripts',
|
|
28
|
+
'llm-bridge-daemon.sh',
|
|
29
|
+
);
|
|
19
30
|
|
|
20
31
|
const plist = `<?xml version="1.0" encoding="UTF-8"?>
|
|
21
32
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
@@ -40,8 +51,13 @@ export async function installDaemonCommand(): Promise<void> {
|
|
|
40
51
|
</dict>
|
|
41
52
|
</plist>`;
|
|
42
53
|
|
|
43
|
-
|
|
44
|
-
|
|
54
|
+
try {
|
|
55
|
+
fs.mkdirSync(path.dirname(plistPath), { recursive: true });
|
|
56
|
+
fs.writeFileSync(plistPath, plist);
|
|
57
|
+
} catch (e) {
|
|
58
|
+
console.error('Failed to write plist file:', e);
|
|
59
|
+
process.exit(1);
|
|
60
|
+
}
|
|
45
61
|
|
|
46
62
|
try {
|
|
47
63
|
execSync(`launchctl bootstrap "gui/$(id -u)" "${plistPath}"`, { stdio: 'inherit' });
|
|
@@ -53,12 +69,58 @@ export async function installDaemonCommand(): Promise<void> {
|
|
|
53
69
|
}
|
|
54
70
|
}
|
|
55
71
|
|
|
72
|
+
async function installLinuxDaemon(): Promise<void> {
|
|
73
|
+
const serviceDir = path.join(os.homedir(), '.config', 'systemd', 'user');
|
|
74
|
+
const servicePath = path.join(serviceDir, 'llm-bridge.service');
|
|
75
|
+
|
|
76
|
+
const binaryPath = process.execPath;
|
|
77
|
+
|
|
78
|
+
const unit = `[Unit]
|
|
79
|
+
Description=llm-bridge daemon
|
|
80
|
+
After=network.target
|
|
81
|
+
|
|
82
|
+
[Service]
|
|
83
|
+
Type=simple
|
|
84
|
+
ExecStart=${binaryPath} start
|
|
85
|
+
Restart=on-failure
|
|
86
|
+
RestartSec=5
|
|
87
|
+
|
|
88
|
+
[Install]
|
|
89
|
+
WantedBy=default.target
|
|
90
|
+
`;
|
|
91
|
+
|
|
92
|
+
try {
|
|
93
|
+
fs.mkdirSync(serviceDir, { recursive: true });
|
|
94
|
+
fs.writeFileSync(servicePath, unit);
|
|
95
|
+
} catch (e) {
|
|
96
|
+
console.error('Failed to write service file:', e);
|
|
97
|
+
process.exit(1);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
try {
|
|
101
|
+
execSync('systemctl --user daemon-reload', { stdio: 'inherit' });
|
|
102
|
+
execSync('systemctl --user enable --now llm-bridge', { stdio: 'inherit' });
|
|
103
|
+
console.log(`Installed systemd user service: ${servicePath}`);
|
|
104
|
+
console.log('Logs: journalctl --user -u llm-bridge -f');
|
|
105
|
+
} catch (e) {
|
|
106
|
+
console.error('Failed to enable systemd service:', e);
|
|
107
|
+
process.exit(1);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
56
111
|
export async function uninstallDaemonCommand(): Promise<void> {
|
|
57
|
-
|
|
58
|
-
|
|
112
|
+
const platform = getPlatform();
|
|
113
|
+
if (platform === 'darwin') {
|
|
114
|
+
await uninstallMacOSDaemon();
|
|
115
|
+
} else if (platform === 'linux') {
|
|
116
|
+
await uninstallLinuxDaemon();
|
|
117
|
+
} else {
|
|
118
|
+
console.error('Daemon uninstallation is only supported on macOS and Linux.');
|
|
59
119
|
process.exit(1);
|
|
60
120
|
}
|
|
121
|
+
}
|
|
61
122
|
|
|
123
|
+
async function uninstallMacOSDaemon(): Promise<void> {
|
|
62
124
|
const plistPath = path.join(os.homedir(), 'Library', 'LaunchAgents', `${LABEL}.plist`);
|
|
63
125
|
|
|
64
126
|
try {
|
|
@@ -77,8 +139,32 @@ export async function uninstallDaemonCommand(): Promise<void> {
|
|
|
77
139
|
}
|
|
78
140
|
}
|
|
79
141
|
|
|
142
|
+
async function uninstallLinuxDaemon(): Promise<void> {
|
|
143
|
+
const servicePath = path.join(os.homedir(), '.config', 'systemd', 'user', 'llm-bridge.service');
|
|
144
|
+
|
|
145
|
+
try {
|
|
146
|
+
execSync('systemctl --user disable --now llm-bridge 2>/dev/null || true', {
|
|
147
|
+
stdio: 'inherit',
|
|
148
|
+
});
|
|
149
|
+
} catch {
|
|
150
|
+
// Ignore errors during disable
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (fs.existsSync(servicePath)) {
|
|
154
|
+
fs.unlinkSync(servicePath);
|
|
155
|
+
console.log(`Removed systemd service: ${servicePath}`);
|
|
156
|
+
} else {
|
|
157
|
+
console.log('No systemd service found.');
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
try {
|
|
161
|
+
execSync('systemctl --user daemon-reload', { stdio: 'inherit' });
|
|
162
|
+
} catch {
|
|
163
|
+
// Ignore errors during reload
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
80
167
|
export async function daemonStatusCommand(): Promise<void> {
|
|
81
|
-
const { createWindsurfDaemon } = await import('@ai-ide-bridge/windsurf/daemon.js');
|
|
82
168
|
const daemon = createWindsurfDaemon();
|
|
83
169
|
|
|
84
170
|
const path = await daemon.locate();
|
|
@@ -93,7 +179,6 @@ export async function daemonStatusCommand(): Promise<void> {
|
|
|
93
179
|
}
|
|
94
180
|
|
|
95
181
|
export async function daemonDownloadCommand(): Promise<void> {
|
|
96
|
-
const { createWindsurfDaemon } = await import('@ai-ide-bridge/windsurf/daemon.js');
|
|
97
182
|
const daemon = createWindsurfDaemon();
|
|
98
183
|
|
|
99
184
|
console.log('Downloading Windsurf language server...');
|
|
@@ -107,7 +192,6 @@ export async function daemonDownloadCommand(): Promise<void> {
|
|
|
107
192
|
}
|
|
108
193
|
|
|
109
194
|
export async function daemonLocateCommand(): Promise<void> {
|
|
110
|
-
const { createWindsurfDaemon } = await import('@ai-ide-bridge/windsurf/daemon.js');
|
|
111
195
|
const daemon = createWindsurfDaemon();
|
|
112
196
|
|
|
113
197
|
const path = await daemon.locate();
|
|
@@ -118,3 +202,18 @@ export async function daemonLocateCommand(): Promise<void> {
|
|
|
118
202
|
process.exit(1);
|
|
119
203
|
}
|
|
120
204
|
}
|
|
205
|
+
|
|
206
|
+
export async function daemonReloadCommand(): Promise<void> {
|
|
207
|
+
if (getPlatform() !== 'linux') {
|
|
208
|
+
console.error('Daemon reload is only supported on Linux.');
|
|
209
|
+
process.exit(1);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
try {
|
|
213
|
+
execSync('systemctl --user reload-or-restart llm-bridge', { stdio: 'inherit' });
|
|
214
|
+
console.log('Daemon reloaded.');
|
|
215
|
+
} catch (e) {
|
|
216
|
+
console.error('Failed to reload daemon:', e);
|
|
217
|
+
process.exit(1);
|
|
218
|
+
}
|
|
219
|
+
}
|
package/src/commands/doctor.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import http from 'node:http';
|
|
2
2
|
import { readConfig } from '../utils/config.js';
|
|
3
3
|
import fs from 'node:fs';
|
|
4
|
-
import { configPath } from '
|
|
4
|
+
import { configPath } from '../core/index.js';
|
|
5
5
|
|
|
6
6
|
export async function doctorCommand(): Promise<void> {
|
|
7
7
|
const config = readConfig();
|
package/src/commands/init.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { setPluginConfig, writeConfig, readConfig } from '../utils/config.js';
|
|
2
|
-
import { CursorBridgePlugin } from '
|
|
3
|
-
import { CopilotBridgePlugin } from '
|
|
4
|
-
import { WindsurfBridgePlugin } from '
|
|
2
|
+
import { CursorBridgePlugin } from '../plugins/cursor/index.js';
|
|
3
|
+
import { CopilotBridgePlugin } from '../plugins/copilot/index.js';
|
|
4
|
+
import { WindsurfBridgePlugin } from '../plugins/windsurf/index.js';
|
|
5
5
|
import { createInterface } from 'node:readline';
|
|
6
|
+
import { stdin as processStdin, stdout as processStdout } from 'node:process';
|
|
7
|
+
import { loginCommand } from './login.js';
|
|
6
8
|
|
|
7
9
|
const PROVIDERS = ['cursor', 'copilot', 'windsurf'] as const;
|
|
8
10
|
type Provider = (typeof PROVIDERS)[number];
|
|
@@ -23,7 +25,7 @@ function getCredentialPrompt(provider: Provider): string {
|
|
|
23
25
|
case 'cursor':
|
|
24
26
|
return 'Enter your CURSOR_API_KEY: ';
|
|
25
27
|
case 'copilot':
|
|
26
|
-
return 'Enter your GITHUB_TOKEN: ';
|
|
28
|
+
return 'Enter your GITHUB_TOKEN (or leave empty for OAuth): ';
|
|
27
29
|
case 'windsurf':
|
|
28
30
|
return 'Enter your WINDSURF_TOKEN: ';
|
|
29
31
|
}
|
|
@@ -40,6 +42,56 @@ function getEnvVar(provider: Provider): string {
|
|
|
40
42
|
}
|
|
41
43
|
}
|
|
42
44
|
|
|
45
|
+
async function authenticateWithOAuth(provider: Provider): Promise<boolean> {
|
|
46
|
+
if (provider === 'copilot') {
|
|
47
|
+
await loginCommand('copilot');
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
console.log(`OAuth not available for ${provider}. Use token authentication instead.`);
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function askHidden(prompt: string): Promise<string> {
|
|
55
|
+
return new Promise<string>((resolve, reject) => {
|
|
56
|
+
const stdin = processStdin;
|
|
57
|
+
const stdout = processStdout;
|
|
58
|
+
|
|
59
|
+
stdout.write(prompt);
|
|
60
|
+
|
|
61
|
+
const wasRaw = stdin.isRaw;
|
|
62
|
+
stdin.setRawMode(true);
|
|
63
|
+
stdin.resume();
|
|
64
|
+
|
|
65
|
+
let input = '';
|
|
66
|
+
|
|
67
|
+
const onData = (data: Buffer) => {
|
|
68
|
+
const char = data.toString();
|
|
69
|
+
if (char === '\r' || char === '\n') {
|
|
70
|
+
stdin.setRawMode(wasRaw);
|
|
71
|
+
stdin.pause();
|
|
72
|
+
stdin.removeListener('data', onData);
|
|
73
|
+
stdout.write('\n');
|
|
74
|
+
resolve(input);
|
|
75
|
+
} else if (char === '\x7f' || char === '\b') {
|
|
76
|
+
if (input.length > 0) {
|
|
77
|
+
input = input.slice(0, -1);
|
|
78
|
+
stdout.write('\b \b');
|
|
79
|
+
}
|
|
80
|
+
} else if (char === '\x03') {
|
|
81
|
+
stdin.setRawMode(wasRaw);
|
|
82
|
+
stdin.pause();
|
|
83
|
+
stdin.removeListener('data', onData);
|
|
84
|
+
process.exit(1);
|
|
85
|
+
} else {
|
|
86
|
+
input += char;
|
|
87
|
+
stdout.write('*');
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
stdin.on('data', onData);
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
43
95
|
export async function initCommand(): Promise<void> {
|
|
44
96
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
45
97
|
const ask = (q: string) => new Promise<string>((resolve) => rl.question(q, resolve));
|
|
@@ -69,8 +121,23 @@ export async function initCommand(): Promise<void> {
|
|
|
69
121
|
|
|
70
122
|
const provider = input as Provider;
|
|
71
123
|
|
|
124
|
+
if (provider === 'copilot') {
|
|
125
|
+
const method = await ask('Auth method (token/oauth): ');
|
|
126
|
+
if (method.toLowerCase() === 'oauth') {
|
|
127
|
+
const ok = await authenticateWithOAuth(provider);
|
|
128
|
+
if (!ok) continue;
|
|
129
|
+
console.log(`Authentication successful for ${provider}.`);
|
|
130
|
+
setPluginConfig(provider, { COPILOT_OAUTH: 'true' });
|
|
131
|
+
configuredProviders.push(provider);
|
|
132
|
+
if (!firstProvider) firstProvider = provider;
|
|
133
|
+
const more = await ask('Configure another provider? (y/n): ');
|
|
134
|
+
if (more.toLowerCase() !== 'y') break;
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
72
139
|
const envVar = getEnvVar(provider);
|
|
73
|
-
const credential = await
|
|
140
|
+
const credential = await askHidden(getCredentialPrompt(provider));
|
|
74
141
|
if (!credential) {
|
|
75
142
|
console.error('Credential is required.');
|
|
76
143
|
continue;
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { createInterface } from 'node:readline';
|
|
2
|
+
import { createTokenStore, DeviceFlow, providers } from '../oauth/index.js';
|
|
3
|
+
|
|
4
|
+
const SUPPORTED_PROVIDERS = ['copilot', 'cursor'] as const;
|
|
5
|
+
type SupportedProvider = (typeof SUPPORTED_PROVIDERS)[number];
|
|
6
|
+
|
|
7
|
+
export async function loginCommand(provider?: string): Promise<void> {
|
|
8
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
9
|
+
const ask = (q: string) => new Promise<string>((resolve) => rl.question(q, resolve));
|
|
10
|
+
|
|
11
|
+
let providerId: SupportedProvider;
|
|
12
|
+
|
|
13
|
+
if (provider && SUPPORTED_PROVIDERS.includes(provider as SupportedProvider)) {
|
|
14
|
+
providerId = provider as SupportedProvider;
|
|
15
|
+
} else {
|
|
16
|
+
const input = await ask(`Provider to login (${SUPPORTED_PROVIDERS.join(', ')}): `);
|
|
17
|
+
if (!SUPPORTED_PROVIDERS.includes(input as SupportedProvider)) {
|
|
18
|
+
console.error(`Unknown provider: ${input}`);
|
|
19
|
+
rl.close();
|
|
20
|
+
process.exit(1);
|
|
21
|
+
}
|
|
22
|
+
providerId = input as SupportedProvider;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const oauthProvider = providers[providerId];
|
|
26
|
+
if (!oauthProvider) {
|
|
27
|
+
console.error(`OAuth not configured for provider: ${providerId}`);
|
|
28
|
+
rl.close();
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const store = createTokenStore();
|
|
33
|
+
|
|
34
|
+
if (oauthProvider.deviceFlow) {
|
|
35
|
+
await loginWithDeviceFlow(rl, oauthProvider, store);
|
|
36
|
+
} else {
|
|
37
|
+
await loginWithPKCE(rl, oauthProvider, store);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
rl.close();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function loginWithDeviceFlow(
|
|
44
|
+
rl: ReturnType<typeof createInterface>,
|
|
45
|
+
provider: {
|
|
46
|
+
id: string;
|
|
47
|
+
name: string;
|
|
48
|
+
authUrl: string;
|
|
49
|
+
tokenUrl: string;
|
|
50
|
+
scopes: string[];
|
|
51
|
+
clientId: string;
|
|
52
|
+
},
|
|
53
|
+
store: ReturnType<typeof createTokenStore>,
|
|
54
|
+
): Promise<void> {
|
|
55
|
+
const config = {
|
|
56
|
+
provider,
|
|
57
|
+
store,
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const deviceFlow = new DeviceFlow(config);
|
|
61
|
+
|
|
62
|
+
console.log(`\nAuthenticating with ${provider.name}...`);
|
|
63
|
+
console.log('Requesting device code...');
|
|
64
|
+
|
|
65
|
+
try {
|
|
66
|
+
const deviceCode = await deviceFlow.start();
|
|
67
|
+
|
|
68
|
+
console.log(`\nOpen this URL in your browser: ${deviceCode.verificationUri}`);
|
|
69
|
+
console.log(`Enter this code: ${deviceCode.userCode}\n`);
|
|
70
|
+
console.log('Waiting for authorization...');
|
|
71
|
+
|
|
72
|
+
const token = await deviceFlow.poll();
|
|
73
|
+
|
|
74
|
+
const expiresIn = Math.round((token.expiresAt - Date.now()) / 1000);
|
|
75
|
+
console.log(`\nAuthentication successful!`);
|
|
76
|
+
console.log(`Token stored securely. Expires in ${expiresIn}s.`);
|
|
77
|
+
} catch (err: any) {
|
|
78
|
+
console.error(`\nAuthentication failed: ${err.message}`);
|
|
79
|
+
process.exit(1);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async function loginWithPKCE(
|
|
84
|
+
rl: ReturnType<typeof createInterface>,
|
|
85
|
+
provider: {
|
|
86
|
+
id: string;
|
|
87
|
+
name: string;
|
|
88
|
+
authUrl: string;
|
|
89
|
+
tokenUrl: string;
|
|
90
|
+
scopes: string[];
|
|
91
|
+
clientId: string;
|
|
92
|
+
},
|
|
93
|
+
store: ReturnType<typeof createTokenStore>,
|
|
94
|
+
): Promise<void> {
|
|
95
|
+
console.log(`\nOAuth with PKCE is not yet supported for ${provider.name}.`);
|
|
96
|
+
console.log('Use a personal access token instead: llm-bridge init');
|
|
97
|
+
process.exit(1);
|
|
98
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { createTokenStore } from '../oauth/index.js';
|
|
2
|
+
|
|
3
|
+
export async function logoutCommand(provider?: string): Promise<void> {
|
|
4
|
+
const store = createTokenStore();
|
|
5
|
+
|
|
6
|
+
if (provider) {
|
|
7
|
+
await store.delete(provider);
|
|
8
|
+
console.log(`Logged out from ${provider}.`);
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
await store.delete('copilot');
|
|
13
|
+
await store.delete('cursor');
|
|
14
|
+
console.log('Logged out from all providers.');
|
|
15
|
+
}
|
package/src/commands/start.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { BridgeServer, loadConfig } from '
|
|
2
|
-
import { CursorBridgePlugin } from '
|
|
3
|
-
import { CopilotBridgePlugin } from '
|
|
4
|
-
import { WindsurfBridgePlugin } from '
|
|
1
|
+
import { BridgeServer, loadConfig } from '../core/index.js';
|
|
2
|
+
import { CursorBridgePlugin } from '../plugins/cursor/index.js';
|
|
3
|
+
import { CopilotBridgePlugin } from '../plugins/copilot/index.js';
|
|
4
|
+
import { WindsurfBridgePlugin } from '../plugins/windsurf/index.js';
|
|
5
5
|
|
|
6
6
|
async function getPlugin(name: string) {
|
|
7
7
|
switch (name) {
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import { BridgeConfig, DefaultConfig } from './types.js';
|
|
5
|
+
|
|
6
|
+
export function configPath(): string {
|
|
7
|
+
const home = os.homedir();
|
|
8
|
+
return path.join(home, '.config', 'llm-bridge', 'config.json');
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function loadConfig(): BridgeConfig {
|
|
12
|
+
const envPort = process.env.LLM_BRIDGE_PORT;
|
|
13
|
+
const envHost = process.env.LLM_BRIDGE_HOST;
|
|
14
|
+
|
|
15
|
+
try {
|
|
16
|
+
const filePath = process.env.LLM_BRIDGE_CONFIG ?? configPath();
|
|
17
|
+
if (fs.existsSync(filePath)) {
|
|
18
|
+
const raw = fs.readFileSync(filePath, 'utf8');
|
|
19
|
+
const fileConfig = JSON.parse(raw) as Partial<BridgeConfig>;
|
|
20
|
+
const config = { ...DefaultConfig, ...fileConfig };
|
|
21
|
+
|
|
22
|
+
if (fileConfig.activePlugin && !fileConfig.defaultPlugin) {
|
|
23
|
+
config.defaultPlugin = fileConfig.activePlugin;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (envPort) config.port = parseInt(envPort, 10);
|
|
27
|
+
if (envHost) config.host = envHost;
|
|
28
|
+
return config;
|
|
29
|
+
}
|
|
30
|
+
} catch (err) {
|
|
31
|
+
console.warn('[llm-bridge] failed to load config file, using defaults:', err);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const config = { ...DefaultConfig };
|
|
35
|
+
if (envPort) config.port = parseInt(envPort, 10);
|
|
36
|
+
if (envHost) config.host = envHost;
|
|
37
|
+
return config;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function saveConfig(config: BridgeConfig): void {
|
|
41
|
+
const filePath = configPath();
|
|
42
|
+
const dir = path.dirname(filePath);
|
|
43
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
44
|
+
fs.writeFileSync(filePath, JSON.stringify(config, null, 2));
|
|
45
|
+
}
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import type { ChildProcess } from 'node:child_process';
|
|
2
|
+
import type { BridgeSession, Message, ToolDefinition, StreamChunk } from './types.js';
|
|
3
|
+
import type { DaemonManager } from './daemon.js';
|
|
4
|
+
|
|
5
|
+
const MAX_BUFFER_SIZE = 1024 * 1024;
|
|
6
|
+
|
|
7
|
+
export class DaemonBridgeSession implements BridgeSession {
|
|
8
|
+
private proc: ChildProcess | null = null;
|
|
9
|
+
private requestId = 0;
|
|
10
|
+
private busy = false;
|
|
11
|
+
|
|
12
|
+
constructor(
|
|
13
|
+
private daemon: DaemonManager,
|
|
14
|
+
private token: string,
|
|
15
|
+
private model: string,
|
|
16
|
+
private cwd: string,
|
|
17
|
+
) {}
|
|
18
|
+
|
|
19
|
+
async *send(messages: Message[], tools?: ToolDefinition[]): AsyncIterable<StreamChunk> {
|
|
20
|
+
if (this.busy) {
|
|
21
|
+
yield {
|
|
22
|
+
type: 'error',
|
|
23
|
+
content: 'Session is busy — concurrent send() calls are not supported',
|
|
24
|
+
finishReason: 'error',
|
|
25
|
+
};
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
this.busy = true;
|
|
29
|
+
|
|
30
|
+
if (!this.proc) {
|
|
31
|
+
const binaryPath = await this.daemon.locate();
|
|
32
|
+
if (!binaryPath) {
|
|
33
|
+
this.busy = false;
|
|
34
|
+
throw new Error(`Daemon binary '${this.daemon.binaryName}' not found`);
|
|
35
|
+
}
|
|
36
|
+
this.proc = this.daemon.spawn(binaryPath, []);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const id = ++this.requestId;
|
|
40
|
+
const request = {
|
|
41
|
+
jsonrpc: '2.0',
|
|
42
|
+
id,
|
|
43
|
+
method: 'chat/completions',
|
|
44
|
+
params: {
|
|
45
|
+
token: this.token,
|
|
46
|
+
model: this.model,
|
|
47
|
+
messages,
|
|
48
|
+
stream: true,
|
|
49
|
+
tools: tools?.map((t) => ({
|
|
50
|
+
type: 'function' as const,
|
|
51
|
+
function: {
|
|
52
|
+
name: t.function.name,
|
|
53
|
+
description: t.function.description,
|
|
54
|
+
parameters: t.function.parameters,
|
|
55
|
+
},
|
|
56
|
+
})),
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
this.proc.stdin!.write(JSON.stringify(request) + '\n');
|
|
61
|
+
|
|
62
|
+
let buffer = '';
|
|
63
|
+
let stderrBuffer = '';
|
|
64
|
+
let finished = false;
|
|
65
|
+
let capturedError: Error | null = null;
|
|
66
|
+
let onDataResolve: (() => void) | null = null;
|
|
67
|
+
|
|
68
|
+
const onStderr = (data: Buffer) => {
|
|
69
|
+
stderrBuffer += data.toString();
|
|
70
|
+
};
|
|
71
|
+
this.proc.stderr?.on('data', onStderr);
|
|
72
|
+
|
|
73
|
+
const onData = (data: Buffer) => {
|
|
74
|
+
if (buffer.length <= MAX_BUFFER_SIZE) {
|
|
75
|
+
buffer += data.toString();
|
|
76
|
+
}
|
|
77
|
+
if (buffer.length > MAX_BUFFER_SIZE && !capturedError) {
|
|
78
|
+
capturedError = new Error(`stdout buffer exceeded max size of ${MAX_BUFFER_SIZE} bytes`);
|
|
79
|
+
onDataResolve?.();
|
|
80
|
+
}
|
|
81
|
+
onDataResolve?.();
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const onError = (err: Error) => {
|
|
85
|
+
if (!capturedError) {
|
|
86
|
+
capturedError = err;
|
|
87
|
+
onDataResolve?.();
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const onExit = (code: number | null) => {
|
|
92
|
+
if (!finished && !capturedError) {
|
|
93
|
+
capturedError = new Error(`Process exited with code ${code ?? 'unknown'}`);
|
|
94
|
+
onDataResolve?.();
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
this.proc.stdout!.on('data', onData);
|
|
99
|
+
this.proc.on('error', onError);
|
|
100
|
+
this.proc.on('exit', onExit);
|
|
101
|
+
|
|
102
|
+
try {
|
|
103
|
+
while (!finished) {
|
|
104
|
+
if (capturedError) {
|
|
105
|
+
const stderrInfo = stderrBuffer.trim() ? ` (stderr: ${stderrBuffer.slice(0, 500)})` : '';
|
|
106
|
+
yield {
|
|
107
|
+
type: 'error',
|
|
108
|
+
content: `Process error: ${(capturedError as Error).message}${stderrInfo}`,
|
|
109
|
+
finishReason: 'error',
|
|
110
|
+
};
|
|
111
|
+
break;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const newlineIndex = buffer.indexOf('\n');
|
|
115
|
+
if (newlineIndex === -1) {
|
|
116
|
+
const waitForData = new Promise<void>((resolve) => {
|
|
117
|
+
onDataResolve = resolve;
|
|
118
|
+
});
|
|
119
|
+
await waitForData;
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const line = buffer.slice(0, newlineIndex);
|
|
124
|
+
buffer = buffer.slice(newlineIndex + 1);
|
|
125
|
+
|
|
126
|
+
if (!line.trim()) continue;
|
|
127
|
+
|
|
128
|
+
let parsed: any;
|
|
129
|
+
try {
|
|
130
|
+
parsed = JSON.parse(line);
|
|
131
|
+
} catch {
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (parsed.error) {
|
|
136
|
+
yield {
|
|
137
|
+
type: 'error',
|
|
138
|
+
content: `JSON-RPC error: ${parsed.error.message}`,
|
|
139
|
+
finishReason: 'error',
|
|
140
|
+
};
|
|
141
|
+
finished = true;
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (parsed.method === 'chat/chunk' && parsed.params) {
|
|
146
|
+
const delta = parsed.params.delta;
|
|
147
|
+
if (delta?.content) {
|
|
148
|
+
yield { type: 'text', content: delta.content };
|
|
149
|
+
}
|
|
150
|
+
if (delta?.tool_calls) {
|
|
151
|
+
for (const tc of delta.tool_calls) {
|
|
152
|
+
yield {
|
|
153
|
+
type: 'tool_call',
|
|
154
|
+
toolCall: {
|
|
155
|
+
id: tc.id ?? '',
|
|
156
|
+
name: tc.name ?? '',
|
|
157
|
+
arguments: tc.arguments ?? '',
|
|
158
|
+
},
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (parsed.method === 'chat/done' && parsed.params) {
|
|
165
|
+
const reason = parsed.params.finishReason;
|
|
166
|
+
yield {
|
|
167
|
+
type: 'done',
|
|
168
|
+
finishReason:
|
|
169
|
+
reason === 'stop'
|
|
170
|
+
? 'stop'
|
|
171
|
+
: reason === 'tool_calls'
|
|
172
|
+
? 'tool_calls'
|
|
173
|
+
: reason === 'length'
|
|
174
|
+
? 'length'
|
|
175
|
+
: 'error',
|
|
176
|
+
};
|
|
177
|
+
finished = true;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (parsed.id === id && parsed.result) {
|
|
181
|
+
finished = true;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
} finally {
|
|
185
|
+
this.proc?.stdout?.removeListener('data', onData);
|
|
186
|
+
this.proc?.removeListener('error', onError);
|
|
187
|
+
this.proc?.removeListener('exit', onExit);
|
|
188
|
+
this.proc?.stderr?.removeListener('data', onStderr);
|
|
189
|
+
this.busy = false;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
async dispose(): Promise<void> {
|
|
194
|
+
if (this.proc) {
|
|
195
|
+
this.proc.kill();
|
|
196
|
+
this.proc = null;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|