llm-cli-gateway 1.15.2 → 1.16.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 CHANGED
@@ -4,6 +4,64 @@ All notable changes to the llm-cli-gateway project.
4
4
 
5
5
  ## Unreleased
6
6
 
7
+ ## [1.16.0] - 2026-05-29 — remove Redis session dependency
8
+
9
+ Feature release that removes the optional Redis/ioredis layer from the
10
+ PostgreSQL-backed session manager and tightens the public README around the
11
+ project's current demand and quality signals.
12
+
13
+ ### Removed
14
+
15
+ - Removed the optional `ioredis` peer/dev dependency and its transitive
16
+ packages from the install graph.
17
+ - Removed `REDIS_URL` as a requirement for PostgreSQL-backed sessions.
18
+ - Removed Redis from the PostgreSQL test Docker Compose stack and PG test
19
+ harness.
20
+
21
+ ### Changed
22
+
23
+ - PostgreSQL-backed sessions now require only `DATABASE_URL` plus the optional
24
+ `pg` peer dependency. PostgreSQL remains the source of truth for session
25
+ records and active-session state.
26
+ - Simplified database health reporting to PostgreSQL connectivity only.
27
+ - Simplified the PG session manager by removing Redis cache-aside reads/writes
28
+ and Redis lock handling.
29
+ - Updated migration and testing docs to describe the Postgres-only backend.
30
+ - Updated release-readiness and Socket-alert documentation now that the Redis
31
+ client dependency is no longer present.
32
+ - Refocused the README first screen around the strongest current trust and
33
+ demand signals: npm monthly downloads, passing CI/security workflows,
34
+ OpenSSF status, Sigstore-signed releases, and MIT licensing.
35
+
36
+ ### Added
37
+
38
+ - Added `docs/plans/provider-workflow-assets.dag.toml`, a machine-readable
39
+ implementation plan for provider-specific skill and DAG-TOML pairs for
40
+ Claude, Codex, Gemini, Grok, and Mistral Vibe.
41
+
42
+ ## [1.15.3] - 2026-05-29 — remove retired PyPI plugin
43
+
44
+ Patch release removing the retired Python `llm` plugin integration so the
45
+ project no longer depends on Simon Willison's `llm` package.
46
+
47
+ ### Removed
48
+
49
+ - Removed `integrations/llm-plugin/`, including the `gateway-claude`,
50
+ `gateway-codex`, and `gateway-gemini` aliases that were registered through
51
+ the external `llm` package.
52
+ - Removed the PyPI trusted-publishing workflow. Releases now publish npm and
53
+ signed GitHub installer artifacts only.
54
+ - Removed the plugin-specific Dependabot and security-lint wiring for the
55
+ deleted Python package.
56
+
57
+ ### Changed
58
+
59
+ - Removed README guidance that advertised `llm install llm-gateway` and
60
+ `llm -m gateway-*` usage.
61
+ - Added an archived PyPI retirement description explaining the supported npm
62
+ and direct-MCP install paths for users who discover the historical PyPI
63
+ package.
64
+
7
65
  ## [1.15.2] - 2026-05-29 — security quality follow-up
8
66
 
9
67
  Patch release for GitHub Security & quality follow-up findings and Scorecard
