@fangyb/ahchat-bridge 0.1.3 → 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.
@@ -1,15 +1,8 @@
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
-
8
1
  // src/config.ts
9
2
  import crypto from "crypto";
10
3
  import fs from "fs";
11
4
  import os from "os";
12
- import path2 from "path";
5
+ import path from "path";
13
6
  var DEFAULT_QUERY_CONFIG = {
14
7
  maxActive: 50,
15
8
  idleTimeoutMs: 6e4,
@@ -63,9 +56,9 @@ function mergeQueryConfig(file) {
63
56
  function loadBridgeConfig() {
64
57
  const dataDir = readEnvString(
65
58
  "AHCHAT_DATA_DIR",
66
- path2.join(os.homedir(), ".ahchat")
59
+ path.join(os.homedir(), ".ahchat")
67
60
  );
68
- const fileConfig = tryReadJsonConfig(path2.join(dataDir, "bridge.json"));
61
+ const fileConfig = tryReadJsonConfig(path.join(dataDir, "bridge.json"));
69
62
  return {
70
63
  serverUrl: readEnvString(
71
64
  "AHCHAT_BRIDGE_SERVER_URL",
@@ -82,7 +75,7 @@ function loadBridgeConfig() {
82
75
  dataDir,
83
76
  dbPath: readEnvString(
84
77
  "AHCHAT_DB_PATH",
85
- fileConfig.dbPath ?? path2.join(dataDir, "data.db")
78
+ fileConfig.dbPath ?? path.join(dataDir, "data.db")
86
79
  ),
87
80
  serverApiUrl: readEnvString(
88
81
  "AHCHAT_SERVER_API_URL",
@@ -97,7 +90,7 @@ function ensureDir(dirPath) {
97
90
 
98
91
  // src/logger.ts
99
92
  import os3 from "os";
100
- import path4 from "path";
93
+ import path3 from "path";
101
94
 
102
95
  // ../logger/src/types.ts
103
96
  var LOG_LEVEL_VALUE = {
@@ -717,7 +710,7 @@ function consoleTransport(opts) {
717
710
  }
718
711
 
719
712
  // ../logger/src/transports/file.ts
720
- import path3 from "path";
713
+ import path2 from "path";
721
714
 
722
715
  // ../../node_modules/.pnpm/rotating-file-stream@3.2.9/node_modules/rotating-file-stream/dist/esm/index.js
723
716
  import { exec } from "child_process";
@@ -762,11 +755,11 @@ var RotatingFileStream = class extends Writable {
762
755
  timeout;
763
756
  timeoutPromise;
764
757
  constructor(generator, options) {
765
- const { encoding, history, maxFiles, maxSize, path: path11 } = options;
758
+ const { encoding, history, maxFiles, maxSize, path: path10 } = options;
766
759
  super({ decodeStrings: true, defaultEncoding: encoding });
767
760
  this.createGzip = createGzip;
768
761
  this.exec = exec;
769
- this.filename = path11 + generator(null);
762
+ this.filename = path10 + generator(null);
770
763
  this.fsCreateReadStream = createReadStream;
771
764
  this.fsCreateWriteStream = createWriteStream;
772
765
  this.fsOpen = open;
@@ -778,7 +771,7 @@ var RotatingFileStream = class extends Writable {
778
771
  this.options = options;
779
772
  this.stdout = process.stdout;
780
773
  if (maxFiles || maxSize)
781
- options.history = path11 + (history ? history : this.generator(null) + ".txt");
774
+ options.history = path10 + (history ? history : this.generator(null) + ".txt");
782
775
  this.on("close", () => this.finished ? null : this.emit("finish"));
783
776
  this.on("finish", () => this.finished = this.clear());
784
777
  (async () => {
@@ -906,9 +899,9 @@ var RotatingFileStream = class extends Writable {
906
899
  return this.move();
907
900
  }
908
901
  async findName() {
909
- const { interval, path: path11, intervalBoundary } = this.options;
902
+ const { interval, path: path10, intervalBoundary } = this.options;
910
903
  for (let index = 1; index < 1e3; ++index) {
911
- const filename = path11 + this.generator(interval && intervalBoundary ? new Date(this.prev) : this.rotation, index);
904
+ const filename = path10 + this.generator(interval && intervalBoundary ? new Date(this.prev) : this.rotation, index);
912
905
  if (!await exists(filename))
913
906
  return filename;
914
907
  }
@@ -938,11 +931,11 @@ var RotatingFileStream = class extends Writable {
938
931
  return this.unlink(filename);
939
932
  }
940
933
  async classical() {
941
- const { compress, path: path11, rotate } = this.options;
934
+ const { compress, path: path10, rotate } = this.options;
942
935
  let rotatedName = "";
943
936
  for (let count = rotate; count > 0; --count) {
944
- const currName = path11 + this.generator(count);
945
- const prevName = count === 1 ? this.filename : path11 + this.generator(count - 1);
937
+ const currName = path10 + this.generator(count);
938
+ const prevName = count === 1 ? this.filename : path10 + this.generator(count - 1);
946
939
  if (!await exists(prevName))
947
940
  continue;
948
941
  if (!rotatedName)
@@ -1335,8 +1328,8 @@ function parseSize(maxSize) {
1335
1328
  }
1336
1329
  function fileTransport(opts) {
1337
1330
  const fmt = opts.formatter ?? jsonFormatter;
1338
- const dir = path3.dirname(opts.path);
1339
- const filename = path3.basename(opts.path);
1331
+ const dir = path2.dirname(opts.path);
1332
+ const filename = path2.basename(opts.path);
1340
1333
  const stream = createStream(filename, {
1341
1334
  path: dir,
1342
1335
  size: opts.rotate?.maxSize ? parseSize(opts.rotate.maxSize) : "50M",
@@ -1356,8 +1349,8 @@ function createLogger(config) {
1356
1349
  // src/logger.ts
1357
1350
  var bridgeConfig = loadBridgeConfig();
1358
1351
  var isTest = !!process.env["VITEST"];
1359
- var LOG_DIR = path4.join(os3.homedir(), ".ahchat", "logs");
1360
- var LOG_FILE = path4.join(LOG_DIR, "bridge.log");
1352
+ var LOG_DIR = path3.join(os3.homedir(), ".ahchat", "logs");
1353
+ var LOG_FILE = path3.join(LOG_DIR, "bridge.log");
1361
1354
  if (!isTest) ensureDir(LOG_DIR);
1362
1355
  function createModuleLogger(module) {
1363
1356
  const transports = [consoleTransport({ formatter: prettyFormatter })];
@@ -1755,7 +1748,7 @@ var wsMetrics = new WsMetrics();
1755
1748
  // src/agentManager.ts
1756
1749
  import fs2 from "fs/promises";
1757
1750
  import os4 from "os";
1758
- import path7 from "path";
1751
+ import path6 from "path";
1759
1752
 
1760
1753
  // src/inputController.ts
1761
1754
  var InputController = class {
@@ -1813,17 +1806,17 @@ var InputController = class {
1813
1806
  };
1814
1807
 
1815
1808
  // src/permissionGuard.ts
1816
- import path6 from "path";
1809
+ import path5 from "path";
1817
1810
 
1818
1811
  // ../shared/src/utils/pathSafety.ts
1819
- import path5 from "path";
1812
+ import path4 from "path";
1820
1813
  function isPathInside(parent, child) {
1821
- const resolvedParent = path5.resolve(parent);
1822
- const resolvedChild = path5.resolve(child);
1814
+ const resolvedParent = path4.resolve(parent);
1815
+ const resolvedChild = path4.resolve(child);
1823
1816
  if (resolvedParent === resolvedChild) return true;
1824
- const rel = path5.relative(resolvedParent, resolvedChild);
1817
+ const rel = path4.relative(resolvedParent, resolvedChild);
1825
1818
  if (rel === "") return true;
1826
- return !rel.startsWith("..") && !path5.isAbsolute(rel);
1819
+ return !rel.startsWith("..") && !path4.isAbsolute(rel);
1827
1820
  }
1828
1821
 
1829
1822
  // src/permissionGuard.ts
@@ -1838,7 +1831,7 @@ function makeCwdPermissionGuard(cwd, agentId, scope, log) {
1838
1831
  if (typeof raw !== "string" || raw.length === 0) {
1839
1832
  return { behavior: "allow" };
1840
1833
  }
1841
- const abs = path6.isAbsolute(raw) ? raw : path6.resolve(cwd, raw);
1834
+ const abs = path5.isAbsolute(raw) ? raw : path5.resolve(cwd, raw);
1842
1835
  if (isPathInside(cwd, abs)) {
1843
1836
  return { behavior: "allow" };
1844
1837
  }
@@ -2888,14 +2881,14 @@ var AgentManager = class {
2888
2881
  this.emit = emit;
2889
2882
  if (typeof options === "function") {
2890
2883
  this.queryFn = options;
2891
- this.workspacesDir = path7.join(os4.homedir(), ".ahchat", "workspaces");
2884
+ this.workspacesDir = path6.join(os4.homedir(), ".ahchat", "workspaces");
2892
2885
  this.queryConfig = DEFAULT_QUERY_CONFIG;
2893
2886
  this.askQuestionRegistry = new AskQuestionRegistry();
2894
2887
  this.neuralBusManager = new NeuralBusManager();
2895
2888
  this.groupRegistry = null;
2896
2889
  } else {
2897
2890
  this.queryFn = options?.queryFn ?? null;
2898
- this.workspacesDir = options?.workspacesDir ?? path7.join(os4.homedir(), ".ahchat", "workspaces");
2891
+ this.workspacesDir = options?.workspacesDir ?? path6.join(os4.homedir(), ".ahchat", "workspaces");
2899
2892
  this.queryConfig = options?.queryConfig ?? DEFAULT_QUERY_CONFIG;
2900
2893
  this.askQuestionRegistry = options?.askQuestionRegistry ?? new AskQuestionRegistry();
2901
2894
  this.neuralBusManager = options?.neuralBusManager ?? new NeuralBusManager();
@@ -3364,7 +3357,7 @@ ${relay.message}`,
3364
3357
  status: existingProc.status
3365
3358
  });
3366
3359
  } else {
3367
- const cwd = agentConfig.workingDirectory || path7.join(this.workspacesDir, agentConfig.id);
3360
+ const cwd = agentConfig.workingDirectory || path6.join(this.workspacesDir, agentConfig.id);
3368
3361
  void this.acquire(agentConfig, scope, cwd).then((proc) => {
3369
3362
  if (suppressEmit) proc.internalRelayId = suppressEmit;
3370
3363
  return this.sendMessage({ ...task, agentId: agentConfig.id, scope });
@@ -3499,7 +3492,7 @@ ${relay.message}`,
3499
3492
  break;
3500
3493
  }
3501
3494
  try {
3502
- const cwd = agent.workingDirectory || path7.join(this.workspacesDir, agent.id);
3495
+ const cwd = agent.workingDirectory || path6.join(this.workspacesDir, agent.id);
3503
3496
  await this.acquire(agent, { kind: "single" }, cwd);
3504
3497
  warmed++;
3505
3498
  logger8.info("Agent process pre-created for recovery", { agentId: agent.id });
@@ -3695,8 +3688,8 @@ var HttpAgentRegistry = class {
3695
3688
  agents = /* @__PURE__ */ new Map();
3696
3689
  apiUrl(suffix) {
3697
3690
  const base = this.serverApiUrl.replace(/\/$/, "");
3698
- const path11 = suffix.startsWith("/") ? suffix : `/${suffix}`;
3699
- return `${base}${path11}`;
3691
+ const path10 = suffix.startsWith("/") ? suffix : `/${suffix}`;
3692
+ return `${base}${path10}`;
3700
3693
  }
3701
3694
  async refresh() {
3702
3695
  try {
@@ -4081,11 +4074,11 @@ var ServerConnector = class {
4081
4074
  // src/modelQuerier.ts
4082
4075
  import fs3 from "fs/promises";
4083
4076
  import os5 from "os";
4084
- import path8 from "path";
4077
+ import path7 from "path";
4085
4078
  var logger12 = createModuleLogger("bridge.modelQuerier");
4086
4079
  async function listModels(queryFn, opts = {}) {
4087
4080
  const t0 = Date.now();
4088
- const cwd = opts.cwd ?? path8.join(os5.homedir(), ".ahchat", "workspaces", "_list_models");
4081
+ const cwd = opts.cwd ?? path7.join(os5.homedir(), ".ahchat", "workspaces", "_list_models");
4089
4082
  await fs3.mkdir(cwd, { recursive: true });
4090
4083
  const fn = queryFn ?? (await import("@anthropic-ai/claude-agent-sdk")).query;
4091
4084
  const ic = new InputController();
@@ -4144,7 +4137,7 @@ async function listModels(queryFn, opts = {}) {
4144
4137
 
4145
4138
  // src/lockfile.ts
4146
4139
  import fs4 from "fs";
4147
- import path9 from "path";
4140
+ import path8 from "path";
4148
4141
  var logger13 = createModuleLogger("bridge.lockfile");
4149
4142
  var lockPath = null;
4150
4143
  function isProcessAlive(pid) {
@@ -4158,7 +4151,7 @@ function isProcessAlive(pid) {
4158
4151
  }
4159
4152
  }
4160
4153
  function acquireLock(dataDir) {
4161
- const file = path9.join(dataDir, "bridge.lock");
4154
+ const file = path8.join(dataDir, "bridge.lock");
4162
4155
  lockPath = file;
4163
4156
  if (fs4.existsSync(file)) {
4164
4157
  const raw = fs4.readFileSync(file, "utf-8").trim();
@@ -4170,7 +4163,7 @@ function acquireLock(dataDir) {
4170
4163
  logger13.warn("Removing stale bridge.lock (process not found)", { pid, path: file });
4171
4164
  }
4172
4165
  }
4173
- fs4.mkdirSync(path9.dirname(file), { recursive: true });
4166
+ fs4.mkdirSync(path8.dirname(file), { recursive: true });
4174
4167
  fs4.writeFileSync(file, String(process.pid), "utf-8");
4175
4168
  logger13.info("Acquired bridge lock", { path: file, pid: process.pid });
4176
4169
  const release = () => {
@@ -4438,13 +4431,13 @@ function createGroupTaskDispatchHandler(agentManager, agentRegistry, emit) {
4438
4431
 
4439
4432
  // src/sessionStore.ts
4440
4433
  import fs5 from "fs";
4441
- import path10 from "path";
4434
+ import path9 from "path";
4442
4435
  var logger15 = createModuleLogger("session.store");
4443
4436
  var SessionStore = class {
4444
4437
  filePath;
4445
4438
  cache;
4446
4439
  constructor(dataDir) {
4447
- this.filePath = path10.join(dataDir, "sessions.json");
4440
+ this.filePath = path9.join(dataDir, "sessions.json");
4448
4441
  this.cache = this.loadFromDisk();
4449
4442
  }
4450
4443
  cacheKey(agentId, scope) {
@@ -4504,7 +4497,7 @@ var SessionStore = class {
4504
4497
  }
4505
4498
  saveToDisk() {
4506
4499
  try {
4507
- const dir = path10.dirname(this.filePath);
4500
+ const dir = path9.dirname(this.filePath);
4508
4501
  fs5.mkdirSync(dir, { recursive: true });
4509
4502
  fs5.writeFileSync(this.filePath, JSON.stringify(this.cache, null, 2), "utf-8");
4510
4503
  } catch (e) {
@@ -4514,7 +4507,6 @@ var SessionStore = class {
4514
4507
  };
4515
4508
 
4516
4509
  export {
4517
- __dirname,
4518
4510
  loadBridgeConfig,
4519
4511
  ensureDir,
4520
4512
  createModuleLogger,
package/dist/cli.js CHANGED
@@ -1,3 +1,4 @@
1
+ #!/usr/bin/env node
1
2
  import {
2
3
  AgentManager,
3
4
  AskQuestionRegistry,
@@ -5,7 +6,6 @@ import {
5
6
  HttpAgentRegistry,
6
7
  ServerConnector,
7
8
  SessionStore,
8
- __dirname,
9
9
  acquireLock,
10
10
  createGroupTaskDispatchHandler,
11
11
  createModuleLogger,
@@ -15,7 +15,7 @@ import {
15
15
  listModels,
16
16
  loadBridgeConfig,
17
17
  wsMetrics
18
- } from "./chunk-MO54RNR2.js";
18
+ } from "./chunk-7SODRWIG.js";
19
19
 
20
20
  // src/cli.ts
21
21
  import cac from "cac";
@@ -25,7 +25,10 @@ import { execSync } from "child_process";
25
25
  import fs from "fs";
26
26
  import os from "os";
27
27
  import path from "path";
28
+ import { fileURLToPath } from "url";
28
29
  var logger = createModuleLogger("bridge.protocol");
30
+ var __filename = fileURLToPath(import.meta.url);
31
+ var __dirname = path.dirname(__filename);
29
32
  function getBridgeExePath() {
30
33
  const pkgDir = path.resolve(__dirname, "..");
31
34
  return path.join(pkgDir, "dist", "cli.js");
@@ -43,13 +46,30 @@ function registerProtocolHandler() {
43
46
  }
44
47
  function registerWindows() {
45
48
  const exe = getBridgeExePath();
46
- const handler = `"${process.execPath}" "${exe}" launch --url "%1"`;
47
- const iconPath = process.execPath;
49
+ const nodeExe = process.execPath;
50
+ const ahchatDir = path.join(os.homedir(), ".ahchat");
51
+ const urlFilePath = path.join(ahchatDir, ".bridge-launch-url");
52
+ fs.mkdirSync(ahchatDir, { recursive: true });
53
+ const psScriptPath = path.join(ahchatDir, "launch-bridge.ps1");
54
+ const psContent = `param([string]$url)
55
+ if (-not $url) {
56
+ if (Test-Path '${urlFilePath}') {
57
+ $url = Get-Content '${urlFilePath}' -Raw
58
+ }
59
+ }
60
+ if (-not $url) {
61
+ Write-Error "No URL provided"
62
+ exit 1
63
+ }
64
+ & '${nodeExe}' '${exe}' launch --url $url
65
+ `;
66
+ fs.writeFileSync(psScriptPath, psContent);
67
+ const handler = `powershell -ExecutionPolicy Bypass -File "${psScriptPath}" -url "%1"`;
48
68
  const regCommands = [
49
69
  `REG ADD "HKCU\\Software\\Classes\\ahchat" /ve /d "URL:ahchat" /f`,
50
70
  `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`
71
+ `REG ADD "HKCU\\Software\\Classes\\ahchat\\DefaultIcon" /ve /d "${nodeExe}" /f`,
72
+ `REG ADD "HKCU\\Software\\Classes\\ahchat\\shell\\open\\command" /ve /t REG_SZ /d "${handler}" /f`
53
73
  ];
54
74
  for (const cmd of regCommands) {
55
75
  try {
@@ -59,7 +79,7 @@ function registerWindows() {
59
79
  throw new Error(`Failed to register protocol handler: ${cmd}`);
60
80
  }
61
81
  }
62
- logger.info("Windows protocol handler registered");
82
+ logger.info("Windows protocol handler registered", { psScriptPath });
63
83
  }
64
84
  function registerMacOS() {
65
85
  const appDir = path.join(os.homedir(), "Applications", "AHChatBridge.app");
@@ -132,6 +152,16 @@ function unregisterProtocolHandler() {
132
152
  if (platform === "win32") {
133
153
  try {
134
154
  execSync('REG DELETE "HKCU\\Software\\Classes\\ahchat" /f', { stdio: "pipe" });
155
+ const psScriptPath = path.join(os.homedir(), ".ahchat", "launch-bridge.ps1");
156
+ const urlFilePath = path.join(os.homedir(), ".ahchat", ".bridge-launch-url");
157
+ try {
158
+ fs.unlinkSync(psScriptPath);
159
+ } catch {
160
+ }
161
+ try {
162
+ fs.unlinkSync(urlFilePath);
163
+ } catch {
164
+ }
135
165
  logger.info("Windows protocol handler unregistered");
136
166
  } catch (e) {
137
167
  logger.warn("Failed to unregister Windows protocol handler", { error: e });
@@ -225,10 +255,15 @@ cli.version("0.1.0");
225
255
  cli.parse();
226
256
  function parseAhchatUrl(url) {
227
257
  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");
258
+ if (!url.startsWith("ahchat://")) return null;
259
+ const afterProtocol = url.slice("ahchat://".length);
260
+ const tildeIndex = afterProtocol.indexOf("~");
261
+ if (tildeIndex < 0) return null;
262
+ const rest = afterProtocol.slice(tildeIndex + 1);
263
+ const lastTilde = rest.lastIndexOf("~");
264
+ if (lastTilde < 0) return null;
265
+ const serverUrl = decodeURIComponent(rest.slice(0, lastTilde));
266
+ const token = decodeURIComponent(rest.slice(lastTilde + 1));
232
267
  if (!serverUrl || !token) return null;
233
268
  return { serverUrl, token };
234
269
  } catch {
package/dist/index.js CHANGED
@@ -14,7 +14,7 @@ import {
14
14
  listModels,
15
15
  loadBridgeConfig,
16
16
  wsMetrics
17
- } from "./chunk-MO54RNR2.js";
17
+ } from "./chunk-7SODRWIG.js";
18
18
 
19
19
  // src/index.ts
20
20
  var logger = createModuleLogger("bridge");
package/package.json CHANGED
@@ -1,42 +1,33 @@
1
1
  {
2
2
  "name": "@fangyb/ahchat-bridge",
3
- "version": "0.1.3",
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.