@softerist/heuristic-mcp 3.0.15 → 3.0.16
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/README.md +104 -104
- package/config.jsonc +173 -173
- package/features/ann-config.js +131 -0
- package/features/clear-cache.js +84 -0
- package/features/find-similar-code.js +291 -0
- package/features/hybrid-search.js +544 -0
- package/features/index-codebase.js +3268 -0
- package/features/lifecycle.js +1189 -0
- package/features/package-version.js +302 -0
- package/features/register.js +408 -0
- package/features/resources.js +156 -0
- package/features/set-workspace.js +265 -0
- package/index.js +96 -96
- package/lib/cache-ops.js +22 -22
- package/lib/cache-utils.js +565 -565
- package/lib/cache.js +1870 -1870
- package/lib/call-graph.js +396 -396
- package/lib/cli.js +1 -1
- package/lib/config.js +517 -517
- package/lib/constants.js +39 -39
- package/lib/embed-query-process.js +7 -7
- package/lib/embedding-process.js +7 -7
- package/lib/embedding-worker.js +299 -299
- package/lib/ignore-patterns.js +316 -316
- package/lib/json-worker.js +14 -14
- package/lib/json-writer.js +337 -337
- package/lib/logging.js +164 -164
- package/lib/memory-logger.js +13 -13
- package/lib/onnx-backend.js +193 -193
- package/lib/project-detector.js +84 -84
- package/lib/server-lifecycle.js +165 -165
- package/lib/settings-editor.js +754 -754
- package/lib/tokenizer.js +256 -256
- package/lib/utils.js +428 -428
- package/lib/vector-store-binary.js +627 -627
- package/lib/vector-store-sqlite.js +95 -95
- package/lib/workspace-env.js +28 -28
- package/mcp_config.json +9 -9
- package/package.json +86 -75
- package/scripts/clear-cache.js +20 -0
- package/scripts/download-model.js +43 -0
- package/scripts/mcp-launcher.js +49 -0
- package/scripts/postinstall.js +12 -0
- package/search-configs.js +36 -36
- package/.prettierrc +0 -7
- package/debug-pids.js +0 -30
- package/eslint.config.js +0 -36
- package/specs/plan.md +0 -23
- package/vitest.config.js +0 -39
package/lib/settings-editor.js
CHANGED
|
@@ -1,754 +1,754 @@
|
|
|
1
|
-
function detectNewline(text) {
|
|
2
|
-
return text.includes('\r\n') ? '\r\n' : '\n';
|
|
3
|
-
}
|
|
4
|
-
|
|
5
|
-
function detectIndentUnit(text) {
|
|
6
|
-
const lines = text.split(/\r?\n/);
|
|
7
|
-
for (const line of lines) {
|
|
8
|
-
const match = line.match(/^(\s+)"/);
|
|
9
|
-
if (match) return match[1];
|
|
10
|
-
}
|
|
11
|
-
return ' ';
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
function getLineIndent(text, index) {
|
|
15
|
-
const lineStart = Math.max(text.lastIndexOf('\n', index - 1), text.lastIndexOf('\r', index - 1));
|
|
16
|
-
const start = lineStart === -1 ? 0 : lineStart + 1;
|
|
17
|
-
let i = start;
|
|
18
|
-
while (i < text.length && (text[i] === ' ' || text[i] === '\t')) i += 1;
|
|
19
|
-
return text.slice(start, i);
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
function stripJsonComments(text) {
|
|
23
|
-
let out = '';
|
|
24
|
-
let inString = false;
|
|
25
|
-
let escape = false;
|
|
26
|
-
let inLine = false;
|
|
27
|
-
let inBlock = false;
|
|
28
|
-
|
|
29
|
-
for (let i = 0; i < text.length; i += 1) {
|
|
30
|
-
const ch = text[i];
|
|
31
|
-
const next = text[i + 1];
|
|
32
|
-
|
|
33
|
-
if (inLine) {
|
|
34
|
-
if (ch === '\n') {
|
|
35
|
-
inLine = false;
|
|
36
|
-
out += ch;
|
|
37
|
-
}
|
|
38
|
-
continue;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
if (inBlock) {
|
|
42
|
-
if (ch === '*' && next === '/') {
|
|
43
|
-
inBlock = false;
|
|
44
|
-
i += 1;
|
|
45
|
-
}
|
|
46
|
-
continue;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
if (inString) {
|
|
50
|
-
out += ch;
|
|
51
|
-
if (escape) {
|
|
52
|
-
escape = false;
|
|
53
|
-
} else if (ch === '\\') {
|
|
54
|
-
escape = true;
|
|
55
|
-
} else if (ch === '"') {
|
|
56
|
-
inString = false;
|
|
57
|
-
}
|
|
58
|
-
continue;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
if (ch === '/' && next === '/') {
|
|
62
|
-
inLine = true;
|
|
63
|
-
i += 1;
|
|
64
|
-
continue;
|
|
65
|
-
}
|
|
66
|
-
if (ch === '/' && next === '*') {
|
|
67
|
-
inBlock = true;
|
|
68
|
-
i += 1;
|
|
69
|
-
continue;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
if (ch === '"') {
|
|
73
|
-
inString = true;
|
|
74
|
-
}
|
|
75
|
-
out += ch;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
return out;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
function stripTrailingCommas(text) {
|
|
82
|
-
let out = '';
|
|
83
|
-
let inString = false;
|
|
84
|
-
let escape = false;
|
|
85
|
-
|
|
86
|
-
for (let i = 0; i < text.length; i += 1) {
|
|
87
|
-
const ch = text[i];
|
|
88
|
-
|
|
89
|
-
if (inString) {
|
|
90
|
-
out += ch;
|
|
91
|
-
if (escape) {
|
|
92
|
-
escape = false;
|
|
93
|
-
} else if (ch === '\\') {
|
|
94
|
-
escape = true;
|
|
95
|
-
} else if (ch === '"') {
|
|
96
|
-
inString = false;
|
|
97
|
-
}
|
|
98
|
-
continue;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
if (ch === '"') {
|
|
102
|
-
inString = true;
|
|
103
|
-
out += ch;
|
|
104
|
-
continue;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
if (ch === ',') {
|
|
108
|
-
let j = i + 1;
|
|
109
|
-
while (j < text.length && /\s/.test(text[j])) j += 1;
|
|
110
|
-
if (text[j] === '}' || text[j] === ']') {
|
|
111
|
-
continue;
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
out += ch;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
return out;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
export function parseJsonc(text) {
|
|
122
|
-
const cleaned = stripTrailingCommas(stripJsonComments(text));
|
|
123
|
-
try {
|
|
124
|
-
return JSON.parse(cleaned);
|
|
125
|
-
} catch {
|
|
126
|
-
return null;
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
function skipWhitespaceAndComments(text, start, end) {
|
|
131
|
-
let i = start;
|
|
132
|
-
while (i < end) {
|
|
133
|
-
const ch = text[i];
|
|
134
|
-
const next = text[i + 1];
|
|
135
|
-
if (ch === ' ' || ch === '\t' || ch === '\r' || ch === '\n') {
|
|
136
|
-
i += 1;
|
|
137
|
-
continue;
|
|
138
|
-
}
|
|
139
|
-
if (ch === '/' && next === '/') {
|
|
140
|
-
i += 2;
|
|
141
|
-
while (i < end && text[i] !== '\n') i += 1;
|
|
142
|
-
continue;
|
|
143
|
-
}
|
|
144
|
-
if (ch === '/' && next === '*') {
|
|
145
|
-
i += 2;
|
|
146
|
-
while (i < end - 1 && !(text[i] === '*' && text[i + 1] === '/')) i += 1;
|
|
147
|
-
i += 2;
|
|
148
|
-
continue;
|
|
149
|
-
}
|
|
150
|
-
break;
|
|
151
|
-
}
|
|
152
|
-
return i;
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
function readString(text, start, end) {
|
|
156
|
-
let i = start + 1;
|
|
157
|
-
let value = '';
|
|
158
|
-
let escape = false;
|
|
159
|
-
while (i < end) {
|
|
160
|
-
const ch = text[i];
|
|
161
|
-
if (escape) {
|
|
162
|
-
value += ch;
|
|
163
|
-
escape = false;
|
|
164
|
-
} else if (ch === '\\') {
|
|
165
|
-
value += ch;
|
|
166
|
-
escape = true;
|
|
167
|
-
} else if (ch === '"') {
|
|
168
|
-
return { value, end: i + 1 };
|
|
169
|
-
} else {
|
|
170
|
-
value += ch;
|
|
171
|
-
}
|
|
172
|
-
i += 1;
|
|
173
|
-
}
|
|
174
|
-
return { value, end: i };
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
function scanComposite(text, start, end, openChar, closeChar) {
|
|
178
|
-
let depth = 0;
|
|
179
|
-
let inString = false;
|
|
180
|
-
let escape = false;
|
|
181
|
-
let inLine = false;
|
|
182
|
-
let inBlock = false;
|
|
183
|
-
|
|
184
|
-
for (let i = start; i < end; i += 1) {
|
|
185
|
-
const ch = text[i];
|
|
186
|
-
const next = text[i + 1];
|
|
187
|
-
|
|
188
|
-
if (inLine) {
|
|
189
|
-
if (ch === '\n') inLine = false;
|
|
190
|
-
continue;
|
|
191
|
-
}
|
|
192
|
-
if (inBlock) {
|
|
193
|
-
if (ch === '*' && next === '/') {
|
|
194
|
-
inBlock = false;
|
|
195
|
-
i += 1;
|
|
196
|
-
}
|
|
197
|
-
continue;
|
|
198
|
-
}
|
|
199
|
-
if (inString) {
|
|
200
|
-
if (escape) {
|
|
201
|
-
escape = false;
|
|
202
|
-
} else if (ch === '\\') {
|
|
203
|
-
escape = true;
|
|
204
|
-
} else if (ch === '"') {
|
|
205
|
-
inString = false;
|
|
206
|
-
}
|
|
207
|
-
continue;
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
if (ch === '/' && next === '/') {
|
|
211
|
-
inLine = true;
|
|
212
|
-
i += 1;
|
|
213
|
-
continue;
|
|
214
|
-
}
|
|
215
|
-
if (ch === '/' && next === '*') {
|
|
216
|
-
inBlock = true;
|
|
217
|
-
i += 1;
|
|
218
|
-
continue;
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
if (ch === '"') {
|
|
222
|
-
inString = true;
|
|
223
|
-
continue;
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
if (ch === openChar) {
|
|
227
|
-
depth += 1;
|
|
228
|
-
continue;
|
|
229
|
-
}
|
|
230
|
-
if (ch === closeChar) {
|
|
231
|
-
depth -= 1;
|
|
232
|
-
if (depth === 0) return i + 1;
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
return end;
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
function scanValue(text, start, end) {
|
|
239
|
-
let i = skipWhitespaceAndComments(text, start, end);
|
|
240
|
-
const valueStart = i;
|
|
241
|
-
if (i >= end) {
|
|
242
|
-
return { valueStart: end, valueEnd: end, valueEndWithComma: end };
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
const ch = text[i];
|
|
246
|
-
let valueEnd = i;
|
|
247
|
-
|
|
248
|
-
if (ch === '{') {
|
|
249
|
-
valueEnd = scanComposite(text, i, end, '{', '}');
|
|
250
|
-
} else if (ch === '[') {
|
|
251
|
-
valueEnd = scanComposite(text, i, end, '[', ']');
|
|
252
|
-
} else if (ch === '"') {
|
|
253
|
-
const result = readString(text, i, end);
|
|
254
|
-
valueEnd = result.end;
|
|
255
|
-
} else {
|
|
256
|
-
let inString = false;
|
|
257
|
-
let escape = false;
|
|
258
|
-
let inLine = false;
|
|
259
|
-
let inBlock = false;
|
|
260
|
-
for (; i < end; i += 1) {
|
|
261
|
-
const c = text[i];
|
|
262
|
-
const next = text[i + 1];
|
|
263
|
-
if (inLine) {
|
|
264
|
-
if (c === '\n') inLine = false;
|
|
265
|
-
continue;
|
|
266
|
-
}
|
|
267
|
-
if (inBlock) {
|
|
268
|
-
if (c === '*' && next === '/') {
|
|
269
|
-
inBlock = false;
|
|
270
|
-
i += 1;
|
|
271
|
-
}
|
|
272
|
-
continue;
|
|
273
|
-
}
|
|
274
|
-
if (inString) {
|
|
275
|
-
if (escape) {
|
|
276
|
-
escape = false;
|
|
277
|
-
} else if (c === '\\') {
|
|
278
|
-
escape = true;
|
|
279
|
-
} else if (c === '"') {
|
|
280
|
-
inString = false;
|
|
281
|
-
}
|
|
282
|
-
continue;
|
|
283
|
-
}
|
|
284
|
-
if (c === '/' && next === '/') {
|
|
285
|
-
inLine = true;
|
|
286
|
-
i += 1;
|
|
287
|
-
continue;
|
|
288
|
-
}
|
|
289
|
-
if (c === '/' && next === '*') {
|
|
290
|
-
inBlock = true;
|
|
291
|
-
i += 1;
|
|
292
|
-
continue;
|
|
293
|
-
}
|
|
294
|
-
if (c === '"') {
|
|
295
|
-
inString = true;
|
|
296
|
-
continue;
|
|
297
|
-
}
|
|
298
|
-
if (c === ',' || c === '}' || c === ']') {
|
|
299
|
-
break;
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
valueEnd = i;
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
let valueEndWithComma = valueEnd;
|
|
306
|
-
let j = skipWhitespaceAndComments(text, valueEnd, end);
|
|
307
|
-
if (text[j] === ',') {
|
|
308
|
-
valueEndWithComma = j + 1;
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
return { valueStart, valueEnd, valueEndWithComma };
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
function findRootObjectRange(text) {
|
|
315
|
-
let inString = false;
|
|
316
|
-
let escape = false;
|
|
317
|
-
let inLine = false;
|
|
318
|
-
let inBlock = false;
|
|
319
|
-
|
|
320
|
-
for (let i = 0; i < text.length; i += 1) {
|
|
321
|
-
const ch = text[i];
|
|
322
|
-
const next = text[i + 1];
|
|
323
|
-
|
|
324
|
-
if (inLine) {
|
|
325
|
-
if (ch === '\n') inLine = false;
|
|
326
|
-
continue;
|
|
327
|
-
}
|
|
328
|
-
if (inBlock) {
|
|
329
|
-
if (ch === '*' && next === '/') {
|
|
330
|
-
inBlock = false;
|
|
331
|
-
i += 1;
|
|
332
|
-
}
|
|
333
|
-
continue;
|
|
334
|
-
}
|
|
335
|
-
if (inString) {
|
|
336
|
-
if (escape) {
|
|
337
|
-
escape = false;
|
|
338
|
-
} else if (ch === '\\') {
|
|
339
|
-
escape = true;
|
|
340
|
-
} else if (ch === '"') {
|
|
341
|
-
inString = false;
|
|
342
|
-
}
|
|
343
|
-
continue;
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
if (ch === '/' && next === '/') {
|
|
347
|
-
inLine = true;
|
|
348
|
-
i += 1;
|
|
349
|
-
continue;
|
|
350
|
-
}
|
|
351
|
-
if (ch === '/' && next === '*') {
|
|
352
|
-
inBlock = true;
|
|
353
|
-
i += 1;
|
|
354
|
-
continue;
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
if (ch === '"') {
|
|
358
|
-
inString = true;
|
|
359
|
-
continue;
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
if (ch === '{') {
|
|
363
|
-
const end = scanComposite(text, i, text.length, '{', '}');
|
|
364
|
-
if (end > i) return { start: i, end };
|
|
365
|
-
}
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
return null;
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
function findPropertyValueRange(text, objRange, key) {
|
|
372
|
-
let i = objRange.start + 1;
|
|
373
|
-
const end = objRange.end - 1;
|
|
374
|
-
let expectKey = true;
|
|
375
|
-
|
|
376
|
-
while (i < end) {
|
|
377
|
-
i = skipWhitespaceAndComments(text, i, end);
|
|
378
|
-
if (i >= end) break;
|
|
379
|
-
const ch = text[i];
|
|
380
|
-
if (ch === '}') break;
|
|
381
|
-
if (ch === ',') {
|
|
382
|
-
expectKey = true;
|
|
383
|
-
i += 1;
|
|
384
|
-
continue;
|
|
385
|
-
}
|
|
386
|
-
if (!expectKey) {
|
|
387
|
-
const valueInfo = scanValue(text, i, end);
|
|
388
|
-
i = valueInfo.valueEndWithComma;
|
|
389
|
-
expectKey = true;
|
|
390
|
-
continue;
|
|
391
|
-
}
|
|
392
|
-
if (ch !== '"') {
|
|
393
|
-
i += 1;
|
|
394
|
-
continue;
|
|
395
|
-
}
|
|
396
|
-
const keyResult = readString(text, i, end);
|
|
397
|
-
const keyName = keyResult.value;
|
|
398
|
-
const keyStart = i;
|
|
399
|
-
const keyEnd = keyResult.end;
|
|
400
|
-
let afterKey = skipWhitespaceAndComments(text, keyEnd, end);
|
|
401
|
-
if (text[afterKey] !== ':') {
|
|
402
|
-
i = keyEnd;
|
|
403
|
-
continue;
|
|
404
|
-
}
|
|
405
|
-
const valueInfo = scanValue(text, afterKey + 1, end);
|
|
406
|
-
if (keyName === key) {
|
|
407
|
-
return {
|
|
408
|
-
keyStart,
|
|
409
|
-
keyEnd,
|
|
410
|
-
valueStart: valueInfo.valueStart,
|
|
411
|
-
valueEnd: valueInfo.valueEnd,
|
|
412
|
-
valueEndWithComma: valueInfo.valueEndWithComma,
|
|
413
|
-
};
|
|
414
|
-
}
|
|
415
|
-
i = valueInfo.valueEndWithComma;
|
|
416
|
-
expectKey = true;
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
return null;
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
function formatJsonValue(value, indentUnit, parentIndent, newline) {
|
|
423
|
-
const raw = JSON.stringify(value, null, indentUnit);
|
|
424
|
-
if (!raw.includes('\n')) return raw;
|
|
425
|
-
return raw.replace(/\n/g, `${newline}${parentIndent}`);
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
function findObjectRangeFromValue(text, valueStart, valueEnd) {
|
|
429
|
-
const start = skipWhitespaceAndComments(text, valueStart, valueEnd);
|
|
430
|
-
if (text[start] !== '{') return null;
|
|
431
|
-
const end = scanComposite(text, start, valueEnd, '{', '}');
|
|
432
|
-
return { start, end };
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
function insertPropertyIntoObject(text, objRange, key, valueText, indentUnit, newline) {
|
|
436
|
-
const objectIndent = getLineIndent(text, objRange.start);
|
|
437
|
-
const propertyIndent = `${objectIndent}${indentUnit}`;
|
|
438
|
-
const entry = `${propertyIndent}"${key}": ${valueText}`;
|
|
439
|
-
|
|
440
|
-
const before = text.slice(0, objRange.start + 1);
|
|
441
|
-
const inside = text.slice(objRange.start + 1, objRange.end - 1);
|
|
442
|
-
const after = text.slice(objRange.end - 1);
|
|
443
|
-
|
|
444
|
-
const contentIndex = skipWhitespaceAndComments(text, objRange.start + 1, objRange.end - 1);
|
|
445
|
-
const hasEntries = contentIndex < objRange.end - 1;
|
|
446
|
-
if (!hasEntries) {
|
|
447
|
-
return `${before}${newline}${entry}${newline}${objectIndent}}${after.slice(1)}`;
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
const insideEndIndex = objRange.start + 1 + inside.length;
|
|
451
|
-
let insertPoint = insideEndIndex;
|
|
452
|
-
let j = objRange.end - 2;
|
|
453
|
-
while (j > objRange.start && /\s/.test(text[j])) j -= 1;
|
|
454
|
-
const needsComma = text[j] !== ',';
|
|
455
|
-
const comma = needsComma ? ',' : '';
|
|
456
|
-
|
|
457
|
-
return (
|
|
458
|
-
text.slice(0, insertPoint) +
|
|
459
|
-
comma +
|
|
460
|
-
newline +
|
|
461
|
-
entry +
|
|
462
|
-
newline +
|
|
463
|
-
objectIndent +
|
|
464
|
-
text.slice(objRange.end - 1)
|
|
465
|
-
);
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
function replaceRange(text, start, end, replacement) {
|
|
469
|
-
return text.slice(0, start) + replacement + text.slice(end);
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
function resolveContainer(text, rootRange, preferredContainerKey = 'mcpServers') {
|
|
473
|
-
const baseContainers = [
|
|
474
|
-
{ type: 'key', key: 'mcpServers' },
|
|
475
|
-
{ type: 'key', key: 'servers' },
|
|
476
|
-
{ type: 'key', key: 'cline.mcpServers' },
|
|
477
|
-
{ type: 'nested', key: 'cline', child: 'mcpServers' },
|
|
478
|
-
];
|
|
479
|
-
|
|
480
|
-
const preferredIndex = baseContainers.findIndex(
|
|
481
|
-
(candidate) => candidate.type === 'key' && candidate.key === preferredContainerKey
|
|
482
|
-
);
|
|
483
|
-
if (preferredIndex > 0) {
|
|
484
|
-
const [preferred] = baseContainers.splice(preferredIndex, 1);
|
|
485
|
-
baseContainers.unshift(preferred);
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
for (const candidate of baseContainers) {
|
|
489
|
-
if (candidate.type === 'key') {
|
|
490
|
-
const entry = findPropertyValueRange(text, rootRange, candidate.key);
|
|
491
|
-
if (entry) {
|
|
492
|
-
return { entry, containerKey: candidate.key };
|
|
493
|
-
}
|
|
494
|
-
continue;
|
|
495
|
-
}
|
|
496
|
-
if (candidate.type === 'nested') {
|
|
497
|
-
const parent = findPropertyValueRange(text, rootRange, candidate.key);
|
|
498
|
-
if (!parent) continue;
|
|
499
|
-
const parentObj = findObjectRangeFromValue(text, parent.valueStart, parent.valueEnd);
|
|
500
|
-
if (!parentObj) {
|
|
501
|
-
return {
|
|
502
|
-
entry: parent,
|
|
503
|
-
containerKey: candidate.key,
|
|
504
|
-
needsObjectReplace: true,
|
|
505
|
-
childKey: candidate.child,
|
|
506
|
-
};
|
|
507
|
-
}
|
|
508
|
-
const child = findPropertyValueRange(text, parentObj, candidate.child);
|
|
509
|
-
if (child) {
|
|
510
|
-
return {
|
|
511
|
-
entry: child,
|
|
512
|
-
containerKey: candidate.child,
|
|
513
|
-
parentRange: parentObj,
|
|
514
|
-
parentEntry: parent,
|
|
515
|
-
};
|
|
516
|
-
}
|
|
517
|
-
return {
|
|
518
|
-
entry: null,
|
|
519
|
-
containerKey: candidate.child,
|
|
520
|
-
parentRange: parentObj,
|
|
521
|
-
parentEntry: parent,
|
|
522
|
-
};
|
|
523
|
-
}
|
|
524
|
-
}
|
|
525
|
-
|
|
526
|
-
return null;
|
|
527
|
-
}
|
|
528
|
-
|
|
529
|
-
export function upsertMcpServerEntryInText(
|
|
530
|
-
text,
|
|
531
|
-
serverName,
|
|
532
|
-
serverConfig,
|
|
533
|
-
preferredContainerKey = 'mcpServers'
|
|
534
|
-
) {
|
|
535
|
-
const newline = detectNewline(text);
|
|
536
|
-
const indentUnit = detectIndentUnit(text);
|
|
537
|
-
const trimmed = text.trim();
|
|
538
|
-
|
|
539
|
-
if (!trimmed) {
|
|
540
|
-
const payload = {
|
|
541
|
-
[preferredContainerKey]: {
|
|
542
|
-
[serverName]: serverConfig,
|
|
543
|
-
},
|
|
544
|
-
};
|
|
545
|
-
return JSON.stringify(payload, null, indentUnit) + newline;
|
|
546
|
-
}
|
|
547
|
-
|
|
548
|
-
const rootRange = findRootObjectRange(text);
|
|
549
|
-
if (!rootRange) {
|
|
550
|
-
return null;
|
|
551
|
-
}
|
|
552
|
-
|
|
553
|
-
const container = resolveContainer(text, rootRange, preferredContainerKey);
|
|
554
|
-
|
|
555
|
-
if (!container) {
|
|
556
|
-
const objectIndent = getLineIndent(text, rootRange.start);
|
|
557
|
-
const propertyIndent = `${objectIndent}${indentUnit}`;
|
|
558
|
-
const valueText = formatJsonValue(
|
|
559
|
-
{ [serverName]: serverConfig },
|
|
560
|
-
indentUnit,
|
|
561
|
-
propertyIndent,
|
|
562
|
-
newline
|
|
563
|
-
);
|
|
564
|
-
return insertPropertyIntoObject(
|
|
565
|
-
text,
|
|
566
|
-
rootRange,
|
|
567
|
-
preferredContainerKey,
|
|
568
|
-
valueText,
|
|
569
|
-
indentUnit,
|
|
570
|
-
newline
|
|
571
|
-
);
|
|
572
|
-
}
|
|
573
|
-
|
|
574
|
-
if (container.needsObjectReplace) {
|
|
575
|
-
const replacementValue = container.childKey
|
|
576
|
-
? { [container.childKey]: { [serverName]: serverConfig } }
|
|
577
|
-
: { [serverName]: serverConfig };
|
|
578
|
-
const parentIndent = getLineIndent(text, container.entry.keyStart);
|
|
579
|
-
const valueText = formatJsonValue(replacementValue, indentUnit, parentIndent, newline);
|
|
580
|
-
return replaceRange(text, container.entry.valueStart, container.entry.valueEnd, valueText);
|
|
581
|
-
}
|
|
582
|
-
|
|
583
|
-
if (container.parentRange && !container.entry) {
|
|
584
|
-
const propertyIndent = `${getLineIndent(text, container.parentRange.start)}${indentUnit}`;
|
|
585
|
-
const valueText = formatJsonValue(
|
|
586
|
-
{ [serverName]: serverConfig },
|
|
587
|
-
indentUnit,
|
|
588
|
-
propertyIndent,
|
|
589
|
-
newline
|
|
590
|
-
);
|
|
591
|
-
return insertPropertyIntoObject(
|
|
592
|
-
text,
|
|
593
|
-
container.parentRange,
|
|
594
|
-
container.containerKey,
|
|
595
|
-
valueText,
|
|
596
|
-
indentUnit,
|
|
597
|
-
newline
|
|
598
|
-
);
|
|
599
|
-
}
|
|
600
|
-
|
|
601
|
-
if (!container.entry) {
|
|
602
|
-
return null;
|
|
603
|
-
}
|
|
604
|
-
|
|
605
|
-
const containerObject = findObjectRangeFromValue(
|
|
606
|
-
text,
|
|
607
|
-
container.entry.valueStart,
|
|
608
|
-
container.entry.valueEnd
|
|
609
|
-
);
|
|
610
|
-
if (!containerObject) {
|
|
611
|
-
const parentIndent = getLineIndent(text, container.entry.keyStart);
|
|
612
|
-
const valueText = formatJsonValue(
|
|
613
|
-
{ [serverName]: serverConfig },
|
|
614
|
-
indentUnit,
|
|
615
|
-
parentIndent,
|
|
616
|
-
newline
|
|
617
|
-
);
|
|
618
|
-
return replaceRange(text, container.entry.valueStart, container.entry.valueEnd, valueText);
|
|
619
|
-
}
|
|
620
|
-
|
|
621
|
-
const existingEntry = findPropertyValueRange(text, containerObject, serverName);
|
|
622
|
-
if (existingEntry) {
|
|
623
|
-
const entryIndent = getLineIndent(text, existingEntry.keyStart);
|
|
624
|
-
const valueText = formatJsonValue(serverConfig, indentUnit, entryIndent, newline);
|
|
625
|
-
return replaceRange(text, existingEntry.valueStart, existingEntry.valueEnd, valueText);
|
|
626
|
-
}
|
|
627
|
-
|
|
628
|
-
const objectIndent = getLineIndent(text, containerObject.start);
|
|
629
|
-
const propertyIndent = `${objectIndent}${indentUnit}`;
|
|
630
|
-
const valueText = formatJsonValue(serverConfig, indentUnit, propertyIndent, newline);
|
|
631
|
-
return insertPropertyIntoObject(
|
|
632
|
-
text,
|
|
633
|
-
containerObject,
|
|
634
|
-
serverName,
|
|
635
|
-
valueText,
|
|
636
|
-
indentUnit,
|
|
637
|
-
newline
|
|
638
|
-
);
|
|
639
|
-
}
|
|
640
|
-
|
|
641
|
-
export function findMcpServerEntry(config, serverName) {
|
|
642
|
-
if (!config || typeof config !== 'object') return null;
|
|
643
|
-
if (config.mcpServers && config.mcpServers[serverName]) {
|
|
644
|
-
return { containerKey: 'mcpServers', entry: config.mcpServers[serverName] };
|
|
645
|
-
}
|
|
646
|
-
if (config.servers && config.servers[serverName]) {
|
|
647
|
-
return { containerKey: 'servers', entry: config.servers[serverName] };
|
|
648
|
-
}
|
|
649
|
-
if (config['cline.mcpServers'] && config['cline.mcpServers'][serverName]) {
|
|
650
|
-
return {
|
|
651
|
-
containerKey: 'cline.mcpServers',
|
|
652
|
-
entry: config['cline.mcpServers'][serverName],
|
|
653
|
-
};
|
|
654
|
-
}
|
|
655
|
-
if (config.cline && config.cline.mcpServers && config.cline.mcpServers[serverName]) {
|
|
656
|
-
return {
|
|
657
|
-
containerKey: 'cline.mcpServers',
|
|
658
|
-
entry: config.cline.mcpServers[serverName],
|
|
659
|
-
};
|
|
660
|
-
}
|
|
661
|
-
return null;
|
|
662
|
-
}
|
|
663
|
-
|
|
664
|
-
function formatTomlString(value) {
|
|
665
|
-
return JSON.stringify(String(value));
|
|
666
|
-
}
|
|
667
|
-
|
|
668
|
-
function formatTomlArray(values) {
|
|
669
|
-
const list = Array.isArray(values) ? values : [];
|
|
670
|
-
return `[${list.map((value) => formatTomlString(value)).join(', ')}]`;
|
|
671
|
-
}
|
|
672
|
-
|
|
673
|
-
function formatTomlMcpSection(serverName, serverConfig, newline) {
|
|
674
|
-
const lines = [`[mcp_servers.${serverName}]`];
|
|
675
|
-
if (serverConfig.command !== undefined) {
|
|
676
|
-
lines.push(`command = ${formatTomlString(serverConfig.command)}`);
|
|
677
|
-
}
|
|
678
|
-
if (serverConfig.args !== undefined) {
|
|
679
|
-
lines.push(`args = ${formatTomlArray(serverConfig.args)}`);
|
|
680
|
-
}
|
|
681
|
-
if (serverConfig.disabled !== undefined) {
|
|
682
|
-
lines.push(`disabled = ${serverConfig.disabled ? 'true' : 'false'}`);
|
|
683
|
-
}
|
|
684
|
-
return lines.join(newline);
|
|
685
|
-
}
|
|
686
|
-
|
|
687
|
-
function findTomlSectionRange(source, sectionName) {
|
|
688
|
-
const headerRegex = /^\s*\[([^\]\r\n]+)\]\s*$/gm;
|
|
689
|
-
let start = -1;
|
|
690
|
-
let end = source.length;
|
|
691
|
-
let match;
|
|
692
|
-
|
|
693
|
-
while ((match = headerRegex.exec(source)) !== null) {
|
|
694
|
-
const currentSection = String(match[1] || '').trim();
|
|
695
|
-
if (start === -1) {
|
|
696
|
-
if (currentSection === sectionName) {
|
|
697
|
-
start = match.index;
|
|
698
|
-
}
|
|
699
|
-
continue;
|
|
700
|
-
}
|
|
701
|
-
|
|
702
|
-
end = match.index;
|
|
703
|
-
break;
|
|
704
|
-
}
|
|
705
|
-
|
|
706
|
-
if (start === -1) {
|
|
707
|
-
return null;
|
|
708
|
-
}
|
|
709
|
-
|
|
710
|
-
return { start, end };
|
|
711
|
-
}
|
|
712
|
-
|
|
713
|
-
export function upsertMcpServerEntryInToml(text, serverName, serverConfig) {
|
|
714
|
-
const source = String(text || '');
|
|
715
|
-
const newline = detectNewline(source || '\n');
|
|
716
|
-
const section = formatTomlMcpSection(serverName, serverConfig, newline);
|
|
717
|
-
const sectionName = `mcp_servers.${serverName}`;
|
|
718
|
-
const range = findTomlSectionRange(source, sectionName);
|
|
719
|
-
|
|
720
|
-
if (!source.trim()) {
|
|
721
|
-
return `${section}${newline}`;
|
|
722
|
-
}
|
|
723
|
-
|
|
724
|
-
if (range) {
|
|
725
|
-
const before = source.slice(0, range.start);
|
|
726
|
-
const after = source.slice(range.end).replace(/^\s*\r?\n?/, '');
|
|
727
|
-
const normalizedBefore =
|
|
728
|
-
before.endsWith('\n') || before.endsWith('\r') || !before ? before : `${before}${newline}`;
|
|
729
|
-
const between = after ? newline : '';
|
|
730
|
-
return `${normalizedBefore}${section}${between}${after}`;
|
|
731
|
-
}
|
|
732
|
-
|
|
733
|
-
const withTrailingNewline = source.endsWith('\n') || source.endsWith('\r') ? source : `${source}${newline}`;
|
|
734
|
-
return `${withTrailingNewline}${newline}${section}${newline}`;
|
|
735
|
-
}
|
|
736
|
-
|
|
737
|
-
export function setMcpServerDisabledInToml(text, serverName, disabled) {
|
|
738
|
-
const source = String(text || '');
|
|
739
|
-
const sectionName = `mcp_servers.${serverName}`;
|
|
740
|
-
const range = findTomlSectionRange(source, sectionName);
|
|
741
|
-
|
|
742
|
-
if (!range) {
|
|
743
|
-
return source;
|
|
744
|
-
}
|
|
745
|
-
|
|
746
|
-
const sectionBlock = source.slice(range.start, range.end);
|
|
747
|
-
const newline = detectNewline(sectionBlock || '\n');
|
|
748
|
-
const disabledLine = `disabled = ${disabled ? 'true' : 'false'}`;
|
|
749
|
-
const updatedSection = /^\s*disabled\s*=.*$/m.test(sectionBlock)
|
|
750
|
-
? sectionBlock.replace(/^\s*disabled\s*=.*$/m, disabledLine)
|
|
751
|
-
: `${sectionBlock.trimEnd()}${newline}${disabledLine}${newline}`;
|
|
752
|
-
|
|
753
|
-
return `${source.slice(0, range.start)}${updatedSection}${source.slice(range.end)}`;
|
|
754
|
-
}
|
|
1
|
+
function detectNewline(text) {
|
|
2
|
+
return text.includes('\r\n') ? '\r\n' : '\n';
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
function detectIndentUnit(text) {
|
|
6
|
+
const lines = text.split(/\r?\n/);
|
|
7
|
+
for (const line of lines) {
|
|
8
|
+
const match = line.match(/^(\s+)"/);
|
|
9
|
+
if (match) return match[1];
|
|
10
|
+
}
|
|
11
|
+
return ' ';
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function getLineIndent(text, index) {
|
|
15
|
+
const lineStart = Math.max(text.lastIndexOf('\n', index - 1), text.lastIndexOf('\r', index - 1));
|
|
16
|
+
const start = lineStart === -1 ? 0 : lineStart + 1;
|
|
17
|
+
let i = start;
|
|
18
|
+
while (i < text.length && (text[i] === ' ' || text[i] === '\t')) i += 1;
|
|
19
|
+
return text.slice(start, i);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function stripJsonComments(text) {
|
|
23
|
+
let out = '';
|
|
24
|
+
let inString = false;
|
|
25
|
+
let escape = false;
|
|
26
|
+
let inLine = false;
|
|
27
|
+
let inBlock = false;
|
|
28
|
+
|
|
29
|
+
for (let i = 0; i < text.length; i += 1) {
|
|
30
|
+
const ch = text[i];
|
|
31
|
+
const next = text[i + 1];
|
|
32
|
+
|
|
33
|
+
if (inLine) {
|
|
34
|
+
if (ch === '\n') {
|
|
35
|
+
inLine = false;
|
|
36
|
+
out += ch;
|
|
37
|
+
}
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (inBlock) {
|
|
42
|
+
if (ch === '*' && next === '/') {
|
|
43
|
+
inBlock = false;
|
|
44
|
+
i += 1;
|
|
45
|
+
}
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (inString) {
|
|
50
|
+
out += ch;
|
|
51
|
+
if (escape) {
|
|
52
|
+
escape = false;
|
|
53
|
+
} else if (ch === '\\') {
|
|
54
|
+
escape = true;
|
|
55
|
+
} else if (ch === '"') {
|
|
56
|
+
inString = false;
|
|
57
|
+
}
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (ch === '/' && next === '/') {
|
|
62
|
+
inLine = true;
|
|
63
|
+
i += 1;
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
if (ch === '/' && next === '*') {
|
|
67
|
+
inBlock = true;
|
|
68
|
+
i += 1;
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (ch === '"') {
|
|
73
|
+
inString = true;
|
|
74
|
+
}
|
|
75
|
+
out += ch;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return out;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function stripTrailingCommas(text) {
|
|
82
|
+
let out = '';
|
|
83
|
+
let inString = false;
|
|
84
|
+
let escape = false;
|
|
85
|
+
|
|
86
|
+
for (let i = 0; i < text.length; i += 1) {
|
|
87
|
+
const ch = text[i];
|
|
88
|
+
|
|
89
|
+
if (inString) {
|
|
90
|
+
out += ch;
|
|
91
|
+
if (escape) {
|
|
92
|
+
escape = false;
|
|
93
|
+
} else if (ch === '\\') {
|
|
94
|
+
escape = true;
|
|
95
|
+
} else if (ch === '"') {
|
|
96
|
+
inString = false;
|
|
97
|
+
}
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (ch === '"') {
|
|
102
|
+
inString = true;
|
|
103
|
+
out += ch;
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (ch === ',') {
|
|
108
|
+
let j = i + 1;
|
|
109
|
+
while (j < text.length && /\s/.test(text[j])) j += 1;
|
|
110
|
+
if (text[j] === '}' || text[j] === ']') {
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
out += ch;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return out;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function parseJsonc(text) {
|
|
122
|
+
const cleaned = stripTrailingCommas(stripJsonComments(text));
|
|
123
|
+
try {
|
|
124
|
+
return JSON.parse(cleaned);
|
|
125
|
+
} catch {
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function skipWhitespaceAndComments(text, start, end) {
|
|
131
|
+
let i = start;
|
|
132
|
+
while (i < end) {
|
|
133
|
+
const ch = text[i];
|
|
134
|
+
const next = text[i + 1];
|
|
135
|
+
if (ch === ' ' || ch === '\t' || ch === '\r' || ch === '\n') {
|
|
136
|
+
i += 1;
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
if (ch === '/' && next === '/') {
|
|
140
|
+
i += 2;
|
|
141
|
+
while (i < end && text[i] !== '\n') i += 1;
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
if (ch === '/' && next === '*') {
|
|
145
|
+
i += 2;
|
|
146
|
+
while (i < end - 1 && !(text[i] === '*' && text[i + 1] === '/')) i += 1;
|
|
147
|
+
i += 2;
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
break;
|
|
151
|
+
}
|
|
152
|
+
return i;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function readString(text, start, end) {
|
|
156
|
+
let i = start + 1;
|
|
157
|
+
let value = '';
|
|
158
|
+
let escape = false;
|
|
159
|
+
while (i < end) {
|
|
160
|
+
const ch = text[i];
|
|
161
|
+
if (escape) {
|
|
162
|
+
value += ch;
|
|
163
|
+
escape = false;
|
|
164
|
+
} else if (ch === '\\') {
|
|
165
|
+
value += ch;
|
|
166
|
+
escape = true;
|
|
167
|
+
} else if (ch === '"') {
|
|
168
|
+
return { value, end: i + 1 };
|
|
169
|
+
} else {
|
|
170
|
+
value += ch;
|
|
171
|
+
}
|
|
172
|
+
i += 1;
|
|
173
|
+
}
|
|
174
|
+
return { value, end: i };
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function scanComposite(text, start, end, openChar, closeChar) {
|
|
178
|
+
let depth = 0;
|
|
179
|
+
let inString = false;
|
|
180
|
+
let escape = false;
|
|
181
|
+
let inLine = false;
|
|
182
|
+
let inBlock = false;
|
|
183
|
+
|
|
184
|
+
for (let i = start; i < end; i += 1) {
|
|
185
|
+
const ch = text[i];
|
|
186
|
+
const next = text[i + 1];
|
|
187
|
+
|
|
188
|
+
if (inLine) {
|
|
189
|
+
if (ch === '\n') inLine = false;
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
if (inBlock) {
|
|
193
|
+
if (ch === '*' && next === '/') {
|
|
194
|
+
inBlock = false;
|
|
195
|
+
i += 1;
|
|
196
|
+
}
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
if (inString) {
|
|
200
|
+
if (escape) {
|
|
201
|
+
escape = false;
|
|
202
|
+
} else if (ch === '\\') {
|
|
203
|
+
escape = true;
|
|
204
|
+
} else if (ch === '"') {
|
|
205
|
+
inString = false;
|
|
206
|
+
}
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (ch === '/' && next === '/') {
|
|
211
|
+
inLine = true;
|
|
212
|
+
i += 1;
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
if (ch === '/' && next === '*') {
|
|
216
|
+
inBlock = true;
|
|
217
|
+
i += 1;
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (ch === '"') {
|
|
222
|
+
inString = true;
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (ch === openChar) {
|
|
227
|
+
depth += 1;
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
if (ch === closeChar) {
|
|
231
|
+
depth -= 1;
|
|
232
|
+
if (depth === 0) return i + 1;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
return end;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function scanValue(text, start, end) {
|
|
239
|
+
let i = skipWhitespaceAndComments(text, start, end);
|
|
240
|
+
const valueStart = i;
|
|
241
|
+
if (i >= end) {
|
|
242
|
+
return { valueStart: end, valueEnd: end, valueEndWithComma: end };
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const ch = text[i];
|
|
246
|
+
let valueEnd = i;
|
|
247
|
+
|
|
248
|
+
if (ch === '{') {
|
|
249
|
+
valueEnd = scanComposite(text, i, end, '{', '}');
|
|
250
|
+
} else if (ch === '[') {
|
|
251
|
+
valueEnd = scanComposite(text, i, end, '[', ']');
|
|
252
|
+
} else if (ch === '"') {
|
|
253
|
+
const result = readString(text, i, end);
|
|
254
|
+
valueEnd = result.end;
|
|
255
|
+
} else {
|
|
256
|
+
let inString = false;
|
|
257
|
+
let escape = false;
|
|
258
|
+
let inLine = false;
|
|
259
|
+
let inBlock = false;
|
|
260
|
+
for (; i < end; i += 1) {
|
|
261
|
+
const c = text[i];
|
|
262
|
+
const next = text[i + 1];
|
|
263
|
+
if (inLine) {
|
|
264
|
+
if (c === '\n') inLine = false;
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
267
|
+
if (inBlock) {
|
|
268
|
+
if (c === '*' && next === '/') {
|
|
269
|
+
inBlock = false;
|
|
270
|
+
i += 1;
|
|
271
|
+
}
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
274
|
+
if (inString) {
|
|
275
|
+
if (escape) {
|
|
276
|
+
escape = false;
|
|
277
|
+
} else if (c === '\\') {
|
|
278
|
+
escape = true;
|
|
279
|
+
} else if (c === '"') {
|
|
280
|
+
inString = false;
|
|
281
|
+
}
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
284
|
+
if (c === '/' && next === '/') {
|
|
285
|
+
inLine = true;
|
|
286
|
+
i += 1;
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
289
|
+
if (c === '/' && next === '*') {
|
|
290
|
+
inBlock = true;
|
|
291
|
+
i += 1;
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
294
|
+
if (c === '"') {
|
|
295
|
+
inString = true;
|
|
296
|
+
continue;
|
|
297
|
+
}
|
|
298
|
+
if (c === ',' || c === '}' || c === ']') {
|
|
299
|
+
break;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
valueEnd = i;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
let valueEndWithComma = valueEnd;
|
|
306
|
+
let j = skipWhitespaceAndComments(text, valueEnd, end);
|
|
307
|
+
if (text[j] === ',') {
|
|
308
|
+
valueEndWithComma = j + 1;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
return { valueStart, valueEnd, valueEndWithComma };
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function findRootObjectRange(text) {
|
|
315
|
+
let inString = false;
|
|
316
|
+
let escape = false;
|
|
317
|
+
let inLine = false;
|
|
318
|
+
let inBlock = false;
|
|
319
|
+
|
|
320
|
+
for (let i = 0; i < text.length; i += 1) {
|
|
321
|
+
const ch = text[i];
|
|
322
|
+
const next = text[i + 1];
|
|
323
|
+
|
|
324
|
+
if (inLine) {
|
|
325
|
+
if (ch === '\n') inLine = false;
|
|
326
|
+
continue;
|
|
327
|
+
}
|
|
328
|
+
if (inBlock) {
|
|
329
|
+
if (ch === '*' && next === '/') {
|
|
330
|
+
inBlock = false;
|
|
331
|
+
i += 1;
|
|
332
|
+
}
|
|
333
|
+
continue;
|
|
334
|
+
}
|
|
335
|
+
if (inString) {
|
|
336
|
+
if (escape) {
|
|
337
|
+
escape = false;
|
|
338
|
+
} else if (ch === '\\') {
|
|
339
|
+
escape = true;
|
|
340
|
+
} else if (ch === '"') {
|
|
341
|
+
inString = false;
|
|
342
|
+
}
|
|
343
|
+
continue;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
if (ch === '/' && next === '/') {
|
|
347
|
+
inLine = true;
|
|
348
|
+
i += 1;
|
|
349
|
+
continue;
|
|
350
|
+
}
|
|
351
|
+
if (ch === '/' && next === '*') {
|
|
352
|
+
inBlock = true;
|
|
353
|
+
i += 1;
|
|
354
|
+
continue;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
if (ch === '"') {
|
|
358
|
+
inString = true;
|
|
359
|
+
continue;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
if (ch === '{') {
|
|
363
|
+
const end = scanComposite(text, i, text.length, '{', '}');
|
|
364
|
+
if (end > i) return { start: i, end };
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
return null;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function findPropertyValueRange(text, objRange, key) {
|
|
372
|
+
let i = objRange.start + 1;
|
|
373
|
+
const end = objRange.end - 1;
|
|
374
|
+
let expectKey = true;
|
|
375
|
+
|
|
376
|
+
while (i < end) {
|
|
377
|
+
i = skipWhitespaceAndComments(text, i, end);
|
|
378
|
+
if (i >= end) break;
|
|
379
|
+
const ch = text[i];
|
|
380
|
+
if (ch === '}') break;
|
|
381
|
+
if (ch === ',') {
|
|
382
|
+
expectKey = true;
|
|
383
|
+
i += 1;
|
|
384
|
+
continue;
|
|
385
|
+
}
|
|
386
|
+
if (!expectKey) {
|
|
387
|
+
const valueInfo = scanValue(text, i, end);
|
|
388
|
+
i = valueInfo.valueEndWithComma;
|
|
389
|
+
expectKey = true;
|
|
390
|
+
continue;
|
|
391
|
+
}
|
|
392
|
+
if (ch !== '"') {
|
|
393
|
+
i += 1;
|
|
394
|
+
continue;
|
|
395
|
+
}
|
|
396
|
+
const keyResult = readString(text, i, end);
|
|
397
|
+
const keyName = keyResult.value;
|
|
398
|
+
const keyStart = i;
|
|
399
|
+
const keyEnd = keyResult.end;
|
|
400
|
+
let afterKey = skipWhitespaceAndComments(text, keyEnd, end);
|
|
401
|
+
if (text[afterKey] !== ':') {
|
|
402
|
+
i = keyEnd;
|
|
403
|
+
continue;
|
|
404
|
+
}
|
|
405
|
+
const valueInfo = scanValue(text, afterKey + 1, end);
|
|
406
|
+
if (keyName === key) {
|
|
407
|
+
return {
|
|
408
|
+
keyStart,
|
|
409
|
+
keyEnd,
|
|
410
|
+
valueStart: valueInfo.valueStart,
|
|
411
|
+
valueEnd: valueInfo.valueEnd,
|
|
412
|
+
valueEndWithComma: valueInfo.valueEndWithComma,
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
i = valueInfo.valueEndWithComma;
|
|
416
|
+
expectKey = true;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
return null;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
function formatJsonValue(value, indentUnit, parentIndent, newline) {
|
|
423
|
+
const raw = JSON.stringify(value, null, indentUnit);
|
|
424
|
+
if (!raw.includes('\n')) return raw;
|
|
425
|
+
return raw.replace(/\n/g, `${newline}${parentIndent}`);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
function findObjectRangeFromValue(text, valueStart, valueEnd) {
|
|
429
|
+
const start = skipWhitespaceAndComments(text, valueStart, valueEnd);
|
|
430
|
+
if (text[start] !== '{') return null;
|
|
431
|
+
const end = scanComposite(text, start, valueEnd, '{', '}');
|
|
432
|
+
return { start, end };
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
function insertPropertyIntoObject(text, objRange, key, valueText, indentUnit, newline) {
|
|
436
|
+
const objectIndent = getLineIndent(text, objRange.start);
|
|
437
|
+
const propertyIndent = `${objectIndent}${indentUnit}`;
|
|
438
|
+
const entry = `${propertyIndent}"${key}": ${valueText}`;
|
|
439
|
+
|
|
440
|
+
const before = text.slice(0, objRange.start + 1);
|
|
441
|
+
const inside = text.slice(objRange.start + 1, objRange.end - 1);
|
|
442
|
+
const after = text.slice(objRange.end - 1);
|
|
443
|
+
|
|
444
|
+
const contentIndex = skipWhitespaceAndComments(text, objRange.start + 1, objRange.end - 1);
|
|
445
|
+
const hasEntries = contentIndex < objRange.end - 1;
|
|
446
|
+
if (!hasEntries) {
|
|
447
|
+
return `${before}${newline}${entry}${newline}${objectIndent}}${after.slice(1)}`;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
const insideEndIndex = objRange.start + 1 + inside.length;
|
|
451
|
+
let insertPoint = insideEndIndex;
|
|
452
|
+
let j = objRange.end - 2;
|
|
453
|
+
while (j > objRange.start && /\s/.test(text[j])) j -= 1;
|
|
454
|
+
const needsComma = text[j] !== ',';
|
|
455
|
+
const comma = needsComma ? ',' : '';
|
|
456
|
+
|
|
457
|
+
return (
|
|
458
|
+
text.slice(0, insertPoint) +
|
|
459
|
+
comma +
|
|
460
|
+
newline +
|
|
461
|
+
entry +
|
|
462
|
+
newline +
|
|
463
|
+
objectIndent +
|
|
464
|
+
text.slice(objRange.end - 1)
|
|
465
|
+
);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
function replaceRange(text, start, end, replacement) {
|
|
469
|
+
return text.slice(0, start) + replacement + text.slice(end);
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
function resolveContainer(text, rootRange, preferredContainerKey = 'mcpServers') {
|
|
473
|
+
const baseContainers = [
|
|
474
|
+
{ type: 'key', key: 'mcpServers' },
|
|
475
|
+
{ type: 'key', key: 'servers' },
|
|
476
|
+
{ type: 'key', key: 'cline.mcpServers' },
|
|
477
|
+
{ type: 'nested', key: 'cline', child: 'mcpServers' },
|
|
478
|
+
];
|
|
479
|
+
|
|
480
|
+
const preferredIndex = baseContainers.findIndex(
|
|
481
|
+
(candidate) => candidate.type === 'key' && candidate.key === preferredContainerKey
|
|
482
|
+
);
|
|
483
|
+
if (preferredIndex > 0) {
|
|
484
|
+
const [preferred] = baseContainers.splice(preferredIndex, 1);
|
|
485
|
+
baseContainers.unshift(preferred);
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
for (const candidate of baseContainers) {
|
|
489
|
+
if (candidate.type === 'key') {
|
|
490
|
+
const entry = findPropertyValueRange(text, rootRange, candidate.key);
|
|
491
|
+
if (entry) {
|
|
492
|
+
return { entry, containerKey: candidate.key };
|
|
493
|
+
}
|
|
494
|
+
continue;
|
|
495
|
+
}
|
|
496
|
+
if (candidate.type === 'nested') {
|
|
497
|
+
const parent = findPropertyValueRange(text, rootRange, candidate.key);
|
|
498
|
+
if (!parent) continue;
|
|
499
|
+
const parentObj = findObjectRangeFromValue(text, parent.valueStart, parent.valueEnd);
|
|
500
|
+
if (!parentObj) {
|
|
501
|
+
return {
|
|
502
|
+
entry: parent,
|
|
503
|
+
containerKey: candidate.key,
|
|
504
|
+
needsObjectReplace: true,
|
|
505
|
+
childKey: candidate.child,
|
|
506
|
+
};
|
|
507
|
+
}
|
|
508
|
+
const child = findPropertyValueRange(text, parentObj, candidate.child);
|
|
509
|
+
if (child) {
|
|
510
|
+
return {
|
|
511
|
+
entry: child,
|
|
512
|
+
containerKey: candidate.child,
|
|
513
|
+
parentRange: parentObj,
|
|
514
|
+
parentEntry: parent,
|
|
515
|
+
};
|
|
516
|
+
}
|
|
517
|
+
return {
|
|
518
|
+
entry: null,
|
|
519
|
+
containerKey: candidate.child,
|
|
520
|
+
parentRange: parentObj,
|
|
521
|
+
parentEntry: parent,
|
|
522
|
+
};
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
return null;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
export function upsertMcpServerEntryInText(
|
|
530
|
+
text,
|
|
531
|
+
serverName,
|
|
532
|
+
serverConfig,
|
|
533
|
+
preferredContainerKey = 'mcpServers'
|
|
534
|
+
) {
|
|
535
|
+
const newline = detectNewline(text);
|
|
536
|
+
const indentUnit = detectIndentUnit(text);
|
|
537
|
+
const trimmed = text.trim();
|
|
538
|
+
|
|
539
|
+
if (!trimmed) {
|
|
540
|
+
const payload = {
|
|
541
|
+
[preferredContainerKey]: {
|
|
542
|
+
[serverName]: serverConfig,
|
|
543
|
+
},
|
|
544
|
+
};
|
|
545
|
+
return JSON.stringify(payload, null, indentUnit) + newline;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
const rootRange = findRootObjectRange(text);
|
|
549
|
+
if (!rootRange) {
|
|
550
|
+
return null;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
const container = resolveContainer(text, rootRange, preferredContainerKey);
|
|
554
|
+
|
|
555
|
+
if (!container) {
|
|
556
|
+
const objectIndent = getLineIndent(text, rootRange.start);
|
|
557
|
+
const propertyIndent = `${objectIndent}${indentUnit}`;
|
|
558
|
+
const valueText = formatJsonValue(
|
|
559
|
+
{ [serverName]: serverConfig },
|
|
560
|
+
indentUnit,
|
|
561
|
+
propertyIndent,
|
|
562
|
+
newline
|
|
563
|
+
);
|
|
564
|
+
return insertPropertyIntoObject(
|
|
565
|
+
text,
|
|
566
|
+
rootRange,
|
|
567
|
+
preferredContainerKey,
|
|
568
|
+
valueText,
|
|
569
|
+
indentUnit,
|
|
570
|
+
newline
|
|
571
|
+
);
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
if (container.needsObjectReplace) {
|
|
575
|
+
const replacementValue = container.childKey
|
|
576
|
+
? { [container.childKey]: { [serverName]: serverConfig } }
|
|
577
|
+
: { [serverName]: serverConfig };
|
|
578
|
+
const parentIndent = getLineIndent(text, container.entry.keyStart);
|
|
579
|
+
const valueText = formatJsonValue(replacementValue, indentUnit, parentIndent, newline);
|
|
580
|
+
return replaceRange(text, container.entry.valueStart, container.entry.valueEnd, valueText);
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
if (container.parentRange && !container.entry) {
|
|
584
|
+
const propertyIndent = `${getLineIndent(text, container.parentRange.start)}${indentUnit}`;
|
|
585
|
+
const valueText = formatJsonValue(
|
|
586
|
+
{ [serverName]: serverConfig },
|
|
587
|
+
indentUnit,
|
|
588
|
+
propertyIndent,
|
|
589
|
+
newline
|
|
590
|
+
);
|
|
591
|
+
return insertPropertyIntoObject(
|
|
592
|
+
text,
|
|
593
|
+
container.parentRange,
|
|
594
|
+
container.containerKey,
|
|
595
|
+
valueText,
|
|
596
|
+
indentUnit,
|
|
597
|
+
newline
|
|
598
|
+
);
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
if (!container.entry) {
|
|
602
|
+
return null;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
const containerObject = findObjectRangeFromValue(
|
|
606
|
+
text,
|
|
607
|
+
container.entry.valueStart,
|
|
608
|
+
container.entry.valueEnd
|
|
609
|
+
);
|
|
610
|
+
if (!containerObject) {
|
|
611
|
+
const parentIndent = getLineIndent(text, container.entry.keyStart);
|
|
612
|
+
const valueText = formatJsonValue(
|
|
613
|
+
{ [serverName]: serverConfig },
|
|
614
|
+
indentUnit,
|
|
615
|
+
parentIndent,
|
|
616
|
+
newline
|
|
617
|
+
);
|
|
618
|
+
return replaceRange(text, container.entry.valueStart, container.entry.valueEnd, valueText);
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
const existingEntry = findPropertyValueRange(text, containerObject, serverName);
|
|
622
|
+
if (existingEntry) {
|
|
623
|
+
const entryIndent = getLineIndent(text, existingEntry.keyStart);
|
|
624
|
+
const valueText = formatJsonValue(serverConfig, indentUnit, entryIndent, newline);
|
|
625
|
+
return replaceRange(text, existingEntry.valueStart, existingEntry.valueEnd, valueText);
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
const objectIndent = getLineIndent(text, containerObject.start);
|
|
629
|
+
const propertyIndent = `${objectIndent}${indentUnit}`;
|
|
630
|
+
const valueText = formatJsonValue(serverConfig, indentUnit, propertyIndent, newline);
|
|
631
|
+
return insertPropertyIntoObject(
|
|
632
|
+
text,
|
|
633
|
+
containerObject,
|
|
634
|
+
serverName,
|
|
635
|
+
valueText,
|
|
636
|
+
indentUnit,
|
|
637
|
+
newline
|
|
638
|
+
);
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
export function findMcpServerEntry(config, serverName) {
|
|
642
|
+
if (!config || typeof config !== 'object') return null;
|
|
643
|
+
if (config.mcpServers && config.mcpServers[serverName]) {
|
|
644
|
+
return { containerKey: 'mcpServers', entry: config.mcpServers[serverName] };
|
|
645
|
+
}
|
|
646
|
+
if (config.servers && config.servers[serverName]) {
|
|
647
|
+
return { containerKey: 'servers', entry: config.servers[serverName] };
|
|
648
|
+
}
|
|
649
|
+
if (config['cline.mcpServers'] && config['cline.mcpServers'][serverName]) {
|
|
650
|
+
return {
|
|
651
|
+
containerKey: 'cline.mcpServers',
|
|
652
|
+
entry: config['cline.mcpServers'][serverName],
|
|
653
|
+
};
|
|
654
|
+
}
|
|
655
|
+
if (config.cline && config.cline.mcpServers && config.cline.mcpServers[serverName]) {
|
|
656
|
+
return {
|
|
657
|
+
containerKey: 'cline.mcpServers',
|
|
658
|
+
entry: config.cline.mcpServers[serverName],
|
|
659
|
+
};
|
|
660
|
+
}
|
|
661
|
+
return null;
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
function formatTomlString(value) {
|
|
665
|
+
return JSON.stringify(String(value));
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
function formatTomlArray(values) {
|
|
669
|
+
const list = Array.isArray(values) ? values : [];
|
|
670
|
+
return `[${list.map((value) => formatTomlString(value)).join(', ')}]`;
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
function formatTomlMcpSection(serverName, serverConfig, newline) {
|
|
674
|
+
const lines = [`[mcp_servers.${serverName}]`];
|
|
675
|
+
if (serverConfig.command !== undefined) {
|
|
676
|
+
lines.push(`command = ${formatTomlString(serverConfig.command)}`);
|
|
677
|
+
}
|
|
678
|
+
if (serverConfig.args !== undefined) {
|
|
679
|
+
lines.push(`args = ${formatTomlArray(serverConfig.args)}`);
|
|
680
|
+
}
|
|
681
|
+
if (serverConfig.disabled !== undefined) {
|
|
682
|
+
lines.push(`disabled = ${serverConfig.disabled ? 'true' : 'false'}`);
|
|
683
|
+
}
|
|
684
|
+
return lines.join(newline);
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
function findTomlSectionRange(source, sectionName) {
|
|
688
|
+
const headerRegex = /^\s*\[([^\]\r\n]+)\]\s*$/gm;
|
|
689
|
+
let start = -1;
|
|
690
|
+
let end = source.length;
|
|
691
|
+
let match;
|
|
692
|
+
|
|
693
|
+
while ((match = headerRegex.exec(source)) !== null) {
|
|
694
|
+
const currentSection = String(match[1] || '').trim();
|
|
695
|
+
if (start === -1) {
|
|
696
|
+
if (currentSection === sectionName) {
|
|
697
|
+
start = match.index;
|
|
698
|
+
}
|
|
699
|
+
continue;
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
end = match.index;
|
|
703
|
+
break;
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
if (start === -1) {
|
|
707
|
+
return null;
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
return { start, end };
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
export function upsertMcpServerEntryInToml(text, serverName, serverConfig) {
|
|
714
|
+
const source = String(text || '');
|
|
715
|
+
const newline = detectNewline(source || '\n');
|
|
716
|
+
const section = formatTomlMcpSection(serverName, serverConfig, newline);
|
|
717
|
+
const sectionName = `mcp_servers.${serverName}`;
|
|
718
|
+
const range = findTomlSectionRange(source, sectionName);
|
|
719
|
+
|
|
720
|
+
if (!source.trim()) {
|
|
721
|
+
return `${section}${newline}`;
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
if (range) {
|
|
725
|
+
const before = source.slice(0, range.start);
|
|
726
|
+
const after = source.slice(range.end).replace(/^\s*\r?\n?/, '');
|
|
727
|
+
const normalizedBefore =
|
|
728
|
+
before.endsWith('\n') || before.endsWith('\r') || !before ? before : `${before}${newline}`;
|
|
729
|
+
const between = after ? newline : '';
|
|
730
|
+
return `${normalizedBefore}${section}${between}${after}`;
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
const withTrailingNewline = source.endsWith('\n') || source.endsWith('\r') ? source : `${source}${newline}`;
|
|
734
|
+
return `${withTrailingNewline}${newline}${section}${newline}`;
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
export function setMcpServerDisabledInToml(text, serverName, disabled) {
|
|
738
|
+
const source = String(text || '');
|
|
739
|
+
const sectionName = `mcp_servers.${serverName}`;
|
|
740
|
+
const range = findTomlSectionRange(source, sectionName);
|
|
741
|
+
|
|
742
|
+
if (!range) {
|
|
743
|
+
return source;
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
const sectionBlock = source.slice(range.start, range.end);
|
|
747
|
+
const newline = detectNewline(sectionBlock || '\n');
|
|
748
|
+
const disabledLine = `disabled = ${disabled ? 'true' : 'false'}`;
|
|
749
|
+
const updatedSection = /^\s*disabled\s*=.*$/m.test(sectionBlock)
|
|
750
|
+
? sectionBlock.replace(/^\s*disabled\s*=.*$/m, disabledLine)
|
|
751
|
+
: `${sectionBlock.trimEnd()}${newline}${disabledLine}${newline}`;
|
|
752
|
+
|
|
753
|
+
return `${source.slice(0, range.start)}${updatedSection}${source.slice(range.end)}`;
|
|
754
|
+
}
|