package/README.md CHANGED
@@ -3,16 +3,38 @@
3
3
  [![CI](https://github.com/verivus-oss/llm-cli-gateway/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/verivus-oss/llm-cli-gateway/actions/workflows/ci.yml)
4
4
  [![Security](https://github.com/verivus-oss/llm-cli-gateway/actions/workflows/security.yml/badge.svg?branch=main)](https://github.com/verivus-oss/llm-cli-gateway/actions/workflows/security.yml)
5
5
  [![OpenSSF Scorecard](https://api.scorecard.dev/projects/github.com/verivus-oss/llm-cli-gateway/badge)](https://scorecard.dev/viewer/?uri=github.com/verivus-oss/llm-cli-gateway)
6
- [![OpenSSF Best Practices](https://www.bestpractices.dev/projects/13025/badge)](https://www.bestpractices.dev/projects/13025)
7
6
  [![npm](https://img.shields.io/npm/v/llm-cli-gateway.svg)](https://www.npmjs.com/package/llm-cli-gateway)
7
+ [![npm monthly downloads](https://img.shields.io/npm/dm/llm-cli-gateway.svg)](https://www.npmjs.com/package/llm-cli-gateway)
8
8
  [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
9
- [![Releases: Sigstore signed](https://img.shields.io/badge/releases-Sigstore%20signed-2e7d32.svg)](SECURITY.md#release-signing)
10
9
 
11
10
  > _"Without consultation, plans are frustrated, but with many counselors they succeed."_
12
11
  > — Proverbs 15:22 (LSB)
13
12
 
14
13
  A Model Context Protocol (MCP) gateway for running Claude Code, Codex, Gemini, Grok, and Mistral (Vibe) CLIs from one MCP endpoint, with durable async jobs, session continuity, cache-aware prompting, observability, and personal-appliance setup tooling.
15
14
 
15
+ **Why developers try it:** one local MCP endpoint for cross-LLM validation, multi-agent coding workflows, and repeatable assistant-led setup across five provider CLIs.
16
+
17
+ **Current signals:** crossed 5k monthly npm downloads in May 2026; live npm downloads are shown above. CI and security workflows pass on `main`, OpenSSF Scorecard is published, OpenSSF Best Practices is passing, releases use Sigstore signing, and the package is MIT licensed.
18
+
19
+ ## Quick Start
20
+
21
+ ```bash
22
+ npm install -g llm-cli-gateway
23
+ ```
24
+
25
+ Or use directly with `npx` from an MCP client:
26
+
27
+ ```json
28
+ {
29
+ "mcpServers": {
30
+ "llm-gateway": {
31
+ "command": "npx",
32
+ "args": ["-y", "llm-cli-gateway"]
33
+ }
34
+ }
35
+ }
36
+ ```
37
+
16
38
  ## What It Provides Today
17
39
 
18
40
  `llm-cli-gateway` is a single-user MCP gateway for cross-LLM validation and multi-agent coding workflows. It is more than a thin CLI wrapper:
@@ -24,6 +46,24 @@ A Model Context Protocol (MCP) gateway for running Claude Code, Codex, Gemini, G
24
46
  - Can run requests inside gateway-managed git worktrees for isolated multi-agent review and implementation loops.
25
47
  - Ships personal-appliance setup surfaces: HTTP transport with bearer-token auth, `doctor --json`, setup UI artifacts, provider setup snippets, Docker fallback, and checked release bundles.
26
48
 
49
+ ## Workflow Assets
50
+
51
+ The repo ships agent-ready workflow skills under [`.agents/skills`](.agents/skills) for async orchestration, session continuity, multi-LLM review, implement-review-fix loops, and secure approval-gated dispatch. Machine-readable DAG-TOML plans live under [`docs/plans`](docs/plans) and [`setup/install-plan.dag.toml`](setup/install-plan.dag.toml) for workflows that need deterministic sequencing and verification gates.
52
+
53
+ The next documentation focus is provider-specific skill and DAG-TOML pairs for each outbound CLI: Claude, Codex, Gemini, Grok, and Mistral Vibe. The implementation plan is tracked in [`docs/plans/provider-workflow-assets.dag.toml`](docs/plans/provider-workflow-assets.dag.toml), with each provider asset expected to cover install/login checks, session behavior, approval modes, cache/telemetry surfaces, failure modes, and a smoke-test gate.
54
+
55
+ ## Trust & Supply Chain
56
+
57
+ [![OpenSSF Best Practices](https://www.bestpractices.dev/projects/13025/badge)](https://www.bestpractices.dev/projects/13025)
58
+ [![npm weekly downloads](https://img.shields.io/npm/dw/llm-cli-gateway.svg)](https://www.npmjs.com/package/llm-cli-gateway)
59
+ [![GitHub release downloads](https://img.shields.io/github/downloads/verivus-oss/llm-cli-gateway/total.svg)](https://github.com/verivus-oss/llm-cli-gateway/releases)
60
+ [![Releases: Sigstore signed](https://img.shields.io/badge/releases-Sigstore%20signed-2e7d32.svg)](SECURITY.md#release-signing)
61
+
62
+ - CI runs build, lint, format, tests, package checks, and npm audit.
63
+ - Security CI runs actionlint, zizmor, shellcheck, typos, osv-scanner, gitleaks, and lychee.
64
+ - GitHub release installer artifacts are checksummed and signed with Sigstore keyless signing.
65
+ - npm releases use provenance through OIDC trusted publishing.
66
+
27
67
  ## Personal MCP Appliance
28
68
 
29
69
  The personal-appliance contract keeps that surface intentionally narrow: one trusted user runs the gateway on a machine or volume they own, connects one MCP endpoint, and asks any connected client for cross-LLM validation.
@@ -173,7 +213,7 @@ Opt-in flags (all default off) live under `[cache_awareness]` in `~/.llm-cli-gat
173
213
  - **No Secret Leakage**: Generic session descriptions only (file permissions 0o600)
174
214
  - **No ReDoS**: Bounded regex patterns prevent catastrophic backtracking
175
215
  - **Type Safety**: Strict TypeScript with comprehensive error handling
176
- - **Supply-chain hardening**: a dedicated `.github/workflows/security.yml` runs actionlint, zizmor, shellcheck, typos, osv-scanner, gitleaks, ruff, bandit, and lychee on every push and PR (see `SECURITY.md` for the threat model)
216
+ - **Supply-chain hardening**: a dedicated `.github/workflows/security.yml` runs actionlint, zizmor, shellcheck, typos, osv-scanner, gitleaks, and lychee on every push and PR (see `SECURITY.md` for the threat model)
177
217
 
178
218
  ## Prerequisites
179
219
 
@@ -967,25 +1007,6 @@ Each CLI can be configured through its own configuration files:
967
1007
  - Codex: `~/.codex/config.toml`
968
1008
  - Gemini: `~/.gemini/config.json`
969
1009
 
970
- ## For Fans of Simon Willison
971
-
972
- Simon's `llm` tool made it trivially easy to talk to any LLM from the command line. But as AI-assisted development matures, the challenge shifts from "how do I call a model" to "how do I orchestrate multiple models reliably, and what did they actually do?"
973
-
974
- **Multiple models increase the confidence factor.** When Claude writes code, Codex reviews it, and Gemini checks for bugs -- each bringing different training data and reasoning patterns -- the result is more robust than any single model alone. And often this isn't even enough. Having the models do iterative reviews is where you start getting real confidence.
975
-
976
- **Every interaction should be queryable data.** Inspired by `llm`'s SQLite logging philosophy, the gateway records every request and response to a local SQLite database. Not just prompts and responses -- retry counts, circuit breaker states, approval decisions, thinking blocks, cost estimates. Open it with Datasette and you have a complete operational picture of your AI usage:
977
-
978
- datasette ~/.llm-cli-gateway/logs.db
979
-
980
- **The `llm-gateway` plugin bridges both worlds.** Install it, and your existing `llm` workflows gain orchestration features without changing how you work:
981
-
982
- llm install llm-gateway
983
- llm -m gateway-claude "explain this function"
984
-
985
- Your gateway interactions appear in both `llm logs` (for your personal history) and the gateway's flight recorder (for operational observability). Two audiences, one workflow.
986
-
987
- **Composability over monoliths.** The gateway doesn't replace `llm` -- it complements it. Use `llm` directly when you want simplicity. Route through the gateway when you want resilience, multi-model coordination, or detailed operational telemetry. The plugin is the bridge, not the destination.
988
-
989
1010
  ## Development
990
1011
 
991
1012
  ### Project Structure
@@ -1159,7 +1180,6 @@ If you're vetting `llm-cli-gateway` through [Socket](https://socket.dev/npm/pack
1159
1180
  | **Shell access** | `src/executor.ts` uses `child_process.spawn(cmd, args, …)` to invoke the underlying LLM CLIs. | `spawn` is called with an argument array and **never** `shell: true`, so there is no shell interpolation path for caller input. The command name is restricted to an allow-list of known CLI binaries (`claude`, `codex`, `gemini`, `grok`, `vibe`). |
1160
1181
  | **Uses eval** | None in our source. Transitive: `@modelcontextprotocol/sdk` → `ajv@8` uses `new Function(...)` in `ajv/dist/compile/index.js` to compile JSON Schema validators. | This is ajv's standard codegen path. Only known schemas (defined in our source and the MCP SDK) flow into it; no caller-supplied data ever reaches the compiled function body. |
1161
1182
  | **better-sqlite3 PRAGMA helper** | Transitive: `better-sqlite3/lib/methods/pragma.js` interpolates its caller-provided `source` into a `PRAGMA ${source}` statement. | We do not call `db.pragma()` from production source. Internal SQLite setup uses fixed literal `db.exec("PRAGMA ...")` statements, and `npm run security:audit` fails the release if production code reintroduces `.pragma()` calls. |
1162
- | **ioredis obfuscated code** | Optional peer/dev dependency: `ioredis@5.10.1` may be flagged at `built/constants/TLSProfiles.js` for base64-looking strings. | Reviewed as a false positive. The file is a Redis Cloud TLS CA certificate bundle in PEM format, which is base64 by design. It contains no decoder loop, dynamic evaluation, network call, or hidden execution path. The same file is byte-for-byte identical in `ioredis@5.9.2`; our default production install does not install `ioredis`, and our code does not pass ioredis TLS profile options. |
1163
1183
  | **Dependency ownership** | A handful of small transitive packages (e.g. `bindings` via `better-sqlite3`, `media-typer` via `@modelcontextprotocol/sdk`) trip Socket's "unstable ownership" or "obfuscated code" heuristics. | These are pinned, well-known micro-deps in the Node ecosystem with no known issues. We pin direct override versions of `content-type` and `type-is` in `package.json#overrides`. Our previous direct dependency on `toml@3.0.0` (also single-maintainer, last released 2020) was replaced with the actively-maintained `smol-toml` to reduce inherited risk. |
1164
1184
 
1165
1185
  See [`socket.yml`](./socket.yml) for the same context in machine-readable form.
package/dist/config.d.ts CHANGED
@@ -1,9 +1,4 @@
1
1
  import type { Logger } from "./logger.js";
2
- export interface CacheTtl {
3
- session: number;
4
- activeSession: number;
5
- sessionList: number;
6
- }
7
2
  export interface DatabaseConfig {
8
3
  connectionString: string;
9
4
  pool: {
@@ -13,25 +8,15 @@ export interface DatabaseConfig {
13
8
  statementTimeout: number;
14
9
  };
15
10
  }
16
- export interface RedisConfig {
17
- url: string;
18
- retryStrategy: {
19
- maxRetries: number;
20
- initialDelay: number;
21
- maxDelay: number;
22
- };
23
- }
24
11
  export declare const DEFAULT_SESSION_TTL_SECONDS = 2592000;
25
12
  export interface Config {
26
13
  database?: DatabaseConfig;
27
- redis?: RedisConfig;
28
- cacheTtl: CacheTtl;
29
14
  sessionTtl: number;
30
15
  }
31
16
  /**
32
17
  * Load configuration from environment variables.
33
- * Always returns a Config object with base fields (cacheTtl, sessionTtl).
34
- * Database and Redis fields are populated only when both env vars are set.
18
+ * Always returns a Config object with base fields.
19
+ * Database fields are populated when DATABASE_URL is set.
35
20
  */
36
21
  export declare function loadConfig(): Config;
37
22
  export declare const PERSISTENCE_BACKENDS: readonly ["sqlite", "postgres", "memory", "none"];
package/dist/config.js CHANGED
@@ -11,37 +11,28 @@ const DatabaseUrlSchema = z
11
11
  .refine(url => url.startsWith("postgresql://") || url.startsWith("postgres://"), {
12
12
  message: "Database URL must start with postgresql:// or postgres://",
13
13
  });
14
- const RedisUrlSchema = z.string().url().startsWith("redis://");
15
14
  export const DEFAULT_SESSION_TTL_SECONDS = 2592000; // 30 days
16
15
  /**
17
16
  * Load configuration from environment variables.
18
- * Always returns a Config object with base fields (cacheTtl, sessionTtl).
19
- * Database and Redis fields are populated only when both env vars are set.
17
+ * Always returns a Config object with base fields.
18
+ * Database fields are populated when DATABASE_URL is set.
20
19
  */
21
20
  export function loadConfig() {
22
21
  const databaseUrl = process.env.DATABASE_URL;
23
- const redisUrl = process.env.REDIS_URL;
24
- // Default cache TTLs
25
- const cacheTtl = {
26
- session: 3600, // 1 hour
27
- activeSession: 1800, // 30 minutes
28
- sessionList: 120, // 2 minutes
29
- };
30
22
  const rawSessionTtl = parseInt(process.env.SESSION_TTL || String(DEFAULT_SESSION_TTL_SECONDS), 10);
31
23
  const sessionTtl = Number.isFinite(rawSessionTtl) && rawSessionTtl > 0
32
24
  ? rawSessionTtl
33
25
  : DEFAULT_SESSION_TTL_SECONDS;
34
26
  // If no database config, return base config (file-based storage)
35
- if (!databaseUrl || !redisUrl) {
36
- return { cacheTtl, sessionTtl };
27
+ if (!databaseUrl) {
28
+ return { sessionTtl };
37
29
  }
38
- // Validate URLs
30
+ // Validate URL
39
31
  try {
40
32
  DatabaseUrlSchema.parse(databaseUrl);
41
- RedisUrlSchema.parse(redisUrl);
42
33
  }
43
34
  catch (error) {
44
- throw new Error(`Invalid database or redis URL: ${error instanceof Error ? error.message : String(error)}`);
35
+ throw new Error(`Invalid database URL: ${error instanceof Error ? error.message : String(error)}`);
45
36
  }
46
37
  return {
47
38
  database: {
@@ -53,15 +44,6 @@ export function loadConfig() {
53
44
  statementTimeout: 10000,
54
45
  },
55
46
  },
56
- redis: {
57
- url: redisUrl,
58
- retryStrategy: {
59
- maxRetries: 3,
60
- initialDelay: 50,
61
- maxDelay: 2000,
62
- },
63
- },
64
- cacheTtl,
65
47
  sessionTtl,
66
48
  };
67
49
  }
package/dist/db.d.ts CHANGED
@@ -1,5 +1,4 @@
1
1
  import type { Pool } from "pg";
2
- import type { Redis } from "ioredis";
3
2
  import { Config } from "./config.js";
4
3
  import type { Logger } from "./logger.js";
5
4
  export interface HealthCheckResult {
@@ -7,22 +6,17 @@ export interface HealthCheckResult {
7
6
  connected: boolean;
8
7
  latency: number;
9
8
  };
10
- redis: {
11
- connected: boolean;
12
- latency: number;
13
- };
14
9
  }
15
10
  /**
16
- * Database connection manager for PostgreSQL and Redis
11
+ * Database connection manager for PostgreSQL-backed sessions.
17
12
  */
18
13
  export declare class DatabaseConnection {
19
14
  private logger;
20
15
  private pool;
21
- private redis;
22
16
  private config;
23
17
  constructor(config: Config, logger?: Logger);
24
18
  /**
25
- * Initialize connections to PostgreSQL and Redis
19
+ * Initialize connection to PostgreSQL.
26
20
  */
27
21
  connect(): Promise<void>;
28
22
  /**
@@ -30,17 +24,13 @@ export declare class DatabaseConnection {
30
24
  */
31
25
  disconnect(): Promise<void>;
32
26
  /**
33
- * Health check for PostgreSQL and Redis
27
+ * Health check for PostgreSQL.
34
28
  */
35
29
  healthCheck(): Promise<HealthCheckResult>;
36
30
  /**
37
31
  * Get PostgreSQL pool
38
32
  */
39
33
  getPool(): Pool;
40
- /**
41
- * Get Redis client
42
- */
43
- getRedis(): Redis;
44
34
  }
45
35
  /**
46
36
  * Factory function to create and connect DatabaseConnection
package/dist/db.js CHANGED
@@ -1,25 +1,23 @@
1
1
  import { noopLogger } from "./logger.js";
2
2
  /**
3
- * Database connection manager for PostgreSQL and Redis
3
+ * Database connection manager for PostgreSQL-backed sessions.
4
4
  */
5
5
  export class DatabaseConnection {
6
6
  logger;
7
7
  pool = null;
8
- redis = null;
9
8
  config;
10
9
  constructor(config, logger = noopLogger) {
11
10
  this.logger = logger;
12
- if (!config.database || !config.redis) {
13
- throw new Error("Database and Redis configuration required");
11
+ if (!config.database) {
12
+ throw new Error("Database configuration required");
14
13
  }
15
14
  this.config = config;
16
15
  }
17
16
  /**
18
- * Initialize connections to PostgreSQL and Redis
17
+ * Initialize connection to PostgreSQL.
19
18
  */
20
19
  async connect() {
21
20
  const { Pool } = await importOptionalPg();
22
- const Redis = await importOptionalRedis();
23
21
  // Initialize PostgreSQL pool
24
22
  const poolConfig = {
25
23
  connectionString: this.config.database.connectionString,
@@ -40,32 +38,6 @@ export class DatabaseConnection {
40
38
  this.logger.error("Failed to connect to PostgreSQL", { error });
41
39
  throw new Error(`Failed to connect to PostgreSQL: ${error instanceof Error ? error.message : String(error)}`);
42
40
  }
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
41
  }
70
42
  /**
71
43
  * Graceful shutdown - close all connections
@@ -82,26 +54,16 @@ export class DatabaseConnection {
82
54
  errors.push(new Error(`PostgreSQL disconnect error: ${error instanceof Error ? error.message : String(error)}`));
83
55
  }
84
56
  }
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
57
  if (errors.length > 0) {
95
58
  throw new Error(`Disconnect errors: ${errors.map(e => e.message).join("; ")}`);
96
59
  }
97
60
  }
98
61
  /**
99
- * Health check for PostgreSQL and Redis
62
+ * Health check for PostgreSQL.
100
63
  */
101
64
  async healthCheck() {
102
65
  const result = {
103
66
  postgres: { connected: false, latency: 0 },
104
- redis: { connected: false, latency: 0 },
105
67
  };
106
68
  // Check PostgreSQL
107
69
  if (this.pool) {
@@ -123,21 +85,8 @@ export class DatabaseConnection {
123
85
  }
124
86
  }
125
87
  }
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
88
  this.logger.debug("Health check completed", {
139
89
  postgres: result.postgres.connected,
140
- redis: result.redis.connected,
141
90
  });
142
91
  return result;
143
92
  }
@@ -150,15 +99,6 @@ export class DatabaseConnection {
150
99
  }
151
100
  return this.pool;
152
101
  }
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
102
  }
163
103
  async function importOptionalPg() {
164
104
  try {
@@ -166,19 +106,7 @@ async function importOptionalPg() {
166
106
  }
167
107
  catch (error) {
168
108
  if (error?.code === "ERR_MODULE_NOT_FOUND" || error?.code === "MODULE_NOT_FOUND") {
169
- throw new Error("PostgreSQL sessions require optional peer dependency 'pg'. Install it alongside llm-cli-gateway to use DATABASE_URL/REDIS_URL-backed sessions.");
170
- }
171
- throw error;
172
- }
173
- }
174
- async function importOptionalRedis() {
175
- try {
176
- const mod = await import("ioredis");
177
- return mod.Redis ?? mod.default;
178
- }
179
- catch (error) {
180
- if (error?.code === "ERR_MODULE_NOT_FOUND" || error?.code === "MODULE_NOT_FOUND") {
181
- throw new Error("PostgreSQL sessions require optional peer dependency 'ioredis'. Install it alongside llm-cli-gateway to use DATABASE_URL/REDIS_URL-backed sessions.");
109
+ throw new Error("PostgreSQL sessions require optional peer dependency 'pg'. Install it alongside llm-cli-gateway to use DATABASE_URL-backed sessions.");
182
110
  }
183
111
  throw error;
184
112
  }
package/dist/health.d.ts CHANGED
@@ -6,10 +6,6 @@ export interface HealthStatus {
6
6
  status: "up" | "down";
7
7
  latency: number;
8
8
  };
9
- redis: {
10
- status: "up" | "down";
11
- latency: number;
12
- };
13
9
  timestamp: string;
14
10
  }
15
11
  export interface ProviderRuntimeHealth {
@@ -18,10 +14,7 @@ export interface ProviderRuntimeHealth {
18
14
  timestamp: string;
19
15
  }
20
16
  /**
21
- * Check health status of PostgreSQL and Redis
22
- * - Both up → healthy
23
- * - Only PostgreSQL up → degraded (Redis down but DB works)
24
- * - PostgreSQL down → unhealthy (critical failure)
17
+ * Check health status of PostgreSQL.
25
18
  */
26
19
  export declare function checkHealth(db: DatabaseConnection): Promise<HealthStatus>;
27
20
  export declare function checkProviderRuntimeHealth(): ProviderRuntimeHealth;
package/dist/health.js CHANGED
@@ -1,9 +1,6 @@
1
1
  import { listProviderRuntimeStatuses } from "./provider-status.js";
2
2
  /**
3
- * Check health status of PostgreSQL and Redis
4
- * - Both up → healthy
5
- * - Only PostgreSQL up → degraded (Redis down but DB works)
6
- * - PostgreSQL down → unhealthy (critical failure)
3
+ * Check health status of PostgreSQL.
7
4
  */
8
5
  export async function checkHealth(db) {
9
6
  const result = await db.healthCheck();
@@ -13,22 +10,9 @@ export async function checkHealth(db) {
13
10
  status: result.postgres.connected ? "up" : "down",
14
11
  latency: result.postgres.latency,
15
12
  },
16
- redis: {
17
- status: result.redis.connected ? "up" : "down",
18
- latency: result.redis.latency,
19
- },
20
13
  timestamp: new Date().toISOString(),
21
14
  };
22
- // Determine overall health status
23
- if (result.postgres.connected && result.redis.connected) {
24
- health.status = "healthy";
25
- }
26
- else if (result.postgres.connected && !result.redis.connected) {
27
- health.status = "degraded";
28
- }
29
- else {
30
- health.status = "unhealthy";
31
- }
15
+ health.status = result.postgres.connected ? "healthy" : "unhealthy";
32
16
  return health;
33
17
  }
34
18
  export function checkProviderRuntimeHealth() {
package/dist/index.js CHANGED
@@ -5101,8 +5101,8 @@ async function initializeSessionManager() {
5101
5101
  // own a single filesystem); revisit if/when worktree support extends
5102
5102
  // there.
5103
5103
  const worktreeCleanupHook = createWorktreeSessionCleanupHook(logger);
5104
- if (config.database && config.redis) {
5105
- logger.info("Initializing PostgreSQL + Redis session manager");
5104
+ if (config.database) {
5105
+ logger.info("Initializing PostgreSQL session manager");
5106
5106
  const { createDatabaseConnection } = await import("./db.js");
5107
5107
  db = await createDatabaseConnection(config, logger);
5108
5108
  sessionManager = await createSessionManager(config, db, logger);
@@ -82,7 +82,6 @@ Options:
82
82
 
83
83
  Environment Variables:
84
84
  DATABASE_URL PostgreSQL connection string (required)
85
- REDIS_URL Redis connection string (required)
86
85
  `);
87
86
  process.exit(args[0] === "--help" || args[0] === "-h" ? 0 : 1);
88
87
  }
@@ -97,18 +96,17 @@ Environment Variables:
97
96
  console.error(`Migration Configuration:`);
98
97
  console.error(` Source: ${filePath}`);
99
98
  console.error(` DATABASE_URL: ${process.env.DATABASE_URL ? "[set]" : "[not set]"}`);
100
- console.error(` REDIS_URL: ${process.env.REDIS_URL ? "[set]" : "[not set]"}`);
101
99
  console.error("");
102
100
  // Load config
103
101
  const config = loadConfig();
104
- if (!config.database || !config.redis) {
105
- console.error("ERROR: DATABASE_URL and REDIS_URL must be set");
102
+ if (!config.database) {
103
+ console.error("ERROR: DATABASE_URL must be set");
106
104
  process.exit(1);
107
105
  }
108
106
  // Connect to database
109
107
  console.error("Connecting to database...");
110
108
  const db = await createDatabaseConnection(config, logger);
111
- const pgManager = new PostgreSQLSessionManager(db.getPool(), db.getRedis(), config.cacheTtl, logger);
109
+ const pgManager = new PostgreSQLSessionManager(db.getPool());
112
110
  console.error("✓ Connected to database\n");
113
111
  try {
114
112
  // Run migration
@@ -1,77 +1,48 @@
1
1
  import type { Pool } from "pg";
2
- import type { Redis } from "ioredis";
3
2
  import { Session, CliType } from "./session-manager.js";
4
- import { CacheTtl } from "./config.js";
5
- import type { Logger } from "./logger.js";
6
3
  export type { Logger } from "./logger.js";
7
4
  /**
8
- * PostgreSQL-backed session manager with Redis caching
5
+ * PostgreSQL-backed session manager. PostgreSQL is the source of truth and
6
+ * the only required service for this backend.
9
7
  */
10
8
  export declare class PostgreSQLSessionManager {
11
9
  private pool;
12
- private redis;
13
- private cacheTtl;
14
- private logger;
15
- constructor(pool: Pool, redis: Redis, cacheTtl: CacheTtl, logger: Logger);
10
+ constructor(pool: Pool);
16
11
  /**
17
- * Acquire distributed lock using Redis SET NX EX
18
- * Returns [success, lockValue] tuple
19
- */
20
- private acquireLock;
21
- private sleep;
22
- /**
23
- * Acquire a distributed lock with bounded retries to smooth contention spikes.
24
- */
25
- private acquireLockWithRetry;
26
- /**
27
- * Release distributed lock with optimistic Redis transaction semantics.
28
- * Only releases if lockValue matches, which prevents releasing another
29
- * process's lock after expiry/reacquire.
30
- */
31
- private releaseLock;
32
- /**
33
- * Invalidate session cache
34
- */
35
- private invalidateCache;
36
- /**
37
- * Invalidate session list cache using SCAN (non-blocking)
38
- */
39
- private invalidateListCache;
40
- /**
41
- * Create a new session
12
+ * Create a new session.
42
13
  */
43
14
  createSession(cli: CliType, description?: string, sessionId?: string): Promise<Session>;
44
15
  /**
45
- * Get session by ID (cache-aside pattern)
16
+ * Get session by ID.
46
17
  */
47
18
  getSession(sessionId: string): Promise<Session | null>;
48
19
  /**
49
- * List all sessions, optionally filtered by CLI
20
+ * List all sessions, optionally filtered by CLI.
50
21
  */
51
22
  listSessions(cli?: CliType): Promise<Session[]>;
52
23
  /**
53
- * Delete a session
24
+ * Delete a session.
54
25
  */
55
26
  deleteSession(sessionId: string): Promise<boolean>;
56
27
  /**
57
- * Set active session for a CLI (with distributed locking)
28
+ * Set active session for a CLI. The row-level update is serialized by
29
+ * PostgreSQL and the session FK keeps stale IDs from being recorded.
58
30
  */
59
31
  setActiveSession(cli: CliType, sessionId: string | null): Promise<boolean>;
60
32
  /**
61
- * Get active session for a CLI
33
+ * Get active session for a CLI.
62
34
  */
63
35
  getActiveSession(cli: CliType): Promise<Session | null>;
64
36
  /**
65
- * Update session usage timestamp
37
+ * Update session usage timestamp.
66
38
  */
67
39
  updateSessionUsage(sessionId: string): Promise<void>;
68
40
  /**
69
- * Update session metadata (atomic JSONB merge)
41
+ * Update session metadata using PostgreSQL's atomic JSONB merge.
70
42
  */
71
43
  updateSessionMetadata(sessionId: string, metadata: Record<string, any>): Promise<boolean>;
72
44
  /**
73
- * Clear all sessions, optionally filtered by CLI
74
- * Invalidates all related caches (session, active, list)
45
+ * Clear all sessions, optionally filtered by CLI.
75
46
  */
76
47
  clearAllSessions(cli?: CliType): Promise<number>;
77
48
  }
@@ -7,112 +7,16 @@ const DEFAULT_SESSION_DESCRIPTIONS = {
7
7
  mistral: "Mistral Session",
8
8
  };
9
9
  /**
10
- * PostgreSQL-backed session manager with Redis caching
10
+ * PostgreSQL-backed session manager. PostgreSQL is the source of truth and
11
+ * the only required service for this backend.
11
12
  */
12
13
  export class PostgreSQLSessionManager {
13
14
  pool;
14
- redis;
15
- cacheTtl;
16
- logger;
17
- constructor(pool, redis, cacheTtl, logger) {
15
+ constructor(pool) {
18
16
  this.pool = pool;
19
- this.redis = redis;
20
- this.cacheTtl = cacheTtl;
21
- this.logger = logger;
22
17
  }
23
18
  /**
24
- * Acquire distributed lock using Redis SET NX EX
25
- * Returns [success, lockValue] tuple
26
- */
27
- async acquireLock(key, ttlSeconds) {
28
- const lockKey = `lock:${key}`;
29
- const lockValue = randomUUID();
30
- // SET NX EX atomic operation
31
- const result = await this.redis.set(lockKey, lockValue, "EX", ttlSeconds, "NX");
32
- return [result === "OK", lockValue];
33
- }
34
- async sleep(ms) {
35
- await new Promise(resolve => setTimeout(resolve, ms));
36
- }
37
- /**
38
- * Acquire a distributed lock with bounded retries to smooth contention spikes.
39
- */
40
- async acquireLockWithRetry(key, ttlSeconds, errorLabel, maxWaitMs = 6000) {
41
- const deadline = Date.now() + maxWaitMs;
42
- while (true) {
43
- const [lockAcquired, lockValue] = await this.acquireLock(key, ttlSeconds);
44
- if (lockAcquired) {
45
- return lockValue;
46
- }
47
- if (Date.now() >= deadline) {
48
- throw new Error(`Failed to acquire lock for ${errorLabel}`);
49
- }
50
- // Small jitter avoids lock-step retries from concurrent callers.
51
- await this.sleep(25 + Math.floor(Math.random() * 25));
52
- }
53
- }
54
- /**
55
- * Release distributed lock with optimistic Redis transaction semantics.
56
- * Only releases if lockValue matches, which prevents releasing another
57
- * process's lock after expiry/reacquire.
58
- */
59
- async releaseLock(key, lockValue) {
60
- const lockKey = `lock:${key}`;
61
- await this.redis.watch(lockKey);
62
- try {
63
- const currentValue = await this.redis.get(lockKey);
64
- if (currentValue !== lockValue) {
65
- await this.redis.unwatch();
66
- return;
67
- }
68
- await this.redis.multi().del(lockKey).exec();
69
- }
70
- catch (error) {
71
- await this.redis.unwatch().catch(() => undefined);
72
- throw error;
73
- }
74
- }
75
- /**
76
- * Invalidate session cache
77
- */
78
- async invalidateCache(sessionId) {
79
- try {
80
- await this.redis.del(`session:${sessionId}`);
81
- }
82
- catch (error) {
83
- // Graceful degradation - log but don't fail
84
- this.logger.error(`Cache invalidation failed for session ${sessionId}`, { error, sessionId });
85
- }
86
- }
87
- /**
88
- * Invalidate session list cache using SCAN (non-blocking)
89
- */
90
- async invalidateListCache(cli) {
91
- try {
92
- if (cli) {
93
- await this.redis.del(`session_list:${cli}`);
94
- }
95
- else {
96
- // Use SCAN instead of KEYS to avoid blocking Redis
97
- const keys = [];
98
- let cursor = "0";
99
- do {
100
- const [nextCursor, matchedKeys] = await this.redis.scan(cursor, "MATCH", "session_list:*", "COUNT", 100);
101
- cursor = nextCursor;
102
- keys.push(...matchedKeys);
103
- } while (cursor !== "0");
104
- // Delete in batches to avoid overwhelming Redis
105
- if (keys.length > 0) {
106
- await this.redis.del(...keys);
107
- }
108
- }
109
- }
110
- catch (error) {
111
- this.logger.error("List cache invalidation failed", { error });
112
- }
113
- }
114
- /**
115
- * Create a new session
19
+ * Create a new session.
116
20
  */
117
21
  async createSession(cli, description, sessionId) {
118
22
  const id = sessionId || randomUUID();
@@ -121,32 +25,19 @@ export class PostgreSQLSessionManager {
121
25
  const client = await this.pool.connect();
122
26
  try {
123
27
  await client.query("BEGIN");
124
- // Insert session
125
28
  await client.query(`INSERT INTO sessions (id, cli, description, created_at, last_used_at)
126
29
  VALUES ($1, $2, $3, $4, $5)`, [id, cli, sessionDescription, now, now]);
127
- // Set as active if none exists
128
30
  await client.query(`INSERT INTO active_sessions (cli, session_id, updated_at)
129
31
  VALUES ($1, $2, $3)
130
32
  ON CONFLICT (cli) DO NOTHING`, [cli, id, now]);
131
33
  await client.query("COMMIT");
132
- const session = {
34
+ return {
133
35
  id,
134
36
  cli,
135
37
  createdAt: now,
136
38
  lastUsedAt: now,
137
39
  description: sessionDescription,
138
40
  };
139
- // Write-through to cache
140
- try {
141
- await this.redis.setex(`session:${id}`, this.cacheTtl.session, JSON.stringify(session));
142
- }
143
- catch (error) {
144
- // Graceful degradation
145
- this.logger.error("Cache write failed", { error });
146
- }
147
- // Invalidate list cache
148
- await this.invalidateListCache(cli);
149
- return session;
150
41
  }
151
42
  catch (error) {
152
43
  await client.query("ROLLBACK");
@@ -157,55 +48,18 @@ export class PostgreSQLSessionManager {
157
48
  }
158
49
  }
159
50
  /**
160
- * Get session by ID (cache-aside pattern)
51
+ * Get session by ID.
161
52
  */
162
53
  async getSession(sessionId) {
163
- // Try cache first
164
- try {
165
- const cached = await this.redis.get(`session:${sessionId}`);
166
- if (cached) {
167
- return JSON.parse(cached);
168
- }
169
- }
170
- catch (error) {
171
- // Graceful degradation - fallback to DB
172
- this.logger.error("Cache read failed", { error });
173
- }
174
- // Cache miss - query database
175
54
  const result = await this.pool.query(`SELECT id, cli, description, metadata, created_at AS "createdAt", last_used_at AS "lastUsedAt"
176
55
  FROM sessions
177
56
  WHERE id = $1`, [sessionId]);
178
- if (result.rows.length === 0) {
179
- return null;
180
- }
181
- const session = result.rows[0];
182
- // Populate cache
183
- try {
184
- await this.redis.setex(`session:${sessionId}`, this.cacheTtl.session, JSON.stringify(session));
185
- }
186
- catch (error) {
187
- this.logger.error("Cache write failed", { error });
188
- }
189
- return session;
57
+ return result.rows[0] ?? null;
190
58
  }
191
59
  /**
192
- * List all sessions, optionally filtered by CLI
60
+ * List all sessions, optionally filtered by CLI.
193
61
  */
194
62
  async listSessions(cli) {
195
- // Try cache for CLI-specific lists
196
- const cacheKey = cli ? `session_list:${cli}` : null;
197
- if (cacheKey) {
198
- try {
199
- const cached = await this.redis.get(cacheKey);
200
- if (cached) {
201
- return JSON.parse(cached);
202
- }
203
- }
204
- catch (error) {
205
- this.logger.error("Cache read failed", { error });
206
- }
207
- }
208
- // Query database
209
63
  const query = cli
210
64
  ? `SELECT id, cli, description, metadata, created_at AS "createdAt", last_used_at AS "lastUsedAt"
211
65
  FROM sessions
@@ -217,176 +71,70 @@ export class PostgreSQLSessionManager {
217
71
  const result = cli
218
72
  ? await this.pool.query(query, [cli])
219
73
  : await this.pool.query(query);
220
- const sessions = result.rows;
221
- // Cache CLI-specific lists
222
- if (cacheKey) {
223
- try {
224
- await this.redis.setex(cacheKey, this.cacheTtl.sessionList, JSON.stringify(sessions));
225
- }
226
- catch (error) {
227
- this.logger.error("Cache write failed", { error });
228
- }
229
- }
230
- return sessions;
74
+ return result.rows;
231
75
  }
232
76
  /**
233
- * Delete a session
77
+ * Delete a session.
234
78
  */
235
79
  async deleteSession(sessionId) {
236
- // Get session to find CLI type
237
80
  const session = await this.getSession(sessionId);
238
81
  if (!session) {
239
82
  return false;
240
83
  }
241
- // Delete from database (CASCADE will handle active_sessions)
242
84
  const result = await this.pool.query("DELETE FROM sessions WHERE id = $1", [sessionId]);
243
- if (result.rowCount === 0) {
244
- return false;
245
- }
246
- // Invalidate caches (session, active session for this CLI, and list)
247
- await this.invalidateCache(sessionId);
248
- try {
249
- await this.redis.del(`active_session:${session.cli}`);
250
- }
251
- catch (error) {
252
- this.logger.error(`Failed to invalidate active session cache for ${session.cli}`, { error });
253
- }
254
- await this.invalidateListCache(session.cli);
255
- return true;
85
+ return result.rowCount !== 0;
256
86
  }
257
87
  /**
258
- * Set active session for a CLI (with distributed locking)
88
+ * Set active session for a CLI. The row-level update is serialized by
89
+ * PostgreSQL and the session FK keeps stale IDs from being recorded.
259
90
  */
260
91
  async setActiveSession(cli, sessionId) {
261
- // Validate session exists if not null
262
92
  if (sessionId !== null) {
263
93
  const session = await this.getSession(sessionId);
264
94
  if (!session || session.cli !== cli) {
265
95
  return false;
266
96
  }
267
97
  }
268
- // Acquire lock with bounded retries to avoid failing benign concurrent updates.
269
- const lockValue = await this.acquireLockWithRetry(`active_session:${cli}`, 5, `active session ${cli}`);
270
- try {
271
- // UPSERT active session
272
- const now = new Date().toISOString();
273
- await this.pool.query(`INSERT INTO active_sessions (cli, session_id, updated_at)
274
- VALUES ($1, $2, $3)
275
- ON CONFLICT (cli) DO UPDATE SET session_id = $2, updated_at = $3`, [cli, sessionId, now]);
276
- // Update cache
277
- try {
278
- if (sessionId) {
279
- await this.redis.setex(`active_session:${cli}`, this.cacheTtl.activeSession, sessionId);
280
- }
281
- else {
282
- await this.redis.del(`active_session:${cli}`);
283
- }
284
- }
285
- catch (error) {
286
- this.logger.error("Cache update failed", { error });
287
- }
288
- return true;
289
- }
290
- finally {
291
- // Release lock with ownership verification
292
- try {
293
- await this.releaseLock(`active_session:${cli}`, lockValue);
294
- }
295
- catch (error) {
296
- this.logger.error(`Failed to release lock for active session ${cli}`, { error, cli });
297
- }
298
- }
98
+ const now = new Date().toISOString();
99
+ await this.pool.query(`INSERT INTO active_sessions (cli, session_id, updated_at)
100
+ VALUES ($1, $2, $3)
101
+ ON CONFLICT (cli) DO UPDATE SET session_id = $2, updated_at = $3`, [cli, sessionId, now]);
102
+ return true;
299
103
  }
300
104
  /**
301
- * Get active session for a CLI
105
+ * Get active session for a CLI.
302
106
  */
303
107
  async getActiveSession(cli) {
304
- // Try cache first
305
- try {
306
- const cachedId = await this.redis.get(`active_session:${cli}`);
307
- if (cachedId) {
308
- return await this.getSession(cachedId);
309
- }
310
- }
311
- catch (error) {
312
- this.logger.error("Cache read failed", { error });
313
- }
314
- // Query database
315
108
  const result = await this.pool.query("SELECT session_id FROM active_sessions WHERE cli = $1", [cli]);
316
- if (result.rows.length === 0 || !result.rows[0].session_id) {
109
+ const sessionId = result.rows[0]?.session_id;
110
+ if (!sessionId) {
317
111
  return null;
318
112
  }
319
- const sessionId = result.rows[0].session_id;
320
- // Populate cache
321
- try {
322
- await this.redis.setex(`active_session:${cli}`, this.cacheTtl.activeSession, sessionId);
323
- }
324
- catch (error) {
325
- this.logger.error("Cache write failed", { error });
326
- }
327
113
  return await this.getSession(sessionId);
328
114
  }
329
115
  /**
330
- * Update session usage timestamp
116
+ * Update session usage timestamp.
331
117
  */
332
118
  async updateSessionUsage(sessionId) {
333
119
  const now = new Date().toISOString();
334
120
  await this.pool.query("UPDATE sessions SET last_used_at = $1 WHERE id = $2", [now, sessionId]);
335
- // Invalidate cache to force refresh
336
- await this.invalidateCache(sessionId);
337
121
  }
338
122
  /**
339
- * Update session metadata (atomic JSONB merge)
123
+ * Update session metadata using PostgreSQL's atomic JSONB merge.
340
124
  */
341
125
  async updateSessionMetadata(sessionId, metadata) {
342
- // Use PostgreSQL JSONB || operator for atomic merge (prevents race conditions)
343
126
  const result = await this.pool.query(`UPDATE sessions
344
127
  SET metadata = COALESCE(metadata, '{}'::jsonb) || $1::jsonb
345
128
  WHERE id = $2
346
129
  RETURNING id`, [JSON.stringify(metadata), sessionId]);
347
- if (result.rowCount === 0) {
348
- return false;
349
- }
350
- // Invalidate cache
351
- await this.invalidateCache(sessionId);
352
- return true;
130
+ return result.rowCount !== 0;
353
131
  }
354
132
  /**
355
- * Clear all sessions, optionally filtered by CLI
356
- * Invalidates all related caches (session, active, list)
133
+ * Clear all sessions, optionally filtered by CLI.
357
134
  */
358
135
  async clearAllSessions(cli) {
359
- // First get all sessions to invalidate their caches
360
- const sessions = await this.listSessions(cli);
361
- // Delete from database
362
136
  const query = cli ? "DELETE FROM sessions WHERE cli = $1" : "DELETE FROM sessions";
363
137
  const result = cli ? await this.pool.query(query, [cli]) : await this.pool.query(query);
364
- // Invalidate individual session caches (concurrent — each has its own try/catch)
365
- await Promise.all(sessions.map(session => this.invalidateCache(session.id)));
366
- // Invalidate active session caches
367
- if (cli) {
368
- try {
369
- await this.redis.del(`active_session:${cli}`);
370
- }
371
- catch (error) {
372
- this.logger.error(`Failed to invalidate active session cache for ${cli}`, { error, cli });
373
- }
374
- }
375
- else {
376
- // Invalidate all active session caches
377
- try {
378
- await Promise.all([
379
- this.redis.del("active_session:claude"),
380
- this.redis.del("active_session:codex"),
381
- this.redis.del("active_session:gemini"),
382
- ]);
383
- }
384
- catch (error) {
385
- this.logger.error("Failed to invalidate active session caches", { error });
386
- }
387
- }
388
- // Invalidate list caches
389
- await this.invalidateListCache(cli);
390
138
  return result.rowCount || 0;
391
139
  }
392
140
  }
@@ -230,15 +230,15 @@ export const SessionManager = FileSessionManager;
230
230
  * @param logger - Logger instance for structured logging
231
231
  */
232
232
  export async function createSessionManager(config, db, logger, opts) {
233
- if (config?.database && config?.redis) {
234
- // Import dynamically to avoid loading pg/ioredis if not needed
233
+ if (config?.database) {
234
+ // Import dynamically to avoid loading pg if not needed.
235
235
  const { PostgreSQLSessionManager } = await import("./session-manager-pg.js");
236
236
  // Use provided db connection or create new one
237
237
  if (!db) {
238
238
  const { createDatabaseConnection } = await import("./db.js");
239
239
  db = await createDatabaseConnection(config, logger);
240
240
  }
241
- return new PostgreSQLSessionManager(db.getPool(), db.getRedis(), config.cacheTtl, logger ?? noopLogger);
241
+ return new PostgreSQLSessionManager(db.getPool());
242
242
  }
243
243
  else {
244
244
  // Use file-based storage with TTL from config
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "llm-cli-gateway",
3
- "version": "1.15.2",
3
+ "version": "1.16.0",
4
4
  "mcpName": "io.github.verivus-oss/llm-cli-gateway",
5
5
  "description": "MCP server providing unified access to Claude Code, Codex, Gemini, Grok, and Mistral Vibe CLIs with session management, retry logic, async job orchestration, durable job results, and cross-LLM validation.",
6
6
  "license": "MIT",
@@ -90,13 +90,9 @@
90
90
  "zod": "^3.23.0"
91
91
  },
92
92
  "peerDependencies": {
93
- "ioredis": "^5.4.1",
94
93
  "pg": "^8.12.0"
95
94
  },
96
95
  "peerDependenciesMeta": {
97
- "ioredis": {
98
- "optional": true
99
- },
100
96
  "pg": {
101
97
  "optional": true
102
98
  }
@@ -111,7 +107,6 @@
111
107
  "eslint": "^8.57.1",
112
108
  "eslint-config-prettier": "^9.0.0",
113
109
  "eslint-plugin-security": "^3.0.1",
114
- "ioredis": "5.9.2",
115
110
  "pg": "^8.12.0",
116
111
  "prettier": "^3.0.0",
117
112
  "typescript": "^5.0.0",