llm-cli-gateway 1.0.1 → 1.1.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
@@ -2,6 +2,27 @@
2
2
 
3
3
  All notable changes to the llm-cli-gateway project.
4
4
 
5
+ ## [1.1.0] - 2026-04-04
6
+
7
+ ### Added
8
+
9
+ - **SQLite flight recorder** — New `src/flight-recorder.ts` module logs all LLM requests/responses to `~/.llm-cli-gateway/logs.db` with two-phase logging (logStart/logComplete), WAL mode for concurrent Datasette reads, and graceful degradation when better-sqlite3 is unavailable
10
+ - **`LLM_GATEWAY_LOGS_DB` env var** — Configure flight recorder database path; set to empty string or `"none"` to disable logging entirely
11
+ - **`structuredContent` in MCP tool responses** — All tool handlers now return machine-readable metadata (model, cli, correlationId, sessionId, durationMs, token usage, exitCode) alongside the text response
12
+ - **`better-sqlite3` dependency** — Native SQLite addon for flight recorder (synchronous writes, WAL support)
13
+
14
+ ### Changed
15
+
16
+ - **review-integrity.ts simplified** — Reduced from 323 lines to 83 lines. Retains 3 violation types: empty_allowed_tools, critical_tools_disallowed, tool_suppression. Removed inlined_code detection and multi-pattern matching
17
+ - **`buildCliResponse` signature** — Now requires `cli` and `durationMs` parameters for structuredContent population
18
+ - **`createErrorResponse`** — Returns sanitized `errorCategory` enum in structuredContent instead of raw error messages (prevents path/secret leakage)
19
+ - **Flight recorder writes are idempotent** — logComplete only updates rows with status='started', preventing double-completion
20
+
21
+ ### Tests
22
+
23
+ - 284 tests passing (15 test files)
24
+ - Rewritten review-integrity tests to match simplified API
25
+
5
26
  ## [1.3.0] - 2026-02-15
6
27
 
7
28
  ### Fixed
package/README.md CHANGED
@@ -14,6 +14,10 @@ A Model Context Protocol (MCP) server providing unified access to Claude Code, C
14
14
  - **Correlation ID Tracking**: Full request tracing across all LLM interactions
15
15
  - **Cross-Tool Collaboration**: LLMs can use each other via MCP (validated through dogfooding)
16
16
 
