@fangyb/ahchat-bridge 0.1.4 → 0.1.5

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 CHANGED
@@ -1,3 +1,4 @@
1
+ #!/usr/bin/env node
1
2
  import {
2
3
  AgentManager,
3
4
  AskQuestionRegistry,
package/package.json CHANGED
@@ -1,42 +1,33 @@
1
1
  {
2
2
  "name": "@fangyb/ahchat-bridge",
3
- "version": "0.1.4",
4
- "description": "AHChat Bridge CLI — connect your local Claude Code agents to an AHChat server",
3
+ "version": "0.1.5",
4
+ "files": ["dist"],
5
5
  "type": "module",
6
- "main": "dist/index.js",
6
+ "main": "src/index.ts",
7
+ "types": "src/index.ts",
7
8
  "bin": {
8
9
  "ahchat-bridge": "./dist/cli.js"
9
10
  },
10
11
  "scripts": {
11
- "build": "tsup src/index.ts src/cli.ts --format esm --clean --shims",
12
12
  "dev": "tsx src/index.ts",
13
13
  "start": "tsx src/index.ts",
14
+ "build": "tsup",
14
15
  "typecheck": "tsc -p tsconfig.json --noEmit"
15
16
  },
16
17
  "dependencies": {
18
+ "@ahchat/logger": "workspace:*",
19
+ "@ahchat/shared": "workspace:*",
17
20
  "@anthropic-ai/claude-agent-sdk": "^0.2",
18
- "cac": "^6.7.14",
21
+ "cac": "^3.3.3",
19
22
  "ws": "^8.18.0",
20
23
  "zod": "^4.0.0"
21
24
  },
22
25
  "devDependencies": {
23
- "@ahchat/logger": "workspace:*",
24
- "@ahchat/shared": "workspace:*",
25
26
  "@types/node": "^22.15.21",
26
27
  "@types/ws": "^8.5.14",
27
- "tsup": "^8.3.5",
28
+ "tsup": "^8.0.0",
28
29
  "tsx": "^4.19.4",
29
30
  "typescript": "^5.8.3",
30
31
  "vitest": "^3.1.4"
31
- },
32
- "files": [
33
- "dist",
34
- "README.md"
35
- ],
36
- "engines": {
37
- "node": ">=20"
38
- },
39
- "publishConfig": {
40
- "access": "public"
41
32
  }
42
33
  }
package/src/index.ts ADDED
@@ -0,0 +1,156 @@
1
+ import type { WSMessage } from '@ahchat/shared'
2
+
3
+ import { AgentManager } from './agentManager'
4
+ import { AskQuestionRegistry } from './askQuestionRegistry'
5
+ import { formatAnswerForSDK } from './askUserQuestionGuard'
6
+ import { HttpAgentRegistry } from './agentRegistry'
7
+ import { GroupRegistry } from './groupRegistry'
8
+ import { loadBridgeConfig, ensureDir } from './config'
9
+ import { ServerConnector } from './connector'
10
+ import { listModels } from './modelQuerier'
11
+ import { acquireLock } from './lockfile'
12
+ import { createModuleLogger } from './logger'
13
+ import { createGroupTaskDispatchHandler, createTaskDispatchHandler } from './messageHandler'
14
+ import { SessionStore } from './sessionStore'
15
+ import { wsMetrics } from './wsMetrics'
16
+
17
+ const logger = createModuleLogger('bridge')
18
+
19
+ async function main(): Promise<void> {
20
+ const config = loadBridgeConfig()
21
+ ensureDir(config.dataDir)
22
+
23
+ acquireLock(config.dataDir)
24
+
25
+ logger.info('Bridge starting', {
26
+ bridgeId: config.bridgeId,
27
+ serverUrl: config.serverUrl,
28
+ serverApiUrl: config.serverApiUrl,
29
+ })
30
+
31
+ wsMetrics.start(5_000)
32
+
33
+ const sessionStore = new SessionStore(config.dataDir)
34
+ const agentRegistry = new HttpAgentRegistry(config.serverApiUrl)
35
+ const groupRegistry = new GroupRegistry(config.serverApiUrl)
36
+ await agentRegistry.refresh()
37
+ await groupRegistry.refresh()
38
+
39
+ let connector: ServerConnector | null = null
40
+
41
+ const emit = (msg: WSMessage): void => {
42
+ connector?.send(msg)
43
+ }
44
+
45
+ const askQuestionRegistry = new AskQuestionRegistry()
46
+ const agentManager = new AgentManager(sessionStore, emit, {
47
+ queryConfig: config.queryConfig,
48
+ askQuestionRegistry,
49
+ groupRegistry,
50
+ })
51
+
52
+ const taskDispatchHandler = createTaskDispatchHandler(agentManager, agentRegistry, emit)
53
+ const groupTaskDispatchHandler = createGroupTaskDispatchHandler(agentManager, agentRegistry, emit)
54
+
55
+ let statusInterval: ReturnType<typeof setInterval> | null = null
56
+
57
+ connector = new ServerConnector({
58
+ config,
59
+ agentIds: () => agentRegistry.getAll().map((a) => a.id),
60
+ onTaskDispatch: taskDispatchHandler,
61
+ onGroupTaskDispatch: groupTaskDispatchHandler,
62
+ onStopGeneration: async (payload) => {
63
+ await agentManager.cancelReply(payload)
64
+ },
65
+ onConnected: async () => {
66
+ await agentRegistry.refresh()
67
+ await groupRegistry.refresh()
68
+ await agentManager.recoverFromRestart(agentRegistry.getAll())
69
+ },
70
+ onServerPush: async (msg) => {
71
+ switch (msg.type) {
72
+ case 'bridge:list_models_request': {
73
+ const { requestId } = msg.payload
74
+ logger.info('list_models request received', { requestId })
75
+ try {
76
+ const models = await listModels()
77
+ connector?.send({
78
+ type: 'bridge:list_models_response',
79
+ payload: { requestId, models },
80
+ })
81
+ logger.info('list_models response sent', { requestId, count: models.length })
82
+ } catch (e) {
83
+ const err = e instanceof Error ? e.message : String(e)
84
+ connector?.send({
85
+ type: 'bridge:list_models_response',
86
+ payload: { requestId, error: err },
87
+ })
88
+ logger.error('list_models failed', { requestId, error: e })
89
+ }
90
+ break
91
+ }
92
+ case 'agent:terminate':
93
+ await agentManager.terminate(msg.payload.agentId)
94
+ break
95
+ case 'agent:terminate_scope':
96
+ logger.info('agent:terminate_scope received', {
97
+ agentId: msg.payload.agentId,
98
+ scope: msg.payload.scope,
99
+ })
100
+ await agentManager.terminateScope(msg.payload.agentId, msg.payload.scope)
101
+ break
102
+ case 'agent:created':
103
+ case 'agent:updated':
104
+ agentRegistry.upsert(msg.payload.agent)
105
+ break
106
+ case 'agent:deleted':
107
+ agentRegistry.remove(msg.payload.agentId)
108
+ break
109
+ case 'user:answer_question': {
110
+ const p = msg.payload
111
+ const answerText = formatAnswerForSDK(p)
112
+ const ok = askQuestionRegistry.resolve(p.questionId, answerText)
113
+ logger.info('user:answer_question handled', {
114
+ questionId: p.questionId,
115
+ agentId: p.agentId,
116
+ resolved: ok,
117
+ traceId: p.traceId,
118
+ })
119
+ break
120
+ }
121
+ default:
122
+ break
123
+ }
124
+ },
125
+ })
126
+
127
+ connector.connect()
128
+
129
+ statusInterval = setInterval(() => {
130
+ if (!connector?.isConnected) return
131
+ void agentRegistry.refresh().then(() => {
132
+ connector?.send(agentManager.getQueryStatus(config.bridgeId))
133
+ })
134
+ }, config.queryConfig.statusReportIntervalMs)
135
+
136
+ const shutdown = async (signal: string) => {
137
+ logger.info('Shutdown signal received', { signal })
138
+ if (statusInterval) {
139
+ clearInterval(statusInterval)
140
+ statusInterval = null
141
+ }
142
+ wsMetrics.stop()
143
+ connector?.close()
144
+ await agentManager.shutdownAll()
145
+ logger.info('Bridge stopped')
146
+ process.exit(0)
147
+ }
148
+
149
+ process.on('SIGINT', () => void shutdown('SIGINT'))
150
+ process.on('SIGTERM', () => void shutdown('SIGTERM'))
151
+ }
152
+
153
+ void main().catch((e) => {
154
+ logger.error('Bridge failed to start', { error: e })
155
+ process.exit(1)
156
+ })
package/README.md DELETED
@@ -1,197 +0,0 @@
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.