llm-cli-gateway 1.0.0
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/CHANGELOG.md +541 -0
- package/LICENSE +21 -0
- package/README.md +545 -0
- package/dist/approval-manager.d.ts +43 -0
- package/dist/approval-manager.js +156 -0
- package/dist/async-job-manager.d.ts +57 -0
- package/dist/async-job-manager.js +334 -0
- package/dist/claude-mcp-config.d.ts +8 -0
- package/dist/claude-mcp-config.js +161 -0
- package/dist/config.d.ts +35 -0
- package/dist/config.js +56 -0
- package/dist/db.d.ts +48 -0
- package/dist/db.js +170 -0
- package/dist/executor.d.ts +30 -0
- package/dist/executor.js +315 -0
- package/dist/health.d.ts +20 -0
- package/dist/health.js +32 -0
- package/dist/index.d.ts +67 -0
- package/dist/index.js +1503 -0
- package/dist/logger.d.ts +6 -0
- package/dist/logger.js +5 -0
- package/dist/metrics.d.ts +23 -0
- package/dist/metrics.js +57 -0
- package/dist/migrate-sessions.d.ts +12 -0
- package/dist/migrate-sessions.js +145 -0
- package/dist/migrate.d.ts +2 -0
- package/dist/migrate.js +100 -0
- package/dist/model-registry.d.ts +10 -0
- package/dist/model-registry.js +346 -0
- package/dist/optimizer.d.ts +3 -0
- package/dist/optimizer.js +183 -0
- package/dist/process-monitor.d.ts +54 -0
- package/dist/process-monitor.js +146 -0
- package/dist/request-helpers.d.ts +25 -0
- package/dist/request-helpers.js +32 -0
- package/dist/resources.d.ts +26 -0
- package/dist/resources.js +201 -0
- package/dist/retry.d.ts +72 -0
- package/dist/retry.js +146 -0
- package/dist/review-integrity.d.ts +50 -0
- package/dist/review-integrity.js +283 -0
- package/dist/session-manager-pg.d.ts +76 -0
- package/dist/session-manager-pg.js +383 -0
- package/dist/session-manager.d.ts +62 -0
- package/dist/session-manager.js +223 -0
- package/dist/stream-json-parser.d.ts +35 -0
- package/dist/stream-json-parser.js +94 -0
- package/package.json +90 -0
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync, renameSync, openSync, fsyncSync, closeSync, chmodSync } from "fs";
|
|
2
|
+
import { homedir } from "os";
|
|
3
|
+
import { dirname, join } from "path";
|
|
4
|
+
import { parse as parseToml } from "toml";
|
|
5
|
+
export const CLAUDE_MCP_SERVER_NAMES = ["sqry", "exa", "ref_tools", "trstr"];
|
|
6
|
+
function asStringArray(value) {
|
|
7
|
+
if (!Array.isArray(value)) {
|
|
8
|
+
return undefined;
|
|
9
|
+
}
|
|
10
|
+
const strings = value.filter((item) => typeof item === "string");
|
|
11
|
+
return strings.length === value.length ? strings : undefined;
|
|
12
|
+
}
|
|
13
|
+
function asStringRecord(value) {
|
|
14
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
15
|
+
return undefined;
|
|
16
|
+
}
|
|
17
|
+
const record = {};
|
|
18
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
19
|
+
if (typeof entry === "string" || typeof entry === "number" || typeof entry === "boolean") {
|
|
20
|
+
record[key] = String(entry);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return Object.keys(record).length > 0 ? record : undefined;
|
|
24
|
+
}
|
|
25
|
+
function readCodexServerConfig(server) {
|
|
26
|
+
const codexConfigPath = join(homedir(), ".codex", "config.toml");
|
|
27
|
+
if (!existsSync(codexConfigPath)) {
|
|
28
|
+
return {};
|
|
29
|
+
}
|
|
30
|
+
try {
|
|
31
|
+
const content = readFileSync(codexConfigPath, "utf-8");
|
|
32
|
+
const parsed = parseToml(content);
|
|
33
|
+
const mcpServers = parsed.mcp_servers;
|
|
34
|
+
if (!mcpServers || typeof mcpServers !== "object" || Array.isArray(mcpServers)) {
|
|
35
|
+
return {};
|
|
36
|
+
}
|
|
37
|
+
const serverConfig = mcpServers[server];
|
|
38
|
+
if (!serverConfig || typeof serverConfig !== "object" || Array.isArray(serverConfig)) {
|
|
39
|
+
return {};
|
|
40
|
+
}
|
|
41
|
+
const obj = serverConfig;
|
|
42
|
+
const command = typeof obj.command === "string" ? obj.command : undefined;
|
|
43
|
+
const args = asStringArray(obj.args);
|
|
44
|
+
const env = asStringRecord(obj.env);
|
|
45
|
+
return {
|
|
46
|
+
command,
|
|
47
|
+
args,
|
|
48
|
+
env
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return {};
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
function findInstalledExaEntrypoint() {
|
|
56
|
+
const nvmVersionsDir = join(homedir(), ".nvm", "versions", "node");
|
|
57
|
+
if (!existsSync(nvmVersionsDir)) {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
let versions = [];
|
|
61
|
+
try {
|
|
62
|
+
versions = readdirSync(nvmVersionsDir);
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
const candidates = [];
|
|
68
|
+
for (const version of versions) {
|
|
69
|
+
const entrypoint = join(nvmVersionsDir, version, "lib", "node_modules", "exa-mcp-server", ".smithery", "stdio", "index.cjs");
|
|
70
|
+
if (existsSync(entrypoint)) {
|
|
71
|
+
candidates.push(entrypoint);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
if (candidates.length === 0) {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
candidates.sort((a, b) => b.localeCompare(a, undefined, { numeric: true, sensitivity: "base" }));
|
|
78
|
+
return candidates[0];
|
|
79
|
+
}
|
|
80
|
+
function defaultServerDef(server) {
|
|
81
|
+
switch (server) {
|
|
82
|
+
case "sqry":
|
|
83
|
+
return { command: join(homedir(), ".local", "bin", "sqry-mcp"), args: [] };
|
|
84
|
+
case "trstr":
|
|
85
|
+
return { command: join(homedir(), ".local", "bin", "trstr-mcp"), args: [] };
|
|
86
|
+
case "exa": {
|
|
87
|
+
const exaEntrypoint = findInstalledExaEntrypoint();
|
|
88
|
+
if (exaEntrypoint) {
|
|
89
|
+
return { command: "node", args: [exaEntrypoint] };
|
|
90
|
+
}
|
|
91
|
+
return { command: "npx", args: ["-y", "exa-mcp-server"] };
|
|
92
|
+
}
|
|
93
|
+
case "ref_tools":
|
|
94
|
+
return { command: "npx", args: ["-y", "ref-tools-mcp"] };
|
|
95
|
+
default: {
|
|
96
|
+
const _exhaustive = server;
|
|
97
|
+
throw new Error(`Unknown MCP server: ${_exhaustive}`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
function toClaudeServerDef(server) {
|
|
102
|
+
const codexDef = readCodexServerConfig(server);
|
|
103
|
+
const fallback = defaultServerDef(server);
|
|
104
|
+
const command = codexDef.command || fallback.command;
|
|
105
|
+
const args = codexDef.args || fallback.args || [];
|
|
106
|
+
const env = {};
|
|
107
|
+
if (codexDef.env) {
|
|
108
|
+
Object.assign(env, codexDef.env);
|
|
109
|
+
}
|
|
110
|
+
if (server === "exa" && process.env.EXA_API_KEY) {
|
|
111
|
+
env.EXA_API_KEY = process.env.EXA_API_KEY;
|
|
112
|
+
}
|
|
113
|
+
if (server === "ref_tools" && process.env.REF_API_KEY) {
|
|
114
|
+
env.REF_API_KEY = process.env.REF_API_KEY;
|
|
115
|
+
}
|
|
116
|
+
// sqry should always be usable without env, but exa/ref_tools typically need credentials.
|
|
117
|
+
if ((server === "exa" && !env.EXA_API_KEY) || (server === "ref_tools" && !env.REF_API_KEY)) {
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
return {
|
|
121
|
+
command,
|
|
122
|
+
args,
|
|
123
|
+
...(Object.keys(env).length > 0 ? { env } : {})
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
export function buildClaudeMcpConfig(servers) {
|
|
127
|
+
const uniqueServers = [...new Set(servers)];
|
|
128
|
+
const enabled = [];
|
|
129
|
+
const missing = [];
|
|
130
|
+
const mcpServers = {};
|
|
131
|
+
for (const server of uniqueServers) {
|
|
132
|
+
const def = toClaudeServerDef(server);
|
|
133
|
+
if (!def) {
|
|
134
|
+
missing.push(server);
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
mcpServers[server] = def;
|
|
138
|
+
enabled.push(server);
|
|
139
|
+
}
|
|
140
|
+
const configPath = join(homedir(), ".llm-cli-gateway", "claude-mcp.generated.json");
|
|
141
|
+
const configDir = dirname(configPath);
|
|
142
|
+
try {
|
|
143
|
+
mkdirSync(configDir, { recursive: true });
|
|
144
|
+
const tempPath = `${configPath}.tmp.${process.pid}`;
|
|
145
|
+
writeFileSync(tempPath, JSON.stringify({ mcpServers }, null, 2), { encoding: "utf-8", mode: 0o600 });
|
|
146
|
+
const fd = openSync(tempPath, "r+");
|
|
147
|
+
try {
|
|
148
|
+
fsyncSync(fd);
|
|
149
|
+
}
|
|
150
|
+
finally {
|
|
151
|
+
closeSync(fd);
|
|
152
|
+
}
|
|
153
|
+
renameSync(tempPath, configPath);
|
|
154
|
+
chmodSync(configPath, 0o600);
|
|
155
|
+
}
|
|
156
|
+
catch (error) {
|
|
157
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
158
|
+
throw new Error(`Failed to write Claude MCP config: ${message}`);
|
|
159
|
+
}
|
|
160
|
+
return { path: configPath, enabled, missing };
|
|
161
|
+
}
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export interface CacheTtl {
|
|
2
|
+
session: number;
|
|
3
|
+
activeSession: number;
|
|
4
|
+
sessionList: number;
|
|
5
|
+
}
|
|
6
|
+
export interface DatabaseConfig {
|
|
7
|
+
connectionString: string;
|
|
8
|
+
pool: {
|
|
9
|
+
max: number;
|
|
10
|
+
idleTimeoutMillis: number;
|
|
11
|
+
connectionTimeoutMillis: number;
|
|
12
|
+
statementTimeout: number;
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
export interface RedisConfig {
|
|
16
|
+
url: string;
|
|
17
|
+
retryStrategy: {
|
|
18
|
+
maxRetries: number;
|
|
19
|
+
initialDelay: number;
|
|
20
|
+
maxDelay: number;
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
export declare const DEFAULT_SESSION_TTL_SECONDS = 2592000;
|
|
24
|
+
export interface Config {
|
|
25
|
+
database?: DatabaseConfig;
|
|
26
|
+
redis?: RedisConfig;
|
|
27
|
+
cacheTtl: CacheTtl;
|
|
28
|
+
sessionTtl: number;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Load configuration from environment variables.
|
|
32
|
+
* Always returns a Config object with base fields (cacheTtl, sessionTtl).
|
|
33
|
+
* Database and Redis fields are populated only when both env vars are set.
|
|
34
|
+
*/
|
|
35
|
+
export declare function loadConfig(): Config;
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
// Zod schemas for configuration validation
|
|
3
|
+
const DatabaseUrlSchema = z.string().url().refine((url) => url.startsWith("postgresql://") || url.startsWith("postgres://"), { message: "Database URL must start with postgresql:// or postgres://" });
|
|
4
|
+
const RedisUrlSchema = z.string().url().startsWith("redis://");
|
|
5
|
+
export const DEFAULT_SESSION_TTL_SECONDS = 2592000; // 30 days
|
|
6
|
+
/**
|
|
7
|
+
* Load configuration from environment variables.
|
|
8
|
+
* Always returns a Config object with base fields (cacheTtl, sessionTtl).
|
|
9
|
+
* Database and Redis fields are populated only when both env vars are set.
|
|
10
|
+
*/
|
|
11
|
+
export function loadConfig() {
|
|
12
|
+
const databaseUrl = process.env.DATABASE_URL;
|
|
13
|
+
const redisUrl = process.env.REDIS_URL;
|
|
14
|
+
// Default cache TTLs
|
|
15
|
+
const cacheTtl = {
|
|
16
|
+
session: 3600, // 1 hour
|
|
17
|
+
activeSession: 1800, // 30 minutes
|
|
18
|
+
sessionList: 120 // 2 minutes
|
|
19
|
+
};
|
|
20
|
+
const rawSessionTtl = parseInt(process.env.SESSION_TTL || String(DEFAULT_SESSION_TTL_SECONDS), 10);
|
|
21
|
+
const sessionTtl = (Number.isFinite(rawSessionTtl) && rawSessionTtl > 0)
|
|
22
|
+
? rawSessionTtl : DEFAULT_SESSION_TTL_SECONDS;
|
|
23
|
+
// If no database config, return base config (file-based storage)
|
|
24
|
+
if (!databaseUrl || !redisUrl) {
|
|
25
|
+
return { cacheTtl, sessionTtl };
|
|
26
|
+
}
|
|
27
|
+
// Validate URLs
|
|
28
|
+
try {
|
|
29
|
+
DatabaseUrlSchema.parse(databaseUrl);
|
|
30
|
+
RedisUrlSchema.parse(redisUrl);
|
|
31
|
+
}
|
|
32
|
+
catch (error) {
|
|
33
|
+
throw new Error(`Invalid database or redis URL: ${error instanceof Error ? error.message : String(error)}`);
|
|
34
|
+
}
|
|
35
|
+
return {
|
|
36
|
+
database: {
|
|
37
|
+
connectionString: databaseUrl,
|
|
38
|
+
pool: {
|
|
39
|
+
max: 10,
|
|
40
|
+
idleTimeoutMillis: 30000,
|
|
41
|
+
connectionTimeoutMillis: 5000,
|
|
42
|
+
statementTimeout: 10000
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
redis: {
|
|
46
|
+
url: redisUrl,
|
|
47
|
+
retryStrategy: {
|
|
48
|
+
maxRetries: 3,
|
|
49
|
+
initialDelay: 50,
|
|
50
|
+
maxDelay: 2000
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
cacheTtl,
|
|
54
|
+
sessionTtl
|
|
55
|
+
};
|
|
56
|
+
}
|
package/dist/db.d.ts
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { Pool } from "pg";
|
|
2
|
+
import { Redis } from "ioredis";
|
|
3
|
+
import { Config } from "./config.js";
|
|
4
|
+
import type { Logger } from "./logger.js";
|
|
5
|
+
export interface HealthCheckResult {
|
|
6
|
+
postgres: {
|
|
7
|
+
connected: boolean;
|
|
8
|
+
latency: number;
|
|
9
|
+
};
|
|
10
|
+
redis: {
|
|
11
|
+
connected: boolean;
|
|
12
|
+
latency: number;
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Database connection manager for PostgreSQL and Redis
|
|
17
|
+
*/
|
|
18
|
+
export declare class DatabaseConnection {
|
|
19
|
+
private logger;
|
|
20
|
+
private pool;
|
|
21
|
+
private redis;
|
|
22
|
+
private config;
|
|
23
|
+
constructor(config: Config, logger?: Logger);
|
|
24
|
+
/**
|
|
25
|
+
* Initialize connections to PostgreSQL and Redis
|
|
26
|
+
*/
|
|
27
|
+
connect(): Promise<void>;
|
|
28
|
+
/**
|
|
29
|
+
* Graceful shutdown - close all connections
|
|
30
|
+
*/
|
|
31
|
+
disconnect(): Promise<void>;
|
|
32
|
+
/**
|
|
33
|
+
* Health check for PostgreSQL and Redis
|
|
34
|
+
*/
|
|
35
|
+
healthCheck(): Promise<HealthCheckResult>;
|
|
36
|
+
/**
|
|
37
|
+
* Get PostgreSQL pool
|
|
38
|
+
*/
|
|
39
|
+
getPool(): Pool;
|
|
40
|
+
/**
|
|
41
|
+
* Get Redis client
|
|
42
|
+
*/
|
|
43
|
+
getRedis(): Redis;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Factory function to create and connect DatabaseConnection
|
|
47
|
+
*/
|
|
48
|
+
export declare function createDatabaseConnection(config: Config, logger?: Logger): Promise<DatabaseConnection>;
|
package/dist/db.js
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { Pool } from "pg";
|
|
2
|
+
import { Redis } from "ioredis";
|
|
3
|
+
import { noopLogger } from "./logger.js";
|
|
4
|
+
/**
|
|
5
|
+
* Database connection manager for PostgreSQL and Redis
|
|
6
|
+
*/
|
|
7
|
+
export class DatabaseConnection {
|
|
8
|
+
logger;
|
|
9
|
+
pool = null;
|
|
10
|
+
redis = null;
|
|
11
|
+
config;
|
|
12
|
+
constructor(config, logger = noopLogger) {
|
|
13
|
+
this.logger = logger;
|
|
14
|
+
if (!config.database || !config.redis) {
|
|
15
|
+
throw new Error("Database and Redis configuration required");
|
|
16
|
+
}
|
|
17
|
+
this.config = config;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Initialize connections to PostgreSQL and Redis
|
|
21
|
+
*/
|
|
22
|
+
async connect() {
|
|
23
|
+
// Initialize PostgreSQL pool
|
|
24
|
+
const poolConfig = {
|
|
25
|
+
connectionString: this.config.database.connectionString,
|
|
26
|
+
max: this.config.database.pool.max,
|
|
27
|
+
idleTimeoutMillis: this.config.database.pool.idleTimeoutMillis,
|
|
28
|
+
connectionTimeoutMillis: this.config.database.pool.connectionTimeoutMillis,
|
|
29
|
+
statement_timeout: this.config.database.pool.statementTimeout
|
|
30
|
+
};
|
|
31
|
+
this.pool = new Pool(poolConfig);
|
|
32
|
+
// Test PostgreSQL connection
|
|
33
|
+
try {
|
|
34
|
+
const client = await this.pool.connect();
|
|
35
|
+
await client.query("SELECT 1");
|
|
36
|
+
client.release();
|
|
37
|
+
this.logger.info("PostgreSQL connection established");
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
this.logger.error("Failed to connect to PostgreSQL", { error });
|
|
41
|
+
throw new Error(`Failed to connect to PostgreSQL: ${error instanceof Error ? error.message : String(error)}`);
|
|
42
|
+
}
|
|
43
|
+
// Initialize Redis client
|
|
44
|
+
const redisOptions = {
|
|
45
|
+
retryStrategy: (times) => {
|
|
46
|
+
const { maxRetries, initialDelay, maxDelay } = this.config.redis.retryStrategy;
|
|
47
|
+
if (times > maxRetries) {
|
|
48
|
+
return null; // Stop retrying
|
|
49
|
+
}
|
|
50
|
+
return Math.min(initialDelay * times, maxDelay);
|
|
51
|
+
},
|
|
52
|
+
lazyConnect: false,
|
|
53
|
+
reconnectOnError: (err) => {
|
|
54
|
+
// Reconnect on READONLY and ECONNRESET errors
|
|
55
|
+
const targetErrors = ["READONLY", "ECONNRESET"];
|
|
56
|
+
return targetErrors.some(targetError => err.message.includes(targetError));
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
this.redis = new Redis(this.config.redis.url, redisOptions);
|
|
60
|
+
// Test Redis connection
|
|
61
|
+
try {
|
|
62
|
+
await this.redis.ping();
|
|
63
|
+
this.logger.info("Redis connection established");
|
|
64
|
+
}
|
|
65
|
+
catch (error) {
|
|
66
|
+
this.logger.error("Failed to connect to Redis", { error });
|
|
67
|
+
throw new Error(`Failed to connect to Redis: ${error instanceof Error ? error.message : String(error)}`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Graceful shutdown - close all connections
|
|
72
|
+
*/
|
|
73
|
+
async disconnect() {
|
|
74
|
+
this.logger.info("Disconnecting database connections");
|
|
75
|
+
const errors = [];
|
|
76
|
+
if (this.pool) {
|
|
77
|
+
try {
|
|
78
|
+
await this.pool.end();
|
|
79
|
+
this.pool = null;
|
|
80
|
+
}
|
|
81
|
+
catch (error) {
|
|
82
|
+
errors.push(new Error(`PostgreSQL disconnect error: ${error instanceof Error ? error.message : String(error)}`));
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
if (this.redis) {
|
|
86
|
+
try {
|
|
87
|
+
this.redis.disconnect();
|
|
88
|
+
this.redis = null;
|
|
89
|
+
}
|
|
90
|
+
catch (error) {
|
|
91
|
+
errors.push(new Error(`Redis disconnect error: ${error instanceof Error ? error.message : String(error)}`));
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
if (errors.length > 0) {
|
|
95
|
+
throw new Error(`Disconnect errors: ${errors.map(e => e.message).join("; ")}`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Health check for PostgreSQL and Redis
|
|
100
|
+
*/
|
|
101
|
+
async healthCheck() {
|
|
102
|
+
const result = {
|
|
103
|
+
postgres: { connected: false, latency: 0 },
|
|
104
|
+
redis: { connected: false, latency: 0 }
|
|
105
|
+
};
|
|
106
|
+
// Check PostgreSQL
|
|
107
|
+
if (this.pool) {
|
|
108
|
+
const pgStart = Date.now();
|
|
109
|
+
let client = null;
|
|
110
|
+
try {
|
|
111
|
+
client = await this.pool.connect();
|
|
112
|
+
await client.query("SELECT 1");
|
|
113
|
+
result.postgres.connected = true;
|
|
114
|
+
result.postgres.latency = Date.now() - pgStart;
|
|
115
|
+
}
|
|
116
|
+
catch (error) {
|
|
117
|
+
result.postgres.connected = false;
|
|
118
|
+
}
|
|
119
|
+
finally {
|
|
120
|
+
// Always release the client to prevent connection leaks
|
|
121
|
+
if (client) {
|
|
122
|
+
client.release();
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
// Check Redis
|
|
127
|
+
if (this.redis) {
|
|
128
|
+
const redisStart = Date.now();
|
|
129
|
+
try {
|
|
130
|
+
await this.redis.ping();
|
|
131
|
+
result.redis.connected = true;
|
|
132
|
+
result.redis.latency = Date.now() - redisStart;
|
|
133
|
+
}
|
|
134
|
+
catch (error) {
|
|
135
|
+
result.redis.connected = false;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
this.logger.debug("Health check completed", {
|
|
139
|
+
postgres: result.postgres.connected,
|
|
140
|
+
redis: result.redis.connected
|
|
141
|
+
});
|
|
142
|
+
return result;
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Get PostgreSQL pool
|
|
146
|
+
*/
|
|
147
|
+
getPool() {
|
|
148
|
+
if (!this.pool) {
|
|
149
|
+
throw new Error("PostgreSQL pool not initialized");
|
|
150
|
+
}
|
|
151
|
+
return this.pool;
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Get Redis client
|
|
155
|
+
*/
|
|
156
|
+
getRedis() {
|
|
157
|
+
if (!this.redis) {
|
|
158
|
+
throw new Error("Redis client not initialized");
|
|
159
|
+
}
|
|
160
|
+
return this.redis;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Factory function to create and connect DatabaseConnection
|
|
165
|
+
*/
|
|
166
|
+
export async function createDatabaseConnection(config, logger) {
|
|
167
|
+
const db = new DatabaseConnection(config, logger);
|
|
168
|
+
await db.connect();
|
|
169
|
+
return db;
|
|
170
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { ChildProcess } from "child_process";
|
|
2
|
+
import type { Logger } from "./logger.js";
|
|
3
|
+
export interface ExecuteOptions {
|
|
4
|
+
timeout?: number;
|
|
5
|
+
idleTimeout?: number;
|
|
6
|
+
cwd?: string;
|
|
7
|
+
logger?: Logger;
|
|
8
|
+
}
|
|
9
|
+
export interface ExecuteResult {
|
|
10
|
+
stdout: string;
|
|
11
|
+
stderr: string;
|
|
12
|
+
code: number;
|
|
13
|
+
}
|
|
14
|
+
export declare function getExtendedPath(): string;
|
|
15
|
+
export declare function registerProcessGroup(pid: number): void;
|
|
16
|
+
export declare function unregisterProcessGroup(pid: number): void;
|
|
17
|
+
/**
|
|
18
|
+
* Kill all active process groups. Called on gateway shutdown.
|
|
19
|
+
* Sends SIGTERM to all groups, waits 3s, then SIGKILL survivors.
|
|
20
|
+
* Returns a Promise that resolves after SIGKILL escalation completes.
|
|
21
|
+
* The returned Promise keeps the event loop alive (no .unref()),
|
|
22
|
+
* ensuring the process does NOT exit before SIGKILL fires.
|
|
23
|
+
*/
|
|
24
|
+
export declare function killAllProcessGroups(): Promise<void>;
|
|
25
|
+
/**
|
|
26
|
+
* Kill an entire process group. Falls back to killing just the process
|
|
27
|
+
* if the group kill fails (e.g., pid not yet assigned).
|
|
28
|
+
*/
|
|
29
|
+
export declare function killProcessGroup(proc: ChildProcess, signal: NodeJS.Signals): boolean;
|
|
30
|
+
export declare function executeCli(command: string, args: string[], options?: ExecuteOptions): Promise<ExecuteResult>;
|