@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.
- package/dist/cli.js +255 -16
- 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
|
-
|
|
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("
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
411
|
+
logger2.info("Bridge stopped");
|
|
173
412
|
process.exit(0);
|
|
174
413
|
};
|
|
175
414
|
process.on("SIGINT", () => void shutdown("SIGINT"));
|