@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
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
export class PluginRegistry {
|
|
2
|
+
plugins = new Map();
|
|
3
|
+
activePluginName = null;
|
|
4
|
+
defaultPluginName = null;
|
|
5
|
+
health = new Map();
|
|
6
|
+
register(plugin) {
|
|
7
|
+
this.plugins.set(plugin.name, plugin);
|
|
8
|
+
this.health.set(plugin.name, {
|
|
9
|
+
name: plugin.name,
|
|
10
|
+
healthy: true,
|
|
11
|
+
lastChecked: new Date(),
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
getPlugin(name) {
|
|
15
|
+
return this.plugins.get(name);
|
|
16
|
+
}
|
|
17
|
+
setActive(name) {
|
|
18
|
+
if (!this.plugins.has(name)) {
|
|
19
|
+
throw new Error(`Plugin "${name}" is not registered`);
|
|
20
|
+
}
|
|
21
|
+
this.activePluginName = name;
|
|
22
|
+
}
|
|
23
|
+
setDefault(name) {
|
|
24
|
+
if (!this.plugins.has(name)) {
|
|
25
|
+
throw new Error(`Plugin "${name}" is not registered`);
|
|
26
|
+
}
|
|
27
|
+
this.defaultPluginName = name;
|
|
28
|
+
}
|
|
29
|
+
getActivePlugin() {
|
|
30
|
+
if (!this.activePluginName)
|
|
31
|
+
return null;
|
|
32
|
+
return this.plugins.get(this.activePluginName) ?? null;
|
|
33
|
+
}
|
|
34
|
+
getDefaultPlugin() {
|
|
35
|
+
if (!this.defaultPluginName)
|
|
36
|
+
return null;
|
|
37
|
+
return this.plugins.get(this.defaultPluginName) ?? null;
|
|
38
|
+
}
|
|
39
|
+
listPlugins() {
|
|
40
|
+
return Array.from(this.plugins.values());
|
|
41
|
+
}
|
|
42
|
+
markUnhealthy(name, error) {
|
|
43
|
+
this.health.set(name, {
|
|
44
|
+
name,
|
|
45
|
+
healthy: false,
|
|
46
|
+
lastChecked: new Date(),
|
|
47
|
+
error,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
getHealth(name) {
|
|
51
|
+
return this.health.get(name);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { BridgeConfig, BridgePlugin } from './types.js';
|
|
2
|
+
export declare class BridgeServer {
|
|
3
|
+
private server;
|
|
4
|
+
private registry;
|
|
5
|
+
private sessions;
|
|
6
|
+
private config;
|
|
7
|
+
constructor(config?: Partial<BridgeConfig>);
|
|
8
|
+
start(): Promise<void>;
|
|
9
|
+
stop(): Promise<void>;
|
|
10
|
+
address(): import('net').AddressInfo | string | null;
|
|
11
|
+
private handleRequest;
|
|
12
|
+
private handleModels;
|
|
13
|
+
private resolvePlugin;
|
|
14
|
+
private handleChatCompletions;
|
|
15
|
+
private jsonResponse;
|
|
16
|
+
registerPlugin(plugin: BridgePlugin): void;
|
|
17
|
+
setActivePlugin(name: string): void;
|
|
18
|
+
setDefaultPlugin(name: string): void;
|
|
19
|
+
}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import http from 'node:http';
|
|
2
|
+
import crypto from 'node:crypto';
|
|
3
|
+
import { DefaultConfig } from './types.js';
|
|
4
|
+
import { parseChatRequest } from './parser.js';
|
|
5
|
+
import { PluginRegistry } from './registry.js';
|
|
6
|
+
import { SessionStore } from './session.js';
|
|
7
|
+
import { formatStreamChunk, formatCompletion } from './formatter.js';
|
|
8
|
+
export class BridgeServer {
|
|
9
|
+
server = null;
|
|
10
|
+
registry;
|
|
11
|
+
sessions;
|
|
12
|
+
config;
|
|
13
|
+
constructor(config = {}) {
|
|
14
|
+
this.config = { ...DefaultConfig, ...config };
|
|
15
|
+
this.registry = new PluginRegistry();
|
|
16
|
+
this.sessions = new SessionStore(this.config.sessionTTL);
|
|
17
|
+
}
|
|
18
|
+
async start() {
|
|
19
|
+
this.server = http.createServer((req, res) => {
|
|
20
|
+
this.handleRequest(req, res).catch((err) => {
|
|
21
|
+
console.error('[llm-bridge] unhandled error:', err);
|
|
22
|
+
if (!res.headersSent) {
|
|
23
|
+
this.jsonResponse(res, 500, { error: { message: 'internal error', type: 'internal' } });
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
return new Promise((resolve) => {
|
|
28
|
+
this.server.listen(this.config.port, this.config.host, () => {
|
|
29
|
+
const address = this.server.address();
|
|
30
|
+
const port = typeof address === 'object' ? address?.port : this.config.port;
|
|
31
|
+
console.error(`[llm-bridge] listening on http://${this.config.host}:${port}`);
|
|
32
|
+
resolve();
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
async stop() {
|
|
37
|
+
await this.sessions.disposeAll();
|
|
38
|
+
return new Promise((resolve) => {
|
|
39
|
+
this.server?.close(() => resolve());
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
address() {
|
|
43
|
+
return this.server?.address() ?? null;
|
|
44
|
+
}
|
|
45
|
+
async handleRequest(req, res) {
|
|
46
|
+
const url = new URL(req.url ?? '/', `http://${this.config.host}`);
|
|
47
|
+
const path = url.pathname.replace(/\/+$/, '') || '/';
|
|
48
|
+
if (req.method === 'GET' && path === '/health') {
|
|
49
|
+
this.jsonResponse(res, 200, { ok: true, service: 'llm-bridge' });
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
if (req.method === 'GET' && path === '/v1/models') {
|
|
53
|
+
await this.handleModels(req, res);
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
if (req.method === 'POST' && path === '/v1/chat/completions') {
|
|
57
|
+
await this.handleChatCompletions(req, res);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
this.jsonResponse(res, 404, { error: { message: `Not found: ${path}`, type: 'not_found' } });
|
|
61
|
+
}
|
|
62
|
+
async handleModels(_req, res) {
|
|
63
|
+
const allPlugins = this.registry.listPlugins();
|
|
64
|
+
if (allPlugins.length === 0) {
|
|
65
|
+
this.jsonResponse(res, 503, {
|
|
66
|
+
error: { message: 'No plugins configured', type: 'configuration_error' },
|
|
67
|
+
});
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
try {
|
|
71
|
+
const models = [];
|
|
72
|
+
const now = Math.floor(Date.now() / 1000);
|
|
73
|
+
for (const plugin of allPlugins) {
|
|
74
|
+
const config = this.config.plugins[plugin.name] ?? {};
|
|
75
|
+
const pluginModels = await plugin.listModels(config);
|
|
76
|
+
for (const m of pluginModels) {
|
|
77
|
+
models.push({
|
|
78
|
+
id: `${plugin.name}/${m.id}`,
|
|
79
|
+
object: 'model',
|
|
80
|
+
created: now,
|
|
81
|
+
owned_by: plugin.name,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
this.jsonResponse(res, 200, {
|
|
86
|
+
object: 'list',
|
|
87
|
+
data: models,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
catch (e) {
|
|
91
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
92
|
+
this.jsonResponse(res, 502, { error: { message: msg, type: 'provider_error' } });
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
resolvePlugin(model) {
|
|
96
|
+
const slashIndex = model.indexOf('/');
|
|
97
|
+
if (slashIndex !== -1) {
|
|
98
|
+
const prefix = model.slice(0, slashIndex);
|
|
99
|
+
const plugin = this.registry.getPlugin(prefix);
|
|
100
|
+
if (plugin)
|
|
101
|
+
return { plugin, error: null };
|
|
102
|
+
return { plugin: null, error: `Unknown plugin: "${prefix}"` };
|
|
103
|
+
}
|
|
104
|
+
return { plugin: this.registry.getDefaultPlugin(), error: null };
|
|
105
|
+
}
|
|
106
|
+
async handleChatCompletions(req, res) {
|
|
107
|
+
const parsed = await parseChatRequest(req);
|
|
108
|
+
if (!parsed.success) {
|
|
109
|
+
this.jsonResponse(res, 400, {
|
|
110
|
+
error: { message: parsed.error, type: 'invalid_request_error' },
|
|
111
|
+
});
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
const { messages, model, stream, tools } = parsed.data;
|
|
115
|
+
const { plugin, error } = this.resolvePlugin(model);
|
|
116
|
+
if (error) {
|
|
117
|
+
this.jsonResponse(res, 400, {
|
|
118
|
+
error: { message: error, type: 'invalid_request_error' },
|
|
119
|
+
});
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
if (!plugin) {
|
|
123
|
+
this.jsonResponse(res, 503, {
|
|
124
|
+
error: { message: 'No default plugin configured', type: 'configuration_error' },
|
|
125
|
+
});
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
try {
|
|
129
|
+
const config = this.config.plugins[plugin.name] ?? {};
|
|
130
|
+
const modelId = model.includes('/') ? model.slice(model.indexOf('/') + 1) : model;
|
|
131
|
+
const session = await plugin.createSession(config, modelId);
|
|
132
|
+
const sessionId = req.headers['x-session-id'];
|
|
133
|
+
this.sessions.set(sessionId ?? crypto.randomUUID(), session);
|
|
134
|
+
res.writeHead(200, {
|
|
135
|
+
'Content-Type': stream ? 'text/event-stream; charset=utf-8' : 'application/json',
|
|
136
|
+
'Cache-Control': 'no-cache',
|
|
137
|
+
Connection: 'keep-alive',
|
|
138
|
+
});
|
|
139
|
+
const chunks = [];
|
|
140
|
+
for await (const chunk of session.send(messages, tools)) {
|
|
141
|
+
if (stream) {
|
|
142
|
+
res.write(formatStreamChunk(chunk, model, `chatcmpl-${crypto.randomUUID()}`));
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
if (chunk.type === 'text' && chunk.content) {
|
|
146
|
+
chunks.push(chunk.content);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
if (!stream) {
|
|
151
|
+
const completionId = `chatcmpl-${crypto.randomUUID()}`;
|
|
152
|
+
const completion = formatCompletion(chunks.join(''), model, completionId);
|
|
153
|
+
res.end(JSON.stringify(completion));
|
|
154
|
+
}
|
|
155
|
+
else {
|
|
156
|
+
res.write(`data: [DONE]\n\n`);
|
|
157
|
+
res.end();
|
|
158
|
+
}
|
|
159
|
+
await session.dispose();
|
|
160
|
+
}
|
|
161
|
+
catch (e) {
|
|
162
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
163
|
+
if (stream && !res.writableEnded) {
|
|
164
|
+
res.write(`data: ${JSON.stringify({ error: { message: msg, type: 'provider_error' } })}\n\n`);
|
|
165
|
+
res.end();
|
|
166
|
+
}
|
|
167
|
+
else if (!res.headersSent) {
|
|
168
|
+
this.jsonResponse(res, 502, { error: { message: msg, type: 'provider_error' } });
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
jsonResponse(res, status, body) {
|
|
173
|
+
res.writeHead(status, { 'Content-Type': 'application/json' });
|
|
174
|
+
res.end(JSON.stringify(body));
|
|
175
|
+
}
|
|
176
|
+
registerPlugin(plugin) {
|
|
177
|
+
this.registry.register(plugin);
|
|
178
|
+
}
|
|
179
|
+
setActivePlugin(name) {
|
|
180
|
+
this.registry.setActive(name);
|
|
181
|
+
}
|
|
182
|
+
setDefaultPlugin(name) {
|
|
183
|
+
this.registry.setDefault(name);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { BridgeSession } from './types.js';
|
|
2
|
+
export declare class SessionStore {
|
|
3
|
+
private sessions;
|
|
4
|
+
private ttlMs;
|
|
5
|
+
constructor(ttlSeconds?: number);
|
|
6
|
+
set(id: string, session: BridgeSession): void;
|
|
7
|
+
get(id: string): BridgeSession | undefined;
|
|
8
|
+
cleanup(): void;
|
|
9
|
+
disposeAll(): Promise<void>;
|
|
10
|
+
get size(): number;
|
|
11
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export class SessionStore {
|
|
2
|
+
sessions = new Map();
|
|
3
|
+
ttlMs;
|
|
4
|
+
constructor(ttlSeconds = 1800) {
|
|
5
|
+
this.ttlMs = ttlSeconds * 1000;
|
|
6
|
+
}
|
|
7
|
+
set(id, session) {
|
|
8
|
+
this.sessions.set(id, { session, lastActive: Date.now() });
|
|
9
|
+
}
|
|
10
|
+
get(id) {
|
|
11
|
+
const entry = this.sessions.get(id);
|
|
12
|
+
if (entry) {
|
|
13
|
+
entry.lastActive = Date.now();
|
|
14
|
+
return entry.session;
|
|
15
|
+
}
|
|
16
|
+
return undefined;
|
|
17
|
+
}
|
|
18
|
+
cleanup() {
|
|
19
|
+
const now = Date.now();
|
|
20
|
+
for (const [id, entry] of this.sessions.entries()) {
|
|
21
|
+
if (now - entry.lastActive > this.ttlMs) {
|
|
22
|
+
this.sessions.delete(id);
|
|
23
|
+
entry.session.dispose().catch((err) => {
|
|
24
|
+
console.error(`[session] dispose error for ${id}:`, err);
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
async disposeAll() {
|
|
30
|
+
const disposals = Array.from(this.sessions.values()).map((entry) => entry.session.dispose().catch((err) => {
|
|
31
|
+
console.error('[session] disposeAll error:', err);
|
|
32
|
+
}));
|
|
33
|
+
this.sessions.clear();
|
|
34
|
+
await Promise.all(disposals);
|
|
35
|
+
}
|
|
36
|
+
get size() {
|
|
37
|
+
return this.sessions.size;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export declare const MessageSchema: z.ZodObject<{
|
|
3
|
+
role: z.ZodEnum<["system", "user", "assistant", "tool", "function"]>;
|
|
4
|
+
content: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
5
|
+
name: z.ZodOptional<z.ZodString>;
|
|
6
|
+
tool_call_id: z.ZodOptional<z.ZodString>;
|
|
7
|
+
tool_calls: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
8
|
+
id: z.ZodString;
|
|
9
|
+
type: z.ZodLiteral<"function">;
|
|
10
|
+
function: z.ZodObject<{
|
|
11
|
+
name: z.ZodString;
|
|
12
|
+
arguments: z.ZodString;
|
|
13
|
+
}, "strip", z.ZodTypeAny, {
|
|
14
|
+
name: string;
|
|
15
|
+
arguments: string;
|
|
16
|
+
}, {
|
|
17
|
+
name: string;
|
|
18
|
+
arguments: string;
|
|
19
|
+
}>;
|
|
20
|
+
}, "strip", z.ZodTypeAny, {
|
|
21
|
+
function: {
|
|
22
|
+
name: string;
|
|
23
|
+
arguments: string;
|
|
24
|
+
};
|
|
25
|
+
type: "function";
|
|
26
|
+
id: string;
|
|
27
|
+
}, {
|
|
28
|
+
function: {
|
|
29
|
+
name: string;
|
|
30
|
+
arguments: string;
|
|
31
|
+
};
|
|
32
|
+
type: "function";
|
|
33
|
+
id: string;
|
|
34
|
+
}>, "many">>;
|
|
35
|
+
}, "strip", z.ZodTypeAny, {
|
|
36
|
+
role: "function" | "system" | "user" | "assistant" | "tool";
|
|
37
|
+
content?: string | null | undefined;
|
|
38
|
+
name?: string | undefined;
|
|
39
|
+
tool_call_id?: string | undefined;
|
|
40
|
+
tool_calls?: {
|
|
41
|
+
function: {
|
|
42
|
+
name: string;
|
|
43
|
+
arguments: string;
|
|
44
|
+
};
|
|
45
|
+
type: "function";
|
|
46
|
+
id: string;
|
|
47
|
+
}[] | undefined;
|
|
48
|
+
}, {
|
|
49
|
+
role: "function" | "system" | "user" | "assistant" | "tool";
|
|
50
|
+
content?: string | null | undefined;
|
|
51
|
+
name?: string | undefined;
|
|
52
|
+
tool_call_id?: string | undefined;
|
|
53
|
+
tool_calls?: {
|
|
54
|
+
function: {
|
|
55
|
+
name: string;
|
|
56
|
+
arguments: string;
|
|
57
|
+
};
|
|
58
|
+
type: "function";
|
|
59
|
+
id: string;
|
|
60
|
+
}[] | undefined;
|
|
61
|
+
}>;
|
|
62
|
+
export type Message = z.infer<typeof MessageSchema>;
|
|
63
|
+
export declare const ToolDefinitionSchema: z.ZodObject<{
|
|
64
|
+
type: z.ZodLiteral<"function">;
|
|
65
|
+
function: z.ZodObject<{
|
|
66
|
+
name: z.ZodString;
|
|
67
|
+
description: z.ZodOptional<z.ZodString>;
|
|
68
|
+
parameters: z.ZodRecord<z.ZodString, z.ZodUnknown>;
|
|
69
|
+
}, "strip", z.ZodTypeAny, {
|
|
70
|
+
name: string;
|
|
71
|
+
parameters: Record<string, unknown>;
|
|
72
|
+
description?: string | undefined;
|
|
73
|
+
}, {
|
|
74
|
+
name: string;
|
|
75
|
+
parameters: Record<string, unknown>;
|
|
76
|
+
description?: string | undefined;
|
|
77
|
+
}>;
|
|
78
|
+
}, "strip", z.ZodTypeAny, {
|
|
79
|
+
function: {
|
|
80
|
+
name: string;
|
|
81
|
+
parameters: Record<string, unknown>;
|
|
82
|
+
description?: string | undefined;
|
|
83
|
+
};
|
|
84
|
+
type: "function";
|
|
85
|
+
}, {
|
|
86
|
+
function: {
|
|
87
|
+
name: string;
|
|
88
|
+
parameters: Record<string, unknown>;
|
|
89
|
+
description?: string | undefined;
|
|
90
|
+
};
|
|
91
|
+
type: "function";
|
|
92
|
+
}>;
|
|
93
|
+
export type ToolDefinition = z.infer<typeof ToolDefinitionSchema>;
|
|
94
|
+
export declare const ModelInfoSchema: z.ZodObject<{
|
|
95
|
+
id: z.ZodString;
|
|
96
|
+
name: z.ZodString;
|
|
97
|
+
capabilities: z.ZodOptional<z.ZodObject<{
|
|
98
|
+
streaming: z.ZodOptional<z.ZodBoolean>;
|
|
99
|
+
tools: z.ZodOptional<z.ZodBoolean>;
|
|
100
|
+
vision: z.ZodOptional<z.ZodBoolean>;
|
|
101
|
+
}, "strip", z.ZodTypeAny, {
|
|
102
|
+
streaming?: boolean | undefined;
|
|
103
|
+
tools?: boolean | undefined;
|
|
104
|
+
vision?: boolean | undefined;
|
|
105
|
+
}, {
|
|
106
|
+
streaming?: boolean | undefined;
|
|
107
|
+
tools?: boolean | undefined;
|
|
108
|
+
vision?: boolean | undefined;
|
|
109
|
+
}>>;
|
|
110
|
+
}, "strip", z.ZodTypeAny, {
|
|
111
|
+
name: string;
|
|
112
|
+
id: string;
|
|
113
|
+
capabilities?: {
|
|
114
|
+
streaming?: boolean | undefined;
|
|
115
|
+
tools?: boolean | undefined;
|
|
116
|
+
vision?: boolean | undefined;
|
|
117
|
+
} | undefined;
|
|
118
|
+
}, {
|
|
119
|
+
name: string;
|
|
120
|
+
id: string;
|
|
121
|
+
capabilities?: {
|
|
122
|
+
streaming?: boolean | undefined;
|
|
123
|
+
tools?: boolean | undefined;
|
|
124
|
+
vision?: boolean | undefined;
|
|
125
|
+
} | undefined;
|
|
126
|
+
}>;
|
|
127
|
+
export type ModelInfo = z.infer<typeof ModelInfoSchema>;
|
|
128
|
+
export type StreamChunkType = 'text' | 'tool_call' | 'tool_result' | 'error' | 'done';
|
|
129
|
+
export type FinishReason = 'stop' | 'tool_calls' | 'error' | 'length';
|
|
130
|
+
export interface StreamChunk {
|
|
131
|
+
type: StreamChunkType;
|
|
132
|
+
content?: string;
|
|
133
|
+
toolCall?: {
|
|
134
|
+
id: string;
|
|
135
|
+
name: string;
|
|
136
|
+
arguments: string;
|
|
137
|
+
};
|
|
138
|
+
finishReason?: FinishReason;
|
|
139
|
+
}
|
|
140
|
+
export interface BridgePlugin {
|
|
141
|
+
name: string;
|
|
142
|
+
version: string;
|
|
143
|
+
authenticate(config: Record<string, string>): Promise<boolean>;
|
|
144
|
+
listModels(config: Record<string, string>): Promise<ModelInfo[]>;
|
|
145
|
+
createSession(config: Record<string, string>, model: string): Promise<BridgeSession>;
|
|
146
|
+
}
|
|
147
|
+
export interface BridgeSession {
|
|
148
|
+
send(messages: Message[], tools?: ToolDefinition[]): AsyncIterable<StreamChunk>;
|
|
149
|
+
dispose(): Promise<void>;
|
|
150
|
+
}
|
|
151
|
+
export interface PluginHealth {
|
|
152
|
+
name: string;
|
|
153
|
+
healthy: boolean;
|
|
154
|
+
lastChecked: Date;
|
|
155
|
+
error?: string;
|
|
156
|
+
}
|
|
157
|
+
export interface BridgeConfig {
|
|
158
|
+
activePlugin?: string;
|
|
159
|
+
defaultPlugin: string;
|
|
160
|
+
port: number;
|
|
161
|
+
host: string;
|
|
162
|
+
plugins: Record<string, Record<string, string>>;
|
|
163
|
+
sessionTTL: number;
|
|
164
|
+
toolMode: 'strict' | 'lenient';
|
|
165
|
+
}
|
|
166
|
+
export declare const DefaultConfig: BridgeConfig;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export const MessageSchema = z.object({
|
|
3
|
+
role: z.enum(['system', 'user', 'assistant', 'tool', 'function']),
|
|
4
|
+
content: z.string().nullable().optional(),
|
|
5
|
+
name: z.string().optional(),
|
|
6
|
+
tool_call_id: z.string().optional(),
|
|
7
|
+
tool_calls: z
|
|
8
|
+
.array(z.object({
|
|
9
|
+
id: z.string(),
|
|
10
|
+
type: z.literal('function'),
|
|
11
|
+
function: z.object({
|
|
12
|
+
name: z.string(),
|
|
13
|
+
arguments: z.string(),
|
|
14
|
+
}),
|
|
15
|
+
}))
|
|
16
|
+
.optional(),
|
|
17
|
+
});
|
|
18
|
+
export const ToolDefinitionSchema = z.object({
|
|
19
|
+
type: z.literal('function'),
|
|
20
|
+
function: z.object({
|
|
21
|
+
name: z.string(),
|
|
22
|
+
description: z.string().optional(),
|
|
23
|
+
parameters: z.record(z.unknown()),
|
|
24
|
+
}),
|
|
25
|
+
});
|
|
26
|
+
export const ModelInfoSchema = z.object({
|
|
27
|
+
id: z.string(),
|
|
28
|
+
name: z.string(),
|
|
29
|
+
capabilities: z
|
|
30
|
+
.object({
|
|
31
|
+
streaming: z.boolean().optional(),
|
|
32
|
+
tools: z.boolean().optional(),
|
|
33
|
+
vision: z.boolean().optional(),
|
|
34
|
+
})
|
|
35
|
+
.optional(),
|
|
36
|
+
});
|
|
37
|
+
export const DefaultConfig = {
|
|
38
|
+
defaultPlugin: 'cursor',
|
|
39
|
+
port: 3849,
|
|
40
|
+
host: '127.0.0.1',
|
|
41
|
+
plugins: {},
|
|
42
|
+
sessionTTL: 1800,
|
|
43
|
+
toolMode: 'lenient',
|
|
44
|
+
};
|
package/dist/index.js
CHANGED
|
@@ -3,8 +3,10 @@ import { initCommand } from './commands/init.js';
|
|
|
3
3
|
import { startCommand } from './commands/start.js';
|
|
4
4
|
import { configureOpencodeCommand } from './commands/configure.js';
|
|
5
5
|
import { doctorCommand } from './commands/doctor.js';
|
|
6
|
-
import { daemonStatusCommand, daemonDownloadCommand, daemonLocateCommand, } from './commands/daemon.js';
|
|
6
|
+
import { daemonStatusCommand, daemonDownloadCommand, daemonLocateCommand, daemonReloadCommand, } from './commands/daemon.js';
|
|
7
7
|
import { installDaemonCommand, uninstallDaemonCommand } from './commands/daemon.js';
|
|
8
|
+
import { loginCommand } from './commands/login.js';
|
|
9
|
+
import { logoutCommand } from './commands/logout.js';
|
|
8
10
|
const command = process.argv[2] ?? 'help';
|
|
9
11
|
async function main() {
|
|
10
12
|
switch (command) {
|
|
@@ -38,11 +40,24 @@ async function main() {
|
|
|
38
40
|
case 'locate':
|
|
39
41
|
await daemonLocateCommand();
|
|
40
42
|
break;
|
|
43
|
+
case 'reload':
|
|
44
|
+
await daemonReloadCommand();
|
|
45
|
+
break;
|
|
41
46
|
default:
|
|
42
|
-
console.log('Usage: llm-bridge daemon [status|download|locate]');
|
|
47
|
+
console.log('Usage: llm-bridge daemon [status|download|locate|reload]');
|
|
43
48
|
}
|
|
44
49
|
break;
|
|
45
50
|
}
|
|
51
|
+
case 'login': {
|
|
52
|
+
const provider = process.argv[3];
|
|
53
|
+
await loginCommand(provider);
|
|
54
|
+
break;
|
|
55
|
+
}
|
|
56
|
+
case 'logout': {
|
|
57
|
+
const provider = process.argv[3];
|
|
58
|
+
await logoutCommand(provider);
|
|
59
|
+
break;
|
|
60
|
+
}
|
|
46
61
|
case 'help':
|
|
47
62
|
default:
|
|
48
63
|
console.log(`llm-bridge v1.0.0
|
|
@@ -50,11 +65,13 @@ async function main() {
|
|
|
50
65
|
Usage:
|
|
51
66
|
llm-bridge init Interactive setup wizard (configure one or more providers)
|
|
52
67
|
llm-bridge start Launch bridge server (all configured plugins registered)
|
|
68
|
+
llm-bridge login [provider] OAuth login (copilot, cursor)
|
|
69
|
+
llm-bridge logout [provider] Remove stored OAuth token
|
|
53
70
|
llm-bridge configure Inject OpenCode config for the default provider
|
|
54
71
|
llm-bridge doctor Run diagnostics
|
|
55
|
-
llm-bridge install-daemon Install
|
|
56
|
-
llm-bridge uninstall-daemon Remove
|
|
57
|
-
llm-bridge daemon [status|download|locate] Manage Windsurf daemon binary
|
|
72
|
+
llm-bridge install-daemon Install platform daemon (LaunchAgent or systemd)
|
|
73
|
+
llm-bridge uninstall-daemon Remove platform daemon
|
|
74
|
+
llm-bridge daemon [status|download|locate|reload] Manage Windsurf daemon binary
|
|
58
75
|
llm-bridge help Show this help`);
|
|
59
76
|
}
|
|
60
77
|
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { OAuthConfig, StoredToken, DeviceCodeResponse } from './types.js';
|
|
2
|
+
export declare class DeviceFlow {
|
|
3
|
+
private config;
|
|
4
|
+
private deviceCode;
|
|
5
|
+
private interval;
|
|
6
|
+
private expiresAt;
|
|
7
|
+
private firstPoll;
|
|
8
|
+
constructor(config: OAuthConfig);
|
|
9
|
+
start(): Promise<DeviceCodeResponse>;
|
|
10
|
+
poll(): Promise<StoredToken>;
|
|
11
|
+
private sleep;
|
|
12
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
const SLOW_DOWN_BACKOFF_MS = 5000;
|
|
2
|
+
const DEFAULT_POLL_INTERVAL_S = 5;
|
|
3
|
+
const DEFAULT_TOKEN_EXPIRY_S = 3600;
|
|
4
|
+
export class DeviceFlow {
|
|
5
|
+
config;
|
|
6
|
+
deviceCode = '';
|
|
7
|
+
interval = DEFAULT_POLL_INTERVAL_S * 1000;
|
|
8
|
+
expiresAt = 0;
|
|
9
|
+
firstPoll = true;
|
|
10
|
+
constructor(config) {
|
|
11
|
+
this.config = config;
|
|
12
|
+
}
|
|
13
|
+
async start() {
|
|
14
|
+
const params = new URLSearchParams({
|
|
15
|
+
client_id: this.config.provider.clientId,
|
|
16
|
+
scope: this.config.provider.scopes.join(' '),
|
|
17
|
+
});
|
|
18
|
+
const response = await fetch(this.config.provider.authUrl, {
|
|
19
|
+
method: 'POST',
|
|
20
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded', Accept: 'application/json' },
|
|
21
|
+
body: params.toString(),
|
|
22
|
+
});
|
|
23
|
+
if (!response.ok) {
|
|
24
|
+
const body = await response.text();
|
|
25
|
+
throw new Error(`Device code request failed: ${response.status} ${body}`);
|
|
26
|
+
}
|
|
27
|
+
const data = (await response.json());
|
|
28
|
+
if (!data.device_code || !data.user_code || !data.verification_uri || !data.expires_in) {
|
|
29
|
+
throw new Error('Invalid device code response: missing required fields');
|
|
30
|
+
}
|
|
31
|
+
this.deviceCode = data.device_code;
|
|
32
|
+
this.interval = (data.interval ?? DEFAULT_POLL_INTERVAL_S) * 1000;
|
|
33
|
+
this.expiresAt = Date.now() + data.expires_in * 1000;
|
|
34
|
+
this.firstPoll = true;
|
|
35
|
+
return {
|
|
36
|
+
deviceCode: this.deviceCode,
|
|
37
|
+
userCode: data.user_code,
|
|
38
|
+
verificationUri: data.verification_uri,
|
|
39
|
+
expiresIn: data.expires_in,
|
|
40
|
+
interval: data.interval ?? DEFAULT_POLL_INTERVAL_S,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
async poll() {
|
|
44
|
+
if (!this.deviceCode) {
|
|
45
|
+
throw new Error('No device code. Call start() first.');
|
|
46
|
+
}
|
|
47
|
+
while (Date.now() < this.expiresAt) {
|
|
48
|
+
if (!this.firstPoll) {
|
|
49
|
+
await this.sleep(this.interval);
|
|
50
|
+
}
|
|
51
|
+
this.firstPoll = false;
|
|
52
|
+
const params = new URLSearchParams({
|
|
53
|
+
client_id: this.config.provider.clientId,
|
|
54
|
+
device_code: this.deviceCode,
|
|
55
|
+
grant_type: 'urn:ietf:params:oauth:grant-type:device_code',
|
|
56
|
+
});
|
|
57
|
+
const response = await fetch(this.config.provider.tokenUrl, {
|
|
58
|
+
method: 'POST',
|
|
59
|
+
headers: {
|
|
60
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
61
|
+
Accept: 'application/json',
|
|
62
|
+
},
|
|
63
|
+
body: params.toString(),
|
|
64
|
+
});
|
|
65
|
+
const data = (await response.json());
|
|
66
|
+
if (response.ok) {
|
|
67
|
+
if (!data.access_token) {
|
|
68
|
+
throw new Error('Invalid token response: missing access_token');
|
|
69
|
+
}
|
|
70
|
+
const token = {
|
|
71
|
+
version: 1,
|
|
72
|
+
accessToken: data.access_token,
|
|
73
|
+
refreshToken: data.refresh_token,
|
|
74
|
+
expiresAt: Date.now() + (data.expires_in ?? DEFAULT_TOKEN_EXPIRY_S) * 1000,
|
|
75
|
+
scopes: data.scope?.split(' ') ?? this.config.provider.scopes,
|
|
76
|
+
};
|
|
77
|
+
await this.config.store.set(this.config.provider.id, token);
|
|
78
|
+
return token;
|
|
79
|
+
}
|
|
80
|
+
const error = data.error;
|
|
81
|
+
if (error === 'slow_down') {
|
|
82
|
+
this.interval += (data.interval ?? 0) * 1000 + SLOW_DOWN_BACKOFF_MS;
|
|
83
|
+
}
|
|
84
|
+
else if (error !== 'authorization_pending') {
|
|
85
|
+
throw new Error(`Device flow error: ${error}`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
throw new Error('Device flow expired');
|
|
89
|
+
}
|
|
90
|
+
sleep(ms) {
|
|
91
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
92
|
+
}
|
|
93
|
+
}
|