@mcp-shark/mcp-shark 1.5.2 → 1.5.3

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/bin/mcp-shark.js CHANGED
@@ -6,7 +6,7 @@ import { dirname, join, resolve } from 'node:path';
6
6
  import { fileURLToPath } from 'node:url';
7
7
  import { Command } from 'commander';
8
8
  import open from 'open';
9
- import logger from '../shared/logger.js';
9
+ import logger from '#common/logger';
10
10
 
11
11
  const SERVER_URL = 'http://localhost:9853';
12
12
  const BROWSER_OPEN_DELAY = 1000;
@@ -40,46 +40,6 @@ function displayWelcomeBanner() {
40
40
 
41
41
  const uiDir = join(rootDir, 'ui');
42
42
  const distDir = join(uiDir, 'dist');
43
- const rootNodeModules = join(rootDir, 'node_modules');
44
-
45
- /**
46
- * Run a command and return a promise that resolves when it completes
47
- */
48
- function runCommand(command, args, options) {
49
- return new Promise((resolve, reject) => {
50
- const process = spawn(command, args, {
51
- ...options,
52
- stdio: 'inherit',
53
- shell: true,
54
- });
55
-
56
- process.on('close', (code) => {
57
- if (code !== 0) {
58
- reject(new Error(`Command failed with exit code ${code}`));
59
- } else {
60
- resolve();
61
- }
62
- });
63
-
64
- process.on('error', (error) => {
65
- reject(error);
66
- });
67
- });
68
- }
69
-
70
- /**
71
- * Install dependencies in the root directory
72
- */
73
- async function installDependencies() {
74
- logger.info('Installing dependencies...');
75
- try {
76
- await runCommand('npm', ['install'], { cwd: rootDir });
77
- logger.info('Dependencies installed successfully!\n');
78
- } catch (error) {
79
- logger.error({ error: error.message }, 'Failed to install dependencies');
80
- process.exit(1);
81
- }
82
- }
83
43
 
84
44
  /**
85
45
  * Validate that UI dist directory exists
@@ -172,15 +132,6 @@ function validateDirectories() {
172
132
  }
173
133
  }
174
134
 
175
- /**
176
- * Ensure dependencies are installed
177
- */
178
- async function ensureDependencies() {
179
- if (!existsSync(rootNodeModules)) {
180
- await installDependencies();
181
- }
182
- }
183
-
184
135
  /**
185
136
  * Main execution function
186
137
  */
@@ -197,9 +148,6 @@ async function main() {
197
148
  // Validate environment
198
149
  validateDirectories();
199
150
 
200
- // Ensure dependencies are installed
201
- await ensureDependencies();
202
-
203
151
  // Validate UI is built (pre-built files should be included in package)
204
152
  validateUIBuilt();
205
153
 
@@ -0,0 +1,108 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
2
+ import { homedir } from 'node:os';
3
+ import { join } from 'node:path';
4
+
5
+ const WORKING_DIRECTORY_NAME = '.mcp-shark';
6
+ const MCP_CONFIG_NAME = 'mcps.json';
7
+ const APP_DB_DIR_NAME = 'db';
8
+ const APP_DB_FILE_NAME = 'mcp-shark.sqlite';
9
+ const HELP_STATE_NAME = 'help-state.json';
10
+
11
+ export function getWorkingDirectory() {
12
+ return join(homedir(), WORKING_DIRECTORY_NAME);
13
+ }
14
+
15
+ export function getDatabasePath() {
16
+ return join(getWorkingDirectory(), APP_DB_DIR_NAME);
17
+ }
18
+
19
+ export function getDatabaseFile() {
20
+ return join(getDatabasePath(), APP_DB_FILE_NAME);
21
+ }
22
+
23
+ export function createWorkingDirectorySpaces() {
24
+ const workingDirectory = getWorkingDirectory();
25
+ if (!existsSync(workingDirectory)) {
26
+ mkdirSync(workingDirectory, { recursive: true });
27
+ }
28
+ }
29
+
30
+ export function createDatabaseSpaces() {
31
+ createWorkingDirectorySpaces();
32
+ const databasePath = getDatabasePath();
33
+ if (!existsSync(databasePath)) {
34
+ mkdirSync(databasePath, { recursive: true });
35
+ const databaseFile = getDatabaseFile();
36
+ if (!existsSync(databaseFile)) {
37
+ writeFileSync(databaseFile, '');
38
+ }
39
+ }
40
+ }
41
+
42
+ export function getMcpConfigPath() {
43
+ return join(getWorkingDirectory(), MCP_CONFIG_NAME);
44
+ }
45
+
46
+ export function prepareAppDataSpaces() {
47
+ createWorkingDirectorySpaces();
48
+ createDatabaseSpaces();
49
+ }
50
+
51
+ export function getHelpStatePath() {
52
+ return join(getWorkingDirectory(), HELP_STATE_NAME);
53
+ }
54
+
55
+ export function readHelpState() {
56
+ try {
57
+ const helpStatePath = getHelpStatePath();
58
+ if (existsSync(helpStatePath)) {
59
+ const content = readFileSync(helpStatePath, 'utf8');
60
+ const state = JSON.parse(content);
61
+ // Ensure we have the expected structure
62
+ return {
63
+ dismissed: state.dismissed || false,
64
+ tourCompleted: state.tourCompleted || false,
65
+ dismissedAt: state.dismissedAt || null,
66
+ version: state.version || '1.0.0',
67
+ };
68
+ }
69
+ return {
70
+ dismissed: false,
71
+ tourCompleted: false,
72
+ dismissedAt: null,
73
+ version: '1.0.0',
74
+ };
75
+ } catch (_error) {
76
+ // Error reading help state - return defaults
77
+ return {
78
+ dismissed: false,
79
+ tourCompleted: false,
80
+ dismissedAt: null,
81
+ version: '1.0.0',
82
+ };
83
+ }
84
+ }
85
+
86
+ export function writeHelpState(state) {
87
+ try {
88
+ const helpStatePath = getHelpStatePath();
89
+ prepareAppDataSpaces(); // Ensure directory exists
90
+
91
+ // Merge with existing state to preserve other fields
92
+ const existingState = readHelpState();
93
+ const newState = {
94
+ ...existingState,
95
+ ...state,
96
+ dismissedAt:
97
+ state.dismissed || state.tourCompleted
98
+ ? new Date().toISOString()
99
+ : existingState.dismissedAt,
100
+ version: '1.0.0',
101
+ };
102
+
103
+ writeFileSync(helpStatePath, JSON.stringify(newState, null, 2));
104
+ return true;
105
+ } catch (_error) {
106
+ return false;
107
+ }
108
+ }
@@ -0,0 +1,132 @@
1
+ import * as fs from 'node:fs';
2
+ import * as path from 'node:path';
3
+ import Database from 'better-sqlite3';
4
+
5
+ function createTables(db) {
6
+ db.exec(`
7
+ -- Packet capture table
8
+ -- Each HTTP request/response is stored as a packet for forensic analysis
9
+ CREATE TABLE IF NOT EXISTS packets (
10
+ frame_number INTEGER PRIMARY KEY AUTOINCREMENT,
11
+
12
+ -- Timestamps (nanosecond precision)
13
+ timestamp_ns INTEGER NOT NULL, -- Unix timestamp in nanoseconds
14
+ timestamp_iso TEXT NOT NULL, -- ISO 8601 formatted timestamp for readability
15
+
16
+ -- Packet direction and protocol
17
+ direction TEXT NOT NULL CHECK(direction IN ('request', 'response')),
18
+ protocol TEXT NOT NULL DEFAULT 'HTTP',
19
+
20
+ -- Session identification (normalized from various header formats)
21
+ session_id TEXT, -- Normalized session ID (from mcp-session-id, Mcp-Session-Id, or X-MCP-Session-Id)
22
+
23
+ -- HTTP metadata
24
+ method TEXT, -- HTTP method (GET, POST, etc.)
25
+ url TEXT, -- Request URL/path
26
+ status_code INTEGER, -- HTTP status code (for responses)
27
+
28
+ -- Headers and body
29
+ headers_json TEXT NOT NULL, -- Full HTTP headers as JSON
30
+ body_raw TEXT, -- Raw body content
31
+ body_json TEXT, -- Parsed JSON body (if applicable)
32
+
33
+ -- JSON-RPC metadata (for correlation)
34
+ jsonrpc_id TEXT, -- JSON-RPC request ID
35
+ jsonrpc_method TEXT, -- JSON-RPC method (e.g., 'tools/list', 'tools/call')
36
+ jsonrpc_result TEXT, -- JSON-RPC result (for responses, as JSON string)
37
+ jsonrpc_error TEXT, -- JSON-RPC error (for error responses, as JSON string)
38
+
39
+ -- Packet metadata
40
+ length INTEGER NOT NULL, -- Total packet size in bytes
41
+ info TEXT, -- Summary info for quick viewing
42
+
43
+ -- Network metadata
44
+ user_agent TEXT, -- User agent string
45
+ remote_address TEXT, -- Remote IP address
46
+ host TEXT -- Host header value
47
+ );
48
+
49
+ -- Conversations table - correlates request/response pairs
50
+ CREATE TABLE IF NOT EXISTS conversations (
51
+ conversation_id INTEGER PRIMARY KEY AUTOINCREMENT,
52
+ request_frame_number INTEGER NOT NULL,
53
+ response_frame_number INTEGER,
54
+ session_id TEXT,
55
+ jsonrpc_id TEXT,
56
+ method TEXT,
57
+ request_timestamp_ns INTEGER NOT NULL,
58
+ response_timestamp_ns INTEGER,
59
+ duration_ms REAL, -- Round-trip time in milliseconds
60
+ status TEXT DEFAULT 'pending' CHECK(status IN ('pending', 'completed', 'timeout', 'error')),
61
+
62
+ FOREIGN KEY (request_frame_number) REFERENCES packets(frame_number),
63
+ FOREIGN KEY (response_frame_number) REFERENCES packets(frame_number)
64
+ );
65
+
66
+ -- Sessions table - tracks session metadata
67
+ CREATE TABLE IF NOT EXISTS sessions (
68
+ session_id TEXT PRIMARY KEY,
69
+ first_seen_ns INTEGER NOT NULL,
70
+ last_seen_ns INTEGER NOT NULL,
71
+ packet_count INTEGER DEFAULT 0,
72
+ user_agent TEXT,
73
+ remote_address TEXT,
74
+ host TEXT
75
+ );
76
+
77
+ -- Create indexes for forensic analysis
78
+ CREATE INDEX IF NOT EXISTS idx_packets_timestamp ON packets(timestamp_ns);
79
+ CREATE INDEX IF NOT EXISTS idx_packets_session ON packets(session_id);
80
+ CREATE INDEX IF NOT EXISTS idx_packets_direction ON packets(direction);
81
+ CREATE INDEX IF NOT EXISTS idx_packets_jsonrpc_id ON packets(jsonrpc_id);
82
+ CREATE INDEX IF NOT EXISTS idx_packets_jsonrpc_method ON packets(jsonrpc_method);
83
+ CREATE INDEX IF NOT EXISTS idx_packets_method ON packets(method);
84
+ CREATE INDEX IF NOT EXISTS idx_packets_status_code ON packets(status_code);
85
+ CREATE INDEX IF NOT EXISTS idx_packets_session_timestamp ON packets(session_id, timestamp_ns);
86
+
87
+ CREATE INDEX IF NOT EXISTS idx_conversations_session ON conversations(session_id);
88
+ CREATE INDEX IF NOT EXISTS idx_conversations_jsonrpc_id ON conversations(jsonrpc_id);
89
+ CREATE INDEX IF NOT EXISTS idx_conversations_request_frame ON conversations(request_frame_number);
90
+ CREATE INDEX IF NOT EXISTS idx_conversations_response_frame ON conversations(response_frame_number);
91
+ CREATE INDEX IF NOT EXISTS idx_conversations_timestamp ON conversations(request_timestamp_ns);
92
+
93
+ CREATE INDEX IF NOT EXISTS idx_sessions_first_seen ON sessions(first_seen_ns);
94
+ CREATE INDEX IF NOT EXISTS idx_sessions_last_seen ON sessions(last_seen_ns);
95
+ `);
96
+
97
+ return db;
98
+ }
99
+
100
+ export function initDb(dbConnectionString) {
101
+ const db = new Database(dbConnectionString);
102
+ db.pragma('journal_mode = WAL');
103
+ db.pragma('foreign_keys = ON');
104
+
105
+ return createTables(db);
106
+ }
107
+
108
+ /**
109
+ * Open or create a database file, ensuring the directory exists
110
+ * Creates tables if the database is new or ensures they exist
111
+ */
112
+ export function openDb(dbPath) {
113
+ // Ensure the directory exists
114
+ const dbDir = path.dirname(dbPath);
115
+ if (!fs.existsSync(dbDir)) {
116
+ fs.mkdirSync(dbDir, { recursive: true });
117
+ }
118
+
119
+ // Check if database file exists
120
+ const _dbExists = fs.existsSync(dbPath);
121
+
122
+ // Open or create the database
123
+ const db = new Database(dbPath);
124
+ db.pragma('journal_mode = WAL');
125
+ db.pragma('foreign_keys = ON');
126
+
127
+ // Create tables if database is new or tables don't exist
128
+ // Even if database exists, ensure tables exist (in case schema changed)
129
+ createTables(db);
130
+
131
+ return db;
132
+ }
@@ -0,0 +1,349 @@
1
+ /**
2
+ * Helper functions for logging HTTP packets to the database for forensic analysis
3
+ */
4
+
5
+ function getTimestampNs() {
6
+ return Number(process.hrtime.bigint());
7
+ }
8
+
9
+ function getTimestampISO() {
10
+ return new Date().toISOString();
11
+ }
12
+
13
+ function calculateDurationMs(startNs, endNs) {
14
+ return (endNs - startNs) / 1_000_000;
15
+ }
16
+
17
+ /**
18
+ * Normalize session ID from various header formats
19
+ * Supports: mcp-session-id, Mcp-Session-Id, X-MCP-Session-Id
20
+ */
21
+ function normalizeSessionId(headers) {
22
+ if (!headers || typeof headers !== 'object') {
23
+ return null;
24
+ }
25
+
26
+ // Check all possible header formats (case-insensitive)
27
+ const sessionHeaderKeys = [
28
+ 'mcp-session-id',
29
+ 'Mcp-Session-Id',
30
+ 'X-MCP-Session-Id',
31
+ 'x-mcp-session-id',
32
+ 'MCP-Session-Id',
33
+ ];
34
+
35
+ for (const key of sessionHeaderKeys) {
36
+ if (headers[key]) {
37
+ return headers[key];
38
+ }
39
+ }
40
+
41
+ return null;
42
+ }
43
+
44
+ /**
45
+ * Extract JSON-RPC metadata from body
46
+ */
47
+ function extractJsonRpcMetadata(bodyJson) {
48
+ if (!bodyJson) {
49
+ return { id: null, method: null, result: null, error: null };
50
+ }
51
+
52
+ try {
53
+ const parsed = typeof bodyJson === 'string' ? JSON.parse(bodyJson) : bodyJson;
54
+ return {
55
+ id: parsed.id !== undefined ? String(parsed.id) : null,
56
+ method: parsed.method || null,
57
+ result: parsed.result ? JSON.stringify(parsed.result) : null,
58
+ error: parsed.error ? JSON.stringify(parsed.error) : null,
59
+ };
60
+ } catch {
61
+ return { id: null, method: null, result: null, error: null };
62
+ }
63
+ }
64
+
65
+ /**
66
+ * Generate info summary for quick packet identification
67
+ */
68
+ function generateInfo(direction, method, url, statusCode, jsonrpcMethod) {
69
+ if (direction === 'request') {
70
+ const rpcInfo = jsonrpcMethod ? ` ${jsonrpcMethod}` : '';
71
+ return `${method} ${url}${rpcInfo}`;
72
+ }
73
+
74
+ const rpcInfo = jsonrpcMethod ? ` ${jsonrpcMethod}` : '';
75
+ return `${statusCode}${rpcInfo}`;
76
+ }
77
+
78
+ /**
79
+ * Log an HTTP request packet
80
+ */
81
+ function logRequestPacket(db, options) {
82
+ const { method, url, headers = {}, body, userAgent = null, remoteAddress = null } = options;
83
+
84
+ const timestampNs = getTimestampNs();
85
+ const timestampISO = getTimestampISO();
86
+ const sessionId = normalizeSessionId(headers);
87
+ const host = headers.host || headers.Host || null;
88
+
89
+ // Prepare body data
90
+ const { bodyRaw, bodyJson } = (() => {
91
+ if (!body) {
92
+ return { bodyRaw: '', bodyJson: null };
93
+ }
94
+ if (typeof body === 'string') {
95
+ return { bodyRaw: body, bodyJson: body };
96
+ }
97
+ if (typeof body === 'object') {
98
+ const raw = JSON.stringify(body);
99
+ return { bodyRaw: raw, bodyJson: raw };
100
+ }
101
+ return { bodyRaw: '', bodyJson: null };
102
+ })();
103
+ const headersJson = JSON.stringify(headers);
104
+
105
+ // Extract JSON-RPC metadata
106
+ const jsonrpc = extractJsonRpcMetadata(bodyJson || bodyRaw);
107
+ const jsonrpcId = jsonrpc.id;
108
+ const jsonrpcMethod = jsonrpc.method;
109
+
110
+ // Calculate packet length
111
+ const length = Buffer.byteLength(headersJson, 'utf8') + Buffer.byteLength(bodyRaw, 'utf8');
112
+
113
+ // Generate info summary
114
+ const info = generateInfo('request', method, url, null, jsonrpcMethod);
115
+
116
+ const stmt = db.prepare(`
117
+ INSERT INTO packets (
118
+ timestamp_ns,
119
+ timestamp_iso,
120
+ direction,
121
+ protocol,
122
+ session_id,
123
+ method,
124
+ url,
125
+ headers_json,
126
+ body_raw,
127
+ body_json,
128
+ jsonrpc_id,
129
+ jsonrpc_method,
130
+ length,
131
+ info,
132
+ user_agent,
133
+ remote_address,
134
+ host
135
+ ) VALUES (?, ?, 'request', 'HTTP', ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
136
+ `);
137
+
138
+ const result = stmt.run(
139
+ timestampNs,
140
+ timestampISO,
141
+ sessionId,
142
+ method,
143
+ url,
144
+ headersJson,
145
+ bodyRaw,
146
+ bodyJson,
147
+ jsonrpcId,
148
+ jsonrpcMethod,
149
+ length,
150
+ info,
151
+ userAgent,
152
+ remoteAddress,
153
+ host
154
+ );
155
+
156
+ const frameNumber = result.lastInsertRowid;
157
+
158
+ // Update or create session record
159
+ if (sessionId) {
160
+ const sessionStmt = db.prepare(`
161
+ INSERT INTO sessions (session_id, first_seen_ns, last_seen_ns, packet_count, user_agent, remote_address, host)
162
+ VALUES (?, ?, ?, 1, ?, ?, ?)
163
+ ON CONFLICT(session_id) DO UPDATE SET
164
+ last_seen_ns = excluded.last_seen_ns,
165
+ packet_count = packet_count + 1,
166
+ user_agent = COALESCE(excluded.user_agent, user_agent),
167
+ remote_address = COALESCE(excluded.remote_address, remote_address),
168
+ host = COALESCE(excluded.host, host)
169
+ `);
170
+ sessionStmt.run(sessionId, timestampNs, timestampNs, userAgent, remoteAddress, host);
171
+ }
172
+
173
+ // Create conversation entry for request
174
+ if (jsonrpcId) {
175
+ const convStmt = db.prepare(`
176
+ INSERT INTO conversations (
177
+ request_frame_number,
178
+ session_id,
179
+ jsonrpc_id,
180
+ method,
181
+ request_timestamp_ns,
182
+ status
183
+ ) VALUES (?, ?, ?, ?, ?, 'pending')
184
+ `);
185
+ convStmt.run(frameNumber, sessionId, jsonrpcId, jsonrpcMethod || method, timestampNs);
186
+ }
187
+
188
+ return { frameNumber, timestampNs };
189
+ }
190
+
191
+ /**
192
+ * Log an HTTP response packet
193
+ */
194
+ function logResponsePacket(db, options) {
195
+ const {
196
+ statusCode,
197
+ headers = {},
198
+ body,
199
+ requestFrameNumber = null,
200
+ requestTimestampNs = null,
201
+ jsonrpcId = null,
202
+ userAgent = null,
203
+ remoteAddress = null,
204
+ } = options;
205
+
206
+ const timestampNs = getTimestampNs();
207
+ const timestampISO = getTimestampISO();
208
+ const sessionId = normalizeSessionId(headers);
209
+ const host = headers.host || headers.Host || null;
210
+
211
+ // Prepare body data
212
+ const { bodyRaw, bodyJson } = (() => {
213
+ if (!body) {
214
+ return { bodyRaw: '', bodyJson: null };
215
+ }
216
+ if (typeof body === 'string') {
217
+ return { bodyRaw: body, bodyJson: body };
218
+ }
219
+ if (typeof body === 'object') {
220
+ const raw = JSON.stringify(body);
221
+ return { bodyRaw: raw, bodyJson: raw };
222
+ }
223
+ return { bodyRaw: '', bodyJson: null };
224
+ })();
225
+ const headersJson = JSON.stringify(headers);
226
+
227
+ // Extract JSON-RPC metadata
228
+ const jsonrpc = extractJsonRpcMetadata(bodyJson || bodyRaw);
229
+ const jsonrpcIdFromBody = jsonrpc.id || jsonrpcId;
230
+ const jsonrpcMethod = jsonrpc.method;
231
+ const jsonrpcResult = jsonrpc.result;
232
+ const jsonrpcError = jsonrpc.error;
233
+
234
+ // Calculate packet length
235
+ const length = Buffer.byteLength(headersJson, 'utf8') + Buffer.byteLength(bodyRaw, 'utf8');
236
+
237
+ // Generate info summary
238
+ const info = generateInfo('response', null, null, statusCode, jsonrpcMethod);
239
+
240
+ const stmt = db.prepare(`
241
+ INSERT INTO packets (
242
+ timestamp_ns,
243
+ timestamp_iso,
244
+ direction,
245
+ protocol,
246
+ session_id,
247
+ status_code,
248
+ headers_json,
249
+ body_raw,
250
+ body_json,
251
+ jsonrpc_id,
252
+ jsonrpc_method,
253
+ jsonrpc_result,
254
+ jsonrpc_error,
255
+ length,
256
+ info,
257
+ user_agent,
258
+ remote_address,
259
+ host
260
+ ) VALUES (?, ?, 'response', 'HTTP', ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
261
+ `);
262
+
263
+ const result = stmt.run(
264
+ timestampNs,
265
+ timestampISO,
266
+ sessionId,
267
+ statusCode,
268
+ headersJson,
269
+ bodyRaw,
270
+ bodyJson,
271
+ jsonrpcIdFromBody,
272
+ jsonrpcMethod,
273
+ jsonrpcResult,
274
+ jsonrpcError,
275
+ length,
276
+ info,
277
+ userAgent,
278
+ remoteAddress,
279
+ host
280
+ );
281
+
282
+ const frameNumber = result.lastInsertRowid;
283
+
284
+ // Update session record
285
+ if (sessionId) {
286
+ const sessionStmt = db.prepare(`
287
+ INSERT INTO sessions (session_id, first_seen_ns, last_seen_ns, packet_count, user_agent, remote_address, host)
288
+ VALUES (?, ?, ?, 1, ?, ?, ?)
289
+ ON CONFLICT(session_id) DO UPDATE SET
290
+ last_seen_ns = excluded.last_seen_ns,
291
+ packet_count = packet_count + 1,
292
+ user_agent = COALESCE(excluded.user_agent, user_agent),
293
+ remote_address = COALESCE(excluded.remote_address, remote_address),
294
+ host = COALESCE(excluded.host, host)
295
+ `);
296
+ sessionStmt.run(sessionId, timestampNs, timestampNs, userAgent, remoteAddress, host);
297
+ }
298
+
299
+ // Update conversation entry with response
300
+ if (jsonrpcIdFromBody || requestFrameNumber) {
301
+ const durationMs = requestTimestampNs
302
+ ? calculateDurationMs(requestTimestampNs, timestampNs)
303
+ : null;
304
+
305
+ const status = statusCode >= 200 && statusCode < 300 ? 'completed' : 'error';
306
+
307
+ if (requestFrameNumber) {
308
+ // Update existing conversation
309
+ const updateConvStmt = db.prepare(`
310
+ UPDATE conversations
311
+ SET response_frame_number = ?,
312
+ response_timestamp_ns = ?,
313
+ duration_ms = ?,
314
+ status = ?
315
+ WHERE request_frame_number = ?
316
+ `);
317
+ updateConvStmt.run(frameNumber, timestampNs, durationMs, status, requestFrameNumber);
318
+ } else if (jsonrpcIdFromBody) {
319
+ // Try to find conversation by JSON-RPC ID
320
+ const findConvStmt = db.prepare(`
321
+ SELECT request_frame_number FROM conversations
322
+ WHERE jsonrpc_id = ? AND response_frame_number IS NULL
323
+ ORDER BY request_timestamp_ns DESC
324
+ LIMIT 1
325
+ `);
326
+ const conv = findConvStmt.get(jsonrpcIdFromBody);
327
+ if (conv) {
328
+ const updateConvStmt = db.prepare(`
329
+ UPDATE conversations
330
+ SET response_frame_number = ?,
331
+ response_timestamp_ns = ?,
332
+ duration_ms = ?,
333
+ status = ?
334
+ WHERE request_frame_number = ?
335
+ `);
336
+ updateConvStmt.run(frameNumber, timestampNs, durationMs, status, conv.request_frame_number);
337
+ }
338
+ }
339
+ }
340
+
341
+ return frameNumber;
342
+ }
343
+
344
+ export function getLogger(db) {
345
+ return {
346
+ logRequestPacket: logRequestPacket.bind(null, db),
347
+ logResponsePacket: logResponsePacket.bind(null, db),
348
+ };
349
+ }