17
+ ### Observability
18
+ - **SQLite Flight Recorder**: Every request/response logged to `~/.llm-cli-gateway/logs.db` with correlation IDs, token usage, duration, retry counts, and circuit breaker state. Browse with [Datasette](https://datasette.io/): `datasette ~/.llm-cli-gateway/logs.db`
19
+ - **Structured Metadata**: Tool responses include machine-readable `structuredContent` (model, cli, correlationId, sessionId, durationMs, token counts)
20
+
17
21
  ### Reliability & Performance
18
22
  - **Retry Logic**: Exponential backoff with circuit breaker for transient failures
19
23
  - **Atomic File Writes**: Process-specific temp files with fsync for data integrity
@@ -22,7 +26,7 @@ A Model Context Protocol (MCP) server providing unified access to Claude Code, C
22
26
  - **Long-Running Jobs**: Non-time-bound async execution via `*_request_async` + polling tools
23
27
 
24
28
  ### Security & Quality
25
- - **Comprehensive Testing**: 221 tests covering unit, integration, and regression scenarios
29
+ - **Comprehensive Testing**: 284 tests covering unit, integration, and regression scenarios
26
30
  - **Input Validation**: Zod schemas prevent injection attacks
27
31
  - **No Secret Leakage**: Generic session descriptions only (file permissions 0o600)
28
32
  - **No ReDoS**: Bounded regex patterns prevent catastrophic backtracking
@@ -360,6 +364,13 @@ await callTool("session_delete", {
360
364
  ```bash
361
365
  LLM_GATEWAY_APPROVAL_POLICY=strict node dist/index.js
362
366
  ```
367
+ - `LLM_GATEWAY_LOGS_DB`: Path to SQLite flight recorder database. Default: `~/.llm-cli-gateway/logs.db`. Set to empty string or `none` to disable logging.
368
+ ```bash
369
+ # Custom path
370
+ LLM_GATEWAY_LOGS_DB=/var/log/gateway/logs.db node dist/index.js
371
+ # Disable flight recorder
372
+ LLM_GATEWAY_LOGS_DB=none node dist/index.js
373
+ ```
363
374
 
364
375
  ### CLI-Specific Settings
365
376
 
@@ -368,6 +379,25 @@ Each CLI can be configured through its own configuration files:
368
379
  - Codex: `~/.codex/config.toml`
369
380
  - Gemini: `~/.gemini/config.json`
370
381
 
382
+ ## For Fans of Simon Willison
383
+
384
+ 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?"
385
+
386
+ **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.
387
+
388
+ **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:
389
+
390
+ datasette ~/.llm-cli-gateway/logs.db
391
+
392
+ **The `llm-gateway` plugin bridges both worlds.** Install it, and your existing `llm` workflows gain orchestration features without changing how you work:
393
+
394
+ llm install llm-gateway
395
+ llm -m gateway-claude "explain this function"
396
+
397
+ 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.
398
+
399
+ **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.
400
+
371
401
  ## Development
372
402
 
373
403
  ### Project Structure
@@ -83,7 +83,9 @@ export class ApprovalManager {
83
83
  // Canonicalize to handle scoped forms like "Read(*)", "Bash(git:*)"
84
84
  const canonicalized = request.disallowedTools.map(s => {
85
85
  const trimmed = s.trim();
86
- const cut = Math.min(...[trimmed.indexOf("("), trimmed.indexOf(":")].filter(i => i >= 0).concat([trimmed.length]));
86
+ const cut = Math.min(...[trimmed.indexOf("("), trimmed.indexOf(":")]
87
+ .filter(i => i >= 0)
88
+ .concat([trimmed.length]));
87
89
  return trimmed.slice(0, cut).trim();
88
90
  });
89
91
  const blockedCritical = criticalTools.filter(t => canonicalized.includes(t));
@@ -103,7 +105,8 @@ export class ApprovalManager {
103
105
  if (request.reviewIntegrity && request.reviewIntegrity.violations.length > 0) {
104
106
  for (const violation of request.reviewIntegrity.violations) {
105
107
  // Skip empty_allowed_tools and critical_tools_disallowed — already handled in context-dependent scoring above
106
- if (violation.type === "empty_allowed_tools" || violation.type === "critical_tools_disallowed")
108
+ if (violation.type === "empty_allowed_tools" ||
109
+ violation.type === "critical_tools_disallowed")
107
110
  continue;
108
111
  score += violation.score;
109
112
  reasons.push(`Review integrity: ${violation.detail}`);
@@ -128,12 +131,12 @@ export class ApprovalManager {
128
131
  bypassRequested: request.bypassRequested,
129
132
  fullAuto: request.fullAuto,
130
133
  metadata: request.metadata,
131
- reviewIntegrity: request.reviewIntegrity
134
+ reviewIntegrity: request.reviewIntegrity,
132
135
  };
133
136
  appendFileSync(this.logPath, `${JSON.stringify(record)}\n`, { encoding: "utf-8", mode: 0o600 });
134
137
  this.logger.info(`Approval decision: ${status} (score=${score}, policy=${policy})`, {
135
138
  cli: request.cli,
136
- operation: request.operation
139
+ operation: request.operation,
137
140
  });
138
141
  return record;
139
142
  }
@@ -1,6 +1,6 @@
1
1
  import { spawn } from "child_process";
2
2
  import { randomUUID } from "crypto";
3
- import { getExtendedPath, killProcessGroup, registerProcessGroup, unregisterProcessGroup } from "./executor.js";
3
+ import { getExtendedPath, killProcessGroup, registerProcessGroup, unregisterProcessGroup, } from "./executor.js";
4
4
  import { noopLogger } from "./logger.js";
5
5
  import { ProcessMonitor } from "./process-monitor.js";
6
6
  const MAX_OUTPUT_SIZE = 50 * 1024 * 1024;
@@ -12,7 +12,7 @@ function truncateText(value, maxChars) {
12
12
  }
13
13
  return {
14
14
  text: value.slice(value.length - maxChars),
15
- truncated: true
15
+ truncated: true,
16
16
  };
17
17
  }
18
18
  export class AsyncJobManager {
@@ -101,7 +101,7 @@ export class AsyncJobManager {
101
101
  cwd,
102
102
  detached: true,
103
103
  stdio: ["ignore", "pipe", "pipe"],
104
- env: { ...process.env, PATH: getExtendedPath() }
104
+ env: { ...process.env, PATH: getExtendedPath() },
105
105
  });
106
106
  if (child.pid)
107
107
  registerProcessGroup(child.pid);
@@ -133,7 +133,7 @@ export class AsyncJobManager {
133
133
  exited: false,
134
134
  metricsRecorded: false,
135
135
  outputFormat,
136
- cleanupGroup
136
+ cleanupGroup,
137
137
  };
138
138
  this.jobs.set(id, job);
139
139
  this.logger.info(`Job ${id} started for ${cli}`, { correlationId });
@@ -152,7 +152,9 @@ export class AsyncJobManager {
152
152
  job.error = `Process killed after ${idleTimeoutMs}ms of inactivity`;
153
153
  job.finishedAt = new Date().toISOString();
154
154
  killProcessGroup(job.process, "SIGTERM");
155
- this.logger.info(`Job ${id} killed due to inactivity (${idleTimeoutMs}ms)`, { correlationId });
155
+ this.logger.info(`Job ${id} killed due to inactivity (${idleTimeoutMs}ms)`, {
156
+ correlationId,
157
+ });
156
158
  this.emitMetrics(job);
157
159
  setTimeout(() => {
158
160
  if (!job.exited)
@@ -233,7 +235,7 @@ export class AsyncJobManager {
233
235
  stdout: stdout.text,
234
236
  stderr: stderr.text,
235
237
  stdoutTruncated: stdout.truncated,
236
- stderrTruncated: stderr.truncated
238
+ stderrTruncated: stderr.truncated,
237
239
  };
238
240
  }
239
241
  cancelJob(jobId) {
@@ -262,8 +264,11 @@ export class AsyncJobManager {
262
264
  for (const [id, job] of this.jobs) {
263
265
  if (job.status === "running") {
264
266
  result.push({
265
- jobId: id, cli: job.cli, status: job.status,
266
- pid: job.process.pid ?? null, startedAt: job.startedAt
267
+ jobId: id,
268
+ cli: job.cli,
269
+ status: job.status,
270
+ pid: job.process.pid ?? null,
271
+ startedAt: job.startedAt,
267
272
  });
268
273
  }
269
274
  }
@@ -279,7 +284,7 @@ export class AsyncJobManager {
279
284
  runningJobs: running.length,
280
285
  deadJobs: health.filter(h => h.isDead).length,
281
286
  zombieJobs: health.filter(h => h.isZombie).length,
282
- jobs: health
287
+ jobs: health,
283
288
  };
284
289
  }
285
290
  getJobOutputFormat(jobId) {
@@ -298,7 +303,7 @@ export class AsyncJobManager {
298
303
  stdoutBytes: Buffer.byteLength(job.stdout),
299
304
  stderrBytes: Buffer.byteLength(job.stderr),
300
305
  error: job.error,
301
- exited: job.exited
306
+ exited: job.exited,
302
307
  };
303
308
  }
304
309
  appendOutput(job, stream, chunk) {
@@ -312,7 +317,9 @@ export class AsyncJobManager {
312
317
  job.finishedAt = new Date().toISOString();
313
318
  job.clearIdleTimer?.();
314
319
  killProcessGroup(job.process, "SIGTERM");
315
- this.logger.info(`Job ${job.id} killed due to output overflow`, { correlationId: job.correlationId });
320
+ this.logger.info(`Job ${job.id} killed due to output overflow`, {
321
+ correlationId: job.correlationId,
322
+ });
316
323
  this.emitMetrics(job);
317
324
  setTimeout(() => {
318
325
  if (!job.exited)
@@ -1,4 +1,4 @@
1
- import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync, renameSync, openSync, fsyncSync, closeSync, chmodSync } from "fs";
1
+ import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync, renameSync, openSync, fsyncSync, closeSync, chmodSync, } from "fs";
2
2
  import { homedir } from "os";
3
3
  import { dirname, join } from "path";
4
4
  import { parse as parseToml } from "toml";
@@ -45,7 +45,7 @@ function readCodexServerConfig(server) {
45
45
  return {
46
46
  command,
47
47
  args,
48
- env
48
+ env,
49
49
  };
50
50
  }
51
51
  catch {
@@ -120,7 +120,7 @@ function toClaudeServerDef(server) {
120
120
  return {
121
121
  command,
122
122
  args,
123
- ...(Object.keys(env).length > 0 ? { env } : {})
123
+ ...(Object.keys(env).length > 0 ? { env } : {}),
124
124
  };
125
125
  }
126
126
  export function buildClaudeMcpConfig(servers) {
@@ -142,7 +142,10 @@ export function buildClaudeMcpConfig(servers) {
142
142
  try {
143
143
  mkdirSync(configDir, { recursive: true });
144
144
  const tempPath = `${configPath}.tmp.${process.pid}`;
145
- writeFileSync(tempPath, JSON.stringify({ mcpServers }, null, 2), { encoding: "utf-8", mode: 0o600 });
145
+ writeFileSync(tempPath, JSON.stringify({ mcpServers }, null, 2), {
146
+ encoding: "utf-8",
147
+ mode: 0o600,
148
+ });
146
149
  const fd = openSync(tempPath, "r+");
147
150
  try {
148
151
  fsyncSync(fd);
package/dist/config.js CHANGED
@@ -1,6 +1,11 @@
1
1
  import { z } from "zod";
2
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://" });
3
+ const DatabaseUrlSchema = z
4
+ .string()
5
+ .url()
6
+ .refine(url => url.startsWith("postgresql://") || url.startsWith("postgres://"), {
7
+ message: "Database URL must start with postgresql:// or postgres://",
8
+ });
4
9
  const RedisUrlSchema = z.string().url().startsWith("redis://");
5
10
  export const DEFAULT_SESSION_TTL_SECONDS = 2592000; // 30 days
6
11
  /**
@@ -15,11 +20,12 @@ export function loadConfig() {
15
20
  const cacheTtl = {
16
21
  session: 3600, // 1 hour
17
22
  activeSession: 1800, // 30 minutes
18
- sessionList: 120 // 2 minutes
23
+ sessionList: 120, // 2 minutes
19
24
  };
20
25
  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;
26
+ const sessionTtl = Number.isFinite(rawSessionTtl) && rawSessionTtl > 0
27
+ ? rawSessionTtl
28
+ : DEFAULT_SESSION_TTL_SECONDS;
23
29
  // If no database config, return base config (file-based storage)
24
30
  if (!databaseUrl || !redisUrl) {
25
31
  return { cacheTtl, sessionTtl };
@@ -39,18 +45,18 @@ export function loadConfig() {
39
45
  max: 10,
40
46
  idleTimeoutMillis: 30000,
41
47
  connectionTimeoutMillis: 5000,
42
- statementTimeout: 10000
43
- }
48
+ statementTimeout: 10000,
49
+ },
44
50
  },
45
51
  redis: {
46
52
  url: redisUrl,
47
53
  retryStrategy: {
48
54
  maxRetries: 3,
49
55
  initialDelay: 50,
50
- maxDelay: 2000
51
- }
56
+ maxDelay: 2000,
57
+ },
52
58
  },
53
59
  cacheTtl,
54
- sessionTtl
60
+ sessionTtl,
55
61
  };
56
62
  }
package/dist/db.js CHANGED
@@ -26,7 +26,7 @@ export class DatabaseConnection {
26
26
  max: this.config.database.pool.max,
27
27
  idleTimeoutMillis: this.config.database.pool.idleTimeoutMillis,
28
28
  connectionTimeoutMillis: this.config.database.pool.connectionTimeoutMillis,
29
- statement_timeout: this.config.database.pool.statementTimeout
29
+ statement_timeout: this.config.database.pool.statementTimeout,
30
30
  };
31
31
  this.pool = new Pool(poolConfig);
32
32
  // Test PostgreSQL connection
@@ -54,7 +54,7 @@ export class DatabaseConnection {
54
54
  // Reconnect on READONLY and ECONNRESET errors
55
55
  const targetErrors = ["READONLY", "ECONNRESET"];
56
56
  return targetErrors.some(targetError => err.message.includes(targetError));
57
- }
57
+ },
58
58
  };
59
59
  this.redis = new Redis(this.config.redis.url, redisOptions);
60
60
  // Test Redis connection
@@ -101,7 +101,7 @@ export class DatabaseConnection {
101
101
  async healthCheck() {
102
102
  const result = {
103
103
  postgres: { connected: false, latency: 0 },
104
- redis: { connected: false, latency: 0 }
104
+ redis: { connected: false, latency: 0 },
105
105
  };
106
106
  // Check PostgreSQL
107
107
  if (this.pool) {
@@ -137,7 +137,7 @@ export class DatabaseConnection {
137
137
  }
138
138
  this.logger.debug("Health check completed", {
139
139
  postgres: result.postgres.connected,
140
- redis: result.redis.connected
140
+ redis: result.redis.connected,
141
141
  });
142
142
  return result;
143
143
  }
package/dist/executor.js CHANGED
@@ -28,7 +28,7 @@ function getNvmPath() {
28
28
  try {
29
29
  const versions = readdirSync(nvmVersionsDir);
30
30
  cachedNvmPath = versions.length
31
- ? versions.map((version) => join(nvmVersionsDir, version, "bin")).join(":")
31
+ ? versions.map(version => join(nvmVersionsDir, version, "bin")).join(":")
32
32
  : null;
33
33
  }
34
34
  catch {
@@ -43,7 +43,7 @@ export function getExtendedPath() {
43
43
  join(home, ".local/bin"),
44
44
  dirname(process.execPath), // Current node's bin directory
45
45
  "/usr/local/bin",
46
- "/usr/bin"
46
+ "/usr/bin",
47
47
  ];
48
48
  // Add all nvm node version bin directories
49
49
  const nvmPath = getNvmPath();
@@ -75,7 +75,9 @@ export function killAllProcessGroups() {
75
75
  try {
76
76
  process.kill(-pid, "SIGTERM");
77
77
  }
78
- catch { /* ESRCH ok */ }
78
+ catch {
79
+ /* ESRCH ok */
80
+ }
79
81
  }
80
82
  return new Promise(resolve => {
81
83
  setTimeout(() => {
@@ -83,7 +85,9 @@ export function killAllProcessGroups() {
83
85
  try {
84
86
  process.kill(-pid, "SIGKILL");
85
87
  }
86
- catch { /* ESRCH ok */ }
88
+ catch {
89
+ /* ESRCH ok */
90
+ }
87
91
  }
88
92
  activeProcessGroups.clear();
89
93
  resolve();
@@ -129,7 +133,7 @@ export async function executeCli(command, args, options = {}) {
129
133
  cwd,
130
134
  detached: true,
131
135
  stdio: ["ignore", "pipe", "pipe"],
132
- env: { ...process.env, PATH: extendedPath }
136
+ env: { ...process.env, PATH: extendedPath },
133
137
  });
134
138
  if (proc.pid)
135
139
  registerProcessGroup(proc.pid);
@@ -152,7 +156,9 @@ export async function executeCli(command, args, options = {}) {
152
156
  if (proc.pid)
153
157
  unregisterProcessGroup(proc.pid);
154
158
  };
155
- const timeoutMs = typeof timeout === "number" && Number.isFinite(timeout) && timeout > 0 ? timeout : undefined;
159
+ const timeoutMs = typeof timeout === "number" && Number.isFinite(timeout) && timeout > 0
160
+ ? timeout
161
+ : undefined;
156
162
  const timeoutId = timeoutMs
157
163
  ? setTimeout(() => {
158
164
  timedOut = true;
@@ -166,7 +172,8 @@ export async function executeCli(command, args, options = {}) {
166
172
  : undefined;
167
173
  // Idle timeout: kill process if no stdout/stderr activity for idleMs
168
174
  const idleMs = typeof idleTimeout === "number" && Number.isFinite(idleTimeout) && idleTimeout > 0
169
- ? idleTimeout : undefined;
175
+ ? idleTimeout
176
+ : undefined;
170
177
  let idleTimerId;
171
178
  const resetIdleTimer = () => {
172
179
  if (!idleMs)
@@ -222,19 +229,19 @@ export async function executeCli(command, args, options = {}) {
222
229
  stderr += text;
223
230
  }
224
231
  };
225
- proc.stdout.on("data", (data) => {
232
+ proc.stdout.on("data", data => {
226
233
  if (settled) {
227
234
  return;
228
235
  }
229
236
  handleOutputChunk(data, "stdout");
230
237
  });
231
- proc.stderr.on("data", (data) => {
238
+ proc.stderr.on("data", data => {
232
239
  if (settled) {
233
240
  return;
234
241
  }
235
242
  handleOutputChunk(data, "stderr");
236
243
  });
237
- proc.on("close", (code) => {
244
+ proc.on("close", code => {
238
245
  exited = true;
239
246
  if (idleTimerId) {
240
247
  clearTimeout(idleTimerId);
@@ -253,7 +260,7 @@ export async function executeCli(command, args, options = {}) {
253
260
  const result = {
254
261
  stdout,
255
262
  stderr: stderr + `\nProcess timed out after ${timeoutMs}ms`,
256
- code: 124 // Standard timeout exit code
263
+ code: 124, // Standard timeout exit code
257
264
  };
258
265
  const error = new Error(result.stderr);
259
266
  error.code = 124;
@@ -265,7 +272,7 @@ export async function executeCli(command, args, options = {}) {
265
272
  const result = {
266
273
  stdout,
267
274
  stderr: stderr + `\nProcess killed after ${idleMs}ms of inactivity`,
268
- code: 125
275
+ code: 125,
269
276
  };
270
277
  const error = new Error(result.stderr);
271
278
  error.code = 125;
@@ -283,7 +290,7 @@ export async function executeCli(command, args, options = {}) {
283
290
  }
284
291
  resolve(result);
285
292
  });
286
- proc.on("error", (err) => {
293
+ proc.on("error", err => {
287
294
  exited = true;
288
295
  if (idleTimerId) {
289
296
  clearTimeout(idleTimerId);
@@ -0,0 +1,48 @@
1
+ export interface FlightLogStart {
2
+ correlationId: string;
3
+ cli: "claude" | "codex" | "gemini";
4
+ model: string;
5
+ prompt: string;
6
+ system?: string;
7
+ sessionId?: string;
8
+ asyncJobId?: string;
9
+ }
10
+ export interface FlightLogResult {
11
+ response: string;
12
+ inputTokens?: number;
13
+ outputTokens?: number;
14
+ durationMs: number;
15
+ retryCount: number;
16
+ circuitBreakerState: string;
17
+ costUsd?: number;
18
+ approvalDecision?: string;
19
+ optimizationApplied: boolean;
20
+ thinkingBlocks?: string[];
21
+ exitCode: number;
22
+ errorMessage?: string;
23
+ status: "completed" | "failed";
24
+ }
25
+ interface LoggerLike {
26
+ info: (message: string, ...args: any[]) => void;
27
+ error: (message: string, ...args: any[]) => void;
28
+ }
29
+ export declare function resolveFlightRecorderDbPath(): string | null;
30
+ export declare class FlightRecorder {
31
+ private db;
32
+ private insertStartTxn;
33
+ private updateCompleteTxn;
34
+ constructor(dbPath: string);
35
+ logStart(entry: FlightLogStart): void;
36
+ logComplete(correlationId: string, result: FlightLogResult): void;
37
+ flush(): void;
38
+ close(): void;
39
+ }
40
+ export declare class NoopFlightRecorder {
41
+ logStart(_entry: FlightLogStart): void;
42
+ logComplete(_correlationId: string, _result: FlightLogResult): void;
43
+ flush(): void;
44
+ close(): void;
45
+ }
46
+ export type FlightRecorderLike = FlightRecorder | NoopFlightRecorder;
47
+ export declare function createFlightRecorder(logger: LoggerLike): FlightRecorderLike;
48
+ export {};