@hasna/bridge 0.1.1 → 0.1.2

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.
@@ -0,0 +1,123 @@
1
+ import type { BridgeConfig } from "../types.js";
2
+ export type DaemonSupervisor = "process" | "launchd" | "systemd";
3
+ export type DaemonSupervisorOption = DaemonSupervisor | "auto";
4
+ export interface DaemonPaths {
5
+ dir: string;
6
+ lockDir: string;
7
+ metadataFile: string;
8
+ stdoutLog: string;
9
+ stderrLog: string;
10
+ launchdPlist: string;
11
+ systemdUnit: string;
12
+ }
13
+ export interface DaemonMetadata {
14
+ version: 1;
15
+ supervisor: "process";
16
+ pid: number;
17
+ pgid?: number;
18
+ startedAt: string;
19
+ identity: {
20
+ command: string;
21
+ cwd: string;
22
+ configPath: string;
23
+ statePath: string;
24
+ daemonDir: string;
25
+ bridgeHome: string;
26
+ };
27
+ command: string[];
28
+ cwd: string;
29
+ configPath: string;
30
+ statePath: string;
31
+ intervalMs: number;
32
+ serveJson: boolean;
33
+ daemonDir: string;
34
+ bridgeHome: string;
35
+ stdoutLog: string;
36
+ stderrLog: string;
37
+ }
38
+ export interface DaemonStatus {
39
+ running: boolean;
40
+ stale: boolean;
41
+ supervisor: DaemonSupervisor;
42
+ pid?: number;
43
+ startedAt?: string;
44
+ uptimeSeconds?: number;
45
+ detail?: string;
46
+ installedDetail?: string;
47
+ metadata?: DaemonMetadata;
48
+ paths: DaemonPaths;
49
+ installed: {
50
+ launchd: boolean;
51
+ systemd: boolean;
52
+ };
53
+ telegramApiBase: {
54
+ overridden: boolean;
55
+ origin: string;
56
+ pathname: string;
57
+ error?: string;
58
+ };
59
+ }
60
+ export interface DaemonStartOptions {
61
+ supervisor?: DaemonSupervisorOption;
62
+ daemonDir?: string;
63
+ configPath?: string;
64
+ statePath?: string;
65
+ intervalMs?: number;
66
+ serveJson?: boolean;
67
+ }
68
+ export interface DaemonStopOptions {
69
+ supervisor?: DaemonSupervisorOption;
70
+ daemonDir?: string;
71
+ timeoutMs?: number;
72
+ force?: boolean;
73
+ }
74
+ export interface DaemonInstallOptions {
75
+ supervisor?: DaemonSupervisorOption;
76
+ daemonDir?: string;
77
+ configPath?: string;
78
+ statePath?: string;
79
+ intervalMs?: number;
80
+ serveJson?: boolean;
81
+ }
82
+ export interface DaemonInstallResult {
83
+ supervisor: DaemonSupervisor;
84
+ path: string;
85
+ command: string[];
86
+ requiredEnv: string[];
87
+ warning?: string;
88
+ }
89
+ export declare function resolveSupervisor(supervisor?: DaemonSupervisorOption): DaemonSupervisor;
90
+ export declare function defaultDaemonDir(): string;
91
+ export declare function daemonPaths(daemonDir?: string): DaemonPaths;
92
+ export declare function ensureDaemonDir(dir?: string): Promise<DaemonPaths>;
93
+ export declare function requiredTelegramEnvVars(config: BridgeConfig): string[];
94
+ export declare function daemonStatus(options?: {
95
+ daemonDir?: string;
96
+ supervisor?: DaemonSupervisorOption;
97
+ }): Promise<DaemonStatus>;
98
+ export declare function startProcessDaemon(options?: DaemonStartOptions): Promise<DaemonStatus>;
99
+ export declare function stopProcessDaemon(options?: DaemonStopOptions): Promise<DaemonStatus>;
100
+ export declare function restartProcessDaemon(options?: DaemonStartOptions & DaemonStopOptions): Promise<DaemonStatus>;
101
+ export declare function renderLaunchdPlist(command: string[], paths: DaemonPaths): string;
102
+ export declare function renderSystemdUnit(command: string[], paths: DaemonPaths): string;
103
+ export declare function installDaemon(options?: DaemonInstallOptions): Promise<DaemonInstallResult>;
104
+ export declare function startInstalledDaemon(options?: DaemonInstallOptions): Promise<DaemonInstallResult>;
105
+ export declare function stopInstalledDaemon(options?: DaemonStopOptions): Promise<void>;
106
+ export declare function restartInstalledDaemon(options?: DaemonInstallOptions & DaemonStopOptions): Promise<DaemonInstallResult | DaemonStatus>;
107
+ export declare function uninstallDaemon(options?: {
108
+ supervisor?: DaemonSupervisorOption;
109
+ daemonDir?: string;
110
+ }): Promise<{
111
+ supervisor: DaemonSupervisor;
112
+ removed: string[];
113
+ }>;
114
+ export declare function tailFile(path: string, lines: number): Promise<string>;
115
+ export declare function daemonLogs(options?: {
116
+ daemonDir?: string;
117
+ lines?: number;
118
+ }): Promise<{
119
+ stdout: string;
120
+ stderr: string;
121
+ paths: DaemonPaths;
122
+ }>;
123
+ //# sourceMappingURL=daemon.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"daemon.d.ts","sourceRoot":"","sources":["../../src/lib/daemon.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"doctor.d.ts","sourceRoot":"","sources":["../../src/lib/doctor.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAe,YAAY,EAAE,MAAM,aAAa,CAAC;AA0B7D,wBAAsB,MAAM,CAAC,UAAU,SAAsB,EAAE,SAAS,SAAqB,GAAG,OAAO,CAAC,YAAY,CAAC,CAuCpH"}
1
+ {"version":3,"file":"doctor.d.ts","sourceRoot":"","sources":["../../src/lib/doctor.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAe,YAAY,EAAE,MAAM,aAAa,CAAC;AAsC7D,wBAAsB,MAAM,CAAC,UAAU,SAAsB,EAAE,SAAS,SAAqB,GAAG,OAAO,CAAC,YAAY,CAAC,CA+DpH"}
@@ -17,6 +17,12 @@ export interface TelegramUpdate {
17
17
  date?: number;
18
18
  };
19
19
  }
