mcp-compression-proxy 1.0.2 → 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.
@@ -14,10 +14,28 @@ export declare function handleInfo(socketPath: string, serverTool: string): Prom
14
14
  * mcp-cli call <server>/<tool> '<json>' — execute a tool
15
15
  */
16
16
  export declare function handleCall(socketPath: string, serverTool: string, jsonPayload: string): Promise<void>;
17
+ export declare function handlePayloadRead(socketPath: string, id: string, options?: {
18
+ offset?: number;
19
+ length?: number;
20
+ all?: boolean;
21
+ }): Promise<void>;
22
+ export declare function handlePayloadFind(socketPath: string, id: string, query: string): Promise<void>;
23
+ export declare function handleScript(socketPath: string, jsonPayload: string): Promise<void>;
17
24
  /**
18
25
  * mcp-cli stats — get compression/server statistics
19
26
  */
20
27
  export declare function handleStats(socketPath: string): Promise<void>;
28
+ /**
29
+ * Last `count` lines of a log file's contents.
30
+ *
31
+ * Kept as a pure function so it is testable without a real log on disk - the
32
+ * CLI entry point is excluded from coverage collection, this file is not.
33
+ */
34
+ export declare function tailLines(content: string, count: number): string;
35
+ /**
36
+ * mcp-cli doctor — validate config and report live backend health
37
+ */
38
+ export declare function handleDoctor(socketPath: string): Promise<void>;
21
39
  /**
22
40
  * mcp-cli daemon status — show daemon status
23
41
  */
@@ -1,4 +1,5 @@
1
1
  import { sendRequest, isDaemonRunning } from './ipc-client.js';
2
+ import { loadJSONServers } from '../config/loader.js';
2
3
  /**
3
4
  * Format tool entries as aligned plain text:
4
5
  * server/tool_name Short description
@@ -7,9 +8,9 @@ function formatToolList(tools) {
7
8
  if (tools.length === 0)
8
9
  return 'No tools found.';
9
10
  // Calculate column width for alignment
10
- const names = tools.map(t => `${t.server}/${t.tool}`);
11
- const maxLen = Math.min(Math.max(...names.map(n => n.length)), 40);
12
- const lines = tools.map(t => {
11
+ const names = tools.map((t) => `${t.server}/${t.tool}`);
12
+ const maxLen = Math.min(Math.max(...names.map((n) => n.length)), 40);
13
+ const lines = tools.map((t) => {
13
14
  const name = `${t.server}/${t.tool}`;
14
15
  const padded = name.padEnd(maxLen + 4);
15
16
  return `${padded}${t.description}`;
@@ -28,7 +29,7 @@ export async function handleTools(socketPath) {
28
29
  const result = response.result;
29
30
  console.log(formatToolList(result.tools));
30
31
  // Summary line
31
- const serverCount = new Set(result.tools.map(t => t.server)).size;
32
+ const serverCount = new Set(result.tools.map((t) => t.server)).size;
32
33
  console.log(`\n(${result.count} tools across ${serverCount} servers)`);
33
34
  }
34
35
  /**
@@ -76,7 +77,7 @@ export async function handleInfo(socketPath, serverTool) {
76
77
  export async function handleCall(socketPath, serverTool, jsonPayload) {
77
78
  const slashIndex = serverTool.indexOf('/');
78
79
  if (slashIndex === -1) {
79
- console.error('Usage: mcp-cli call <server>/<tool> \'<json_payload>\'');
80
+ console.error("Usage: mcp-cli call <server>/<tool> '<json_payload>'");
80
81
  process.exit(1);
81
82
  }
82
83
  const server = serverTool.slice(0, slashIndex);
@@ -107,6 +108,49 @@ export async function handleCall(socketPath, serverTool, jsonPayload) {
107
108
  }
108
109
  console.log(result.output);
109
110
  }
111
+ export async function handlePayloadRead(socketPath, id, options = {}) {
112
+ const response = await sendRequest(socketPath, 'payload-read', {
113
+ id,
114
+ ...options,
115
+ });
116
+ if (response.error) {
117
+ console.error(`Error: ${response.error.message}`);
118
+ process.exit(1);
119
+ }
120
+ console.log(JSON.stringify(response.result, null, 2));
121
+ }
122
+ export async function handlePayloadFind(socketPath, id, query) {
123
+ const response = await sendRequest(socketPath, 'payload-find', {
124
+ id,
125
+ query,
126
+ });
127
+ if (response.error) {
128
+ console.error(`Error: ${response.error.message}`);
129
+ process.exit(1);
130
+ }
131
+ console.log(JSON.stringify(response.result, null, 2));
132
+ }
133
+ export async function handleScript(socketPath, jsonPayload) {
134
+ let parsed;
135
+ try {
136
+ parsed = JSON.parse(jsonPayload);
137
+ }
138
+ catch {
139
+ console.error('Error: Invalid JSON script');
140
+ process.exit(1);
141
+ }
142
+ const steps = Array.isArray(parsed) ? parsed : parsed?.steps;
143
+ if (!Array.isArray(steps)) {
144
+ console.error('Error: Script must be an array of steps or an object with a steps array');
145
+ process.exit(1);
146
+ }
147
+ const response = await sendRequest(socketPath, 'script', { steps });
148
+ if (response.error) {
149
+ console.error(`Error: ${response.error.message}`);
150
+ process.exit(1);
151
+ }
152
+ console.log(JSON.stringify(response.result, null, 2));
153
+ }
110
154
  /**
111
155
  * mcp-cli stats — get compression/server statistics
112
156
  */
