@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.
- package/README.md +81 -0
- package/dist/cli/index.js +831 -81
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +726 -59
- package/dist/lib/daemon.d.ts +123 -0
- package/dist/lib/daemon.d.ts.map +1 -0
- package/dist/lib/doctor.d.ts.map +1 -1
- package/dist/lib/telegram.d.ts +6 -0
- package/dist/lib/telegram.d.ts.map +1 -1
- package/dist/mcp/index.js +273 -35
- package/docs/architecture.md +31 -0
- package/package.json +1 -1
|
@@ -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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"}
|
package/dist/lib/doctor.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"doctor.d.ts","sourceRoot":"","sources":["../../src/lib/doctor.ts"],"names":[],"mappings":"
|
|
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"}
|
package/dist/lib/telegram.d.ts
CHANGED
|
@@ -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;
|
|
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/
|
|
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/
|
|
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
|
|
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 (
|
|
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
|
-
|
|
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];
|
package/docs/architecture.md
CHANGED
|
@@ -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.
|