@byoky/bridge 0.2.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.
@@ -0,0 +1,26 @@
1
+ #!/usr/bin/env node
2
+
3
+ const command = process.argv[2];
4
+
5
+ if (command === 'install' || command === 'uninstall' || command === 'status') {
6
+ // CLI mode: manage native messaging registration
7
+ const { install, uninstall, status } = await import('../dist/installer.js');
8
+
9
+ console.log('Byoky Bridge\n');
10
+
11
+ if (command === 'install') install();
12
+ else if (command === 'uninstall') uninstall();
13
+ else if (command === 'status') status();
14
+ } else if (!command || command === 'host') {
15
+ // Native messaging host mode (called by browser)
16
+ await import('../dist/host.js');
17
+ } else {
18
+ console.log(`Usage: byoky-bridge <command>
19
+
20
+ Commands:
21
+ install Register native messaging host with browsers
22
+ uninstall Remove native messaging registration
23
+ status Check registration status
24
+
25
+ The bridge runs automatically when called by the Byoky extension.`);
26
+ }
package/dist/host.d.ts ADDED
@@ -0,0 +1,2 @@
1
+
2
+ export { }
package/dist/host.js ADDED
@@ -0,0 +1,333 @@
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}]`);
34
+ }
35
+ }
36
+ return parts.join("\n\n");
37
+ }
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
+ }
52
+ }
53
+ }
54
+ if (buffer.trim()) {
55
+ try {
56
+ yield JSON.parse(buffer.trim());
57
+ } catch {
58
+ }
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
+ }
104
+ }
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
123
+ };
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
146
+ };
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 }
186
+ }
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
+ `;
218
+ }
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
+ `;
229
+ }
230
+
231
+ // src/host.ts
232
+ function readMessage() {
233
+ return new Promise((resolve, reject) => {
234
+ const lengthBuf = Buffer.alloc(4);
235
+ let bytesRead = 0;
236
+ function readLength() {
237
+ const chunk = process.stdin.read(4 - bytesRead);
238
+ if (!chunk) {
239
+ process.stdin.once("readable", readLength);
240
+ return;
241
+ }
242
+ chunk.copy(lengthBuf, bytesRead);
243
+ bytesRead += chunk.length;
244
+ if (bytesRead < 4) {
245
+ process.stdin.once("readable", readLength);
246
+ return;
247
+ }
248
+ const msgLength = lengthBuf.readUInt32LE(0);
249
+ if (msgLength === 0) {
250
+ resolve(null);
251
+ return;
252
+ }
253
+ readBody(msgLength);
254
+ }
255
+ function readBody(length) {
256
+ let body = Buffer.alloc(0);
257
+ function readChunk() {
258
+ const chunk = process.stdin.read(length - body.length);
259
+ if (!chunk) {
260
+ process.stdin.once("readable", readChunk);
261
+ return;
262
+ }
263
+ body = Buffer.concat([body, chunk]);
264
+ if (body.length < length) {
265
+ process.stdin.once("readable", readChunk);
266
+ return;
267
+ }
268
+ try {
269
+ resolve(JSON.parse(body.toString("utf-8")));
270
+ } catch (e) {
271
+ reject(new Error(`Invalid JSON: ${e.message}`));
272
+ }
273
+ }
274
+ readChunk();
275
+ }
276
+ process.stdin.once("readable", readLength);
277
+ process.stdin.on("end", () => resolve(null));
278
+ });
279
+ }
280
+ function writeMessage(msg) {
281
+ const json = JSON.stringify(msg);
282
+ const buf = Buffer.from(json, "utf-8");
283
+ const lengthBuf = Buffer.alloc(4);
284
+ lengthBuf.writeUInt32LE(buf.length, 0);
285
+ process.stdout.write(lengthBuf);
286
+ process.stdout.write(buf);
287
+ }
288
+ async function handleMessage(msg) {
289
+ if (!msg || typeof msg !== "object") return;
290
+ const message = msg;
291
+ if (message.type === "ping") {
292
+ writeMessage({ type: "pong", version: "0.2.0" });
293
+ return;
294
+ }
295
+ if (message.type === "proxy") {
296
+ const req = msg;
297
+ await handleProxy(req);
298
+ return;
299
+ }
300
+ }
301
+ async function handleProxy(req) {
302
+ try {
303
+ const apiRequest = JSON.parse(req.body);
304
+ const result = await translateRequest(apiRequest, req.setupToken);
305
+ writeMessage({
306
+ type: "proxy_response",
307
+ requestId: req.requestId,
308
+ status: result.status,
309
+ headers: result.headers,
310
+ body: result.body
311
+ });
312
+ } catch (e) {
313
+ writeMessage({
314
+ type: "proxy_error",
315
+ requestId: req.requestId,
316
+ error: e.message
317
+ });
318
+ }
319
+ }
320
+ async function main() {
321
+ process.stdin.resume();
322
+ while (true) {
323
+ const msg = await readMessage();
324
+ if (msg === null) break;
325
+ await handleMessage(msg);
326
+ }
327
+ process.exit(0);
328
+ }
329
+ main().catch((e) => {
330
+ process.stderr.write(`byoky-bridge fatal: ${e.message}
331
+ `);
332
+ process.exit(1);
333
+ });
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Installs/uninstalls the native messaging host manifest for each browser.
3
+ *
4
+ * Usage:
5
+ * byoky-bridge install — register with Chrome, Firefox, and Safari
6
+ * byoky-bridge uninstall — remove registrations
7
+ * byoky-bridge status — check if registered
8
+ */
9
+ declare function install(): void;
10
+ declare function uninstall(): void;
11
+ declare function status(): void;
12
+
13
+ export { install, status, uninstall };
@@ -0,0 +1,149 @@
1
+ // src/installer.ts
2
+ import { writeFileSync, mkdirSync, unlinkSync, existsSync } from "fs";
3
+ import { dirname, resolve } from "path";
4
+ import { homedir, platform } from "os";
5
+ import { execSync } from "child_process";
6
+ var HOST_NAME = "com.byoky.bridge";
7
+ function getHostPath() {
8
+ try {
9
+ return execSync("which byoky-bridge", { encoding: "utf-8" }).trim();
10
+ } catch {
11
+ return resolve(dirname(new URL(import.meta.url).pathname), "../bin/byoky-bridge.js");
12
+ }
13
+ }
14
+ function buildManifest(hostPath, browserType) {
15
+ const base = {
16
+ name: HOST_NAME,
17
+ description: "Byoky Bridge \u2014 routes setup token requests through Claude Code CLI",
18
+ path: hostPath,
19
+ type: "stdio"
20
+ };
21
+ if (browserType === "chrome") {
22
+ return {
23
+ ...base,
24
+ allowed_origins: [
25
+ // Chrome uses extension IDs — we allow all since the ID varies per install
26
+ "chrome-extension://*/"
27
+ ]
28
+ };
29
+ }
30
+ return {
31
+ ...base,
32
+ allowed_extensions: ["byoky@byoky.com"]
33
+ };
34
+ }
35
+ function getManifestLocations() {
36
+ const home = homedir();
37
+ const os = platform();
38
+ if (os === "darwin") {
39
+ return [
40
+ {
41
+ browser: "Chrome",
42
+ path: `${home}/Library/Application Support/Google/Chrome/NativeMessagingHosts/${HOST_NAME}.json`,
43
+ type: "chrome"
44
+ },
45
+ {
46
+ browser: "Chromium",
47
+ path: `${home}/Library/Application Support/Chromium/NativeMessagingHosts/${HOST_NAME}.json`,
48
+ type: "chrome"
49
+ },
50
+ {
51
+ browser: "Brave",
52
+ path: `${home}/Library/Application Support/BraveSoftware/Brave-Browser/NativeMessagingHosts/${HOST_NAME}.json`,
53
+ type: "chrome"
54
+ },
55
+ {
56
+ browser: "Firefox",
57
+ path: `${home}/Library/Application Support/Mozilla/NativeMessagingHosts/${HOST_NAME}.json`,
58
+ type: "firefox"
59
+ }
60
+ ];
61
+ }
62
+ if (os === "linux") {
63
+ return [
64
+ {
65
+ browser: "Chrome",
66
+ path: `${home}/.config/google-chrome/NativeMessagingHosts/${HOST_NAME}.json`,
67
+ type: "chrome"
68
+ },
69
+ {
70
+ browser: "Chromium",
71
+ path: `${home}/.config/chromium/NativeMessagingHosts/${HOST_NAME}.json`,
72
+ type: "chrome"
73
+ },
74
+ {
75
+ browser: "Firefox",
76
+ path: `${home}/.mozilla/native-messaging-hosts/${HOST_NAME}.json`,
77
+ type: "firefox"
78
+ }
79
+ ];
80
+ }
81
+ if (os === "win32") {
82
+ const appData = process.env.LOCALAPPDATA || `${home}/AppData/Local`;
83
+ return [
84
+ {
85
+ browser: "Chrome",
86
+ path: `${appData}/Google/Chrome/User Data/NativeMessagingHosts/${HOST_NAME}.json`,
87
+ type: "chrome"
88
+ },
89
+ {
90
+ browser: "Firefox",
91
+ path: `${appData}/Mozilla/NativeMessagingHosts/${HOST_NAME}.json`,
92
+ type: "firefox"
93
+ }
94
+ ];
95
+ }
96
+ return [];
97
+ }
98
+ function install() {
99
+ const hostPath = getHostPath();
100
+ const locations = getManifestLocations();
101
+ if (locations.length === 0) {
102
+ console.error("Unsupported platform");
103
+ process.exit(1);
104
+ }
105
+ let installed = 0;
106
+ for (const loc of locations) {
107
+ try {
108
+ const manifest = buildManifest(hostPath, loc.type);
109
+ mkdirSync(dirname(loc.path), { recursive: true });
110
+ writeFileSync(loc.path, JSON.stringify(manifest, null, 2));
111
+ console.log(` Registered with ${loc.browser}`);
112
+ installed++;
113
+ } catch {
114
+ }
115
+ }
116
+ if (installed > 0) {
117
+ console.log(`
118
+ Byoky Bridge installed for ${installed} browser(s).`);
119
+ console.log("Restart your browser for changes to take effect.");
120
+ } else {
121
+ console.error("No supported browsers found.");
122
+ }
123
+ }
124
+ function uninstall() {
125
+ const locations = getManifestLocations();
126
+ for (const loc of locations) {
127
+ try {
128
+ if (existsSync(loc.path)) {
129
+ unlinkSync(loc.path);
130
+ console.log(` Removed from ${loc.browser}`);
131
+ }
132
+ } catch {
133
+ }
134
+ }
135
+ console.log("\nByoky Bridge uninstalled.");
136
+ }
137
+ function status() {
138
+ const locations = getManifestLocations();
139
+ for (const loc of locations) {
140
+ const exists = existsSync(loc.path);
141
+ const icon = exists ? "\u2713" : "\u2717";
142
+ console.log(` ${icon} ${loc.browser}: ${exists ? "registered" : "not registered"}`);
143
+ }
144
+ }
145
+ export {
146
+ install,
147
+ status,
148
+ uninstall
149
+ };
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "@byoky/bridge",
3
+ "version": "0.2.0",
4
+ "description": "Native messaging bridge for Byoky — routes setup token requests through Claude Code CLI",
5
+ "type": "module",
6
+ "main": "dist/host.js",
7
+ "bin": {
8
+ "byoky-bridge": "./bin/byoky-bridge.js"
9
+ },
10
+ "files": [
11
+ "dist",
12
+ "bin"
13
+ ],
14
+ "scripts": {
15
+ "build": "tsup",
16
+ "dev": "tsup --watch"
17
+ },
18
+ "keywords": [
19
+ "byoky",
20
+ "claude",
21
+ "native-messaging",
22
+ "bridge"
23
+ ],
24
+ "license": "MIT",
25
+ "repository": {
26
+ "type": "git",
27
+ "url": "https://github.com/MichaelLod/byoky.git",
28
+ "directory": "packages/bridge"
29
+ },
30
+ "devDependencies": {
31
+ "@types/node": "25.5.0",
32
+ "tsup": "^8.0.0",
33
+ "typescript": "^5.5.0"
34
+ }
35
+ }