@@ -118,6 +162,87 @@ export async function handleStats(socketPath) {
118
162
  }
119
163
  console.log(JSON.stringify(response.result, null, 2));
120
164
  }
165
+ /**
166
+ * Last `count` lines of a log file's contents.
167
+ *
168
+ * Kept as a pure function so it is testable without a real log on disk - the
169
+ * CLI entry point is excluded from coverage collection, this file is not.
170
+ */
171
+ export function tailLines(content, count) {
172
+ if (!content)
173
+ return '';
174
+ // A trailing newline is a terminator, not an empty final line; without this
175
+ // `-n 1` would return a blank.
176
+ const lines = content.replace(/\n$/, '').split('\n');
177
+ return lines.slice(Math.max(0, lines.length - count)).join('\n');
178
+ }
179
+ /**
180
+ * mcp-cli doctor — validate config and report live backend health
181
+ */
182
+ export async function handleDoctor(socketPath) {
183
+ let healthy = true;
184
+ console.log('Configuration');
185
+ // Uncached on purpose: a doctor reports what is on disk right now.
186
+ // A schema error throws, and an unformatted stack trace here would bury the
187
+ // one thing the user came for.
188
+ let configuredServers = [];
189
+ try {
190
+ const config = loadJSONServers();
191
+ if (!config) {
192
+ console.log(' ! No servers.json found (user or project level)');
193
+ console.log(' The proxy will start with management tools only.');
194
+ healthy = false;
195
+ }
196
+ else {
197
+ configuredServers = config.servers.map((server) => server.name);
198
+ const disabled = config.servers.filter((server) => server.enabled === false).length;
199
+ console.log(` ✓ Loaded ${config.servers.length} server(s)${disabled ? `, ${disabled} disabled` : ''}`);
200
+ if (config.excludePatterns.length > 0) {
201
+ console.log(` excludeTools: ${config.excludePatterns.join(', ')}`);
202
+ }
203
+ if (config.noCompressPatterns.length > 0) {
204
+ console.log(` noCompressTools: ${config.noCompressPatterns.join(', ')}`);
205
+ }
206
+ }
207
+ }
208
+ catch (error) {
209
+ console.log(' ✗ Invalid configuration');
210
+ for (const line of String(error instanceof Error ? error.message : error).split('\n')) {
211
+ console.log(` ${line}`);
212
+ }
213
+ console.log('\nFix the configuration before checking backend health.');
214
+ process.exit(1);
215
+ }
216
+ console.log('\nBackends');
217
+ const response = await sendRequest(socketPath, 'daemon-status');
218
+ if (response.error) {
219
+ console.log(` ✗ Daemon did not respond: ${response.error.message}`);
220
+ process.exit(1);
221
+ }
222
+ const status = response.result;
223
+ for (const server of status.servers) {
224
+ if (server.connected) {
225
+ console.log(` ✓ ${server.name}`);
226
+ }
227
+ else {
228
+ console.log(` ✗ ${server.name}: ${server.lastError || 'not connected'}`);
229
+ healthy = false;
230
+ }
231
+ }
232
+ // A server in the config that the daemon has no record of predates the
233
+ // daemon's own startup, so its warm connections are stale.
234
+ const known = new Set(status.servers.map((server) => server.name));
235
+ const missing = configuredServers.filter((name) => !known.has(name));
236
+ if (missing.length > 0) {
237
+ console.log(` ! Not known to the running daemon: ${missing.join(', ')}`);
238
+ console.log(' Restart it to pick them up: mcp-cli daemon restart');
239
+ healthy = false;
240
+ }
241
+ console.log(`\n${healthy ? 'All checks passed.' : 'Some checks failed (see above).'}`);
242
+ if (!healthy) {
243
+ process.exit(1);
244
+ }
245
+ }
121
246
  /**
122
247
  * mcp-cli daemon status — show daemon status
123
248
  */