20
+ export interface TelegramApiBaseInfo {
21
+ overridden: boolean;
22
+ origin: string;
23
+ pathname: string;
24
+ }
25
+ export declare function telegramApiBaseInfo(): TelegramApiBaseInfo;
20
26
  export declare function telegramToken(channel: TelegramChannelConfig): string;
21
27
  export declare function telegramChatAllowed(channel: TelegramChannelConfig, chatId: string | undefined): boolean;
22
28
  export declare function sendTelegramMessage(token: string, chatId: string, text: string): Promise<unknown>;
@@ -1 +1 @@
1
- {"version":3,"file":"telegram.d.ts","sourceRoot":"","sources":["../../src/lib/telegram.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,qBAAqB,EAAE,MAAM,aAAa,CAAC;AAExE,MAAM,WAAW,cAAc;IAC7B,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE;QACR,UAAU,EAAE,MAAM,CAAC;QACnB,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,IAAI,EAAE;YAAE,EAAE,EAAE,MAAM,GAAG,MAAM,CAAC;YAAC,IAAI,CAAC,EAAE,MAAM,CAAC;YAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;SAAE,CAAC;QAChE,IAAI,CAAC,EAAE;YAAE,EAAE,EAAE,MAAM,GAAG,MAAM,CAAC;YAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;YAAC,UAAU,CAAC,EAAE,MAAM,CAAA;SAAE,CAAC;QACvE,IAAI,CAAC,EAAE,MAAM,CAAC;KACf,CAAC;CACH;AAED,wBAAgB,aAAa,CAAC,OAAO,EAAE,qBAAqB,GAAG,MAAM,CAKpE;AAED,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,qBAAqB,EAAE,MAAM,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,CAIvG;AAED,wBAAsB,mBAAmB,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CASvG;AAED,wBAAsB,kBAAkB,CACtC,KAAK,EAAE,MAAM,EACb,OAAO,GAAE;IAAE,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,cAAc,CAAC,EAAE,MAAM,CAAA;CAAO,GACzD,OAAO,CAAC,cAAc,EAAE,CAAC,CAU3B;AAED,wBAAgB,uBAAuB,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,cAAc,GAAG,aAAa,GAAG,SAAS,CAa5G"}
1
+ {"version":3,"file":"telegram.d.ts","sourceRoot":"","sources":["../../src/lib/telegram.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,qBAAqB,EAAE,MAAM,aAAa,CAAC;AAExE,MAAM,WAAW,cAAc;IAC7B,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE;QACR,UAAU,EAAE,MAAM,CAAC;QACnB,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,IAAI,EAAE;YAAE,EAAE,EAAE,MAAM,GAAG,MAAM,CAAC;YAAC,IAAI,CAAC,EAAE,MAAM,CAAC;YAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;SAAE,CAAC;QAChE,IAAI,CAAC,EAAE;YAAE,EAAE,EAAE,MAAM,GAAG,MAAM,CAAC;YAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;YAAC,UAAU,CAAC,EAAE,MAAM,CAAA;SAAE,CAAC;QACvE,IAAI,CAAC,EAAE,MAAM,CAAC;KACf,CAAC;CACH;AAID,MAAM,WAAW,mBAAmB;IAClC,UAAU,EAAE,OAAO,CAAC;IACpB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;CAClB;AAiBD,wBAAgB,mBAAmB,IAAI,mBAAmB,CAOzD;AAUD,wBAAgB,aAAa,CAAC,OAAO,EAAE,qBAAqB,GAAG,MAAM,CAKpE;AAED,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,qBAAqB,EAAE,MAAM,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,CAIvG;AAED,wBAAsB,mBAAmB,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CASvG;AAED,wBAAsB,kBAAkB,CACtC,KAAK,EAAE,MAAM,EACb,OAAO,GAAE;IAAE,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,cAAc,CAAC,EAAE,MAAM,CAAA;CAAO,GACzD,OAAO,CAAC,cAAc,EAAE,CAAC,CAU3B;AAED,wBAAgB,uBAAuB,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,cAAc,GAAG,aAAa,GAAG,SAAS,CAa5G"}
package/dist/mcp/index.js CHANGED
@@ -4225,8 +4225,9 @@ async function loadConfig(configPath = defaultConfigPath()) {
4225
4225
  throw err;
4226
4226
  }
4227
4227
  }
4228
- // src/lib/doctor.ts
4229
- import { stat } from "fs/promises";
4228
+ // src/lib/daemon.ts
4229
+ import { chmod as chmod2, mkdir as mkdir2, readFile as readFile2, rename, rm, rmdir, stat, writeFile as writeFile2 } from "fs/promises";
4230
+ import { dirname as dirname2, join as join3, resolve } from "path";
4230
4231
 
4231
4232
  // src/lib/state.ts
4232
4233
  import { dirname, join as join2 } from "path";
@@ -4234,18 +4235,261 @@ function defaultStatePath() {
4234
4235
  return process.env["BRIDGE_STATE"] || join2(bridgeHome(), "state.json");
4235
4236
  }
4236
4237
 
4237
- // src/lib/doctor.ts
4238
+ // src/lib/telegram.ts
4239
+ var DEFAULT_TELEGRAM_API_BASE = "https://api.telegram.org";
4240
+ function telegramApiBase() {
4241
+ const raw = process.env["BRIDGE_TELEGRAM_API_BASE"] || DEFAULT_TELEGRAM_API_BASE;
4242
+ const parsed = new URL(raw);
4243
+ if (!["http:", "https:"].includes(parsed.protocol)) {
4244
+ throw new Error("BRIDGE_TELEGRAM_API_BASE must use http or https");
4245
+ }
4246
+ if (parsed.username || parsed.password) {
4247
+ throw new Error("BRIDGE_TELEGRAM_API_BASE must not contain credentials");
4248
+ }
4249
+ if (parsed.search || parsed.hash) {
4250
+ throw new Error("BRIDGE_TELEGRAM_API_BASE must not contain query strings or fragments");
4251
+ }
4252
+ return parsed;
4253
+ }
4254
+ function telegramApiBaseInfo() {
4255
+ const parsed = telegramApiBase();
4256
+ return {
4257
+ overridden: parsed.href.replace(/\/$/, "") !== DEFAULT_TELEGRAM_API_BASE,
4258
+ origin: parsed.origin,
4259
+ pathname: parsed.pathname
4260
+ };
4261
+ }
4262
+ function telegramMethodUrl(token, method) {
4263
+ const base = telegramApiBase();
4264
+ const prefix = base.pathname.replace(/\/$/, "");
4265
+ base.pathname = `${prefix}/bot${token}/${method}`;
4266
+ base.search = "";
4267
+ return base.toString();
4268
+ }
4269
+ function telegramToken(channel) {
4270
+ const envName = channel.botTokenEnv || "TELEGRAM_BOT_TOKEN";
4271
+ const token = process.env[envName];
4272
+ if (!token)
4273
+ throw new Error(`Missing Telegram bot token env var: ${envName}`);
4274
+ return token;
4275
+ }
4276
+ function telegramChatAllowed(channel, chatId) {
4277
+ if (channel.allowAllChats)
4278
+ return true;
4279
+ if (!channel.allowedChatIds?.length)
4280
+ return false;
4281
+ return Boolean(chatId && channel.allowedChatIds.includes(chatId));
4282
+ }
4283
+ async function sendTelegramMessage(token, chatId, text) {
4284
+ const response = await fetch(telegramMethodUrl(token, "sendMessage"), {
4285
+ method: "POST",
4286
+ headers: { "content-type": "application/json" },
4287
+ body: JSON.stringify({ chat_id: chatId, text })
4288
+ });
4289
+ const body = await response.json().catch(() => {
4290
+ return;
4291
+ });
4292
+ if (!response.ok)
4293
+ throw new Error(`Telegram sendMessage failed (${response.status}): ${JSON.stringify(body)}`);
4294
+ return body;
4295
+ }
4296
+
4297
+ // src/lib/daemon.ts
4238
4298
  function isNotFound(err) {
4239
4299
  return Boolean(err && typeof err === "object" && "code" in err && err.code === "ENOENT");
4240
4300
  }
4301
+ function currentPlatformSupervisor() {
4302
+ if (process.platform === "darwin")
4303
+ return "launchd";
4304
+ if (process.platform === "linux")
4305
+ return "systemd";
4306
+ return "process";
4307
+ }
4308
+ function resolveSupervisor(supervisor = "process") {
4309
+ return supervisor === "auto" ? currentPlatformSupervisor() : supervisor;
4310
+ }
4311
+ function defaultDaemonDir() {
4312
+ return join3(bridgeHome(), "daemon");
4313
+ }
4314
+ function daemonPaths(daemonDir = defaultDaemonDir()) {
4315
+ const dir = resolve(daemonDir);
4316
+ return {
4317
+ dir,
4318
+ lockDir: join3(dir, "lock"),
4319
+ metadataFile: join3(dir, "bridge-daemon.json"),
4320
+ stdoutLog: join3(dir, "bridge.out.log"),
4321
+ stderrLog: join3(dir, "bridge.err.log"),
4322
+ launchdPlist: join3(process.env["HOME"] || process.cwd(), "Library", "LaunchAgents", "com.hasna.bridge.plist"),
4323
+ systemdUnit: join3(process.env["HOME"] || process.cwd(), ".config", "systemd", "user", "hasna-bridge.service")
4324
+ };
4325
+ }
4326
+ async function fileExists(path) {
4327
+ try {
4328
+ await stat(path);
4329
+ return true;
4330
+ } catch (err) {
4331
+ if (isNotFound(err))
4332
+ return false;
4333
+ throw err;
4334
+ }
4335
+ }
4336
+ async function readMetadata(paths) {
4337
+ try {
4338
+ return JSON.parse(await readFile2(paths.metadataFile, "utf-8"));
4339
+ } catch (err) {
4340
+ if (isNotFound(err))
4341
+ return;
4342
+ throw err;
4343
+ }
4344
+ }
4345
+ function pidAlive(pid) {
4346
+ try {
4347
+ process.kill(pid, 0);
4348
+ return true;
4349
+ } catch {
4350
+ return false;
4351
+ }
4352
+ }
4353
+ async function processCommand(pid) {
4354
+ const proc = Bun.spawn(["ps", "-p", String(pid), "-o", "command="], {
4355
+ stdout: "pipe",
4356
+ stderr: "ignore"
4357
+ });
4358
+ if (await proc.exited !== 0)
4359
+ return;
4360
+ return (await new Response(proc.stdout).text()).trim();
4361
+ }
4362
+ async function processPgid(pid) {
4363
+ const proc = Bun.spawn(["ps", "-p", String(pid), "-o", "pgid="], {
4364
+ stdout: "pipe",
4365
+ stderr: "ignore"
4366
+ });
4367
+ if (await proc.exited !== 0)
4368
+ return;
4369
+ const parsed = Number.parseInt((await new Response(proc.stdout).text()).trim(), 10);
4370
+ return Number.isInteger(parsed) ? parsed : undefined;
4371
+ }
4372
+ async function processMatches(metadata) {
4373
+ if (!pidAlive(metadata.pid))
4374
+ return false;
4375
+ const command = await processCommand(metadata.pid);
4376
+ if (!command)
4377
+ return false;
4378
+ if (!metadata.pgid)
4379
+ return false;
4380
+ const pgid = await processPgid(metadata.pid);
4381
+ if (pgid !== metadata.pgid)
4382
+ return false;
4383
+ const requiredArgs = [
4384
+ metadata.command[1],
4385
+ "serve",
4386
+ "--config",
4387
+ metadata.configPath,
4388
+ "--state",
4389
+ metadata.statePath,
4390
+ "--interval",
4391
+ String(metadata.intervalMs)
4392
+ ].filter((arg) => Boolean(arg));
4393
+ if (metadata.serveJson)
4394
+ requiredArgs.push("--json");
4395
+ return requiredArgs.every((arg) => command.includes(arg));
4396
+ }
4397
+ function safeTelegramApiBaseInfo() {
4398
+ try {
4399
+ return telegramApiBaseInfo();
4400
+ } catch (err) {
4401
+ return {
4402
+ overridden: true,
4403
+ origin: "",
4404
+ pathname: "",
4405
+ error: err instanceof Error ? err.message : String(err)
4406
+ };
4407
+ }
4408
+ }
4409
+ async function runCapture(command) {
4410
+ const proc = Bun.spawn(command, { stdout: "pipe", stderr: "pipe" });
4411
+ const [exitCode, stdout, stderr] = await Promise.all([
4412
+ proc.exited,
4413
+ new Response(proc.stdout).text(),
4414
+ new Response(proc.stderr).text()
4415
+ ]);
4416
+ return { exitCode, stdout, stderr };
4417
+ }
4418
+ async function installedSupervisorStatus(supervisor, paths) {
4419
+ if (supervisor === "launchd") {
4420
+ if (!await fileExists(paths.launchdPlist))
4421
+ return { running: false, detail: "launchd plist not installed" };
4422
+ const uid = typeof process.getuid === "function" ? process.getuid() : undefined;
4423
+ if (uid === undefined)
4424
+ return { running: false, detail: "launchd status requires a numeric uid" };
4425
+ const result = await runCapture(["launchctl", "print", `gui/${uid}/com.hasna.bridge`]);
4426
+ if (result.exitCode !== 0)
4427
+ return { running: false, detail: result.stderr.trim() || result.stdout.trim() || "launchd service not loaded" };
4428
+ const running = /state\s*=\s*running/.test(result.stdout);
4429
+ return { running, detail: running ? "launchd running" : "launchd loaded but not running" };
4430
+ }
4431
+ if (supervisor === "systemd") {
4432
+ if (!await fileExists(paths.systemdUnit))
4433
+ return { running: false, detail: "systemd unit not installed" };
4434
+ const result = await runCapture(["systemctl", "--user", "is-active", "hasna-bridge.service"]);
4435
+ const state = result.stdout.trim() || result.stderr.trim() || "unknown";
4436
+ return { running: result.exitCode === 0 && state === "active", detail: `systemd ${state}` };
4437
+ }
4438
+ return { running: false, detail: "process supervisor has no installed status" };
4439
+ }
4440
+ async function daemonStatus(options = {}) {
4441
+ const supervisor = resolveSupervisor(options.supervisor);
4442
+ const paths = daemonPaths(options.daemonDir);
4443
+ const metadata = await readMetadata(paths);
4444
+ const live = metadata ? await processMatches(metadata) : false;
4445
+ const stale = Boolean(metadata && !live);
4446
+ const startedAt = metadata?.startedAt;
4447
+ const uptimeSeconds = live && startedAt ? Math.max(0, Math.floor((Date.now() - Date.parse(startedAt)) / 1000)) : undefined;
4448
+ const installed = {
4449
+ launchd: await fileExists(paths.launchdPlist),
4450
+ systemd: await fileExists(paths.systemdUnit)
4451
+ };
4452
+ const installedRuntime = supervisor === "process" ? undefined : await installedSupervisorStatus(supervisor, paths);
4453
+ return {
4454
+ running: installedRuntime ? installedRuntime.running : live,
4455
+ stale: installedRuntime ? false : stale,
4456
+ supervisor,
4457
+ pid: metadata?.pid,
4458
+ startedAt,
4459
+ uptimeSeconds,
4460
+ detail: installedRuntime?.detail || (stale ? "stale process metadata" : live ? "running" : "not running"),
4461
+ installedDetail: installedRuntime?.detail,
4462
+ metadata,
4463
+ paths,
4464
+ installed,
4465
+ telegramApiBase: safeTelegramApiBaseInfo()
4466
+ };
4467
+ }
4468
+ // src/lib/doctor.ts
4469
+ import { stat as stat2 } from "fs/promises";
4470
+ function isNotFound2(err) {
4471
+ return Boolean(err && typeof err === "object" && "code" in err && err.code === "ENOENT");
4472
+ }
4241
4473
  async function privateFileCheck(name, path) {
4242
4474
  try {
4243
- const info = await stat(path);
4475
+ const info = await stat2(path);
4244
4476
  const mode = info.mode & 511;
4245
4477
  const ok = (mode & 63) === 0;
4246
4478
  return { name, ok, detail: `${path} mode=${mode.toString(8)}` };
4247
4479
  } catch (err) {
4248
- if (isNotFound(err))
4480
+ if (isNotFound2(err))
4481
+ return { name, ok: true, detail: `not created yet: ${path}` };
4482
+ return { name, ok: false, detail: `${path}: ${err instanceof Error ? err.message : String(err)}` };
4483
+ }
4484
+ }
4485
+ async function privateDirCheck(name, path) {
4486
+ try {
4487
+ const info = await stat2(path);
4488
+ const mode = info.mode & 511;
4489
+ const ok = (mode & 63) === 0;
4490
+ return { name, ok, detail: `${path} mode=${mode.toString(8)}` };
4491
+ } catch (err) {
4492
+ if (isNotFound2(err))
4249
4493
  return { name, ok: true, detail: `not created yet: ${path}` };
4250
4494
  return { name, ok: false, detail: `${path}: ${err instanceof Error ? err.message : String(err)}` };
4251
4495
  }
@@ -4259,9 +4503,32 @@ async function commandExists(command) {
4259
4503
  }
4260
4504
  async function doctor(configPath = defaultConfigPath(), statePath = defaultStatePath()) {
4261
4505
  const checks = [];
4262
- let config = await loadConfig(configPath);
4506
+ const config = await loadConfig(configPath);
4507
+ const daemon = await daemonStatus();
4508
+ const paths = daemonPaths();
4263
4509
  checks.push(await privateFileCheck("config", configPath));
4264
4510
  checks.push(await privateFileCheck("state", statePath));
4511
+ checks.push(await privateDirCheck("daemon-dir", paths.dir));
4512
+ checks.push(await privateFileCheck("daemon-metadata", paths.metadataFile));
4513
+ checks.push({
4514
+ name: "daemon-status",
4515
+ ok: !daemon.stale,
4516
+ detail: daemon.running ? `running pid=${daemon.pid}` : daemon.stale ? `stale pid=${daemon.pid}` : "not running"
4517
+ });
4518
+ try {
4519
+ const apiBase = telegramApiBaseInfo();
4520
+ checks.push({
4521
+ name: "telegram-api-base",
4522
+ ok: true,
4523
+ detail: apiBase.overridden ? `overridden: ${apiBase.origin}${apiBase.pathname}` : apiBase.origin
4524
+ });
4525
+ } catch (err) {
4526
+ checks.push({
4527
+ name: "telegram-api-base",
4528
+ ok: false,
4529
+ detail: err instanceof Error ? err.message : String(err)
4530
+ });
4531
+ }
4265
4532
  for (const command of ["bridge", "codewith", "claude", "aicopilot"]) {
4266
4533
  checks.push({
4267
4534
  name: `command:${command}`,
@@ -4292,35 +4559,6 @@ async function doctor(configPath = defaultConfigPath(), statePath = defaultState
4292
4559
  }
4293
4560
  return { ok: checks.every((check) => check.ok), configPath, checks };
4294
4561
  }
4295
- // src/lib/telegram.ts
4296
- function telegramToken(channel) {
4297
- const envName = channel.botTokenEnv || "TELEGRAM_BOT_TOKEN";
4298
- const token = process.env[envName];
4299
- if (!token)
4300
- throw new Error(`Missing Telegram bot token env var: ${envName}`);
4301
- return token;
4302
- }
4303
- function telegramChatAllowed(channel, chatId) {
4304
- if (channel.allowAllChats)
4305
- return true;
4306
- if (!channel.allowedChatIds?.length)
4307
- return false;
4308
- return Boolean(chatId && channel.allowedChatIds.includes(chatId));
4309
- }
4310
- async function sendTelegramMessage(token, chatId, text) {
4311
- const response = await fetch(`https://api.telegram.org/bot${token}/sendMessage`, {
4312
- method: "POST",
4313
- headers: { "content-type": "application/json" },
4314
- body: JSON.stringify({ chat_id: chatId, text })
4315
- });
4316
- const body = await response.json().catch(() => {
4317
- return;
4318
- });
4319
- if (!response.ok)
4320
- throw new Error(`Telegram sendMessage failed (${response.status}): ${JSON.stringify(body)}`);
4321
- return body;
4322
- }
4323
-
4324
4562
  // src/lib/router.ts
4325
4563
  function matchingRoutes(config, message) {
4326
4564
  const channel = config.channels[message.channelId];
@@ -49,3 +49,34 @@ secrets are not exposed through `bridge_config`.
49
49
 
50
50
  Long-poll offsets are persisted in a private state file so process restarts do
51
51
  not replay already-seen updates and re-run agents.
52
+
53
+ `BRIDGE_TELEGRAM_API_BASE` can override the Telegram API origin for local tests.
54
+ The override accepts only `http` or `https` URLs without credentials. It is not
55
+ intended for normal production use because bot tokens are part of Telegram API
56
+ request paths.
57
+
58
+ ## Daemon Model
59
+
60
+ The foreground runtime remains `bridge serve`. Daemon commands are lifecycle
61
+ wrappers around that same runtime:
62
+
63
+ - `bridge daemon start` uses a local process supervisor by default.
64
+ - `bridge daemon status` reads private metadata and verifies the recorded
65
+ process still looks like `bridge serve`.
66
+ - `bridge daemon stop` terminates the process group and removes stale metadata.
67
+ - `bridge daemon logs` reads private stdout/stderr logs.
68
+ - `bridge daemon install` writes user `launchd` or user `systemd` files.
69
+
70
+ The process supervisor is the quickest local testing path because it inherits
71
+ the shell environment, including Telegram token env vars. Installed launchd and
72
+ systemd services do not store token values by default; operators must make token
73
+ env vars available through their service manager.
74
+
75
+ Daemon files live under `~/.hasna/bridge/daemon` by default. The directory is
76
+ `0700`; metadata and log files are `0600`. Logs are considered sensitive because
77
+ they can contain prompts, Telegram text, agent stdout/stderr, and routing
78
+ errors.
79
+
80
+ `bridge serve` handles per-channel poll errors without exiting in long-running
81
+ mode. `serve --once` still fails fast so health checks and tests can catch
82
+ misconfiguration.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/bridge",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "Agent messaging bridge for Telegram and other channels",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",