@fangyb/ahchat-bridge 0.1.1 → 0.1.3
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/README.md +197 -0
- package/dist/{chunk-7SODRWIG.js → chunk-MO54RNR2.js} +48 -40
- package/dist/cli.js +222 -17
- package/dist/index.js +1 -1
- package/package.json +3 -2
package/README.md
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
# @fangyb/ahchat-bridge
|
|
2
|
+
|
|
3
|
+
Connect your local Claude Code agents to an [AHChat](https://github.com/fangyb/ahchat) server.
|
|
4
|
+
|
|
5
|
+
## What is this?
|
|
6
|
+
|
|
7
|
+
AHChat is a multi-agent chat platform where AI agents (powered by Claude Code) collaborate like a real team. The **Bridge** is the local process that runs on your machine — it manages Claude Code SDK sessions, handles agent working directories, and communicates with the AHChat server over WebSocket.
|
|
8
|
+
|
|
9
|
+
Think of it as: **Server (cloud/self-hosted) ←→ Bridge (your machine) ←→ Claude Code (local agents)**
|
|
10
|
+
|
|
11
|
+
## Prerequisites
|
|
12
|
+
|
|
13
|
+
- **Node.js >= 20**
|
|
14
|
+
- **Anthropic API key** — set `ANTHROPIC_API_KEY` in your environment
|
|
15
|
+
- **AHChat server** — either self-hosted or a cloud instance you have access to
|
|
16
|
+
|
|
17
|
+
## Quick Start
|
|
18
|
+
|
|
19
|
+
### 1. Get an auth token
|
|
20
|
+
|
|
21
|
+
From your AHChat server admin panel or via API:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
curl -X POST http://your-server:3001/api/bridge/token \
|
|
25
|
+
-H "Content-Type: application/json" \
|
|
26
|
+
-d '{"label":"my-laptop","expiresHours":24}'
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Response:
|
|
30
|
+
|
|
31
|
+
```json
|
|
32
|
+
{
|
|
33
|
+
"token": "5S5vFj1kRT3jxdcQYReRETEQfsbNYlPU",
|
|
34
|
+
"id": "btk_8da0e5b1445993e9",
|
|
35
|
+
"label": "my-laptop",
|
|
36
|
+
"expiresAt": "2026-05-20T03:44:45.394Z"
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### 2. Run the bridge
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
npx @fangyb/ahchat-bridge --server-url wss://your-server:3001/ws/bridge --token YOUR_TOKEN
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
That's it. The bridge will:
|
|
47
|
+
- Connect to the server
|
|
48
|
+
- Register your local agents
|
|
49
|
+
- Start processing tasks (messages, group chats, tool calls)
|
|
50
|
+
|
|
51
|
+
### 3. (Optional) Install globally
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
npm install -g @fangyb/ahchat-bridge
|
|
55
|
+
ahchat-bridge --server-url wss://your-server:3001/ws/bridge --token YOUR_TOKEN
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## CLI Commands
|
|
59
|
+
|
|
60
|
+
### `run` (default)
|
|
61
|
+
|
|
62
|
+
Start the bridge and connect to the server.
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
npx @fangyb/ahchat-bridge run \
|
|
66
|
+
--server-url wss://your-server:3001/ws/bridge \
|
|
67
|
+
--token YOUR_TOKEN \
|
|
68
|
+
--data-dir ~/.ahchat \
|
|
69
|
+
--log-level INFO
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
| Flag | Description | Default |
|
|
73
|
+
|---|---|---|
|
|
74
|
+
| `--server-url` | WebSocket URL of the AHChat server | `ws://localhost:3001/ws/bridge` |
|
|
75
|
+
| `--token` | Auth token for server registration | _(none)_ |
|
|
76
|
+
| `--data-dir` | Data directory for sessions & workspaces | `~/.ahchat` |
|
|
77
|
+
| `--log-level` | Log level: `DEBUG`, `INFO`, `WARN`, `ERROR` | `INFO` |
|
|
78
|
+
|
|
79
|
+
### `version`
|
|
80
|
+
|
|
81
|
+
Show bridge version.
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
npx @fangyb/ahchat-bridge version
|
|
85
|
+
# ahchat-bridge v0.1.0
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### `--help`
|
|
89
|
+
|
|
90
|
+
Show all commands and options.
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
npx @fangyb/ahchat-bridge --help
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Configuration
|
|
97
|
+
|
|
98
|
+
### Environment Variables
|
|
99
|
+
|
|
100
|
+
| Variable | Description | Default |
|
|
101
|
+
|---|---|---|
|
|
102
|
+
| `AHCHAT_BRIDGE_SERVER_URL` | WebSocket URL to server | `ws://localhost:3001/ws/bridge` |
|
|
103
|
+
| `AHCHAT_SERVER_API_URL` | HTTP REST API base URL | `http://localhost:3001` |
|
|
104
|
+
| `AHCHAT_BRIDGE_ID` | Stable bridge identifier | Auto-generated from hostname |
|
|
105
|
+
| `AHCHAT_DATA_DIR` | Data directory | `~/.ahchat` |
|
|
106
|
+
| `AHCHAT_DB_PATH` | Database path | `{dataDir}/data.db` |
|
|
107
|
+
| `AHCHAT_LOG_LEVEL` | Log level | `INFO` |
|
|
108
|
+
| `AHCHAT_BRIDGE_MAX_ACTIVE` | Max active SDK runtimes | `50` |
|
|
109
|
+
| `AHCHAT_BRIDGE_IDLE_TIMEOUT_MS` | Idle runtime timeout | `60000` |
|
|
110
|
+
| `ANTHROPIC_API_KEY` | **Required** — Anthropic API key | _(none)_ |
|
|
111
|
+
|
|
112
|
+
### JSON Config File
|
|
113
|
+
|
|
114
|
+
Create `~/.ahchat/bridge.json`:
|
|
115
|
+
|
|
116
|
+
```json
|
|
117
|
+
{
|
|
118
|
+
"serverUrl": "wss://your-server:3001/ws/bridge",
|
|
119
|
+
"serverApiUrl": "https://your-server:3001",
|
|
120
|
+
"logLevel": "DEBUG"
|
|
121
|
+
}
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
CLI flags always override config file, which always overrides environment variables.
|
|
125
|
+
|
|
126
|
+
## How It Works
|
|
127
|
+
|
|
128
|
+
```
|
|
129
|
+
┌─────────────────┐ HTTPS/WSS ┌──────────────────┐ WebSocket ┌─────────────────┐
|
|
130
|
+
│ Web Frontend │ ◄─────────────────► │ AHChat Server │ ◄──────────────────► │ ahchat-bridge │
|
|
131
|
+
│ (browser) │ /ws/client │ (Fastify) │ /ws/bridge │ (your machine) │
|
|
132
|
+
│ │ HTTP REST API │ (SQLite) │ task:dispatch │ Claude Code SDK│
|
|
133
|
+
└─────────────────┘ └──────────────────┘ └─────────────────┘
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
1. **You send a message** in the web UI to an agent
|
|
137
|
+
2. **Server** receives it, persists it, and dispatches `task:dispatch` to your Bridge
|
|
138
|
+
3. **Bridge** acquires the agent's Claude Code SDK runtime, pushes the message via `InputController`
|
|
139
|
+
4. **Claude Code** processes the task — thinking, tool calls, text generation
|
|
140
|
+
5. **Bridge** streams events back: `thinking_chunk`, `tool_use`, `text_chunk`, `done`
|
|
141
|
+
6. **Server** forwards events to the web UI in real-time
|
|
142
|
+
|
|
143
|
+
## Data Storage
|
|
144
|
+
|
|
145
|
+
All data is stored locally in `~/.ahchat/`:
|
|
146
|
+
|
|
147
|
+
| Path | Purpose |
|
|
148
|
+
|---|---|
|
|
149
|
+
| `~/.ahchat/data.db` | Local SQLite database (sessions, config) |
|
|
150
|
+
| `~/.ahchat/workspaces/<agentId>/` | Agent working directories |
|
|
151
|
+
| `~/.ahchat/sessions.json` | SDK session tracking for resume |
|
|
152
|
+
| `~/.ahchat/bridge.json` | Optional config file |
|
|
153
|
+
| `~/.ahchat/bridge.log` | Rotating log file (max 10MB) |
|
|
154
|
+
|
|
155
|
+
## Security
|
|
156
|
+
|
|
157
|
+
- **Token auth**: Bridge connects with a one-time auth token (sent as `?token=` query param on WebSocket upgrade)
|
|
158
|
+
- **Token expiry**: Tokens expire after a configurable period (default 24h)
|
|
159
|
+
- **Single use**: Each token can only be used once
|
|
160
|
+
- **TLS**: Use `wss://` for production connections
|
|
161
|
+
|
|
162
|
+
## Troubleshooting
|
|
163
|
+
|
|
164
|
+
### "Bridge connection rejected: invalid token"
|
|
165
|
+
|
|
166
|
+
The token is incorrect or has already been used. Generate a new token from your server.
|
|
167
|
+
|
|
168
|
+
### "Bridge connection rejected: token expired"
|
|
169
|
+
|
|
170
|
+
The token has expired. Generate a new one with a longer `expiresHours` value.
|
|
171
|
+
|
|
172
|
+
### "ANTHROPIC_API_KEY not set"
|
|
173
|
+
|
|
174
|
+
Set your Anthropic API key:
|
|
175
|
+
|
|
176
|
+
```bash
|
|
177
|
+
# Linux/macOS
|
|
178
|
+
export ANTHROPIC_API_KEY=sk-ant-...
|
|
179
|
+
|
|
180
|
+
# Windows (PowerShell)
|
|
181
|
+
$env:ANTHROPIC_API_KEY="sk-ant-..."
|
|
182
|
+
|
|
183
|
+
# Windows (cmd)
|
|
184
|
+
set ANTHROPIC_API_KEY=sk-ant-...
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
### Bridge keeps reconnecting
|
|
188
|
+
|
|
189
|
+
Check that the server URL is correct and the server is running. The bridge auto-reconnects with exponential backoff (1s → 2s → 4s → 8s → 16s → 30s).
|
|
190
|
+
|
|
191
|
+
### Agent status stays "offline"
|
|
192
|
+
|
|
193
|
+
Make sure the bridge is running and has successfully registered. Check the bridge logs for registration errors.
|
|
194
|
+
|
|
195
|
+
## License
|
|
196
|
+
|
|
197
|
+
Proprietary. See the AHChat repository for details.
|
|
@@ -1,8 +1,15 @@
|
|
|
1
|
+
// ../../node_modules/.pnpm/tsup@8.5.1_jiti@1.21.7_postcss@8.5.14_tsx@4.21.0_typescript@5.9.3/node_modules/tsup/assets/esm_shims.js
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { fileURLToPath } from "url";
|
|
4
|
+
var getFilename = () => fileURLToPath(import.meta.url);
|
|
5
|
+
var getDirname = () => path.dirname(getFilename());
|
|
6
|
+
var __dirname = /* @__PURE__ */ getDirname();
|
|
7
|
+
|
|
1
8
|
// src/config.ts
|
|
2
9
|
import crypto from "crypto";
|
|
3
10
|
import fs from "fs";
|
|
4
11
|
import os from "os";
|
|
5
|
-
import
|
|
12
|
+
import path2 from "path";
|
|
6
13
|
var DEFAULT_QUERY_CONFIG = {
|
|
7
14
|
maxActive: 50,
|
|
8
15
|
idleTimeoutMs: 6e4,
|
|
@@ -56,9 +63,9 @@ function mergeQueryConfig(file) {
|
|
|
56
63
|
function loadBridgeConfig() {
|
|
57
64
|
const dataDir = readEnvString(
|
|
58
65
|
"AHCHAT_DATA_DIR",
|
|
59
|
-
|
|
66
|
+
path2.join(os.homedir(), ".ahchat")
|
|
60
67
|
);
|
|
61
|
-
const fileConfig = tryReadJsonConfig(
|
|
68
|
+
const fileConfig = tryReadJsonConfig(path2.join(dataDir, "bridge.json"));
|
|
62
69
|
return {
|
|
63
70
|
serverUrl: readEnvString(
|
|
64
71
|
"AHCHAT_BRIDGE_SERVER_URL",
|
|
@@ -75,7 +82,7 @@ function loadBridgeConfig() {
|
|
|
75
82
|
dataDir,
|
|
76
83
|
dbPath: readEnvString(
|
|
77
84
|
"AHCHAT_DB_PATH",
|
|
78
|
-
fileConfig.dbPath ??
|
|
85
|
+
fileConfig.dbPath ?? path2.join(dataDir, "data.db")
|
|
79
86
|
),
|
|
80
87
|
serverApiUrl: readEnvString(
|
|
81
88
|
"AHCHAT_SERVER_API_URL",
|
|
@@ -90,7 +97,7 @@ function ensureDir(dirPath) {
|
|
|
90
97
|
|
|
91
98
|
// src/logger.ts
|
|
92
99
|
import os3 from "os";
|
|
93
|
-
import
|
|
100
|
+
import path4 from "path";
|
|
94
101
|
|
|
95
102
|
// ../logger/src/types.ts
|
|
96
103
|
var LOG_LEVEL_VALUE = {
|
|
@@ -710,7 +717,7 @@ function consoleTransport(opts) {
|
|
|
710
717
|
}
|
|
711
718
|
|
|
712
719
|
// ../logger/src/transports/file.ts
|
|
713
|
-
import
|
|
720
|
+
import path3 from "path";
|
|
714
721
|
|
|
715
722
|
// ../../node_modules/.pnpm/rotating-file-stream@3.2.9/node_modules/rotating-file-stream/dist/esm/index.js
|
|
716
723
|
import { exec } from "child_process";
|
|
@@ -755,11 +762,11 @@ var RotatingFileStream = class extends Writable {
|
|
|
755
762
|
timeout;
|
|
756
763
|
timeoutPromise;
|
|
757
764
|
constructor(generator, options) {
|
|
758
|
-
const { encoding, history, maxFiles, maxSize, path:
|
|
765
|
+
const { encoding, history, maxFiles, maxSize, path: path11 } = options;
|
|
759
766
|
super({ decodeStrings: true, defaultEncoding: encoding });
|
|
760
767
|
this.createGzip = createGzip;
|
|
761
768
|
this.exec = exec;
|
|
762
|
-
this.filename =
|
|
769
|
+
this.filename = path11 + generator(null);
|
|
763
770
|
this.fsCreateReadStream = createReadStream;
|
|
764
771
|
this.fsCreateWriteStream = createWriteStream;
|
|
765
772
|
this.fsOpen = open;
|
|
@@ -771,7 +778,7 @@ var RotatingFileStream = class extends Writable {
|
|
|
771
778
|
this.options = options;
|
|
772
779
|
this.stdout = process.stdout;
|
|
773
780
|
if (maxFiles || maxSize)
|
|
774
|
-
options.history =
|
|
781
|
+
options.history = path11 + (history ? history : this.generator(null) + ".txt");
|
|
775
782
|
this.on("close", () => this.finished ? null : this.emit("finish"));
|
|
776
783
|
this.on("finish", () => this.finished = this.clear());
|
|
777
784
|
(async () => {
|
|
@@ -899,9 +906,9 @@ var RotatingFileStream = class extends Writable {
|
|
|
899
906
|
return this.move();
|
|
900
907
|
}
|
|
901
908
|
async findName() {
|
|
902
|
-
const { interval, path:
|
|
909
|
+
const { interval, path: path11, intervalBoundary } = this.options;
|
|
903
910
|
for (let index = 1; index < 1e3; ++index) {
|
|
904
|
-
const filename =
|
|
911
|
+
const filename = path11 + this.generator(interval && intervalBoundary ? new Date(this.prev) : this.rotation, index);
|
|
905
912
|
if (!await exists(filename))
|
|
906
913
|
return filename;
|
|
907
914
|
}
|
|
@@ -931,11 +938,11 @@ var RotatingFileStream = class extends Writable {
|
|
|
931
938
|
return this.unlink(filename);
|
|
932
939
|
}
|
|
933
940
|
async classical() {
|
|
934
|
-
const { compress, path:
|
|
941
|
+
const { compress, path: path11, rotate } = this.options;
|
|
935
942
|
let rotatedName = "";
|
|
936
943
|
for (let count = rotate; count > 0; --count) {
|
|
937
|
-
const currName =
|
|
938
|
-
const prevName = count === 1 ? this.filename :
|
|
944
|
+
const currName = path11 + this.generator(count);
|
|
945
|
+
const prevName = count === 1 ? this.filename : path11 + this.generator(count - 1);
|
|
939
946
|
if (!await exists(prevName))
|
|
940
947
|
continue;
|
|
941
948
|
if (!rotatedName)
|
|
@@ -1328,8 +1335,8 @@ function parseSize(maxSize) {
|
|
|
1328
1335
|
}
|
|
1329
1336
|
function fileTransport(opts) {
|
|
1330
1337
|
const fmt = opts.formatter ?? jsonFormatter;
|
|
1331
|
-
const dir =
|
|
1332
|
-
const filename =
|
|
1338
|
+
const dir = path3.dirname(opts.path);
|
|
1339
|
+
const filename = path3.basename(opts.path);
|
|
1333
1340
|
const stream = createStream(filename, {
|
|
1334
1341
|
path: dir,
|
|
1335
1342
|
size: opts.rotate?.maxSize ? parseSize(opts.rotate.maxSize) : "50M",
|
|
@@ -1349,8 +1356,8 @@ function createLogger(config) {
|
|
|
1349
1356
|
// src/logger.ts
|
|
1350
1357
|
var bridgeConfig = loadBridgeConfig();
|
|
1351
1358
|
var isTest = !!process.env["VITEST"];
|
|
1352
|
-
var LOG_DIR =
|
|
1353
|
-
var LOG_FILE =
|
|
1359
|
+
var LOG_DIR = path4.join(os3.homedir(), ".ahchat", "logs");
|
|
1360
|
+
var LOG_FILE = path4.join(LOG_DIR, "bridge.log");
|
|
1354
1361
|
if (!isTest) ensureDir(LOG_DIR);
|
|
1355
1362
|
function createModuleLogger(module) {
|
|
1356
1363
|
const transports = [consoleTransport({ formatter: prettyFormatter })];
|
|
@@ -1748,7 +1755,7 @@ var wsMetrics = new WsMetrics();
|
|
|
1748
1755
|
// src/agentManager.ts
|
|
1749
1756
|
import fs2 from "fs/promises";
|
|
1750
1757
|
import os4 from "os";
|
|
1751
|
-
import
|
|
1758
|
+
import path7 from "path";
|
|
1752
1759
|
|
|
1753
1760
|
// src/inputController.ts
|
|
1754
1761
|
var InputController = class {
|
|
@@ -1806,17 +1813,17 @@ var InputController = class {
|
|
|
1806
1813
|
};
|
|
1807
1814
|
|
|
1808
1815
|
// src/permissionGuard.ts
|
|
1809
|
-
import
|
|
1816
|
+
import path6 from "path";
|
|
1810
1817
|
|
|
1811
1818
|
// ../shared/src/utils/pathSafety.ts
|
|
1812
|
-
import
|
|
1819
|
+
import path5 from "path";
|
|
1813
1820
|
function isPathInside(parent, child) {
|
|
1814
|
-
const resolvedParent =
|
|
1815
|
-
const resolvedChild =
|
|
1821
|
+
const resolvedParent = path5.resolve(parent);
|
|
1822
|
+
const resolvedChild = path5.resolve(child);
|
|
1816
1823
|
if (resolvedParent === resolvedChild) return true;
|
|
1817
|
-
const rel =
|
|
1824
|
+
const rel = path5.relative(resolvedParent, resolvedChild);
|
|
1818
1825
|
if (rel === "") return true;
|
|
1819
|
-
return !rel.startsWith("..") && !
|
|
1826
|
+
return !rel.startsWith("..") && !path5.isAbsolute(rel);
|
|
1820
1827
|
}
|
|
1821
1828
|
|
|
1822
1829
|
// src/permissionGuard.ts
|
|
@@ -1831,7 +1838,7 @@ function makeCwdPermissionGuard(cwd, agentId, scope, log) {
|
|
|
1831
1838
|
if (typeof raw !== "string" || raw.length === 0) {
|
|
1832
1839
|
return { behavior: "allow" };
|
|
1833
1840
|
}
|
|
1834
|
-
const abs =
|
|
1841
|
+
const abs = path6.isAbsolute(raw) ? raw : path6.resolve(cwd, raw);
|
|
1835
1842
|
if (isPathInside(cwd, abs)) {
|
|
1836
1843
|
return { behavior: "allow" };
|
|
1837
1844
|
}
|
|
@@ -2881,14 +2888,14 @@ var AgentManager = class {
|
|
|
2881
2888
|
this.emit = emit;
|
|
2882
2889
|
if (typeof options === "function") {
|
|
2883
2890
|
this.queryFn = options;
|
|
2884
|
-
this.workspacesDir =
|
|
2891
|
+
this.workspacesDir = path7.join(os4.homedir(), ".ahchat", "workspaces");
|
|
2885
2892
|
this.queryConfig = DEFAULT_QUERY_CONFIG;
|
|
2886
2893
|
this.askQuestionRegistry = new AskQuestionRegistry();
|
|
2887
2894
|
this.neuralBusManager = new NeuralBusManager();
|
|
2888
2895
|
this.groupRegistry = null;
|
|
2889
2896
|
} else {
|
|
2890
2897
|
this.queryFn = options?.queryFn ?? null;
|
|
2891
|
-
this.workspacesDir = options?.workspacesDir ??
|
|
2898
|
+
this.workspacesDir = options?.workspacesDir ?? path7.join(os4.homedir(), ".ahchat", "workspaces");
|
|
2892
2899
|
this.queryConfig = options?.queryConfig ?? DEFAULT_QUERY_CONFIG;
|
|
2893
2900
|
this.askQuestionRegistry = options?.askQuestionRegistry ?? new AskQuestionRegistry();
|
|
2894
2901
|
this.neuralBusManager = options?.neuralBusManager ?? new NeuralBusManager();
|
|
@@ -3357,7 +3364,7 @@ ${relay.message}`,
|
|
|
3357
3364
|
status: existingProc.status
|
|
3358
3365
|
});
|
|
3359
3366
|
} else {
|
|
3360
|
-
const cwd = agentConfig.workingDirectory ||
|
|
3367
|
+
const cwd = agentConfig.workingDirectory || path7.join(this.workspacesDir, agentConfig.id);
|
|
3361
3368
|
void this.acquire(agentConfig, scope, cwd).then((proc) => {
|
|
3362
3369
|
if (suppressEmit) proc.internalRelayId = suppressEmit;
|
|
3363
3370
|
return this.sendMessage({ ...task, agentId: agentConfig.id, scope });
|
|
@@ -3492,7 +3499,7 @@ ${relay.message}`,
|
|
|
3492
3499
|
break;
|
|
3493
3500
|
}
|
|
3494
3501
|
try {
|
|
3495
|
-
const cwd = agent.workingDirectory ||
|
|
3502
|
+
const cwd = agent.workingDirectory || path7.join(this.workspacesDir, agent.id);
|
|
3496
3503
|
await this.acquire(agent, { kind: "single" }, cwd);
|
|
3497
3504
|
warmed++;
|
|
3498
3505
|
logger8.info("Agent process pre-created for recovery", { agentId: agent.id });
|
|
@@ -3688,8 +3695,8 @@ var HttpAgentRegistry = class {
|
|
|
3688
3695
|
agents = /* @__PURE__ */ new Map();
|
|
3689
3696
|
apiUrl(suffix) {
|
|
3690
3697
|
const base = this.serverApiUrl.replace(/\/$/, "");
|
|
3691
|
-
const
|
|
3692
|
-
return `${base}${
|
|
3698
|
+
const path11 = suffix.startsWith("/") ? suffix : `/${suffix}`;
|
|
3699
|
+
return `${base}${path11}`;
|
|
3693
3700
|
}
|
|
3694
3701
|
async refresh() {
|
|
3695
3702
|
try {
|
|
@@ -4074,11 +4081,11 @@ var ServerConnector = class {
|
|
|
4074
4081
|
// src/modelQuerier.ts
|
|
4075
4082
|
import fs3 from "fs/promises";
|
|
4076
4083
|
import os5 from "os";
|
|
4077
|
-
import
|
|
4084
|
+
import path8 from "path";
|
|
4078
4085
|
var logger12 = createModuleLogger("bridge.modelQuerier");
|
|
4079
4086
|
async function listModels(queryFn, opts = {}) {
|
|
4080
4087
|
const t0 = Date.now();
|
|
4081
|
-
const cwd = opts.cwd ??
|
|
4088
|
+
const cwd = opts.cwd ?? path8.join(os5.homedir(), ".ahchat", "workspaces", "_list_models");
|
|
4082
4089
|
await fs3.mkdir(cwd, { recursive: true });
|
|
4083
4090
|
const fn = queryFn ?? (await import("@anthropic-ai/claude-agent-sdk")).query;
|
|
4084
4091
|
const ic = new InputController();
|
|
@@ -4137,7 +4144,7 @@ async function listModels(queryFn, opts = {}) {
|
|
|
4137
4144
|
|
|
4138
4145
|
// src/lockfile.ts
|
|
4139
4146
|
import fs4 from "fs";
|
|
4140
|
-
import
|
|
4147
|
+
import path9 from "path";
|
|
4141
4148
|
var logger13 = createModuleLogger("bridge.lockfile");
|
|
4142
4149
|
var lockPath = null;
|
|
4143
4150
|
function isProcessAlive(pid) {
|
|
@@ -4151,7 +4158,7 @@ function isProcessAlive(pid) {
|
|
|
4151
4158
|
}
|
|
4152
4159
|
}
|
|
4153
4160
|
function acquireLock(dataDir) {
|
|
4154
|
-
const file =
|
|
4161
|
+
const file = path9.join(dataDir, "bridge.lock");
|
|
4155
4162
|
lockPath = file;
|
|
4156
4163
|
if (fs4.existsSync(file)) {
|
|
4157
4164
|
const raw = fs4.readFileSync(file, "utf-8").trim();
|
|
@@ -4163,7 +4170,7 @@ function acquireLock(dataDir) {
|
|
|
4163
4170
|
logger13.warn("Removing stale bridge.lock (process not found)", { pid, path: file });
|
|
4164
4171
|
}
|
|
4165
4172
|
}
|
|
4166
|
-
fs4.mkdirSync(
|
|
4173
|
+
fs4.mkdirSync(path9.dirname(file), { recursive: true });
|
|
4167
4174
|
fs4.writeFileSync(file, String(process.pid), "utf-8");
|
|
4168
4175
|
logger13.info("Acquired bridge lock", { path: file, pid: process.pid });
|
|
4169
4176
|
const release = () => {
|
|
@@ -4431,13 +4438,13 @@ function createGroupTaskDispatchHandler(agentManager, agentRegistry, emit) {
|
|
|
4431
4438
|
|
|
4432
4439
|
// src/sessionStore.ts
|
|
4433
4440
|
import fs5 from "fs";
|
|
4434
|
-
import
|
|
4441
|
+
import path10 from "path";
|
|
4435
4442
|
var logger15 = createModuleLogger("session.store");
|
|
4436
4443
|
var SessionStore = class {
|
|
4437
4444
|
filePath;
|
|
4438
4445
|
cache;
|
|
4439
4446
|
constructor(dataDir) {
|
|
4440
|
-
this.filePath =
|
|
4447
|
+
this.filePath = path10.join(dataDir, "sessions.json");
|
|
4441
4448
|
this.cache = this.loadFromDisk();
|
|
4442
4449
|
}
|
|
4443
4450
|
cacheKey(agentId, scope) {
|
|
@@ -4497,7 +4504,7 @@ var SessionStore = class {
|
|
|
4497
4504
|
}
|
|
4498
4505
|
saveToDisk() {
|
|
4499
4506
|
try {
|
|
4500
|
-
const dir =
|
|
4507
|
+
const dir = path10.dirname(this.filePath);
|
|
4501
4508
|
fs5.mkdirSync(dir, { recursive: true });
|
|
4502
4509
|
fs5.writeFileSync(this.filePath, JSON.stringify(this.cache, null, 2), "utf-8");
|
|
4503
4510
|
} catch (e) {
|
|
@@ -4507,6 +4514,7 @@ var SessionStore = class {
|
|
|
4507
4514
|
};
|
|
4508
4515
|
|
|
4509
4516
|
export {
|
|
4517
|
+
__dirname,
|
|
4510
4518
|
loadBridgeConfig,
|
|
4511
4519
|
ensureDir,
|
|
4512
4520
|
createModuleLogger,
|
package/dist/cli.js
CHANGED
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
HttpAgentRegistry,
|
|
6
6
|
ServerConnector,
|
|
7
7
|
SessionStore,
|
|
8
|
+
__dirname,
|
|
8
9
|
acquireLock,
|
|
9
10
|
createGroupTaskDispatchHandler,
|
|
10
11
|
createModuleLogger,
|
|
@@ -14,19 +15,199 @@ import {
|
|
|
14
15
|
listModels,
|
|
15
16
|
loadBridgeConfig,
|
|
16
17
|
wsMetrics
|
|
17
|
-
} from "./chunk-
|
|
18
|
+
} from "./chunk-MO54RNR2.js";
|
|
18
19
|
|
|
19
20
|
// src/cli.ts
|
|
20
21
|
import cac from "cac";
|
|
21
|
-
|
|
22
|
+
|
|
23
|
+
// src/protocol.ts
|
|
24
|
+
import { execSync } from "child_process";
|
|
25
|
+
import fs from "fs";
|
|
26
|
+
import os from "os";
|
|
27
|
+
import path from "path";
|
|
28
|
+
var logger = createModuleLogger("bridge.protocol");
|
|
29
|
+
function getBridgeExePath() {
|
|
30
|
+
const pkgDir = path.resolve(__dirname, "..");
|
|
31
|
+
return path.join(pkgDir, "dist", "cli.js");
|
|
32
|
+
}
|
|
33
|
+
function registerProtocolHandler() {
|
|
34
|
+
const platform = os.platform();
|
|
35
|
+
if (platform === "win32") {
|
|
36
|
+
registerWindows();
|
|
37
|
+
} else if (platform === "darwin") {
|
|
38
|
+
registerMacOS();
|
|
39
|
+
} else {
|
|
40
|
+
registerLinux();
|
|
41
|
+
}
|
|
42
|
+
logger.info("Protocol handler registered", { platform });
|
|
43
|
+
}
|
|
44
|
+
function registerWindows() {
|
|
45
|
+
const exe = getBridgeExePath();
|
|
46
|
+
const handler = `"${process.execPath}" "${exe}" launch --url "%1"`;
|
|
47
|
+
const iconPath = process.execPath;
|
|
48
|
+
const regCommands = [
|
|
49
|
+
`REG ADD "HKCU\\Software\\Classes\\ahchat" /ve /d "URL:ahchat" /f`,
|
|
50
|
+
`REG ADD "HKCU\\Software\\Classes\\ahchat" /v "URL Protocol" /d "" /f`,
|
|
51
|
+
`REG ADD "HKCU\\Software\\Classes\\ahchat\\DefaultIcon" /ve /d "${iconPath}" /f`,
|
|
52
|
+
`REG ADD "HKCU\\Software\\Classes\\ahchat\\shell\\open\\command" /ve /d ${handler} /f`
|
|
53
|
+
];
|
|
54
|
+
for (const cmd of regCommands) {
|
|
55
|
+
try {
|
|
56
|
+
execSync(cmd, { stdio: "pipe" });
|
|
57
|
+
} catch (e) {
|
|
58
|
+
logger.error("Failed to register Windows protocol handler", { error: e, cmd });
|
|
59
|
+
throw new Error(`Failed to register protocol handler: ${cmd}`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
logger.info("Windows protocol handler registered");
|
|
63
|
+
}
|
|
64
|
+
function registerMacOS() {
|
|
65
|
+
const appDir = path.join(os.homedir(), "Applications", "AHChatBridge.app");
|
|
66
|
+
const contentsDir = path.join(appDir, "Contents");
|
|
67
|
+
const macosDir = path.join(contentsDir, "MacOS");
|
|
68
|
+
const resourcesDir = path.join(contentsDir, "Resources");
|
|
69
|
+
fs.mkdirSync(macosDir, { recursive: true });
|
|
70
|
+
fs.mkdirSync(resourcesDir, { recursive: true });
|
|
71
|
+
const infoPlist = `<?xml version="1.0" encoding="UTF-8"?>
|
|
72
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
73
|
+
<plist version="1.0">
|
|
74
|
+
<dict>
|
|
75
|
+
<key>CFBundleName</key>
|
|
76
|
+
<string>AHChatBridge</string>
|
|
77
|
+
<key>CFBundleDisplayName</key>
|
|
78
|
+
<string>AHChat Bridge</string>
|
|
79
|
+
<key>CFBundleIdentifier</key>
|
|
80
|
+
<string>com.fangyb.ahchat-bridge</string>
|
|
81
|
+
<key>CFBundleVersion</key>
|
|
82
|
+
<string>0.1.0</string>
|
|
83
|
+
<key>CFBundlePackageType</key>
|
|
84
|
+
<string>APPL</string>
|
|
85
|
+
<key>CFBundleExecutable</key>
|
|
86
|
+
<string>launch.sh</string>
|
|
87
|
+
<key>CFBundleURLTypes</key>
|
|
88
|
+
<array>
|
|
89
|
+
<dict>
|
|
90
|
+
<key>CFBundleURLName</key>
|
|
91
|
+
<string>AHChat Bridge</string>
|
|
92
|
+
<key>CFBundleURLSchemes</key>
|
|
93
|
+
<array>
|
|
94
|
+
<string>ahchat</string>
|
|
95
|
+
</array>
|
|
96
|
+
</dict>
|
|
97
|
+
</array>
|
|
98
|
+
</dict>
|
|
99
|
+
</plist>`;
|
|
100
|
+
const launchScript = `#!/bin/bash
|
|
101
|
+
URL="$1"
|
|
102
|
+
exec "${process.execPath}" "${getBridgeExePath()}" launch --url "$URL"`;
|
|
103
|
+
fs.writeFileSync(path.join(contentsDir, "Info.plist"), infoPlist);
|
|
104
|
+
fs.writeFileSync(path.join(macosDir, "launch.sh"), launchScript);
|
|
105
|
+
fs.chmodSync(path.join(macosDir, "launch.sh"), 493);
|
|
106
|
+
logger.info("macOS protocol handler registered", { appDir });
|
|
107
|
+
}
|
|
108
|
+
function registerLinux() {
|
|
109
|
+
const desktopFile = `[Desktop Entry]
|
|
110
|
+
Name=AHChat Bridge
|
|
111
|
+
Exec=${process.execPath} ${getBridgeExePath()} launch --url %u
|
|
112
|
+
Type=Application
|
|
113
|
+
NoDisplay=true
|
|
114
|
+
MimeType=x-scheme-handler/ahchat;`;
|
|
115
|
+
const desktopPath = path.join(
|
|
116
|
+
os.homedir(),
|
|
117
|
+
".local",
|
|
118
|
+
"share",
|
|
119
|
+
"applications",
|
|
120
|
+
"ahchat-bridge.desktop"
|
|
121
|
+
);
|
|
122
|
+
fs.mkdirSync(path.dirname(desktopPath), { recursive: true });
|
|
123
|
+
fs.writeFileSync(desktopPath, desktopFile);
|
|
124
|
+
try {
|
|
125
|
+
execSync("update-desktop-database ~/.local/share/applications/", { stdio: "pipe" });
|
|
126
|
+
} catch {
|
|
127
|
+
}
|
|
128
|
+
logger.info("Linux protocol handler registered", { desktopPath });
|
|
129
|
+
}
|
|
130
|
+
function unregisterProtocolHandler() {
|
|
131
|
+
const platform = os.platform();
|
|
132
|
+
if (platform === "win32") {
|
|
133
|
+
try {
|
|
134
|
+
execSync('REG DELETE "HKCU\\Software\\Classes\\ahchat" /f', { stdio: "pipe" });
|
|
135
|
+
logger.info("Windows protocol handler unregistered");
|
|
136
|
+
} catch (e) {
|
|
137
|
+
logger.warn("Failed to unregister Windows protocol handler", { error: e });
|
|
138
|
+
}
|
|
139
|
+
} else if (platform === "darwin") {
|
|
140
|
+
const appDir = path.join(os.homedir(), "Applications", "AHChatBridge.app");
|
|
141
|
+
try {
|
|
142
|
+
fs.rmSync(appDir, { recursive: true, force: true });
|
|
143
|
+
logger.info("macOS protocol handler unregistered");
|
|
144
|
+
} catch (e) {
|
|
145
|
+
logger.warn("Failed to unregister macOS protocol handler", { error: e });
|
|
146
|
+
}
|
|
147
|
+
} else {
|
|
148
|
+
const desktopPath = path.join(
|
|
149
|
+
os.homedir(),
|
|
150
|
+
".local",
|
|
151
|
+
"share",
|
|
152
|
+
"applications",
|
|
153
|
+
"ahchat-bridge.desktop"
|
|
154
|
+
);
|
|
155
|
+
try {
|
|
156
|
+
fs.unlinkSync(desktopPath);
|
|
157
|
+
logger.info("Linux protocol handler unregistered");
|
|
158
|
+
} catch (e) {
|
|
159
|
+
logger.warn("Failed to unregister Linux protocol handler", { error: e });
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
function isProtocolRegistered() {
|
|
164
|
+
const platform = os.platform();
|
|
165
|
+
if (platform === "win32") {
|
|
166
|
+
try {
|
|
167
|
+
execSync('REG QUERY "HKCU\\Software\\Classes\\ahchat" /ve', { stdio: "pipe" });
|
|
168
|
+
return true;
|
|
169
|
+
} catch {
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
172
|
+
} else if (platform === "darwin") {
|
|
173
|
+
const appDir = path.join(os.homedir(), "Applications", "AHChatBridge.app");
|
|
174
|
+
return fs.existsSync(path.join(appDir, "Contents", "Info.plist"));
|
|
175
|
+
} else {
|
|
176
|
+
const desktopPath = path.join(
|
|
177
|
+
os.homedir(),
|
|
178
|
+
".local",
|
|
179
|
+
"share",
|
|
180
|
+
"applications",
|
|
181
|
+
"ahchat-bridge.desktop"
|
|
182
|
+
);
|
|
183
|
+
return fs.existsSync(desktopPath);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// src/cli.ts
|
|
188
|
+
var logger2 = createModuleLogger("bridge");
|
|
22
189
|
var cli = cac("ahchat-bridge");
|
|
23
|
-
cli.command("
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
190
|
+
cli.command("install", "Register ahchat:// protocol handler (one-time setup)").action(() => {
|
|
191
|
+
registerProtocolHandler();
|
|
192
|
+
console.log("ahchat:// protocol handler registered successfully.");
|
|
193
|
+
console.log("You can now launch the bridge from your browser with one click.");
|
|
194
|
+
});
|
|
195
|
+
cli.command("uninstall", "Remove ahchat:// protocol handler").action(() => {
|
|
196
|
+
unregisterProtocolHandler();
|
|
197
|
+
console.log("ahchat:// protocol handler removed.");
|
|
198
|
+
});
|
|
199
|
+
cli.command("status", "Check if protocol handler is registered").action(() => {
|
|
200
|
+
const registered = isProtocolRegistered();
|
|
201
|
+
console.log(registered ? "ahchat:// protocol is registered." : "ahchat:// protocol is NOT registered.");
|
|
202
|
+
console.log('Run "npx @fangyb/ahchat-bridge install" to register it.');
|
|
203
|
+
});
|
|
204
|
+
cli.command("launch", "Launch bridge from ahchat:// URL (called by OS)").option("--url <url>", "ahchat:// URL with server and token params").action((args) => {
|
|
205
|
+
const url = args.url;
|
|
206
|
+
if (!url) {
|
|
207
|
+
console.error("Error: --url is required");
|
|
208
|
+
process.exit(1);
|
|
209
|
+
}
|
|
210
|
+
void runBridgeFromUrl(url);
|
|
30
211
|
});
|
|
31
212
|
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
213
|
void runBridge({
|
|
@@ -42,6 +223,30 @@ cli.command("version", "Show bridge version").action(() => {
|
|
|
42
223
|
cli.help();
|
|
43
224
|
cli.version("0.1.0");
|
|
44
225
|
cli.parse();
|
|
226
|
+
function parseAhchatUrl(url) {
|
|
227
|
+
try {
|
|
228
|
+
const parsed = new URL(url);
|
|
229
|
+
if (parsed.protocol !== "ahchat:") return null;
|
|
230
|
+
const serverUrl = parsed.searchParams.get("server");
|
|
231
|
+
const token = parsed.searchParams.get("token");
|
|
232
|
+
if (!serverUrl || !token) return null;
|
|
233
|
+
return { serverUrl, token };
|
|
234
|
+
} catch {
|
|
235
|
+
return null;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
async function runBridgeFromUrl(url) {
|
|
239
|
+
const parsed = parseAhchatUrl(url);
|
|
240
|
+
if (!parsed) {
|
|
241
|
+
console.error("Invalid ahchat:// URL:", url);
|
|
242
|
+
console.error("Expected format: ahchat://bridge?server=ws://host:port/ws/bridge&token=xxx");
|
|
243
|
+
process.exit(1);
|
|
244
|
+
}
|
|
245
|
+
await runBridge({
|
|
246
|
+
serverUrl: parsed.serverUrl,
|
|
247
|
+
token: parsed.token
|
|
248
|
+
});
|
|
249
|
+
}
|
|
45
250
|
async function runBridge(args) {
|
|
46
251
|
let config = loadBridgeConfig();
|
|
47
252
|
if (args.serverUrl) {
|
|
@@ -60,7 +265,7 @@ async function runBridge(args) {
|
|
|
60
265
|
}
|
|
61
266
|
ensureDir(config.dataDir);
|
|
62
267
|
acquireLock(config.dataDir);
|
|
63
|
-
|
|
268
|
+
logger2.info("Bridge starting", {
|
|
64
269
|
bridgeId: config.bridgeId,
|
|
65
270
|
serverUrl: config.serverUrl,
|
|
66
271
|
serverApiUrl: config.serverApiUrl
|
|
@@ -101,21 +306,21 @@ async function runBridge(args) {
|
|
|
101
306
|
switch (msg.type) {
|
|
102
307
|
case "bridge:list_models_request": {
|
|
103
308
|
const { requestId } = msg.payload;
|
|
104
|
-
|
|
309
|
+
logger2.info("list_models request received", { requestId });
|
|
105
310
|
try {
|
|
106
311
|
const models = await listModels();
|
|
107
312
|
connector?.send({
|
|
108
313
|
type: "bridge:list_models_response",
|
|
109
314
|
payload: { requestId, models }
|
|
110
315
|
});
|
|
111
|
-
|
|
316
|
+
logger2.info("list_models response sent", { requestId, count: models.length });
|
|
112
317
|
} catch (e) {
|
|
113
318
|
const err = e instanceof Error ? e.message : String(e);
|
|
114
319
|
connector?.send({
|
|
115
320
|
type: "bridge:list_models_response",
|
|
116
321
|
payload: { requestId, error: err }
|
|
117
322
|
});
|
|
118
|
-
|
|
323
|
+
logger2.error("list_models failed", { requestId, error: e });
|
|
119
324
|
}
|
|
120
325
|
break;
|
|
121
326
|
}
|
|
@@ -123,7 +328,7 @@ async function runBridge(args) {
|
|
|
123
328
|
await agentManager.terminate(msg.payload.agentId);
|
|
124
329
|
break;
|
|
125
330
|
case "agent:terminate_scope":
|
|
126
|
-
|
|
331
|
+
logger2.info("agent:terminate_scope received", {
|
|
127
332
|
agentId: msg.payload.agentId,
|
|
128
333
|
scope: msg.payload.scope
|
|
129
334
|
});
|
|
@@ -140,7 +345,7 @@ async function runBridge(args) {
|
|
|
140
345
|
const p = msg.payload;
|
|
141
346
|
const answerText = formatAnswerForSDK(p);
|
|
142
347
|
const ok = askQuestionRegistry.resolve(p.questionId, answerText);
|
|
143
|
-
|
|
348
|
+
logger2.info("user:answer_question handled", {
|
|
144
349
|
questionId: p.questionId,
|
|
145
350
|
agentId: p.agentId,
|
|
146
351
|
resolved: ok,
|
|
@@ -161,7 +366,7 @@ async function runBridge(args) {
|
|
|
161
366
|
});
|
|
162
367
|
}, config.queryConfig.statusReportIntervalMs);
|
|
163
368
|
const shutdown = async (signal) => {
|
|
164
|
-
|
|
369
|
+
logger2.info("Shutdown signal received", { signal });
|
|
165
370
|
if (statusInterval) {
|
|
166
371
|
clearInterval(statusInterval);
|
|
167
372
|
statusInterval = null;
|
|
@@ -169,7 +374,7 @@ async function runBridge(args) {
|
|
|
169
374
|
wsMetrics.stop();
|
|
170
375
|
connector?.close();
|
|
171
376
|
await agentManager.shutdownAll();
|
|
172
|
-
|
|
377
|
+
logger2.info("Bridge stopped");
|
|
173
378
|
process.exit(0);
|
|
174
379
|
};
|
|
175
380
|
process.on("SIGINT", () => void shutdown("SIGINT"));
|
package/dist/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fangyb/ahchat-bridge",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
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",
|
|
@@ -30,7 +30,8 @@
|
|
|
30
30
|
"vitest": "^3.1.4"
|
|
31
31
|
},
|
|
32
32
|
"files": [
|
|
33
|
-
"dist"
|
|
33
|
+
"dist",
|
|
34
|
+
"README.md"
|
|
34
35
|
],
|
|
35
36
|
"engines": {
|
|
36
37
|
"node": ">=20"
|