@ebowwa/claude-code-config-mcp 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,210 @@
1
+ // ============================================================================
2
+ // Error Handling - Categorization, retry logic, and user-friendly messages
3
+ // ============================================================================
4
+
5
+ export enum ErrorCategory {
6
+ TRANSIENT = 'TRANSIENT',
7
+ PERMISSION = 'PERMISSION',
8
+ VALIDATION = 'VALIDATION',
9
+ SYSTEM = 'SYSTEM',
10
+ UNKNOWN = 'UNKNOWN',
11
+ }
12
+
13
+ /**
14
+ * Categorize an error for appropriate handling
15
+ */
16
+ export function categorizeError(error: Error | unknown): ErrorCategory {
17
+ if (!(error instanceof Error)) {
18
+ return ErrorCategory.UNKNOWN;
19
+ }
20
+
21
+ const message = error.message.toLowerCase();
22
+
23
+ // Permission errors
24
+ if (
25
+ message.includes('eacces') ||
26
+ message.includes('eperm') ||
27
+ message.includes('permission denied')
28
+ ) {
29
+ return ErrorCategory.PERMISSION;
30
+ }
31
+
32
+ // File not found (validation)
33
+ if (message.includes('enoent') || message.includes('no such file')) {
34
+ return ErrorCategory.VALIDATION;
35
+ }
36
+
37
+ // Transient errors (retryable)
38
+ if (
39
+ message.includes('eagain') ||
40
+ message.includes('etimedout') ||
41
+ message.includes('econnreset') ||
42
+ message.includes('econnrefused') ||
43
+ message.includes('timeout') ||
44
+ message.includes('temporary failure')
45
+ ) {
46
+ return ErrorCategory.TRANSIENT;
47
+ }
48
+
49
+ // System errors
50
+ if (
51
+ message.includes('enospc') ||
52
+ message.includes('disk full') ||
53
+ message.includes('eisdir') ||
54
+ message.includes('enotdir')
55
+ ) {
56
+ return ErrorCategory.SYSTEM;
57
+ }
58
+
59
+ return ErrorCategory.UNKNOWN;
60
+ }
61
+
62
+ /**
63
+ * Get user-friendly action message for an error
64
+ */
65
+ export function getUserAction(category: ErrorCategory): string | null {
66
+ switch (category) {
67
+ case ErrorCategory.PERMISSION:
68
+ return 'Check file permissions and ensure Claude Code has access to the config directory';
69
+ case ErrorCategory.VALIDATION:
70
+ return 'Verify the file path and try again';
71
+ case ErrorCategory.SYSTEM:
72
+ return 'Check system resources and Claude Code configuration';
73
+ case ErrorCategory.TRANSIENT:
74
+ return 'The operation failed temporarily. Please try again';
75
+ default:
76
+ return null;
77
+ }
78
+ }
79
+
80
+ /**
81
+ * Check if an error is retryable
82
+ */
83
+ export function isRetryable(error: Error | unknown): boolean {
84
+ return categorizeError(error) === ErrorCategory.TRANSIENT;
85
+ }
86
+
87
+ /**
88
+ * Retry configuration
89
+ */
90
+ export interface RetryOptions {
91
+ maxAttempts: number;
92
+ baseDelay: number;
93
+ maxDelay: number;
94
+ jitter: boolean;
95
+ }
96
+
97
+ const DEFAULT_RETRY_OPTIONS: RetryOptions = {
98
+ maxAttempts: 3,
99
+ baseDelay: 1000,
100
+ maxDelay: 10000,
101
+ jitter: true,
102
+ };
103
+
104
+ /**
105
+ * Retry an operation with exponential backoff
106
+ */
107
+ export async function retryWithBackoff<T>(
108
+ operation: () => Promise<T>,
109
+ options: RetryOptions = DEFAULT_RETRY_OPTIONS
110
+ ): Promise<T> {
111
+ let lastError: Error;
112
+
113
+ for (let attempt = 0; attempt < options.maxAttempts; attempt++) {
114
+ try {
115
+ return await operation();
116
+ } catch (error) {
117
+ lastError = error as Error;
118
+
119
+ // Don't retry if not retryable
120
+ if (!isRetryable(error)) {
121
+ throw error;
122
+ }
123
+
124
+ // Calculate delay with exponential backoff
125
+ const delay = Math.min(
126
+ options.baseDelay * Math.pow(2, attempt),
127
+ options.maxDelay
128
+ );
129
+
130
+ // Add jitter to prevent thundering herd
131
+ const finalDelay = options.jitter
132
+ ? delay + Math.random() * 1000
133
+ : delay;
134
+
135
+ // Wait before retry
136
+ await new Promise(resolve => setTimeout(resolve, finalDelay));
137
+ }
138
+ }
139
+
140
+ throw lastError!;
141
+ }
142
+
143
+ /**
144
+ * Create a standardized error response
145
+ */
146
+ export interface MCPError {
147
+ success: false;
148
+ error: {
149
+ category: ErrorCategory;
150
+ message: string;
151
+ retryable: boolean;
152
+ userAction: string | null;
153
+ };
154
+ }
155
+
156
+ export function createMCPError(error: Error | unknown): MCPError {
157
+ const category = categorizeError(error);
158
+ const message = error instanceof Error ? error.message : 'Unknown error';
159
+
160
+ return {
161
+ success: false,
162
+ error: {
163
+ category,
164
+ message,
165
+ retryable: category === ErrorCategory.TRANSIENT,
166
+ userAction: getUserAction(category),
167
+ },
168
+ };
169
+ }
170
+
171
+ /**
172
+ * Error codes for different scenarios
173
+ */
174
+ export enum ErrorCode {
175
+ FILE_NOT_FOUND = 'FILE_NOT_FOUND',
176
+ PERMISSION_DENIED = 'PERMISSION_DENIED',
177
+ INVALID_JSON = 'INVALID_JSON',
178
+ VALIDATION_FAILED = 'VALIDATION_FAILED',
179
+ PATH_TRAVERSAL = 'PATH_TRAVERSAL',
180
+ DISK_FULL = 'DISK_FULL',
181
+ LOCKED = 'LOCKED',
182
+ UNKNOWN = 'UNKNOWN',
183
+ }
184
+
185
+ /**
186
+ * Get error code from error
187
+ */
188
+ export function getErrorCode(error: Error | unknown): ErrorCode {
189
+ const category = categorizeError(error);
190
+
191
+ switch (category) {
192
+ case ErrorCategory.VALIDATION:
193
+ if (error instanceof Error && error.message.includes('JSON')) {
194
+ return ErrorCode.INVALID_JSON;
195
+ }
196
+ return ErrorCode.FILE_NOT_FOUND;
197
+ case ErrorCategory.PERMISSION:
198
+ return ErrorCode.PERMISSION_DENIED;
199
+ case ErrorCategory.SYSTEM:
200
+ if (error instanceof Error && error.message.includes('enospc')) {
201
+ return ErrorCode.DISK_FULL;
202
+ }
203
+ if (error instanceof Error && error.message.includes('etxtbsy')) {
204
+ return ErrorCode.LOCKED;
205
+ }
206
+ return ErrorCode.UNKNOWN;
207
+ default:
208
+ return ErrorCode.UNKNOWN;
209
+ }
210
+ }
@@ -0,0 +1,251 @@
1
+ "use strict";
2
+ // ============================================================================
3
+ // Atomic File Operations - Safe config file writes with backup support
4
+ // ============================================================================
5
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
6
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
7
+ return new (P || (P = Promise))(function (resolve, reject) {
8
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
9
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
10
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
11
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
12
+ });
13
+ };
14
+ var __generator = (this && this.__generator) || function (thisArg, body) {
15
+ var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype);
16
+ return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
17
+ function verb(n) { return function (v) { return step([n, v]); }; }
18
+ function step(op) {
19
+ if (f) throw new TypeError("Generator is already executing.");
20
+ while (g && (g = 0, op[0] && (_ = 0)), _) try {
21
+ if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
22
+ if (y = 0, t) op = [op[0] & 2, t.value];
23
+ switch (op[0]) {
24
+ case 0: case 1: t = op; break;
25
+ case 4: _.label++; return { value: op[1], done: false };
26
+ case 5: _.label++; y = op[1]; op = [0]; continue;
27
+ case 7: op = _.ops.pop(); _.trys.pop(); continue;
28
+ default:
29
+ if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
30
+ if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
31
+ if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
32
+ if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
33
+ if (t[2]) _.ops.pop();
34
+ _.trys.pop(); continue;
35
+ }
36
+ op = body.call(thisArg, _);
37
+ } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
38
+ if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
39
+ }
40
+ };
41
+ Object.defineProperty(exports, "__esModule", { value: true });
42
+ exports.atomicWrite = atomicWrite;
43
+ exports.safeReadFile = safeReadFile;
44
+ exports.isFileWritable = isFileWritable;
45
+ exports.normalizeLineEndings = normalizeLineEndings;
46
+ exports.stripBOM = stripBOM;
47
+ exports.formatJSON = formatJSON;
48
+ exports.safeJSONParse = safeJSONParse;
49
+ var promises_1 = require("node:fs/promises");
50
+ var node_fs_1 = require("node:fs");
51
+ var node_path_1 = require("node:path");
52
+ var node_os_1 = require("node:os");
53
+ var node_crypto_1 = require("node:crypto");
54
+ /**
55
+ * Atomic write implementation
56
+ * Writes to a temporary file, then renames over the target
57
+ * This is safe against crashes and concurrent access
58
+ */
59
+ function atomicWrite(filePath_1, content_1) {
60
+ return __awaiter(this, arguments, void 0, function (filePath, content, options) {
61
+ var _a, createBackup, backupDir, _b, encoding, parentDir, tempDir, tempFileName, tempFilePath, error_1, _c, _d;
62
+ if (options === void 0) { options = {}; }
63
+ return __generator(this, function (_e) {
64
+ switch (_e.label) {
65
+ case 0:
66
+ _a = options.createBackup, createBackup = _a === void 0 ? true : _a, backupDir = options.backupDir, _b = options.encoding, encoding = _b === void 0 ? 'utf8' : _b;
67
+ parentDir = (0, node_path_1.dirname)(filePath);
68
+ return [4 /*yield*/, (0, promises_1.mkdir)(parentDir, { recursive: true })];
69
+ case 1:
70
+ _e.sent();
71
+ if (!(createBackup && (0, node_fs_1.existsSync)(filePath))) return [3 /*break*/, 3];
72
+ return [4 /*yield*/, createBackupFile(filePath, backupDir)];
73
+ case 2:
74
+ _e.sent();
75
+ _e.label = 3;
76
+ case 3: return [4 /*yield*/, (0, promises_1.mkdtemp)((0, node_path_1.join)(getTempDir(), 'mcp-atomic-'))];
77
+ case 4:
78
+ tempDir = _e.sent();
79
+ tempFileName = "".concat((0, node_crypto_1.randomBytes)(16).toString('hex'), ".tmp");
80
+ tempFilePath = (0, node_path_1.join)(tempDir, tempFileName);
81
+ _e.label = 5;
82
+ case 5:
83
+ _e.trys.push([5, 8, 13, 17]);
84
+ // Write to temp file
85
+ return [4 /*yield*/, (0, promises_1.writeFile)(tempFilePath, content, { encoding: encoding, mode: 420 })];
86
+ case 6:
87
+ // Write to temp file
88
+ _e.sent();
89
+ // Atomic rename (overwrites target if exists)
90
+ return [4 /*yield*/, (0, promises_1.rename)(tempFilePath, filePath)];
91
+ case 7:
92
+ // Atomic rename (overwrites target if exists)
93
+ _e.sent();
94
+ return [3 /*break*/, 17];
95
+ case 8:
96
+ error_1 = _e.sent();
97
+ _e.label = 9;
98
+ case 9:
99
+ _e.trys.push([9, 11, , 12]);
100
+ return [4 /*yield*/, (0, promises_1.unlink)(tempFilePath)];
101
+ case 10:
102
+ _e.sent();
103
+ return [3 /*break*/, 12];
104
+ case 11:
105
+ _c = _e.sent();
106
+ return [3 /*break*/, 12];
107
+ case 12: throw error_1;
108
+ case 13:
109
+ _e.trys.push([13, 15, , 16]);
110
+ return [4 /*yield*/, (0, promises_1.unlink)(tempDir).catch(function () { })];
111
+ case 14:
112
+ _e.sent();
113
+ return [3 /*break*/, 16];
114
+ case 15:
115
+ _d = _e.sent();
116
+ return [3 /*break*/, 16];
117
+ case 16: return [7 /*endfinally*/];
118
+ case 17: return [2 /*return*/];
119
+ }
120
+ });
121
+ });
122
+ }
123
+ /**
124
+ * Create a backup of a file
125
+ */
126
+ function createBackupFile(filePath, backupDir) {
127
+ return __awaiter(this, void 0, void 0, function () {
128
+ var timestamp, backupFileName, backupPath, backupDirPath, content;
129
+ return __generator(this, function (_a) {
130
+ switch (_a.label) {
131
+ case 0:
132
+ timestamp = new Date().toISOString().replace(/[:.]/g, '-');
133
+ backupFileName = "".concat(basename(filePath), ".backup.").concat(timestamp);
134
+ backupPath = backupDir
135
+ ? (0, node_path_1.join)(backupDir, backupFileName)
136
+ : "".concat(filePath, ".backup");
137
+ backupDirPath = (0, node_path_1.dirname)(backupPath);
138
+ return [4 /*yield*/, (0, promises_1.mkdir)(backupDirPath, { recursive: true })];
139
+ case 1:
140
+ _a.sent();
141
+ return [4 /*yield*/, (0, promises_1.readFile)(filePath)];
142
+ case 2:
143
+ content = _a.sent();
144
+ return [4 /*yield*/, (0, promises_1.writeFile)(backupPath, content)];
145
+ case 3:
146
+ _a.sent();
147
+ return [2 /*return*/, backupPath];
148
+ }
149
+ });
150
+ });
151
+ }
152
+ /**
153
+ * Get temp directory
154
+ */
155
+ function getTempDir() {
156
+ return (0, node_os_1.tmpdir)();
157
+ }
158
+ /**
159
+ * Get file basename without extension
160
+ */
161
+ function basename(filePath) {
162
+ return filePath.split('/').pop() || filePath;
163
+ }
164
+ /**
165
+ * Safe file read with encoding detection
166
+ */
167
+ function safeReadFile(filePath_1) {
168
+ return __awaiter(this, arguments, void 0, function (filePath, encoding) {
169
+ var content, error_2;
170
+ if (encoding === void 0) { encoding = 'utf8'; }
171
+ return __generator(this, function (_a) {
172
+ switch (_a.label) {
173
+ case 0:
174
+ _a.trys.push([0, 2, , 3]);
175
+ return [4 /*yield*/, (0, promises_1.readFile)(filePath, { encoding: encoding })];
176
+ case 1:
177
+ content = _a.sent();
178
+ // Remove BOM if present
179
+ if (content.charCodeAt(0) === 0xFEFF) {
180
+ return [2 /*return*/, content.slice(1)];
181
+ }
182
+ return [2 /*return*/, content];
183
+ case 2:
184
+ error_2 = _a.sent();
185
+ throw new Error("Failed to read file ".concat(filePath, ": ").concat(error_2.message));
186
+ case 3: return [2 /*return*/];
187
+ }
188
+ });
189
+ });
190
+ }
191
+ /**
192
+ * Check if file is writable
193
+ */
194
+ function isFileWritable(filePath) {
195
+ return __awaiter(this, void 0, void 0, function () {
196
+ var _a;
197
+ return __generator(this, function (_b) {
198
+ switch (_b.label) {
199
+ case 0:
200
+ _b.trys.push([0, 3, , 4]);
201
+ return [4 /*yield*/, (0, promises_1.writeFile)(filePath, '', { flag: node_fs_1.constants.O_WRONLY | node_fs_1.constants.O_CREAT })];
202
+ case 1:
203
+ _b.sent();
204
+ return [4 /*yield*/, (0, promises_1.unlink)(filePath)];
205
+ case 2:
206
+ _b.sent();
207
+ return [2 /*return*/, true];
208
+ case 3:
209
+ _a = _b.sent();
210
+ return [2 /*return*/, false];
211
+ case 4: return [2 /*return*/];
212
+ }
213
+ });
214
+ });
215
+ }
216
+ /**
217
+ * Normalize line endings to LF
218
+ */
219
+ function normalizeLineEndings(content) {
220
+ return content.replace(/\r\n/g, '\n');
221
+ }
222
+ /**
223
+ * Detect and strip BOM
224
+ */
225
+ function stripBOM(content) {
226
+ if (content.charCodeAt(0) === 0xFEFF) {
227
+ return content.slice(1);
228
+ }
229
+ return content;
230
+ }
231
+ /**
232
+ * Format JSON with proper indentation
233
+ */
234
+ function formatJSON(obj, indent) {
235
+ if (indent === void 0) { indent = 2; }
236
+ return JSON.stringify(obj, null, indent) + '\n';
237
+ }
238
+ /**
239
+ * Parse JSON with error reporting
240
+ */
241
+ function safeJSONParse(content, filePath) {
242
+ try {
243
+ return JSON.parse(content);
244
+ }
245
+ catch (error) {
246
+ var message = error.message;
247
+ var match = message.match(/position (\d+)/);
248
+ var position = match ? match[1] : 'unknown';
249
+ throw new Error("Invalid JSON in ".concat(filePath, " at position ").concat(position, ": ").concat(message));
250
+ }
251
+ }
@@ -0,0 +1,174 @@
1
+ // ============================================================================
2
+ // Atomic File Operations - Safe config file writes with backup support
3
+ // ============================================================================
4
+
5
+ import { writeFile, rename, unlink, readFile, mkdir, mkdtemp } from 'node:fs/promises';
6
+ import { existsSync, constants } from 'node:fs';
7
+ import { join, dirname } from 'node:path';
8
+ import { tmpdir } from 'node:os';
9
+ import { randomBytes } from 'node:crypto';
10
+ import type { AtomicWriteOptions } from '../types.js';
11
+
12
+ /**
13
+ * Atomic write implementation
14
+ * Writes to a temporary file, then renames over the target
15
+ * This is safe against crashes and concurrent access
16
+ */
17
+ export async function atomicWrite(
18
+ filePath: string,
19
+ content: string,
20
+ options: AtomicWriteOptions = {}
21
+ ): Promise<void> {
22
+ const {
23
+ createBackup = true,
24
+ backupDir,
25
+ encoding = 'utf8',
26
+ } = options;
27
+
28
+ // Create parent directory if it doesn't exist
29
+ const parentDir = dirname(filePath);
30
+ await mkdir(parentDir, { recursive: true });
31
+
32
+ // Create backup if requested and file exists
33
+ if (createBackup && existsSync(filePath)) {
34
+ await createBackupFile(filePath, backupDir);
35
+ }
36
+
37
+ // Create temp file in OS temp directory (same filesystem for atomic rename)
38
+ const tempDir = await mkdtemp(join(getTempDir(), 'mcp-atomic-'));
39
+ const tempFileName = `${randomBytes(16).toString('hex')}.tmp`;
40
+ const tempFilePath = join(tempDir, tempFileName);
41
+
42
+ try {
43
+ // Write to temp file
44
+ await writeFile(tempFilePath, content, { encoding, mode: 0o644 });
45
+
46
+ // Atomic rename (overwrites target if exists)
47
+ await rename(tempFilePath, filePath);
48
+ } catch (error) {
49
+ // Clean up temp file if rename failed
50
+ try {
51
+ await unlink(tempFilePath);
52
+ } catch {
53
+ // Ignore cleanup errors
54
+ }
55
+ throw error;
56
+ } finally {
57
+ // Clean up temp directory
58
+ try {
59
+ await unlink(tempDir).catch(() => {});
60
+ } catch {
61
+ // Ignore cleanup errors
62
+ }
63
+ }
64
+ }
65
+
66
+ /**
67
+ * Create a backup of a file
68
+ */
69
+ async function createBackupFile(filePath: string, backupDir?: string): Promise<string> {
70
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
71
+ const backupFileName = `${basename(filePath)}.backup.${timestamp}`;
72
+ const backupPath = backupDir
73
+ ? join(backupDir, backupFileName)
74
+ : `${filePath}.backup`;
75
+
76
+ // Ensure backup directory exists
77
+ const backupDirPath = dirname(backupPath);
78
+ await mkdir(backupDirPath, { recursive: true });
79
+
80
+ // Copy file to backup location
81
+ const content = await readFile(filePath);
82
+ await writeFile(backupPath, content);
83
+
84
+ return backupPath;
85
+ }
86
+
87
+ /**
88
+ * Get temp directory
89
+ */
90
+ function getTempDir(): string {
91
+ return tmpdir();
92
+ }
93
+
94
+ /**
95
+ * Get file basename without extension
96
+ */
97
+ function basename(filePath: string): string {
98
+ return filePath.split('/').pop() || filePath;
99
+ }
100
+
101
+ /**
102
+ * Safe file read with encoding detection
103
+ */
104
+ export async function safeReadFile(
105
+ filePath: string,
106
+ encoding: BufferEncoding = 'utf8'
107
+ ): Promise<string> {
108
+ try {
109
+ const content = await readFile(filePath, { encoding });
110
+
111
+ // Remove BOM if present
112
+ if (content.charCodeAt(0) === 0xFEFF) {
113
+ return content.slice(1);
114
+ }
115
+
116
+ return content;
117
+ } catch (error) {
118
+ throw new Error(`Failed to read file ${filePath}: ${(error as Error).message}`);
119
+ }
120
+ }
121
+
122
+ /**
123
+ * Check if file is writable
124
+ */
125
+ export async function isFileWritable(filePath: string): Promise<boolean> {
126
+ try {
127
+ await writeFile(filePath, '', { flag: constants.O_WRONLY | constants.O_CREAT });
128
+ await unlink(filePath);
129
+ return true;
130
+ } catch {
131
+ return false;
132
+ }
133
+ }
134
+
135
+ /**
136
+ * Normalize line endings to LF
137
+ */
138
+ export function normalizeLineEndings(content: string): string {
139
+ return content.replace(/\r\n/g, '\n');
140
+ }
141
+
142
+ /**
143
+ * Detect and strip BOM
144
+ */
145
+ export function stripBOM(content: string): string {
146
+ if (content.charCodeAt(0) === 0xFEFF) {
147
+ return content.slice(1);
148
+ }
149
+ return content;
150
+ }
151
+
152
+ /**
153
+ * Format JSON with proper indentation
154
+ */
155
+ export function formatJSON(obj: unknown, indent: number = 2): string {
156
+ return JSON.stringify(obj, null, indent) + '\n';
157
+ }
158
+
159
+ /**
160
+ * Parse JSON with error reporting
161
+ */
162
+ export function safeJSONParse<T>(content: string, filePath: string): T {
163
+ try {
164
+ return JSON.parse(content) as T;
165
+ } catch (error) {
166
+ const message = (error as Error).message;
167
+ const match = message.match(/position (\d+)/);
168
+ const position = match ? match[1] : 'unknown';
169
+
170
+ throw new Error(
171
+ `Invalid JSON in ${filePath} at position ${position}: ${message}`
172
+ );
173
+ }
174
+ }