@byoky/bridge 0.2.0 → 0.4.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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 byoky contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -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,145 @@
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
+ const pending = pendingRequests.get(msg.requestId);
7
+ if (!pending) return;
8
+ if (msg.type === "proxy_http_response_meta") {
9
+ const headers = { ...msg.headers };
10
+ delete headers["transfer-encoding"];
11
+ delete headers["content-encoding"];
12
+ delete headers["content-length"];
13
+ pending.res.writeHead(msg.status, headers);
14
+ } else if (msg.type === "proxy_http_response_chunk") {
15
+ pending.res.write(msg.chunk);
16
+ } else if (msg.type === "proxy_http_response_done") {
17
+ pending.res.end();
18
+ clearTimeout(pending.timeout);
19
+ pendingRequests.delete(msg.requestId);
20
+ } else if (msg.type === "proxy_http_error") {
21
+ if (!pending.res.headersSent) {
22
+ pending.res.writeHead(502, { "Content-Type": "application/json" });
34
23
  }
24
+ pending.res.end(JSON.stringify({ error: msg.error }));
25
+ clearTimeout(pending.timeout);
26
+ pendingRequests.delete(msg.requestId);
35
27
  }
36
- return parts.join("\n\n");
37
28
  }
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
- }
29
+ function startProxyServer(config) {
30
+ const { port, sessionKey, providers, sendToExtension } = config;
31
+ const server = createServer(async (req, res) => {
32
+ const host = req.headers.host || "";
33
+ const hostWithoutPort = host.split(":")[0];
34
+ if (hostWithoutPort !== "127.0.0.1" && hostWithoutPort !== "localhost") {
35
+ res.writeHead(403, { "Content-Type": "application/json" });
36
+ res.end(JSON.stringify({ error: "Forbidden: invalid Host header" }));
37
+ return;
52
38
  }
53
- }
54
- if (buffer.trim()) {
55
- try {
56
- yield JSON.parse(buffer.trim());
57
- } catch {
39
+ if (req.method === "OPTIONS") {
40
+ res.writeHead(204);
41
+ res.end();
42
+ return;
58
43
  }
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
- }
44
+ if (req.url === "/health") {
45
+ res.writeHead(200, { "Content-Type": "application/json" });
46
+ res.end(JSON.stringify({ status: "ok", providers }));
47
+ return;
104
48
  }
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
49
+ const match = req.url?.match(/^\/([^/]+)(\/.*)?$/);
50
+ if (!match) {
51
+ res.writeHead(404, { "Content-Type": "application/json" });
52
+ res.end(JSON.stringify({ error: "Unknown route. Use /<providerId>/..." }));
53
+ return;
54
+ }
55
+ const providerId = match[1];
56
+ const path = match[2] || "/";
57
+ if (!providers.includes(providerId)) {
58
+ res.writeHead(404, { "Content-Type": "application/json" });
59
+ res.end(JSON.stringify({ error: `Provider "${providerId}" not available in this session` }));
60
+ return;
61
+ }
62
+ const body = await readBody(req);
63
+ const providerUrls = {
64
+ anthropic: "https://api.anthropic.com",
65
+ openai: "https://api.openai.com",
66
+ gemini: "https://generativelanguage.googleapis.com",
67
+ mistral: "https://api.mistral.ai",
68
+ cohere: "https://api.cohere.com",
69
+ xai: "https://api.x.ai",
70
+ deepseek: "https://api.deepseek.com",
71
+ perplexity: "https://api.perplexity.ai",
72
+ groq: "https://api.groq.com",
73
+ together: "https://api.together.xyz",
74
+ fireworks: "https://api.fireworks.ai",
75
+ replicate: "https://api.replicate.com",
76
+ openrouter: "https://openrouter.ai/api",
77
+ huggingface: "https://api-inference.huggingface.co",
78
+ azure_openai: "https://YOUR_RESOURCE.openai.azure.com"
123
79
  };
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
80
+ const baseUrl = providerUrls[providerId];
81
+ if (!baseUrl) {
82
+ res.writeHead(400, { "Content-Type": "application/json" });
83
+ res.end(JSON.stringify({ error: `Unknown provider base URL for "${providerId}"` }));
84
+ return;
85
+ }
86
+ const realUrl = `${baseUrl}${path}`;
87
+ const requestId = `proxy-${crypto.randomUUID()}`;
88
+ const headers = {};
89
+ for (const [key, value] of Object.entries(req.headers)) {
90
+ if (key === "host" || key === "connection") continue;
91
+ if (typeof value === "string") headers[key] = value;
92
+ }
93
+ const proxyMsg = {
94
+ type: "proxy_http",
95
+ requestId,
96
+ sessionKey,
97
+ providerId,
98
+ url: realUrl,
99
+ method: req.method || "GET",
100
+ headers,
101
+ body: body || void 0
146
102
  };
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 }
103
+ if (pendingRequests.size >= MAX_PENDING_REQUESTS) {
104
+ res.writeHead(503, { "Content-Type": "application/json" });
105
+ res.end(JSON.stringify({ error: "Too many concurrent requests" }));
106
+ return;
186
107
  }
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
- `;
108
+ const timeout = setTimeout(() => {
109
+ if (pendingRequests.has(requestId)) {
110
+ pendingRequests.delete(requestId);
111
+ if (!res.headersSent) {
112
+ res.writeHead(504, { "Content-Type": "application/json" });
113
+ }
114
+ res.end(JSON.stringify({ error: "Request timed out" }));
115
+ }
116
+ }, 12e4);
117
+ pendingRequests.set(requestId, { res, timeout });
118
+ sendToExtension(proxyMsg);
119
+ });
120
+ server.listen(port, "127.0.0.1", () => {
121
+ process.stderr.write(`Byoky proxy listening on http://127.0.0.1:${port}
122
+ `);
123
+ });
124
+ return server;
218
125
  }
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
- `;
126
+ var MAX_BODY_SIZE = 10 * 1024 * 1024;
127
+ function readBody(req) {
128
+ return new Promise((resolve, reject) => {
129
+ let body = "";
130
+ let size = 0;
131
+ req.on("data", (chunk) => {
132
+ size += chunk.length;
133
+ if (size > MAX_BODY_SIZE) {
134
+ req.destroy();
135
+ reject(new Error("Request body too large"));
136
+ return;
137
+ }
138
+ body += chunk.toString();
139
+ });
140
+ req.on("end", () => resolve(body));
141
+ req.on("error", reject);
142
+ });
229
143
  }
230
144
 
231
145
  // src/host.ts
@@ -250,9 +164,13 @@ function readMessage() {
250
164
  resolve(null);
251
165
  return;
252
166
  }
253
- readBody(msgLength);
167
+ if (msgLength > 1048576) {
168
+ reject(new Error(`Message too large: ${msgLength} bytes (max 1MB)`));
169
+ return;
170
+ }
171
+ readBody2(msgLength);
254
172
  }
255
- function readBody(length) {
173
+ function readBody2(length) {
256
174
  let body = Buffer.alloc(0);
257
175
  function readChunk() {
258
176
  const chunk = process.stdin.read(length - body.length);
@@ -289,30 +207,80 @@ async function handleMessage(msg) {
289
207
  if (!msg || typeof msg !== "object") return;
290
208
  const message = msg;
291
209
  if (message.type === "ping") {
292
- writeMessage({ type: "pong", version: "0.2.0" });
210
+ writeMessage({ type: "pong", version: "0.3.0" });
293
211
  return;
294
212
  }
295
213
  if (message.type === "proxy") {
296
214
  const req = msg;
297
- await handleProxy(req);
215
+ await handleStreamingFetch(req.requestId, req.url, req.method, req.headers, req.body, "bridge");
298
216
  return;
299
217
  }
218
+ if (message.type === "proxy_direct_fetch") {
219
+ const req = msg;
220
+ await handleStreamingFetch(req.requestId, req.url, req.method, req.headers, req.body, "proxy_http");
221
+ return;
222
+ }
223
+ if (message.type === "start-proxy") {
224
+ const req = msg;
225
+ handleStartProxy(req);
226
+ return;
227
+ }
228
+ if (msg && typeof msg === "object" && "type" in msg && (msg.type === "proxy_http_response_meta" || msg.type === "proxy_http_response_chunk" || msg.type === "proxy_http_response_done" || msg.type === "proxy_http_error")) {
229
+ handleProxyResponse(msg);
230
+ return;
231
+ }
232
+ }
233
+ async function handleStreamingFetch(requestId, url, method, headers, body, mode) {
234
+ const send = (msg) => {
235
+ if (mode === "bridge") {
236
+ writeMessage(msg);
237
+ } else {
238
+ handleProxyResponse(msg);
239
+ }
240
+ };
241
+ const prefix = mode === "bridge" ? "proxy_response" : "proxy_http_response";
242
+ const errorType = mode === "bridge" ? "proxy_error" : "proxy_http_error";
243
+ try {
244
+ const res = await fetch(url, {
245
+ method,
246
+ headers,
247
+ body: body || void 0
248
+ });
249
+ const resHeaders = {};
250
+ res.headers.forEach((v, k) => {
251
+ resHeaders[k] = v;
252
+ });
253
+ send({ type: `${prefix}_meta`, requestId, status: res.status, headers: resHeaders });
254
+ if (res.body) {
255
+ const reader = res.body.getReader();
256
+ const decoder = new TextDecoder();
257
+ for (; ; ) {
258
+ const { done, value } = await reader.read();
259
+ if (done) break;
260
+ send({ type: `${prefix}_chunk`, requestId, chunk: decoder.decode(value, { stream: true }) });
261
+ }
262
+ }
263
+ send({ type: `${prefix}_done`, requestId });
264
+ } catch (e) {
265
+ send({ type: errorType, requestId, error: e.message });
266
+ }
300
267
  }
301
- async function handleProxy(req) {
268
+ function handleStartProxy(req) {
302
269
  try {
303
- const apiRequest = JSON.parse(req.body);
304
- const result = await translateRequest(apiRequest, req.setupToken);
270
+ startProxyServer({
271
+ port: req.port,
272
+ sessionKey: req.sessionKey,
273
+ providers: req.providers,
274
+ sendToExtension: (msg) => writeMessage(msg)
275
+ });
305
276
  writeMessage({
306
- type: "proxy_response",
307
- requestId: req.requestId,
308
- status: result.status,
309
- headers: result.headers,
310
- body: result.body
277
+ type: "proxy-started",
278
+ port: req.port
311
279
  });
312
280
  } catch (e) {
313
281
  writeMessage({
314
282
  type: "proxy_error",
315
- requestId: req.requestId,
283
+ requestId: "start-proxy",
316
284
  error: e.message
317
285
  });
318
286
  }
@@ -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.4.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",
@@ -11,10 +11,6 @@
11
11
  "dist",
12
12
  "bin"
13
13
  ],
14
- "scripts": {
15
- "build": "tsup",
16
- "dev": "tsup --watch"
17
- },
18
14
  "keywords": [
19
15
  "byoky",
20
16
  "claude",
@@ -31,5 +27,9 @@
31
27
  "@types/node": "25.5.0",
32
28
  "tsup": "^8.0.0",
33
29
  "typescript": "^5.5.0"
30
+ },
31
+ "scripts": {
32
+ "build": "tsup",
33
+ "dev": "tsup --watch"
34
34
  }
35
- }
35
+ }