@fangyb/ahchat-bridge 0.1.2 → 0.1.4

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.
Files changed (2) hide show
  1. package/dist/cli.js +255 -16
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -18,15 +18,225 @@ import {
18
18
 
19
19
  // src/cli.ts
20
20
  import cac from "cac";
21
- var logger = createModuleLogger("bridge");
21
+
22
+ // src/protocol.ts
23
+ import { execSync } from "child_process";
24
+ import fs from "fs";
25
+ import os from "os";
26
+ import path from "path";
27
+ import { fileURLToPath } from "url";
28
+ var logger = createModuleLogger("bridge.protocol");
29
+ var __filename = fileURLToPath(import.meta.url);
30
+ var __dirname = path.dirname(__filename);
31
+ function getBridgeExePath() {
32
+ const pkgDir = path.resolve(__dirname, "..");
33
+ return path.join(pkgDir, "dist", "cli.js");
34
+ }
35
+ function registerProtocolHandler() {
36
+ const platform = os.platform();
37
+ if (platform === "win32") {
38
+ registerWindows();
39
+ } else if (platform === "darwin") {
40
+ registerMacOS();
41
+ } else {
42
+ registerLinux();
43
+ }
44
+ logger.info("Protocol handler registered", { platform });
45
+ }
46
+ function registerWindows() {
47
+ const exe = getBridgeExePath();
48
+ const nodeExe = process.execPath;
49
+ const ahchatDir = path.join(os.homedir(), ".ahchat");
50
+ const urlFilePath = path.join(ahchatDir, ".bridge-launch-url");
51
+ fs.mkdirSync(ahchatDir, { recursive: true });
52
+ const psScriptPath = path.join(ahchatDir, "launch-bridge.ps1");
53
+ const psContent = `param([string]$url)
54
+ if (-not $url) {
55
+ if (Test-Path '${urlFilePath}') {
56
+ $url = Get-Content '${urlFilePath}' -Raw
57
+ }
58
+ }
59
+ if (-not $url) {
60
+ Write-Error "No URL provided"
61
+ exit 1
62
+ }
63
+ & '${nodeExe}' '${exe}' launch --url $url
64
+ `;
65
+ fs.writeFileSync(psScriptPath, psContent);
66
+ const handler = `powershell -ExecutionPolicy Bypass -File "${psScriptPath}" -url "%1"`;
67
+ const regCommands = [
68
+ `REG ADD "HKCU\\Software\\Classes\\ahchat" /ve /d "URL:ahchat" /f`,
69
+ `REG ADD "HKCU\\Software\\Classes\\ahchat" /v "URL Protocol" /d "" /f`,
70
+ `REG ADD "HKCU\\Software\\Classes\\ahchat\\DefaultIcon" /ve /d "${nodeExe}" /f`,
71
+ `REG ADD "HKCU\\Software\\Classes\\ahchat\\shell\\open\\command" /ve /t REG_SZ /d "${handler}" /f`
72
+ ];
73
+ for (const cmd of regCommands) {
74
+ try {
75
+ execSync(cmd, { stdio: "pipe" });
76
+ } catch (e) {
77
+ logger.error("Failed to register Windows protocol handler", { error: e, cmd });
78
+ throw new Error(`Failed to register protocol handler: ${cmd}`);
79
+ }
80
+ }
81
+ logger.info("Windows protocol handler registered", { psScriptPath });
82
+ }
83
+ function registerMacOS() {
84
+ const appDir = path.join(os.homedir(), "Applications", "AHChatBridge.app");
85
+ const contentsDir = path.join(appDir, "Contents");
86
+ const macosDir = path.join(contentsDir, "MacOS");
87
+ const resourcesDir = path.join(contentsDir, "Resources");
88
+ fs.mkdirSync(macosDir, { recursive: true });
89
+ fs.mkdirSync(resourcesDir, { recursive: true });
90
+ const infoPlist = `<?xml version="1.0" encoding="UTF-8"?>
91
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
92
+ <plist version="1.0">
93
+ <dict>
94
+ <key>CFBundleName</key>
95
+ <string>AHChatBridge</string>
96
+ <key>CFBundleDisplayName</key>
97
+ <string>AHChat Bridge</string>
98
+ <key>CFBundleIdentifier</key>
99
+ <string>com.fangyb.ahchat-bridge</string>
100
+ <key>CFBundleVersion</key>
101
+ <string>0.1.0</string>
102
+ <key>CFBundlePackageType</key>
103
+ <string>APPL</string>
104
+ <key>CFBundleExecutable</key>
105
+ <string>launch.sh</string>
106
+ <key>CFBundleURLTypes</key>
107
+ <array>
108
+ <dict>
109
+ <key>CFBundleURLName</key>
110
+ <string>AHChat Bridge</string>
111
+ <key>CFBundleURLSchemes</key>
112
+ <array>
113
+ <string>ahchat</string>
114
+ </array>
115
+ </dict>
116
+ </array>
117
+ </dict>
118
+ </plist>`;
119
+ const launchScript = `#!/bin/bash
120
+ URL="$1"
121
+ exec "${process.execPath}" "${getBridgeExePath()}" launch --url "$URL"`;
122
+ fs.writeFileSync(path.join(contentsDir, "Info.plist"), infoPlist);
123
+ fs.writeFileSync(path.join(macosDir, "launch.sh"), launchScript);
124
+ fs.chmodSync(path.join(macosDir, "launch.sh"), 493);
125
+ logger.info("macOS protocol handler registered", { appDir });
126
+ }
127
+ function registerLinux() {
128
+ const desktopFile = `[Desktop Entry]
129
+ Name=AHChat Bridge
130
+ Exec=${process.execPath} ${getBridgeExePath()} launch --url %u
131
+ Type=Application
132
+ NoDisplay=true
133
+ MimeType=x-scheme-handler/ahchat;`;
134
+ const desktopPath = path.join(
135
+ os.homedir(),
136
+ ".local",
137
+ "share",
138
+ "applications",
139
+ "ahchat-bridge.desktop"
140
+ );
141
+ fs.mkdirSync(path.dirname(desktopPath), { recursive: true });
142
+ fs.writeFileSync(desktopPath, desktopFile);
143
+ try {
144
+ execSync("update-desktop-database ~/.local/share/applications/", { stdio: "pipe" });
145
+ } catch {
146
+ }
147
+ logger.info("Linux protocol handler registered", { desktopPath });
148
+ }
149
+ function unregisterProtocolHandler() {
150
+ const platform = os.platform();
151
+ if (platform === "win32") {
152
+ try {
153
+ execSync('REG DELETE "HKCU\\Software\\Classes\\ahchat" /f', { stdio: "pipe" });
154
+ const psScriptPath = path.join(os.homedir(), ".ahchat", "launch-bridge.ps1");
155
+ const urlFilePath = path.join(os.homedir(), ".ahchat", ".bridge-launch-url");
156
+ try {
157
+ fs.unlinkSync(psScriptPath);
158
+ } catch {
159
+ }
160
+ try {
161
+ fs.unlinkSync(urlFilePath);
162
+ } catch {
163
+ }
164
+ logger.info("Windows protocol handler unregistered");
165
+ } catch (e) {
166
+ logger.warn("Failed to unregister Windows protocol handler", { error: e });
167
+ }
168
+ } else if (platform === "darwin") {
169
+ const appDir = path.join(os.homedir(), "Applications", "AHChatBridge.app");
170
+ try {
171
+ fs.rmSync(appDir, { recursive: true, force: true });
172
+ logger.info("macOS protocol handler unregistered");
173
+ } catch (e) {
174
+ logger.warn("Failed to unregister macOS protocol handler", { error: e });
175
+ }
176
+ } else {
177
+ const desktopPath = path.join(
178
+ os.homedir(),
179
+ ".local",
180
+ "share",
181
+ "applications",
182
+ "ahchat-bridge.desktop"
183
+ );
184
+ try {
185
+ fs.unlinkSync(desktopPath);
186
+ logger.info("Linux protocol handler unregistered");
187
+ } catch (e) {
188
+ logger.warn("Failed to unregister Linux protocol handler", { error: e });
189
+ }
190
+ }
191
+ }
192
+ function isProtocolRegistered() {
193
+ const platform = os.platform();
194
+ if (platform === "win32") {
195
+ try {
196
+ execSync('REG QUERY "HKCU\\Software\\Classes\\ahchat" /ve', { stdio: "pipe" });
197
+ return true;
198
+ } catch {
199
+ return false;
200
+ }
201
+ } else if (platform === "darwin") {
202
+ const appDir = path.join(os.homedir(), "Applications", "AHChatBridge.app");
203
+ return fs.existsSync(path.join(appDir, "Contents", "Info.plist"));
204
+ } else {
205
+ const desktopPath = path.join(
206
+ os.homedir(),
207
+ ".local",
208
+ "share",
209
+ "applications",
210
+ "ahchat-bridge.desktop"
211
+ );
212
+ return fs.existsSync(desktopPath);
213
+ }
214
+ }
215
+
216
+ // src/cli.ts
217
+ var logger2 = createModuleLogger("bridge");
22
218
  var cli = cac("ahchat-bridge");
23
- cli.command("[command]", "Start the bridge (default: run)").option("--server-url <url>", "WebSocket URL of the AHChat server").option("--token <token>", "Auth token for server registration").option("--data-dir <dir>", "Data directory (default: ~/.ahchat)").option("--log-level <level>", "Log level (default: INFO)").action((args) => {
24
- void runBridge({
25
- serverUrl: args["server-url"],
26
- token: args.token,
27
- dataDir: args["data-dir"],
28
- logLevel: args["log-level"]
29
- });
219
+ cli.command("install", "Register ahchat:// protocol handler (one-time setup)").action(() => {
220
+ registerProtocolHandler();
221
+ console.log("ahchat:// protocol handler registered successfully.");
222
+ console.log("You can now launch the bridge from your browser with one click.");
223
+ });
224
+ cli.command("uninstall", "Remove ahchat:// protocol handler").action(() => {
225
+ unregisterProtocolHandler();
226
+ console.log("ahchat:// protocol handler removed.");
227
+ });
228
+ cli.command("status", "Check if protocol handler is registered").action(() => {
229
+ const registered = isProtocolRegistered();
230
+ console.log(registered ? "ahchat:// protocol is registered." : "ahchat:// protocol is NOT registered.");
231
+ console.log('Run "npx @fangyb/ahchat-bridge install" to register it.');
232
+ });
233
+ cli.command("launch", "Launch bridge from ahchat:// URL (called by OS)").option("--url <url>", "ahchat:// URL with server and token params").action((args) => {
234
+ const url = args.url;
235
+ if (!url) {
236
+ console.error("Error: --url is required");
237
+ process.exit(1);
238
+ }
239
+ void runBridgeFromUrl(url);
30
240
  });
31
241
  cli.command("run", "Start the bridge and connect to server").option("--server-url <url>", "WebSocket URL of the AHChat server").option("--token <token>", "Auth token for server registration").option("--data-dir <dir>", "Data directory (default: ~/.ahchat)").option("--log-level <level>", "Log level (default: INFO)").action((args) => {
32
242
  void runBridge({
@@ -42,6 +252,35 @@ cli.command("version", "Show bridge version").action(() => {
42
252
  cli.help();
43
253
  cli.version("0.1.0");
44
254
  cli.parse();
255
+ function parseAhchatUrl(url) {
256
+ try {
257
+ if (!url.startsWith("ahchat://")) return null;
258
+ const afterProtocol = url.slice("ahchat://".length);
259
+ const tildeIndex = afterProtocol.indexOf("~");
260
+ if (tildeIndex < 0) return null;
261
+ const rest = afterProtocol.slice(tildeIndex + 1);
262
+ const lastTilde = rest.lastIndexOf("~");
263
+ if (lastTilde < 0) return null;
264
+ const serverUrl = decodeURIComponent(rest.slice(0, lastTilde));
265
+ const token = decodeURIComponent(rest.slice(lastTilde + 1));
266
+ if (!serverUrl || !token) return null;
267
+ return { serverUrl, token };
268
+ } catch {
269
+ return null;
270
+ }
271
+ }
272
+ async function runBridgeFromUrl(url) {
273
+ const parsed = parseAhchatUrl(url);
274
+ if (!parsed) {
275
+ console.error("Invalid ahchat:// URL:", url);
276
+ console.error("Expected format: ahchat://bridge?server=ws://host:port/ws/bridge&token=xxx");
277
+ process.exit(1);
278
+ }
279
+ await runBridge({
280
+ serverUrl: parsed.serverUrl,
281
+ token: parsed.token
282
+ });
283
+ }
45
284
  async function runBridge(args) {
46
285
  let config = loadBridgeConfig();
47
286
  if (args.serverUrl) {
@@ -60,7 +299,7 @@ async function runBridge(args) {
60
299
  }
61
300
  ensureDir(config.dataDir);
62
301
  acquireLock(config.dataDir);
63
- logger.info("Bridge starting", {
302
+ logger2.info("Bridge starting", {
64
303
  bridgeId: config.bridgeId,
65
304
  serverUrl: config.serverUrl,
66
305
  serverApiUrl: config.serverApiUrl
@@ -101,21 +340,21 @@ async function runBridge(args) {
101
340
  switch (msg.type) {
102
341
  case "bridge:list_models_request": {
103
342
  const { requestId } = msg.payload;
104
- logger.info("list_models request received", { requestId });
343
+ logger2.info("list_models request received", { requestId });
105
344
  try {
106
345
  const models = await listModels();
107
346
  connector?.send({
108
347
  type: "bridge:list_models_response",
109
348
  payload: { requestId, models }
110
349
  });
111
- logger.info("list_models response sent", { requestId, count: models.length });
350
+ logger2.info("list_models response sent", { requestId, count: models.length });
112
351
  } catch (e) {
113
352
  const err = e instanceof Error ? e.message : String(e);
114
353
  connector?.send({
115
354
  type: "bridge:list_models_response",
116
355
  payload: { requestId, error: err }
117
356
  });
118
- logger.error("list_models failed", { requestId, error: e });
357
+ logger2.error("list_models failed", { requestId, error: e });
119
358
  }
120
359
  break;
121
360
  }
@@ -123,7 +362,7 @@ async function runBridge(args) {
123
362
  await agentManager.terminate(msg.payload.agentId);
124
363
  break;
125
364
  case "agent:terminate_scope":
126
- logger.info("agent:terminate_scope received", {
365
+ logger2.info("agent:terminate_scope received", {
127
366
  agentId: msg.payload.agentId,
128
367
  scope: msg.payload.scope
129
368
  });
@@ -140,7 +379,7 @@ async function runBridge(args) {
140
379
  const p = msg.payload;
141
380
  const answerText = formatAnswerForSDK(p);
142
381
  const ok = askQuestionRegistry.resolve(p.questionId, answerText);
143
- logger.info("user:answer_question handled", {
382
+ logger2.info("user:answer_question handled", {
144
383
  questionId: p.questionId,
145
384
  agentId: p.agentId,
146
385
  resolved: ok,
@@ -161,7 +400,7 @@ async function runBridge(args) {
161
400
  });
162
401
  }, config.queryConfig.statusReportIntervalMs);
163
402
  const shutdown = async (signal) => {
164
- logger.info("Shutdown signal received", { signal });
403
+ logger2.info("Shutdown signal received", { signal });
165
404
  if (statusInterval) {
166
405
  clearInterval(statusInterval);
167
406
  statusInterval = null;
@@ -169,7 +408,7 @@ async function runBridge(args) {
169
408
  wsMetrics.stop();
170
409
  connector?.close();
171
410
  await agentManager.shutdownAll();
172
- logger.info("Bridge stopped");
411
+ logger2.info("Bridge stopped");
173
412
  process.exit(0);
174
413
  };
175
414
  process.on("SIGINT", () => void shutdown("SIGINT"));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fangyb/ahchat-bridge",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "AHChat Bridge CLI — connect your local Claude Code agents to an AHChat server",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",