@byoky/bridge 0.2.0 → 0.3.0

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.
@@ -8,7 +8,12 @@ if (command === 'install' || command === 'uninstall' || command === 'status') {
8
8
 
9
9
  console.log('Byoky Bridge\n');
10
10
 
11
- if (command === 'install') install();
11
+ if (command === 'install') {
12
+ // --extension-id <id> for custom/unpacked extension IDs
13
+ const idIdx = process.argv.indexOf('--extension-id');
14
+ const extensionId = idIdx !== -1 ? process.argv[idIdx + 1] : undefined;
15
+ install(extensionId);
16
+ }
12
17
  else if (command === 'uninstall') uninstall();
13
18
  else if (command === 'status') status();
14
19
  } else if (!command || command === 'host') {
package/dist/host.js CHANGED
@@ -1,231 +1,144 @@
1
- // src/claude-runner.ts
2
- import { spawn } from "child_process";
3
- function runClaude(request, setupToken) {
4
- const prompt = formatMessagesAsPrompt(request);
5
- const args = ["-p", "--output-format", "stream-json"];
6
- if (request.max_tokens) {
7
- args.push("--max-tokens", String(request.max_tokens));
8
- }
9
- if (request.model) {
10
- args.push("--model", request.model);
11
- }
12
- const proc = spawn("claude", args, {
13
- env: {
14
- ...process.env,
15
- ANTHROPIC_AUTH_TOKEN: setupToken
16
- },
17
- stdio: ["pipe", "pipe", "pipe"]
18
- });
19
- proc.stdin.write(prompt);
20
- proc.stdin.end();
21
- const output = streamEvents(proc);
22
- return { process: proc, output };
23
- }
24
- function formatMessagesAsPrompt(request) {
25
- const parts = [];
26
- if (request.system) {
27
- parts.push(`[System: ${request.system}]`);
28
- }
29
- for (const msg of request.messages) {
30
- if (msg.role === "user") {
31
- parts.push(msg.content);
32
- } else if (msg.role === "assistant") {
33
- parts.push(`[Previous assistant response: ${msg.content}]`);
1
+ // src/proxy-server.ts
2
+ import { createServer } from "http";
3
+ var MAX_PENDING_REQUESTS = 100;
4
+ var pendingRequests = /* @__PURE__ */ new Map();
5
+ function handleProxyResponse(msg) {
6
+ if (msg.type === "proxy_http_response") {
7
+ const pending = pendingRequests.get(msg.requestId);
8
+ if (pending) {
9
+ pending.resolve(msg);
10
+ pendingRequests.delete(msg.requestId);
11
+ }
12
+ } else if (msg.type === "proxy_http_error") {
13
+ const pending = pendingRequests.get(msg.requestId);
14
+ if (pending) {
15
+ pending.reject(new Error(msg.error));
16
+ pendingRequests.delete(msg.requestId);
34
17
  }
35
18
  }
36
- return parts.join("\n\n");
37
19
  }
38
- async function* streamEvents(proc) {
39
- if (!proc.stdout) return;
40
- let buffer = "";
41
- for await (const chunk of proc.stdout) {
42
- buffer += chunk.toString();
43
- const lines = buffer.split("\n");
44
- buffer = lines.pop() ?? "";
45
- for (const line of lines) {
46
- const trimmed = line.trim();
47
- if (!trimmed) continue;
48
- try {
49
- yield JSON.parse(trimmed);
50
- } catch {
51
- }
20
+ function startProxyServer(config) {
21
+ const { port, sessionKey, providers, sendToExtension } = config;
22
+ const server = createServer(async (req, res) => {
23
+ const host = req.headers.host || "";
24
+ const hostWithoutPort = host.split(":")[0];
25
+ if (hostWithoutPort !== "127.0.0.1" && hostWithoutPort !== "localhost") {
26
+ res.writeHead(403, { "Content-Type": "application/json" });
27
+ res.end(JSON.stringify({ error: "Forbidden: invalid Host header" }));
28
+ return;
52
29
  }
53
- }
54
- if (buffer.trim()) {
55
- try {
56
- yield JSON.parse(buffer.trim());
57
- } catch {
30
+ if (req.method === "OPTIONS") {
31
+ res.writeHead(204);
32
+ res.end();
33
+ return;
58
34
  }
59
- }
60
- }
61
-
62
- // src/translator.ts
63
- async function translateRequest(request, setupToken) {
64
- const messages = request.messages.map((m) => ({
65
- role: m.role,
66
- content: typeof m.content === "string" ? m.content : m.content.filter((c) => c.type === "text").map((c) => c.text).join("\n")
67
- }));
68
- let system;
69
- if (typeof request.system === "string") {
70
- system = request.system;
71
- } else if (Array.isArray(request.system)) {
72
- system = request.system.map((s) => s.text).join("\n");
73
- }
74
- const claudeRequest = {
75
- model: request.model,
76
- messages,
77
- max_tokens: request.max_tokens,
78
- system
79
- };
80
- const { process: proc, output } = runClaude(claudeRequest, setupToken);
81
- let fullText = "";
82
- let inputTokens = 0;
83
- let outputTokens = 0;
84
- const streamChunks = [];
85
- try {
86
- for await (const event of output) {
87
- if (event.type === "assistant" && event.content) {
88
- fullText += event.content;
89
- if (request.stream) {
90
- streamChunks.push(
91
- buildStreamChunk(event.content, request.model)
92
- );
93
- }
94
- }
95
- if (event.type === "result") {
96
- if (event.result && !fullText) {
97
- fullText = event.result;
98
- }
99
- if (event.tokens) {
100
- inputTokens = event.tokens.input;
101
- outputTokens = event.tokens.output;
102
- }
103
- }
35
+ if (req.url === "/health") {
36
+ res.writeHead(200, { "Content-Type": "application/json" });
37
+ res.end(JSON.stringify({ status: "ok", providers }));
38
+ return;
104
39
  }
105
- } catch {
106
- }
107
- await new Promise((resolve) => {
108
- proc.on("close", () => resolve());
109
- if (proc.exitCode !== null) resolve();
110
- });
111
- if (!fullText && proc.exitCode !== 0) {
112
- return {
113
- status: 500,
114
- headers: { "content-type": "application/json" },
115
- body: JSON.stringify({
116
- type: "error",
117
- error: {
118
- type: "api_error",
119
- message: "Claude Code process failed. Is Claude Code installed and is the setup token valid?"
120
- }
121
- }),
122
- isStream: false
40
+ const match = req.url?.match(/^\/([^/]+)(\/.*)?$/);
41
+ if (!match) {
42
+ res.writeHead(404, { "Content-Type": "application/json" });
43
+ res.end(JSON.stringify({ error: "Unknown route. Use /<providerId>/..." }));
44
+ return;
45
+ }
46
+ const providerId = match[1];
47
+ const path = match[2] || "/";
48
+ if (!providers.includes(providerId)) {
49
+ res.writeHead(404, { "Content-Type": "application/json" });
50
+ res.end(JSON.stringify({ error: `Provider "${providerId}" not available in this session` }));
51
+ return;
52
+ }
53
+ const body = await readBody(req);
54
+ const providerUrls = {
55
+ anthropic: "https://api.anthropic.com",
56
+ openai: "https://api.openai.com",
57
+ gemini: "https://generativelanguage.googleapis.com",
58
+ mistral: "https://api.mistral.ai",
59
+ cohere: "https://api.cohere.com",
60
+ xai: "https://api.x.ai",
61
+ deepseek: "https://api.deepseek.com",
62
+ perplexity: "https://api.perplexity.ai",
63
+ groq: "https://api.groq.com",
64
+ together: "https://api.together.xyz",
65
+ fireworks: "https://api.fireworks.ai",
66
+ replicate: "https://api.replicate.com",
67
+ openrouter: "https://openrouter.ai/api",
68
+ huggingface: "https://api-inference.huggingface.co",
69
+ azure_openai: "https://YOUR_RESOURCE.openai.azure.com"
123
70
  };
124
- }
125
- const responseId = `msg_byoky_${Date.now().toString(36)}`;
126
- if (request.stream) {
127
- streamChunks.push(
128
- buildStreamMessageStart(responseId, request.model)
129
- );
130
- streamChunks.push(
131
- buildStreamContentDelta(fullText)
132
- );
133
- streamChunks.push(
134
- buildStreamMessageDelta(inputTokens, outputTokens)
135
- );
136
- streamChunks.push('event: message_stop\ndata: {"type":"message_stop"}\n\n');
137
- return {
138
- status: 200,
139
- headers: {
140
- "content-type": "text/event-stream",
141
- "cache-control": "no-cache"
142
- },
143
- body: streamChunks.join(""),
144
- isStream: true,
145
- streamChunks
71
+ const baseUrl = providerUrls[providerId];
72
+ if (!baseUrl) {
73
+ res.writeHead(400, { "Content-Type": "application/json" });
74
+ res.end(JSON.stringify({ error: `Unknown provider base URL for "${providerId}"` }));
75
+ return;
76
+ }
77
+ const realUrl = `${baseUrl}${path}`;
78
+ const requestId = `proxy-${crypto.randomUUID()}`;
79
+ const headers = {};
80
+ for (const [key, value] of Object.entries(req.headers)) {
81
+ if (key === "host" || key === "connection") continue;
82
+ if (typeof value === "string") headers[key] = value;
83
+ }
84
+ const proxyMsg = {
85
+ type: "proxy_http",
86
+ requestId,
87
+ sessionKey,
88
+ providerId,
89
+ url: realUrl,
90
+ method: req.method || "GET",
91
+ headers,
92
+ body: body || void 0
146
93
  };
147
- }
148
- const response = {
149
- id: responseId,
150
- type: "message",
151
- role: "assistant",
152
- content: [{ type: "text", text: fullText }],
153
- model: request.model,
154
- stop_reason: "end_turn",
155
- usage: { input_tokens: inputTokens, output_tokens: outputTokens }
156
- };
157
- return {
158
- status: 200,
159
- headers: { "content-type": "application/json" },
160
- body: JSON.stringify(response),
161
- isStream: false
162
- };
163
- }
164
- function buildStreamChunk(text, _model) {
165
- const data = {
166
- type: "content_block_delta",
167
- index: 0,
168
- delta: { type: "text_delta", text }
169
- };
170
- return `event: content_block_delta
171
- data: ${JSON.stringify(data)}
172
-
173
- `;
174
- }
175
- function buildStreamMessageStart(id, model) {
176
- const data = {
177
- type: "message_start",
178
- message: {
179
- id,
180
- type: "message",
181
- role: "assistant",
182
- content: [],
183
- model,
184
- stop_reason: null,
185
- usage: { input_tokens: 0, output_tokens: 0 }
94
+ if (pendingRequests.size >= MAX_PENDING_REQUESTS) {
95
+ res.writeHead(503, { "Content-Type": "application/json" });
96
+ res.end(JSON.stringify({ error: "Too many concurrent requests" }));
97
+ return;
186
98
  }
187
- };
188
- return `event: message_start
189
- data: ${JSON.stringify(data)}
190
-
191
- `;
192
- }
193
- function buildStreamContentDelta(text) {
194
- const start = {
195
- type: "content_block_start",
196
- index: 0,
197
- content_block: { type: "text", text: "" }
198
- };
199
- const delta = {
200
- type: "content_block_delta",
201
- index: 0,
202
- delta: { type: "text_delta", text }
203
- };
204
- const stop = {
205
- type: "content_block_stop",
206
- index: 0
207
- };
208
- return `event: content_block_start
209
- data: ${JSON.stringify(start)}
210
-
211
- event: content_block_delta
212
- data: ${JSON.stringify(delta)}
213
-
214
- event: content_block_stop
215
- data: ${JSON.stringify(stop)}
216
-
217
- `;
99
+ try {
100
+ const response = await new Promise((resolve, reject) => {
101
+ pendingRequests.set(requestId, { resolve, reject });
102
+ setTimeout(() => {
103
+ if (pendingRequests.has(requestId)) {
104
+ pendingRequests.delete(requestId);
105
+ reject(new Error("Request timed out"));
106
+ }
107
+ }, 12e4);
108
+ sendToExtension(proxyMsg);
109
+ });
110
+ const responseHeaders = { ...response.headers };
111
+ delete responseHeaders["transfer-encoding"];
112
+ res.writeHead(response.status, responseHeaders);
113
+ res.end(response.body);
114
+ } catch (err) {
115
+ res.writeHead(502, { "Content-Type": "application/json" });
116
+ res.end(JSON.stringify({ error: err.message }));
117
+ }
118
+ });
119
+ server.listen(port, "127.0.0.1", () => {
120
+ process.stderr.write(`Byoky proxy listening on http://127.0.0.1:${port}
121
+ `);
122
+ });
123
+ return server;
218
124
  }
219
- function buildStreamMessageDelta(inputTokens, outputTokens) {
220
- const data = {
221
- type: "message_delta",
222
- delta: { stop_reason: "end_turn" },
223
- usage: { input_tokens: inputTokens, output_tokens: outputTokens }
224
- };
225
- return `event: message_delta
226
- data: ${JSON.stringify(data)}
227
-
228
- `;
125
+ var MAX_BODY_SIZE = 10 * 1024 * 1024;
126
+ function readBody(req) {
127
+ return new Promise((resolve, reject) => {
128
+ let body = "";
129
+ let size = 0;
130
+ req.on("data", (chunk) => {
131
+ size += chunk.length;
132
+ if (size > MAX_BODY_SIZE) {
133
+ req.destroy();
134
+ reject(new Error("Request body too large"));
135
+ return;
136
+ }
137
+ body += chunk.toString();
138
+ });
139
+ req.on("end", () => resolve(body));
140
+ req.on("error", reject);
141
+ });
229
142
  }
230
143
 
231
144
  // src/host.ts
@@ -250,9 +163,13 @@ function readMessage() {
250
163
  resolve(null);
251
164
  return;
252
165
  }
253
- readBody(msgLength);
166
+ if (msgLength > 1048576) {
167
+ reject(new Error(`Message too large: ${msgLength} bytes (max 1MB)`));
168
+ return;
169
+ }
170
+ readBody2(msgLength);
254
171
  }
255
- function readBody(length) {
172
+ function readBody2(length) {
256
173
  let body = Buffer.alloc(0);
257
174
  function readChunk() {
258
175
  const chunk = process.stdin.read(length - body.length);
@@ -294,20 +211,37 @@ async function handleMessage(msg) {
294
211
  }
295
212
  if (message.type === "proxy") {
296
213
  const req = msg;
297
- await handleProxy(req);
214
+ await handleSetupTokenProxy(req);
215
+ return;
216
+ }
217
+ if (message.type === "start-proxy") {
218
+ const req = msg;
219
+ handleStartProxy(req);
220
+ return;
221
+ }
222
+ if (message.type === "proxy_http_response" || message.type === "proxy_http_error") {
223
+ handleProxyResponse(msg);
298
224
  return;
299
225
  }
300
226
  }
301
- async function handleProxy(req) {
227
+ async function handleSetupTokenProxy(req) {
302
228
  try {
303
- const apiRequest = JSON.parse(req.body);
304
- const result = await translateRequest(apiRequest, req.setupToken);
229
+ const res = await fetch(req.url, {
230
+ method: req.method,
231
+ headers: req.headers,
232
+ body: req.body || void 0
233
+ });
234
+ const body = await res.text();
235
+ const headers = {};
236
+ res.headers.forEach((v, k) => {
237
+ headers[k] = v;
238
+ });
305
239
  writeMessage({
306
240
  type: "proxy_response",
307
241
  requestId: req.requestId,
308
- status: result.status,
309
- headers: result.headers,
310
- body: result.body
242
+ status: res.status,
243
+ headers,
244
+ body
311
245
  });
312
246
  } catch (e) {
313
247
  writeMessage({
@@ -317,6 +251,26 @@ async function handleProxy(req) {
317
251
  });
318
252
  }
319
253
  }
254
+ function handleStartProxy(req) {
255
+ try {
256
+ startProxyServer({
257
+ port: req.port,
258
+ sessionKey: req.sessionKey,
259
+ providers: req.providers,
260
+ sendToExtension: (msg) => writeMessage(msg)
261
+ });
262
+ writeMessage({
263
+ type: "proxy-started",
264
+ port: req.port
265
+ });
266
+ } catch (e) {
267
+ writeMessage({
268
+ type: "proxy_error",
269
+ requestId: "start-proxy",
270
+ error: e.message
271
+ });
272
+ }
273
+ }
320
274
  async function main() {
321
275
  process.stdin.resume();
322
276
  while (true) {
@@ -6,7 +6,7 @@
6
6
  * byoky-bridge uninstall — remove registrations
7
7
  * byoky-bridge status — check if registered
8
8
  */
9
- declare function install(): void;
9
+ declare function install(extensionId?: string): void;
10
10
  declare function uninstall(): void;
11
11
  declare function status(): void;
12
12
 
package/dist/installer.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // src/installer.ts
2
- import { writeFileSync, mkdirSync, unlinkSync, existsSync } from "fs";
2
+ import { writeFileSync, mkdirSync, unlinkSync, existsSync, chmodSync } from "fs";
3
3
  import { dirname, resolve } from "path";
4
4
  import { homedir, platform } from "os";
5
5
  import { execSync } from "child_process";
@@ -11,7 +11,20 @@ function getHostPath() {
11
11
  return resolve(dirname(new URL(import.meta.url).pathname), "../bin/byoky-bridge.js");
12
12
  }
13
13
  }
14
- function buildManifest(hostPath, browserType) {
14
+ function createNativeWrapper(hostPath, manifestDir) {
15
+ const nodePath = process.execPath;
16
+ const wrapperPath = resolve(manifestDir, "byoky-bridge-host");
17
+ const userPath = process.env.PATH || "";
18
+ const script = `#!/bin/bash
19
+ export PATH="${userPath}"
20
+ exec "${nodePath}" "${hostPath}" host "$@"
21
+ `;
22
+ writeFileSync(wrapperPath, script);
23
+ chmodSync(wrapperPath, 493);
24
+ return wrapperPath;
25
+ }
26
+ var DEFAULT_EXTENSION_ID = "ahhecmfcclkjdgjnmackoacldnmgmipl";
27
+ function buildManifest(hostPath, browserType, extensionId) {
15
28
  const base = {
16
29
  name: HOST_NAME,
17
30
  description: "Byoky Bridge \u2014 routes setup token requests through Claude Code CLI",
@@ -19,11 +32,11 @@ function buildManifest(hostPath, browserType) {
19
32
  type: "stdio"
20
33
  };
21
34
  if (browserType === "chrome") {
35
+ const id = extensionId || DEFAULT_EXTENSION_ID;
22
36
  return {
23
37
  ...base,
24
38
  allowed_origins: [
25
- // Chrome uses extension IDs — we allow all since the ID varies per install
26
- "chrome-extension://*/"
39
+ `chrome-extension://${id}/`
27
40
  ]
28
41
  };
29
42
  }
@@ -95,7 +108,7 @@ function getManifestLocations() {
95
108
  }
96
109
  return [];
97
110
  }
98
- function install() {
111
+ function install(extensionId) {
99
112
  const hostPath = getHostPath();
100
113
  const locations = getManifestLocations();
101
114
  if (locations.length === 0) {
@@ -105,8 +118,10 @@ function install() {
105
118
  let installed = 0;
106
119
  for (const loc of locations) {
107
120
  try {
108
- const manifest = buildManifest(hostPath, loc.type);
109
- mkdirSync(dirname(loc.path), { recursive: true });
121
+ const manifestDir = dirname(loc.path);
122
+ mkdirSync(manifestDir, { recursive: true });
123
+ const wrapperPath = createNativeWrapper(hostPath, manifestDir);
124
+ const manifest = buildManifest(wrapperPath, loc.type, extensionId);
110
125
  writeFileSync(loc.path, JSON.stringify(manifest, null, 2));
111
126
  console.log(` Registered with ${loc.browser}`);
112
127
  installed++;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@byoky/bridge",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Native messaging bridge for Byoky — routes setup token requests through Claude Code CLI",
5
5
  "type": "module",
6
6
  "main": "dist/host.js",