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.
Files changed (48) hide show
  1. package/CHANGELOG.md +541 -0
  2. package/LICENSE +21 -0
  3. package/README.md +545 -0
  4. package/dist/approval-manager.d.ts +43 -0
  5. package/dist/approval-manager.js +156 -0
  6. package/dist/async-job-manager.d.ts +57 -0
  7. package/dist/async-job-manager.js +334 -0
  8. package/dist/claude-mcp-config.d.ts +8 -0
  9. package/dist/claude-mcp-config.js +161 -0
  10. package/dist/config.d.ts +35 -0
  11. package/dist/config.js +56 -0
  12. package/dist/db.d.ts +48 -0
  13. package/dist/db.js +170 -0
  14. package/dist/executor.d.ts +30 -0
  15. package/dist/executor.js +315 -0
  16. package/dist/health.d.ts +20 -0
  17. package/dist/health.js +32 -0
  18. package/dist/index.d.ts +67 -0
  19. package/dist/index.js +1503 -0
  20. package/dist/logger.d.ts +6 -0
  21. package/dist/logger.js +5 -0
  22. package/dist/metrics.d.ts +23 -0
  23. package/dist/metrics.js +57 -0
  24. package/dist/migrate-sessions.d.ts +12 -0
  25. package/dist/migrate-sessions.js +145 -0
  26. package/dist/migrate.d.ts +2 -0
  27. package/dist/migrate.js +100 -0
  28. package/dist/model-registry.d.ts +10 -0
  29. package/dist/model-registry.js +346 -0
  30. package/dist/optimizer.d.ts +3 -0
  31. package/dist/optimizer.js +183 -0
  32. package/dist/process-monitor.d.ts +54 -0
  33. package/dist/process-monitor.js +146 -0
  34. package/dist/request-helpers.d.ts +25 -0
  35. package/dist/request-helpers.js +32 -0
  36. package/dist/resources.d.ts +26 -0
  37. package/dist/resources.js +201 -0
  38. package/dist/retry.d.ts +72 -0
  39. package/dist/retry.js +146 -0
  40. package/dist/review-integrity.d.ts +50 -0
  41. package/dist/review-integrity.js +283 -0
  42. package/dist/session-manager-pg.d.ts +76 -0
  43. package/dist/session-manager-pg.js +383 -0
  44. package/dist/session-manager.d.ts +62 -0
  45. package/dist/session-manager.js +223 -0
  46. package/dist/stream-json-parser.d.ts +35 -0
  47. package/dist/stream-json-parser.js +94 -0
  48. 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
+ }
@@ -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>;