@@ -136,16 +261,30 @@ export async function handleDaemonStatus(socketPath) {
136
261
  const hours = Math.floor(status.uptime / 3600);
137
262
  const minutes = Math.floor((status.uptime % 3600) / 60);
138
263
  const uptimeStr = hours > 0 ? `${hours}h ${minutes}m` : `${minutes}m`;
139
- console.log(`Daemon running (PID ${status.pid}, uptime ${uptimeStr})`);
140
- console.log(`Servers: ${status.connectedServers} connected, ${status.totalServers - status.connectedServers} failed`);
264
+ console.log(`Daemon running (PID ${status.pid}, release ${status.releaseId ?? 'legacy'}, uptime ${uptimeStr})`);
265
+ const failed = status.servers.filter((server) => server.state === 'failed');
266
+ const inactive = status.servers.filter((server) => !server.connected && server.state !== 'failed');
267
+ console.log(`Servers: ${status.connectedServers} connected, ${inactive.length} inactive, ${failed.length} failed`);
141
268
  console.log(`Tools: ${status.cachedToolCount} cached`);
142
269
  console.log(`Socket: ${status.socketPath}`);
143
- // Show failed servers
144
- const failed = status.servers.filter(s => !s.connected);
145
- if (failed.length > 0) {
146
- console.log('\nFailed servers:');
147
- for (const s of failed) {
148
- console.log(` - ${s.name}: ${s.lastError || 'unknown error'}`);
270
+ if (status.servers.length > 0) {
271
+ console.log('\nConnection lifecycle:');
272
+ for (const server of status.servers) {
273
+ const details = [
274
+ `state=${server.state ?? (server.connected ? 'ready' : 'failed')}`,
275
+ server.generation ? `generation=${server.generation}` : undefined,
276
+ server.connectionAgeSeconds !== undefined
277
+ ? `age=${server.connectionAgeSeconds}s`
278
+ : undefined,
279
+ `active=${server.activeCalls ?? 0}`,
280
+ `recycles=${server.recycleCount ?? 0}`,
281
+ `auth-resets=${server.authInvalidations ?? 0}`,
282
+ `failures=${server.consecutiveFailures ?? 0}`,
283
+ ].filter((value) => value !== undefined);
284
+ console.log(` - ${server.name}: ${details.join(', ')}`);
285
+ if (server.lastError) {
286
+ console.log(` last error: ${server.lastError}`);
287
+ }
149
288
  }
150
289
  }
151
290
  }
@@ -2,18 +2,18 @@
2
2
  import net from 'net';
3
3
  import fs from 'fs';
4
4
  import path from 'path';
5
- import { homedir } from 'os';
6
5
  import pino from 'pino';
7
6
  import { MCPClientManager } from '../mcp/client-manager.js';
7
+ import { callToolWithAuthRecovery } from '../mcp/tool-call-executor.js';
8
8
  import { CompressionCache } from '../services/compression-cache.js';
9
9
  import { SessionManager } from '../services/session-manager.js';
10
10
  import { StatsService } from '../services/stats-service.js';
11
- import { loadJSONServers, matchesIgnorePattern } from '../config/loader.js';
12
- import { interceptPayload } from './payload-interceptor.js';
13
- const BASE_DIR = path.join(homedir(), '.mcp-compression-proxy');
14
- const SOCKET_PATH = path.join(BASE_DIR, 'daemon.sock');
15
- const PID_FILE = path.join(BASE_DIR, 'daemon.pid');
16
- const LOG_FILE = path.join(BASE_DIR, 'daemon.log');
11
+ import { loadJSONServers, loadJSONServersCached, matchesIgnorePattern } from '../config/loader.js';
12
+ import { DEFAULT_PAYLOAD_THRESHOLD, PayloadStore } from './payload-interceptor.js';
13
+ import { runCallScript } from '../mcp/call-script.js';
14
+ import { getDaemonRuntimePaths } from './runtime-paths.js';
15
+ const RUNTIME_PATHS = getDaemonRuntimePaths();
16
+ const { baseDir: BASE_DIR, socketPath: SOCKET_PATH, pidFile: PID_FILE, readyFile: READY_FILE, logFile: LOG_FILE, payloadDir: PAYLOAD_DIR, releaseId: RELEASE_ID, } = RUNTIME_PATHS;
17
17
  export function getSocketPath() {
18
18
  return SOCKET_PATH;
19
19
  }
@@ -33,6 +33,12 @@ async function startDaemon() {
33
33
  // mkdirSync ignores `mode` when the directory already exists, so an
34
34
  // upgrade from a previous version still gets tightened.
35
35
  fs.chmodSync(BASE_DIR, 0o700);
36
+ for (const runtimePath of [SOCKET_PATH, PID_FILE, READY_FILE, LOG_FILE]) {
37
+ fs.mkdirSync(path.dirname(runtimePath), {
38
+ recursive: true,
39
+ mode: 0o700,
40
+ });
41
+ }
36
42
  const logger = pino({
37
43
  name: 'mcp-cli-daemon',
38
44
  level: process.env.LOG_LEVEL || 'info',
@@ -48,11 +54,15 @@ async function startDaemon() {
48
54
  },
49
55
  });
50
56
  const startTime = Date.now();
51
- logger.info({ pid: process.pid }, 'Daemon starting');
57
+ logger.info({ pid: process.pid, releaseId: RELEASE_ID }, 'Daemon starting');
52
58
  // Write PID file
53
59
  fs.writeFileSync(PID_FILE, String(process.pid), 'utf-8');
54
60
  // Initialize services (reusing existing components)
55
61
  const clientManager = new MCPClientManager(logger);
62
+ const payloadStore = new PayloadStore({
63
+ directory: PAYLOAD_DIR,
64
+ removeDirectoryOnDestroy: false,
65
+ });
56
66
  const compressionCache = new CompressionCache(logger);
57
67
  const sessionManager = new SessionManager(logger);
58
68
  const statsService = new StatsService(logger, clientManager, compressionCache, sessionManager);
@@ -73,10 +83,19 @@ async function startDaemon() {
73
83
  if (rawConfig.cli && typeof rawConfig.cli === 'object') {
74
84
  cliConfig = rawConfig.cli;
75
85
  }
76
- const enabledServers = config.servers.filter(s => s.enabled !== false);
86
+ const enabledServers = config.servers.filter((s) => s.enabled !== false);
77
87
  logger.info({ total: config.servers.length, enabled: enabledServers.length }, 'Initializing backend MCP servers');
78
88
  try {
79
- await clientManager.initializeServers(enabledServers, config.defaultTimeout);
89
+ // inheritEnv must be passed here too: the config watch below reconciles
90
+ // with it, so omitting it makes every connection's stored config differ
91
+ // from the reconciled one on the first tick - bouncing every healthy
92
+ // backend ~5s after startup.
93
+ await clientManager.initializeServers(enabledServers, config.defaultTimeout, config.inheritEnv, {
94
+ softMaxConnectionAgeSeconds: config.softMaxConnectionAgeSeconds,
95
+ hardMaxConnectionAgeSeconds: config.hardMaxConnectionAgeSeconds,
96
+ authErrorPatterns: config.authErrorPatterns,
97
+ authRetryTools: config.authRetryTools,
98
+ });
80
99
  logger.info('Backend MCP servers initialization complete');
81
100
  }
82
101
  catch (error) {
@@ -86,6 +105,13 @@ async function startDaemon() {
86
105
  else {
87
106
  logger.warn('No configuration found. Daemon started with no backend servers.');
88
107
  }
108
+ // The daemon outlives any single CLI invocation, so it is the entry point
109
+ // that most needs this: editing servers.json would otherwise mean stopping a
110
+ // daemon that is holding warm connections. Cached loader rather than the
111
+ // uncached one used above - the poll is what its mtime fingerprint is for.
112
+ clientManager.startConfigWatch(loadJSONServersCached, undefined, (reloaded) => {
113
+ compressionCache.setNoCompressPatterns(reloaded.noCompressPatterns);
114
+ });
89
115
  // Clean up stale socket file if it exists
90
116
  if (fs.existsSync(SOCKET_PATH)) {
91
117
  fs.unlinkSync(SOCKET_PATH);
@@ -97,18 +123,18 @@ async function startDaemon() {
97
123
  try {
98
124
  switch (method) {
99
125
  case 'tools': {
100
- const clients = clientManager.getConnectedClients();
126
+ const serverNames = clientManager.getConfiguredServerNames();
101
127
  const toolEntries = [];
102
- for (const { name, client } of clients) {
128
+ for (const name of serverNames) {
103
129
  try {
104
- const result = await client.listTools();
130
+ const result = await clientManager.withClient(name, async ({ client }) => client.listTools());
105
131
  for (const tool of result.tools) {
106
132
  const fullName = `${name}__${tool.name}`;
107
133
  if (matchesIgnorePattern(fullName, excludePatterns))
108
134
  continue;
109
- const desc = compressionCache.getCompressedDescription(name, tool.name)
110
- || tool.description
111
- || '';
135
+ const desc = compressionCache.getCompressedDescription(name, tool.name) ||
136
+ tool.description ||
137
+ '';
112
138
  // Truncate to ~60 chars for compact listing
113
139
  const shortDesc = desc.length > 60 ? desc.slice(0, 57) + '...' : desc;
114
140
  toolEntries.push({ server: name, tool: tool.name, description: shortDesc });
@@ -122,18 +148,18 @@ async function startDaemon() {
122
148
  }
123
149
  case 'search': {
124
150
  const query = String(params?.query || '').toLowerCase();
125
- const clients = clientManager.getConnectedClients();
151
+ const serverNames = clientManager.getConfiguredServerNames();
126
152
  const matches = [];
127
- for (const { name, client } of clients) {
153
+ for (const name of serverNames) {
128
154
  try {
129
- const result = await client.listTools();
155
+ const result = await clientManager.withClient(name, async ({ client }) => client.listTools());
130
156
  for (const tool of result.tools) {
131
157
  const fullName = `${name}__${tool.name}`;
132
158
  if (matchesIgnorePattern(fullName, excludePatterns))
133
159
  continue;
134
- const desc = compressionCache.getCompressedDescription(name, tool.name)
135
- || tool.description
136
- || '';
160
+ const desc = compressionCache.getCompressedDescription(name, tool.name) ||
161
+ tool.description ||
162
+ '';
137
163
  const searchText = `${name}/${tool.name} ${desc}`.toLowerCase();
138
164
  if (searchText.includes(query)) {
139
165
  const shortDesc = desc.length > 60 ? desc.slice(0, 57) + '...' : desc;
@@ -150,15 +176,17 @@ async function startDaemon() {
150
176
  case 'info': {
151
177
  const serverName = String(params?.server || '');
152
178
  const toolName = String(params?.tool || '');
153
- const client = clientManager.getClient(serverName);
154
- if (!client) {
155
- return { id, error: { code: -1, message: `Server '${serverName}' not found or not connected` } };
156
- }
157
179
  try {
158
- const result = await client.listTools();
159
- const tool = result.tools.find(t => t.name === toolName);
180
+ const result = await clientManager.withClient(serverName, async ({ client }) => client.listTools());
181
+ const tool = result.tools.find((t) => t.name === toolName);
160
182
  if (!tool) {
161
- return { id, error: { code: -1, message: `Tool '${toolName}' not found on server '${serverName}'` } };
183
+ return {
184
+ id,
185
+ error: {
186
+ code: -1,
187
+ message: `Tool '${toolName}' not found on server '${serverName}'`,
188
+ },
189
+ };
162
190
  }
163
191
  return {
164
192
  id,
@@ -179,28 +207,69 @@ async function startDaemon() {
179
207
  const serverName = String(params?.server || '');
180
208
  const toolName = String(params?.tool || '');
181
209
  const args = (params?.arguments || {});
182
- const threshold = cliConfig.payloadThreshold ?? 500;
183
- const client = clientManager.getClient(serverName);
184
- if (!client) {
185
- return { id, error: { code: -1, message: `Server '${serverName}' not found or not connected` } };
186
- }
210
+ const threshold = cliConfig.payloadThreshold ?? DEFAULT_PAYLOAD_THRESHOLD;
187
211
  try {
188
- const result = await client.callTool({ name: toolName, arguments: args });
212
+ const result = await callToolWithAuthRecovery(clientManager, logger, serverName, toolName, args);
189
213
  const content = result.content;
190
214
  // Extract text content
191
215
  // flatMap rather than filter+map: filter does not narrow the
192
216
  // element type, which is why this needed a non-null assertion.
193
- const textParts = content.flatMap((c) => c.type === 'text' && c.text ? [c.text] : []);
217
+ const textParts = content.flatMap((c) => (c.type === 'text' && c.text ? [c.text] : []));
194
218
  const fullOutput = textParts.join('\n');
195
219
  // Apply payload interception
196
- const output = interceptPayload(fullOutput, threshold);
197
- return { id, result: { output, isError: result.isError } };
220
+ const captured = payloadStore.capture(fullOutput, threshold);
221
+ return {
222
+ id,
223
+ result: {
224
+ output: captured.output,
225
+ isError: result.isError,
226
+ payload: captured.reference,
227
+ },
228
+ };
198
229
  }
199
230
  catch (error) {
200
231
  const msg = error instanceof Error ? error.message : 'Unknown error';
201
232
  return { id, error: { code: -1, message: msg } };
202
233
  }
203
234
  }
235
+ case 'payload-read': {
236
+ const payloadId = String(params?.id || '');
237
+ const result = payloadStore.read(payloadId, {
238
+ offset: params?.offset,
239
+ length: params?.length,
240
+ all: params?.all,
241
+ });
242
+ return { id, result };
243
+ }
244
+ case 'payload-find': {
245
+ const payloadId = String(params?.id || '');
246
+ const query = String(params?.query || '');
247
+ const result = payloadStore.find(payloadId, query, {
248
+ caseSensitive: params?.caseSensitive,
249
+ maxMatches: params?.maxMatches,
250
+ contextChars: params?.contextChars,
251
+ });
252
+ return { id, result };
253
+ }
254
+ case 'script': {
255
+ const steps = params?.steps;
256
+ if (!Array.isArray(steps)) {
257
+ return {
258
+ id,
259
+ error: { code: -1, message: 'Script steps must be an array' },
260
+ };
261
+ }
262
+ const threshold = cliConfig.payloadThreshold ?? DEFAULT_PAYLOAD_THRESHOLD;
263
+ const result = await runCallScript(steps, async (serverName, toolName, args) => {
264
+ const callResult = await callToolWithAuthRecovery(clientManager, logger, serverName, toolName, args);
265
+ const content = callResult.content;
266
+ const output = content
267
+ .flatMap((item) => (item.type === 'text' && item.text ? [item.text] : []))
268
+ .join('\n');
269
+ return { output, isError: callResult.isError };
270
+ }, payloadStore, threshold);
271
+ return { id, result };
272
+ }
204
273
  case 'stats': {
205
274
  const stats = await statsService.getStats({
206
275
  serverName: params?.serverName,
@@ -210,13 +279,14 @@ async function startDaemon() {
210
279
  }
211
280
  case 'daemon-status': {
212
281
  const statuses = clientManager.getServerStatuses();
213
- const connectedCount = statuses.filter(s => s.connected).length;
282
+ const connectedCount = statuses.filter((s) => s.connected).length;
214
283
  const cacheMetrics = compressionCache.getCacheMetrics();
215
284
  return {
216
285
  id,
217
286
  result: {
218
287
  running: true,
219
288
  pid: process.pid,
289
+ releaseId: RELEASE_ID,
220
290
  uptime: Math.floor((Date.now() - startTime) / 1000),
221
291
  servers: statuses,
222
292
  cachedToolCount: cacheMetrics.totalCached,
@@ -257,7 +327,10 @@ async function startDaemon() {
257
327
  .catch((error) => {
258
328
  const errResponse = {
259
329
  id: request.id,
260
- error: { code: -1, message: error instanceof Error ? error.message : 'Unknown error' },
330
+ error: {
331
+ code: -1,
332
+ message: error instanceof Error ? error.message : 'Unknown error',
333
+ },
261
334
  };
262
335
  socket.write(JSON.stringify(errResponse) + '\n');
263
336
  });
@@ -291,37 +364,48 @@ async function startDaemon() {
291
364
  try {
292
365
  fs.unlinkSync(PID_FILE);
293
366
  }
294
- catch { /* ignore */ }
367
+ catch {
368
+ /* ignore */
369
+ }
295
370
  process.exit(1);
296
371
  });
297
372
  server.listen(SOCKET_PATH, () => {
298
373
  logger.info({ socketPath: SOCKET_PATH, pid: process.pid }, 'Daemon listening');
299
374
  // Signal readiness by writing a ready marker
300
- const readyFile = path.join(BASE_DIR, 'daemon.ready');
301
- fs.writeFileSync(readyFile, String(Date.now()), 'utf-8');
375
+ fs.writeFileSync(READY_FILE, String(Date.now()), 'utf-8');
302
376
  });
303
377
  // Graceful shutdown
304
378
  function shutdown() {
305
379
  logger.info('Daemon shutting down');
306
380
  server.close();
307
- clientManager.disconnectAll().then(() => {
381
+ clientManager
382
+ .disconnectAll()
383
+ .then(() => {
308
384
  sessionManager.destroy();
385
+ payloadStore.destroy();
309
386
  // Clean up files
310
387
  try {
311
388
  fs.unlinkSync(SOCKET_PATH);
312
389
  }
313
- catch { /* ignore */ }
390
+ catch {
391
+ /* ignore */
392
+ }
314
393
  try {
315
394
  fs.unlinkSync(PID_FILE);
316
395
  }
317
- catch { /* ignore */ }
396
+ catch {
397
+ /* ignore */
398
+ }
318
399
  try {
319
- fs.unlinkSync(path.join(BASE_DIR, 'daemon.ready'));
400
+ fs.unlinkSync(READY_FILE);
401
+ }
402
+ catch {
403
+ /* ignore */
320
404
  }
321
- catch { /* ignore */ }
322
405
  logger.info('Daemon stopped');
323
406
  process.exit(0);
324
- }).catch(() => {
407
+ })
408
+ .catch(() => {
325
409
  process.exit(1);
326
410
  });
327
411
  }